use oxc::ast::ast::Str;
use oxc::{
  allocator::{Box as ArenaBox, IntoIn, TakeIn},
  ast::{
    NONE,
    ast::{self, ExportDefaultDeclarationKind, Expression, ObjectPropertyKind, Statement},
  },
  semantic::{IsGlobalReference, Scoping, SymbolId},
  span::{SPAN, Span},
};

use rolldown_common::{
  ExternalModule, ImportRecordIdx, ImportRecordMeta, IndexModules, Module, ModuleIdx, NormalModule,
};
use rolldown_ecmascript::CJS_REQUIRE_REF_STR;
use rolldown_ecmascript_utils::{AstFactory, ExpressionExt};
use rolldown_utils::{
  ecmascript::is_validate_identifier_name,
  indexmap::{FxIndexMap, FxIndexSet},
};
use rustc_hash::{FxHashMap, FxHashSet};

use crate::hmr::utils::{HmrAstBuilder, MODULE_EXPORTS_NAME_FOR_ESM};

pub struct HmrAstFinalizer<'me, 'ast> {
  // Outside input
  pub ast_factory: AstFactory<'ast>,
  pub modules: &'me IndexModules,
  pub module: &'me NormalModule,
  pub affected_module_idx_to_init_fn_name: &'me FxHashMap<ModuleIdx, String>,
  pub use_pife_for_module_wrappers: bool,

  /// Whether the runtime should short-circuit re-execution of the wrapped module body
  /// when its stable id is already registered.
  ///
  /// `true` for lazy-compilation chunks: when a module appears in two lazy bundles served
  /// concurrently, we want only the first one to actually execute the body.
  ///
  /// `false` for HMR patches: the patch's whole point is to re-execute the body and
  /// replace the registered exports, so deduping would silently drop the update.
  ///
  /// Note: This works only as a **workaround**. In the future, HMR runtime should
  /// provide a runtime API to trigger module disposal and re-execution. @hana
  pub dedup_module_initializer: bool,

  // Each module has a unique index, which is used to generate something that needs to be unique.
  pub unique_index: usize,

  // --- Internal state
  /// For
  ///
  /// ```js
  /// import foo, { bar } from './foo.js';
  /// ```
  ///
  /// , assuming `foo` and `bar` have symbol id `1` and `2`, we will store mapping like:
  ///
  /// - 1 => "import_foo_1.default"
  /// - 2 => "import_foo_1.bar"
  ///
  /// `import_foo_1` is the binding name we gonna used to generate code like
  ///
  /// ```js
  /// const import_foo_1 = __rolldown_runtime__.loadExports('./foo.js');
  /// ```
  pub import_bindings: FxHashMap<SymbolId, String>,
  pub exports: oxc::allocator::Vec<'ast, ObjectPropertyKind<'ast>>,
  pub re_export_all_dependencies: FxIndexSet<ModuleIdx>,
  pub dependencies: FxIndexSet<ModuleIdx>,
  pub imports: FxHashSet<ModuleIdx>,
  pub generated_static_import_infos: FxHashMap<ModuleIdx, String>,
  // We need to store the static import statements for external separately, so we could put them outside of the `try` block.
  pub generated_static_import_stmts_from_external: FxIndexMap<ModuleIdx, ast::Statement<'ast>>,
  pub named_exports: FxHashMap<Str<'ast>, NamedExport>,
}

impl<'ast> HmrAstFinalizer<'_, 'ast> {
  #[expect(clippy::too_many_lines)]
  pub fn handle_top_level_stmt(
    &mut self,
    program_body: &mut oxc::allocator::Vec<'ast, ast::Statement<'ast>>,
    mut node: ast::Statement<'ast>,
    scoping: &Scoping,
  ) {
    match node {
      ref mut module_decl @ ast::match_module_declaration!(Statement) => {
        let module_decl = module_decl.to_module_declaration_mut();

        match module_decl {
          ast::ModuleDeclaration::ImportDeclaration(import_decl) => {
            // Transform
            // ```js
            // import foo, { bar } from './foo.js';
            // console.log(foo, bar);
            // ```
            // to
            // ```js
            // const import_foo = __rolldown_runtime__.loadExports('./foo.js');
            // console.log(import_foo.default, import_foo.bar);
            // ```
            let rec_id = self.module.imports[&import_decl.node_id()];
            let rec = &self.module.import_records[rec_id];
            let Some(importee_idx) = rec.resolved_module else { return };
            let importee = &self.modules[importee_idx];
            self.dependencies.insert(importee_idx);
            let binding_name = self.ensure_static_import_info(importee_idx, rec_id).to_string();
            import_decl.specifiers.as_ref().inspect(|specifiers| {
              specifiers.iter().for_each(|spec| match spec {
                ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
                  self.import_bindings.insert(
                    import_specifier.local.symbol_id(),
                    format!("{binding_name}.{}", import_specifier.imported.name()),
                  );
                }
                ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
                  import_default_specifier,
                ) => {
                  self.import_bindings.insert(
                    import_default_specifier.local.symbol_id(),
                    format!("{binding_name}.default"),
                  );
                }
                ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
                  import_namespace_specifier,
                ) => {
                  self
                    .import_bindings
                    .insert(import_namespace_specifier.local.symbol_id(), binding_name.clone());
                }
              });
            });
            match importee {
              Module::Normal(_) => {
                if let Some(stmt) =
                  self.create_load_exports_call_stmt(importee, &binding_name, import_decl.span)
                {
                  program_body.push(stmt);
                }
              }
              Module::External(importee_ext) => {
                self.create_static_import_stmt_from_external_module(
                  importee_ext,
                  &binding_name,
                  import_decl.span,
                );
              }
            }
          }
          ast::ModuleDeclaration::ExportNamedDeclaration(decl) => {
            if let Some(_source) = &decl.source {
              // export {} from '...'
              let rec_id = self.module.imports[&decl.node_id()];
              let rec = &self.module.import_records[rec_id];
              let Some(importee_idx) = rec.resolved_module else { return };
              let importee = &self.modules[importee_idx];
              self.dependencies.insert(importee_idx);
              let binding_name = self.ensure_static_import_info(importee_idx, rec_id).to_string();
              self.exports.extend(decl.specifiers.iter().map(|specifier| {
                        self.ast_factory.make_lazy_export_property(
                          &specifier.exported.name(),
                          match &specifier.local {
                            ast::ModuleExportName::IdentifierName(ident) => {
                              Expression::StaticMemberExpression(
                                self.ast_factory.alloc_static_member_expression(
                                  SPAN,
                                  self.ast_factory.make_id_ref_expr(SPAN, &binding_name),
                                  self.ast_factory.identifier_name(SPAN, ident.name.as_str()),
                                  false,
                                ),
                              )
                            }
                            ast::ModuleExportName::StringLiteral(str) => {
                              Expression::ComputedMemberExpression(
                                self.ast_factory.alloc_computed_member_expression(
                                  SPAN,
                                  self.ast_factory.make_id_ref_expr(SPAN, &binding_name),
                                  self.ast_factory.expression_string_literal(
                                    SPAN, str.value.as_str(), None
                                  ),
                                  false,
                                ),
                              )
                            }
                            ast::ModuleExportName::IdentifierReference(_) => {
                              unreachable!(
                                "ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
                              )
                            }
                          },
                          matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
                        )
                      }));
              if let Some(stmt) =
                self.create_load_exports_call_stmt(importee, &binding_name, decl.span)
              {
                program_body.push(stmt);
              }
            } else if let Some(decl) = &mut decl.declaration {
              match decl {
                ast::Declaration::VariableDeclaration(var_decl) => {
                  // export var foo = 1
                  // export var { foo, bar } = { foo: 1, bar: 2 }
                  self.exports.extend(var_decl.declarations.iter().flat_map(|decl| {
                    decl.id.get_binding_identifiers().into_iter().map(|ident| {
                      self.ast_factory.make_lazy_export_property(
                        ident.name.as_str(),
                        self.ast_factory.make_id_ref_expr(SPAN, ident.name.as_str()),
                        false,
                      )
                    })
                  }));
                }
                ast::Declaration::FunctionDeclaration(fn_decl) => {
                  // export function foo() {}
                  let id = fn_decl.id.as_ref().unwrap().name.as_str();
                  self.exports.push(self.ast_factory.make_lazy_export_property(
                    id,
                    self.ast_factory.make_id_ref_expr(SPAN, id),
                    false,
                  ));
                }
                ast::Declaration::ClassDeclaration(cls_decl) => {
                  // export class Foo {}
                  let id = cls_decl.id.as_ref().unwrap().name.as_str();
                  self.exports.push(self.ast_factory.make_lazy_export_property(
                    id,
                    self.ast_factory.make_id_ref_expr(SPAN, id),
                    false,
                  ));
                }
                _ => unreachable!("doesn't support ts now"),
              }
              program_body.push(ast::Statement::from(decl.take_in(self.ast_factory.allocator)));
            } else {
              // export { foo, bar as bar2 }
              decl.specifiers.iter().for_each(|specifier| {
                if let Some(symbol_id) = scoping.get_root_binding(specifier.local.name().into()) {
                  self
                    .named_exports
                    .insert(specifier.exported.name(), NamedExport { local_binding: symbol_id });
                } else {
                  // TODO: export undefined variable
                }
              });
            }
          }
          ast::ModuleDeclaration::ExportDefaultDeclaration(decl) => match &mut decl.declaration {
            ast::ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
              if let Some(id) = &function.id {
                self.exports.push(self.ast_factory.make_lazy_export_property(
                  "default",
                  self.ast_factory.make_id_ref_expr(SPAN, &id.name),
                  false,
                ));
              } else {
                function.id = Some(self.ast_factory.make_id(SPAN, "__rolldown_default__"));
                self.exports.push(self.ast_factory.make_lazy_export_property(
                  "default",
                  self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_default__"),
                  false,
                ));
              }
              program_body.push(ast::Statement::FunctionDeclaration(ArenaBox::new_in(
                function.as_mut().take_in(self.ast_factory.allocator),
                self.ast_factory.allocator,
              )));
            }
            ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
              if let Some(id) = &class.id {
                self.exports.push(self.ast_factory.make_lazy_export_property(
                  "default",
                  self.ast_factory.make_id_ref_expr(SPAN, &id.name),
                  false,
                ));
              } else {
                class.id = Some(self.ast_factory.make_id(SPAN, "__rolldown_default__"));
                self.exports.push(self.ast_factory.make_lazy_export_property(
                  "default",
                  self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_default__"),
                  false,
                ));
              }
              program_body.push(ast::Statement::ClassDeclaration(ArenaBox::new_in(
                class.as_mut().take_in(self.ast_factory.allocator),
                self.ast_factory.allocator,
              )));
            }
            expr @ ast::match_expression!(ExportDefaultDeclarationKind) => {
              let expr = expr.to_expression_mut();
              // Transform `export default [expression]` => `var __rolldown_default__ = [expression]`
              program_body.push(
                self
                  .ast_factory
                  .make_var_decl("__rolldown_default__", expr.take_in(self.ast_factory.allocator)),
              );
              self.exports.push(self.ast_factory.make_lazy_export_property(
                "default",
                self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_default__"),
                false,
              ));
            }
            unhandled_kind => {
              unreachable!("Unexpected export default declaration kind: {unhandled_kind:#?}");
            }
          },
          ast::ModuleDeclaration::ExportAllDeclaration(export_all_decl) => {
            let rec_id = self.module.imports[&export_all_decl.node_id()];
            let rec = &self.module.import_records[rec_id];
            let Some(importee_idx) = rec.resolved_module else { return };
            let importee = &self.modules[importee_idx];
            self.dependencies.insert(importee_idx);
            let binding_name = self.ensure_static_import_info(importee_idx, rec_id).to_string();
            if let Some(stmt) =
              self.create_load_exports_call_stmt(importee, &binding_name, export_all_decl.span)
            {
              program_body.push(stmt);
            }
            if let Some(stmt) =
              self.create_re_export_call_stmt(importee, &binding_name, export_all_decl.span)
            {
              program_body.push(stmt);
            }
          }
          ast::ModuleDeclaration::TSExportAssignment(_)
          | ast::ModuleDeclaration::TSNamespaceExportDeclaration(_) => {
            // Typescript code should be pre-processed by rolldown. If it doesn't, it means there's error. Instead of panic, we'll just keep
            // the original code.
            program_body.push(node);
          }
        }
      }
      _ => {
        program_body.push(node);
      }
    }
  }

  pub fn ensure_static_import_info(
    &mut self,
    importee_idx: ModuleIdx,
    rec_id: ImportRecordIdx,
  ) -> &str {
    self.generated_static_import_infos.entry(importee_idx).or_insert_with(|| {
      let importee = &self.modules[importee_idx];

      format!("import_{}_{}{}", importee.repr_name(), self.unique_index, rec_id.raw())
    })
  }

  pub fn module_exports_name(&self) -> &'static str {
    if self.module.exports_kind.is_commonjs() {
      "module.exports"
    } else {
      MODULE_EXPORTS_NAME_FOR_ESM
    }
  }

  pub fn generate_runtime_module_register_for_hmr(
    &mut self,
    scoping: &Scoping,
  ) -> Vec<ast::Statement<'ast>> {
    let mut ret = vec![];
    if self.module.exports_kind == rolldown_common::ExportsKind::Esm {
      let binding_name_for_namespace_object_ref = self.module_exports_name();

      ret.extend(self.generate_declaration_of_module_namespace_object(
        binding_name_for_namespace_object_ref,
        scoping,
      ));
    }
    ret.push(self.create_register_module_stmt());

    ret
  }

  pub fn rewrite_hot_accept_call_deps(&self, call_expr: &mut ast::CallExpression<'ast>) {
    // Check whether the callee is `import.meta.hot.accept`.
    if !call_expr.callee.is_import_meta_hot_accept() {
      return;
    }

    if call_expr.arguments.is_empty() {
      // `import.meta.hot.accept()`
      return;
    }

    match &mut call_expr.arguments[0] {
      ast::Argument::StringLiteral(string_literal) => {
        // `import.meta.hot.accept('./dep.js', ...)`
        let import_record = &self.module.import_records
          [self.module.hmr_info.module_request_to_import_record_idx[string_literal.value.as_str()]];
        let Some(module_idx) = import_record.resolved_module else { return };
        // Use stable module ID for consistent runtime lookup
        string_literal.value = self.ast_factory.str(self.modules[module_idx].stable_id());
      }
      ast::Argument::ArrayExpression(array_expression) => {
        // `import.meta.hot.accept(['./dep1.js', './dep2.js'], ...)`
        array_expression.elements.iter_mut().for_each(|element| {
          if let ast::ArrayExpressionElement::StringLiteral(string_literal) = element {
            let import_record =
              &self.module.import_records[self.module.hmr_info.module_request_to_import_record_idx
                [string_literal.value.as_str()]];
            let Some(module_idx) = import_record.resolved_module else { return };
            // Use stable module ID for consistent runtime lookup
            string_literal.value = self.ast_factory.str(self.modules[module_idx].stable_id());
          }
        });
      }
      _ => {}
    }
  }

  pub fn rewrite_import_meta_hot(&self, expr: &mut ast::Expression<'ast>) {
    if expr.is_import_meta_hot() {
      let hot_name = format!("hot_{}", self.module.repr_name);
      *expr = self.ast_factory.make_id_ref_expr(SPAN, &hot_name);
    }
  }

  fn create_load_exports_call_stmt(
    &mut self,
    importee: &Module,
    binding_name: &str,
    span: Span,
  ) -> Option<Statement<'ast>> {
    if self.imports.contains(&importee.idx()) {
      return None;
    }
    self.imports.insert(importee.idx());

    // Use stable module ID for consistent runtime lookup
    let id = importee.stable_id();
    let interop = match importee {
      Module::Normal(importee) => self.module.interop(importee),
      Module::External(_) => None,
    };
    let call_expr = self.ast_factory.make_call_with_arg(
      self.ast_factory.make_member_access_expr("__rolldown_runtime__", "loadExports"),
      self.ast_factory.expression_string_literal(SPAN, self.ast_factory.str(id), None),
      false,
    );

    // var [binding_name] = [__toESM-wrapped loadExports call];
    let stmt = Statement::from(
      self.ast_factory.declaration_variable(
        span,
        ast::VariableDeclarationKind::Var,
        self.ast_factory.vec1(
          self.ast_factory.variable_declarator(
            SPAN,
            ast::VariableDeclarationKind::Var,
            self
              .ast_factory
              .binding_pattern_binding_identifier(SPAN, self.ast_factory.str(binding_name)),
            NONE,
            Some(self.ast_factory.make_to_esm_call_with_interop(
              "__rolldown_runtime__.__toESM",
              call_expr,
              interop,
            )),
            false,
          ),
        ),
        false,
      ),
    );
    Some(stmt)
  }

  fn create_static_import_stmt_from_external_module(
    &mut self,
    importee: &ExternalModule,
    binding_name: &str,
    _span: Span,
  ) {
    if self.generated_static_import_stmts_from_external.contains_key(&importee.idx) {
      return;
    }

    let module_request = &importee.id;

    // import * as [binding_name] from 'external';
    let stmt = self.ast_factory.make_import_star_stmt(module_request, binding_name);

    self.generated_static_import_stmts_from_external.insert(importee.idx, stmt);
  }

  fn create_re_export_call_stmt(
    &mut self,
    importee: &Module,
    binding_name: &str,
    span: Span,
  ) -> Option<Statement<'ast>> {
    if self.re_export_all_dependencies.contains(&importee.idx()) {
      return None;
    }
    self.re_export_all_dependencies.insert(importee.idx());

    let self_exports = self.module_exports_name();

    let call_expr = self.ast_factory.expression_call(
      SPAN,
      self.ast_factory.make_member_access_expr("__rolldown_runtime__", "__reExport"),
      NONE,
      self.ast_factory.vec_from_iter([
        ast::Argument::from(self.ast_factory.make_id_ref_expr(SPAN, self_exports)),
        ast::Argument::from(self.ast_factory.make_id_ref_expr(SPAN, binding_name)),
      ]),
      false,
    );

    Some(ast::Statement::ExpressionStatement(
      self.ast_factory.alloc_expression_statement(span, call_expr),
    ))
  }

  fn generate_declaration_of_module_namespace_object(
    &mut self,
    binding_name_for_namespace_object_ref: &str,
    scoping: &Scoping,
  ) -> Vec<ast::Statement<'ast>> {
    // TODO reexport external module

    // construct `{ prop_name: () => returned, ... }`
    let mut arg_obj_expr = self
      .ast_factory
      .alloc_object_expression(SPAN, self.ast_factory.vec_with_capacity(self.exports.len()));
    arg_obj_expr.properties.extend(self.exports.drain(..));
    arg_obj_expr.properties.extend(self.named_exports.iter().map(|(exported, named_export)| {
      let expr = if let Some(local_binding) = self.import_bindings.get(&named_export.local_binding)
      {
        self.ast_factory.make_id_ref_expr(SPAN, local_binding)
      } else {
        let name = scoping.symbol_name(named_export.local_binding);
        self.ast_factory.make_id_ref_expr(SPAN, name)
      };
      // Use computed property syntax for non-identifier export names (e.g., 'rolldown:exports')
      let computed = !is_validate_identifier_name(exported.as_str());
      self.ast_factory.make_lazy_export_property(exported, expr, computed)
    }));

    // construct `__export(ns_name, { prop_name: () => returned, ... })`
    let export_call_expr = self.ast_factory.expression_call(
      SPAN,
      self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_runtime__.__exportAll"),
      NONE,
      self.ast_factory.vec_from_array([ast::Argument::ObjectExpression(
        arg_obj_expr.into_in(self.ast_factory.allocator),
      )]),
      false,
    );

    // construct `var [binding_name_for_namespace_object_ref] = __exportAll({ prop_name: () => returned, ... })`
    let decl_stmt =
      self.ast_factory.make_var_decl(binding_name_for_namespace_object_ref, export_call_expr);
    vec![decl_stmt]
  }

  // Rewrite `import(...)` to sensible form.
  pub fn try_rewrite_dynamic_import(&self, it: &mut ast::Expression<'ast>) {
    let ast::Expression::ImportExpression(import_expr) = it else {
      return;
    };

    let Some(rec_idx) = self.module.imports.get(&import_expr.node_id()) else {
      return;
    };

    let Some(importee_idx) = self.module.import_records[*rec_idx].resolved_module else {
      return;
    };

    let Module::Normal(importee) = &self.modules[importee_idx] else {
      // Not a normal module, skip
      return;
    };

    // Handle lazy proxy modules - rewrite to mirror the proxy module's runtime contract.
    //
    // In a regular full build, scope finalizer rewrites `import('./foo')` to point at the
    // proxy module's chunk URL. That chunk's content is `proxy-module-template.js`, which
    // exposes `'rolldown:exports'` at the top level so consumers can do
    // `.then(__unwrap_lazy_compilation_entry).then(m => m.X)`.
    //
    // In HMR partial bundles there's no separately bundled proxy chunk - the proxy module's
    // body gets wrapped inside a `createEsmInitializer` and its top-level `export` is lost.
    // To keep the same surface as the full build, we rewrite the dynamic import to:
    //
    //   import(`/@vite/lazy?id=...&clientId=...`)
    //     .then(() => __rolldown_runtime__.loadExports("<stable_proxy_id>"))
    //
    // After the partial bundle evaluates, the proxy module is registered under
    // `<stable_proxy_id>` with a `'rolldown:exports'` getter (set up by `__exportAll` inside
    // the init wrapper). Reading it back via `loadExports` yields the namespace object that
    // the existing `__unwrap_lazy_compilation_entry` chain expects.
    //
    // TODO: hyf0 should switch to a more robust way to identify lazy proxy modules
    if importee.id.contains("?rolldown-lazy=1") {
      // Build: encodeURIComponent(importee.id)
      let encode_call =
        ast::Expression::CallExpression(self.ast_factory.alloc_call_expression(
          SPAN,
          self.ast_factory.make_id_ref_expr(SPAN, "encodeURIComponent"),
          NONE,
          self.ast_factory.vec1(ast::Argument::StringLiteral(
            self.ast_factory.alloc_string_literal(SPAN, self.ast_factory.str(&importee.id), None),
          )),
          false,
        ));

      // Build template literal: `/@vite/lazy?id=${encodeURIComponent(importee.id)}&clientId=${__rolldown_runtime__.clientId}`
      let url_expr = {
        let quasis = self.ast_factory.vec_from_iter([
          self.ast_factory.template_element(
            SPAN,
            ast::TemplateElementValue {
              raw: self.ast_factory.str("/@vite/lazy?id="),
              cooked: None,
            },
            false,
          ),
          self.ast_factory.template_element(
            SPAN,
            ast::TemplateElementValue { raw: self.ast_factory.str("&clientId="), cooked: None },
            false,
          ),
          self.ast_factory.template_element(
            SPAN,
            ast::TemplateElementValue { raw: self.ast_factory.str(""), cooked: None },
            true,
          ),
        ]);
        let expressions = self.ast_factory.vec_from_iter([
          encode_call,
          self.ast_factory.make_member_access_expr("__rolldown_runtime__", "clientId"),
        ]);
        self.ast_factory.expression_template_literal(SPAN, quasis, expressions)
      };

      // Build: import(`/@vite/lazy?id=...&clientId=...`)
      let import_expr = self.ast_factory.expression_import(SPAN, url_expr, None, None);

      // Build: __rolldown_runtime__.loadExports("<stable_proxy_id>")
      let load_exports_call =
        ast::Expression::CallExpression(self.ast_factory.alloc_call_expression(
          SPAN,
          self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_runtime__.loadExports"),
          NONE,
          self.ast_factory.vec1(ast::Argument::StringLiteral(
            self.ast_factory.alloc_string_literal(
              SPAN,
              self.ast_factory.str(&importee.stable_id),
              None,
            ),
          )),
          false,
        ));

      // Build: () => __rolldown_runtime__.loadExports("<stable_proxy_id>")
      let arrow_fn = self.ast_factory.expression_arrow_function(
        SPAN,
        /* expression */ true,
        /* async */ false,
        NONE,
        self.ast_factory.formal_parameters(
          SPAN,
          ast::FormalParameterKind::ArrowFormalParameters,
          self.ast_factory.vec(),
          NONE,
        ),
        NONE,
        self.ast_factory.function_body(
          SPAN,
          self.ast_factory.vec(),
          self.ast_factory.vec1(ast::Statement::ExpressionStatement(
            self.ast_factory.alloc_expression_statement(SPAN, load_exports_call),
          )),
        ),
      );

      // Build: import(...).then(() => __rolldown_runtime__.loadExports("..."))
      let then_callee =
        Expression::StaticMemberExpression(self.ast_factory.alloc_static_member_expression(
          SPAN,
          import_expr,
          self.ast_factory.identifier_name(SPAN, "then"),
          false,
        ));

      *it = self.ast_factory.expression_call(
        SPAN,
        then_callee,
        NONE,
        self.ast_factory.vec1(ast::Argument::from(arrow_fn)),
        false,
      );
      return;
    }

    // FIXME: consider about CommonJS interop
    let is_importee_cjs = importee.exports_kind == rolldown_common::ExportsKind::CommonJs;

    // __rolldown_runtime__.loadExports('./foo.js')
    // Use stable module ID for consistent runtime lookup
    let mut load_exports_call_expr =
      ast::Expression::CallExpression(self.ast_factory.alloc_call_expression(
        SPAN,
        self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_runtime__.loadExports"),
        NONE,
        self.ast_factory.vec1(ast::Argument::StringLiteral(self.ast_factory.alloc_string_literal(
          SPAN,
          self.ast_factory.str(&importee.stable_id),
          None,
        ))),
        false,
      ));

    if is_importee_cjs {
      let is_node_cjs = importee.def_format.is_commonjs();

      let mut args = self.ast_factory.vec1(ast::Argument::from(load_exports_call_expr));
      if is_node_cjs {
        args.push(ast::Argument::from(self.ast_factory.expression_numeric_literal(
          SPAN,
          1.0,
          None,
          ast::NumberBase::Decimal,
        )));
      }

      // __rolldown_runtime__.__toDynamicImportESM(__rolldown_runtime__.loadExports('./foo.js'), node_mode)
      load_exports_call_expr = self.ast_factory.expression_call(
        SPAN,
        self.ast_factory.make_id_ref_expr(SPAN, "__rolldown_runtime__.__toDynamicImportESM"),
        NONE,
        args,
        false,
      );
    }

    if let Some(init_fn_name) = self.affected_module_idx_to_init_fn_name.get(&importee_idx) {
      // If the importee is in the propagation chain, we need to call the init function to re-execute the module.
      // Turn `import('./foo.js')` into `(init_foo(), Promise.resolve().then(() => __rolldown_runtime__.loadExports('./foo.js')))`

      // init_foo()
      let init_fn_call = self.ast_factory.alloc_call_expression(
        SPAN,
        self.ast_factory.make_id_ref_expr(SPAN, init_fn_name),
        NONE,
        self.ast_factory.vec(),
        false,
      );

      // Promise.resolve().then(() => __rolldown_runtime__.loadExports('./foo.js'))
      let promise_resolve_then_load_exports =
        self.ast_factory.make_promise_resolve_then(load_exports_call_expr);

      // (init_foo(), Promise.resolve().then(() => __rolldown_runtime__.loadExports('./foo.js')))
      let ret_expr =
        ast::Expression::SequenceExpression(self.ast_factory.alloc_sequence_expression(
          SPAN,
          self.ast_factory.vec_from_array([
            ast::Expression::CallExpression(init_fn_call),
            promise_resolve_then_load_exports,
          ]),
        ));
      *it = ret_expr;
    } else {
      // Turn `import('./foo.js')` into `Promise.resolve().then(() => __rolldown_runtime__.loadExports('./foo.js'))`

      // `Promise.resolve().then(() => __rolldown_runtime__.loadExports('./foo.js'))`
      *it = self.ast_factory.make_promise_resolve_then(load_exports_call_expr);
    }
  }

  pub fn try_rewrite_require(
    &self,
    it: &mut ast::Expression<'ast>,
    ctx: &oxc_traverse::TraverseCtx<'ast, ()>,
  ) {
    let scoping = ctx.scoping();

    // Rewrite standalone `require` to `__rolldown_runtime__.loadExports`
    if let Some(id_ref) = it.as_identifier()
      && id_ref.name == CJS_REQUIRE_REF_STR
      && id_ref.is_global_reference(scoping)
      && !ctx.parent().is_call_expression()
    {
      *it = self.ast_factory.make_member_access_expr("__rolldown_runtime__", "loadExports");
    }

    // Rewrite `require(...)` to `(require_xxx(), __rolldown_runtime__.loadExports())` or keep it as is for external module importee.
    let ast::Expression::CallExpression(call_expr) = it else {
      return;
    };

    if !call_expr
      .callee
      .as_identifier()
      .is_some_and(|id| id.name == CJS_REQUIRE_REF_STR && id.is_global_reference(scoping))
    {
      return;
    }

    let Some(rec_idx) = self.module.imports.get(&call_expr.node_id()) else {
      return;
    };

    let rec = &self.module.import_records[*rec_idx];
    let Some(importee_idx) = rec.resolved_module else {
      return;
    };

    let Module::Normal(importee) = &self.modules[importee_idx] else {
      // Not a normal module, skip
      return;
    };

    let is_importee_cjs = importee.exports_kind == rolldown_common::ExportsKind::CommonJs;

    // Use stable module ID for consistent runtime lookup
    let load_exports_call = self.ast_factory.make_call_with_arg(
      self.ast_factory.make_member_access_expr("__rolldown_runtime__", "loadExports"),
      self.ast_factory.expression_string_literal(
        SPAN,
        self.ast_factory.str(&importee.stable_id),
        None,
      ),
      false,
    );

    let load_exports_expr = if importee.meta.has_lazy_export() || is_importee_cjs {
      // Note that HMR finalizer is only able to see scanner-level exports_kind, this means that the result
      // from `determine_module_exports_kind` is not available here. So we have to use some heuristics to determine
      // whether the importee is CommonJS or has lazy export, and handle them in a special way.
      //
      // 1. For the case of `is_importee_cjs`,
      // the runtime will always have `module.exports`. This is determined in `determine_module_exports_kind`.
      //
      // 2. For the case of `has_lazy_export`,
      // here we're inside `try_rewrite_require`, which means the original code is `require(...)`.
      //
      // Modules that have lazy export is of these `ModuleType`: `Json`, `Text`, `Base64`, `Dataurl`.
      // These data type does not have `export`, `module.exports` or any export keyword at runtime,
      // so they're `ExportsKind::None` by default.
      //
      // For those lazy export modules, if `ImportKind` is `Require`, which is the case here,
      // and the importee has `ExportsKind::None`, then the importee's `WrapKind` is set to `WrapKind::Cjs`.
      // So here we know for sure that the importee is using `module.exports` at runtime.
      // So `loadExports(id)` returns the value directly.
      //
      // This is a way to mimic the same mechanism of `determine_module_exports_kind`.
      //
      // TODO(hana): we should think about a more robust way to track the consolidated export type of a module in the future.
      // Listing all the special cases like this is error-prone.
      load_exports_call
    } else if rec.meta.contains(ImportRecordMeta::JsonModule) {
      // Vite-mode JSON: ESM-wrapped at runtime, unwrap to the JSON value.
      let to_commonjs_call = self.ast_factory.make_call_with_arg(
        self.ast_factory.make_member_access_expr("__rolldown_runtime__", "__toCommonJS"),
        load_exports_call,
        false,
      );
      Expression::from(self.ast_factory.member_expression_static(
        SPAN,
        to_commonjs_call,
        self.ast_factory.make_id_name(SPAN, "default"),
        false,
      ))
    } else {
      self.ast_factory.make_call_with_arg(
        self.ast_factory.make_member_access_expr("__rolldown_runtime__", "__toCommonJS"),
        load_exports_call,
        false,
      )
    };

    if let Some(init_fn_name) = self.affected_module_idx_to_init_fn_name.get(&importee_idx) {
      // If the importee is in the current patch, call init before loading exports
      // Turn `require('./foo.js')` into `(init_foo(), __rolldown_runtime__.loadExports('./foo.js'))`
      *it = self.ast_factory.make_seq_in_parens(
        self.ast_factory.expression_call(
          SPAN,
          self.ast_factory.make_id_ref_expr(SPAN, init_fn_name),
          NONE,
          self.ast_factory.vec(),
          false,
        ),
        load_exports_expr,
      );
    } else {
      // Importee is not in current patch (already executed by client), just load its exports
      // Turn `require('./foo.js')` into `__rolldown_runtime__.loadExports('./foo.js')`
      *it = load_exports_expr;
    }
  }
}

pub struct NamedExport {
  pub local_binding: SymbolId,
}
