///|
/// One encoded declaration group, never expanded into individual variables.
/// Declared ordinals exclude parameters and use half-open intervals.
pub struct FunctionLocalGroup {
  group_index : Int
  quantity : UInt
  declared_ordinal_start : UInt64
  declared_ordinal_end : UInt64
  encoding : FunctionInstructionRange
  quantity_encoding : FunctionInstructionRange
  type_encoding : FunctionInstructionRange
  type_hex : String
  type_label : String?
}

///|
pub struct FunctionLocalsReport {
  file_size : Int
  sha256 : String
  target : FunctionPathNode
  entry : FunctionInstructionRange
  prefix : FunctionInstructionRange
  body : FunctionInstructionRange
  locals_encoding : FunctionInstructionRange
  group_count_encoding : FunctionInstructionRange
  /// Undecoded remainder, not a validated instruction region.
  body_remainder : FunctionInstructionRange
  group_count : Int
  declared_count : UInt64
  quantity_encoding_bytes : Int
  type_encoding_bytes : Int
  groups : Array[FunctionLocalGroup]
  scope : String
}

///|
pub enum FunctionLocalsError {
  LocalsInputError(WasmError)
  InvalidLocalsTarget(Int, Int)
  ImportedLocalsTarget(Int)
  InvalidLocalsEncoding(Int?, String)
}

///|
pub fn describe_function_locals_error(error : FunctionLocalsError) -> String {
  match error {
    LocalsInputError(error) => describe_error(error)
    InvalidLocalsTarget(index, count) =>
      "target function index \{index} is outside the function index space of \{count}"
    ImportedLocalsTarget(index) =>
      "target function \{index} is imported and has no local body"
    InvalidLocalsEncoding(offset, reason) =>
      "invalid locals encoding" +
      (match offset {
        Some(at) => " at file offset \{at}"
        None => ""
      }) +
      ": " +
      reason
  }
}

///|
/// Labels only for exact, confirmed one-byte encodings. Raw bytes remain the
/// source of truth; the existing read_valtype consumer is not a type validator.
fn function_locals_type_label(hex : String) -> String? {
  match hex {
    "7f" => Some("i32")
    "7e" => Some("i64")
    "7d" => Some("f32")
    "7c" => Some("f64")
    "7b" => Some("v128")
    "70" => Some("funcref")
    "6f" => Some("externref")
    _ => None
  }
}

///|
/// Consume only the selected local body's declarations once; never decode any
/// function's instructions or signature and never allocate by variable count.
pub fn inspect_function_locals(
  data : Bytes,
  index : Int,
) -> Result[FunctionLocalsReport, FunctionLocalsError] {
  let parsed = match parse_module(data) {
    Ok(value) => value
    Err(error) => return Err(LocalsInputError(error))
  }
  let inventory = match path_inventory(data, parsed) {
    Ok(value) => value
    Err(error) => return Err(LocalsInputError(error))
  }
  if index < 0 || index >= inventory.length() {
    return Err(InvalidLocalsTarget(index, inventory.length()))
  }
  let target = inventory[index]
  if target.imported {
    return Err(ImportedLocalsTarget(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(
        InvalidLocalsEncoding(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(LocalsInputError(error))
  }
  if index < imported || bodies.length() != inventory.length() - imported {
    return Err(
      InvalidLocalsEncoding(
        Some(code.offset),
        "inventory and body counts differ",
      ),
    )
  }
  let selected = bodies[index - imported]
  if selected.index != index ||
    target.index != index ||
    target.encoded_size != Some(selected.total_size) ||
    selected.body_size < 0 ||
    selected.body_size >= selected.total_size {
    return Err(
      InvalidLocalsEncoding(
        Some(selected.offset),
        "target identity or range differs",
      ),
    )
  }
  let entry_reader = match
    Reader::new_range(data, selected.offset, selected.total_size) {
    Ok(value) => value
    Err(error) => return Err(LocalsInputError(error))
  }
  let prefix_size = selected.total_size - selected.body_size
  match entry_reader.read_bytes(prefix_size) {
    Ok(_) => ()
    Err(error) => return Err(LocalsInputError(error))
  }
  let body_offset = entry_reader.position()
  let reader = match Reader::new_range(data, body_offset, selected.body_size) {
    Ok(value) => value
    Err(error) => return Err(LocalsInputError(error))
  }
  let group_count = match read_u32_as_int(reader) {
    Ok(value) => value
    Err(error) => return Err(LocalsInputError(error))
  }
  let count_end = reader.position()
  // Each group needs at least one quantity byte and one type byte. Check before
  // iterating/allocating; body boundaries may not borrow the next entry's bytes.
  if group_count > reader.remaining() / 2 {
    return Err(
      InvalidLocalsEncoding(
        Some(count_end),
        "declaration groups exceed remaining body bytes",
      ),
    )
  }
  let groups : Array[FunctionLocalGroup] = []
  let mut declared_count = 0UL
  let mut quantity_encoding_bytes = 0
  let mut type_encoding_bytes = 0
  for group_index in 0.. value
      Err(error) => return Err(LocalsInputError(error))
    }
    let type_start = reader.position()
    match reader.read_valtype() {
      Ok(_) => ()
      Err(error) => return Err(LocalsInputError(error))
    }
    let end = reader.position()
    let amount = quantity.to_uint64()
    if amount > 18446744073709551615UL - declared_count {
      return Err(
        InvalidLocalsEncoding(Some(start), "declared quantity sum exceeds u64"),
      )
    }
    let ordinal_start = declared_count
    declared_count += amount
    let type_hex = data_preview_hex(data[type_start:end])
    let quantity_size = type_start - start
    let type_size = end - type_start
    groups.push({
      group_index,
      quantity,
      declared_ordinal_start: ordinal_start,
      declared_ordinal_end: declared_count,
      encoding: { file_offset: start, size: end - start, },
      quantity_encoding: { file_offset: start, size: quantity_size, },
      type_encoding: { file_offset: type_start, size: type_size, },
      type_hex,
      type_label: function_locals_type_label(type_hex),
    })
    quantity_encoding_bytes += quantity_size
    type_encoding_bytes += type_size
  }
  let remainder = reader.position()
  let locals_size = remainder - body_offset
  let count_size = count_end - body_offset
  if locals_size - count_size != quantity_encoding_bytes + type_encoding_bytes {
    return Err(
      InvalidLocalsEncoding(
        Some(body_offset),
        "locals byte ledger does not close",
      ),
    )
  }
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    target,
    entry: { file_offset: selected.offset, size: selected.total_size, },
    prefix: { file_offset: selected.offset, size: prefix_size, },
    body: { file_offset: body_offset, size: selected.body_size, },
    locals_encoding: { file_offset: body_offset, size: locals_size, },
    group_count_encoding: { file_offset: body_offset, size: count_size, },
    body_remainder: { file_offset: remainder, size: reader.remaining(), },
    group_count,
    declared_count,
    quantity_encoding_bytes,
    type_encoding_bytes,
    groups,
    scope: "Encoded local declaration groups only, in original order, including zero-quantity groups. Declared ordinals are half-open and exclude parameters; they are not full local indices. No signature parsing or local.get/set/tee binding. Quantities are u32; cumulative counts/ordinals use u64 and decimal strings in JSON. No variable expansion or adjacent-group merging. Raw type bytes are authoritative; limited labels and the existing read_valtype consumer do not provide full type, heap-type or default-initialization validation. Body remainder is undecoded, not a validated instruction region; no function opcodes are decoded. Existing module/inventory/name and body-range checks still inspect the module. File offsets are not memory addresses; encoded sizes do not measure runtime stack/memory or predict deletion, recompilation or gzip savings. Top limits text group rows only, not parsing cost or complete JSON.",
  })
}