///|
/// Escapes a string for use as a JSON string value.
pub fn json_escape(value : String) -> String {
  let builder = StringBuilder(size_hint=value.length())
  for index = 0; index < value.length(); index = index + 1 {
    match value.unsafe_get(index) {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      '\b' => builder.write_string("\\b")
      '\u{000C}' => builder.write_string("\\f")
      char => builder.write_char(char.to_int().unsafe_to_char())
    }
  }
  builder.to_string()
}

///|
/// Encodes one journal event. `payload_json` must return a valid JSON value.
pub fn[P] event_to_json(
  event : JournalEvent[P],
  payload_json : (P) -> String,
) -> String {
  "{\"sequence\":\{event.sequence},\"kind\":\"\{json_escape(event.kind)}\",\"payload\":\{payload_json(event.payload)},\"correlation_id\":\"\{json_escape(event.correlation_id)}\",\"previous_hash\":\{event.previous_hash},\"hash\":\{event.hash}}"
}

///|
/// Encodes a journal as a stable JSON array.
pub fn[P] journal_to_json(
  journal : Journal[P],
  payload_json : (P) -> String,
) -> String {
  let builder = StringBuilder()
  builder.write_char('[')
  let events = journal.events()
  for index = 0; index < events.length(); index = index + 1 {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write_string(event_to_json(events[index], payload_json))
  }
  builder.write_char(']')
  builder.to_string()
}

///|
/// Returns a stable machine-readable label for a replay status.
pub fn replay_status_name(status : ReplayStatus) -> String {
  match status {
    Completed => "completed"
    Rejected(_, _) => "rejected"
    InvalidJournal(_) => "invalid_journal"
    InvalidCheckpoint(_) => "invalid_checkpoint"
  }
}

///|
/// Returns a stable machine-readable label for a divergence kind.
pub fn divergence_kind_name(kind : DivergenceKind) -> String {
  match kind {
    StateMismatch => "state_mismatch"
    StatusMismatch => "status_mismatch"
    LengthMismatch => "length_mismatch"
    EventMismatch => "event_mismatch"
  }
}

///|
/// Encodes the summary and final state of a replay report.
///
/// `state_json` must return a valid JSON value.
pub fn[S] replay_report_to_json(
  report : ReplayReport[S],
  state_json : (S) -> String,
) -> String {
  "{\"status\":\"\{replay_status_name(report.status)}\",\"initial_sequence\":\{report.initial_sequence},\"applied_events\":\{report.applied_events},\"final_sequence\":\{report.final_sequence},\"final_state\":\{state_json(report.final_state)},\"final_state_hash\":\{report.final_state_hash},\"journal_tail_hash\":\{report.journal_tail_hash}}"
}

///|
/// Encodes the portable summary of a replay evidence tree.
pub fn replay_evidence_to_json(tree : ReplayEvidenceTree) -> String {
  "{\"event_count\":\{tree.event_count},\"root\":\{tree.root},\"levels\":\{tree.levels.length()}}"
}

///|
/// Encodes an inclusion proof without application state serialization.
pub fn replay_inclusion_proof_to_json(proof : ReplayInclusionProof) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "{\"sequence\":\{proof.sequence},\"event_hash\":\{proof.event_hash},\"state_hash\":\{proof.state_hash},\"leaf_hash\":\{proof.leaf_hash},\"root\":\{proof.root},\"path\":[",
  )
  for index = 0; index < proof.siblings.length(); index = index + 1 {
    if index > 0 {
      builder.write_char(',')
    }
    let side = if proof.sibling_on_left[index] { "left" } else { "right" }
    builder.write_string(
      "{\"sibling\":\{proof.siblings[index]},\"side\":\"\{side}\"}",
    )
  }
  builder.write_string("]}")
  builder.to_string()
}