use std::collections::VecDeque;
use std::collections::hash_map::Entry;
use std::sync::Arc;

use arcstr::ArcStr;
use futures::future::join_all;
use itertools::Itertools;
use oxc::semantic::NodeId;
use oxc::semantic::{ScopeId, Scoping};
use oxc::span::Span;
use oxc::transformer_plugins::ReplaceGlobalDefinesConfig;
use oxc_index::IndexVec;
use rolldown_common::dynamic_import_usage::DynamicImportExportsUsage;
use rolldown_common::{
  EcmaModuleAstUsage, EcmaRelated, EntryPoint, EntryPointKind, ExternalModule,
  ExternalModuleTaskResult, FlatOptions, HybridIndexVec, ImportKind, ImportRecordIdx,
  ImportRecordMeta, ImportedExports, ImporterRecord, Module, ModuleId, ModuleIdx, ModuleLoaderMsg,
  ModuleType, NormalModuleTaskResult, PreserveEntrySignatures, RUNTIME_MODULE_ID, ResolvedId,
  RuntimeModuleBrief, RuntimeModuleTaskResult, ScanMode, SourceMapGenMsg, StmtInfoIdx, StmtInfos,
  SymbolRefDb, SymbolRefDbForModule,
};
use rolldown_ecmascript::EcmaAst;
use rolldown_error::{
  BuildDiagnostic, BuildResult, DiagnosableResolveError, consolidate_diagnostics,
};
use rolldown_fs::FileSystem;
use rolldown_plugin::SharedPluginDriver;
use rolldown_utils::indexmap::FxIndexSet;
use rolldown_utils::rayon::{IntoParallelIterator, ParallelIterator};
use rolldown_utils::rustc_hash::FxHashSetExt;
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::Instrument;

use crate::module_loader::module_task::ModuleTaskOwner;
use crate::types::scan_stage_cache::ScanStageCache;
use crate::utils::load_entry_module::load_entry_module;
use crate::{SharedOptions, SharedResolver};

use super::external_module_task::ExternalModuleTask;
use super::module_task::ModuleTask;
use super::runtime_module_task::RuntimeModuleTask;
use super::task_context::{TaskContext, TaskContextMeta};

pub struct IntermediateNormalModules {
  pub modules: HybridIndexVec<ModuleIdx, Option<Module>>,
  pub importers: IndexVec<ModuleIdx, Vec<ImporterRecord>>,
  pub index_ecma_ast: HybridIndexVec<ModuleIdx, Option<EcmaAst>>,
  /// Per-module statement-info table collected as modules complete. Held here
  /// instead of on `EcmaView` so the link stage can carry it as a side
  /// `IndexVec<ModuleIdx, StmtInfos>` without a `mem::replace`.
  pub stmt_infos: HybridIndexVec<ModuleIdx, Option<StmtInfos>>,
}

impl IntermediateNormalModules {
  pub fn new(is_full_scan: bool, importers: IndexVec<ModuleIdx, Vec<ImporterRecord>>) -> Self {
    Self {
      modules: if is_full_scan {
        HybridIndexVec::IndexVec(IndexVec::default())
      } else {
        HybridIndexVec::Map(FxHashMap::default())
      },
      importers,
      index_ecma_ast: if is_full_scan {
        HybridIndexVec::IndexVec(IndexVec::default())
      } else {
        HybridIndexVec::Map(FxHashMap::default())
      },
      stmt_infos: if is_full_scan {
        HybridIndexVec::IndexVec(IndexVec::default())
      } else {
        HybridIndexVec::Map(FxHashMap::default())
      },
    }
  }

  pub fn alloc_ecma_module_idx(&mut self) -> ModuleIdx {
    let id = self.modules.push(None);
    self.index_ecma_ast.push(None);
    self.stmt_infos.push(None);
    self.importers.push(Vec::new());
    id
  }

  pub fn alloc_ecma_module_idx_sparse(&mut self, i: ModuleIdx) -> ModuleIdx {
    self.modules.insert(i, None);
    self.index_ecma_ast.insert(i, None);
    self.stmt_infos.insert(i, None);
    if i >= self.importers.len() {
      self.importers.push(Vec::new());
    }
    i
  }
}

#[derive(Debug, Clone, Copy)]
pub enum VisitState {
  Seen(ModuleIdx),
  Invalidate(ModuleIdx),
}

impl VisitState {
  pub fn idx(self) -> ModuleIdx {
    match self {
      VisitState::Seen(idx) | VisitState::Invalidate(idx) => idx,
    }
  }
}

pub struct ModuleLoader<'a, Fs: FileSystem + Clone + 'static> {
  pub shared_context: Arc<TaskContext<Fs>>,
  rx: tokio::sync::mpsc::UnboundedReceiver<ModuleLoaderMsg>,
  remaining: u32,
  intermediate_normal_modules: IntermediateNormalModules,
  symbol_ref_db: SymbolRefDb,
  is_full_scan: bool,
  new_added_modules_from_partial_scan: FxIndexSet<ModuleIdx>,
  cache: &'a mut ScanStageCache,
  pub flat_options: FlatOptions,
  pub magic_string_tx: Option<std::sync::mpsc::Sender<SourceMapGenMsg>>,
  tla_module_count: usize,
  /// Centralized map from modules that contain top-level `await` to the span
  /// of the first TLA keyword. Stored here instead of on every `EcmaView`
  /// because top-level await is rarely used in real-world apps.
  tla_keyword_span_map: FxHashMap<ModuleIdx, Span>,
}

pub struct ModuleLoaderOutput {
  // Stored all modules
  pub module_table: HybridIndexVec<ModuleIdx, Module>,
  pub index_ecma_ast: HybridIndexVec<ModuleIdx, Option<EcmaAst>>,
  /// Side table of per-module `StmtInfos` (one slot per module index, with
  /// `StmtInfos::new()` placeholder for external modules). Threaded through
  /// `ScanStageOutput`/`NormalizedScanStageOutput` to the link stage instead
  /// of living on each `EcmaView`.
  pub stmt_infos: HybridIndexVec<ModuleIdx, StmtInfos>,
  pub symbol_ref_db: SymbolRefDb,
  // Entries that user defined + dynamic import entries
  pub entry_points: Vec<EntryPoint>,
  pub runtime: RuntimeModuleBrief,
  pub warnings: Vec<BuildDiagnostic>,
  pub dynamic_import_exports_usage_map: FxHashMap<ModuleIdx, DynamicImportExportsUsage>,
  // Empty if it is a full scan
  pub new_added_modules_from_partial_scan: FxIndexSet<ModuleIdx>,
  pub overrode_preserve_entry_signature_map: FxHashMap<ModuleIdx, PreserveEntrySignatures>,
  /// Record entry point and related reference ids generated by `this.emitFile`.
  /// Note, one entry point may related to multiple reference ids
  /// e.g. https://stackblitz.com/edit/rolldown-rolldown-starter-stackblitz-jqg7vnkw?file=rolldown.config.mjs,src%2Findex.js,package.json
  pub entry_point_to_reference_ids: FxHashMap<EntryPoint, Vec<ArcStr>>,
  pub flat_options: FlatOptions,
  pub user_defined_entry_modules: FxHashSet<ModuleIdx>,
  pub tla_module_count: usize,
  pub tla_keyword_span_map: FxHashMap<ModuleIdx, Span>,
}

impl<Fs: FileSystem + Clone> Drop for ModuleLoader<'_, Fs> {
  fn drop(&mut self) {
    self.cache.importers = std::mem::take(&mut self.intermediate_normal_modules.importers);
  }
}

impl<'a, Fs: FileSystem + Clone + 'static> ModuleLoader<'a, Fs> {
  pub fn new(
    fs: Fs,
    options: SharedOptions,
    resolver: SharedResolver<Fs>,
    plugin_driver: SharedPluginDriver,
    cache: &'a mut ScanStageCache,
    is_full_scan: bool,
    magic_string_tx: Option<std::sync::mpsc::Sender<SourceMapGenMsg>>,
  ) -> BuildResult<Self> {
    if is_full_scan {
      // TODO: drop the cache in another thread
      // Since we may also run a full fetch in hmr mode when multiple files changed at the same time, we need to clear the cache
      // if we are in full scan mode
      std::mem::take(cache);
    }

    let flat_options = FlatOptions::from_shared_options(&options);
    let symbol_ref_db = SymbolRefDb::new();
    let meta = TaskContextMeta {
      replace_global_define_config: if options.define.is_empty() {
        None
      } else {
        ReplaceGlobalDefinesConfig::new(&options.define).map(Some).map_err(|errs| {
          errs
            .into_iter()
            .map(|err| BuildDiagnostic::invalid_define_config(err.message.to_string()))
            .collect::<Vec<BuildDiagnostic>>()
        })?
      },
    };

    // Unbounded as defense in depth. If any sender ends up driven from a sync
    // napi binding through `block_on`, a bounded channel could deadlock: the
    // send future would wait on capacity that only the consumer can free, but
    // the consumer may need the JS thread — pinned by `block_on` — to run
    // plugin hooks first. Keeping the channel unbounded removes that edge.
    let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
    let shared_context = Arc::new(TaskContext { options, tx, resolver, fs, plugin_driver, meta });

    let importers = std::mem::take(&mut cache.importers);
    let intermediate_normal_modules = IntermediateNormalModules::new(is_full_scan, importers);

    Ok(Self {
      rx,
      cache,
      remaining: 0,
      is_full_scan,
      shared_context,
      symbol_ref_db,
      intermediate_normal_modules,
      new_added_modules_from_partial_scan: FxIndexSet::default(),
      flat_options,
      magic_string_tx,
      tla_module_count: 0,
      tla_keyword_span_map: FxHashMap::default(),
    })
  }

  /// Lazy barrel must not skip any of an entry's re-exports — an entry has to
  /// preserve all of its exports. Request `All` from it: if it has already been
  /// loaded as a barrel, load its remaining (deferred) records now; otherwise
  /// record the request so they get loaded once it finishes loading.
  #[expect(clippy::rc_buffer)]
  fn request_all_exports_for_entry(
    &mut self,
    idx: ModuleIdx,
    user_defined_entries: &Arc<Vec<(Option<ArcStr>, ResolvedId)>>,
  ) {
    if let Some(barrel_module_state) = self.cache.barrel_state.barrel_infos.get(&idx) {
      // If the module is already a barrel module, we need to load its remaining re-export import records
      if barrel_module_state.is_some() {
        self.process_barrel_import_record(
          &mut VecDeque::from_iter([(idx, ImportedExports::All)]),
          user_defined_entries,
        );
      }
    } else {
      self.cache.barrel_state.requested_exports.insert(idx, ImportedExports::All);
    }
  }

  #[expect(clippy::rc_buffer)]
  fn try_spawn_new_task(
    &mut self,
    resolved_id: ResolvedId,
    owner: Option<ModuleTaskOwner>,
    is_user_defined_entry: bool,
    assert_module_type: Option<&ModuleType>,
    user_defined_entries: &Arc<Vec<(Option<ArcStr>, ResolvedId)>>,
  ) -> ModuleIdx {
    let idx = match self.cache.module_id_to_idx.get(&resolved_id.id).copied() {
      Some(VisitState::Seen(idx)) => {
        if self.flat_options.is_lazy_barrel_enabled() && owner.is_none() {
          self.request_all_exports_for_entry(idx, user_defined_entries);
        }
        return idx;
      }
      Some(VisitState::Invalidate(idx)) => {
        // Full scan mode the idx will never be invalidated right?
        self.intermediate_normal_modules.alloc_ecma_module_idx_sparse(idx);
        self.cache.module_id_to_idx.insert(resolved_id.id.clone(), VisitState::Seen(idx));
        idx
      }
      None if !self.is_full_scan => {
        // This means some new module has been added in partial scan mode
        let len = self.cache.module_id_to_idx.len();
        let idx = self.intermediate_normal_modules.alloc_ecma_module_idx_sparse(len.into());
        self.new_added_modules_from_partial_scan.insert(idx);
        self.cache.module_id_to_idx.insert(resolved_id.id.clone(), VisitState::Seen(idx));
        idx
      }
      None => {
        let idx = self.intermediate_normal_modules.alloc_ecma_module_idx();
        self.cache.module_id_to_idx.insert(resolved_id.id.clone(), VisitState::Seen(idx));
        idx
      }
    };
    if self.flat_options.is_lazy_barrel_enabled() && owner.is_none() {
      self.request_all_exports_for_entry(idx, user_defined_entries);
    }
    let ctx = Arc::clone(&self.shared_context);
    if resolved_id.external.is_external() {
      let task = ExternalModuleTask::new(ctx, idx, resolved_id, Arc::clone(user_defined_entries));
      tokio::spawn(task.run().instrument(tracing::info_span!("external_module_task")));
    } else {
      let task = ModuleTask::new(
        ctx,
        idx,
        resolved_id,
        owner,
        is_user_defined_entry,
        assert_module_type.cloned(),
        self.flat_options,
        self.magic_string_tx.clone(),
      );
      tokio::spawn(task.run().instrument(tracing::info_span!("normal_module_task")));
    }
    self.remaining += 1;
    idx
  }

  /// For `fetch_modules` we need to support three scenarios:
  /// - Full scan mode in none watch mode, scan all modules from user defined entries.
  /// - Partial scan mode, scan the changed modules, it maybe none initial
  /// build in incremental watch mode
  /// - Full scan mode in watch mode, scan all modules from user defined entries, it maybe first
  /// time build in watch mode or edgecase in HMR(User update `node_modules` too much modules are
  /// updated at same time, patch them one by one is not efficient, so we do full scan directly)
  #[tracing::instrument(level = "debug", skip_all)]
  #[expect(clippy::too_many_lines)]
  pub async fn fetch_modules(
    &mut self,
    fetch_mode: ScanMode<ResolvedId>,
  ) -> BuildResult<ModuleLoaderOutput> {
    let mut errors = vec![];
    let mut all_warnings = vec![];

    // Initialize runtime module task if not yet started
    if let Entry::Vacant(e) = self.cache.module_id_to_idx.entry(RUNTIME_MODULE_ID) {
      let idx = self.intermediate_normal_modules.alloc_ecma_module_idx();
      let task = RuntimeModuleTask::new(idx, Arc::clone(&self.shared_context), self.flat_options);
      tokio::spawn(task.run().instrument(tracing::info_span!("runtime_module_task")));
      e.insert(VisitState::Seen(idx));
      self.remaining += 1;
    }

    let user_defined_entries = Arc::new(match fetch_mode {
      ScanMode::Full => self.resolve_user_defined_entries().await?,
      ScanMode::Partial(_) => vec![],
    });

    let entries_count = user_defined_entries.len() + /* runtime */ 1;
    self.intermediate_normal_modules.modules.reserve(entries_count);
    self.intermediate_normal_modules.index_ecma_ast.reserve(entries_count);

    // Store the already consider as entry module
    let mut entry_points = FxIndexSet::default();
    let mut user_defined_entry_ids = FxHashSet::with_capacity(user_defined_entries.len());
    for (name, resolved_id) in user_defined_entries.iter().cloned() {
      let idx = self.try_spawn_new_task(resolved_id, None, true, None, &user_defined_entries);
      user_defined_entry_ids.insert(idx);
      entry_points.insert(EntryPoint {
        idx,
        name,
        kind: EntryPointKind::UserDefined,
        file_name: None,
        related_stmt_infos: vec![],
      });
    }

    if self.is_full_scan && self.shared_context.options.experimental.is_incremental_build_enabled()
    {
      self
        .cache
        .user_defined_entry
        .extend(user_defined_entries.iter().map(|(_, resolved_id)| resolved_id.id.clone()));
    }

    // If it is in partial scan mode, we need to invalidate the changed modules
    // and re-fetch them, do nothing in full scan mode
    for resolved_id in fetch_mode.iter().cloned() {
      self.shared_context.plugin_driver.invalidate_context_load_module(&resolved_id.id);
      if let Entry::Occupied(mut occ) = self.cache.module_id_to_idx.entry(resolved_id.id.clone()) {
        let idx = occ.get().idx();
        occ.insert(VisitState::Invalidate(idx));
        self.cache.barrel_state.barrel_infos.remove(&idx);
      }
      // User may update the entry module in incremental mode, so we need to make sure
      // if it is a user defined entry to avoid generate wrong asset file
      let is_user_defined_entry = self.cache.user_defined_entry.contains(&resolved_id.id);
      // Setting `Owner` to `None` is safe here since `Owner` is only used to emit
      // `Unloadable` diagnostics, and we know this module exists in the filesystem.
      // TODO: copy assert_module_type
      self.try_spawn_new_task(
        resolved_id,
        None,
        is_user_defined_entry,
        None,
        &user_defined_entries,
      );
    }

    let mut work_queue: VecDeque<(ModuleIdx, ImportedExports)> = VecDeque::new();
    let mut dynamic_import_entry_ids: FxHashMap<
      ModuleIdx,
      Vec<(ModuleIdx, StmtInfoIdx, NodeId, ImportRecordIdx)>,
    > = FxHashMap::default();

    let mut dynamic_import_exports_usage_pairs = vec![];
    let mut extra_entry_points = vec![];
    let mut entry_point_to_reference_ids: FxHashMap<EntryPoint, Vec<ArcStr>> = FxHashMap::default();

    let mut runtime_brief = None;
    let mut overrode_preserve_entry_signature_map = FxHashMap::default();

    while self.remaining > 0 {
      let Some(msg) = self.rx.recv().await else {
        break;
      };
      match msg {
        ModuleLoaderMsg::NormalModuleDone(task_result) => {
          let NormalModuleTaskResult {
            mut module,
            mut barrel_info,
            ecma_related:
              EcmaRelated {
                ast,
                symbols,
                mut dynamic_import_rec_exports_usage,
                preserve_jsx,
                stmt_infos,
              },
            resolved_deps,
            raw_import_records,
            warnings,
            tla_keyword_span,
          } = *task_result;
          all_warnings.extend(warnings);

          let module_idx = module.idx();

          // remove importers from previous scan
          if !self.is_full_scan
            && let Some(previous_module) =
              self.cache.get_snapshot().module_table.modules.get(module_idx)
          {
            let resolved_deps =
              previous_module.import_records().into_iter().filter_map(|r| r.resolved_module);
            for dep_idx in resolved_deps {
              self.intermediate_normal_modules.importers[dep_idx]
                .retain(|v| v.importer_idx != module_idx);
            }
          }

          let normal_module = module.as_normal().unwrap();
          if normal_module.ast_usage.contains(EcmaModuleAstUsage::TopLevelAwait) {
            self.tla_module_count += 1;
          }
          if let Some(span) = tla_keyword_span {
            self.tla_keyword_span_map.insert(module_idx, span);
          }
          let mut import_records = IndexVec::with_capacity(raw_import_records.len());

          let mut tracked_records = FxHashMap::default();
          let mut initialized_barrel_tracking =
            self.flat_options.is_lazy_barrel_enabled().then(|| {
              self.cache.barrel_state.initialize_barrel_tracking(
                normal_module,
                &raw_import_records,
                &mut barrel_info,
              )
            });

          for ((rec_idx, mut raw_rec), resolved_id) in
            raw_import_records.into_iter_enumerated().zip(resolved_deps)
          {
            if self.shared_context.options.experimental.vite_mode.unwrap_or_default()
              && resolved_id.id.as_str().ends_with(".json")
            {
              raw_rec.meta.insert(ImportRecordMeta::JsonModule);
            }

            // Lazy barrel optimization: skip loading modules that are not needed yet.
            // - `imported_exports_per_record`: maps all import records to their required imported exports
            // - `initial_needed_records`: subset of records that need to be loaded initially
            if let Some((ref imported_exports_per_record, ref initial_needed_records)) =
              initialized_barrel_tracking
              && raw_rec.kind == ImportKind::Import
            {
              // Track remaining import records for later on-demand loading
              if imported_exports_per_record.contains_key(&rec_idx) {
                tracked_records.insert(rec_idx, (raw_rec.state.clone(), resolved_id.clone()));
              }
              // Skip records not in initial_needed_records - they may be loaded later if needed
              if !initial_needed_records.contains_key(&rec_idx) {
                import_records.push(raw_rec.into_resolved(None));
                continue;
              }
            }

            let is_external = resolved_id.external.is_external();
            let idx = self.try_spawn_new_task(
              resolved_id,
              Some(ModuleTaskOwner::new(normal_module, raw_rec.span)),
              false,
              raw_rec.asserted_module_type.as_ref(),
              &user_defined_entries,
            );

            if let Some((_, ref mut initial_needed_records)) = initialized_barrel_tracking {
              let imported_exports = if raw_rec.kind == ImportKind::Import {
                initial_needed_records.remove(&rec_idx).expect(
                  "If initial_needed_records does not contain the record idx, it should have been skipped above"
                )
              } else {
                ImportedExports::All
              };
              work_queue.push_back((idx, imported_exports));
            }

            // Dynamic imported module will be considered as an entry
            self.intermediate_normal_modules.importers[idx].push(ImporterRecord {
              kind: raw_rec.kind,
              importer_path: module.id().clone(),
              importer_idx: module_idx,
            });

            // Defer usage merging - with only one consumer, keep fetch actions simple
            if let Some(usage) = dynamic_import_rec_exports_usage.remove(&rec_idx) {
              dynamic_import_exports_usage_pairs.push((idx, usage));
            }
            // External modules are excluded here to preserve the bit-position-to-ChunkIdx
            // invariant in code splitting. User-defined and emitted entries are already
            // guaranteed non-external by `load_entry_module()` which rejects them with
            // `entry_cannot_be_external`.
            if matches!(raw_rec.kind, ImportKind::DynamicImport)
              && !is_external
              && !user_defined_entry_ids.contains(&idx)
            {
              match dynamic_import_entry_ids.entry(idx) {
                Entry::Vacant(vac) => match raw_rec.dynamic_import_expr_info.as_ref() {
                  Some(info) => {
                    vac.insert(vec![(module_idx, info.stmt_info_idx, info.node_id, rec_idx)]);
                  }
                  None => {
                    vac.insert(vec![]);
                  }
                },
                Entry::Occupied(mut occ) => {
                  if let Some(info) = raw_rec.dynamic_import_expr_info.as_ref() {
                    occ.get_mut().push((module_idx, info.stmt_info_idx, info.node_id, rec_idx));
                  }
                }
              }
            }
            import_records.push(raw_rec.into_resolved(Some(idx)));
          }

          module.set_import_records(import_records);
          *self.intermediate_normal_modules.index_ecma_ast.get_mut(module_idx) = Some(ast);
          *self.intermediate_normal_modules.stmt_infos.get_mut(module_idx) = Some(stmt_infos);
          *self.intermediate_normal_modules.modules.get_mut(module_idx) = Some(module);

          if let Some((imported_exports_per_record, _)) = initialized_barrel_tracking {
            self.cache.barrel_state.barrel_infos.insert(
              module_idx,
              if tracked_records.is_empty() {
                None
              } else {
                barrel_info.map(|info| {
                  info.into_barrel_module_state(tracked_records, imported_exports_per_record)
                })
              },
            );
            self.process_barrel_import_record(&mut work_queue, &user_defined_entries);
          }

          self.symbol_ref_db.store_local_db(module_idx, symbols);
          if preserve_jsx {
            self.symbol_ref_db.set_has_module_preserve_jsx();
          }
          self.remaining -= 1;
        }
        ModuleLoaderMsg::ExternalModuleDone(task_result) => {
          let ExternalModuleTaskResult {
            id,
            name,
            idx,
            identifier_name,
            side_effects,
            need_renormalize_render_path,
          } = *task_result;

          self.symbol_ref_db.store_local_db(
            idx,
            SymbolRefDbForModule::new(Scoping::default(), idx, ScopeId::new(0)),
          );
          let symbol_ref = self.symbol_ref_db.create_facade_root_symbol_ref(idx, &identifier_name);
          let external_module = Module::External(Box::new(ExternalModule::new(
            idx,
            id,
            name,
            identifier_name,
            side_effects,
            symbol_ref,
            need_renormalize_render_path,
          )));

          *self.intermediate_normal_modules.modules.get_mut(idx) = Some(external_module);
          self.remaining -= 1;
        }
        ModuleLoaderMsg::RuntimeNormalModuleDone(task_result) => {
          let RuntimeModuleTaskResult {
            local_symbol_ref_db,
            mut module,
            stmt_infos,
            runtime,
            ast,
            raw_import_records,
            resolved_deps,
            warnings,
          } = *task_result;
          all_warnings.extend(warnings);

          let mut import_records = IndexVec::with_capacity(raw_import_records.len());
          for (raw_rec, info) in raw_import_records.into_iter().zip(resolved_deps) {
            let idx = self.try_spawn_new_task(
              info,
              None,
              false,
              raw_rec.asserted_module_type.as_ref(),
              &user_defined_entries,
            );
            self.intermediate_normal_modules.importers[idx].push(ImporterRecord {
              kind: raw_rec.kind,
              importer_path: module.id.clone(),
              importer_idx: module.idx,
            });
            import_records.push(raw_rec.into_resolved(Some(idx)));
          }
          module.import_records = import_records;

          *self.intermediate_normal_modules.modules.get_mut(runtime.id()) = Some(module.into());
          *self.intermediate_normal_modules.index_ecma_ast.get_mut(runtime.id()) = Some(ast);
          *self.intermediate_normal_modules.stmt_infos.get_mut(runtime.id()) = Some(stmt_infos);

          self.symbol_ref_db.store_local_db(runtime.id(), local_symbol_ref_db);
          self.remaining -= 1;

          errors.extend(runtime.validate_symbols(&rolldown_common::RUNTIME_HELPER_NAMES));
          runtime_brief = Some(runtime);
        }
        ModuleLoaderMsg::FetchModule(resolve_id) => {
          self.try_spawn_new_task(*resolve_id, None, false, None, &user_defined_entries);
        }
        ModuleLoaderMsg::AddEntryModule(msg) => {
          let data = msg.chunk;
          let result = load_entry_module(
            &self.shared_context.resolver,
            &self.shared_context.plugin_driver,
            &data.id,
            data.importer.as_deref(),
          )
          .await;

          let module_idx = match result {
            Ok(resolved_id) => {
              self.try_spawn_new_task(resolved_id, None, true, None, &user_defined_entries)
            }
            Err(e) => {
              errors.push(e);
              continue;
            }
          };

          if let Some(preserve_entry_signatures) = data.preserve_entry_signatures {
            overrode_preserve_entry_signature_map.insert(module_idx, preserve_entry_signatures);
          }

          user_defined_entry_ids.insert(module_idx);

          let entry = EntryPoint {
            name: data.name.clone(),
            idx: module_idx,
            kind: EntryPointKind::EmittedUserDefined,
            file_name: data.file_name.clone(),
            related_stmt_infos: vec![],
          };

          entry_point_to_reference_ids
            .entry(entry.clone())
            .or_default()
            .push(msg.reference_id.clone());

          extra_entry_points.push(entry);
        }
        ModuleLoaderMsg::BuildErrors(e) => {
          errors.extend(e);
          self.remaining -= 1;
        }
      }
    }

    if !errors.is_empty() {
      // Enrich UNRESOLVED_IMPORT errors with import chain
      for error in &mut errors {
        if let Some(resolve_error) = error.downcast_mut::<DiagnosableResolveError>() {
          let chain = self
            .trace_import_chain_from_modules(&resolve_error.importer_id, &user_defined_entry_ids);
          if !chain.is_empty() {
            resolve_error.import_chain = Some(chain);
          }
        }
      }

      let errors = consolidate_diagnostics(errors);
      return Err(errors.into());
    }
    if let Some(tx) = self.magic_string_tx.as_ref() {
      tx.send(SourceMapGenMsg::Terminate).expect(
        "SourceMapGen: failed to send Terminate message - sourcemap worker thread died unexpectedly"
      );
    }

    let dynamic_import_exports_usage_map = dynamic_import_exports_usage_pairs.into_iter().fold(
      FxHashMap::default(),
      |mut acc, (idx, usage)| {
        match acc.entry(idx) {
          Entry::Vacant(vac) => {
            vac.insert(usage);
          }
          Entry::Occupied(mut occ) => {
            occ.get_mut().merge(usage);
          }
        }
        acc
      },
    );

    let mut idx_of_module_info_need_update = vec![];
    let is_dense_index_vec = self.intermediate_normal_modules.modules.is_index_vec();

    let modules_iter = std::mem::take(&mut self.intermediate_normal_modules.modules)
      .into_iter_enumerated()
      .into_iter()
      .map(|(idx, module)| {
        let mut module = module.expect("Module tasks did't complete as expected");
        if let Some(module) = module.as_normal_mut() {
          // Note: (Compat to rollup)
          // The `dynamic_importers/importers` should be added after `module_parsed` hook.
          let importers = &self.intermediate_normal_modules.importers[idx];
          for importer in importers {
            if importer.kind.is_static() {
              module.importers.insert(importer.importer_path.clone());
              module.importers_idx.insert(importer.importer_idx);
            } else {
              module.dynamic_importers.insert(importer.importer_path.clone());
            }
          }
          if !importers.is_empty() {
            idx_of_module_info_need_update.push(idx);
          }
        }
        (idx, module)
      });

    let module_table = if is_dense_index_vec {
      let vec = modules_iter.map(|(_, module)| module).collect();
      HybridIndexVec::IndexVec(IndexVec::from_vec(vec))
    } else {
      let map = modules_iter.collect::<FxHashMap<_, _>>();
      HybridIndexVec::Map(map)
    };

    // Build the side `stmt_infos` table parallel to `module_table`. Slots that
    // never received a `Some(stmt_infos)` (external modules, or normal modules
    // not produced by tasks) get a fresh empty `StmtInfos` placeholder.
    let stmt_infos_iter = std::mem::take(&mut self.intermediate_normal_modules.stmt_infos)
      .into_iter_enumerated()
      .into_iter()
      .map(|(idx, stmt_infos)| (idx, stmt_infos.unwrap_or_else(StmtInfos::new)));
    let stmt_infos = if is_dense_index_vec {
      let vec = stmt_infos_iter.map(|(_, s)| s).collect();
      HybridIndexVec::IndexVec(IndexVec::from_vec(vec))
    } else {
      let map = stmt_infos_iter.collect::<FxHashMap<_, _>>();
      HybridIndexVec::Map(map)
    };

    // Some module was not treated as an entry, but was emitted by `this.emitFile` during
    // processing, those module info also need to be updated
    // see https://github.com/rolldown/rolldown/issues/5030 as an example
    idx_of_module_info_need_update.extend(extra_entry_points.iter().map(|item| item.idx));
    idx_of_module_info_need_update.into_par_iter().for_each(|idx| {
      let module = module_table.get(idx);
      let Some(module) = module.as_normal() else {
        return;
      };
      self.shared_context.plugin_driver.set_module_info(
        &module.id,
        Arc::new(module.to_module_info(None, user_defined_entry_ids.contains(&module.idx))),
      );
    });

    // Collect module indices from emitted entries to filter dynamic imports
    // When a module is both dynamically imported AND emitted via this.emitFile,
    // the emitted entry takes priority (it has user-specified name, fileName, preserveSignature)
    let emitted_entry_indices: FxHashSet<ModuleIdx> =
      extra_entry_points.iter().map(|e| e.idx).collect();

    for (idx, related_stmt_infos) in dynamic_import_entry_ids {
      if !emitted_entry_indices.contains(&idx) {
        entry_points.insert(EntryPoint {
          name: None,
          idx,
          kind: EntryPointKind::DynamicImport,
          file_name: None,
          related_stmt_infos,
        });
      }
    }

    entry_points.extend(extra_entry_points);
    if entry_points.is_empty() && self.is_full_scan {
      Err(BuildDiagnostic::invalid_option(rolldown_error::InvalidOptionType::NoEntryPoint))?;
    }

    let entry_points = entry_points.into_iter().collect_vec();
    // if it is in incremental mode, we skip the runtime module, since it is always there
    // so use a dummy runtime_brief as a placeholder
    let runtime = if self.is_full_scan {
      tracing::debug!("changed_resolved_ids: {fetch_mode:#?}");
      runtime_brief.expect("Failed to find runtime module. This should not happen")
    } else {
      RuntimeModuleBrief::dummy()
    };

    Ok(ModuleLoaderOutput {
      runtime,
      entry_points,
      module_table,
      warnings: all_warnings,
      dynamic_import_exports_usage_map,
      overrode_preserve_entry_signature_map,
      entry_point_to_reference_ids,
      symbol_ref_db: std::mem::take(&mut self.symbol_ref_db),
      index_ecma_ast: std::mem::take(&mut self.intermediate_normal_modules.index_ecma_ast),
      stmt_infos,
      new_added_modules_from_partial_scan: std::mem::take(
        &mut self.new_added_modules_from_partial_scan,
      ),
      flat_options: self.flat_options,
      user_defined_entry_modules: user_defined_entry_ids,
      tla_module_count: self.tla_module_count,
      tla_keyword_span_map: std::mem::take(&mut self.tla_keyword_span_map),
    })
  }

  #[tracing::instrument(target = "devtool", level = "debug", skip_all)]
  pub async fn resolve_user_defined_entries(
    &self,
  ) -> BuildResult<Vec<(Option<ArcStr>, ResolvedId)>> {
    let resolved_ids =
      join_all(self.shared_context.options.input.iter().map(|input_item| async move {
        let resolved = load_entry_module(
          &self.shared_context.resolver,
          &self.shared_context.plugin_driver,
          &input_item.import,
          None,
        )
        .await;

        resolved.map(|info| (input_item.name.as_ref().map(Into::into), info))
      }))
      .await;

    let mut ret = Vec::with_capacity(self.shared_context.options.input.len());

    let mut errors = vec![];

    for resolve_id in resolved_ids {
      match resolve_id {
        Ok(item) => {
          ret.push(item);
        }
        Err(e) => errors.push(e),
      }
    }

    if !errors.is_empty() {
      Err(errors)?;
    }

    Ok(ret)
  }

  /// Traces the import chain from a module back to an entry point.
  /// Returns a list of module paths from the given module to an entry point.
  /// This version works directly with intermediate modules before the module table is built.
  fn trace_import_chain_from_modules(
    &self,
    importer_id: &str,
    user_defined_entry_ids: &FxHashSet<ModuleIdx>,
  ) -> Vec<String> {
    let importer_module_id = ModuleId::new(importer_id);
    let Some(visit_state) = self.cache.module_id_to_idx.get(&importer_module_id) else {
      return vec![];
    };
    let start_idx = visit_state.idx();

    let mut chain = Vec::new();
    let mut visited = FxHashSet::default();
    let mut current = Some(start_idx);

    while let Some(idx) = current {
      if visited.contains(&idx) {
        break;
      }
      visited.insert(idx);

      let module_opt = self.intermediate_normal_modules.modules.get(idx);
      if let Some(module) = module_opt {
        if let Some(normal) = module.as_normal() {
          chain.push(normal.id.to_string());
        }
      }

      if user_defined_entry_ids.contains(&idx) {
        break;
      }

      current = self
        .intermediate_normal_modules
        .importers
        .get(idx)
        .and_then(|importers| importers.first().map(|rec| rec.importer_idx));
    }
    chain
  }

  #[expect(clippy::rc_buffer)]
  fn process_barrel_import_record(
    &mut self,
    work_queue: &mut VecDeque<(ModuleIdx, ImportedExports)>,
    user_defined_entries: &Arc<Vec<(Option<ArcStr>, ResolvedId)>>,
  ) {
    while let Some((idx, imported_exports)) = work_queue.pop_front() {
      // No entry in `barrel_infos` means this module hasn't been loaded and processed yet.
      // Record it for now and process later when loading is complete.
      let Some(barrel_module_state) = self.cache.barrel_state.barrel_infos.get_mut(&idx) else {
        match self.cache.barrel_state.requested_exports.entry(idx) {
          Entry::Occupied(mut occ) => {
            imported_exports.subtract_and_merge_into(occ.get_mut());
          }
          Entry::Vacant(vac) => {
            vac.insert(imported_exports);
          }
        }
        continue;
      };

      // Not a barrel module, clean up and skip
      let Some(barrel_module_state) = barrel_module_state else {
        self.cache.barrel_state.requested_exports.remove(&idx);
        continue;
      };

      // No tracked records, skip
      if barrel_module_state.tracked_records.is_empty() {
        continue;
      }

      // Calculate new exports to process (subtract already processed)
      let new_exports = match self.cache.barrel_state.requested_exports.entry(idx) {
        Entry::Occupied(mut occ) => {
          let Some(diff) = imported_exports.subtract_and_merge_into(occ.get_mut()) else {
            continue;
          };
          diff
        }
        Entry::Vacant(vac) => {
          vac.insert(imported_exports.clone());
          imported_exports
        }
      };

      let mut tracked_records = std::mem::take(&mut barrel_module_state.tracked_records);
      let mut remaining_imported_specifiers =
        std::mem::take(&mut barrel_module_state.remaining_imported_specifiers);

      // Below repeats the post-module-load processing logic, but only for the remaining `tracked_records`.
      // Additionally, we check whether each record in `tracked_records` still needs to be retained.
      let mut needed_records = barrel_module_state
        .info
        .take_needed_records(&new_exports, &mut remaining_imported_specifiers);

      tracked_records.retain(|&rec_idx, (import_record_state, resolved_id)| {
        let Some(needed_record) = needed_records.remove(&rec_idx) else {
          return true;
        };

        let mut is_module_from_cache_snapshot = false;
        let barrel_normal_module = match &self.intermediate_normal_modules.modules {
          HybridIndexVec::IndexVec(modules) => modules[idx]
            .as_ref()
            .expect("Barrel module should exists in full build")
            .as_normal()
            .unwrap(),
          HybridIndexVec::Map(modules) => {
            if let Some(module) = modules.get(&idx) {
              module
                .as_ref()
                .expect("Barrel module should exists in partial build")
                .as_normal()
                .unwrap()
            } else {
              is_module_from_cache_snapshot = true;
              self
                .cache
                .get_snapshot()
                .module_table
                .get(idx)
                .expect("Barrel module should exist in cache snapshot in partial scan mode")
                .as_normal()
                .unwrap()
            }
          }
        };

        let target_idx = match barrel_normal_module.import_records[rec_idx].resolved_module {
          Some(existing_idx) => existing_idx,
          None => {
            let importer_record = ImporterRecord {
              kind: barrel_normal_module.import_records[rec_idx].kind,
              importer_path: barrel_normal_module.id.clone(),
              importer_idx: barrel_normal_module.idx,
            };
            let new_idx = self.try_spawn_new_task(
              resolved_id.clone(),
              Some(ModuleTaskOwner::new(barrel_normal_module, import_record_state.span)),
              false,
              import_record_state.asserted_module_type.as_ref(),
              user_defined_entries,
            );
            self.intermediate_normal_modules.importers[new_idx].push(importer_record);
            // Update resolved module in either cache snapshot or intermediate modules
            if is_module_from_cache_snapshot {
              self
                .cache
                .barrel_state
                .resolved_barrel_modules
                .entry(idx)
                .or_default()
                .push((rec_idx, new_idx));
            } else {
              self
                .intermediate_normal_modules
                .modules
                .get_mut(idx)
                .as_mut()
                .expect("barrel module should exist")
                .as_normal_mut()
                .unwrap()
                .import_records[rec_idx]
                .resolved_module = Some(new_idx);
            }
            new_idx
          }
        };
        let keep_tracking = match self.cache.barrel_state.barrel_infos.get(&target_idx) {
          Some(Some(s)) => target_idx == idx || !s.tracked_records.is_empty(),
          Some(None) => false,
          None => true,
        };
        work_queue.push_back((target_idx, needed_record));
        if keep_tracking {
          remaining_imported_specifiers.contains_key(&rec_idx)
        } else {
          remaining_imported_specifiers.remove(&rec_idx);
          false
        }
      });

      let barrel_module_state =
        self.cache.barrel_state.barrel_infos.get_mut(&idx).unwrap().as_mut().unwrap();
      barrel_module_state.tracked_records = tracked_records;
      barrel_module_state.remaining_imported_specifiers = remaining_imported_specifiers;
    }
  }
}
