///|
pub struct DuplicateFunctionMember {
  index : Int
  name : String
  raw_name : String?
  type_index : Int
  /// File offset of the actual body-length prefix.
  offset : Int
  body_offset : Int
  body_size : Int
  prefix_size : Int
  total_size : Int
}

///|
pub struct DuplicateFunctionGroup {
  sha256 : String
  body_size : Int
  member_count : Int
  member_body_bytes : Int
  member_prefix_bytes : Int
  member_total_bytes : Int
  members : Array[DuplicateFunctionMember]
}

///|
pub struct DuplicateFunctionsSummary {
  local_function_count : Int
  duplicate_group_count : Int
  duplicate_member_count : Int
  singleton_function_count : Int
  total_body_bytes : Int
  duplicate_body_bytes : Int
  singleton_body_bytes : Int
  total_prefix_bytes : Int
  total_encoded_bytes : Int
}

///|
pub struct DuplicateFunctionsReport {
  file_size : Int
  sha256 : String
  comparison_scope : String
  summary : DuplicateFunctionsSummary
  groups : Array[DuplicateFunctionGroup]
}

///|
/// Temporary original-file views only; public results retain ranges, not bodies.
priv struct DuplicateBodyBucket {
  sha256 : String
  bytes : BytesView
  members : Array[DuplicateFunctionMember]
}

///|
/// Read type references without interpreting the type or GC sections.
fn duplicate_type_indices(
  data : Bytes,
  parsed : WasmModule,
) -> Result[Array[Int], WasmError] {
  for section in parsed.sections {
    if same_kind(section.kind, Function) {
      let reader = match reader_at_payload(data, section) {
        Ok(r) => r
        Err(e) => return Err(e)
      }
      let count = match read_u32_as_int(reader) {
        Ok(n) => n
        Err(e) => return Err(e)
      }
      let indices : Array[Int] = []
      for _ in 0.. indices.push(n)
          Err(e) => return Err(e)
        }
      }
      match finish_section(reader, Function) {
        Ok(_) => return Ok(indices)
        Err(e) => return Err(e)
      }
    }
  }
  Ok([])
}

///|
/// The bounded reader validates offset/total before addition or slicing. Check
/// subtraction before obtaining the prefix, then advance through that prefix.
fn duplicate_body_view(
  data : Bytes,
  body : FunctionBody,
) -> Result[(Int, Int, BytesView), WasmError] {
  let reader = match Reader::new_range(data, body.offset, body.total_size) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  if body.body_size < 0 || body.body_size > body.total_size {
    return Err(
      InvalidReadRange(body.offset, body.body_size, reader.start, reader.end),
    )
  }
  let prefix_size = body.total_size - body.body_size
  match reader.read_bytes(prefix_size) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  let body_offset = reader.position()
  match reader.read_bytes(body.body_size) {
    Ok(bytes) => Ok((body_offset, prefix_size, bytes))
    Err(e) => Err(e)
  }
}

///|
/// Digest plus length selects a bucket; actual bytes decide membership. Kept
/// separate from hashing so collision handling is testable without a public knob.
fn duplicate_insert(
  buckets : Array[DuplicateBodyBucket],
  by_digest : Map[String, Array[Int]],
  digest : String,
  bytes : BytesView,
  definition : DuplicateFunctionMember,
) -> Unit {
  let key = digest + ":" + bytes.length().to_string()
  let candidates = by_digest.get(key).unwrap_or([])
  for index in candidates {
    if data_payload_equal(buckets[index].bytes, bytes) {
      buckets[index].members.push(definition)
      return
    }
  }
  candidates.push(buckets.length())
  by_digest.set(key, candidates)
  buckets.push({ sha256: digest, bytes, members: [definition], })
}

///|
fn duplicate_groups(
  buckets : Array[DuplicateBodyBucket],
) -> Array[DuplicateFunctionGroup] {
  let groups : Array[DuplicateFunctionGroup] = []
  for bucket in buckets {
    if bucket.members.length() < 2 {
      continue
    }
    bucket.members.sort_by(fn(a, b) { a.index.compare(b.index) })
    let mut body_bytes = 0
    let mut prefix_bytes = 0
    let mut encoded_bytes = 0
    for definition in bucket.members {
      body_bytes += definition.body_size
      prefix_bytes += definition.prefix_size
      encoded_bytes += definition.total_size
    }
    groups.push({
      sha256: bucket.sha256,
      body_size: bucket.bytes.length(),
      member_count: bucket.members.length(),
      member_body_bytes: body_bytes,
      member_prefix_bytes: prefix_bytes,
      member_total_bytes: encoded_bytes,
      members: bucket.members,
    })
  }
  groups.sort_by(fn(a, b) {
    let total = b.member_body_bytes.compare(a.member_body_bytes)
    if total != 0 {
      return total
    }
    let length = b.body_size.compare(a.body_size)
    if length != 0 {
      return length
    }
    let hash = a.sha256.compare(b.sha256)
    if hash != 0 {
      return hash
    }
    a.members[0].index.compare(b.members[0].index)
  })
  groups
}

///|
fn duplicate_scope() -> String {
  "Compare exact local function body bytes: locals declarations plus instructions, excluding the body-length prefix. " +
  "SHA-256 and length select candidates; actual bytes confirm every match. No normalization; names and type indices do not determine membership. " +
  "Identical bytes do not prove semantic/type/function-identity equivalence or safe merging; no identical bodies does not prove no semantic duplication. " +
  "Imported functions are excluded. Encoded totals include actual body-length prefixes but exclude code section header/count and all other sections. " +
  "No deletion, recompiled or gzip savings are estimated. No call graph, retained computation or full type/instruction validation is performed. " +
  "Top limits displayed groups only, not analysis cost; summaries and JSON remain complete."
}

///|
/// Structural size-only inspection; no analyze, graph or path query.
pub fn inspect_duplicate_functions(
  data : Bytes,
) -> Result[DuplicateFunctionsReport, WasmError] {
  let parsed = match parse_module(data) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  let sections = find_sections(parsed)
  let types = match duplicate_type_indices(data, parsed) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let imported = match sections.imports {
    None => 0
    Some(section) =>
      match count_imported_functions(data, section) {
        Ok(n) => n
        Err(e) => return Err(e)
      }
  }
  let bodies = match sections.code {
    None => []
    Some(section) =>
      match parse_code_section(data, section, base_index=imported) {
        Ok(v) => v
        Err(e) => return Err(e)
      }
  }
  // parse_module has already checked this; keep index correspondence explicit.
  if types.length() != bodies.length() {
    for section in parsed.sections {
      if same_kind(section.kind, Function) || same_kind(section.kind, Code) {
        return Err(
          FunctionCodeCountMismatch(
            section.offset,
            section.kind,
            types.length(),
            bodies.length(),
          ),
        )
      }
    }
  }
  let names = match sections.names {
    None => None
    Some(section) => parse_name_section(data, section)
  }
  let stats = function_stats(bodies, names)
  let buckets : Array[DuplicateBodyBucket] = []
  let by_digest : Map[String, Array[Int]] = Map([])
  let mut total_body_bytes = 0
  let mut total_prefix_bytes = 0
  let mut total_encoded_bytes = 0
  for ordinal, body in bodies {
    let (body_offset, prefix_size, bytes) = match
      duplicate_body_view(data, body) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    // crypto accepts Bytes, so this one hash uses a temporary owned copy.
    // Only the original-file view is retained while grouping.
    let digest = experiment_sha256(bytes.to_owned())
    duplicate_insert(buckets, by_digest, digest, bytes, {
      index: body.index,
      name: stats[ordinal].name,
      raw_name: stats[ordinal].raw_name,
      type_index: types[ordinal],
      offset: body.offset,
      body_offset,
      body_size: body.body_size,
      prefix_size,
      total_size: body.total_size,
    })
    total_body_bytes += body.body_size
    total_prefix_bytes += prefix_size
    total_encoded_bytes += body.total_size
  }
  let groups = duplicate_groups(buckets)
  let mut duplicate_member_count = 0
  let mut duplicate_body_bytes = 0
  for group in groups {
    duplicate_member_count += group.member_count
    duplicate_body_bytes += group.member_body_bytes
  }
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    comparison_scope: duplicate_scope(),
    groups,
    summary: {
      local_function_count: bodies.length(),
      duplicate_group_count: groups.length(),
      duplicate_member_count,
      singleton_function_count: bodies.length() - duplicate_member_count,
      total_body_bytes,
      duplicate_body_bytes,
      singleton_body_bytes: total_body_bytes - duplicate_body_bytes,
      total_prefix_bytes,
      total_encoded_bytes,
    },
  })
}