///|
/// Half-open original-file range. Offsets are never memory addresses.
pub struct MetadataRange {
  offset : Int
  size : Int
} derive(Eq, Debug)

///|
pub struct MetadataCustom {
  ordinal : Int
  section_ordinal : Int
  encoding : MetadataRange
  section_header_bytes : Int
  name_encoding : MetadataRange
  name_length_prefix_bytes : Int
  name_bytes : MetadataRange
  name : String?
  category : String
  content : MetadataRange
  /// complete/partial for recognized name content; otherwise opaque.
  status : String
  name_subsection_bytes : Int
  unbounded_tail : MetadataRange?
}

///|
pub struct MetadataNameSubsection {
  custom_ordinal : Int
  ordinal : Int
  id : Int
  encoding : MetadataRange
  header_bytes : Int
  payload : MetadataRange
  /// complete/partial for id 1, opaque for all other ids.
  status : String
  declared_count : UInt?
  count_prefix_bytes : Int
  parsed_record_bytes : Int
  unparsed : MetadataRange?
}

///|
pub struct MetadataFunctionName {
  custom_ordinal : Int
  subsection_ordinal : Int
  ordinal : Int
  /// Metadata declaration only, not evidence that the function exists.
  function_index : UInt
  encoding : MetadataRange
  index_prefix_bytes : Int
  name_length_prefix_bytes : Int
  name_bytes : MetadataRange
  name : String?
}

///|
pub struct MetadataDiagnostic {
  offset : Int
  custom_ordinal : Int
  /// Includes a failed framing attempt; there need not be a bounded row.
  subsection_ordinal : Int?
  reason : String
}

///|
pub struct MetadataCategory {
  category : String
  custom_ordinals : Array[Int]
  total_bytes : Int
}

///|
pub struct MetadataNameGroup {
  /// None aggregates unavailable names, without claiming their bytes match.
  name : String?
  custom_ordinals : Array[Int]
  total_bytes : Int
}

///|
pub struct MetadataSummary {
  custom_count : Int
  custom_bytes : Int
  section_header_bytes : Int
  custom_name_encoding_bytes : Int
  content_bytes : Int
  unavailable_custom_count : Int
  unavailable_custom_bytes : Int
  name_custom_count : Int
  name_content_bytes : Int
  name_subsection_bytes : Int
  unbounded_name_tail_bytes : Int
  function_name_payload_bytes : Int
  function_name_count_prefix_bytes : Int
  parsed_function_record_count : Int
  parsed_function_record_bytes : Int
  function_name_unparsed_bytes : Int
  function_index_prefix_bytes : Int
  function_name_length_prefix_bytes : Int
  function_name_utf8_bytes : Int
  unavailable_function_name_count : Int
}

///|
pub struct MetadataReport {
  file_size : Int
  sha256 : String
  byte_ledger_complete : Bool
  custom_name_status : String
  /// absent/complete/partial within the supported name-detail interpretation.
  name_details_status : String
  modeling_scope : String
  summary : MetadataSummary
  categories : Array[MetadataCategory]
  name_groups : Array[MetadataNameGroup]
  customs : Array[MetadataCustom]
  subsections : Array[MetadataNameSubsection]
  function_names : Array[MetadataFunctionName]
  diagnostics : Array[MetadataDiagnostic]
}

///|
priv struct MetadataWork {
  subsections : Array[MetadataNameSubsection]
  records : Array[MetadataFunctionName]
  diagnostics : Array[MetadataDiagnostic]
}

///|
fn metadata_utf8(bytes : BytesView) -> String? {
  let name = @utf8.decode(bytes) catch { _ => return None }
  Some(name)
}

///|
fn metadata_record(
  reader : Reader,
  custom : Int,
  subsection : Int,
  ordinal : Int,
) -> Result[MetadataFunctionName, WasmError] {
  let start = reader.position()
  let function_index = match reader.read_u32_leb() {
    Ok(n) => n
    Err(e) => return Err(e)
  }
  let index_prefix_bytes = reader.position() - start
  let length_start = reader.position()
  let length = match read_u32_as_int(reader) {
    Ok(n) => n
    Err(e) => return Err(e)
  }
  let name_length_prefix_bytes = reader.position() - length_start
  let name_start = reader.position()
  let bytes = match reader.read_bytes(length) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  Ok({
    custom_ordinal: custom,
    subsection_ordinal: subsection,
    ordinal,
    function_index,
    encoding: { offset: start, size: reader.position() - start, },
    index_prefix_bytes,
    name_length_prefix_bytes,
    name_bytes: { offset: name_start, size: length, },
    name: metadata_utf8(bytes),
  })
}

///|
fn metadata_function_subsection(
  reader : Reader,
  custom : Int,
  ordinal : Int,
  start : Int,
  payload : MetadataRange,
  work : MetadataWork,
) -> MetadataNameSubsection {
  let mut status = "complete"
  let mut prefix = 0
  let mut record_bytes = 0
  let mut unparsed : MetadataRange? = None
  let count = match reader.read_u32_leb() {
    Ok(n) => {
      prefix = reader.position() - payload.offset
      Some(n)
    }
    Err(error) => {
      status = "partial"
      unparsed = Some(payload) // Include all consumed bytes of the failed count.
      work.diagnostics.push({
        offset: payload.offset,
        custom_ordinal: custom,
        subsection_ordinal: Some(ordinal),
        reason: "function-name count: " + describe_error(error),
      })
      None
    }
  }
  if count is Some(n) {
    let mut remaining = n
    let mut record_ordinal = 0
    while remaining > 0U {
      let record_start = reader.position()
      match metadata_record(reader, custom, ordinal, record_ordinal) {
        Err(error) => {
          status = "partial"
          unparsed = Some({
            offset: record_start,
            size: reader.end - record_start,
          })
          work.diagnostics.push({
            offset: record_start,
            custom_ordinal: custom,
            subsection_ordinal: Some(ordinal),
            reason: "function-name record \{record_ordinal}: " +
            describe_error(error),
          })
          break
        }
        Ok(record) => {
          record_bytes += record.encoding.size
          work.records.push(record)
          if record.name is None {
            status = "partial"
            work.diagnostics.push({
              offset: record.name_bytes.offset,
              custom_ordinal: custom,
              subsection_ordinal: Some(ordinal),
              reason: "function-name record \{record_ordinal}: invalid UTF-8 in name starting here; record remains delimited",
            })
          }
        }
      }
      remaining -= 1U
      record_ordinal += 1
    }
    if unparsed is None && !reader.at_end() {
      status = "partial"
      unparsed = Some({ offset: reader.position(), size: reader.remaining(), })
      work.diagnostics.push({
        offset: reader.position(),
        custom_ordinal: custom,
        subsection_ordinal: Some(ordinal),
        reason: "trailing bytes after the declared function-name records",
      })
    }
  }
  // Framing has already been bounded by the outer name reader, even on error.
  {
    custom_ordinal: custom,
    ordinal,
    id: 1,
    encoding: { offset: start, size: reader.end - start, },
    header_bytes: payload.offset - start,
    payload,
    status,
    declared_count: count,
    count_prefix_bytes: prefix,
    parsed_record_bytes: record_bytes,
    unparsed,
  }
}

///|
fn metadata_name_content(
  data : Bytes,
  reader : Reader,
  custom : Int,
  work : MetadataWork,
) -> (String, Int, MetadataRange?) {
  let mut ordinal = 0
  let mut bounded_bytes = 0
  let mut status = "complete"
  while !reader.at_end() {
    let start = reader.position()
    let framing = fn() -> Result[(Int, Int, Int), WasmError] {
      let id = match reader.read_byte() {
        Ok(n) => n
        Err(e) => return Err(e)
      }
      let size = match read_u32_as_int(reader) {
        Ok(n) => n
        Err(e) => return Err(e)
      }
      let payload_start = reader.position()
      match reader.read_bytes(size) {
        Ok(_) => Ok((id, payload_start, size))
        Err(e) => Err(e)
      }
    }
    let (id, payload_start, size) = match framing() {
      Ok(v) => v
      Err(error) => {
        work.diagnostics.push({
          offset: start,
          custom_ordinal: custom,
          subsection_ordinal: Some(ordinal),
          reason: "unbounded name subsection: " + describe_error(error),
        })
        return (
          "partial",
          bounded_bytes,
          Some({ offset: start, size: reader.end - start, }),
        )
      }
    }
    let payload : MetadataRange = { offset: payload_start, size, }
    let subsection = if id == 1 {
      // The entire payload was consumed above before constructing this reader.
      let sub = Reader::{
        data,
        start: payload_start,
        end: reader.position(),
        pos: payload_start,
      }
      metadata_function_subsection(sub, custom, ordinal, start, payload, work)
    } else {
      MetadataNameSubsection::{
        custom_ordinal: custom,
        ordinal,
        id,
        encoding: { offset: start, size: reader.position() - start, },
        header_bytes: payload_start - start,
        payload,
        status: "opaque",
        declared_count: None,
        count_prefix_bytes: 0,
        parsed_record_bytes: 0,
        unparsed: None,
      }
    }
    if subsection.status == "partial" {
      status = "partial"
    }
    bounded_bytes += subsection.encoding.size
    work.subsections.push(subsection)
    ordinal += 1
  }
  (status, bounded_bytes, None)
}

///|
fn metadata_custom(
  data : Bytes,
  section : Section,
  ordinal : Int,
  section_ordinal : Int,
  work : MetadataWork,
) -> Result[MetadataCustom, WasmError] {
  let reader = match reader_at_payload(data, section) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  let name_start = reader.position()
  let length = match read_u32_as_int(reader) {
    Ok(n) => n
    Err(e) => return Err(e)
  }
  let name_prefix = reader.position() - name_start
  let utf8_start = reader.position()
  let bytes = match reader.read_bytes(length) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  let content : MetadataRange = {
    offset: reader.position(),
    size: reader.remaining(),
  }
  let name = metadata_utf8(bytes) // Never trust Section.custom_name's lossy label.
  let category = match name {
    None => "unavailable"
    Some("name") => "name"
    Some("producers") => "producers"
    Some(n) => if n.has_prefix(".debug_") { "debug" } else { "other" }
  }
  if name is None {
    work.diagnostics.push({
      offset: utf8_start,
      custom_ordinal: ordinal,
      subsection_ordinal: None,
      reason: "invalid UTF-8 in custom name starting here; name and classification unavailable",
    })
  }
  let (status, bounded, tail) = if category == "name" {
    metadata_name_content(data, reader, ordinal, work)
  } else {
    ("opaque", 0, None)
  }
  Ok({
    ordinal,
    section_ordinal,
    encoding: { offset: section.offset, size: section.total_size, },
    section_header_bytes: section.total_size - section.payload_size,
    name_encoding: { offset: name_start, size: content.offset - name_start, },
    name_length_prefix_bytes: name_prefix,
    name_bytes: { offset: utf8_start, size: length, },
    name,
    category,
    content,
    status,
    name_subsection_bytes: bounded,
    unbounded_tail: tail,
  })
}

///|
fn metadata_scope() -> String {
  "Actual custom section file bytes; parent and child measurements are decompositions, not additive totals. " +
  "Only reliable full UTF-8 names determine classification; .debug_ is a name prefix, not DWARF validation. " +
  "Only name subsection id 1 is interpreted deeply; other subsections, producers and debug contents remain opaque, not failed. " +
  "Physical instances and records are retained; declared function indices do not establish function existence. " +
  "Partial details retain parsed records and all remainder ranges. Exit 0 does not certify that all custom contents were interpreted or semantically validated. " +
  "Metadata may be used by debuggers, loaders or other tools. No safe deletion, recompiled or transfer savings are inferred. " +
  "Top limits text details only; summaries, diagnostics and JSON remain complete."
}

///|
/// Inspect all physical custom/name records, without changing effective-name
/// selection, invoking analyze, building a graph, or copying opaque contents.
pub fn inspect_metadata(data : Bytes) -> Result[MetadataReport, WasmError] {
  let parsed = match parse_module(data) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  let work : MetadataWork = { subsections: [], records: [], diagnostics: [], }
  let customs : Array[MetadataCustom] = []
  for section_ordinal, section in parsed.sections {
    if section.kind.is_custom() {
      match
        metadata_custom(data, section, customs.length(), section_ordinal, work) {
        Ok(c) => customs.push(c)
        Err(e) => return Err(e)
      }
    }
  }
  let mut custom_bytes = 0
  let mut section_header_bytes = 0
  let mut custom_name_encoding_bytes = 0
  let mut content_bytes = 0
  let mut unavailable_custom_count = 0
  let mut unavailable_custom_bytes = 0
  let mut name_custom_count = 0
  let mut name_content_bytes = 0
  let mut name_subsection_bytes = 0
  let mut unbounded_name_tail_bytes = 0
  let mut name_partial = false
  let by_name : Map[String, Array[Int]] = Map([])
  let unavailable : Array[Int] = []
  for item in customs {
    custom_bytes += item.encoding.size
    section_header_bytes += item.section_header_bytes
    custom_name_encoding_bytes += item.name_encoding.size
    content_bytes += item.content.size
    match item.name {
      None => {
        unavailable.push(item.ordinal)
        unavailable_custom_count += 1
        unavailable_custom_bytes += item.encoding.size
      }
      Some(name) => {
        let ids = by_name.get(name).unwrap_or([])
        ids.push(item.ordinal)
        by_name.set(name, ids)
      }
    }
    if item.category == "name" {
      name_custom_count += 1
      name_content_bytes += item.content.size
      name_subsection_bytes += item.name_subsection_bytes
      unbounded_name_tail_bytes += item.unbounded_tail
        .map(fn(r) { r.size })
        .unwrap_or(0)
      if item.status == "partial" {
        name_partial = true
      }
    }
  }
  let categories : Array[MetadataCategory] = []
  for category in ["name", "producers", "debug", "other", "unavailable"] {
    let ids : Array[Int] = []
    let mut total = 0
    for item in customs {
      if item.category == category {
        ids.push(item.ordinal)
        total += item.encoding.size
      }
    }
    categories.push({ category, custom_ordinals: ids, total_bytes: total, })
  }
  let groups : Array[MetadataNameGroup] = []
  for name, ids in by_name {
    let mut total = 0
    for id in ids {
      total += customs[id].encoding.size
    }
    groups.push({ name: Some(name), custom_ordinals: ids, total_bytes: total, })
  }
  if !unavailable.is_empty() {
    groups.push({
      name: None,
      custom_ordinals: unavailable,
      total_bytes: unavailable_custom_bytes,
    })
  }
  groups.sort_by(fn(a, b) {
    let order = b.total_bytes.compare(a.total_bytes)
    if order != 0 {
      return order
    }
    match (a.name, b.name) {
      (Some(a), Some(b)) => a.compare(b)
      (Some(_), None) => -1
      (None, Some(_)) => 1
      (None, None) => 0
    }
  })
  let mut function_name_payload_bytes = 0
  let mut function_name_count_prefix_bytes = 0
  let mut function_name_unparsed_bytes = 0
  for sub in work.subsections {
    if sub.id == 1 {
      function_name_payload_bytes += sub.payload.size
      function_name_count_prefix_bytes += sub.count_prefix_bytes
      function_name_unparsed_bytes += sub.unparsed
        .map(fn(r) { r.size })
        .unwrap_or(0)
    }
  }
  let mut parsed_function_record_bytes = 0
  let mut function_index_prefix_bytes = 0
  let mut function_name_length_prefix_bytes = 0
  let mut function_name_utf8_bytes = 0
  let mut unavailable_function_name_count = 0
  for record in work.records {
    parsed_function_record_bytes += record.encoding.size
    function_index_prefix_bytes += record.index_prefix_bytes
    function_name_length_prefix_bytes += record.name_length_prefix_bytes
    function_name_utf8_bytes += record.name_bytes.size
    if record.name is None {
      unavailable_function_name_count += 1
    }
  }
  customs.sort_by(fn(a, b) {
    let order = b.encoding.size.compare(a.encoding.size)
    if order == 0 {
      a.encoding.offset.compare(b.encoding.offset)
    } else {
      order
    }
  })
  work.records.sort_by(fn(a, b) {
    let order = b.encoding.size.compare(a.encoding.size)
    if order == 0 {
      a.encoding.offset.compare(b.encoding.offset)
    } else {
      order
    }
  })
  work.diagnostics.sort_by(fn(a, b) {
    let order = a.offset.compare(b.offset)
    if order == 0 {
      a.reason.compare(b.reason)
    } else {
      order
    }
  })
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    byte_ledger_complete: true,
    custom_name_status: if unavailable_custom_count == 0 {
      "complete"
    } else {
      "partial"
    },
    name_details_status: if name_custom_count == 0 {
      "absent"
    } else if name_partial {
      "partial"
    } else {
      "complete"
    },
    modeling_scope: metadata_scope(),
    categories,
    name_groups: groups,
    customs,
    subsections: work.subsections,
    function_names: work.records,
    diagnostics: work.diagnostics,
    summary: {
      custom_count: customs.length(),
      custom_bytes,
      section_header_bytes,
      custom_name_encoding_bytes,
      content_bytes,
      unavailable_custom_count,
      unavailable_custom_bytes,
      name_custom_count,
      name_content_bytes,
      name_subsection_bytes,
      unbounded_name_tail_bytes,
      function_name_payload_bytes,
      function_name_count_prefix_bytes,
      parsed_function_record_count: work.records.length(),
      parsed_function_record_bytes,
      function_name_unparsed_bytes,
      function_index_prefix_bytes,
      function_name_length_prefix_bytes,
      function_name_utf8_bytes,
      unavailable_function_name_count,
    },
  })
}