///|
pub(all) struct PlanPolicy {
  format_version : Int
  allow_low_risk : Bool
  allow_medium_risk : Bool
  allow_high_risk : Bool
  require_regenerable : Bool
  maximum_items : Int
  maximum_reclaimable_bytes : Int64
  allowed_rule_ids : Array[String]
  protected_patterns : Array[String]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum PolicyViolationKind {
  RiskNotAllowed
  NotRegenerable
  TooManyItems
  ReclaimLimitExceeded
  RuleNotAllowed
  ProtectedPath
  ExistingPlanIssues
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct PolicyViolation {
  kind : PolicyViolationKind
  path : String
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn PlanPolicy::conservative() -> PlanPolicy {
  PlanPolicy::{
    format_version: 1,
    allow_low_risk: true,
    allow_medium_risk: false,
    allow_high_risk: false,
    require_regenerable: true,
    maximum_items: 1000,
    maximum_reclaimable_bytes: 10L * 1024L * 1024L * 1024L,
    allowed_rule_ids: [],
    protected_patterns: [".git", ".git/*", "src", "src/*", "LICENSE", "README*"],
  }
}

///|
pub fn PlanPolicy::permissive() -> PlanPolicy {
  PlanPolicy::{
    format_version: 1,
    allow_low_risk: true,
    allow_medium_risk: true,
    allow_high_risk: false,
    require_regenerable: false,
    maximum_items: 10000,
    maximum_reclaimable_bytes: 100L * 1024L * 1024L * 1024L,
    allowed_rule_ids: [],
    protected_patterns: [".git", ".git/*"],
  }
}

///|
fn policy_allows_risk(policy : PlanPolicy, risk : RiskLevel) -> Bool {
  match risk {
    Low => policy.allow_low_risk
    Medium => policy.allow_medium_risk
    High => policy.allow_high_risk
  }
}

///|
fn policy_allows_rule(policy : PlanPolicy, rule_id : String) -> Bool {
  policy.allowed_rule_ids.is_empty() ||
  policy.allowed_rule_ids.any(fn(allowed) { allowed == rule_id })
}

///|
fn policy_protects_path(policy : PlanPolicy, path : String) -> Bool {
  matches_any_pattern(path, policy.protected_patterns)
}

///|
pub fn review_plan_policy(
  plan : CleanupPlan,
  policy : PlanPolicy,
) -> Array[PolicyViolation] {
  let violations : Array[PolicyViolation] = []
  if !plan.issues.is_empty() {
    violations.push(PolicyViolation::{
      kind: ExistingPlanIssues,
      path: "",
      message: "The cleanup plan contains " +
      plan.issues.length().to_string() +
      " unresolved planning issues.",
    })
  }
  if plan.items.length() > policy.maximum_items {
    violations.push(PolicyViolation::{
      kind: TooManyItems,
      path: "",
      message: "The plan contains " +
      plan.items.length().to_string() +
      " items, exceeding the policy limit of " +
      policy.maximum_items.to_string() +
      ".",
    })
  }
  if plan.reclaimable_size > policy.maximum_reclaimable_bytes {
    violations.push(PolicyViolation::{
      kind: ReclaimLimitExceeded,
      path: "",
      message: "The plan would move " +
      format_bytes(plan.reclaimable_size) +
      ", exceeding the policy limit of " +
      format_bytes(policy.maximum_reclaimable_bytes) +
      ".",
    })
  }
  for item in plan.items {
    if !policy_allows_risk(policy, item.risk) {
      violations.push(PolicyViolation::{
        kind: RiskNotAllowed,
        path: item.source,
        message: "The " + risk_name(item.risk) + " risk level is not allowed.",
      })
    }
    if policy.require_regenerable && !item.regenerable {
      violations.push(PolicyViolation::{
        kind: NotRegenerable,
        path: item.source,
        message: "Policy requires every candidate to be regenerable.",
      })
    }
    if !policy_allows_rule(policy, item.rule_id) {
      violations.push(PolicyViolation::{
        kind: RuleNotAllowed,
        path: item.source,
        message: "Rule " + item.rule_id + " is not on the policy allowlist.",
      })
    }
    if policy_protects_path(policy, item.source) {
      violations.push(PolicyViolation::{
        kind: ProtectedPath,
        path: item.source,
        message: "The path matches a protected pattern.",
      })
    }
  }
  violations
}

///|
pub fn render_policy_review(violations : Array[PolicyViolation]) -> String {
  let lines : Array[String] = ["MoonDisk cleanup policy review"]
  if violations.is_empty() {
    lines.push("Approved: the cleanup plan satisfies every configured guard.")
  } else {
    lines.push(
      "Rejected: " + violations.length().to_string() + " violations found.",
    )
    for violation in violations {
      let location = if violation.path == "" {
        ""
      } else {
        " [" + violation.path + "]"
      }
      lines.push("- " + violation.message + location)
    }
  }
  lines.join("\n")
}

///|
pub fn plan_policy_to_json(policy : PlanPolicy) -> String {
  ToJson::to_json(policy).stringify(indent=2)
}

///|
pub fn plan_policy_from_json(source : String) -> PlanPolicy raise {
  let policy : PlanPolicy = @json.from_json(@json.parse(source))
  if policy.format_version != 1 {
    raise Failure("Unsupported cleanup policy version")
  }
  if policy.maximum_items < 0 || policy.maximum_reclaimable_bytes < 0L {
    raise Failure("Cleanup policy limits cannot be negative")
  }
  for pattern in policy.protected_patterns {
    if pattern.trim().is_empty() {
      raise Failure("Protected path patterns cannot be empty")
    }
  }
  policy
}

///|
pub async fn write_plan_policy(path : String, policy : PlanPolicy) -> Unit {
  @fs.write_file(path, plan_policy_to_json(policy))
}

///|
pub async fn read_plan_policy(path : String) -> PlanPolicy {
  plan_policy_from_json(@fs.read_file(path).text())
}