///|
/// Aggregated, deterministic policy audit suitable for CI output.
pub struct PolicyAudit {
  policy : Policy
  diagnostics : Array[Diagnostic]
  matrix : DecisionMatrix
  risk_score : Int
  grade : String
  explicit_decisions : Int
  default_decisions : Int
} derive(Eq, Debug)

///|
pub fn PolicyAudit::policy(self : PolicyAudit) -> Policy {
  self.policy
}

///|
pub fn PolicyAudit::diagnostics(self : PolicyAudit) -> Array[Diagnostic] {
  self.diagnostics
}

///|
pub fn PolicyAudit::matrix(self : PolicyAudit) -> DecisionMatrix {
  self.matrix
}

///|
pub fn PolicyAudit::risk_score(self : PolicyAudit) -> Int {
  self.risk_score
}

///|
pub fn PolicyAudit::grade(self : PolicyAudit) -> String {
  self.grade
}

///|
pub fn PolicyAudit::explicit_decisions(self : PolicyAudit) -> Int {
  self.explicit_decisions
}

///|
pub fn PolicyAudit::default_decisions(self : PolicyAudit) -> Int {
  self.default_decisions
}

///|
pub fn PolicyAudit::passed(self : PolicyAudit) -> Bool {
  self.policy.error_count() == 0 && self.risk_score < 60
}

///|
fn risk_for_diagnostic(diagnostic : Diagnostic) -> Int {
  match diagnostic.severity {
    Error => 20
    Warning => 7
    Info => 1
  }
}

///|
fn clamp_score(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 100 {
    100
  } else {
    value
  }
}

///|
fn grade_for_score(score : Int) -> String {
  if score < 10 {
    "A"
  } else if score < 25 {
    "B"
  } else if score < 45 {
    "C"
  } else if score < 70 {
    "D"
  } else {
    "F"
  }
}

///|
fn calculate_risk(
  policy : Policy,
  diagnostics : Array[Diagnostic],
  matrix : DecisionMatrix,
) -> Int {
  let mut score = 0
  for diagnostic in diagnostics {
    score = score + risk_for_diagnostic(diagnostic)
  }
  if policy.truncated {
    score = score + 30
  }
  if policy.groups.length() == 0 {
    score = score + 10
  }
  if matrix.total() > 0 && matrix.default_count == matrix.total() {
    score = score + 8
  }
  if matrix.denied_count == matrix.total() && matrix.total() > 0 {
    score = score + 12
  }
  clamp_score(score)
}

///|
/// Parses, lints, and probes a policy in one deterministic operation.
pub fn audit(
  input : String,
  agents : Array[String],
  paths : Array[String],
) -> PolicyAudit {
  let policy = parse(input)
  let diagnostics = all_diagnostics(policy)
  let matrix = run_matrix(policy, build_probe_grid(agents, paths))
  let risk_score = calculate_risk(policy, diagnostics, matrix)
  {
    policy,
    diagnostics,
    matrix,
    risk_score,
    grade: grade_for_score(risk_score),
    explicit_decisions: matrix.total() - matrix.default_count,
    default_decisions: matrix.default_count,
  }
}

///|
pub fn audit_default(input : String) -> PolicyAudit {
  audit(input, ["*", "Googlebot", "Bingbot", "GPTBot"], [
    "/", "/robots.txt", "/admin/", "/api/", "/private/", "/public/",
  ])
}

///|
fn count_severity(diagnostics : Array[Diagnostic], severity : Severity) -> Int {
  let mut count = 0
  for diagnostic in diagnostics {
    if diagnostic.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
pub fn render_audit_markdown(audit : PolicyAudit) -> String {
  let stats = audit.policy.stats()
  let output = StringBuilder::new()
  output.write_string("# MoonRobots Gate audit\n\n")
  output.write_string("Grade: **\{audit.grade}**\n\n")
  output.write_string("Risk score: **\{audit.risk_score}/100**\n\n")
  output.write_string("## Policy\n\n")
  output.write_string("- Groups: \{stats.groups}\n")
  output.write_string("- Agents: \{stats.agents}\n")
  output.write_string("- Rules: \{stats.rules}\n")
  output.write_string("- Sitemaps: \{stats.sitemaps}\n")
  output.write_string("- Truncated: \{audit.policy.truncated}\n\n")
  output.write_string("## Diagnostics\n\n")
  output.write_string("- Errors: \{count_severity(audit.diagnostics, Error)}\n")
  output.write_string(
    "- Warnings: \{count_severity(audit.diagnostics, Warning)}\n",
  )
  output.write_string(
    "- Information: \{count_severity(audit.diagnostics, Info)}\n\n",
  )
  for diagnostic in audit.diagnostics {
    output.write_string("- ")
    output.write_string(diagnostic.summary())
    output.write_char('\n')
  }
  output.write_string("\n## Access matrix\n\n")
  output.write_string(render_matrix_markdown(audit.matrix))
  output.to_string()
}

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

///|
pub fn render_audit_json(audit : PolicyAudit) -> String {
  let stats = audit.policy.stats()
  let output = StringBuilder::new()
  output.write_string("{\n")
  output.write_string("  \"grade\": \"\{json_escape(audit.grade)}\",\n")
  output.write_string("  \"risk_score\": \{audit.risk_score},\n")
  output.write_string("  \"passed\": \{audit.passed()},\n")
  output.write_string("  \"groups\": \{stats.groups},\n")
  output.write_string("  \"rules\": \{stats.rules},\n")
  output.write_string("  \"diagnostics\": [\n")
  for index, diagnostic in audit.diagnostics {
    output.write_string("    {")
    output.write_string("\"severity\":\"\{diagnostic.severity.label()}\",")
    output.write_string("\"code\":\"\{json_escape(diagnostic.code)}\",")
    output.write_string("\"line\":\{diagnostic.line()},")
    output.write_string("\"message\":\"\{json_escape(diagnostic.message)}\"}")
    if index + 1 < audit.diagnostics.length() {
      output.write_char(',')
    }
    output.write_char('\n')
  }
  output.write_string("  ],\n")
  output.write_string("  \"probes\": [\n")
  for index, outcome in audit.matrix.outcomes {
    output.write_string("    {")
    output.write_string("\"name\":\"\{json_escape(outcome.probe.name)}\",")
    output.write_string("\"agent\":\"\{json_escape(outcome.probe.agent)}\",")
    output.write_string("\"path\":\"\{json_escape(outcome.probe.path)}\",")
    output.write_string("\"allowed\":\{outcome.decision.allowed},")
    output.write_string("\"line\":\{outcome.decision.line}}")
    if index + 1 < audit.matrix.outcomes.length() {
      output.write_char(',')
    }
    output.write_char('\n')
  }
  output.write_string("  ]\n")
  output.write_string("}\n")
  output.to_string()
}

///|
pub fn audit_exit_code(audit : PolicyAudit) -> Int {
  if audit.policy.error_count() > 0 {
    2
  } else if audit.risk_score >= 60 {
    1
  } else {
    0
  }
}