use std::collections::{VecDeque, hash_map::Entry};

use arcstr::ArcStr;
use itertools::Itertools;
use oxc_index::{IndexVec, index_vec};
use rolldown_common::{
  Chunk, ChunkDebugInfo, ChunkIdx, ChunkKind, ChunkMeta, ChunkReasonType,
  FacadeChunkEliminationReason, Module, ModuleIdx, ModuleNamespaceIncludedReason, ModuleTable,
  NormalModule, PostChunkOptimizationOperation, PreserveEntrySignatures, RuntimeHelper, StmtInfos,
  WrapKind,
};
use rolldown_utils::{BitSet, IndexBitSet, indexmap::FxIndexMap};
use rustc_hash::{FxHashMap, FxHashSet};

use crate::{
  chunk_graph::ChunkGraph,
  stages::link_stage::{
    IncludeContext, SymbolIncludeReason, include_runtime_symbol, include_symbol,
  },
  types::linking_metadata::{
    LinkingMetadata, LinkingMetadataVec, included_info_to_linking_metadata_vec,
    linking_metadata_vec_to_included_info,
  },
};

use super::{
  GenerateStage, chunk_ext::ChunkCreationReason, chunk_ext::ChunkDebugExt,
  code_splitting::IndexSplittingInfo,
};

struct FacadeChunkElimination {
  reason: FacadeChunkEliminationReason,
  entry_module_idx: ModuleIdx,
  from_chunk_idx: ChunkIdx,
  to_chunk_idx: ChunkIdx,
}

struct CommonChunkMerge {
  from_chunk_idx: ChunkIdx,
  to_chunk_idx: ChunkIdx,
}

#[derive(Debug, Default)]
/// A lightweight representation of a chunk used during optimization passes.
struct ChunkCandidate {
  modules: Vec<ModuleIdx>,
  /// Whether this chunk needs to be created in the final chunk graph.
  needs_creation: bool,
  dependencies: FxHashSet<ChunkIdx>,
  /// Whether any module in this chunk has side effects.
  has_side_effects: bool,
}

type TempIndexChunks = IndexVec<ChunkIdx, ChunkCandidate>;

/// A temporary structure for managing chunk graph optimizations.
/// Only store simplified information needed during optimization passes.
#[derive(Debug, Default)]
pub struct ChunkOptimizationGraph {
  chunks: TempIndexChunks,
  bits_to_chunk_idx: FxIndexMap<BitSet, ChunkIdx>,
  /// Mapping from module index to the chunk it belongs to.
  module_to_chunk: IndexVec<ModuleIdx, Option<ChunkIdx>>,
  /// Mapping from chunk_graph indices to temp_chunk_graph indices.
  /// Initial chunks share identical indices; newly-created common chunks
  /// in chunk_graph are registered here so callers can translate.
  chunk_idx_to_temp_chunk_idx: FxHashMap<ChunkIdx, ChunkIdx>,
}

impl ChunkOptimizationGraph {
  pub fn new(
    chunk_optimization: bool,
    chunk_graph: &ChunkGraph,
    bits_to_chunk_idx: &FxHashMap<BitSet, ChunkIdx>,
    module_table: &ModuleTable,
  ) -> Self {
    if !chunk_optimization {
      return Self::default();
    }
    // These initial chunks already exist in the chunk graph, including:
    // - entry chunks
    // - manual code splitting chunks
    let mut module_to_chunk = index_vec![None; chunk_graph.module_to_chunk.len()];
    let mut chunk_idx_to_temp_chunk_idx = FxHashMap::default();
    let chunks = chunk_graph
      .chunk_table
      .iter_enumerated()
      .map(|(chunk_idx, item)| {
        for &module_idx in &item.modules {
          module_to_chunk[module_idx] = Some(chunk_idx);
        }
        // Initial chunks have identical indices in both graphs
        chunk_idx_to_temp_chunk_idx.insert(chunk_idx, chunk_idx);
        let has_side_effects = item
          .modules
          .iter()
          .any(|&module_idx| module_table[module_idx].side_effects().has_side_effects());
        ChunkCandidate {
          modules: item.modules.clone(),
          needs_creation: false,
          dependencies: FxHashSet::default(),
          has_side_effects,
        }
      })
      .collect();

    let mut bits_to_chunk_idx: FxIndexMap<_, _> =
      bits_to_chunk_idx.iter().map(|(k, v)| (k.clone(), *v)).collect();
    bits_to_chunk_idx.sort_unstable_keys();

    Self { chunks, bits_to_chunk_idx, module_to_chunk, chunk_idx_to_temp_chunk_idx }
  }

  /// Assigns a module to a temporary chunk based on its reachability bits.
  ///
  /// If a chunk already exists for the given bit pattern, the module is added to it.
  /// Otherwise, a new temporary chunk is created and marked as `needs_created: true`,
  /// indicating it will need to be materialized in the final chunk graph.
  pub fn init_module_assignment(
    &mut self,
    module_idx: ModuleIdx,
    bits: &BitSet,
    module_table: &ModuleTable,
  ) {
    let module_has_side_effects = module_table[module_idx].side_effects().has_side_effects();
    if let Some(&chunk_idx) = self.bits_to_chunk_idx.get(bits) {
      self.chunks[chunk_idx].modules.push(module_idx);
      self.chunks[chunk_idx].has_side_effects |= module_has_side_effects;
      self.module_to_chunk[module_idx] = Some(chunk_idx);
    } else {
      let temp_chunk = ChunkCandidate {
        modules: vec![module_idx],
        needs_creation: true,
        dependencies: FxHashSet::default(),
        has_side_effects: module_has_side_effects,
      };
      let chunk_idx = self.chunks.push(temp_chunk);
      self.bits_to_chunk_idx.insert(bits.clone(), chunk_idx);
      self.module_to_chunk[module_idx] = Some(chunk_idx);
    }
  }

  pub fn add_module_to_chunk(
    &mut self,
    module_idx: ModuleIdx,
    chunk_idx: ChunkIdx,
    module_table: &ModuleTable,
  ) {
    self.chunks[chunk_idx].modules.push(module_idx);
    self.chunks[chunk_idx].has_side_effects |=
      module_table[module_idx].side_effects().has_side_effects();
    self.module_to_chunk[module_idx] = Some(chunk_idx);
  }

  /// Records the mapping from a newly-created chunk_graph index to its
  /// corresponding temp_chunk_graph index.
  pub fn register_chunk_graph_index(
    &mut self,
    chunk_graph_idx: ChunkIdx,
    temp_chunk_idx: ChunkIdx,
  ) {
    self.chunk_idx_to_temp_chunk_idx.insert(chunk_graph_idx, temp_chunk_idx);
  }

  /// Translates a chunk_graph index into the corresponding temp_chunk_graph index.
  /// Returns `None` if the chunk_graph index has no known temp counterpart.
  pub fn to_temp_idx(&self, chunk_graph_idx: ChunkIdx) -> Option<ChunkIdx> {
    self.chunk_idx_to_temp_chunk_idx.get(&chunk_graph_idx).copied()
  }

  /// Calculates chunk dependencies based on module dependencies.
  ///
  /// For each chunk, iterates through its modules and their dependencies.
  /// If a module dependency belongs to a different chunk, that chunk is added
  /// as a dependency of the current chunk.
  ///
  /// This is similar to Rollup's `addChunkDependenciesAndGetExternalSideEffectAtoms`.
  pub fn calc_chunk_dependencies(&mut self, metas: &IndexVec<ModuleIdx, LinkingMetadata>) {
    for chunk_idx in self.chunks.indices() {
      let modules = std::mem::take(&mut self.chunks[chunk_idx].modules);
      for module_idx in &modules {
        let module_dependencies = &metas[*module_idx].dependencies;
        for &dep_module_idx in module_dependencies {
          if let Some(dep_chunk_idx) = self.module_to_chunk[dep_module_idx] {
            // Only add if dependency is in a different chunk
            if dep_chunk_idx != chunk_idx {
              self.chunks[chunk_idx].dependencies.insert(dep_chunk_idx);
            }
          }
        }
      }
      self.chunks[chunk_idx].modules = modules;
    }
  }

  /// Checks if merging `source_chunk` into `target_chunk` would create a circular dependency.
  ///
  /// Returns `true` if the merge would create a cycle, `false` if it's safe to merge.
  ///
  /// After merging, the combined chunk has deps = `source.deps ∪ target.deps`, and any chunk
  /// that previously depended on `source` now depends on `target`. A cycle exists iff `target`
  /// is reachable from `source.deps ∪ target.deps` in the resulting graph.
  ///
  /// We approximate this by doing a BFS from `source.deps ∪ target.deps`, treating any chunk
  /// that depends on `source` as also pointing to `target` (since after the merge they would).
  /// If `target` is reachable, the merge would create a cycle.
  ///
  /// This is similar to Rollup's check in `getAdditionalSizeIfNoTransitiveDependencyOrNonCorrelatedSideEffect`.
  pub fn would_create_circular_dependency(
    &self,
    source_chunk_idx: ChunkIdx,
    target_chunk_idx: ChunkIdx,
  ) -> bool {
    // Start BFS from the combined deps of source and target.
    let mut queue: VecDeque<ChunkIdx> = self.chunks[source_chunk_idx]
      .dependencies
      .iter()
      .chain(self.chunks[target_chunk_idx].dependencies.iter())
      .copied()
      .collect();
    let mut visited = FxHashSet::default();

    while let Some(chunk_idx) = queue.pop_front() {
      if chunk_idx == target_chunk_idx {
        return true;
      }
      if !visited.insert(chunk_idx) {
        continue;
      }
      for &dep in &self.chunks[chunk_idx].dependencies {
        if !visited.contains(&dep) {
          queue.push_back(dep);
        }
      }
      // Any chunk that depends on source will depend on target after the merge.
      // Simulate this by also queuing target when we encounter such a chunk.
      if self.chunks[chunk_idx].dependencies.contains(&source_chunk_idx) {
        queue.push_back(target_chunk_idx);
      }
    }

    false
  }

  /// Returns true if `from` can transitively reach `to` through the dependency graph.
  pub fn is_reachable(&self, from: ChunkIdx, to: ChunkIdx) -> bool {
    let mut queue: VecDeque<ChunkIdx> = self.chunks[from].dependencies.iter().copied().collect();
    let mut visited = FxHashSet::default();
    while let Some(chunk_idx) = queue.pop_front() {
      if chunk_idx == to {
        return true;
      }
      if !visited.insert(chunk_idx) {
        continue;
      }
      queue.extend(
        self.chunks[chunk_idx].dependencies.iter().copied().filter(|dep| !visited.contains(dep)),
      );
    }
    false
  }

  /// Merges the dependencies of source chunk into target chunk.
  ///
  /// When modules from one chunk are merged into another chunk, the dependencies
  /// of the source chunk should also be merged into the target chunk.
  /// Additionally, removes the target chunk from its own dependencies (self-reference).
  pub fn merge_chunk_dependencies(
    &mut self,
    target_chunk_idx: ChunkIdx,
    source_chunk_idx: ChunkIdx,
  ) {
    let source = &self.chunks[source_chunk_idx];
    let source_has_side_effects = source.has_side_effects;
    let source_dependencies = std::mem::take(&mut self.chunks[source_chunk_idx].dependencies);
    for dep_chunk_idx in source_dependencies {
      // Don't add self-reference
      if dep_chunk_idx != target_chunk_idx {
        self.chunks[target_chunk_idx].dependencies.insert(dep_chunk_idx);
      }
    }
    // Remove self-reference if it exists (source might have depended on target)
    self.chunks[target_chunk_idx].dependencies.remove(&target_chunk_idx);
    self.chunks[target_chunk_idx].has_side_effects |= source_has_side_effects;
  }
}

/// Result of assigning modules during chunk optimization.
enum ChunkAssignment {
  /// Modules were merged into an existing entry chunk (chunk_graph index).
  Merged(ChunkIdx),
  /// A new common chunk was created in chunk_graph (chunk_graph index).
  Created(ChunkIdx),
}

impl GenerateStage<'_> {
  /// Constructs a mapping from static entry chunks to the dynamic entry chunks they can reach.
  ///
  /// This is used to determine if a common module can be safely merged into an entry chunk
  /// when that entry chunk can reach all the dynamic entries that also need the module.
  fn construct_static_entry_to_reached_dynamic_entries_map(
    &self,
    chunk_graph: &ChunkGraph,
  ) -> FxHashMap<ChunkIdx, FxHashSet<ChunkIdx>> {
    let mut ret: FxHashMap<ChunkIdx, FxHashSet<ChunkIdx>> = FxHashMap::default();
    let dynamic_entry_modules = chunk_graph
      .chunk_table
      .iter_enumerated()
      .filter_map(|(idx, chunk)| match chunk.kind {
        ChunkKind::EntryPoint { meta, module, .. } => {
          (!meta.contains(ChunkMeta::UserDefinedEntry)).then_some((module, idx))
        }
        ChunkKind::Common => None,
      })
      .collect::<FxHashMap<ModuleIdx, ChunkIdx>>();
    for (&module_idx, _entry_points) in self
      .link_output
      .entries
      .iter()
      .filter(|(_, entries)| entries.iter().any(|e| e.kind.is_user_defined()))
    {
      let Some(entry_chunk_idx) = chunk_graph.module_to_chunk[module_idx] else {
        continue;
      };
      let mut q = VecDeque::from_iter([module_idx]);
      let mut visited = FxHashSet::default();
      while let Some(cur) = q.pop_front() {
        if visited.contains(&cur) {
          continue;
        }
        visited.insert(cur);
        let Module::Normal(module) = &self.link_output.module_table[cur] else {
          continue;
        };

        for dep_idx in module.import_records.iter().filter_map(|r| r.resolved_module) {
          // Can't put it at the beginning of the loop,
          if let Some(chunk_idx) = dynamic_entry_modules.get(&dep_idx) {
            ret.entry(entry_chunk_idx).or_default().insert(*chunk_idx);
          }
          q.push_back(dep_idx);
        }
      }
    }
    ret
  }

  /// Tries to insert common modules into existing entry chunks instead of creating new common chunks.
  ///
  /// This optimization reduces the total number of chunks by merging shared modules into an appropriate
  /// entry chunk when possible. The function iterates through pending common chunks and either:
  /// 1. Merges modules into an existing entry chunk if safe to do so
  /// 2. Creates a new common chunk if no suitable merge target is found
  pub(super) fn try_insert_common_module_to_exist_chunk(
    &self,
    chunk_graph: &mut ChunkGraph,
    bits_to_chunk: &mut FxHashMap<BitSet, ChunkIdx>,
    input_base: &ArcStr,
    temp_chunk_graph: &mut ChunkOptimizationGraph,
  ) {
    let static_entry_chunk_reference: FxHashMap<ChunkIdx, FxHashSet<ChunkIdx>> =
      self.construct_static_entry_to_reached_dynamic_entries_map(chunk_graph);

    // Calculate on demand to avoid add a new field on each NormalModule.
    let dynamic_entry_to_dynamic_importers: FxHashMap<ModuleIdx, FxHashSet<ModuleIdx>> = {
      // Get dynamic entry modules from chunk_table, then find matched entry points
      // and extract importers from related_stmt_infos
      let mut map: FxHashMap<ModuleIdx, FxHashSet<ModuleIdx>> = FxHashMap::default();
      for chunk in chunk_graph.chunk_table.iter() {
        let ChunkKind::EntryPoint { meta, module, .. } = chunk.kind else {
          continue;
        };
        if meta != ChunkMeta::DynamicImported {
          continue;
        }
        // Find the matched dynamic entry point from link_output.entries
        if let Some(entries) = self.link_output.entries.get(&module) {
          for entry in entries.iter().filter(|e| e.kind.is_dynamic_import()) {
            // Extract importers from related_stmt_infos (first element is the importer ModuleIdx)
            for (importer_idx, _, _, _) in &entry.related_stmt_infos {
              map.entry(module).or_default().insert(*importer_idx);
            }
          }
        }
      }
      map
    };
    // First pass: collect chunk assignment decisions
    // (bits, temp_chunk_idx, chunk_idxs, merge_target)
    let assignments: Vec<_> = temp_chunk_graph
      .bits_to_chunk_idx
      .iter()
      .filter_map(|(bits, temp_chunk_idx)| {
        let temp_chunk = &temp_chunk_graph.chunks[*temp_chunk_idx];
        // Skip those chunks that are already created in chunk graph
        if !temp_chunk.needs_creation {
          return None;
        }
        let chunk_idxs: Vec<_> = bits.index_of_one().map(ChunkIdx::from_raw).collect();

        let merge_target = self.try_insert_into_existing_chunk(
          &chunk_idxs,
          &static_entry_chunk_reference,
          chunk_graph,
          &dynamic_entry_to_dynamic_importers,
          temp_chunk,
          temp_chunk_graph,
        );

        Some((bits.clone(), *temp_chunk_idx, chunk_idxs, merge_target))
      })
      .collect();

    // Second pass: apply chunk assignments
    for (bits, temp_chunk_idx, chunk_idxs, merge_target) in assignments {
      // Check if merging would create a circular dependency
      let merge_target = match merge_target {
        Some(target_chunk_idx)
          if temp_chunk_graph
            .would_create_circular_dependency(temp_chunk_idx, target_chunk_idx) =>
        {
          // Skip merge if it would create a circular dependency
          None
        }
        other => other,
      };

      let temp_modules = &temp_chunk_graph.chunks[temp_chunk_idx].modules;
      match self.assign_modules_to_chunk(
        merge_target,
        &chunk_idxs,
        temp_modules,
        &bits,
        chunk_graph,
        bits_to_chunk,
        input_base,
      ) {
        ChunkAssignment::Merged(target_chunk_idx) => {
          // Merge chunk dependencies immediately after successful merge
          temp_chunk_graph.merge_chunk_dependencies(target_chunk_idx, temp_chunk_idx);
        }
        ChunkAssignment::Created(new_chunk_id) => {
          temp_chunk_graph.register_chunk_graph_index(new_chunk_id, temp_chunk_idx);
        }
      }
    }
  }

  /// Assigns modules to either an existing entry chunk or a new common chunk.
  ///
  /// If a valid merge target is found (and it doesn't have strict entry signature preservation),
  /// modules are merged into that existing chunk. Otherwise, a new common chunk is created.
  #[expect(clippy::too_many_arguments)]
  fn assign_modules_to_chunk(
    &self,
    merge_target: Option<ChunkIdx>,
    chunk_idxs: &[ChunkIdx],
    modules: &[ModuleIdx],
    bits: &BitSet,
    chunk_graph: &mut ChunkGraph,
    bits_to_chunk: &mut FxHashMap<BitSet, ChunkIdx>,
    input_base: &ArcStr,
  ) -> ChunkAssignment {
    match merge_target {
      Some(chunk_idx) => {
        let chunk = &chunk_graph.chunk_table[chunk_idx];
        let is_async_entry_only = matches!(chunk.kind, ChunkKind::EntryPoint { meta, .. } if meta == ChunkMeta::DynamicImported);
        if matches!(chunk.preserve_entry_signature, Some(PreserveEntrySignatures::Strict)) {
          // We can safely merge into this chunk in two scenarios:
          // 1. The target chunk is an async entry - dynamic chunks are not restricted by `PreserveEntrySignatures`.
          // 2. The target chunk has strict signature preservation, but the modules being merged won't alter
          //    the entry's exported interface (they either have no exports or only re-export existing entry symbols).
          if is_async_entry_only || self.can_merge_without_changing_entry_signature(chunk, modules)
          {
            self.merge_modules_into_existing_chunk(chunk_idx, chunk_idxs, modules, chunk_graph);
            ChunkAssignment::Merged(chunk_idx)
          } else {
            let new_chunk_id =
              self.create_common_chunk(modules, bits, chunk_graph, bits_to_chunk, input_base);
            ChunkAssignment::Created(new_chunk_id)
          }
        } else {
          self.merge_modules_into_existing_chunk(chunk_idx, chunk_idxs, modules, chunk_graph);
          ChunkAssignment::Merged(chunk_idx)
        }
      }
      _ => {
        let new_chunk_id =
          self.create_common_chunk(modules, bits, chunk_graph, bits_to_chunk, input_base);
        ChunkAssignment::Created(new_chunk_id)
      }
    }
  }

  /// Merges modules into an existing entry chunk.
  ///
  /// Also initializes imports_from_other_chunks entries for user-defined entry chunks
  /// that will reference the merged chunk, ensuring proper chunk ordering and execution.
  fn merge_modules_into_existing_chunk(
    &self,
    target_chunk_idx: ChunkIdx,
    chunk_idxs: &[ChunkIdx],
    modules: &[ModuleIdx],
    chunk_graph: &mut ChunkGraph,
  ) {
    for idx in chunk_idxs.iter().copied().filter(|idx| *idx != target_chunk_idx) {
      let Some(chunk) = chunk_graph.chunk_table.get_mut(idx) else {
        continue;
      };
      if !matches!(chunk.kind, ChunkKind::EntryPoint { meta, ..} if meta.contains(ChunkMeta::UserDefinedEntry))
      {
        continue;
      }
      chunk.imports_from_other_chunks.entry(target_chunk_idx).or_default();
    }

    for &module_idx in modules {
      chunk_graph.add_module_to_chunk(
        module_idx,
        target_chunk_idx,
        self.link_output.metas[module_idx].depended_runtime_helper,
      );
    }
  }

  /// Creates a new common chunk and assigns modules to it.
  /// Returns the new chunk_graph index.
  fn create_common_chunk(
    &self,
    modules: &[ModuleIdx],
    bits: &BitSet,
    chunk_graph: &mut ChunkGraph,
    bits_to_chunk: &mut FxHashMap<BitSet, ChunkIdx>,
    input_base: &ArcStr,
  ) -> ChunkIdx {
    let mut chunk =
      Chunk::new(None, None, bits.clone(), vec![], ChunkKind::Common, input_base.clone(), None);
    chunk.add_creation_reason(
      ChunkCreationReason::CommonChunk { bits, link_output: self.link_output },
      self.options,
    );
    let chunk_id = chunk_graph.add_chunk(chunk);
    for &module_idx in modules {
      chunk_graph.add_module_to_chunk(
        module_idx,
        chunk_id,
        self.link_output.metas[module_idx].depended_runtime_helper,
      );
    }
    bits_to_chunk.insert(bits.clone(), chunk_id);
    chunk_id
  }

  /// Attempts to find an existing entry chunk that can absorb modules shared between multiple entries.
  ///
  /// This optimization reduces the number of chunks by merging shared modules into an appropriate
  /// entry chunk when possible, rather than creating a separate common chunk.
  ///
  /// Returns `Some(ChunkIdx)` if a suitable merge target is found, `None` otherwise.
  fn try_insert_into_existing_chunk(
    &self,
    chunk_idxs: &[ChunkIdx],
    entry_chunk_reference: &FxHashMap<ChunkIdx, FxHashSet<ChunkIdx>>,
    chunk_graph: &ChunkGraph,
    dynamic_entry_to_dynamic_importers: &FxHashMap<ModuleIdx, FxHashSet<ModuleIdx>>,
    info: &ChunkCandidate,
    temp_chunk_graph: &ChunkOptimizationGraph,
  ) -> Option<ChunkIdx> {
    let module_table = &self.link_output.module_table;
    let linking_infos = &self.link_output.metas;
    let mut user_defined_entry = vec![];
    let mut dynamic_entry = vec![];
    for &idx in chunk_idxs {
      let Some(chunk) = chunk_graph.chunk_table.get(idx) else {
        continue;
      };
      match chunk.kind {
        ChunkKind::EntryPoint { meta, .. } => {
          if meta.intersects(ChunkMeta::UserDefinedEntry | ChunkMeta::EmittedChunk) {
            user_defined_entry.push(idx);
          } else {
            dynamic_entry.push(idx);
          }
        }
        ChunkKind::Common => return None,
      }
    }
    let user_defined_entry_modules = Self::collect_entry_modules(&user_defined_entry, chunk_graph)?;

    let merged_user_defined_chunk =
      Self::find_merge_target(&user_defined_entry, &user_defined_entry_modules, module_table);
    if user_defined_entry.is_empty() {
      let dynamic_chunk_entry_modules = Self::collect_entry_modules(&dynamic_entry, chunk_graph)?;
      let target_chunk_idx =
        Self::find_merge_target(&dynamic_entry, &dynamic_chunk_entry_modules, module_table)?;
      // Cyclic-merge export-pollution guard for `find_merge_target`.
      //
      // `find_merge_target` accepts any candidate whose entry module is
      // statically imported by every other entry module. In a dynamic-entry
      // static cycle, an exporting entry module can be merged into a
      // different dynamic-entry chunk. That chunk file is what
      // `import('./<target>.js')` resolves to at runtime, so the merged-in
      // entry's exports pollute the target dynamic-import namespace observed
      // by callers (issue #9320).
      //
      // See internal-docs/code-splitting/implementation.md for the merge-safety invariant.
      // The selected target may own its own exports, but no other pending
      // dynamic-entry module may contribute observable exports to the target
      // chunk's file-level export list.
      if dynamic_chunk_entry_modules.len() >= 2 {
        let target_entry_module =
          dynamic_entry.iter().zip(dynamic_chunk_entry_modules.iter()).find_map(
            |(chunk_idx, module_idx)| (*chunk_idx == target_chunk_idx).then_some(*module_idx),
          )?;
        let would_pollute_target_namespace = info.modules.iter().any(|&m| {
          m != target_entry_module
            && dynamic_chunk_entry_modules.contains(&m)
            && Self::module_has_observable_exports(m, module_table, linking_infos)
        });
        if would_pollute_target_namespace {
          return None;
        }
      }
      Some(target_chunk_idx)
    } else {
      let chunk_idx = merged_user_defined_chunk?;
      let chunk = &chunk_graph.chunk_table[chunk_idx];
      let reached_dynamic_chunk = entry_chunk_reference.get(&chunk_idx);
      // Check if all dynamic entry chunks are reachable from the merged user-defined entry chunk.
      // If not, we cannot merge the shared modules into the user-defined entry chunk because
      // the dynamic entries would not be able to access the shared modules.
      let all_dynamic_entries_reachable =
        dynamic_entry.iter().all(|idx| reached_dynamic_chunk.is_some_and(|set| set.contains(idx)));
      if !all_dynamic_entries_reachable {
        return None;
      }
      // If any dynamic entry chunk depends on the pending common chunk,
      // merging into the user-defined entry would make the dynamic entry
      // import the entry chunk. This is only safe if the entry chunk
      // has no side effects; otherwise loading the dynamic chunk would
      // trigger the entry's side effects unexpectedly.
      //
      // However, the leak only happens if the dynamic entry can be reached
      // via some *other* user-defined entry. If the dynamic entry is only
      // reachable from `chunk_idx` (the merge target), then loading the
      // dynamic entry always implies `chunk_idx` has already been loaded,
      // so its side effects have already run — no leak.
      // All dynamic entries in `dynamic_entry` already depend on `temp_chunk_idx`
      // (that's how they ended up in `chunk_idxs`), so we check them all directly.
      if temp_chunk_graph.chunks[chunk_idx].has_side_effects && !dynamic_entry.is_empty() {
        // Check whether any *other* user-defined entry can reach these
        // dynamic entries (directly or transitively via dynamic import).
        // If yes, merging would leak `chunk_idx`'s side effects into that
        // other entry's dynamic load.
        let leak_possible = entry_chunk_reference.iter().any(|(other_entry_chunk_idx, reached)| {
          *other_entry_chunk_idx != chunk_idx
            && dynamic_entry.iter().any(|dyn_idx| reached.contains(dyn_idx))
        });
        if leak_possible {
          return None;
        }
      }
      let modules_set = chunk
        .modules
        .iter()
        .copied()
        .chain(
          info
            .modules
            .iter()
            .filter(|idx| !dynamic_entry_to_dynamic_importers.contains_key(idx))
            .copied(),
        )
        .collect::<FxHashSet<_>>();
      // For each module in the shared modules, if it is a dynamic entry module,
      // all its dynamic importers must be in the current chunk (including modules to be merged).
      // This ensures that when a dynamic import occurs, the merged entry chunk (containing the
      // dynamic entry module) is already loaded, preventing missing module errors at runtime.
      let all_dynamic_entry_importers_valid = info.modules.iter().all(|&module_idx| {
        let Some(importers) = dynamic_entry_to_dynamic_importers.get(&module_idx) else {
          // Not a dynamic entry module, no constraint
          return true;
        };
        // all importer are in current chunk (includes those modules will be merged)
        importers.iter().all(|importer| modules_set.contains(importer))
      });
      all_dynamic_entry_importers_valid.then_some(chunk_idx)
    }
  }

  /// Collects entry module indices from a list of chunk indices.
  /// Returns `None` if any chunk is missing or has no entry module.
  fn collect_entry_modules(
    chunk_indices: &[ChunkIdx],
    chunk_graph: &ChunkGraph,
  ) -> Option<Vec<ModuleIdx>> {
    let mut ret = Vec::with_capacity(chunk_indices.len());
    for chunk_idx in chunk_indices {
      let chunk = chunk_graph.chunk_table.get(*chunk_idx)?;
      ret.push(chunk.entry_module_idx()?);
    }
    Some(ret)
  }

  /// Checks if merging the given modules into an entry chunk would change the entry's export signature.
  ///
  /// With `preserveEntrySignatures: 'strict'`, we need to ensure that merging modules doesn't add
  /// new exports to the entry chunk. A module is safe to merge if:
  /// 1. It has no exports of its own (purely internal implementation code), OR
  /// 2. All its exports are already part of the entry's resolved exports (re-exported by the entry)
  pub(super) fn can_merge_without_changing_entry_signature(
    &self,
    chunk: &Chunk,
    modules: &[ModuleIdx],
  ) -> bool {
    let Some(entry_module_idx) = chunk.entry_module_idx() else {
      return false;
    };
    let metas = &self.link_output.metas;
    let module_table = &self.link_output.module_table;

    let entry_exports = &metas[entry_module_idx].resolved_exports;

    modules.iter().all(|&module_idx| {
      // Skip the entry module itself - it's always safe
      if module_idx == entry_module_idx || module_table[module_idx].as_normal().is_none() {
        return true;
      }

      let module_meta = &metas[module_idx];

      // A module is safe to merge if all its exports are already covered by the entry's exports.
      // This means either:
      // 1. The module has no exports (empty resolved_exports)
      // 2. All of the module's exports point to symbols that the entry also exports
      module_meta.resolved_exports.iter().all(|(export_name, resolved_export)| {
        // Check if the entry has an export with the same name that resolves to the same symbol
        entry_exports
          .get(export_name)
          .is_some_and(|entry_export| entry_export.symbol_ref == resolved_export.symbol_ref)
      })
    })
  }

  /// Finds a chunk that can serve as the merge target for all entries.
  /// A chunk can be the merge target if its entry module is imported by or equal to all other entry modules.
  fn find_merge_target(
    chunk_indices: &[ChunkIdx],
    entry_modules: &[ModuleIdx],
    module_table: &ModuleTable,
  ) -> Option<ChunkIdx> {
    chunk_indices.iter().zip(entry_modules.iter()).find_map(|(chunk_idx, entry_module_idx)| {
      let module = module_table[*entry_module_idx].as_normal().expect("Should be normal module");
      let can_merge = entry_modules.iter().all(|other_entry_module_idx| {
        *entry_module_idx == *other_entry_module_idx
          || module.importers_idx.contains(other_entry_module_idx)
      });
      can_merge.then_some(*chunk_idx)
    })
  }

  /// Returns true when a module can contribute keys to a dynamic-import namespace.
  ///
  /// This intentionally uses linked export metadata instead of
  /// `NormalModule::named_exports`, because re-export-only modules can have no
  /// direct named exports while still exposing resolved exports through
  /// `export * from ...`.
  fn module_has_observable_exports(
    module_idx: ModuleIdx,
    module_table: &ModuleTable,
    linking_infos: &LinkingMetadataVec,
  ) -> bool {
    module_table[module_idx].as_normal().is_some()
      && (linking_infos[module_idx].has_dynamic_exports
        || linking_infos[module_idx].canonical_exports(false).next().is_some())
  }

  /// Finds empty dynamic entry chunks that should be merged with their target common chunks.
  /// Returns a tuple of (facade_eliminations, common_chunk_merges, emitted_chunk_groups).
  fn find_facade_chunk_merge_ops(
    &self,
    chunk_graph: &ChunkGraph,
    temp_chunk_opt_graph: &ChunkOptimizationGraph,
  ) -> (Vec<FacadeChunkElimination>, Vec<CommonChunkMerge>, FxHashMap<ChunkIdx, Vec<ChunkIdx>>) {
    let mut facade_eliminations = vec![];
    let mut common_chunk_merges = vec![];
    let mut emitted_chunk_groups: FxHashMap<ChunkIdx, Vec<ChunkIdx>> = FxHashMap::default();
    let temp_runtime_chunk_idx = chunk_graph.module_to_chunk[self.link_output.runtime.id()]
      .and_then(|idx| temp_chunk_opt_graph.to_temp_idx(idx));
    for (from_chunk_idx, chunk) in chunk_graph.chunk_table.iter_enumerated() {
      let ChunkKind::EntryPoint { meta, bit: _, module } = chunk.kind else {
        continue;
      };
      let is_emitted_entry_chunk = if meta.contains(ChunkMeta::EmittedChunk) {
        if matches!(chunk.preserve_entry_signature, Some(PreserveEntrySignatures::AllowExtension)) {
          true
        } else {
          // If an emitted chunk has other `preserveEntrySignatures` values, we can't optimize it.
          // The facade chunk needs to be preserved.
          continue;
        }
      } else {
        false
      };
      if !chunk.modules.is_empty() {
        continue;
      }

      let Some(target_chunk_idx) = chunk_graph.module_to_chunk[module] else {
        continue;
      };
      let target_chunk = &chunk_graph.chunk_table[target_chunk_idx];

      if meta.intersects(ChunkMeta::UserDefinedEntry) {
        if matches!(target_chunk.kind, ChunkKind::Common) {
          let can_merge = match chunk.preserve_entry_signature {
            Some(PreserveEntrySignatures::Strict) => {
              self.can_merge_without_changing_entry_signature(chunk, &target_chunk.modules)
            }
            _ => true,
          };
          if can_merge {
            // merge all common chunk modules into entry chunk
            // swap original from_chunk_idx and target_chunk_idx
            common_chunk_merges.push(CommonChunkMerge {
              from_chunk_idx: target_chunk_idx,
              to_chunk_idx: from_chunk_idx,
            });
          }
        }
        continue;
      }

      let is_target_manual_chunk = matches!(
        target_chunk.chunk_reason_type.as_ref(),
        ChunkReasonType::ManualCodeSplitting { .. }
      );
      let is_target_pure_user_entry_chunk = matches!(target_chunk.kind, ChunkKind::EntryPoint { meta, bit: _, module: _ } if meta.is_pure_user_defined_entry());
      let is_target_common_chunk = matches!(target_chunk.kind, ChunkKind::Common);
      // Five optimization scenarios (scenario 1 is handled above for user-defined entries):
      // 1. User-defined facade entry chunk merged with common chunk
      //    → Common chunk modules are moved into the entry chunk (handled above)
      // 2. Emitted chunk (AllowExtension) merged into manual code splitting group
      //    → Group by target chunk to detect export name conflicts before merging
      // 3. Dynamic entry chunk merged into manual code splitting group
      //    → Directly merge, the facade chunk can be removed
      // 4. Dynamic entry chunk merged into user-defined entry chunk
      //    → Directly merge, the facade chunk can be removed
      // 5. Dynamic entry chunk merged into common chunk
      //    → Directly merge, the facade chunk can be removed
      if is_target_manual_chunk && is_emitted_entry_chunk {
        emitted_chunk_groups.entry(target_chunk_idx).or_default().push(from_chunk_idx);
      } else if !is_emitted_entry_chunk {
        // Check if merging would create a circular dependency.
        // Translate chunk_graph indices to temp_chunk_graph indices, since the two
        // graphs may diverge once new common chunks are materialised.
        if temp_runtime_chunk_idx
          .and_then(|temp_runtime_idx| {
            let temp_target_idx = temp_chunk_opt_graph.to_temp_idx(target_chunk_idx)?;
            Some(temp_chunk_opt_graph.is_reachable(temp_runtime_idx, temp_target_idx))
          })
          // If runtime is not included before, it will not create circular dependency, because
          // the runtime module will be either included in the target chunk or in a separate chunk loaded before.
          // If either index has no temp counterpart, we conservatively allow the merge.
          .unwrap_or(false)
        {
          continue;
        }
        let reason = if is_target_manual_chunk {
          FacadeChunkEliminationReason::DynamicEntryMergedIntoManualGroup
        } else if is_target_pure_user_entry_chunk {
          FacadeChunkEliminationReason::DynamicEntryMergedIntoUserDefinedEntry
        } else if is_target_common_chunk {
          FacadeChunkEliminationReason::DynamicEntryMergedIntoCommonChunk
        } else {
          continue;
        };
        facade_eliminations.push(FacadeChunkElimination {
          reason,
          from_chunk_idx,
          to_chunk_idx: target_chunk_idx,
          entry_module_idx: module,
        });
      }
    }
    (facade_eliminations, common_chunk_merges, emitted_chunk_groups)
  }

  /// Batch process emitted chunk groups to detect export name conflicts.
  /// Chunks with conflicting export names need to keep their facade chunks.
  fn resolve_emitted_chunk_group_conflicts(
    &self,
    chunk_graph: &ChunkGraph,
    emitted_chunks_by_target: FxHashMap<ChunkIdx, Vec<ChunkIdx>>,
    facade_eliminations: &mut Vec<FacadeChunkElimination>,
  ) {
    for (target_chunk_idx, emitted_chunk_indices) in emitted_chunks_by_target {
      let Some(_target_chunk) = chunk_graph.chunk_table.get(target_chunk_idx) else {
        continue;
      };
      let mut emitted_entry_modules: Vec<(ChunkIdx, ModuleIdx)> = emitted_chunk_indices
        .iter()
        .filter_map(|item| {
          let chunk = chunk_graph.chunk_table.get(*item)?;
          let entry_module_idx = chunk.entry_module_idx()?;
          Some((*item, entry_module_idx))
        })
        .collect_vec();
      // Sort by reverse execution order to match the naming order in deconflict_chunk_symbol
      emitted_entry_modules.sort_by_cached_key(|(_idx, module_idx)| {
        std::cmp::Reverse(self.link_output.module_table[*module_idx].exec_order())
      });

      let mut allocated_export_by_name = FxHashMap::default();
      // If an emitted chunk exports a name that conflicts with an already allocated export,
      // its facade chunk cannot be removed.
      for (chunk_idx, entry_module_idx) in emitted_entry_modules {
        let module_meta = &self.link_output.metas[entry_module_idx];
        let needs_facade =
          module_meta.resolved_exports.iter().any(|(export_name, resolved_export)| {
            let canonical_ref =
              resolved_export.symbol_ref.canonical_ref(&self.link_output.symbol_db);
            match allocated_export_by_name.entry(export_name) {
              Entry::Occupied(occupied_entry) => canonical_ref != *occupied_entry.get(),
              Entry::Vacant(vacant_entry) => {
                vacant_entry.insert(canonical_ref);
                false
              }
            }
          });
        if !needs_facade {
          facade_eliminations.push(FacadeChunkElimination {
            reason: FacadeChunkEliminationReason::EmittedChunkMergedIntoManualGroup,
            entry_module_idx,
            from_chunk_idx: chunk_idx,
            to_chunk_idx: target_chunk_idx,
          });
        }
      }
    }
  }

  /// This optimization handles the case where a facade entry chunk (dynamic, emitted, or user-defined)
  /// has no modules of its own because all of its modules were moved to another chunk
  /// (e.g., a common chunk, manual code splitting group, or user-defined entry chunk).
  /// Instead of keeping an empty facade chunk, we rewrite references to point directly to the target chunk
  /// and ensure proper symbol inclusion.
  pub(super) fn optimize_facade_entry_chunks(
    &mut self,
    chunk_graph: &mut ChunkGraph,
    index_splitting_info: &IndexSplittingInfo,
    input_base: &ArcStr,
    module_is_assigned: &mut IndexBitSet<ModuleIdx>,
    temp_chunk_opt_graph: &ChunkOptimizationGraph,
  ) {
    // Find empty dynamic entry chunks that should be merged with their target common chunks
    let (mut facade_eliminations, common_chunk_merges, emitted_chunk_groups) =
      self.find_facade_chunk_merge_ops(chunk_graph, temp_chunk_opt_graph);

    if facade_eliminations.is_empty()
      && common_chunk_merges.is_empty()
      && emitted_chunk_groups.is_empty()
    {
      return;
    }

    let runtime_module_idx = self.link_output.runtime.id();
    self.resolve_emitted_chunk_group_conflicts(
      chunk_graph,
      emitted_chunk_groups,
      &mut facade_eliminations,
    );

    // Namespace symbols by default reference all exported symbols from the module.
    // To preserve dynamic import tree shaking, we should only include symbols that were actually used during the linking stage.
    // This ensures that including a namespace symbol doesn't inadvertently add unused exported symbols.
    for elimination in &facade_eliminations {
      let entry_module_idx = elimination.entry_module_idx;
      let wrap_kind = self.link_output.metas[entry_module_idx].wrap_kind();
      if self.link_output.module_table[entry_module_idx].as_normal().is_none() {
        continue;
      }
      // For CJS modules, we don't need to include `__exportAll` and the namespace symbols.
      // Instead, we should include the wrapper_ref (`require_xxx`), which will be handled
      // in the include_symbol call below.
      if !matches!(wrap_kind, WrapKind::Cjs) {
        // Filter in place to avoid cloning
        self.link_output.stmt_infos[entry_module_idx][StmtInfos::NAMESPACE_STMT_IDX]
          .referenced_symbols
          .retain(|item| match item {
            rolldown_common::SymbolOrMemberExprRef::Symbol(symbol_ref) => {
              // module namespace symbol requires `__exportAll` runtime helper
              self.link_output.used_symbol_refs.contains(symbol_ref)
                || symbol_ref.owner == runtime_module_idx
            }
            rolldown_common::SymbolOrMemberExprRef::MemberExpr(_member_expr_ref) => true,
          });
      }
    }

    let (mut stmt_info_included_vec, mut module_included_vec, mut module_namespace_reason_vec) =
      linking_metadata_vec_to_included_info(&mut self.link_output.metas);

    let runtime = &self.link_output.runtime;
    let context = &mut IncludeContext {
      modules: &self.link_output.module_table.modules,
      stmt_infos: &self.link_output.stmt_infos,
      symbols: &self.link_output.symbol_db,
      is_included_vec: &mut stmt_info_included_vec,
      is_module_included_vec: &mut module_included_vec,
      tree_shaking: self.options.treeshake.is_some(),
      runtime_idx: self.link_output.runtime.id(),
      metas: &self.link_output.metas,
      used_symbol_refs: &mut self.link_output.used_symbol_refs,
      constant_symbol_map: &self.link_output.global_constant_symbol_map,
      options: self.options,
      normal_symbol_exports_chain_map: &self.link_output.normal_symbol_exports_chain_map,
      bailout_cjs_tree_shaking_modules: FxHashSet::default(),
      module_inclusion_changed: false,
      module_namespace_included_reason: &mut module_namespace_reason_vec,
      inline_const_smart: self.options.optimization.is_inline_const_smart_mode(),
      json_module_none_self_reference_included_symbol: FxHashMap::default(),
    };

    let mut runtime_dependent_chunks = FxHashSet::default();

    let mut needs_export_all_helper = false;
    for elimination in &facade_eliminations {
      let FacadeChunkElimination { reason, entry_module_idx, from_chunk_idx, to_chunk_idx } =
        elimination;
      // Point the entry module to related common chunk
      chunk_graph.entry_module_to_entry_chunk.remove(entry_module_idx);

      let Some(module) = context.modules[*entry_module_idx].as_normal() else {
        continue;
      };

      let wrap_kind = self.link_output.metas[*entry_module_idx].wrap_kind();

      chunk_graph.entry_module_to_entry_chunk.insert(*entry_module_idx, *to_chunk_idx);
      let from_chunk = &chunk_graph.chunk_table[*from_chunk_idx];
      let ChunkKind::EntryPoint { meta: chunk_meta, .. } = from_chunk.kind else {
        // We don't have any optimization to merge common chunks into other chunks.
        continue;
      };

      chunk_graph.post_chunk_optimization_operations.insert(
        *from_chunk_idx,
        if chunk_meta.contains(ChunkMeta::EmittedChunk) {
          PostChunkOptimizationOperation::RemovedWithPreservedExports
        } else {
          PostChunkOptimizationOperation::Removed
        },
      );

      // Track emitted chunks so their export names are preserved (not minified)
      if chunk_meta.contains(ChunkMeta::EmittedChunk) {
        chunk_graph
          .common_chunk_preserve_export_names_modules
          .entry(*to_chunk_idx)
          .or_default()
          .insert(*entry_module_idx);
      }

      // If a chunk is not dynamically imported, we don't need to simulate a facade chunk.
      if !chunk_meta.contains(ChunkMeta::DynamicImported) {
        continue;
      }
      chunk_graph
        .common_chunk_exported_facade_chunk_namespace
        .entry(*to_chunk_idx)
        .or_default()
        .insert(*entry_module_idx);

      // Add debug info about eliminated facade chunk to target chunk
      if self.options.experimental.is_attach_debug_info_full() || self.options.devtools {
        let eliminated_chunk_name = chunk_graph.chunk_table[*from_chunk_idx]
          .name
          .as_ref()
          .map_or_else(|| "unnamed".to_string(), ArcStr::to_string);
        let module_stable_id = module.stable_id.to_string();
        chunk_graph.chunk_table[*to_chunk_idx].debug_info.push(
          ChunkDebugInfo::EliminatedFacadeChunk {
            chunk_name: eliminated_chunk_name,
            entry_module_id: module_stable_id,
            reason: *reason,
          },
        );
      }

      // For CJS modules, include the wrapper_ref (require_xxx) instead of namespace
      // and use ToEsm runtime helper instead of ExportAll
      if matches!(wrap_kind, WrapKind::Cjs | WrapKind::Esm) {
        if let Some(wrapper_ref) = self.link_output.metas[*entry_module_idx].wrapper_ref {
          include_symbol(context, wrapper_ref, SymbolIncludeReason::SimulatedFacadeChunk);
        }
        runtime_dependent_chunks.insert(*to_chunk_idx);
      }
      if matches!(wrap_kind, WrapKind::Esm | WrapKind::None) {
        include_symbol(
          context,
          module.namespace_object_ref,
          SymbolIncludeReason::SimulatedFacadeChunk,
        );
        context.module_namespace_included_reason[*entry_module_idx]
          .insert(ModuleNamespaceIncludedReason::SimulateFacadeChunk);
        let target_chunk = &mut chunk_graph.chunk_table[*to_chunk_idx];
        target_chunk.depended_runtime_helper.insert(RuntimeHelper::ExportAll);
        runtime_dependent_chunks.insert(*to_chunk_idx);
        needs_export_all_helper = true;
      }
    }

    Self::apply_common_chunk_merges(
      chunk_graph,
      &common_chunk_merges,
      &mut runtime_dependent_chunks,
    );

    if needs_export_all_helper {
      include_runtime_symbol(context, runtime, RuntimeHelper::ExportAll);
    }

    // Restore the included info before materializing the runtime chunk, because
    // facade elimination may be the first pass that includes a runtime helper.
    included_info_to_linking_metadata_vec(
      &mut self.link_output.metas,
      stmt_info_included_vec,
      &module_included_vec,
      &module_namespace_reason_vec,
    );
    self.extract_standalone_runtime_chunk(
      index_splitting_info,
      module_is_assigned,
      chunk_graph,
      input_base,
    );
    self.try_merge_runtime_chunk(chunk_graph, Some(&runtime_dependent_chunks));
  }

  /// Merge the standalone runtime chunk into a safe existing host. Prefer a
  /// live chunk with the runtime's bitset to preserve pre-existing layouts;
  /// otherwise use a runtime consumer that dominates all other consumers.
  pub(super) fn try_merge_runtime_chunk(
    &self,
    chunk_graph: &mut ChunkGraph,
    additional_runtime_consumers: Option<&FxHashSet<ChunkIdx>>,
  ) {
    let runtime_module_idx = self.link_output.runtime.id();
    let Some(runtime_chunk_idx) = chunk_graph.module_to_chunk[runtime_module_idx] else {
      return;
    };
    if chunk_graph.post_chunk_optimization_operations.contains_key(&runtime_chunk_idx) {
      return;
    }

    let module_table = &self.link_output.module_table;
    let Some(runtime_module) = module_table[runtime_module_idx].as_normal() else {
      return;
    };
    if Self::runtime_has_live_internal_import(runtime_module, runtime_chunk_idx, chunk_graph) {
      return;
    }

    let runtime_chunk = &chunk_graph.chunk_table[runtime_chunk_idx];
    if runtime_chunk.modules.len() != 1 || runtime_chunk.modules[0] != runtime_module_idx {
      return;
    }
    let runtime_chunk_bits = runtime_chunk.bits.clone();

    let consumer_chunks = self.collect_runtime_consumer_chunks(
      chunk_graph,
      runtime_chunk_idx,
      runtime_module_idx,
      additional_runtime_consumers,
    );
    let Some(target_chunk_idx) = self
      .find_single_runtime_consumer(&consumer_chunks)
      .or_else(|| {
        self.find_single_runtime_bitset_host(
          chunk_graph,
          runtime_chunk_idx,
          &runtime_chunk_bits,
          &consumer_chunks,
          module_table,
        )
      })
      .or_else(|| {
        self.find_runtime_bitset_host(
          chunk_graph,
          runtime_chunk_idx,
          &runtime_chunk_bits,
          &consumer_chunks,
          module_table,
        )
      })
      .or_else(|| {
        Self::find_consumer_dominator(&consumer_chunks, chunk_graph, module_table).filter(
          |&target_chunk_idx| {
            Self::runtime_merge_target_is_allowed(chunk_graph, target_chunk_idx)
              && self.runtime_target_is_tla_safe(chunk_graph, target_chunk_idx, &consumer_chunks)
              && self.runtime_merge_preserves_target_signature(
                chunk_graph,
                target_chunk_idx,
                &consumer_chunks,
              )
              && !Self::runtime_target_would_create_static_cycle(
                target_chunk_idx,
                &consumer_chunks,
                chunk_graph,
                module_table,
              )
          },
        )
      })
    else {
      return;
    };
    if target_chunk_idx == runtime_chunk_idx {
      return;
    }
    let runtime_chunk_helpers =
      std::mem::take(&mut chunk_graph.chunk_table[runtime_chunk_idx].depended_runtime_helper);
    chunk_graph.chunk_table[runtime_chunk_idx].modules.clear();
    chunk_graph.add_module_to_chunk(
      runtime_module_idx,
      target_chunk_idx,
      self.link_output.metas[runtime_module_idx].depended_runtime_helper,
    );
    let target_chunk = &mut chunk_graph.chunk_table[target_chunk_idx];
    target_chunk.depended_runtime_helper.insert(runtime_chunk_helpers);
    target_chunk.bits.union(&runtime_chunk_bits);
    chunk_graph
      .post_chunk_optimization_operations
      .insert(runtime_chunk_idx, PostChunkOptimizationOperation::Removed);
  }

  fn find_single_runtime_consumer(
    &self,
    consumer_chunks: &FxHashSet<ChunkIdx>,
  ) -> Option<ChunkIdx> {
    if consumer_chunks.len() != 1 {
      return None;
    }
    // A manual/advanced chunk is a valid runtime host when it is the only
    // runtime consumer. In that case the merge is local to the chunk that
    // already needs helpers, and does not make unrelated chunks load the group.
    consumer_chunks.iter().copied().next()
  }

  fn find_single_runtime_bitset_host(
    &self,
    chunk_graph: &ChunkGraph,
    runtime_chunk_idx: ChunkIdx,
    runtime_chunk_bits: &BitSet,
    consumer_chunks: &FxHashSet<ChunkIdx>,
    module_table: &ModuleTable,
  ) -> Option<ChunkIdx> {
    let mut hosts = chunk_graph.chunk_table.iter_enumerated().filter_map(|(chunk_idx, chunk)| {
      (chunk_idx != runtime_chunk_idx
        && !chunk.modules.is_empty()
        && !chunk_graph.post_chunk_optimization_operations.contains_key(&chunk_idx)
        && chunk.bits == *runtime_chunk_bits)
        .then_some(chunk_idx)
    });
    let host = hosts.next()?;
    (hosts.next().is_none()
      && Self::runtime_merge_target_is_allowed(chunk_graph, host)
      && self.runtime_target_is_tla_safe(chunk_graph, host, consumer_chunks)
      && self.runtime_merge_preserves_target_signature(chunk_graph, host, consumer_chunks)
      && !Self::runtime_target_would_create_static_cycle(
        host,
        consumer_chunks,
        chunk_graph,
        module_table,
      ))
    .then_some(host)
  }

  fn runtime_has_live_internal_import(
    runtime_module: &NormalModule,
    runtime_chunk_idx: ChunkIdx,
    chunk_graph: &ChunkGraph,
  ) -> bool {
    runtime_module
      .import_records
      .iter()
      .filter_map(|rec| chunk_graph.module_to_chunk[rec.resolved_module?])
      .any(|target_chunk_idx| {
        target_chunk_idx != runtime_chunk_idx
          && !chunk_graph.post_chunk_optimization_operations.contains_key(&target_chunk_idx)
      })
  }

  fn runtime_merge_target_is_allowed(chunk_graph: &ChunkGraph, target_chunk_idx: ChunkIdx) -> bool {
    !matches!(
      chunk_graph.chunk_table[target_chunk_idx].chunk_reason_type.as_ref(),
      ChunkReasonType::ManualCodeSplitting { .. }
    )
  }

  fn collect_runtime_consumer_chunks(
    &self,
    chunk_graph: &ChunkGraph,
    runtime_chunk_idx: ChunkIdx,
    runtime_module_idx: ModuleIdx,
    additional_runtime_consumers: Option<&FxHashSet<ChunkIdx>>,
  ) -> FxHashSet<ChunkIdx> {
    let mut consumers = chunk_graph
      .chunk_table
      .iter_enumerated()
      .filter_map(|(chunk_idx, chunk)| {
        if chunk_idx == runtime_chunk_idx
          || chunk_graph.post_chunk_optimization_operations.contains_key(&chunk_idx)
        {
          return None;
        }

        // Cheap-first ordering: the per-module checks below are O(modules) with
        // hashset lookups, whereas `chunk_references_runtime_symbol` resolves a
        // canonical ref per referenced symbol. `||` short-circuits, so keeping the
        // scan last skips it whenever a cheaper signal already proves consumption.
        let consumes_runtime = !chunk.depended_runtime_helper.is_empty()
          || chunk.modules.iter().any(|&module_idx| {
            module_idx != runtime_module_idx
              && (self.link_output.metas[module_idx].wrapper_ref.is_some()
                || self.link_output.metas[module_idx].dependencies.contains(&runtime_module_idx)
                || self.link_output.metas[module_idx].has_side_effectful_runtime_dep)
          })
          || self.chunk_references_runtime_symbol(chunk, runtime_module_idx);
        consumes_runtime.then_some(chunk_idx)
      })
      .collect::<FxHashSet<_>>();
    if let Some(additional_runtime_consumers) = additional_runtime_consumers {
      consumers.extend(additional_runtime_consumers.iter().copied().filter(|&chunk_idx| {
        chunk_idx != runtime_chunk_idx
          && !chunk_graph.post_chunk_optimization_operations.contains_key(&chunk_idx)
      }));
    }
    consumers
  }

  fn chunk_references_runtime_symbol(&self, chunk: &Chunk, runtime_module_idx: ModuleIdx) -> bool {
    chunk.modules.iter().copied().any(|module_idx| {
      if module_idx == runtime_module_idx {
        return false;
      }
      let meta = &self.link_output.metas[module_idx];
      if meta.referenced_symbols_by_entry_point_chunk.iter().any(|(symbol_ref, _)| {
        self.link_output.symbol_db.canonical_ref_resolving_namespace(*symbol_ref).owner
          == runtime_module_idx
      }) {
        return true;
      }
      self.link_output.stmt_infos[module_idx].iter_enumerated().any(|(stmt_info_idx, stmt_info)| {
        if !meta.stmt_info_included.has_bit(stmt_info_idx) {
          return false;
        }
        stmt_info.referenced_symbols.iter().any(|reference_ref| match reference_ref {
          rolldown_common::SymbolOrMemberExprRef::Symbol(symbol_ref) => {
            self.link_output.symbol_db.canonical_ref_resolving_namespace(*symbol_ref).owner
              == runtime_module_idx
          }
          rolldown_common::SymbolOrMemberExprRef::MemberExpr(member_expr_ref) => member_expr_ref
            .represent_symbol_ref(&meta.resolved_member_expr_refs)
            .is_some_and(|symbol_ref| {
              self.link_output.symbol_db.canonical_ref_resolving_namespace(symbol_ref).owner
                == runtime_module_idx
            }),
        })
      })
    })
  }

  fn find_runtime_bitset_host(
    &self,
    chunk_graph: &ChunkGraph,
    runtime_chunk_idx: ChunkIdx,
    runtime_chunk_bits: &BitSet,
    consumer_chunks: &FxHashSet<ChunkIdx>,
    module_table: &ModuleTable,
  ) -> Option<ChunkIdx> {
    chunk_graph.chunk_table.iter_enumerated().find_map(|(chunk_idx, chunk)| {
      if chunk_idx == runtime_chunk_idx
        || chunk.modules.is_empty()
        || chunk_graph.post_chunk_optimization_operations.contains_key(&chunk_idx)
        || !matches!(chunk.kind, ChunkKind::Common)
        || chunk.bits != *runtime_chunk_bits
        || !Self::runtime_merge_target_is_allowed(chunk_graph, chunk_idx)
      {
        return None;
      }

      (!Self::runtime_target_would_create_static_cycle(
        chunk_idx,
        consumer_chunks,
        chunk_graph,
        module_table,
      ) && self.runtime_target_is_tla_safe(chunk_graph, chunk_idx, consumer_chunks))
      .then_some(chunk_idx)
    })
  }

  fn chunk_has_tla_or_tla_dependency(&self, chunk_graph: &ChunkGraph, chunk_idx: ChunkIdx) -> bool {
    chunk_graph.chunk_table[chunk_idx]
      .modules
      .iter()
      .any(|&module_idx| self.link_output.metas[module_idx].is_tla_or_contains_tla_dependency)
  }

  fn runtime_target_is_tla_safe(
    &self,
    chunk_graph: &ChunkGraph,
    target_chunk_idx: ChunkIdx,
    consumer_chunks: &FxHashSet<ChunkIdx>,
  ) -> bool {
    !self.chunk_has_tla_or_tla_dependency(chunk_graph, target_chunk_idx)
      || (consumer_chunks.len() == 1 && consumer_chunks.contains(&target_chunk_idx))
  }

  /// Whether folding the runtime module into `target_chunk_idx` keeps the
  /// target's public export signature intact.
  ///
  /// When another consumer reads helpers from the host cross-chunk, the host has
  /// to *export* those helper symbols. On a user-defined entry whose
  /// `preserveEntrySignatures` fixes its surface, that leaks internal helper
  /// names onto the declared signature, so the runtime must not merge there.
  ///
  /// Safe when any of:
  /// - the target is the only consumer (helpers stay internal — no re-export),
  /// - the target is not a user-defined entry (a common chunk has no signature),
  /// - `preserveEntrySignatures` allows extra exports: `allow-extension`/`false`,
  ///   or `exports-only` for an entry that declares no exports.
  fn runtime_merge_preserves_target_signature(
    &self,
    chunk_graph: &ChunkGraph,
    target_chunk_idx: ChunkIdx,
    consumer_chunks: &FxHashSet<ChunkIdx>,
  ) -> bool {
    // Helpers are only re-exported when a consumer *other than* the host reads
    // them. If every consumer is the host itself, they stay internal.
    if consumer_chunks.iter().all(|&chunk_idx| chunk_idx == target_chunk_idx) {
      return true;
    }
    let target_chunk = &chunk_graph.chunk_table[target_chunk_idx];
    let ChunkKind::EntryPoint { meta, module: entry_module_idx, .. } = target_chunk.kind else {
      return true;
    };
    if !meta.contains(ChunkMeta::UserDefinedEntry) {
      return true;
    }
    match target_chunk.preserve_entry_signature {
      Some(PreserveEntrySignatures::AllowExtension | PreserveEntrySignatures::False) => true,
      Some(PreserveEntrySignatures::Strict) => false,
      // `exports-only` (the default) only fixes the signature of entries that
      // actually declare exports; an export-less entry behaves like `false`.
      Some(PreserveEntrySignatures::ExportsOnly) | None => {
        self.link_output.metas[entry_module_idx].resolved_exports.is_empty()
      }
    }
  }

  fn runtime_target_would_create_static_cycle(
    target_chunk_idx: ChunkIdx,
    consumer_chunks: &FxHashSet<ChunkIdx>,
    chunk_graph: &ChunkGraph,
    module_table: &ModuleTable,
  ) -> bool {
    consumer_chunks.iter().any(|&consumer_chunk_idx| {
      consumer_chunk_idx != target_chunk_idx
        && Self::chunk_reaches_via_static_import(
          target_chunk_idx,
          consumer_chunk_idx,
          chunk_graph,
          module_table,
        )
    })
  }

  /// Return the unique `consumers` member that every other member reaches
  /// through static forward chunk-loading edges (not dynamic imports). Such a
  /// chunk is a "downstream sink" of the consumer set — placing the runtime
  /// there adds no new back-edges, so no cycle can form. Returns `None` when
  /// the set has no dominator (e.g. consumers sit in parallel sub-graphs).
  fn find_consumer_dominator(
    consumers: &FxHashSet<ChunkIdx>,
    chunk_graph: &ChunkGraph,
    module_table: &ModuleTable,
  ) -> Option<ChunkIdx> {
    if consumers.len() <= 1 {
      return consumers.iter().copied().next();
    }
    consumers.iter().copied().find(|&candidate| {
      consumers.iter().all(|&other| {
        other == candidate
          || Self::chunk_reaches_via_static_import(other, candidate, chunk_graph, module_table)
      })
    })
  }

  /// BFS from `from` across chunks, following only import records that become
  /// static chunk-loading edges through still-live target chunks.
  ///
  /// Edge filtering rationale:
  /// - Dynamic imports are not followed. Static imports and `require()` can
  ///   both become static chunk imports in the generated output, so both can
  ///   close the helper-import cycle this check is guarding against.
  /// - Targets present in `post_chunk_optimization_operations` are skipped.
  ///   Those chunks are already slated for removal/redirection by the
  ///   surrounding facade-elimination pass, so their edges aren't part of the
  ///   post-optimization graph.
  /// - Self-edges (`target_chunk == current`) are skipped — an intra-chunk
  ///   import can't form an inter-chunk cycle.
  fn chunk_reaches_via_static_import(
    from: ChunkIdx,
    to: ChunkIdx,
    chunk_graph: &ChunkGraph,
    module_table: &ModuleTable,
  ) -> bool {
    let mut visited = FxHashSet::default();
    let mut queue = VecDeque::from([from]);
    while let Some(current) = queue.pop_front() {
      if !visited.insert(current) {
        continue;
      }
      if current == to {
        return true;
      }
      for &module_idx in &chunk_graph.chunk_table[current].modules {
        let Some(module) = module_table[module_idx].as_normal() else {
          continue;
        };
        queue.extend(
          module
            .import_records
            .iter()
            .filter(|rec| rec.kind.is_static())
            .filter_map(|rec| chunk_graph.module_to_chunk[rec.resolved_module?])
            .filter(|&target_chunk| {
              target_chunk != current
                && !chunk_graph.post_chunk_optimization_operations.contains_key(&target_chunk)
            }),
        );
      }
    }
    false
  }

  /// Move modules from common chunks into facade entry chunks, then retarget
  /// all chunk-graph references that still point at the removed common chunk.
  fn apply_common_chunk_merges(
    chunk_graph: &mut ChunkGraph,
    common_chunk_merges: &[CommonChunkMerge],
    runtime_dependent_chunks: &mut FxHashSet<ChunkIdx>,
  ) {
    for merge in common_chunk_merges {
      let from_chunk = &mut chunk_graph.chunk_table[merge.from_chunk_idx];
      let from_chunk_modules = std::mem::take(&mut from_chunk.modules);
      let from_chunk_debug_info = std::mem::take(&mut from_chunk.debug_info);
      let from_chunk_runtime_helper = std::mem::take(&mut from_chunk.depended_runtime_helper);
      for module_idx in &from_chunk_modules {
        chunk_graph.module_to_chunk[*module_idx] = Some(merge.to_chunk_idx);
      }
      let to_chunk = &mut chunk_graph.chunk_table[merge.to_chunk_idx];
      to_chunk.modules.extend(from_chunk_modules);
      to_chunk.debug_info.extend(from_chunk_debug_info);
      to_chunk.depended_runtime_helper.insert(from_chunk_runtime_helper);
      chunk_graph
        .post_chunk_optimization_operations
        .insert(merge.from_chunk_idx, PostChunkOptimizationOperation::Removed);

      // Retarget entry mappings that point at the removed chunk
      for entry_chunk_idx in chunk_graph.entry_module_to_entry_chunk.values_mut() {
        if *entry_chunk_idx == merge.from_chunk_idx {
          *entry_chunk_idx = merge.to_chunk_idx;
        }
      }

      // Retarget facade chunk namespace exports
      if let Some(modules) =
        chunk_graph.common_chunk_exported_facade_chunk_namespace.remove(&merge.from_chunk_idx)
      {
        chunk_graph
          .common_chunk_exported_facade_chunk_namespace
          .entry(merge.to_chunk_idx)
          .or_default()
          .extend(modules);
      }

      // Retarget preserved export names
      if let Some(modules) =
        chunk_graph.common_chunk_preserve_export_names_modules.remove(&merge.from_chunk_idx)
      {
        chunk_graph
          .common_chunk_preserve_export_names_modules
          .entry(merge.to_chunk_idx)
          .or_default()
          .extend(modules);
      }

      // Retarget runtime_dependent_chunks so runtime is placed in the correct chunk
      if runtime_dependent_chunks.remove(&merge.from_chunk_idx) {
        runtime_dependent_chunks.insert(merge.to_chunk_idx);
      }
    }
  }
}
