///|
/// Human-facing grade for the policy score.
pub(all) enum PolicyGrade {
  GradeExcellent
  GradeGood
  GradeFair
  GradeWeak
  GradeDangerous
} derive(Eq, Debug)

///|
/// Priority for a hardening recommendation.
pub(all) enum RecommendationPriority {
  PriorityInfo
  PriorityLow
  PriorityMedium
  PriorityHigh
  PriorityCritical
} derive(Eq, Debug)

///|
/// Aggregated counts by finding severity.
pub(all) struct SeverityCounts {
  info : Int
  warning : Int
  high : Int
  total : Int
} derive(Eq, Debug)

///|
/// Coverage for baseline and optional hardening directives.
pub(all) struct DirectiveCoverage {
  required : Int
  present : Int
  missing : Array[String]
  optional_present : Int
  unknown : Int
  deprecated : Int
  experimental : Int
} derive(Eq, Debug)

///|
/// Aggregated counts for classified CSP source expressions.
pub(all) struct SourceCounts {
  total : Int
  dangerous : Int
  network : Int
  cryptographic : Int
  plain_http : Int
  any_origin : Int
  data_like : Int
  wildcard_host : Int
} derive(Eq, Debug)

///|
/// Full policy score model.
pub(all) struct PolicyScore {
  raw_score : Int
  final_score : Int
  penalty : Int
  bonus : Int
  grade : PolicyGrade
  findings : SeverityCounts
  directives : DirectiveCoverage
  sources : SourceCounts
} derive(Eq, Debug)

///|
/// Machine-readable hardening suggestion.
pub(all) struct HardeningRecommendation {
  code : String
  priority : RecommendationPriority
  directive : String?
  summary : String
  detail : String
} derive(Eq, Debug)

///|
pub fn PolicyGrade::name(self : PolicyGrade) -> String {
  match self {
    GradeExcellent => "excellent"
    GradeGood => "good"
    GradeFair => "fair"
    GradeWeak => "weak"
    GradeDangerous => "dangerous"
  }
}

///|
pub fn PolicyGrade::label(self : PolicyGrade) -> String {
  match self {
    GradeExcellent => "A"
    GradeGood => "B"
    GradeFair => "C"
    GradeWeak => "D"
    GradeDangerous => "E"
  }
}

///|
pub fn RecommendationPriority::name(self : RecommendationPriority) -> String {
  match self {
    PriorityInfo => "info"
    PriorityLow => "low"
    PriorityMedium => "medium"
    PriorityHigh => "high"
    PriorityCritical => "critical"
  }
}

///|
pub fn RecommendationPriority::weight(self : RecommendationPriority) -> Int {
  match self {
    PriorityInfo => 1
    PriorityLow => 2
    PriorityMedium => 4
    PriorityHigh => 8
    PriorityCritical => 13
  }
}

///|
fn grade_from_score(score : Int) -> PolicyGrade {
  if score >= 90 {
    GradeExcellent
  } else if score >= 75 {
    GradeGood
  } else if score >= 60 {
    GradeFair
  } else if score >= 40 {
    GradeWeak
  } else {
    GradeDangerous
  }
}

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

///|
fn count_findings(findings : Array[Finding]) -> SeverityCounts {
  let mut info = 0
  let mut warning = 0
  let mut high = 0
  for finding in findings {
    if finding.severity == Info {
      info = info + 1
    } else if finding.severity == Warning {
      warning = warning + 1
    } else if finding.severity == High {
      high = high + 1
    }
  }
  { info, warning, high, total: findings.length() }
}

///|
pub fn SeverityCounts::blocking(self : SeverityCounts) -> Int {
  self.high
}

///|
pub fn SeverityCounts::nonblocking(self : SeverityCounts) -> Int {
  self.info + self.warning
}

///|
pub fn SeverityCounts::summary(self : SeverityCounts) -> String {
  "total=\{self.total} high=\{self.high} warning=\{self.warning} info=\{self.info}"
}

///|
pub fn Policy::severity_counts(self : Policy) -> SeverityCounts {
  count_findings(self.audit())
}

///|
fn required_hardening_names() -> Array[String] {
  let names : Array[String] = []
  names.push("default-src")
  names.push("object-src")
  names.push("base-uri")
  names.push("frame-ancestors")
  names
}

///|
fn optional_hardening_names() -> Array[String] {
  let names : Array[String] = []
  names.push("form-action")
  names.push("upgrade-insecure-requests")
  names.push("trusted-types")
  names.push("require-trusted-types-for")
  names.push("report-to")
  names.push("report-uri")
  names
}

///|
pub fn Policy::directive_coverage(self : Policy) -> DirectiveCoverage {
  let missing : Array[String] = []
  let required = required_hardening_names()
  let optional = optional_hardening_names()
  let mut present = 0
  let mut optional_present = 0
  for name in required {
    if self.contains(name[:]) {
      present = present + 1
    } else {
      missing.push(name)
    }
  }
  for name in optional {
    if self.contains(name[:]) {
      optional_present = optional_present + 1
    }
  }
  {
    required: required.length(),
    present,
    missing,
    optional_present,
    unknown: self.unknown_directives().length(),
    deprecated: self.deprecated_directives().length(),
    experimental: self.experimental_directives().length(),
  }
}

///|
pub fn DirectiveCoverage::is_complete(self : DirectiveCoverage) -> Bool {
  self.missing.length() == 0
}

///|
pub fn DirectiveCoverage::summary(self : DirectiveCoverage) -> String {
  "required=\{self.present}/\{self.required} missing=\{self.missing.length()} optional=\{self.optional_present} unknown=\{self.unknown} deprecated=\{self.deprecated}"
}

///|
pub fn Policy::source_counts(self : Policy) -> SourceCounts {
  let sources = self.all_sources()
  let mut dangerous = 0
  let mut network = 0
  let mut cryptographic = 0
  let mut plain_http = 0
  let mut any_origin = 0
  let mut data_like = 0
  let mut wildcard_host = 0
  for source in sources {
    if source.is_dangerous() {
      dangerous = dangerous + 1
    }
    if source.is_network_source() {
      network = network + 1
    }
    if source.is_cryptographic() {
      cryptographic = cryptographic + 1
    }
    if source.uses_plain_http() {
      plain_http = plain_http + 1
    }
    if source.allows_any_origin() {
      any_origin = any_origin + 1
    }
    if source.allows_data_like_url() {
      data_like = data_like + 1
    }
    if source.is_wildcard_host() {
      wildcard_host = wildcard_host + 1
    }
  }
  {
    total: sources.length(),
    dangerous,
    network,
    cryptographic,
    plain_http,
    any_origin,
    data_like,
    wildcard_host,
  }
}

///|
pub fn SourceCounts::summary(self : SourceCounts) -> String {
  "total=\{self.total} dangerous=\{self.dangerous} network=\{self.network} crypto=\{self.cryptographic} plain_http=\{self.plain_http} any_origin=\{self.any_origin}"
}

///|
pub fn SourceCounts::has_severe_source_risk(self : SourceCounts) -> Bool {
  self.plain_http > 0 || self.any_origin > 0 || self.data_like > 0
}

///|
fn score_penalty(
  findings : SeverityCounts,
  coverage : DirectiveCoverage,
  sources : SourceCounts,
) -> Int {
  let mut penalty = 0
  penalty = penalty + findings.high * 18
  penalty = penalty + findings.warning * 7
  penalty = penalty + coverage.missing.length() * 9
  penalty = penalty + coverage.unknown * 4
  penalty = penalty + coverage.deprecated * 6
  penalty = penalty + sources.plain_http * 8
  penalty = penalty + sources.any_origin * 8
  penalty = penalty + sources.data_like * 5
  penalty = penalty + sources.wildcard_host * 4
  penalty
}

///|
fn score_bonus(
  policy : Policy,
  coverage : DirectiveCoverage,
  sources : SourceCounts,
) -> Int {
  let mut bonus = 0
  if coverage.is_complete() {
    bonus = bonus + 8
  }
  if policy.contains("upgrade-insecure-requests") {
    bonus = bonus + 4
  }
  if policy.contains("form-action") {
    bonus = bonus + 3
  }
  if policy.contains("report-to") || policy.contains("report-uri") {
    bonus = bonus + 2
  }
  if sources.cryptographic > 0 {
    bonus = bonus + 5
  }
  if policy.contains("trusted-types") {
    bonus = bonus + 3
  }
  if policy.contains("require-trusted-types-for") {
    bonus = bonus + 3
  }
  bonus
}

///|
pub fn Policy::score(self : Policy) -> PolicyScore {
  let findings = self.severity_counts()
  let coverage = self.directive_coverage()
  let sources = self.source_counts()
  let penalty = score_penalty(findings, coverage, sources)
  let bonus = score_bonus(self, coverage, sources)
  let raw_score = 100 - penalty + bonus
  let final_score = clamp_score(raw_score)
  {
    raw_score,
    final_score,
    penalty,
    bonus,
    grade: grade_from_score(final_score),
    findings,
    directives: coverage,
    sources,
  }
}

///|
pub fn PolicyScore::summary(self : PolicyScore) -> String {
  "score=\{self.final_score} grade=\{self.grade.label()} penalty=\{self.penalty} bonus=\{self.bonus}"
}

///|
pub fn PolicyScore::is_passing(self : PolicyScore) -> Bool {
  self.final_score >= 75 && self.findings.high == 0
}

///|
pub fn PolicyScore::is_blocked(self : PolicyScore) -> Bool {
  self.findings.high > 0 || self.final_score < 40
}

///|
fn make_recommendation(
  code : String,
  priority : RecommendationPriority,
  directive : String?,
  summary : String,
  detail : String,
) -> HardeningRecommendation {
  { code, priority, directive, summary, detail }
}

///|
fn push_missing_directive_recommendations(
  out : Array[HardeningRecommendation],
  policy : Policy,
) -> Unit {
  if !policy.contains("default-src") {
    out.push(
      make_recommendation(
        "add-default-src",
        PriorityCritical,
        Some("default-src"),
        "Add a default-src baseline.",
        "default-src is the fallback for most fetch directives and should be explicit.",
      ),
    )
  }
  if !policy.contains("object-src") {
    out.push(
      make_recommendation(
        "add-object-src",
        PriorityHigh,
        Some("object-src"),
        "Disable plugin-like object loading.",
        "Use object-src 'none' unless the application has a legacy plugin requirement.",
      ),
    )
  }
  if !policy.contains("base-uri") {
    out.push(
      make_recommendation(
        "add-base-uri",
        PriorityHigh,
        Some("base-uri"),
        "Restrict base URL injection.",
        "Use base-uri 'self' or base-uri 'none' to limit attacker-controlled base elements.",
      ),
    )
  }
  if !policy.contains("frame-ancestors") {
    out.push(
      make_recommendation(
        "add-frame-ancestors",
        PriorityHigh,
        Some("frame-ancestors"),
        "Control who can embed the page.",
        "Use frame-ancestors 'none' or an explicit allowlist to reduce clickjacking risk.",
      ),
    )
  }
  if !policy.contains("form-action") {
    out.push(
      make_recommendation(
        "add-form-action",
        PriorityMedium,
        Some("form-action"),
        "Limit form submission targets.",
        "Use form-action 'self' for apps that do not submit forms to third-party origins.",
      ),
    )
  }
}

///|
fn push_source_recommendations(
  out : Array[HardeningRecommendation],
  policy : Policy,
) -> Unit {
  if policy.has_source_kind(SourceWildcard) {
    out.push(
      make_recommendation(
        "replace-wildcard",
        PriorityCritical,
        None,
        "Replace wildcard sources.",
        "Wildcard sources allow broad loading. Prefer explicit hosts, 'self', or nonce/hash sources.",
      ),
    )
  }
  if policy.has_source_kind(SourceUnsafeInline) {
    out.push(
      make_recommendation(
        "remove-unsafe-inline",
        PriorityCritical,
        Some("script-src"),
        "Remove unsafe inline script allowance.",
        "Use nonces, hashes, or strict-dynamic instead of unsafe-inline where possible.",
      ),
    )
  }
  if policy.has_source_kind(SourceUnsafeEval) ||
    policy.has_source_kind(SourceWasmUnsafeEval) {
    out.push(
      make_recommendation(
        "remove-unsafe-eval",
        PriorityCritical,
        Some("script-src"),
        "Remove dynamic code evaluation.",
        "unsafe-eval and wasm-unsafe-eval widen injection impact and should require a documented exception.",
      ),
    )
  }
  if policy.uses_plain_http() {
    out.push(
      make_recommendation(
        "replace-http",
        PriorityHigh,
        None,
        "Replace plain HTTP sources.",
        "Use HTTPS endpoints or upgrade-insecure-requests for transport integrity.",
      ),
    )
  }
  if policy.has_source_kind(SourceData) {
    out.push(
      make_recommendation(
        "limit-data-url",
        PriorityHigh,
        None,
        "Avoid data: in sensitive directives.",
        "data: should be limited to low-risk image or media use cases and avoided for scripts.",
      ),
    )
  }
  if !policy.uses_nonce_or_hash() && policy.contains("script-src") {
    out.push(
      make_recommendation(
        "add-script-nonce-or-hash",
        PriorityMedium,
        Some("script-src"),
        "Consider nonce or hash based script trust.",
        "Nonce or hash sources improve script allowlist precision for modern applications.",
      ),
    )
  }
}

///|
fn push_catalog_recommendations(
  out : Array[HardeningRecommendation],
  policy : Policy,
) -> Unit {
  for directive in policy.unknown_directives() {
    out.push(
      make_recommendation(
        "review-unknown-directive",
        PriorityMedium,
        Some(directive.name),
        "Review unknown directive.",
        "Unknown directives may be misspelled or unsupported by this catalog.",
      ),
    )
  }
  for directive in policy.deprecated_directives() {
    out.push(
      make_recommendation(
        "replace-deprecated-directive",
        PriorityMedium,
        Some(directive.name),
        "Replace deprecated directive.",
        "Deprecated CSP directives usually have modern alternatives or are ignored by browsers.",
      ),
    )
  }
  if !policy.contains("upgrade-insecure-requests") && policy.uses_plain_http() {
    out.push(
      make_recommendation(
        "add-upgrade-insecure-requests",
        PriorityMedium,
        Some("upgrade-insecure-requests"),
        "Add upgrade-insecure-requests.",
        "This helps migrate old HTTP subresource URLs to HTTPS where supported.",
      ),
    )
  }
}

///|
pub fn Policy::hardening_recommendations(
  self : Policy,
) -> Array[HardeningRecommendation] {
  let out : Array[HardeningRecommendation] = []
  push_missing_directive_recommendations(out, self)
  push_source_recommendations(out, self)
  push_catalog_recommendations(out, self)
  out
}

///|
pub fn HardeningRecommendation::summary_line(
  self : HardeningRecommendation,
) -> String {
  let directive = match self.directive {
    Some(name) => name
    None => "-"
  }
  self.priority.name() + " " + self.code + " " + directive + " " + self.summary
}

///|
pub fn HardeningRecommendation::detail_line(
  self : HardeningRecommendation,
) -> String {
  self.summary_line() + " " + self.detail
}

///|
pub fn recommendations_by_priority(
  items : Array[HardeningRecommendation],
  priority : RecommendationPriority,
) -> Array[HardeningRecommendation] {
  items.filter(item => item.priority == priority)
}

///|
pub fn recommendation_weight_total(
  items : Array[HardeningRecommendation],
) -> Int {
  let mut total = 0
  for item in items {
    total = total + item.priority.weight()
  }
  total
}

///|
pub fn Policy::recommendation_summary(self : Policy) -> String {
  let items = self.hardening_recommendations()
  let critical = recommendations_by_priority(items, PriorityCritical)
  let high = recommendations_by_priority(items, PriorityHigh)
  let medium = recommendations_by_priority(items, PriorityMedium)
  let low = recommendations_by_priority(items, PriorityLow)
  let info = recommendations_by_priority(items, PriorityInfo)
  "total=\{items.length()} critical=\{critical.length()} high=\{high.length()} medium=\{medium.length()} low=\{low.length()} info=\{info.length()}"
}