///|
/// Render a concise Markdown report for reviews and release notes.
pub fn AnalysisReport::to_markdown(self : AnalysisReport) -> String {
  let out = StringBuilder()
  out.write_string("# EvoWitness compatibility report\n\n")
  out.write_string(
    "- Contract: `" + markdown_escape(self.contract_name) + "`\n",
  )
  out.write_string(
    "- Versions: `" +
    markdown_escape(self.old_version) +
    "` → `" +
    markdown_escape(self.new_version) +
    "`\n",
  )
  out.write_string("- Mode: `" + self.mode.render() + "`\n")
  out.write_string("- Verdict: **" + self.verdict().to_upper() + "**\n")
  out.write_string(
    "- Findings: " + self.breaking_count().to_string() + " breaking, ",
  )
  out.write_string(self.warning_count().to_string() + " warnings, ")
  out.write_string(self.info_count().to_string() + " informational\n\n")
  if self.changes.is_empty() {
    out.write_string("No compatibility-relevant changes were detected.\n")
    return out.to_string()
  }
  render_summary_section(
    out,
    "Summary by severity",
    severity_summary(self.changes),
  )
  render_summary_section(
    out,
    "Summary by change family",
    family_summary(self.changes),
  )
  out.write_string("| Severity | Direction | Code | Path | Finding |\n")
  out.write_string("|---|---|---|---|---|\n")
  for change in self.changes {
    out.write_string("| " + change.severity.render() + " | ")
    out.write_string(change.direction + " | `" + change.code + "` | `")
    out.write_string(markdown_escape(change.path) + "` | ")
    out.write_string(markdown_escape(change.message) + " |\n")
  }
  for change in self.changes {
    guard change.witness is Some(witness) else { continue }
    out.write_string(
      "\n## " + change.code + " at `" + markdown_escape(change.path) + "`\n\n",
    )
    out.write_string(change.message + "\n\n")
    out.write_string("Minimal counterexample:\n\n```json\n")
    out.write_string(witness.payload + "\n```\n\n")
    out.write_string("Accepted by `" + witness.accepted_by + "`, rejected by `")
    out.write_string(witness.rejected_by + "`: " + witness.reason + ".\n\n")
    out.write_string("Suggested migration: " + change.hint + "\n")
  }
  out.to_string()
}

///|
/// Render a stable JSON document for custom CI integrations.
pub fn AnalysisReport::to_json(self : AnalysisReport) -> String {
  let findings = self.changes.map(change_to_json)
  "{" +
  "\"schemaVersion\":\"1.0\"," +
  "\"contract\":\"" +
  json_escape(self.contract_name) +
  "\"," +
  "\"oldVersion\":\"" +
  json_escape(self.old_version) +
  "\"," +
  "\"newVersion\":\"" +
  json_escape(self.new_version) +
  "\"," +
  "\"mode\":\"" +
  self.mode.render() +
  "\"," +
  "\"verdict\":\"" +
  self.verdict() +
  "\"," +
  "\"summary\":{" +
  "\"breaking\":" +
  self.breaking_count().to_string() +
  "," +
  "\"warning\":" +
  self.warning_count().to_string() +
  "," +
  "\"info\":" +
  self.info_count().to_string() +
  "}," +
  "\"changes\":[" +
  findings.join(",") +
  "]}"
}

///|
/// Render SARIF 2.1.0 so findings can be uploaded to code-scanning systems.
pub fn AnalysisReport::to_sarif(self : AnalysisReport) -> String {
  let results = self.changes.map(change_to_sarif)
  "{" +
  "\"version\":\"2.1.0\"," +
  "\"$schema\":\"https://json.schemastore.org/sarif-2.1.0.json\"," +
  "\"runs\":[{" +
  "\"tool\":{\"driver\":{" +
  "\"name\":\"EvoWitness\"," +
  "\"informationUri\":\"https://github.com/CJR-ai-nb/evowitness\"," +
  "\"semanticVersion\":\"0.1.0\"}}," +
  "\"properties\":{" +
  "\"contract\":\"" +
  json_escape(self.contract_name) +
  "\"," +
  "\"oldVersion\":\"" +
  json_escape(self.old_version) +
  "\"," +
  "\"newVersion\":\"" +
  json_escape(self.new_version) +
  "\"," +
  "\"mode\":\"" +
  self.mode.render() +
  "\"}," +
  "\"results\":[" +
  results.join(",") +
  "]}]}"
}

///|
/// Render one parse error in a compiler-style single-line format.
pub fn ParseError::render(self : ParseError) -> String {
  self.code +
  " at " +
  self.line.to_string() +
  ":" +
  self.column.to_string() +
  ": " +
  self.message
}

///|
fn change_to_json(change : Change) -> String {
  let witness = match change.witness {
    None => "null"
    Some(value) =>
      "{" +
      "\"payload\":" +
      json_string(value.payload) +
      "," +
      "\"acceptedBy\":" +
      json_string(value.accepted_by) +
      "," +
      "\"rejectedBy\":" +
      json_string(value.rejected_by) +
      "," +
      "\"reason\":" +
      json_string(value.reason) +
      "}"
  }
  "{" +
  "\"code\":" +
  json_string(change.code) +
  "," +
  "\"severity\":" +
  json_string(change.severity.render()) +
  "," +
  "\"direction\":" +
  json_string(change.direction) +
  "," +
  "\"path\":" +
  json_string(change.path) +
  "," +
  "\"message\":" +
  json_string(change.message) +
  "," +
  "\"hint\":" +
  json_string(change.hint) +
  "," +
  "\"witness\":" +
  witness +
  "}"
}

///|
fn change_to_sarif(change : Change) -> String {
  let level = match change.severity {
    Breaking => "error"
    Warning => "warning"
    Info => "note"
  }
  let witness = match change.witness {
    Some(value) => json_string(value.payload)
    None => "null"
  }
  "{" +
  "\"ruleId\":" +
  json_string(change.code) +
  "," +
  "\"level\":" +
  json_string(level) +
  "," +
  "\"message\":{\"text\":" +
  json_string(change.message) +
  "}," +
  "\"logicalLocations\":[{" +
  "\"fullyQualifiedName\":" +
  json_string(change.path) +
  "," +
  "\"kind\":\"data contract\"}]," +
  "\"properties\":{" +
  "\"direction\":" +
  json_string(change.direction) +
  "," +
  "\"migrationHint\":" +
  json_string(change.hint) +
  "," +
  "\"witness\":" +
  witness +
  "}}"
}

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

///|
fn markdown_escape(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}

///|
fn render_summary_section(
  out : StringBuilder,
  title : String,
  rows : Array[SummaryRow],
) -> Unit {
  out.write_string("## " + title + "\n\n")
  out.write_string("| Category | Breaking | Warning | Info | Total |\n")
  out.write_string("|---|---|---|---|---|\n")
  for row in rows {
    out.write_string("| " + markdown_escape(row.label) + " | ")
    out.write_string(row.breaking.to_string() + " | ")
    out.write_string(row.warning.to_string() + " | ")
    out.write_string(row.info.to_string() + " | ")
    out.write_string(row.total.to_string() + " |\n")
  }
  out.write_string("\n")
}

///|
fn severity_summary(changes : Array[Change]) -> Array[SummaryRow] {
  let rows : Array[SummaryRow] = []
  for change in changes {
    bump_summary_row(rows, change.severity.render(), change.severity)
  }
  sort_summary_rows(rows)
  rows
}

///|
fn family_summary(changes : Array[Change]) -> Array[SummaryRow] {
  let rows : Array[SummaryRow] = []
  for change in changes {
    bump_summary_row(rows, family_label(change.code), change.severity)
  }
  sort_summary_rows(rows)
  rows
}

///|
fn bump_summary_row(
  rows : Array[SummaryRow],
  label : String,
  severity : Severity,
) -> Unit {
  match rows.search_by(row => row.label == label) {
    Some(index) => {
      let row = rows[index]
      rows[index] = row.bump(severity)
    }
    None => rows.push(new_summary_row(label).bump(severity))
  }
}

///|
fn sort_summary_rows(rows : Array[SummaryRow]) -> Unit {
  rows.sort_by((left, right) => {
    let total_order = right.total - left.total
    if total_order != 0 {
      total_order
    } else if right.breaking != left.breaking {
      right.breaking - left.breaking
    } else if right.warning != left.warning {
      right.warning - left.warning
    } else if right.info != left.info {
      right.info - left.info
    } else {
      left.label.compare(right.label)
    }
  })
}

///|
fn family_label(code : String) -> String {
  if code.has_prefix("CONTRACT_") {
    "contract"
  } else if code.has_prefix("OBJECT_") {
    "object"
  } else if code.has_prefix("FIELD_") || code.has_suffix("_FIELD_ADDED") {
    "field"
  } else if code.has_prefix("ENUM_") {
    "enum"
  } else if code.has_prefix("TYPE_") {
    "type"
  } else if code.has_prefix("MIN_") ||
    code.has_prefix("MAX_") ||
    code == "CONSTRAINTS_CHANGED_COMPATIBLY" {
    "constraint"
  } else {
    "other"
  }
}

///|
fn new_summary_row(label : String) -> SummaryRow {
  { label, breaking: 0, warning: 0, info: 0, total: 0 }
}

///|
priv struct SummaryRow {
  label : String
  breaking : Int
  warning : Int
  info : Int
  total : Int
}

///|
fn SummaryRow::bump(self : SummaryRow, severity : Severity) -> SummaryRow {
  match severity {
    Breaking =>
      {
        label: self.label,
        breaking: self.breaking + 1,
        warning: self.warning,
        info: self.info,
        total: self.total + 1,
      }
    Warning =>
      {
        label: self.label,
        breaking: self.breaking,
        warning: self.warning + 1,
        info: self.info,
        total: self.total + 1,
      }
    Info =>
      {
        label: self.label,
        breaking: self.breaking,
        warning: self.warning,
        info: self.info + 1,
        total: self.total + 1,
      }
  }
}