///|
/// One encoded access; ordinal belongs to the full instruction sequence.
pub struct FunctionLocalAccess {
  instruction_ordinal : Int
  opcode : Opcode
  local_index : Int
  file_offset : Int
  body_relative_offset : Int
  encoded_size : Int
  index_encoding : FunctionInstructionRange
}

///|
/// Sparse numeric index group; members retain all original instruction ordinals.
pub struct FunctionLocalAccessGroup {
  local_index : Int
  access_count : Int
  get_count : Int
  set_count : Int
  tee_count : Int
  encoded_bytes : Int
  index_encoding_bytes : Int
  instruction_ordinals : Array[Int]
}

///|
pub struct FunctionLocalAccessesReport {
  file_size : Int
  sha256 : String
  target : FunctionPathNode
  entry : FunctionInstructionRange
  prefix : FunctionInstructionRange
  body : FunctionInstructionRange
  locals_encoding : FunctionInstructionRange
  instruction_count : Int
  instruction_bytes : Int
  access_count : Int
  get_count : Int
  set_count : Int
  tee_count : Int
  distinct_local_index_count : Int
  access_encoding_bytes : Int
  index_encoding_bytes : Int
  other_instruction_bytes : Int
  accesses : Array[FunctionLocalAccess]
  groups : Array[FunctionLocalAccessGroup]
  scope : String
}

///|
pub enum FunctionLocalAccessesError {
  LocalAccessesInputError(WasmError)
  InvalidLocalAccessesTarget(Int, Int)
  ImportedLocalAccessesTarget(Int)
  InvalidLocalAccessesEvidence(Int?, String)
}

///|
pub fn describe_function_local_accesses_error(
  error : FunctionLocalAccessesError,
) -> String {
  match error {
    LocalAccessesInputError(error) => describe_error(error)
    InvalidLocalAccessesTarget(index, count) =>
      "target function index \{index} is outside the function index space of \{count}"
    ImportedLocalAccessesTarget(index) =>
      "target function \{index} is imported and has no local body"
    InvalidLocalAccessesEvidence(offset, reason) =>
      "invalid local-access evidence" +
      (match offset {
        Some(at) => " at file offset \{at}"
        None => ""
      }) +
      ": " +
      reason
  }
}

///|
/// Decode the selected body once. No graph, locals inspection, prior instruction
/// report, signature parsing or secondary operand decoding.
pub fn inspect_function_local_accesses(
  data : Bytes,
  index : Int,
) -> Result[FunctionLocalAccessesReport, FunctionLocalAccessesError] {
  let parsed = match parse_module(data) {
    Ok(value) => value
    Err(error) => return Err(LocalAccessesInputError(error))
  }
  let inventory = match path_inventory(data, parsed) {
    Ok(value) => value
    Err(error) => return Err(LocalAccessesInputError(error))
  }
  if index < 0 || index >= inventory.length() {
    return Err(InvalidLocalAccessesTarget(index, inventory.length()))
  }
  let target = inventory[index]
  if target.imported {
    return Err(ImportedLocalAccessesTarget(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(
        InvalidLocalAccessesEvidence(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(LocalAccessesInputError(error))
  }
  if index < imported || bodies.length() != inventory.length() - imported {
    return Err(
      InvalidLocalAccessesEvidence(
        Some(code.offset),
        "inventory and local body counts differ",
      ),
    )
  }
  let target_body = bodies[index - imported]
  if target.index != index ||
    target_body.index != index ||
    target.encoded_size != Some(target_body.total_size) ||
    target_body.body_size < 0 ||
    target_body.body_size >= target_body.total_size {
    return Err(
      InvalidLocalAccessesEvidence(
        Some(target_body.offset),
        "target 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(LocalAccessesInputError(error))
  }
  let prefix_size = target_body.total_size - target_body.body_size
  match reader.read_bytes(prefix_size) {
    Ok(_) => ()
    Err(error) => return Err(LocalAccessesInputError(error))
  }
  let body_offset = reader.position()
  match reader.read_bytes(target_body.body_size) {
    Ok(_) => ()
    Err(error) => return Err(LocalAccessesInputError(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(LocalAccessesInputError(error))
  }
  let first = if decoded.is_empty() { body_end } else { decoded[0].offset }
  if first < body_offset || first > body_end {
    return Err(
      InvalidLocalAccessesEvidence(
        Some(body_offset),
        "locals range exceeds body",
      ),
    )
  }
  let accesses : Array[FunctionLocalAccess] = []
  let buckets : Map[Int, Array[FunctionLocalAccess]] = Map([])
  let mut cursor = first
  let mut instruction_bytes = 0
  let mut access_encoding_bytes = 0
  let mut index_encoding_bytes = 0
  let mut get_count = 0
  let mut set_count = 0
  let mut tee_count = 0
  for ordinal, instruction in decoded {
    if instruction.offset != cursor ||
      instruction.size <= 0 ||
      instruction.size > body_end - cursor {
      return Err(
        InvalidLocalAccessesEvidence(
          Some(instruction.offset),
          "instruction range is not contiguous within body",
        ),
      )
    }
    cursor += instruction.size
    instruction_bytes += instruction.size
    match instruction.opcode {
      LocalGet | LocalSet | LocalTee => ()
      _ => continue
    }
    if instruction.operands.length() != 1 ||
      instruction.operands[0] < 0 ||
      instruction.size < 2 {
      return Err(
        InvalidLocalAccessesEvidence(
          Some(instruction.offset),
          "local access needs exactly one nonnegative index and its encoded bytes",
        ),
      )
    }
    // Single-byte opcode; validated size >= 2 within body before this addition.
    let index_encoding : FunctionInstructionRange = {
      file_offset: instruction.offset + 1,
      size: instruction.size - 1,
    }
    let access : FunctionLocalAccess = {
      instruction_ordinal: ordinal,
      opcode: instruction.opcode,
      local_index: instruction.operands[0],
      file_offset: instruction.offset,
      body_relative_offset: instruction.offset - body_offset,
      encoded_size: instruction.size,
      index_encoding,
    }
    accesses.push(access)
    match buckets.get(access.local_index) {
      Some(records) => records.push(access)
      None => buckets.set(access.local_index, [access])
    }
    match instruction.opcode {
      LocalGet => get_count += 1
      LocalSet => set_count += 1
      LocalTee => tee_count += 1
      _ => ()
    }
    access_encoding_bytes += instruction.size
    index_encoding_bytes += index_encoding.size
  }
  let groups : Array[FunctionLocalAccessGroup] = []
  let mut grouped_count = 0
  let mut grouped_bytes = 0
  for local_index, records in buckets {
    let ordinals : Array[Int] = []
    let mut gets = 0
    let mut sets = 0
    let mut tees = 0
    let mut bytes = 0
    let mut index_bytes = 0
    for record in records {
      ordinals.push(record.instruction_ordinal)
      match record.opcode {
        LocalGet => gets += 1
        LocalSet => sets += 1
        LocalTee => tees += 1
        _ => ()
      }
      bytes += record.encoded_size
      index_bytes += record.index_encoding.size
    }
    grouped_count += records.length()
    grouped_bytes += bytes
    groups.push({
      local_index,
      access_count: records.length(),
      get_count: gets,
      set_count: sets,
      tee_count: tees,
      encoded_bytes: bytes,
      index_encoding_bytes: index_bytes,
      instruction_ordinals: ordinals,
    })
  }
  groups.sort_by(fn(a, b) { a.local_index.compare(b.local_index) })
  let locals_size = first - body_offset
  if cursor != body_end ||
    instruction_bytes != target_body.body_size - locals_size ||
    accesses.length() != get_count + set_count + tee_count ||
    access_encoding_bytes > instruction_bytes ||
    access_encoding_bytes - accesses.length() != index_encoding_bytes ||
    grouped_count != accesses.length() ||
    grouped_bytes != access_encoding_bytes {
    return Err(
      InvalidLocalAccessesEvidence(
        Some(body_offset),
        "local-access byte/count ledger does not close",
      ),
    )
  }
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    target,
    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,
    access_count: accesses.length(),
    get_count,
    set_count,
    tee_count,
    distinct_local_index_count: groups.length(),
    access_encoding_bytes,
    index_encoding_bytes,
    other_instruction_bytes: instruction_bytes - access_encoding_bytes,
    accesses,
    groups,
    scope: "Static local.get/local.set/local.tee encodings in one completely decoded local body. Local indices are encoded values in the index space including parameters, unlike --locals declaration ordinals that exclude parameters. No signature/parameter count, parameter-versus-declared-local classification, declaration-group binding, variable names/types or actual index-range validation. Existing decoder limits indices to nonnegative Int; values beyond that range fail, not full-u32 support. Original instruction ordinals and repeated sites are retained without reachability/control-flow filtering; tee counts only as tee. Sparse groups are allocated by observed distinct indices, not maximum index. Existing module/inventory/name and body-range checks still inspect the file, but no other function opcodes are decoded. No analyze, graph or separate locals query; target is decoded once. Not complete Wasm type/stack/control validation. Static counts are not execution frequency or runtime heat; file offsets are not memory addresses; encoded sizes are not runtime memory or deletion/recompilation/gzip savings. No accesses does not imply no variables. Top limits each text table independently, not totals, complete members/JSON or decode cost.",
  })
}