///|
/// Call instruction categories, independent of call-graph EdgeKind.
pub enum FunctionCallKind {
  DirectCall
  IndirectCall
  ReferenceCall
} derive(Eq, Debug)

///|
pub struct FunctionCallRecord {
  instruction_ordinal : Int
  opcode : Opcode
  kind : FunctionCallKind
  is_tail : Bool
  file_offset : Int
  body_relative_offset : Int
  encoded_size : Int
  target_index : Int?
  type_index : Int?
  table_index : Int?
  target : FunctionPathNode?
}

///|
pub struct FunctionCallsReport {
  file_size : Int
  sha256 : String
  caller : FunctionPathNode
  entry : FunctionInstructionRange
  prefix : FunctionInstructionRange
  body : FunctionInstructionRange
  locals_encoding : FunctionInstructionRange
  instruction_count : Int
  instruction_bytes : Int
  call_count : Int
  direct_count : Int
  indirect_count : Int
  reference_call_count : Int
  tail_count : Int
  call_encoding_bytes : Int
  other_instruction_bytes : Int
  calls : Array[FunctionCallRecord]
  scope : String
}

///|
pub enum FunctionCallsError {
  CallsInputError(WasmError)
  InvalidCallsTarget(Int, Int)
  ImportedCallsTarget(Int)
  InvalidCallsEvidence(Int?, String)
}

///|
pub fn describe_function_calls_error(error : FunctionCallsError) -> String {
  match error {
    CallsInputError(error) => describe_error(error)
    InvalidCallsTarget(index, count) =>
      "target function index \{index} is outside the function index space of \{count}"
    ImportedCallsTarget(index) =>
      "target function \{index} is imported and has no local body"
    InvalidCallsEvidence(offset, reason) =>
      "invalid call evidence" +
      (match offset {
        Some(at) => " at file offset \{at}"
        None => ""
      }) +
      ": " +
      reason
  }
}

///|
/// Full instruction decode of one local body only. No graph, analyze, BFS or
/// second decode for operands; ref.func is not a call instruction.
pub fn inspect_function_calls(
  data : Bytes,
  index : Int,
) -> Result[FunctionCallsReport, FunctionCallsError] {
  let parsed = match parse_module(data) {
    Ok(value) => value
    Err(error) => return Err(CallsInputError(error))
  }
  let inventory = match path_inventory(data, parsed) {
    Ok(value) => value
    Err(error) => return Err(CallsInputError(error))
  }
  if index < 0 || index >= inventory.length() {
    return Err(InvalidCallsTarget(index, inventory.length()))
  }
  let caller = inventory[index]
  if caller.imported {
    return Err(ImportedCallsTarget(index))
  }
  let mut imported = 0
  for node in inventory {
    if !node.imported {
      break
    }
    imported += 1
  }
  let code = match find_sections(parsed).code {
    Some(value) => value
    None =>
      return Err(
        InvalidCallsEvidence(None, "local function has no code section"),
      )
  }
  let bodies = match parse_code_section(data, code, base_index=imported) {
    Ok(value) => value
    Err(error) => return Err(CallsInputError(error))
  }
  if index < imported || bodies.length() != inventory.length() - imported {
    return Err(
      InvalidCallsEvidence(
        Some(code.offset),
        "inventory and local body counts differ",
      ),
    )
  }
  let target_body = bodies[index - imported]
  if caller.index != index ||
    target_body.index != index ||
    caller.encoded_size != Some(target_body.total_size) ||
    target_body.body_size < 0 ||
    target_body.body_size >= target_body.total_size {
    return Err(
      InvalidCallsEvidence(
        Some(target_body.offset),
        "caller identity or body range differs",
      ),
    )
  }
  let reader = match
    Reader::new_range(data, target_body.offset, target_body.total_size) {
    Ok(value) => value
    Err(error) => return Err(CallsInputError(error))
  }
  let prefix_size = target_body.total_size - target_body.body_size
  match reader.read_bytes(prefix_size) {
    Ok(_) => ()
    Err(error) => return Err(CallsInputError(error))
  }
  let body_offset = reader.position()
  match reader.read_bytes(target_body.body_size) {
    Ok(_) => ()
    Err(error) => return Err(CallsInputError(error))
  }
  let body_end = reader.position()
  let decoded = match
    decode_instructions(data, body_offset, target_body.body_size) {
    Ok(value) => value
    Err(error) => return Err(CallsInputError(error))
  }
  let first = if decoded.is_empty() { body_end } else { decoded[0].offset }
  if first < body_offset || first > body_end {
    return Err(
      InvalidCallsEvidence(Some(body_offset), "locals range exceeds body"),
    )
  }
  let calls : Array[FunctionCallRecord] = []
  let mut cursor = first
  let mut instruction_bytes = 0
  let mut call_encoding_bytes = 0
  let mut direct_count = 0
  let mut indirect_count = 0
  let mut reference_call_count = 0
  let mut tail_count = 0
  for ordinal, instruction in decoded {
    if instruction.offset != cursor ||
      instruction.size <= 0 ||
      instruction.size > body_end - cursor {
      return Err(
        InvalidCallsEvidence(
          Some(instruction.offset),
          "instruction range is not contiguous within body",
        ),
      )
    }
    cursor += instruction.size // size checked against remaining body before addition
    instruction_bytes += instruction.size
    let (kind, is_tail, operands) = match instruction.opcode {
      Call => (DirectCall, false, 1)
      ReturnCall => (DirectCall, true, 1)
      CallIndirect => (IndirectCall, false, 2)
      ReturnCallIndirect => (IndirectCall, true, 2)
      CallRef => (ReferenceCall, false, 1)
      ReturnCallRef => (ReferenceCall, true, 1)
      _ => continue
    }
    if instruction.operands.length() != operands {
      return Err(
        InvalidCallsEvidence(
          Some(instruction.offset),
          "call operand shape differs from opcode",
        ),
      )
    }
    let mut target_index : Int? = None
    let mut target : FunctionPathNode? = None
    let mut type_index : Int? = None
    let mut table_index : Int? = None
    match kind {
      DirectCall => {
        let callee = instruction.operands[0]
        if callee < 0 || callee >= inventory.length() {
          return Err(
            InvalidCallsEvidence(
              Some(instruction.offset),
              "direct target function index \{callee} is outside the function index space of \{inventory.length()}",
            ),
          )
        }
        target_index = Some(callee)
        target = Some(inventory[callee])
        direct_count += 1
      }
      IndirectCall => {
        type_index = Some(instruction.operands[0])
        table_index = Some(instruction.operands[1])
        indirect_count += 1
      }
      ReferenceCall => {
        type_index = Some(instruction.operands[0])
        reference_call_count += 1
      }
    }
    calls.push({
      instruction_ordinal: ordinal,
      opcode: instruction.opcode,
      kind,
      is_tail,
      file_offset: instruction.offset,
      body_relative_offset: instruction.offset - body_offset,
      encoded_size: instruction.size,
      target_index,
      type_index,
      table_index,
      target,
    })
    call_encoding_bytes += instruction.size
    if is_tail {
      tail_count += 1
    }
  }
  let locals_size = first - body_offset
  if cursor != body_end ||
    instruction_bytes != target_body.body_size - locals_size ||
    calls.length() != direct_count + indirect_count + reference_call_count ||
    call_encoding_bytes > instruction_bytes {
    return Err(
      InvalidCallsEvidence(
        Some(body_offset),
        "call/instruction byte ledger does not close",
      ),
    )
  }
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    caller,
    entry: { file_offset: target_body.offset, size: target_body.total_size, },
    prefix: { file_offset: target_body.offset, size: prefix_size, },
    body: { file_offset: body_offset, size: target_body.body_size, },
    locals_encoding: { file_offset: body_offset, size: locals_size, },
    instruction_count: decoded.length(),
    instruction_bytes,
    call_count: calls.length(),
    direct_count,
    indirect_count,
    reference_call_count,
    tail_count,
    call_encoding_bytes,
    other_instruction_bytes: instruction_bytes - call_encoding_bytes,
    calls,
    scope: "Static call instructions in one completely decoded local body, not an execution trace or a complete outgoing dependency graph. Direct targets are exact encoded function indices; indirect/reference-call targets are unresolved and remain null, with no candidate inference. Type/table indices record encoding only, without existence or compatibility checks. ref.func takes a reference and is excluded. Tail calls are a subset, not extra calls. Different positions remain separate; no reachability filtering. Existing parser/inventory range and best-effort name checks still inspect the module, but other function opcodes are not decoded. No graph, recursive search or strict guarantee. No complete Wasm type/stack/control validation. File offsets are not memory addresses; counts are not execution frequencies; encoded bytes do not predict deletion, recompilation or gzip savings. Top limits text call rows only, not decode cost or complete JSON.",
  })
}