///|
pub fn json_escape(text : String) -> String {
  let builder = StringBuilder()
  for c in text {
    match c {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(c)
    }
  }
  builder.to_string()
}

///|
fn json_string(value : String) -> String {
  "\"\{json_escape(value)}\""
}

///|
pub fn json_span(span : Span) -> String {
  "{\"start\":\{span.start},\"end\":\{span.end}}"
}

///|
pub fn finding_to_json(finding : Finding) -> String {
  "{" +
  "\"id\":" +
  json_string(finding.id) +
  "," +
  "\"kind\":" +
  json_string(phi_kind_name(finding.kind)) +
  "," +
  "\"label\":" +
  json_string(finding.label) +
  "," +
  "\"start\":\{finding.start}," +
  "\"end\":\{finding.end}," +
  "\"text\":" +
  json_string(finding.text) +
  "," +
  "\"replacement\":" +
  json_string(finding.replacement) +
  "," +
  "\"rule_id\":" +
  json_string(finding.rule_id) +
  "," +
  "\"confidence\":\{finding.confidence}" +
  "}"
}

///|
pub fn offset_to_json(offset : OffsetMap) -> String {
  "{" +
  "\"original_start\":\{offset.original_start}," +
  "\"original_end\":\{offset.original_end}," +
  "\"replacement_start\":\{offset.replacement_start}," +
  "\"replacement_end\":\{offset.replacement_end}," +
  "\"finding_id\":" +
  json_string(offset.finding_id) +
  "}"
}

///|
pub fn map_to_json(values : Map[String, Int]) -> String {
  let pieces = []
  for key, value in values {
    pieces.push(json_string(key) + ":\{value}")
  }
  "{" + pieces.join(",") + "}"
}

///|
pub fn audit_to_json(report : AuditReport) -> String {
  let finding_json = report.findings.map(finding_to_json).join(",")
  let offset_json = report.offsets.map(offset_to_json).join(",")
  "{" +
  "\"input_length\":\{report.input_length}," +
  "\"output_length\":\{report.output_length}," +
  "\"finding_count\":\{report.finding_count}," +
  "\"applied_count\":\{report.applied_count}," +
  "\"counts\":" +
  map_to_json(report.counts) +
  "," +
  "\"findings\":[" +
  finding_json +
  "]," +
  "\"offsets\":[" +
  offset_json +
  "]" +
  "}"
}

///|
pub fn result_to_json(result : DeidResult) -> String {
  "{" +
  "\"text\":" +
  json_string(result.text) +
  "," +
  "\"findings\":[" +
  result.findings.map(finding_to_json).join(",") +
  "]," +
  "\"offsets\":[" +
  result.offsets.map(offset_to_json).join(",") +
  "]," +
  "\"audit\":" +
  audit_to_json(result.audit) +
  "}"
}

///|
pub fn audit_csv(report : AuditReport) -> String {
  let lines = ["id,kind,label,start,end,confidence,rule_id,text,replacement"]
  for finding in report.findings {
    lines.push(
      [
        csv_cell(finding.id),
        csv_cell(phi_kind_name(finding.kind)),
        csv_cell(finding.label),
        "\{finding.start}",
        "\{finding.end}",
        "\{finding.confidence}",
        csv_cell(finding.rule_id),
        csv_cell(finding.text),
        csv_cell(finding.replacement),
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
fn csv_cell(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"" + value.replace_all(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn findings_csv(findings : Array[Finding]) -> String {
  audit_csv({
    input_length: 0,
    output_length: 0,
    finding_count: findings.length(),
    applied_count: 0,
    counts: Map([]),
    findings,
    offsets: [],
  })
}

///|
pub fn audit_plain_text(report : AuditReport) -> String {
  let lines = [
    "moonbit-deid audit",
    "input length: \{report.input_length}",
    "output length: \{report.output_length}",
    "findings: \{report.finding_count}",
    "applied: \{report.applied_count}",
  ]
  for key, value in report.counts {
    lines.push("\{key}: \{value}")
  }
  lines.join("\n")
}

///|
pub fn audit_risk_level(report : AuditReport) -> RiskLevel {
  highest_risk(report.findings)
}

///|
pub fn audit_has_critical(report : AuditReport) -> Bool {
  report.findings.any(fn(item) { risk_level(item) == Critical })
}

///|
pub fn audit_is_empty(report : AuditReport) -> Bool {
  report.finding_count == 0
}

///|
pub fn audit_offset_coverage(report : AuditReport) -> Int {
  report.offsets.fold(init=0, (total, item) => {
    total + item.original_end - item.original_start
  })
}

///|
pub fn audit_replacement_delta(report : AuditReport) -> Int {
  report.output_length - report.input_length
}

///|
pub fn audit_summary_lines(report : AuditReport) -> Array[String] {
  [
    "input=\{report.input_length}",
    "output=\{report.output_length}",
    "findings=\{report.finding_count}",
    "applied=\{report.applied_count}",
    "covered=\{audit_offset_coverage(report)}",
    "risk=\{audit_risk_level(report)}",
  ]
}

///|
pub fn audit_summary(report : AuditReport) -> String {
  audit_summary_lines(report).join(" ")
}

///|
pub fn counts_as_sorted_lines(counts : Map[String, Int]) -> Array[String] {
  let lines = []
  for key, value in counts {
    lines.push("\{key}=\{value}")
  }
  lines.sort()
  lines
}

///|
pub fn audit_markdown_table(report : AuditReport) -> String {
  let lines = [
    "| ID | Kind | Span | Confidence | Replacement |", "| --- | --- | ---: | ---: | --- |",
  ]
  for item in report.findings {
    lines.push(
      "| \{item.id} | \{phi_kind_name(item.kind)} | \{item.start}..\{item.end} | \{item.confidence} | \{item.replacement} |",
    )
  }
  lines.join("\n")
}

///|
pub fn audit_report_with_context(
  input : String,
  report : AuditReport,
  radius : Int,
) -> String {
  let lines = [audit_summary(report), "", audit_markdown_table(report)]
  for item in report.findings {
    let window = finding_context(input, item, radius)
    lines.push("\{item.id}: \{quote_for_log(window.text)}")
  }
  lines.join("\n")
}

///|
pub fn policy_decisions_to_csv(decisions : Array[PolicyDecision]) -> String {
  let lines = ["finding_id,action,reason"]
  for decision in decisions {
    lines.push(
      [
        csv_cell(decision.finding_id),
        action_name(decision.action),
        csv_cell(decision.reason),
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
pub fn schema_description() -> String {
  [
    "result.text: redacted document", "result.findings[]: accepted PHI spans", "result.offsets[]: original/output coordinate mapping",
    "result.audit: deterministic aggregate counters",
  ].join("\n")
}

///|
pub fn report_checksum(report : AuditReport) -> String {
  stable_hash(audit_to_json(report))
}

///|
pub fn report_is_deterministic(left : AuditReport, right : AuditReport) -> Bool {
  audit_to_json(left) == audit_to_json(right)
}