///|
pub(all) struct PolicyAuditItem {
  index : Int
  kind : String
  endpoint : String
  severity : String
  reachable : Bool
  executable : Bool
  duplicate : Bool
  advice : String
} derive(Eq, Debug, ToJson)

///|
pub(all) struct PolicyAudit {
  items : Array[PolicyAuditItem]
  total : Int
  executable : Int
  unreachable_count : Int
  duplicates : Int
  high_risk : Int
  coverage_percent : Int
} derive(Eq, Debug, ToJson)

///|
pub fn audit_policies(model : Model) -> PolicyAudit {
  let items : Array[PolicyAuditItem] = []
  let mut executable_count = 0
  let mut unreachable_count = 0
  let mut duplicates = 0
  let mut high_risk = 0
  for index, policy in model.policies {
    let endpoint = policy_endpoint(policy)
    let reachable = policy_endpoint_reachable(model, policy)
    let is_executable = reachable && policy_is_executable(model, policy)
    let duplicate = is_duplicate_policy(model.policies, index, policy)
    if is_executable {
      executable_count += 1
    } else {
      unreachable_count += 1
    }
    if duplicate {
      duplicates += 1
    }
    if policy.severity == "high" {
      high_risk += 1
    }
    items.push({
      index,
      kind: rule_kind_name(policy.kind),
      endpoint,
      severity: normalize_severity(policy.severity),
      reachable,
      executable: is_executable,
      duplicate,
      advice: policy_advice(policy, reachable, duplicate),
    })
  }
  let coverage_percent = if model.policies.length() == 0 {
    0
  } else {
    executable_count * 100 / model.policies.length()
  }
  {
    items,
    total: model.policies.length(),
    executable: executable_count,
    unreachable_count,
    duplicates,
    high_risk,
    coverage_percent,
  }
}

///|
pub fn policy_audit_json(audit : PolicyAudit) -> String {
  audit.to_json().stringify(indent=2)
}

///|
pub fn format_policy_audit(audit : PolicyAudit) -> String {
  let out = StringBuilder()
  out.write_string("policies=\{audit.total}")
  out.write_string(" executable=\{audit.executable}")
  out.write_string(" unreachable=\{audit.unreachable_count}")
  out.write_string(" duplicates=\{audit.duplicates}")
  out.write_string(" coverage=\{audit.coverage_percent}%")
  for item in audit.items {
    out.write_string("\n#\{item.index} \{item.kind} \{item.endpoint}")
    out.write_string(" severity=\{item.severity} reachable=\{item.reachable}")
    out.write_string(
      " executable=\{item.executable} duplicate=\{item.duplicate}",
    )
    out.write_string("\n  \{item.advice}")
  }
  out.to_string()
}

///|
pub fn normalize_severity(severity : String) -> String {
  if severity == "high" ||
    severity == "medium" ||
    severity == "low" ||
    severity == "info" {
    severity
  } else {
    "medium"
  }
}

///|
pub fn policy_is_executable(model : Model, policy : Policy) -> Bool {
  if !policy_endpoint_reachable(model, policy) {
    return false
  }
  match policy.kind {
    Allow => policy.path.length() >= 2
    Deny => policy.path.length() >= 2
    Require => policy.path.length() >= 2 && policy.through != ""
  }
}

///|
pub fn policy_endpoint_reachable(model : Model, policy : Policy) -> Bool {
  if policy.path.length() < 2 {
    return false
  }
  has_path(model, policy.path[0], policy.path[policy.path.length() - 1])
}

///|
pub fn policy_endpoint(policy : Policy) -> String {
  if policy.path.length() < 2 {
    ""
  } else {
    policy.path[0] + " -> " + policy.path[policy.path.length() - 1]
  }
}

///|
fn is_duplicate_policy(
  policies : Array[Policy],
  index : Int,
  policy : Policy,
) -> Bool {
  let mut current = 0
  for other in policies {
    if current < index &&
      other.kind == policy.kind &&
      same_path(other.path, policy.path) &&
      other.through == policy.through {
      return true
    }
    current += 1
  }
  false
}

///|
fn policy_advice(policy : Policy, reachable : Bool, duplicate : Bool) -> String {
  if duplicate {
    "remove the duplicate policy or document why two identical controls are intentionally retained"
  } else if !reachable {
    "verify source and sink names; this rule currently has no executable graph path"
  } else if policy.kind == Require && policy.through == "" {
    "declare the required sanitizer or boundary explicitly"
  } else if policy.kind == Allow {
    "keep the exception exact and include a review reason in the description"
  } else if policy.severity == "high" {
    "keep this control in CI and require an explicit remediation owner"
  } else {
    "retain a fixture that exercises this policy on every relevant branch"
  }
}