///|
fn data_group_kind(kind : DataPayloadGroupKind) -> String {
  match kind {
    SharedPayload => "shared_payload"
    BaselineOnly => "baseline_only"
    CurrentOnly => "current_only"
  }
}

///|
fn data_diff_scope() -> String {
  "Exact payload content groups are not source or stable entry identities. Equal payloads do not prove equal initialization semantics; duplicates do not prove safe deduplication. " +
  "Raw byte differences do not predict recompiled or whole-file gzip savings. " +
  data_scope()
}

///|
/// Display only: absolute encoded delta descending, full hash, length, local id.
fn ranked_data_groups(
  report : DataSegmentsComparison,
) -> Array[DataPayloadGroup] {
  let groups = report.groups.copy()
  groups.sort_by(fn(a, b) {
    let magnitude = delta_magnitude(b.delta.encoded_size) -
      delta_magnitude(a.delta.encoded_size)
    if magnitude != 0 {
      return magnitude
    }
    let hash = diff_identity_compare(a.sha256, b.sha256)
    if hash != 0 {
      return hash
    }
    if a.payload_length != b.payload_length {
      return a.payload_length - b.payload_length
    }
    a.id - b.id
  })
  groups
}

///|
/// Paths are display labels; complete file digests identify the compared bytes.
pub fn render_data_segments_diff(
  report : DataSegmentsComparison,
  baseline_path : String,
  current_path : String,
  top : Int,
) -> String {
  let out = StringBuilder()
  out.write_string("DATA SEGMENT CONTENT COMPARISON\n")
  out.write_string(
    "  baseline: " +
    baseline_path +
    "\n    SHA-256 " +
    report.baseline_sha256 +
    "\n",
  )
  out.write_string(
    "  current:  " +
    current_path +
    "\n    SHA-256 " +
    report.current_sha256 +
    "\n",
  )
  out.write_string(
    "  data section B: before " +
    report.baseline.section_size.to_string() +
    "; after " +
    report.current.section_size.to_string() +
    "; delta " +
    report.delta.section_size.to_string() +
    "\n",
  )
  out.write_string(
    "  delta (current - baseline): payload " +
    report.delta.payload_size.to_string() +
    " B; entry metadata " +
    report.delta.entry_metadata_size.to_string() +
    " B; section overhead " +
    report.delta.section_overhead_size.to_string() +
    " B\n",
  )
  for
    (label, side) in [
      ("baseline", report.baseline),
      ("current", report.current),
    ] {
    out.write_string(
      "  " +
      label +
      ": data " +
      (if side.section is Some(_) { "present" } else { "absent" }) +
      "; file offset " +
      (match side.section {
        Some(section) => section.offset.to_string()
        None => "n/a"
      }) +
      "; payload " +
      side.payload_size.to_string() +
      "; entry metadata " +
      side.entry_metadata_size.to_string() +
      "; section overhead " +
      (side.header_size + side.count_size).to_string() +
      " B; datacount " +
      (match side.datacount {
        Some(n) => n.to_string()
        None => "absent"
      }) +
      " (separate)\n",
    )
  }
  let groups = ranked_data_groups(report)
  let shown = if top < 0 {
    0
  } else if top < groups.length() {
    top
  } else {
    groups.length()
  }
  out.write_string(
    "  Showing " +
    shown.to_string() +
    " of " +
    groups.length().to_string() +
    " content groups; Top affects display only.\n",
  )
  out.write_string(
    "  PAYLOAD HASH  LENGTH  BEFORE COUNT  AFTER COUNT  PAYLOAD DELTA  ENCODED DELTA\n",
  )
  for i = 0; i < shown; i = i + 1 {
    let group = groups[i]
    // Prefix is only a display aid; local group id remains explicit.
    let prefix = @utf8.decode_lossy(@utf8.encode(group.sha256)[0:12])
    out.write_string(
      "  " +
      prefix +
      "  " +
      pad_left(group.payload_length.to_string(), 6) +
      "  " +
      pad_left(group.baseline.count.to_string(), 12) +
      "  " +
      pad_left(group.current.count.to_string(), 11) +
      "  " +
      pad_left(group.delta.payload_size.to_string(), 13) +
      "  " +
      pad_left(group.delta.encoded_size.to_string(), 13) +
      "\n",
    )
    out.write_string(
      "    group " +
      group.id.to_string() +
      " " +
      data_group_kind(group.kind) +
      "; metadata delta " +
      group.delta.metadata_size.to_string() +
      " B\n",
    )
  }
  out.write_string("\n  " + data_diff_scope())
  out.to_string()
}

///|
fn data_group_totals_json(totals : DataGroupTotals) -> Json {
  Json::object({
    "count": int_json(totals.count),
    "payload_size": int_json(totals.payload_size),
    "encoded_size": int_json(totals.encoded_size),
    "metadata_size": int_json(totals.metadata_size),
  })
}

///|
pub fn render_data_segments_diff_json(
  report : DataSegmentsComparison,
  baseline_path : String,
  current_path : String,
) -> String {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-data-segments-diff"))
  root.set("schema_version", int_json(1))
  root.set("scope", Json::string(data_diff_scope()))
  for
    (key, label, hash, details) in [
      ("baseline", baseline_path, report.baseline_sha256, report.baseline),
      ("current", current_path, report.current_sha256, report.current),
    ] {
    root.set(
      key,
      Json::object({
        "path": Json::string(label),
        "sha256": Json::string(hash),
        "details": data_segments_json(details),
      }),
    )
  }
  root.set(
    "delta",
    Json::object({
      "section_size": int_json(report.delta.section_size),
      "payload_size": int_json(report.delta.payload_size),
      "entry_metadata_size": int_json(report.delta.entry_metadata_size),
      "section_overhead_size": int_json(report.delta.section_overhead_size),
    }),
  )
  root.set(
    "groups",
    Json::array(
      report.groups.map(fn(group) {
        Json::object({
          "id": int_json(group.id),
          "sha256": Json::string(group.sha256),
          "payload_length": int_json(group.payload_length),
          "kind": Json::string(data_group_kind(group.kind)),
          "baseline_indices": Json::array(group.baseline_indices.map(int_json)),
          "current_indices": Json::array(group.current_indices.map(int_json)),
          "baseline": data_group_totals_json(group.baseline),
          "current": data_group_totals_json(group.current),
          "delta": data_group_totals_json(group.delta),
        })
      }),
    ),
  )
  Json::object(root).stringify(indent=2) + "\n"
}