///|
/// A finding identity that is stable across line moves and message wording.
fn baseline_finding_key(finding : Finding) -> String {
  finding.code +
  "|" +
  finding.kind.name() +
  "|" +
  finding.primary_id +
  "|" +
  finding.secondary_id +
  "|" +
  finding.shortcut +
  "|" +
  finding.context
}

///|
fn baseline_contains(findings : Array[Finding], target : Finding) -> Bool {
  let key = baseline_finding_key(target)
  for finding in findings {
    if baseline_finding_key(finding) == key {
      return true
    }
  }
  false
}

///|
fn baseline_count(findings : Array[Finding], severity : Severity) -> Int {
  findings.filter(item => item.severity == severity).length()
}

///|
/// A differential report for CI baseline checks.
///
/// Existing findings are retained as debt, while only newly introduced
/// findings affect the default gate. Finding identities deliberately omit line
/// and source so that moving a declaration does not look like a new defect.
pub(all) struct BaselineReport {
  baseline_fingerprint : String
  current_fingerprint : String
  baseline_count : Int
  current_count : Int
  new_findings : Array[Finding]
  resolved_findings : Array[Finding]
  retained_count : Int
  new_error_count : Int
  new_warning_count : Int
  new_info_count : Int
  fail_on_warning : Bool
  passed : Bool
} derive(Eq, @debug.Debug)

///|
/// Compare a current analysis with an accepted baseline.
///
/// By default this is a ratchet: old findings remain visible but do not block
/// a release, while any new error blocks it. Set `fail_on_warning` when a team
/// wants warnings to be part of its release policy as well.
pub fn compare_baseline(
  baseline : Analysis,
  current : Analysis,
  fail_on_warning? : Bool = false,
) -> BaselineReport {
  let new_findings : Array[Finding] = []
  let resolved_findings : Array[Finding] = []
  let mut retained = 0
  for finding in current.findings {
    if baseline_contains(baseline.findings, finding) {
      retained += 1
    } else {
      new_findings.push(finding)
    }
  }
  for finding in baseline.findings {
    if !baseline_contains(current.findings, finding) {
      resolved_findings.push(finding)
    }
  }
  let new_errors = baseline_count(new_findings, Error)
  let new_warnings = baseline_count(new_findings, Warning)
  let new_infos = baseline_count(new_findings, Info)
  {
    baseline_fingerprint: baseline.fingerprint,
    current_fingerprint: current.fingerprint,
    baseline_count: baseline.findings.length(),
    current_count: current.findings.length(),
    new_findings,
    resolved_findings,
    retained_count: retained,
    new_error_count: new_errors,
    new_warning_count: new_warnings,
    new_info_count: new_infos,
    fail_on_warning,
    passed: new_errors == 0 && (!fail_on_warning || new_warnings == 0),
  }
}

///|
/// Return a concise baseline summary for CI logs.
pub fn BaselineReport::summary(self : BaselineReport) -> String {
  "baseline=" +
  self.baseline_count.to_string() +
  " current=" +
  self.current_count.to_string() +
  " new=" +
  self.new_findings.length().to_string() +
  " resolved=" +
  self.resolved_findings.length().to_string() +
  " retained=" +
  self.retained_count.to_string() +
  " result=" +
  (if self.passed { "pass" } else { "fail" })
}

///|
/// Render a baseline report as Markdown suitable for a pull request comment.
pub fn baseline_report_to_markdown(report : BaselineReport) -> String {
  let lines : Array[String] = [
    "## Baseline gate",
    "",
    "Result: **" + (if report.passed { "PASS" } else { "FAIL" }) + "**",
    "",
    "| Metric | Value |",
    "| --- | ---: |",
    "| Baseline findings | " + report.baseline_count.to_string() + " |",
    "| Current findings | " + report.current_count.to_string() + " |",
    "| New findings | " + report.new_findings.length().to_string() + " |",
    "| Resolved findings | " +
    report.resolved_findings.length().to_string() +
    " |",
    "| Retained findings | " + report.retained_count.to_string() + " |",
    "| New errors | " + report.new_error_count.to_string() + " |",
    "| New warnings | " + report.new_warning_count.to_string() + " |",
  ]
  if report.new_findings.length() > 0 {
    lines.push("")
    lines.push("### New findings")
    for finding in report.new_findings {
      lines.push("- `" + finding.code + "` " + finding.to_line())
    }
  }
  lines.join("\n")
}

///|
/// Serialize a baseline result for CI artifacts.
pub fn baseline_report_to_json(report : BaselineReport) -> String {
  let new_rows : Array[String] = []
  for finding in report.new_findings {
    new_rows.push(
      "{\"code\":" +
      json_string(finding.code) +
      ",\"kind\":" +
      json_string(finding.kind.name()) +
      ",\"severity\":" +
      json_string(finding.severity.name()) +
      ",\"id\":" +
      json_string(finding.primary_id) +
      "}",
    )
  }
  "{\"baseline_fingerprint\":" +
  json_string(report.baseline_fingerprint) +
  ",\"current_fingerprint\":" +
  json_string(report.current_fingerprint) +
  ",\"baseline_count\":" +
  report.baseline_count.to_string() +
  ",\"current_count\":" +
  report.current_count.to_string() +
  ",\"new_count\":" +
  report.new_findings.length().to_string() +
  ",\"resolved_count\":" +
  report.resolved_findings.length().to_string() +
  ",\"retained_count\":" +
  report.retained_count.to_string() +
  ",\"new_errors\":" +
  report.new_error_count.to_string() +
  ",\"new_warnings\":" +
  report.new_warning_count.to_string() +
  ",\"new_infos\":" +
  report.new_info_count.to_string() +
  ",\"fail_on_warning\":" +
  (if report.fail_on_warning { "true" } else { "false" }) +
  ",\"passed\":" +
  (if report.passed { "true" } else { "false" }) +
  ",\"new_findings\":[" +
  new_rows.join(",") +
  "]}"
}