use oxc_index::IndexVec;
use oxc_str::CompactStr;
use rolldown_common::{
  Chunk, ChunkIdx, InstantiatedChunk, ModuleRenderOutput, NormalizedBundlerOptions, OutputExports,
  PathsOutputOption, SymbolRef,
};
use rolldown_error::{BuildDiagnostic, BuildResult};
use rolldown_plugin::SharedPluginDriver;
use rolldown_utils::{ecmascript::property_access_str, indexmap::FxIndexMap};
use rustc_hash::FxHashMap;

use crate::{chunk_graph::ChunkGraph, stages::link_stage::LinkStageOutput};

pub struct GenerateContext<'a> {
  pub chunk_idx: ChunkIdx,
  pub chunk: &'a Chunk,
  pub options: &'a NormalizedBundlerOptions,
  pub link_output: &'a LinkStageOutput,
  pub chunk_graph: &'a ChunkGraph,
  pub plugin_driver: &'a SharedPluginDriver,
  pub module_id_to_codegen_ret: Vec<Option<ModuleRenderOutput>>,
  /// The key of the map is exported item symbol,
  /// the value of the map is optional alias. e.g.
  /// - chunkB.js
  /// ```js
  /// export const a = 10000000;
  /// export {a as b}; // symbol_ref points to `a`, and alias is `b`
  /// ```
  pub render_export_items_index_vec: &'a IndexVec<ChunkIdx, FxIndexMap<SymbolRef, Vec<CompactStr>>>,
  /// Pre-resolved paths for external modules (always a `FxHashMap` variant).
  /// Used instead of `options.paths` in sync rendering code to avoid deadlocks.
  pub resolved_paths: Option<&'a PathsOutputOption>,
}

impl GenerateContext<'_> {
  /// A `SymbolRef` might be identifier or a property access. This function will return correct string pattern for the symbol.
  pub fn finalized_string_pattern_for_symbol_ref(
    &self,
    symbol_ref: SymbolRef,
    cur_chunk_idx: ChunkIdx,
    canonical_names: &FxHashMap<SymbolRef, CompactStr>,
  ) -> String {
    let symbol_db = &self.link_output.symbol_db;
    if !symbol_ref.is_declared_in_root_scope(symbol_db) {
      // No fancy things on none root scope symbols
      return self.canonical_name_for(canonical_names, symbol_ref).to_string();
    }

    let canonical_ref = symbol_db.canonical_ref_for(symbol_ref);
    let canonical_symbol = symbol_db.get(canonical_ref);
    let namespace_alias = &canonical_symbol.namespace_alias;
    if let Some(ns_alias) = namespace_alias {
      let canonical_ns_name =
        symbol_db.canonical_name_for_or_original(ns_alias.namespace_ref, canonical_names);
      let property_name = &ns_alias.property_name;
      return property_access_str(canonical_ns_name, property_name);
    }

    if self.link_output.module_table[canonical_ref.owner].is_external() {
      let namespace = symbol_db.canonical_name_for_or_original(canonical_ref, canonical_names);
      return namespace.to_string();
    }

    match self.options.format {
      rolldown_common::OutputFormat::Cjs => {
        let chunk_idx_of_canonical_symbol = canonical_symbol.chunk_idx.unwrap_or_else(|| {
          // Scoped symbols don't get assigned a `ChunkIdx`. There are skipped for performance reason, because they are surely
          // belong to the chunk they are declared in and won't link to other chunks.
          let symbol_name = canonical_ref.name(symbol_db);
          panic!("{canonical_ref:?} {symbol_name:?} is not in any chunk, which is unexpected");
        });

        let is_symbol_in_other_chunk = cur_chunk_idx != chunk_idx_of_canonical_symbol;
        if is_symbol_in_other_chunk {
          // In cjs output, we need convert the `import { foo } from 'foo'; console.log(foo);`;
          // If `foo` is split into another chunk, we need to convert the code `console.log(foo);` to `console.log(require_xxxx.foo);`
          // instead of keeping `console.log(foo)` as we did in esm output. The reason here is we need to keep live binding in cjs output.

          let require_binding = &self.chunk_graph.chunk_table[cur_chunk_idx]
            .require_binding_names_for_other_chunks[&chunk_idx_of_canonical_symbol];

          let exporter_chunk = &self.chunk_graph.chunk_table[chunk_idx_of_canonical_symbol];
          let exported_name = &exporter_chunk.exports_to_other_chunks[&canonical_ref][0];
          if exported_name == "default" {
            // Use the exporter chunk's actual `output_exports`, not the user-level
            // `options.exports`. Each chunk decides its own export mode (e.g. under
            // `preserveModules`, non-input modules are forced to `Named` and emit
            // `exports.default = ...`), so the access pattern must match what the
            // exporter actually renders.
            match exporter_chunk.output_exports {
              OutputExports::Default => require_binding.clone(),
              OutputExports::Named => {
                rolldown_utils::ecmascript::property_access_str(require_binding, exported_name)
              }
              // `Auto` is always resolved away by `determine_export_mode`. `None`
              // is unreachable here because we just indexed
              // `exports_to_other_chunks[canonical_ref]` successfully — a non-empty
              // entry means the chunk has at least one cross-chunk export, which
              // forces `output_exports` to `Default` or `Named` (entry chunks via
              // `determine_export_mode`; common chunks are unconditionally `Named`).
              OutputExports::Auto | OutputExports::None => unreachable!(),
            }
          } else {
            rolldown_utils::ecmascript::property_access_str(require_binding, exported_name)
          }
        } else {
          self.canonical_name_for(canonical_names, canonical_ref).to_string()
        }
      }
      _ => self.canonical_name_for(canonical_names, canonical_ref).to_string(),
    }
  }

  fn canonical_name_for<'name>(
    &'name self,
    canonical_names: &'name FxHashMap<SymbolRef, CompactStr>,
    symbol: SymbolRef,
  ) -> &'name str {
    let symbol_db = &self.link_output.symbol_db;
    symbol_db.canonical_name_for_or_original(symbol, canonical_names)
  }
}

#[derive(Default)]
pub struct GenerateOutput {
  pub chunks: Vec<InstantiatedChunk>,
  pub warnings: Vec<BuildDiagnostic>,
}

pub trait Generator {
  async fn instantiate_chunk(
    ctx: &mut GenerateContext,
  ) -> anyhow::Result<BuildResult<GenerateOutput>>;
}
