///|
pub enum FunctionPathBetweenStatus {
  BetweenPathFound
  BetweenPathNotFound
} derive(Eq)

///|
pub struct FunctionPathBetweenReport {
  source : FunctionPathNode
  target_index : Int
  file_size : Int
  sha256 : String
  function_count : Int
  status : FunctionPathBetweenStatus
  path_nodes : Array[FunctionPathNode]
  path_edges : Array[CallEdge]
  hop_count : Int?
  prepared_closure_function_count : Int
  graph_complete : Bool
  unresolved_count : Int
  diagnostics : Array[GraphDiagnostic]
  graph_warning : String
  modeling_scope : String
}

///|
pub enum FunctionPathBetweenError {
  BetweenDependenciesError(FunctionDependenciesError)
  InvalidBetweenTarget(Int, Int)
  /// Optional caller, callee and file offset belong to actual edge evidence only.
  InvalidBetweenEvidence(Int?, Int?, Int?, String)
}

///|
pub fn describe_function_path_between_error(
  error : FunctionPathBetweenError,
) -> String {
  match error {
    BetweenDependenciesError(error) =>
      describe_function_dependencies_error(error)
    InvalidBetweenTarget(index, count) =>
      "path destination function index \{index} is outside the function index space of \{count}"
    InvalidBetweenEvidence(caller, callee, offset, reason) =>
      "invalid function-to-function path evidence" +
      (match (caller, callee, offset) {
        (Some(a), Some(b), Some(n)) => " for \{a} -> \{b} at file offset \{n}"
        _ => " (no edge position available)"
      }) +
      ": " +
      reason
  }
}

///|
fn between_scope() -> String {
  "One deterministic minimum-hop FROM -> TO path in the current reference model, allowing any valid full indices, including imports; the source need not be a model root or root-reachable. " +
  "Direct means a recorded call dependency; Reference is a reference taken by ref.func; Indirect is a static candidate, not a known execution. Edges keep original caller -> callee direction. CallEdge has no opcode: call/return_call are not guessed; inspect file offsets separately with --locate-offset --locate-decode. " +
  "This prepares the complete original function_dependencies closure exactly once and reconstructs its deterministic first-discovery predecessors; it does not stop at the destination, run another BFS, decode instructions or apply why-index resource limits. Preparation and evidence memory depend on the complete closure and graph. " +
  "Zero hops only means identical source/destination, not a nonempty cycle. Not found means no recorded path in this direction within the current model, not proof of no dependency or host behavior. A missing destination identity is not fabricated. " +
  "Diagnostics and graph_complete describe the entire module, including sites outside this path. No path byte sum, retained, execution frequency, stack depth, recompilation or gzip/deletion savings are computed. " +
  graph_modeling_scope()
}

///|
/// Reconstruct only. One node map and edge-tuple set, no graph build or search.
/// The node-count bound and strictly decreasing depths reject partial/bad chains.
fn between_reconstruct(
  source_node : FunctionPathNode,
  function_count : Int,
  prepared_nodes : Array[FunctionDependencyNode],
  prepared_edges : Array[CallEdge],
  target_index : Int,
) -> Result[
  (FunctionPathBetweenStatus, Array[FunctionPathNode], Array[CallEdge], Int?),
  FunctionPathBetweenError,
] {
  if target_index < 0 || target_index >= function_count {
    return Err(InvalidBetweenTarget(target_index, function_count))
  }
  let indexed : Map[Int, FunctionDependencyNode] = Map([])
  for entry in prepared_nodes {
    if indexed.contains(entry.node.index) {
      return Err(
        InvalidBetweenEvidence(
          None,
          None,
          None,
          "duplicate closure node \{entry.node.index}",
        ),
      )
    }
    indexed.set(entry.node.index, entry)
  }
  let source = match indexed.get(source_node.index) {
    Some(entry) => entry
    None =>
      return Err(
        InvalidBetweenEvidence(
          None,
          None,
          None,
          "source absent from prepared closure",
        ),
      )
  }
  if source.depth != 0 || source.predecessor is Some(_) {
    return Err(
      InvalidBetweenEvidence(
        None,
        None,
        None,
        "source must have depth zero and no predecessor",
      ),
    )
  }
  let destination = match indexed.get(target_index) {
    Some(entry) => entry
    None => return Ok((BetweenPathNotFound, [], [], None))
  }
  if destination.depth < 0 || destination.depth >= prepared_nodes.length() {
    return Err(
      InvalidBetweenEvidence(
        None,
        None,
        None,
        "destination depth outside prepared node bound",
      ),
    )
  }
  let evidence : Set[(Int, Int, Int, Int)] = Set([])
  for edge in prepared_edges {
    evidence.add(
      (edge.caller, edge.callee, path_edge_rank(edge.kind), edge.offset),
    )
  }
  let backwards_nodes : Array[FunctionPathNode] = []
  let backwards_edges : Array[CallEdge] = []
  let mut current = destination
  // At most the prepared node count. No recursive paths or second BFS.
  for _ in 0.. edge
      None =>
        return Err(
          InvalidBetweenEvidence(
            None,
            None,
            None,
            "missing predecessor at function \{current.node.index}",
          ),
        )
    }
    if edge.callee != current.node.index {
      return Err(
        InvalidBetweenEvidence(
          Some(edge.caller),
          Some(edge.callee),
          Some(edge.offset),
          "predecessor callee differs from current node",
        ),
      )
    }
    if !evidence.contains(
        (edge.caller, edge.callee, path_edge_rank(edge.kind), edge.offset),
      ) {
      return Err(
        InvalidBetweenEvidence(
          Some(edge.caller),
          Some(edge.callee),
          Some(edge.offset),
          "predecessor not present in complete closure evidence",
        ),
      )
    }
    let parent = match indexed.get(edge.caller) {
      Some(entry) => entry
      None =>
        return Err(
          InvalidBetweenEvidence(
            Some(edge.caller),
            Some(edge.callee),
            Some(edge.offset),
            "predecessor caller absent from prepared closure",
          ),
        )
    }
    if current.depth <= 0 || parent.depth != current.depth - 1 {
      return Err(
        InvalidBetweenEvidence(
          Some(edge.caller),
          Some(edge.callee),
          Some(edge.offset),
          "predecessor depth does not decrease by one",
        ),
      )
    }
    backwards_edges.push(edge)
    current = parent
  }
  Err(
    InvalidBetweenEvidence(
      None,
      None,
      None,
      "path failed to reach source within prepared node bound",
    ),
  )
}

///|
/// Exactly one dependencies query. No second parse, build, BFS or opcode lookup.
pub fn function_path_between(
  data : Bytes,
  source_index : Int,
  target_index : Int,
) -> Result[FunctionPathBetweenReport, FunctionPathBetweenError] {
  let closure = match function_dependencies(data, source_index) {
    Ok(report) => report
    Err(error) => return Err(BetweenDependenciesError(error))
  }
  let (status, path_nodes, path_edges, hop_count) = match
    between_reconstruct(
      closure.target,
      closure.function_count,
      closure.nodes,
      closure.edges,
      target_index,
    ) {
    Ok(result) => result
    Err(error) => return Err(error)
  }
  Ok({
    source: closure.target,
    target_index,
    file_size: closure.file_size,
    sha256: closure.sha256,
    function_count: closure.function_count,
    status,
    path_nodes,
    path_edges,
    hop_count,
    prepared_closure_function_count: closure.nodes.length(),
    graph_complete: closure.graph_complete,
    unresolved_count: closure.unresolved_count,
    diagnostics: closure.diagnostics,
    graph_warning: closure.graph_warning,
    modeling_scope: between_scope(),
  })
}