///|
/// A narrowly scoped exception for one known compatibility break.
pub(all) struct ChangeAllowance {
  code : String
  direction : String
  path_prefix : String
  reason : String
} derive(Eq, Debug)

///|
/// CI policy applied after compatibility analysis.
pub(all) struct AnalysisPolicy {
  name : String
  required_mode : CompatibilityMode?
  max_unallowed_breaking : Int
  max_warnings : Int
  forbidden_codes : Array[String]
  require_witness : Bool
  allowances : Array[ChangeAllowance]
} derive(Eq, Debug)

///|
/// One reason an analysis report does not satisfy policy.
pub(all) struct PolicyViolation {
  code : String
  path : String
  message : String
  related_change : String?
} derive(Eq, Debug)

///|
/// Deterministic outcome of applying an analysis policy.
pub(all) struct PolicyResult {
  policy_name : String
  violations : Array[PolicyViolation]
  allowed_breaking : Array[Change]
  unallowed_breaking : Array[Change]
} derive(Eq, Debug)

///|
/// Strict release policy: full compatibility and no unapproved breaking change.
pub fn AnalysisPolicy::strict() -> AnalysisPolicy {
  {
    name: "strict",
    required_mode: Some(Full),
    max_unallowed_breaking: 0,
    max_warnings: 0,
    forbidden_codes: [],
    require_witness: true,
    allowances: [],
  }
}

///|
/// Backward-compatible rollout policy for consumer-first deployments.
pub fn AnalysisPolicy::consumer_first() -> AnalysisPolicy {
  {
    name: "consumer-first",
    required_mode: Some(Backward),
    max_unallowed_breaking: 0,
    max_warnings: 10,
    forbidden_codes: ["CONTRACT_RENAMED", "TYPE_REMOVED"],
    require_witness: true,
    allowances: [],
  }
}

///|
/// Development policy that records breaks while enforcing witness quality.
pub fn AnalysisPolicy::development(budget? : Int = 20) -> AnalysisPolicy {
  {
    name: "development",
    required_mode: None,
    max_unallowed_breaking: budget,
    max_warnings: 100,
    forbidden_codes: [],
    require_witness: true,
    allowances: [],
  }
}

///|
/// Return a copy of the policy with one documented compatibility exception.
pub fn AnalysisPolicy::allow(
  self : AnalysisPolicy,
  code : String,
  path_prefix? : String = "$",
  direction? : String = "*",
  reason~ : String,
) -> AnalysisPolicy {
  let allowances = self.allowances.copy()
  allowances.push({ code, direction, path_prefix, reason })
  {
    name: self.name,
    required_mode: self.required_mode,
    max_unallowed_breaking: self.max_unallowed_breaking,
    max_warnings: self.max_warnings,
    forbidden_codes: self.forbidden_codes,
    require_witness: self.require_witness,
    allowances,
  }
}

///|
/// Evaluate a report against budgets, forbidden rules and scoped allowances.
pub fn evaluate_policy(
  report : AnalysisReport,
  policy : AnalysisPolicy,
) -> PolicyResult {
  let violations : Array[PolicyViolation] = []
  let allowed_breaking : Array[Change] = []
  let unallowed_breaking : Array[Change] = []
  match policy.required_mode {
    Some(required) =>
      if report.mode != required {
        violations.push({
          code: "POLICY_MODE_MISMATCH",
          path: "$",
          message: "policy requires '" +
          required.render() +
          "' analysis but report uses '" +
          report.mode.render() +
          "'",
          related_change: None,
        })
      }
    None => ()
  }
  for change in report.changes {
    if policy.forbidden_codes.contains(change.code) {
      violations.push({
        code: "POLICY_FORBIDDEN_CHANGE",
        path: change.path,
        message: "change code '" + change.code + "' is forbidden by policy",
        related_change: Some(change.code),
      })
    }
    if change.severity == Breaking {
      if policy.require_witness && change.witness is None {
        violations.push({
          code: "POLICY_WITNESS_REQUIRED",
          path: change.path,
          message: "breaking change has no reproducible witness",
          related_change: Some(change.code),
        })
      }
      if policy.allows(change) {
        allowed_breaking.push(change)
      } else {
        unallowed_breaking.push(change)
      }
    }
  }
  if unallowed_breaking.length() > policy.max_unallowed_breaking {
    violations.push({
      code: "POLICY_BREAKING_BUDGET_EXCEEDED",
      path: "$",
      message: "found " +
      unallowed_breaking.length().to_string() +
      " unallowed breaking changes; budget is " +
      policy.max_unallowed_breaking.to_string(),
      related_change: None,
    })
  }
  if report.warning_count() > policy.max_warnings {
    violations.push({
      code: "POLICY_WARNING_BUDGET_EXCEEDED",
      path: "$",
      message: "found " +
      report.warning_count().to_string() +
      " warnings; budget is " +
      policy.max_warnings.to_string(),
      related_change: None,
    })
  }
  { policy_name: policy.name, violations, allowed_breaking, unallowed_breaking }
}

///|
pub fn PolicyResult::passed(self : PolicyResult) -> Bool {
  self.violations.is_empty()
}

///|
fn AnalysisPolicy::allows(self : AnalysisPolicy, change : Change) -> Bool {
  self.allowances.any(allowance => allowance.matches(change))
}

///|
fn ChangeAllowance::matches(self : ChangeAllowance, change : Change) -> Bool {
  (self.code == "*" || self.code == change.code) &&
  (self.direction == "*" || self.direction == change.direction) &&
  change.path.has_prefix(self.path_prefix)
}

///|
/// Render a compact CI-friendly policy result.
pub fn PolicyResult::to_markdown(self : PolicyResult) -> String {
  let out = StringBuilder()
  out.write_string("# EvoWitness policy: " + self.policy_name + "\n\n")
  out.write_string(
    "Result: **" + (if self.passed() { "PASS" } else { "FAIL" }) + "**\n\n",
  )
  out.write_string(
    "- Allowed breaking changes: " +
    self.allowed_breaking.length().to_string() +
    "\n",
  )
  out.write_string(
    "- Unallowed breaking changes: " +
    self.unallowed_breaking.length().to_string() +
    "\n",
  )
  out.write_string(
    "- Policy violations: " + self.violations.length().to_string() + "\n",
  )
  if !self.violations.is_empty() {
    out.write_string("\n| Code | Path | Message |\n|---|---|---|\n")
    for violation in self.violations {
      out.write_string(
        "| `" + violation.code + "` | `" + violation.path + "` | ",
      )
      out.write_string(markdown_escape(violation.message) + " |\n")
    }
  }
  out.to_string()
}