///|
pub(all) enum RiskLevel {
  Critical
  High
  Medium
  Low
  Info
} derive(Eq)

///|
pub impl Show for RiskLevel with fn output(self : RiskLevel, logger : &Logger) -> Unit {
  match self {
    Critical => logger.write_string("Critical")
    High => logger.write_string("High")
    Medium => logger.write_string("Medium")
    Low => logger.write_string("Low")
    Info => logger.write_string("Info")
  }
}

///|
pub(all) enum ReportStatus {
  Pass
  Warning
  Fail
} derive(Eq, Debug)

///|
pub impl Show for ReportStatus with fn output(
  self : ReportStatus,
  logger : &Logger,
) -> Unit {
  match self {
    Pass => logger.write_string("Pass")
    Warning => logger.write_string("Warning")
    Fail => logger.write_string("Fail")
  }
}

///|
pub(all) struct Finding {
  title : String
  description : String
  risk_level : RiskLevel
  remediation : String
}

///|
pub(all) struct TyposquatSummaryEntry {
  suspect : String
  hit_count : Int
  first_similar_to : String
  first_attack : String
}

///|
pub(all) struct SecurityReport {
  package_name : String
  package_version : String
  mut overall_status : ReportStatus
  mut findings : Array[Finding]
  mut signature_verified : Bool
  mut typosquat_suspects : Array[String]
  mut file_count : Int
  mut risk_score : Double
  mut audit_timestamp : String
}

///|
pub fn SecurityReport::new(
  package_name : String,
  package_version : String,
) -> SecurityReport {
  SecurityReport::{
    package_name,
    package_version,
    overall_status: Pass,
    findings: [],
    signature_verified: false,
    typosquat_suspects: [],
    file_count: 0,
    risk_score: 0.0,
    audit_timestamp: "2026-01-01T00:00:00Z",
  }
}

///|
pub fn SecurityReport::add_finding(self : SecurityReport, f : Finding) -> Unit {
  self.findings.push(f)
  match f.risk_level {
    Critical | High => self.overall_status = Fail
    Medium => if self.overall_status == Pass { self.overall_status = Warning }
    _ => ()
  }
  self.risk_score = self.risk_score + risk_level_weight(f.risk_level)
}

///|
/// Recompute `risk_score` and clamp to `[0, 10]`. Call after batch-adding
/// findings so callers don't have to track the running total.
pub fn SecurityReport::recompute_score(self : SecurityReport) -> Unit {
  let mut sum = 0.0
  for i = 0; i < self.findings.length(); i = i + 1 {
    sum = sum + risk_level_weight(self.findings[i].risk_level)
  }
  if sum > 10.0 {
    self.risk_score = 10.0
  } else {
    self.risk_score = sum
  }
}

///|
pub fn SecurityReport::summary(self : SecurityReport) -> String {
  let status_str = match self.overall_status {
    Pass => "PASS"
    Warning => "WARNING"
    Fail => "FAIL"
  }
  let mut critical = 0
  let mut high = 0
  let mut medium = 0
  let mut low = 0
  for i = 0; i < self.findings.length(); i = i + 1 {
    match self.findings[i].risk_level {
      Critical => critical = critical + 1
      High => high = high + 1
      Medium => medium = medium + 1
      Low => low = low + 1
      Info => ()
    }
  }
  let mut s = "MoonGuard Security Report\n"
  s = s + "========================\n"
  s = s + "Package: " + self.package_name + " v" + self.package_version + "\n"
  s = s + "Status: " + status_str + "\n"
  s = s + "Risk score: " + format_double(self.risk_score) + " / 10.0\n"
  s = s + "Signature: "
  s = s + (if self.signature_verified { "verified" } else { "unverified" })
  s = s + "\n"
  s = s + "Files: " + self.file_count.to_string() + "\n"
  s = s + "Findings: " + self.findings.length().to_string() + " total"
  if critical > 0 {
    s = s + " (" + critical.to_string() + " critical)"
  }
  if high > 0 {
    s = s + " (" + high.to_string() + " high)"
  }
  if medium > 0 {
    s = s + " (" + medium.to_string() + " medium)"
  }
  if low > 0 {
    s = s + " (" + low.to_string() + " low)"
  }
  s = s + "\n"
  if self.typosquat_suspects.length() > 0 {
    s = s + "Typosquat alerts: "
    s = s + self.typosquat_suspects.length().to_string() + "\n"
  }
  s
}

///|
pub fn report_to_json(report : SecurityReport) -> String {
  let status_str = match report.overall_status {
    Pass => "pass"
    Warning => "warning"
    Fail => "fail"
  }
  let mut s = "{\"package\":\"" +
    escape_json_string(report.package_name) +
    "\","
  s = s + "\"version\":\"" + escape_json_string(report.package_version) + "\","
  s = s + "\"status\":\"" + status_str + "\","
  s = s + "\"risk_score\":" + format_double(report.risk_score) + ","
  s = s +
    "\"audit_timestamp\":\"" +
    escape_json_string(report.audit_timestamp) +
    "\","
  s = s +
    "\"signature_verified\":" +
    report.signature_verified.to_string() +
    ","
  s = s + "\"file_count\":" + report.file_count.to_string() + ","
  s = s + "\"findings\":["
  for i = 0; i < report.findings.length(); i = i + 1 {
    if i > 0 {
      s = s + ","
    }
    let f = report.findings[i]
    s = s + "{\"title\":\"" + escape_json_string(f.title) + "\","
    s = s + "\"risk\":\"" + risk_level_str(f.risk_level) + "\","
    s = s + "\"description\":\"" + escape_json_string(f.description) + "\","
    s = s + "\"remediation\":\"" + escape_json_string(f.remediation) + "\"}"
  }
  s = s + "],"
  s = s + "\"typosquat_suspects\":["
  for i = 0; i < report.typosquat_suspects.length(); i = i + 1 {
    if i > 0 {
      s = s + ","
    }
    s = s + "\"" + escape_json_string(report.typosquat_suspects[i]) + "\""
  }
  s = s + "]}"
  s
}

///|
/// Escape `"`, `\` and control characters so any user-influenced string
/// can't smuggle stray JSON into a report document. This keeps the wire
/// format well-formed even for hostile package/file names.
fn escape_json_string(input : String) -> String {
  let buf = StringBuilder::new(size_hint=input.length() + 8)
  for i = 0; i < input.length(); i = i + 1 {
    let c = input[i]
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      _ => buf.write_char(c.unsafe_to_char())
    }
  }
  buf.to_string()
}

///|
fn risk_level_str(level : RiskLevel) -> String {
  match level {
    Critical => "critical"
    High => "high"
    Medium => "medium"
    Low => "low"
    Info => "info"
  }
}

///|
/// CVSS-style rough weights for each risk level. Critical findings
/// contribute the most to the cumulative risk score; the report
/// clamps the total at 10 so a single Critical + many Lows can't run
/// away.
fn risk_level_weight(level : RiskLevel) -> Double {
  match level {
    Critical => 10.0
    High => 7.0
    Medium => 4.0
    Low => 1.0
    Info => 0.0
  }
}

///|
fn format_double(d : Double) -> String {
  // Render up to 2 decimal places without dragging in locale-sensitive
  // float formatting. Small enough for our `0..10` risk-score range.
  let int_part = d.to_int()
  let frac_part = ((d - int_part.to_double()) * 100.0).to_int()
  if frac_part < 0 {
    return int_part.to_string() + ".00"
  }
  let frac_str = if frac_part < 10 {
    "0" + frac_part.to_string()
  } else {
    frac_part.to_string()
  }
  int_part.to_string() + "." + frac_str
}

///|
/// Convenience constructor that wires a Finding together from common
/// fields. Cuts down on the boilerplate at audit call sites.
pub fn make_finding(
  title : String,
  description : String,
  risk : RiskLevel,
  remediation : String,
) -> Finding {
  { title, description, risk_level: risk, remediation }
}