///|
/// Audit information for a group of resilient cards.
pub(all) struct ResilientDeckAudit {
  deck : String
  expected_cards : Int
  present_cards : Int
  valid_cards : Int
  data_cards : Int
  parity_cards : Int
  missing_data : Array[Int]
  missing_parity : Array[Int]
  duplicate_indices : Array[Int]
  checksum_failures : Array[Int]
  profile_mismatches : Array[Int]
  width_failures : Array[Int]
  recoverable_now : Bool
  summary : String
} derive(Eq, @debug.Debug)

///|
/// Options for human-readable resilient sheet rendering.
pub(all) struct SheetLayout {
  columns : Int
  group_size : Int
  include_header : Bool
  include_audit : Bool
  include_checklist : Bool
} derive(Eq, @debug.Debug)

///|
/// A short estimate for manual copy work.
pub(all) struct CopyEstimate {
  cards : Int
  symbols : Int
  groups : Int
  estimated_minutes : Int
  risk_level : String
} derive(Eq, @debug.Debug)

///|
/// Return a practical default layout for printed notes.
pub fn default_sheet_layout() -> SheetLayout {
  {
    columns: 1,
    group_size: 8,
    include_header: true,
    include_audit: true,
    include_checklist: true,
  }
}

///|
/// Audit a resilient card set without attempting to repair it.
pub fn audit_resilient_deck(
  cards : Array[ResilientShardCard],
) -> ResilientDeckAudit {
  guard cards.length() > 0 else {
    return {
      deck: "",
      expected_cards: 0,
      present_cards: 0,
      valid_cards: 0,
      data_cards: 0,
      parity_cards: 0,
      missing_data: [0],
      missing_parity: [],
      duplicate_indices: [],
      checksum_failures: [],
      profile_mismatches: [],
      width_failures: [],
      recoverable_now: false,
      summary: "empty card set",
    }
  }
  let profile = cards[0]
  let total = profile.data_count + profile.parity_count
  let seen : Array[Bool] = []
  for _ in 0..= 0 && card.index < total {
      if seen[card.index] {
        duplicates.push(card.index)
      } else {
        seen[card.index] = true
      }
    }
    if card.index >= 0 && card.index < profile.data_count {
      data_cards = data_cards + 1
    } else if card.index >= profile.data_count && card.index < total {
      parity_cards = parity_cards + 1
    }
    if valid {
      valid_cards = valid_cards + 1
    }
  }
  let missing_data : Array[Int] = []
  for i in 0.. String {
  let lines : Array[String] = []
  lines.push("## Resilient Deck Audit")
  lines.push("")
  lines.push("- deck: " + audit.deck)
  lines.push("- expected cards: " + audit.expected_cards.to_string())
  lines.push("- present cards: " + audit.present_cards.to_string())
  lines.push("- valid cards: " + audit.valid_cards.to_string())
  lines.push("- data cards: " + audit.data_cards.to_string())
  lines.push("- parity cards: " + audit.parity_cards.to_string())
  lines.push("- missing data: " + render_ints_inline(audit.missing_data))
  lines.push("- missing parity: " + render_ints_inline(audit.missing_parity))
  lines.push(
    "- duplicate indices: " + render_ints_inline(audit.duplicate_indices),
  )
  lines.push(
    "- checksum failures: " + render_ints_inline(audit.checksum_failures),
  )
  lines.push(
    "- profile mismatches: " + render_ints_inline(audit.profile_mismatches),
  )
  lines.push("- width failures: " + render_ints_inline(audit.width_failures))
  lines.push("- recoverable now: " + audit.recoverable_now.to_string())
  lines.push("")
  lines.push(audit.summary)
  lines.join("\n")
}

///|
/// Render resilient cards with optional audit and checklist sections.
pub fn render_resilient_sheet_with_layout(
  cards : Array[ResilientShardCard],
  layout : SheetLayout,
) -> String {
  let clean_layout = normalize_layout(layout)
  let lines : Array[String] = []
  if clean_layout.include_header {
    lines.push("# resilient-shard-note")
    lines.push("")
  }
  let grouped = render_grouped_cards(cards, clean_layout.group_size)
  for line in grouped {
    lines.push(line)
  }
  if clean_layout.include_audit {
    lines.push("")
    lines.push(render_resilient_audit(audit_resilient_deck(cards)))
  }
  if clean_layout.include_checklist {
    lines.push("")
    lines.push(render_recovery_checklist(cards))
  }
  lines.join("\n")
}

///|
/// Render cards as a Markdown table for README examples and issue comments.
pub fn render_resilient_markdown_table(
  cards : Array[ResilientShardCard],
) -> String {
  let lines : Array[String] = [
    "| index | role | checksum | cells |", "| ---: | --- | ---: | --- |",
  ]
  for card in cards {
    lines.push(
      "| " +
      card.index.to_string() +
      " | " +
      resilient_public_role(card) +
      " | " +
      card.checksum.to_string() +
      " | `" +
      hex_cells(card.cells) +
      "` |",
    )
  }
  lines.join("\n")
}

///|
/// Render a manual recovery checklist.
pub fn render_recovery_checklist(cards : Array[ResilientShardCard]) -> String {
  guard cards.length() > 0 else {
    return "## Recovery Checklist\n\n- [ ] Add at least one card."
  }
  let audit = audit_resilient_deck(cards)
  let lines : Array[String] = []
  lines.push("## Recovery Checklist")
  lines.push("")
  lines.push("- [ ] Confirm every card starts with `RSN1|`.")
  lines.push("- [ ] Confirm deck id is `" + audit.deck + "` on every card.")
  lines.push(
    "- [ ] Confirm missing data indices are " +
    render_ints_inline(audit.missing_data) +
    ".",
  )
  lines.push(
    "- [ ] Confirm checksum failures are " +
    render_ints_inline(audit.checksum_failures) +
    ".",
  )
  if audit.recoverable_now {
    lines.push("- [ ] Run `recover_resilient` or `recover_resilient_sheet`.")
  } else {
    lines.push("- [ ] Collect more cards before attempting recovery.")
  }
  lines.join("\n")
}

///|
/// Drop cards with selected indices. This is used by examples and tests.
pub fn drop_resilient_indices(
  cards : Array[ResilientShardCard],
  indices : Array[Int],
) -> Array[ResilientShardCard] {
  let out : Array[ResilientShardCard] = []
  for card in cards {
    if !indices.contains(card.index) {
      out.push(card)
    }
  }
  out
}

///|
/// Keep only selected card indices, preserving original card order.
pub fn keep_resilient_indices(
  cards : Array[ResilientShardCard],
  indices : Array[Int],
) -> Array[ResilientShardCard] {
  let out : Array[ResilientShardCard] = []
  for card in cards {
    if indices.contains(card.index) {
      out.push(card)
    }
  }
  out
}

///|
/// Create a copy of a card with one cell changed.
pub fn corrupt_resilient_cell(
  card : ResilientShardCard,
  offset : Int,
  value : Int,
) -> ResilientShardCard {
  let cells : Array[Int] = []
  for cell in card.cells {
    cells.push(cell)
  }
  if offset >= 0 && offset < cells.length() {
    cells[offset] = normalize_byte(value)
  }
  {
    deck: card.deck,
    index: card.index,
    data_count: card.data_count,
    parity_count: card.parity_count,
    width: card.width,
    original_len: card.original_len,
    checksum: card.checksum,
    cells,
  }
}

///|
/// Return a corrected copy of a card after a deliberate cell edit.
pub fn rewrite_resilient_cell(
  card : ResilientShardCard,
  offset : Int,
  value : Int,
) -> ResilientShardCard {
  let cells : Array[Int] = []
  for cell in card.cells {
    cells.push(cell)
  }
  if offset >= 0 && offset < cells.length() {
    cells[offset] = normalize_byte(value)
  }
  resilient_public_card_from_cells(card, cells)
}

///|
/// Estimate manual copy effort for a group of cards.
pub fn estimate_copy_work(cards : Array[ResilientShardCard]) -> CopyEstimate {
  let mut symbols = 0
  for card in cards {
    symbols = symbols + card.cells.length() * 2
  }
  let groups = ceil_div(symbols, 8)
  let estimated_minutes = max_int(1, ceil_div(groups, 12))
  {
    cards: cards.length(),
    symbols,
    groups,
    estimated_minutes,
    risk_level: copy_risk_level(symbols, cards.length()),
  }
}

///|
/// Render copy effort as one line for CLI output.
pub fn render_copy_estimate(estimate : CopyEstimate) -> String {
  "cards=" +
  estimate.cards.to_string() +
  " symbols=" +
  estimate.symbols.to_string() +
  " groups=" +
  estimate.groups.to_string() +
  " estimated-minutes=" +
  estimate.estimated_minutes.to_string() +
  " risk=" +
  estimate.risk_level
}

///|
/// Return true when all visible cards share the same profile.
pub fn resilient_profiles_match(cards : Array[ResilientShardCard]) -> Bool {
  guard cards.length() > 0 else { return true }
  let profile = cards[0]
  for card in cards {
    if !same_resilient_public_profile(profile, card) {
      return false
    }
  }
  true
}

///|
/// Count visible cards by role as `[data, parity, other]`.
pub fn count_resilient_roles(cards : Array[ResilientShardCard]) -> Array[Int] {
  guard cards.length() > 0 else { return [0, 0, 0] }
  let profile = cards[0]
  let counts = [0, 0, 0]
  for card in cards {
    if card.index >= 0 && card.index < profile.data_count {
      counts[0] = counts[0] + 1
    } else if card.index >= profile.data_count &&
      card.index < profile.data_count + profile.parity_count {
      counts[1] = counts[1] + 1
    } else {
      counts[2] = counts[2] + 1
    }
  }
  counts
}

///|
/// Compute a lightweight payload fingerprint for logs and examples.
pub fn payload_fingerprint(payload : Array[Int]) -> String {
  let mut a = 1
  let mut b = 0
  for value in payload {
    a = (a + normalize_byte(value)) % 65521
    b = (b + a) % 65521
  }
  let combined = (b * 257 + a) % 65521
  combined.to_string()
}

///|
/// Compare two payloads and return mismatch offsets.
pub fn payload_diff_offsets(
  left : Array[Int],
  right : Array[Int],
) -> Array[Int] {
  let out : Array[Int] = []
  let max_len = max_int(left.length(), right.length())
  for i in 0.. String {
  let mut min_value = 255
  let mut max_value = 0
  let mut zero_count = 0
  for value in payload {
    let byte = normalize_byte(value)
    if byte < min_value {
      min_value = byte
    }
    if byte > max_value {
      max_value = byte
    }
    if byte == 0 {
      zero_count = zero_count + 1
    }
  }
  "len=" +
  payload.length().to_string() +
  " min=" +
  min_value.to_string() +
  " max=" +
  max_value.to_string() +
  " zeros=" +
  zero_count.to_string() +
  " fp=" +
  payload_fingerprint(payload)
}

///|
/// Build a deterministic payload for demos and scenario tests.
pub fn deterministic_payload(seed : Int, len : Int) -> Array[Int] {
  let clean_len = max_int(0, len)
  let out : Array[Int] = []
  let mut state = normalize_byte(seed)
  for i in 0.. Bool {
  left.deck == right.deck &&
  left.data_count == right.data_count &&
  left.parity_count == right.parity_count &&
  left.width == right.width &&
  left.original_len == right.original_len
}

///|
fn resilient_public_role(card : ResilientShardCard) -> String {
  if card.index < card.data_count {
    "data"
  } else if card.index < card.data_count + card.parity_count {
    "parity"
  } else {
    "unknown"
  }
}

///|
fn resilient_public_card_from_cells(
  source : ResilientShardCard,
  cells : Array[Int],
) -> ResilientShardCard {
  resilient_card(
    source.deck,
    source.index,
    source.data_count,
    source.parity_count,
    source.width,
    source.original_len,
    cells,
  )
}

///|
fn normalize_layout(layout : SheetLayout) -> SheetLayout {
  {
    columns: clamp(layout.columns, 1, 4),
    group_size: clamp(layout.group_size, 2, 32),
    include_header: layout.include_header,
    include_audit: layout.include_audit,
    include_checklist: layout.include_checklist,
  }
}

///|
fn render_grouped_cards(
  cards : Array[ResilientShardCard],
  group_size : Int,
) -> Array[String] {
  let out : Array[String] = []
  for card in cards {
    out.push(group_card_line(render_resilient_card(card), group_size))
  }
  out
}

///|
fn group_card_line(line : String, group_size : Int) -> String {
  match line.split_once("x=") {
    Some((prefix, hex_view)) => {
      let hex = hex_view.to_owned()
      prefix.to_owned() + "x=" + group_hex(hex, group_size)
    }
    None => line
  }
}

///|
fn group_hex(hex : String, group_size : Int) -> String {
  let parts : Array[String] = []
  let mut start = 0
  while start < hex.length() {
    let end = min_int(start + group_size, hex.length())
    parts.push(hex.sub(start~, end~).to_owned())
    start = end
  }
  parts.join(" ")
}

///|
fn audit_summary_text(
  deck : String,
  present : Int,
  expected : Int,
  missing_data : Array[Int],
  missing_parity : Array[Int],
  checksum_failures : Array[Int],
  recoverable_now : Bool,
) -> String {
  if checksum_failures.length() > 0 {
    return "deck " + deck + " has checksum failures; fix copied cards first"
  }
  if recoverable_now {
    return "deck " +
      deck +
      " is recoverable with " +
      present.to_string() +
      "/" +
      expected.to_string() +
      " visible cards"
  }
  "deck " +
  deck +
  " is not recoverable yet; missing data " +
  render_ints_inline(missing_data) +
  ", missing parity " +
  render_ints_inline(missing_parity)
}

///|
fn copy_risk_level(symbols : Int, cards : Int) -> String {
  if cards <= 0 {
    "empty"
  } else if symbols <= 64 {
    "low"
  } else if symbols <= 256 {
    "medium"
  } else {
    "high"
  }
}