///|
pub(all) struct FindingChange {
  code : String
  path : String
  previous_severity : Severity
  current_severity : Severity
  previous_message : String
  current_message : String
} derive(Eq, Debug)

///|
pub(all) struct AuditDelta {
  project_name : String
  previous_score : Int
  current_score : Int
  previous_ready : Bool
  current_ready : Bool
  added : Array[Finding]
  resolved : Array[Finding]
  unchanged : Array[Finding]
  changed : Array[FindingChange]
} derive(Eq, Debug)

///|
pub fn AuditDelta::score_change(self : AuditDelta) -> Int {
  self.current_score - self.previous_score
}

///|
pub fn AuditDelta::improved(self : AuditDelta) -> Bool {
  self.score_change() > 0 ||
  (!self.previous_ready && self.current_ready) ||
  self.resolved.length() > self.added.length()
}

///|
pub fn AuditDelta::regressed(self : AuditDelta) -> Bool {
  self.score_change() < 0 ||
  (self.previous_ready && !self.current_ready) ||
  self.added.length() > self.resolved.length()
}

///|
pub fn AuditDelta::has_changes(self : AuditDelta) -> Bool {
  !self.added.is_empty() ||
  !self.resolved.is_empty() ||
  !self.changed.is_empty() ||
  self.previous_score != self.current_score ||
  self.previous_ready != self.current_ready
}

///|
fn delta_same_identity(left : Finding, right : Finding) -> Bool {
  left.code == right.code && left.path == right.path
}

///|
fn delta_same_content(left : Finding, right : Finding) -> Bool {
  delta_same_identity(left, right) &&
  left.severity == right.severity &&
  left.message == right.message
}

///|
fn delta_find_identity(
  findings : Array[Finding],
  expected : Finding,
) -> Finding? {
  for candidate in findings {
    if delta_same_identity(candidate, expected) {
      return Some(candidate)
    }
  }
  None
}

///|
/// Compare two audit reports without depending on timestamps or filesystem
/// state. Findings are matched by stable rule code and path.
pub fn compare_reports(
  previous : AuditReport,
  current : AuditReport,
) -> AuditDelta {
  let added = []
  let resolved = []
  let unchanged = []
  let changed = []
  for finding in current.findings {
    match delta_find_identity(previous.findings, finding) {
      None => added.push(finding)
      Some(old) =>
        if delta_same_content(old, finding) {
          unchanged.push(finding)
        } else {
          changed.push({
            code: finding.code,
            path: finding.path,
            previous_severity: old.severity,
            current_severity: finding.severity,
            previous_message: old.message,
            current_message: finding.message,
          })
        }
    }
  }
  for finding in previous.findings {
    if delta_find_identity(current.findings, finding) is None {
      resolved.push(finding)
    }
  }
  {
    project_name: current.project.name,
    previous_score: previous.score,
    current_score: current.score,
    previous_ready: previous.is_ready(),
    current_ready: current.is_ready(),
    added,
    resolved,
    unchanged,
    changed,
  }
}

///|
fn delta_status(delta : AuditDelta) -> String {
  if delta.improved() && !delta.regressed() {
    "IMPROVED"
  } else if delta.regressed() && !delta.improved() {
    "REGRESSED"
  } else if delta.has_changes() {
    "CHANGED"
  } else {
    "UNCHANGED"
  }
}

///|
fn delta_signed(value : Int) -> String {
  if value > 0 {
    "+\{value}"
  } else {
    value.to_string()
  }
}

///|
pub fn render_delta_text(delta : AuditDelta) -> String {
  let out = StringBuilder::new()
  out.write_string("MoonSeal audit delta: \{delta.project_name}\n")
  out.write_string(
    "Status: \{delta_status(delta)} | Score: \{delta.previous_score} -> \{delta.current_score} (\{delta_signed(delta.score_change())})\n",
  )
  out.write_string(
    "Findings: +\{delta.added.length()} added | -\{delta.resolved.length()} resolved | ~\{delta.changed.length()} changed | =\{delta.unchanged.length()} unchanged\n",
  )
  if delta.previous_ready != delta.current_ready {
    out.write_string(
      "Readiness: \{if delta.previous_ready { "READY" } else { "BLOCKED" }} -> \{if delta.current_ready { "READY" } else { "BLOCKED" }}\n",
    )
  }
  if !delta.added.is_empty() {
    out.write_string("\nAdded findings:\n")
    for finding in delta.added {
      out.write_string(
        "+ \{finding.severity.label()} \{finding.code} [\{finding.path}]: \{finding.message}\n",
      )
    }
  }
  if !delta.resolved.is_empty() {
    out.write_string("\nResolved findings:\n")
    for finding in delta.resolved {
      out.write_string(
        "- \{finding.severity.label()} \{finding.code} [\{finding.path}]: \{finding.message}\n",
      )
    }
  }
  if !delta.changed.is_empty() {
    out.write_string("\nChanged findings:\n")
    for change in delta.changed {
      out.write_string(
        "~ \{change.code} [\{change.path}]: \{change.previous_severity.label()} -> \{change.current_severity.label()}\n",
      )
    }
  }
  out.to_string()
}

///|
fn delta_finding_json(finding : Finding) -> Json {
  {
    "severity": finding.severity.label(),
    "code": finding.code,
    "path": finding.path,
    "message": finding.message,
  }
}

///|
fn delta_change_json(change : FindingChange) -> Json {
  {
    "code": change.code,
    "path": change.path,
    "previous": {
      "severity": change.previous_severity.label(),
      "message": change.previous_message,
    },
    "current": {
      "severity": change.current_severity.label(),
      "message": change.current_message,
    },
  }
}

///|
pub fn delta_json(delta : AuditDelta) -> Json {
  let added : Array[Json] = delta.added.map(delta_finding_json)
  let resolved : Array[Json] = delta.resolved.map(delta_finding_json)
  let unchanged : Array[Json] = delta.unchanged.map(delta_finding_json)
  let changed : Array[Json] = delta.changed.map(delta_change_json)
  {
    "project": delta.project_name,
    "status": delta_status(delta),
    "score": {
      "previous": delta.previous_score,
      "current": delta.current_score,
      "change": delta.score_change(),
    },
    "readiness": {
      "previous": delta.previous_ready,
      "current": delta.current_ready,
    },
    "summary": {
      "added": delta.added.length(),
      "resolved": delta.resolved.length(),
      "changed": delta.changed.length(),
      "unchanged": delta.unchanged.length(),
    },
    "added": added,
    "resolved": resolved,
    "changed": changed,
    "unchanged": unchanged,
  }
}

///|
pub fn render_delta_json(delta : AuditDelta, indent? : Int = 2) -> String {
  delta_json(delta).stringify(indent~)
}

///|
pub fn render_delta_markdown(delta : AuditDelta) -> String {
  let out = StringBuilder::new()
  out.write_string("# MoonSeal audit delta: `\{delta.project_name}`\n\n")
  out.write_string("- Status: **\{delta_status(delta)}**\n")
  out.write_string(
    "- Score: \{delta.previous_score} → \{delta.current_score} (\{delta_signed(delta.score_change())})\n",
  )
  out.write_string(
    "- Findings: **+\{delta.added.length()}** added, **-\{delta.resolved.length()}** resolved, **~\{delta.changed.length()}** changed\n",
  )
  if !delta.added.is_empty() {
    out.write_string("\n## Added\n\n")
    out.write_string("| Severity | Code | Path | Message |\n")
    out.write_string("| --- | --- | --- | --- |\n")
    for finding in delta.added {
      out.write_string(
        "| \{finding.severity.label()} | \{finding.code} | `\{finding.path}` | \{finding.message} |\n",
      )
    }
  }
  if !delta.resolved.is_empty() {
    out.write_string("\n## Resolved\n\n")
    out.write_string("| Severity | Code | Path | Message |\n")
    out.write_string("| --- | --- | --- | --- |\n")
    for finding in delta.resolved {
      out.write_string(
        "| \{finding.severity.label()} | \{finding.code} | `\{finding.path}` | \{finding.message} |\n",
      )
    }
  }
  if !delta.changed.is_empty() {
    out.write_string("\n## Changed\n\n")
    out.write_string("| Code | Path | Previous | Current |\n")
    out.write_string("| --- | --- | --- | --- |\n")
    for change in delta.changed {
      out.write_string(
        "| \{change.code} | `\{change.path}` | \{change.previous_severity.label()} | \{change.current_severity.label()} |\n",
      )
    }
  }
  out.to_string()
}