///|
/// CapsuleTrace models data-retention capsules, not repository acceptance proof.
///
/// The library is intentionally deterministic and offline: callers pass a
/// logical day number, retention rules, and in-memory capsule records. The
/// result is a deletion/anonymization/quarantine plan that can be rendered for
/// review, tests, or CI artifacts without scanning a repository or calling a
/// remote service.
pub(all) enum CapsuleSensitivity {
  PublicData
  InternalData
  ConfidentialData
  SensitiveData
  RestrictedData
} derive(Eq, Debug)

///|
pub(all) enum ConsentState {
  ConsentGranted
  ConsentDenied
  ConsentExpired
  ConsentWithdrawn
  ConsentNotRequired
} derive(Eq, Debug)

///|
pub(all) enum SharingMode {
  InternalOnly
  ProcessorShared
  PublicShared
} derive(Eq, Debug)

///|
pub(all) enum RetentionAction {
  KeepData
  AnonymizeData
  DeleteData
  QuarantineData
  BlockCollection
} derive(Eq, Debug)

///|
pub(all) enum RetentionSeverity {
  RetentionCritical
  RetentionHigh
  RetentionMedium
  RetentionLow
  RetentionInfo
} derive(Eq, Debug)

///|
pub(all) enum RetentionIssueKind {
  MissingRule
  ConsentInvalid
  RetentionExpired
  AnonymizationDue
  RegionDenied
  EncryptionMissing
  SharingDenied
  CollectionBlocked
} derive(Eq, Debug)

///|
pub(all) struct RetentionRule {
  id : String
  purpose : String
  data_kind : String
  max_keep_days : Int
  anonymize_after_days : Int
  requires_consent : Bool
  allowed_regions : Array[String]
  allow_external_sharing : Bool
  owner : String
  note : String
} derive(Eq, Debug)

///|
pub(all) struct DataCapsule {
  id : String
  subject_kind : String
  purpose : String
  data_kind : String
  collected_day : Int
  last_used_day : Int
  consent : ConsentState
  sensitivity : CapsuleSensitivity
  region : String
  encrypted : Bool
  sharing : SharingMode
  note : String
} derive(Eq, Debug)

///|
pub(all) struct RetentionReason {
  kind : RetentionIssueKind
  severity : RetentionSeverity
  message : String
  remedy : String
} derive(Eq, Debug)

///|
pub(all) struct CapsuleDecision {
  capsule : DataCapsule
  rule_id : String
  action : RetentionAction
  severity : RetentionSeverity
  age_days : Int
  idle_days : Int
  days_until_delete : Int
  reasons : Array[RetentionReason]
  owner : String
} derive(Eq, Debug)

///|
pub(all) struct PlanSummary {
  total : Int
  keep_count : Int
  anonymize_count : Int
  delete_count : Int
  quarantine_count : Int
  block_count : Int
  critical_count : Int
  high_count : Int
  medium_count : Int
  encrypted_count : Int
  external_shared_count : Int
  risk_score : Int
} derive(Eq, Debug)

///|
pub(all) struct RetentionPlan {
  generated_day : Int
  decisions : Array[CapsuleDecision]
  rules : Array[RetentionRule]
  summary : PlanSummary
} derive(Eq, Debug)

///|
pub(all) struct PurposeSummary {
  purpose : String
  total : Int
  keep_count : Int
  anonymize_count : Int
  delete_count : Int
  quarantine_count : Int
} derive(Eq, Debug)

///|
pub(all) struct RegionSummary {
  region : String
  total : Int
  encrypted_count : Int
  external_shared_count : Int
  high_or_critical_count : Int
} derive(Eq, Debug)

///|
pub(all) struct RetentionWindow {
  day : Int
  action : RetentionAction
  capsule_ids : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct RetentionCalendar {
  generated_day : Int
  windows : Array[RetentionWindow]
} derive(Eq, Debug)

///|
pub fn CapsuleSensitivity::label(self : CapsuleSensitivity) -> String {
  match self {
    PublicData => "public"
    InternalData => "internal"
    ConfidentialData => "confidential"
    SensitiveData => "sensitive"
    RestrictedData => "restricted"
  }
}

///|
pub fn CapsuleSensitivity::risk_weight(self : CapsuleSensitivity) -> Int {
  match self {
    PublicData => 0
    InternalData => 1
    ConfidentialData => 4
    SensitiveData => 7
    RestrictedData => 10
  }
}

///|
pub fn CapsuleSensitivity::requires_encryption(
  self : CapsuleSensitivity,
) -> Bool {
  self == ConfidentialData || self == SensitiveData || self == RestrictedData
}

///|
pub fn ConsentState::label(self : ConsentState) -> String {
  match self {
    ConsentGranted => "granted"
    ConsentDenied => "denied"
    ConsentExpired => "expired"
    ConsentWithdrawn => "withdrawn"
    ConsentNotRequired => "not-required"
  }
}

///|
pub fn ConsentState::allows_processing(self : ConsentState) -> Bool {
  self == ConsentGranted || self == ConsentNotRequired
}

///|
pub fn SharingMode::label(self : SharingMode) -> String {
  match self {
    InternalOnly => "internal"
    ProcessorShared => "processor"
    PublicShared => "public"
  }
}

///|
pub fn SharingMode::is_external(self : SharingMode) -> Bool {
  self == ProcessorShared || self == PublicShared
}

///|
pub fn RetentionAction::label(self : RetentionAction) -> String {
  match self {
    KeepData => "keep"
    AnonymizeData => "anonymize"
    DeleteData => "delete"
    QuarantineData => "quarantine"
    BlockCollection => "block"
  }
}

///|
pub fn RetentionAction::priority(self : RetentionAction) -> Int {
  match self {
    KeepData => 0
    AnonymizeData => 20
    QuarantineData => 35
    BlockCollection => 45
    DeleteData => 50
  }
}

///|
pub fn RetentionSeverity::label(self : RetentionSeverity) -> String {
  match self {
    RetentionCritical => "critical"
    RetentionHigh => "high"
    RetentionMedium => "medium"
    RetentionLow => "low"
    RetentionInfo => "info"
  }
}

///|
pub fn RetentionSeverity::weight(self : RetentionSeverity) -> Int {
  match self {
    RetentionCritical => 40
    RetentionHigh => 24
    RetentionMedium => 10
    RetentionLow => 3
    RetentionInfo => 0
  }
}

///|
pub fn RetentionIssueKind::label(self : RetentionIssueKind) -> String {
  match self {
    MissingRule => "missing-rule"
    ConsentInvalid => "consent-invalid"
    RetentionExpired => "retention-expired"
    AnonymizationDue => "anonymization-due"
    RegionDenied => "region-denied"
    EncryptionMissing => "encryption-missing"
    SharingDenied => "sharing-denied"
    CollectionBlocked => "collection-blocked"
  }
}

///|
pub fn retention_rule(
  id : String,
  purpose : String,
  data_kind : String,
  max_keep_days : Int,
  anonymize_after_days : Int,
  requires_consent : Bool,
  allowed_regions : Array[String],
  allow_external_sharing : Bool,
  owner : String,
  note : String,
) -> RetentionRule {
  {
    id,
    purpose,
    data_kind,
    max_keep_days,
    anonymize_after_days,
    requires_consent,
    allowed_regions,
    allow_external_sharing,
    owner,
    note,
  }
}

///|
pub fn data_capsule(
  id : String,
  subject_kind : String,
  purpose : String,
  data_kind : String,
  collected_day : Int,
  last_used_day : Int,
  consent : ConsentState,
  sensitivity : CapsuleSensitivity,
  region : String,
  encrypted : Bool,
  sharing : SharingMode,
  note : String,
) -> DataCapsule {
  {
    id,
    subject_kind,
    purpose,
    data_kind,
    collected_day,
    last_used_day,
    consent,
    sensitivity,
    region,
    encrypted,
    sharing,
    note,
  }
}

///|
pub fn evaluate_capsule(
  capsule : DataCapsule,
  rules : Array[RetentionRule],
  today : Int,
) -> CapsuleDecision {
  match find_rule_for(capsule, rules) {
    None =>
      build_decision(capsule, "", today, 0, "privacy-team", [
        {
          kind: MissingRule,
          severity: RetentionHigh,
          message: "no retention rule matches purpose '" +
          capsule.purpose +
          "' and data kind '" +
          capsule.data_kind +
          "'",
          remedy: "add a retention rule before collecting or processing this capsule",
        },
      ])
    Some(rule) => {
      let reasons : Array[RetentionReason] = []
      let age_days = days_since(today, capsule.collected_day)
      let idle_days = days_since(today, capsule.last_used_day)
      if rule.requires_consent && capsule.consent == ConsentDenied {
        reasons.push({
          kind: CollectionBlocked,
          severity: RetentionCritical,
          message: "consent was denied for a consent-gated data purpose",
          remedy: "block collection and delete any already-collected records",
        })
      }
      if rule.requires_consent && capsule.consent == ConsentWithdrawn {
        reasons.push({
          kind: ConsentInvalid,
          severity: RetentionCritical,
          message: "consent was withdrawn for this capsule",
          remedy: "delete the capsule unless another lawful basis is documented",
        })
      }
      if rule.requires_consent && capsule.consent == ConsentExpired {
        reasons.push({
          kind: ConsentInvalid,
          severity: RetentionHigh,
          message: "consent has expired for this capsule",
          remedy: "refresh consent or anonymize data before further processing",
        })
      }
      if age_days >= rule.max_keep_days {
        reasons.push({
          kind: RetentionExpired,
          severity: RetentionCritical,
          message: "capsule age exceeds the retention limit of " +
          rule.max_keep_days.to_string() +
          " days",
          remedy: "delete the capsule from active storage and scheduled exports",
        })
      } else if age_days >= rule.anonymize_after_days ||
        idle_days >= rule.anonymize_after_days {
        reasons.push({
          kind: AnonymizationDue,
          severity: RetentionMedium,
          message: "capsule passed the anonymization window of " +
          rule.anonymize_after_days.to_string() +
          " days",
          remedy: "strip direct identifiers or aggregate the capsule",
        })
      }
      if !region_allowed(rule, capsule.region) {
        reasons.push({
          kind: RegionDenied,
          severity: RetentionHigh,
          message: "region '" +
          capsule.region +
          "' is not allowed by rule " +
          rule.id,
          remedy: "move the capsule to an allowed region or quarantine it",
        })
      }
      if capsule.sensitivity.requires_encryption() && !capsule.encrypted {
        reasons.push({
          kind: EncryptionMissing,
          severity: RetentionHigh,
          message: capsule.sensitivity.label() + " data is not encrypted",
          remedy: "encrypt the capsule before processing or export",
        })
      }
      if capsule.sharing.is_external() && !rule.allow_external_sharing {
        reasons.push({
          kind: SharingDenied,
          severity: RetentionHigh,
          message: "external sharing is not permitted by rule " + rule.id,
          remedy: "remove processors/exports or create a separate approved rule",
        })
      }
      build_decision(
        capsule,
        rule.id,
        today,
        rule.max_keep_days,
        rule.owner,
        reasons,
      )
    }
  }
}

///|
pub fn plan_retention(
  capsules : Array[DataCapsule],
  rules : Array[RetentionRule],
  today : Int,
) -> RetentionPlan {
  let decisions : Array[CapsuleDecision] = []
  for capsule in capsules {
    decisions.push(evaluate_capsule(capsule, rules, today))
  }
  let decisions = sort_decisions(decisions)
  { generated_day: today, decisions, rules, summary: summarize(decisions) }
}

///|
pub fn RetentionPlan::is_ready(self : RetentionPlan) -> Bool {
  self.summary.critical_count == 0 && self.summary.high_count == 0
}

///|
pub fn RetentionPlan::has_action(
  self : RetentionPlan,
  action : RetentionAction,
) -> Bool {
  self.decisions.any(decision => decision.action == action)
}

///|
pub fn RetentionPlan::action_count(
  self : RetentionPlan,
  action : RetentionAction,
) -> Int {
  self.decisions.count_if(decision => decision.action == action)
}

///|
pub fn RetentionPlan::decision(
  self : RetentionPlan,
  capsule_id : StringView,
) -> CapsuleDecision? {
  let expected = capsule_id.trim().to_owned()
  self.decisions.iter().find_first(decision => decision.capsule.id == expected)
}

///|
pub fn RetentionPlan::deletion_queue(
  self : RetentionPlan,
) -> Array[CapsuleDecision] {
  self.decisions.filter(decision => decision.action == DeleteData)
}

///|
pub fn RetentionPlan::anonymization_queue(
  self : RetentionPlan,
) -> Array[CapsuleDecision] {
  self.decisions.filter(decision => decision.action == AnonymizeData)
}

///|
pub fn RetentionPlan::quarantine_queue(
  self : RetentionPlan,
) -> Array[CapsuleDecision] {
  self.decisions.filter(decision => decision.action == QuarantineData)
}

///|
pub fn RetentionPlan::blocked_collection(
  self : RetentionPlan,
) -> Array[CapsuleDecision] {
  self.decisions.filter(decision => decision.action == BlockCollection)
}

///|
pub fn RetentionPlan::keep_queue(
  self : RetentionPlan,
) -> Array[CapsuleDecision] {
  self.decisions.filter(decision => decision.action == KeepData)
}

///|
pub fn RetentionPlan::purpose_summaries(
  self : RetentionPlan,
) -> Array[PurposeSummary] {
  let summaries : Array[PurposeSummary] = []
  for decision in self.decisions {
    if find_purpose_summary(summaries, decision.capsule.purpose) is None {
      summaries.push({
        purpose: decision.capsule.purpose,
        total: 0,
        keep_count: 0,
        anonymize_count: 0,
        delete_count: 0,
        quarantine_count: 0,
      })
    }
  }
  let result : Array[PurposeSummary] = []
  for summary in summaries {
    let related = self.decisions.filter(decision => {
      decision.capsule.purpose == summary.purpose
    })
    result.push({
      purpose: summary.purpose,
      total: related.length(),
      keep_count: related.count_if(decision => decision.action == KeepData),
      anonymize_count: related.count_if(decision => {
        decision.action == AnonymizeData
      }),
      delete_count: related.count_if(decision => decision.action == DeleteData),
      quarantine_count: related.count_if(decision => {
        decision.action == QuarantineData || decision.action == BlockCollection
      }),
    })
  }
  sort_purpose_summaries(result)
}

///|
pub fn RetentionPlan::region_summaries(
  self : RetentionPlan,
) -> Array[RegionSummary] {
  let regions : Array[String] = []
  for decision in self.decisions {
    if !regions.contains(decision.capsule.region) {
      regions.push(decision.capsule.region)
    }
  }
  let result : Array[RegionSummary] = []
  for region in regions {
    let related = self.decisions.filter(decision => {
      decision.capsule.region == region
    })
    result.push({
      region,
      total: related.length(),
      encrypted_count: related.count_if(decision => decision.capsule.encrypted),
      external_shared_count: related.count_if(decision => {
        decision.capsule.sharing.is_external()
      }),
      high_or_critical_count: related.count_if(decision => {
        decision.severity == RetentionCritical ||
        decision.severity == RetentionHigh
      }),
    })
  }
  sort_region_summaries(result)
}

///|
pub fn RetentionPlan::build_calendar(self : RetentionPlan) -> RetentionCalendar {
  let windows : Array[RetentionWindow] = []
  for decision in self.decisions {
    let day = action_due_day(decision)
    let action = decision.action
    match find_window(windows, day, action) {
      Some(index) => windows[index].capsule_ids.push(decision.capsule.id)
      None => windows.push({ day, action, capsule_ids: [decision.capsule.id] })
    }
  }
  { generated_day: self.generated_day, windows: sort_windows(windows) }
}

///|
pub fn RetentionPlan::to_markdown(self : RetentionPlan) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Retention Plan")
  lines.push("")
  lines.push("- Generated day: " + self.generated_day.to_string())
  lines.push("- Ready: " + bool_label(self.is_ready()))
  lines.push("- Risk score: " + self.summary.risk_score.to_string())
  lines.push("- Capsules: " + self.summary.total.to_string())
  lines.push(
    "- Actions: keep=" +
    self.summary.keep_count.to_string() +
    ", anonymize=" +
    self.summary.anonymize_count.to_string() +
    ", delete=" +
    self.summary.delete_count.to_string() +
    ", quarantine=" +
    self.summary.quarantine_count.to_string() +
    ", block=" +
    self.summary.block_count.to_string(),
  )
  lines.push("")
  lines.push("## Decisions")
  lines.push("")
  lines.push(
    "| Capsule | Purpose | Kind | Region | Action | Severity | Rule | Reason |",
  )
  lines.push("| --- | --- | --- | --- | --- | --- | --- | --- |")
  if self.decisions.is_empty() {
    lines.push("| - | - | - | - | keep | info | - | no capsules |")
  } else {
    for decision in self.decisions {
      lines.push(
        "| " +
        markdown_cell(decision.capsule.id) +
        " | " +
        markdown_cell(decision.capsule.purpose) +
        " | " +
        markdown_cell(decision.capsule.data_kind) +
        " | " +
        markdown_cell(decision.capsule.region) +
        " | " +
        decision.action.label() +
        " | " +
        decision.severity.label() +
        " | " +
        markdown_cell(printable(decision.rule_id)) +
        " | " +
        markdown_cell(decision.reason_summary()) +
        " |",
      )
    }
  }
  lines.push("")
  lines.push("## Purpose Summary")
  lines.push("")
  lines.push(
    "| Purpose | Total | Keep | Anonymize | Delete | Quarantine/Block |",
  )
  lines.push("| --- | --- | --- | --- | --- | --- |")
  for item in self.purpose_summaries() {
    lines.push(
      "| " +
      markdown_cell(item.purpose) +
      " | " +
      item.total.to_string() +
      " | " +
      item.keep_count.to_string() +
      " | " +
      item.anonymize_count.to_string() +
      " | " +
      item.delete_count.to_string() +
      " | " +
      item.quarantine_count.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn RetentionPlan::to_json_string(self : RetentionPlan) -> String {
  Json::object(
    Map([
      ("generated_day", json_int(self.generated_day)),
      ("ready", Json::boolean(self.is_ready())),
      ("risk_score", json_int(self.summary.risk_score)),
      ("summary", plan_summary_to_json(self.summary)),
      ("decisions", decisions_to_json(self.decisions)),
      ("purpose_summaries", purpose_summaries_to_json(self.purpose_summaries())),
      ("region_summaries", region_summaries_to_json(self.region_summaries())),
    ]),
  ).stringify(indent=2)
}

///|
pub fn CapsuleDecision::reason_summary(self : CapsuleDecision) -> String {
  if self.reasons.is_empty() {
    "within policy"
  } else {
    let labels : Array[String] = []
    for reason in self.reasons {
      labels.push(reason.kind.label())
    }
    labels.join(", ")
  }
}

///|
pub fn CapsuleDecision::needs_operator_action(self : CapsuleDecision) -> Bool {
  self.action != KeepData
}

///|
pub fn RetentionCalendar::to_markdown(self : RetentionCalendar) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Retention Calendar")
  lines.push("")
  lines.push("- Generated day: " + self.generated_day.to_string())
  lines.push("")
  lines.push("| Day | Action | Capsules |")
  lines.push("| --- | --- | --- |")
  if self.windows.is_empty() {
    lines.push("| - | keep | - |")
  } else {
    for window in self.windows {
      lines.push(
        "| " +
        window.day.to_string() +
        " | " +
        window.action.label() +
        " | " +
        markdown_cell(sort_strings(window.capsule_ids).join(", ")) +
        " |",
      )
    }
  }
  lines.join("\n")
}

///|
pub fn sample_rules() -> Array[RetentionRule] {
  [
    retention_rule(
      "analytics.behavior",
      "analytics",
      "behavior",
      180,
      30,
      true,
      ["eu", "cn", "us"],
      false,
      "privacy-analytics",
      "aggregate behavior capsules after short-term product analysis",
    ),
    retention_rule(
      "support.ticket",
      "support",
      "ticket",
      365,
      120,
      false,
      ["cn", "sg"],
      true,
      "support-ops",
      "support tickets may be processed by approved vendors",
    ),
    retention_rule(
      "billing.invoice",
      "billing",
      "invoice",
      2555,
      2555,
      false,
      ["cn"],
      false,
      "finance",
      "billing records stay longer but must remain local and encrypted",
    ),
  ]
}

///|
pub fn sample_capsules() -> Array[DataCapsule] {
  [
    data_capsule(
      "cap.active-analytics",
      "visitor",
      "analytics",
      "behavior",
      200,
      216,
      ConsentGranted,
      SensitiveData,
      "cn",
      true,
      InternalOnly,
      "active product telemetry capsule",
    ),
    data_capsule(
      "cap.idle-analytics",
      "visitor",
      "analytics",
      "behavior",
      120,
      150,
      ConsentGranted,
      SensitiveData,
      "cn",
      true,
      InternalOnly,
      "idle enough to anonymize",
    ),
    data_capsule(
      "cap.withdrawn",
      "visitor",
      "analytics",
      "behavior",
      160,
      190,
      ConsentWithdrawn,
      SensitiveData,
      "cn",
      true,
      InternalOnly,
      "consent withdrawal should trigger deletion",
    ),
    data_capsule(
      "cap.unencrypted-ticket",
      "customer",
      "support",
      "ticket",
      40,
      205,
      ConsentNotRequired,
      ConfidentialData,
      "sg",
      false,
      ProcessorShared,
      "vendor support record missing encryption",
    ),
    data_capsule(
      "cap.region-mismatch",
      "buyer",
      "billing",
      "invoice",
      20,
      210,
      ConsentNotRequired,
      RestrictedData,
      "us",
      true,
      InternalOnly,
      "invoice stored outside allowed region",
    ),
  ]
}

///|
pub fn sample_plan() -> RetentionPlan {
  plan_retention(sample_capsules(), sample_rules(), 220)
}

///|
pub fn compact_sample() -> String {
  sample_plan().to_markdown()
}

///|
fn build_decision(
  capsule : DataCapsule,
  rule_id : String,
  today : Int,
  max_keep_days : Int,
  owner : String,
  reasons : Array[RetentionReason],
) -> CapsuleDecision {
  let age_days = days_since(today, capsule.collected_day)
  let idle_days = days_since(today, capsule.last_used_day)
  let action = action_for(reasons)
  {
    capsule,
    rule_id,
    action,
    severity: severity_for(reasons),
    age_days,
    idle_days,
    days_until_delete: days_remaining(age_days, max_keep_days),
    reasons,
    owner,
  }
}

///|
fn action_for(reasons : Array[RetentionReason]) -> RetentionAction {
  let mut action = KeepData
  for reason in reasons {
    let next = action_for_issue(reason.kind)
    if next.priority() > action.priority() {
      action = next
    }
  }
  action
}

///|
fn action_for_issue(kind : RetentionIssueKind) -> RetentionAction {
  match kind {
    MissingRule => QuarantineData
    ConsentInvalid => DeleteData
    RetentionExpired => DeleteData
    AnonymizationDue => AnonymizeData
    RegionDenied => QuarantineData
    EncryptionMissing => QuarantineData
    SharingDenied => QuarantineData
    CollectionBlocked => BlockCollection
  }
}

///|
fn severity_for(reasons : Array[RetentionReason]) -> RetentionSeverity {
  let mut severity = RetentionInfo
  for reason in reasons {
    severity = max_severity(severity, reason.severity)
  }
  severity
}

///|
fn max_severity(
  a : RetentionSeverity,
  b : RetentionSeverity,
) -> RetentionSeverity {
  if b.weight() > a.weight() {
    b
  } else {
    a
  }
}

///|
fn days_since(today : Int, day : Int) -> Int {
  if today > day {
    today - day
  } else {
    0
  }
}

///|
fn days_remaining(age_days : Int, max_keep_days : Int) -> Int {
  if max_keep_days <= 0 || age_days >= max_keep_days {
    0
  } else {
    max_keep_days - age_days
  }
}

///|
fn region_allowed(rule : RetentionRule, region : String) -> Bool {
  rule.allowed_regions.any(item => {
    normalize_token(item) == normalize_token(region)
  })
}

///|
fn find_rule_for(
  capsule : DataCapsule,
  rules : Array[RetentionRule],
) -> RetentionRule? {
  let purpose = normalize_token(capsule.purpose)
  let data_kind = normalize_token(capsule.data_kind)
  match
    rules
    .iter()
    .find_first(rule => {
      normalize_token(rule.purpose) == purpose &&
      normalize_token(rule.data_kind) == data_kind
    }) {
    Some(rule) => Some(rule)
    None =>
      rules
      .iter()
      .find_first(rule => {
        normalize_token(rule.purpose) == purpose &&
        normalize_token(rule.data_kind) == "*"
      })
  }
}

///|
fn summarize(decisions : Array[CapsuleDecision]) -> PlanSummary {
  let total = decisions.length()
  let keep_count = decisions.count_if(decision => decision.action == KeepData)
  let anonymize_count = decisions.count_if(decision => {
    decision.action == AnonymizeData
  })
  let delete_count = decisions.count_if(decision => {
    decision.action == DeleteData
  })
  let quarantine_count = decisions.count_if(decision => {
    decision.action == QuarantineData
  })
  let block_count = decisions.count_if(decision => {
    decision.action == BlockCollection
  })
  let critical_count = decisions.count_if(decision => {
    decision.severity == RetentionCritical
  })
  let high_count = decisions.count_if(decision => {
    decision.severity == RetentionHigh
  })
  let medium_count = decisions.count_if(decision => {
    decision.severity == RetentionMedium
  })
  let encrypted_count = decisions.count_if(decision => {
    decision.capsule.encrypted
  })
  let external_shared_count = decisions.count_if(decision => {
    decision.capsule.sharing.is_external()
  })
  let mut risk_score = 0
  for decision in decisions {
    risk_score = risk_score +
      decision.severity.weight() +
      decision.capsule.sensitivity.risk_weight()
  }
  {
    total,
    keep_count,
    anonymize_count,
    delete_count,
    quarantine_count,
    block_count,
    critical_count,
    high_count,
    medium_count,
    encrypted_count,
    external_shared_count,
    risk_score,
  }
}

///|
fn sort_decisions(values : Array[CapsuleDecision]) -> Array[CapsuleDecision] {
  let mut sorted : Array[CapsuleDecision] = []
  for value in values {
    let next : Array[CapsuleDecision] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && compare_decision(value, existing) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn compare_decision(a : CapsuleDecision, b : CapsuleDecision) -> Int {
  let severity_delta = b.severity.weight() - a.severity.weight()
  if severity_delta != 0 {
    severity_delta
  } else {
    a.capsule.id.lexical_compare(b.capsule.id)
  }
}

///|
fn sort_strings(values : Array[String]) -> Array[String] {
  let mut sorted : Array[String] = []
  for value in values {
    let next : Array[String] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.lexical_compare(existing) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn find_purpose_summary(
  summaries : Array[PurposeSummary],
  purpose : String,
) -> PurposeSummary? {
  summaries.iter().find_first(summary => summary.purpose == purpose)
}

///|
fn sort_purpose_summaries(
  values : Array[PurposeSummary],
) -> Array[PurposeSummary] {
  let mut sorted : Array[PurposeSummary] = []
  for value in values {
    let next : Array[PurposeSummary] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.purpose.lexical_compare(existing.purpose) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn sort_region_summaries(values : Array[RegionSummary]) -> Array[RegionSummary] {
  let mut sorted : Array[RegionSummary] = []
  for value in values {
    let next : Array[RegionSummary] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.region.lexical_compare(existing.region) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn find_window(
  windows : Array[RetentionWindow],
  day : Int,
  action : RetentionAction,
) -> Int? {
  for index, window in windows.iter2() {
    if window.day == day && window.action == action {
      return Some(index)
    }
  }
  None
}

///|
fn sort_windows(values : Array[RetentionWindow]) -> Array[RetentionWindow] {
  let mut sorted : Array[RetentionWindow] = []
  for value in values {
    let next : Array[RetentionWindow] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && compare_window(value, existing) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn compare_window(a : RetentionWindow, b : RetentionWindow) -> Int {
  if a.day != b.day {
    a.day - b.day
  } else {
    b.action.priority() - a.action.priority()
  }
}

///|
fn action_due_day(decision : CapsuleDecision) -> Int {
  if decision.action == KeepData {
    decision.generated_due_day()
  } else {
    decision.capsule.last_used_day
  }
}

///|
fn CapsuleDecision::generated_due_day(self : CapsuleDecision) -> Int {
  if self.days_until_delete <= 0 {
    self.capsule.collected_day + self.age_days
  } else {
    self.capsule.collected_day + self.age_days + self.days_until_delete
  }
}

///|
fn bool_label(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn printable(value : String) -> String {
  if value.trim().is_empty() {
    "-"
  } else {
    value
  }
}

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

///|
fn normalize_token(value : String) -> String {
  value.trim().to_lower().to_owned()
}

///|
fn json_int(value : Int) -> Json {
  Json::number(value.to_double(), repr=value.to_string())
}

///|
fn plan_summary_to_json(summary : PlanSummary) -> Json {
  Json::object(
    Map([
      ("total", json_int(summary.total)),
      ("keep_count", json_int(summary.keep_count)),
      ("anonymize_count", json_int(summary.anonymize_count)),
      ("delete_count", json_int(summary.delete_count)),
      ("quarantine_count", json_int(summary.quarantine_count)),
      ("block_count", json_int(summary.block_count)),
      ("critical_count", json_int(summary.critical_count)),
      ("high_count", json_int(summary.high_count)),
      ("medium_count", json_int(summary.medium_count)),
      ("encrypted_count", json_int(summary.encrypted_count)),
      ("external_shared_count", json_int(summary.external_shared_count)),
      ("risk_score", json_int(summary.risk_score)),
    ]),
  )
}

///|
fn decisions_to_json(decisions : Array[CapsuleDecision]) -> Json {
  let values : Array[Json] = []
  for decision in decisions {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(decision.capsule.id)),
          ("purpose", Json::string(decision.capsule.purpose)),
          ("data_kind", Json::string(decision.capsule.data_kind)),
          ("region", Json::string(decision.capsule.region)),
          ("sensitivity", Json::string(decision.capsule.sensitivity.label())),
          ("consent", Json::string(decision.capsule.consent.label())),
          ("sharing", Json::string(decision.capsule.sharing.label())),
          ("encrypted", Json::boolean(decision.capsule.encrypted)),
          ("rule_id", Json::string(decision.rule_id)),
          ("action", Json::string(decision.action.label())),
          ("severity", Json::string(decision.severity.label())),
          ("age_days", json_int(decision.age_days)),
          ("idle_days", json_int(decision.idle_days)),
          ("days_until_delete", json_int(decision.days_until_delete)),
          ("owner", Json::string(decision.owner)),
          ("reasons", reasons_to_json(decision.reasons)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn reasons_to_json(reasons : Array[RetentionReason]) -> Json {
  let values : Array[Json] = []
  for reason in reasons {
    values.push(
      Json::object(
        Map([
          ("kind", Json::string(reason.kind.label())),
          ("severity", Json::string(reason.severity.label())),
          ("message", Json::string(reason.message)),
          ("remedy", Json::string(reason.remedy)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn purpose_summaries_to_json(items : Array[PurposeSummary]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("purpose", Json::string(item.purpose)),
          ("total", json_int(item.total)),
          ("keep_count", json_int(item.keep_count)),
          ("anonymize_count", json_int(item.anonymize_count)),
          ("delete_count", json_int(item.delete_count)),
          ("quarantine_count", json_int(item.quarantine_count)),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn region_summaries_to_json(items : Array[RegionSummary]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("region", Json::string(item.region)),
          ("total", json_int(item.total)),
          ("encrypted_count", json_int(item.encrypted_count)),
          ("external_shared_count", json_int(item.external_shared_count)),
          ("high_or_critical_count", json_int(item.high_or_critical_count)),
        ]),
      ),
    )
  }
  Json::array(values)
}