///|
pub struct DependencyOverlapGroup {
  /// shared, left_only, right_only or neither; relative to these two starts only.
  kind : String
  indices : Array[Int]
  function_count : Int
  local_count : Int
  imported_count : Int
  local_encoded_bytes : Int
}

///|
pub struct DependencyOverlapReport {
  file_size : Int
  sha256 : String
  left_index : Int
  right_index : Int
  functions : Array[FunctionPathNode]
  left_reachable : Array[Int]
  right_reachable : Array[Int]
  left_edges : Array[CallEdge]
  right_edges : Array[CallEdge]
  groups : Array[DependencyOverlapGroup]
  function_count : Int
  local_function_count : Int
  imported_function_count : Int
  local_encoded_bytes : Int
  left_function_count : Int
  left_local_count : Int
  left_imported_count : Int
  left_local_encoded_bytes : Int
  right_function_count : Int
  right_local_count : Int
  right_imported_count : Int
  right_local_encoded_bytes : Int
  union_function_count : Int
  union_local_count : Int
  union_imported_count : Int
  union_local_encoded_bytes : Int
  graph_complete : Bool
  unresolved_count : Int
  diagnostics : Array[GraphDiagnostic]
  graph_warning : String
  modeling_scope : String
}

///|
pub enum DependencyOverlapError {
  OverlapInputError(WasmError)
  InvalidOverlapTarget(String, Int, Int)
  OverlapEvidenceError(FunctionDependenciesError)
}

///|
pub fn describe_dependency_overlap_error(
  error : DependencyOverlapError,
) -> String {
  match error {
    OverlapInputError(error) => describe_error(error)
    InvalidOverlapTarget(side, index, count) =>
      "dependency comparison \{side} function index \{index} is outside the function index space of \{count}"
    OverlapEvidenceError(error) =>
      "dependency comparison evidence: " +
      describe_function_dependencies_error(error)
  }
}

///|
fn overlap_scope() -> String {
  "Four disjoint partitions of the full function index space for two outgoing reference-model closures in this single input. Each closure includes its own start, and starts may themselves be shared. Any valid local/imported index is allowed, regardless of export or model-root reachability. " +
  "shared is intersection; left_only/right_only are relative only to these two starts, not global exclusivity. A third export, start or element root may still reach a single-side member. neither is not dead code. Shared does not mean simultaneous execution or entrypoint attribution. " +
  "One inventory and one graph build, at most two existing closure BFS calls; identical starts reuse one result. Each helper may validate and sort graph evidence again. This is not a parse-once or prepare-everything-once claim. Complete left/right edges preserve parallel sites, kinds, candidates and loops; their counts cannot be added as unique call sites. " +
  "Unique local bytes use total_size including body length prefixes, excluding code header/count and data/type/name/other sections. Imported encoded_size remains null, with no body contribution in this file, not a measured zero-cost host. No extra left-plus-right byte sum, retained, deduplication, splitting, removal, recompilation or gzip savings are computed. " +
  "Direct, ref.func references taken and static indirect candidates keep their model meanings, not actual execution. Diagnostics and graph_complete cover the whole module, even outside both closures; unresolved_count retains its table-site meaning. Top limits each of the first three text tables only, not traversal, full indices/edges or JSON; neither has summary only in text. " +
  graph_modeling_scope()
}

///|
/// One inventory scan. Indices, not names or byte fingerprints, define identity.
fn overlap_partition(
  inventory : Array[FunctionPathNode],
  left : Array[Int],
  right : Array[Int],
) -> Array[DependencyOverlapGroup] {
  let a : Set[Int] = Set(left)
  let b : Set[Int] = Set(right)
  let members : Array[Array[Int]] = Array::makei(4, fn(_) { [] })
  let locals = Array::make(4, 0)
  let imports = Array::make(4, 0)
  let sizes = Array::make(4, 0)
  for node in inventory {
    let group = if a.contains(node.index) {
      if b.contains(node.index) {
        0
      } else {
        1
      }
    } else if b.contains(node.index) {
      2
    } else {
      3
    }
    members[group].push(node.index)
    if node.imported {
      imports[group] += 1
    } else {
      locals[group] += 1
      sizes[group] += node.encoded_size.unwrap_or(0)
    }
  }
  let kinds = ["shared", "left_only", "right_only", "neither"]
  Array::makei(4, fn(i) {
    members[i].sort()
    DependencyOverlapGroup::{
      kind: kinds[i],
      indices: members[i],
      function_count: members[i].length(),
      local_count: locals[i],
      imported_count: imports[i],
      local_encoded_bytes: sizes[i],
    }
  })
}

///|
/// One build; different starts invoke the old helper twice, equal starts once.
pub fn compare_function_dependencies(
  data : Bytes,
  left_index : Int,
  right_index : Int,
) -> Result[DependencyOverlapReport, DependencyOverlapError] {
  let parsed = match parse_module(data) {
    Ok(value) => value
    Err(error) => return Err(OverlapInputError(error))
  }
  let inventory = match path_inventory(data, parsed) {
    Ok(value) => value
    Err(error) => return Err(OverlapInputError(error))
  }
  for pair in [("left", left_index), ("right", right_index)] {
    let (side, index) = pair
    if index < 0 || index >= inventory.length() {
      return Err(InvalidOverlapTarget(side, index, inventory.length()))
    }
  }
  let graph = match build(data) {
    Ok(value) => value
    Err(error) => return Err(OverlapInputError(error))
  }
  let (left_nodes, left_edges) = match
    dependencies_closure(inventory, graph.edges, left_index, data.length()) {
    Ok(value) => value
    Err(error) => return Err(OverlapEvidenceError(error))
  }
  let (right_nodes, right_edges) = if left_index == right_index {
    (left_nodes, left_edges)
  } else {
    match
      dependencies_closure(inventory, graph.edges, right_index, data.length()) {
      Ok(value) => value
      Err(error) => return Err(OverlapEvidenceError(error))
    }
  }
  let left_reachable = left_nodes.map(fn(n) { n.node.index })
  let right_reachable = right_nodes.map(fn(n) { n.node.index })
  left_reachable.sort()
  right_reachable.sort()
  let groups = overlap_partition(inventory, left_reachable, right_reachable)
  let shared = groups[0]
  let left = groups[1]
  let right = groups[2]
  let neither = groups[3]
  let union_count = shared.function_count +
    left.function_count +
    right.function_count
  let union_locals = shared.local_count + left.local_count + right.local_count
  let union_imports = shared.imported_count +
    left.imported_count +
    right.imported_count
  let union_bytes = shared.local_encoded_bytes +
    left.local_encoded_bytes +
    right.local_encoded_bytes
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    left_index,
    right_index,
    functions: inventory,
    left_reachable,
    right_reachable,
    left_edges,
    right_edges,
    groups,
    function_count: inventory.length(),
    local_function_count: union_locals + neither.local_count,
    imported_function_count: union_imports + neither.imported_count,
    local_encoded_bytes: union_bytes + neither.local_encoded_bytes,
    left_function_count: shared.function_count + left.function_count,
    left_local_count: shared.local_count + left.local_count,
    left_imported_count: shared.imported_count + left.imported_count,
    left_local_encoded_bytes: shared.local_encoded_bytes +
    left.local_encoded_bytes,
    right_function_count: shared.function_count + right.function_count,
    right_local_count: shared.local_count + right.local_count,
    right_imported_count: shared.imported_count + right.imported_count,
    right_local_encoded_bytes: shared.local_encoded_bytes +
    right.local_encoded_bytes,
    union_function_count: union_count,
    union_local_count: union_locals,
    union_imported_count: union_imports,
    union_local_encoded_bytes: union_bytes,
    graph_complete: is_graph_complete(graph),
    unresolved_count: graph.unresolved_indirect.length(),
    diagnostics: sorted_diagnostics(graph),
    graph_warning: render_graph_warning(graph),
    modeling_scope: overlap_scope(),
  })
}