///|
/// Half-open original file range; never a runtime address.
pub struct ExportByteRange {
  offset : Int
  size : Int
} derive(Eq, Debug)

///|
pub struct ExportDeclaration {
  ordinal : Int
  name : String
  kind : String
  kind_code : Int
  target_index : UInt
  encoding : ExportByteRange
  name_encoding : ExportByteRange
  name_length_prefix_bytes : Int
  name_utf8_bytes : Int
  /// The kind byte plus the target index encoding.
  descriptor : ExportByteRange
  target_index_prefix_bytes : Int
}

///|
pub struct ExportTarget {
  kind : String
  kind_code : Int
  target_index : UInt
  entry_ordinals : Array[Int]
  member_count : Int
  declaration_bytes : Int
  is_alias : Bool
}

///|
pub struct ExportKindCounts {
  functions : Int
  tables : Int
  memories : Int
  globals : Int
  tags : Int
} derive(Eq, Debug)

///|
pub struct ExportsSummary {
  export_count : Int
  kind_counts : ExportKindCounts
  distinct_target_count : Int
  alias_group_count : Int
  alias_member_count : Int
  singleton_target_count : Int
} derive(Eq, Debug)

///|
/// Parent and child byte counts are decompositions, not additional costs.
pub struct ExportsLedger {
  export_section_bytes : Int
  section_header_bytes : Int
  count_prefix_bytes : Int
  declaration_bytes : Int
  name_encoding_bytes : Int
  name_length_prefix_bytes : Int
  name_utf8_bytes : Int
  descriptor_bytes : Int
  kind_bytes : Int
  target_index_prefix_bytes : Int
} derive(Eq, Debug)

///|
pub struct ExportsReport {
  file_size : Int
  sha256 : String
  section : ExportByteRange?
  count_encoding : ExportByteRange?
  summary : ExportsSummary
  ledger : ExportsLedger
  /// Complete physical declaration order, unaffected by text sorting or Top.
  entries : Array[ExportDeclaration]
  /// Kind code then full unsigned index ascending, including singleton targets.
  targets : Array[ExportTarget]
  supported_scope : String
}

///|
priv struct ExportTargetKey {
  kind : Int
  index : UInt
} derive(Eq, Hash)

///|
fn exports_kind_name(code : Int) -> String {
  match code {
    0 => "function"
    1 => "table"
    2 => "memory"
    3 => "global"
    4 => "tag"
    _ => "unknown" // Only 0..4 reach a successful report.
  }
}

///|
/// Inspect only export declarations after ordinary module framing checks.
/// No graph, body decoding, target/type validation or optional partial results.
pub fn inspect_exports(data : Bytes) -> Result[ExportsReport, WasmError] {
  let parsed = match parse_module(data) {
    Ok(parsed) => parsed
    Err(error) => return Err(error)
  }
  let entries : Array[ExportDeclaration] = []
  let seen_names : Map[String, Int] = Map([])
  let buckets : Map[ExportTargetKey, Array[Int]] = Map([])
  let counts = [0, 0, 0, 0, 0]
  let mut section : ExportByteRange? = None
  let mut count_encoding : ExportByteRange? = None
  let mut section_header_bytes = 0
  for item in parsed.sections {
    if !same_kind(item.kind, Export) {
      continue
    }
    let reader = match reader_at_payload(data, item) {
      Ok(reader) => reader
      Err(error) => return Err(error)
    }
    section = Some({ offset: item.offset, size: item.total_size, })
    section_header_bytes = item.total_size - item.payload_size
    let count_start = reader.position()
    let mut remaining = match reader.read_u32_leb() {
      Ok(count) => count
      Err(error) => return Err(error)
    }
    count_encoding = Some({
      offset: count_start,
      size: reader.position() - count_start,
    })
    while remaining > 0U {
      let start = reader.position()
      let length = match read_u32_as_int(reader) {
        Ok(length) => length
        Err(error) => return Err(error)
      }
      let name_length_prefix_bytes = reader.position() - start
      let name_start = reader.position()
      let raw_name = match reader.read_bytes(length) {
        Ok(bytes) => bytes
        Err(error) => return Err(error)
      }
      let name = @utf8.decode(raw_name) catch {
        _ =>
          return Err(
            InvalidExportEncoding(
              name_start, "invalid UTF-8 in export name starting here",
            ),
          )
      }
      if seen_names.get(name) is Some(first) {
        return Err(
          InvalidExportEncoding(
            start,
            "duplicate export name " +
            Json::string(name).stringify() +
            "; first declaration at file offset \{first}",
          ),
        )
      }
      seen_names.set(name, start)
      let descriptor_start = reader.position()
      let kind = match reader.read_byte() {
        Ok(kind) => kind
        Err(error) => return Err(error)
      }
      if kind < 0 || kind > 4 {
        return Err(
          InvalidExportEncoding(descriptor_start, "unknown export kind \{kind}"),
        )
      }
      let index_start = reader.position()
      let target_index = match reader.read_u32_leb() {
        Ok(index) => index
        Err(error) => return Err(error)
      }
      let ordinal = entries.length()
      entries.push({
        ordinal,
        name,
        kind: exports_kind_name(kind),
        kind_code: kind,
        target_index,
        encoding: { offset: start, size: reader.position() - start, },
        name_encoding: { offset: start, size: descriptor_start - start, },
        name_length_prefix_bytes,
        name_utf8_bytes: length,
        descriptor: {
          offset: descriptor_start,
          size: reader.position() - descriptor_start,
        },
        target_index_prefix_bytes: reader.position() - index_start,
      })
      counts[kind] += 1
      let key = ExportTargetKey::{ kind, index: target_index, }
      let ordinals = buckets.get_or_default(key, [])
      ordinals.push(ordinal)
      buckets.set(key, ordinals)
      remaining -= 1U
    }
    match finish_section(reader, Export) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  let targets : Array[ExportTarget] = []
  let mut alias_group_count = 0
  let mut alias_member_count = 0
  for key, ordinals in buckets {
    let mut declaration_bytes = 0
    for ordinal in ordinals {
      declaration_bytes += entries[ordinal].encoding.size
    }
    let is_alias = ordinals.length() >= 2
    if is_alias {
      alias_group_count += 1
      alias_member_count += ordinals.length()
    }
    targets.push({
      kind: exports_kind_name(key.kind),
      kind_code: key.kind,
      target_index: key.index,
      entry_ordinals: ordinals,
      member_count: ordinals.length(),
      declaration_bytes,
      is_alias,
    })
  }
  targets.sort_by(fn(a, b) {
    let order = a.kind_code.compare(b.kind_code)
    if order != 0 {
      order
    } else {
      a.target_index.compare(b.target_index)
    }
  })
  let mut declaration_bytes = 0
  let mut name_encoding_bytes = 0
  let mut name_length_prefix_bytes = 0
  let mut name_utf8_bytes = 0
  let mut descriptor_bytes = 0
  let mut target_index_prefix_bytes = 0
  for entry in entries {
    declaration_bytes += entry.encoding.size
    name_encoding_bytes += entry.name_encoding.size
    name_length_prefix_bytes += entry.name_length_prefix_bytes
    name_utf8_bytes += entry.name_utf8_bytes
    descriptor_bytes += entry.descriptor.size
    target_index_prefix_bytes += entry.target_index_prefix_bytes
  }
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    section,
    count_encoding,
    entries,
    targets,
    summary: {
      export_count: entries.length(),
      kind_counts: {
        functions: counts[0],
        tables: counts[1],
        memories: counts[2],
        globals: counts[3],
        tags: counts[4],
      },
      distinct_target_count: targets.length(),
      alias_group_count,
      alias_member_count,
      singleton_target_count: targets.length() - alias_group_count,
    },
    ledger: {
      export_section_bytes: section.map(fn(range) { range.size }).unwrap_or(0),
      section_header_bytes,
      count_prefix_bytes: count_encoding
      .map(fn(range) { range.size })
      .unwrap_or(0),
      declaration_bytes,
      name_encoding_bytes,
      name_length_prefix_bytes,
      name_utf8_bytes,
      descriptor_bytes,
      kind_bytes: entries.length(),
      target_index_prefix_bytes,
    },
    supported_scope: "Export declarations and exact UTF-8 host-visible names, not internal symbols or source names. Targets are grouped only by declared (kind, unsigned index); ordinals locate declarations within this file. Target existence, signatures, types, imported/local origin, reachability and runtime semantics are not validated. Function targets may be imports with no local body; host cost is not measured. Bytes cover declaration encodings only, not function bodies, dependency closures, memory capacities or host implementations. Alias groups do not establish safe removal or recompiled/gzip savings. Ranges are half-open file offsets, never memory addresses. Top limits text declarations and alias groups only; summaries and JSON remain complete.",
  })
}