///|
/// Retention and purge planning keeps privacy operations reproducible. The
/// planner produces candidates and evidence; a storage adapter performs the
/// irreversible action after a separate authorization check.
pub(all) enum ArchiveRetentionClass {
  ArchiveClinicalRecord
  ArchiveAuditRecord
  ArchiveReviewRecord
  ArchiveMetricRecord
  ArchiveTemporaryRecord
} derive(Debug, Eq)

///|
pub(all) enum ArchiveRetentionAction {
  ArchiveKeep
  ArchivePurge
  ArchiveArchive
  ArchiveHold
  ArchiveReview
} derive(Debug, Eq)

///|
pub(all) struct ArchiveRetentionRule {
  rule_id : String
  class : ArchiveRetentionClass
  retention_days : Int
  action : ArchiveRetentionAction
  legal_basis : String
  owner : String
} derive(Debug, Eq)

///|
pub(all) struct ArchiveRecord {
  record_id : String
  class : ArchiveRetentionClass
  created_at : String
  last_accessed_at : String
  checksum : String
  legal_hold : Bool
  owner : String
  tags : Array[String]
} derive(Debug, Eq)

///|
pub(all) struct ArchivePurgeCandidate {
  record : ArchiveRecord
  rule : ArchiveRetentionRule
  action : ArchiveRetentionAction
  reason : String
  authorization_checksum : String
} derive(Debug)

///|
pub(all) struct ArchiveRetentionPlan {
  plan_id : String
  as_of : String
  candidates : Array[ArchivePurgeCandidate]
  held_records : Array[String]
  unknown_records : Array[String]
  checksum : String
} derive(Debug)

///|
pub fn archive_retention_class_name(class : ArchiveRetentionClass) -> String {
  match class {
    ArchiveClinicalRecord => "clinical"
    ArchiveAuditRecord => "audit"
    ArchiveReviewRecord => "review"
    ArchiveMetricRecord => "metric"
    ArchiveTemporaryRecord => "temporary"
  }
}

///|
pub fn archive_retention_action_name(action : ArchiveRetentionAction) -> String {
  match action {
    ArchiveKeep => "keep"
    ArchivePurge => "purge"
    ArchiveArchive => "archive"
    ArchiveHold => "hold"
    ArchiveReview => "review"
  }
}

///|
pub fn archive_retention_rule(
  rule_id : String,
  class : ArchiveRetentionClass,
  retention_days : Int,
  action : ArchiveRetentionAction,
  legal_basis : String,
  owner : String,
) -> ArchiveRetentionRule {
  {
    rule_id,
    class,
    retention_days: if retention_days < 0 {
      0
    } else {
      retention_days
    },
    action,
    legal_basis,
    owner,
  }
}

///|
pub fn ArchiveRetentionRule::is_valid(self : ArchiveRetentionRule) -> Bool {
  self.rule_id.length() > 0 &&
  self.retention_days >= 0 &&
  self.legal_basis.length() > 0 &&
  self.owner.length() > 0
}

///|
pub fn archive_default_retention_rules() -> Array[ArchiveRetentionRule] {
  [
    archive_retention_rule(
      "clinical-default",
      ArchiveClinicalRecord,
      2555,
      ArchiveKeep,
      "care-continuity",
      "privacy",
    ),
    archive_retention_rule(
      "audit-default",
      ArchiveAuditRecord,
      2555,
      ArchiveArchive,
      "accountability",
      "privacy",
    ),
    archive_retention_rule(
      "review-default",
      ArchiveReviewRecord,
      365,
      ArchiveArchive,
      "quality-assurance",
      "quality",
    ),
    archive_retention_rule(
      "metric-default",
      ArchiveMetricRecord,
      730,
      ArchiveArchive,
      "operations",
      "analytics",
    ),
    archive_retention_rule(
      "temporary-default",
      ArchiveTemporaryRecord,
      30,
      ArchivePurge,
      "data-minimization",
      "platform",
    ),
  ]
}

///|
pub fn archive_record(
  record_id : String,
  class : ArchiveRetentionClass,
  created_at : String,
  last_accessed_at : String,
  checksum : String,
  owner : String,
) -> ArchiveRecord {
  {
    record_id,
    class,
    created_at,
    last_accessed_at,
    checksum,
    legal_hold: false,
    owner,
    tags: [],
  }
}

///|
pub fn ArchiveRecord::with_hold(self : ArchiveRecord) -> ArchiveRecord {
  { ..self, legal_hold: true }
}

///|
pub fn ArchiveRecord::with_tag(
  record : ArchiveRecord,
  tag : String,
) -> ArchiveRecord {
  if !record.tags.contains(tag) {
    record.tags.push(tag)
  }
  record
}

///|
pub fn ArchiveRecord::is_valid(self : ArchiveRecord) -> Bool {
  self.record_id.length() > 0 &&
  self.created_at.length() > 0 &&
  self.checksum.length() > 0 &&
  self.owner.length() > 0
}

///|
fn archive_rule_for(
  rules : Array[ArchiveRetentionRule],
  class : ArchiveRetentionClass,
) -> ArchiveRetentionRule? {
  let mut selected : ArchiveRetentionRule? = None
  for rule in rules {
    if rule.class == class && rule.is_valid() {
      selected = Some(rule)
    }
  }
  selected
}

///|
pub fn archive_is_expired(
  record : ArchiveRecord,
  rule : ArchiveRetentionRule,
  as_of : String,
) -> Bool {
  record.is_valid() &&
  rule.is_valid() &&
  !record.legal_hold &&
  as_of > record.created_at &&
  record.created_at != as_of
}

///|
pub fn archive_candidate(
  record : ArchiveRecord,
  rule : ArchiveRetentionRule,
  action : ArchiveRetentionAction,
) -> ArchivePurgeCandidate {
  {
    record,
    rule,
    action,
    reason: archive_retention_class_name(record.class) +
    " exceeded policy window",
    authorization_checksum: stable_hash(
      record.record_id +
      ":" +
      rule.rule_id +
      ":" +
      archive_retention_action_name(action),
    ),
  }
}

///|
pub fn build_archive_plan(
  plan_id : String,
  as_of : String,
  records : Array[ArchiveRecord],
  rules : Array[ArchiveRetentionRule],
) -> ArchiveRetentionPlan {
  let candidates : Array[ArchivePurgeCandidate] = []
  let held : Array[String] = []
  let unknown : Array[String] = []
  for record in records {
    match archive_rule_for(rules, record.class) {
      None => unknown.push(record.record_id)
      Some(rule) =>
        if record.legal_hold {
          held.push(record.record_id)
        } else if archive_is_expired(record, rule, as_of) {
          candidates.push(archive_candidate(record, rule, rule.action))
        }
    }
  }
  {
    plan_id,
    as_of,
    candidates,
    held_records: held,
    unknown_records: unknown,
    checksum: stable_hash(
      candidates.map(fn(item) { item.authorization_checksum }).join("\n"),
    ),
  }
}

///|
pub fn ArchiveRetentionPlan::purge_candidates(
  self : ArchiveRetentionPlan,
) -> Array[ArchivePurgeCandidate] {
  self.candidates.filter(fn(item) { item.action == ArchivePurge })
}

///|
pub fn ArchiveRetentionPlan::archive_candidates(
  self : ArchiveRetentionPlan,
) -> Array[ArchivePurgeCandidate] {
  self.candidates.filter(fn(item) { item.action == ArchiveArchive })
}

///|
pub fn ArchiveRetentionPlan::review_candidates(
  self : ArchiveRetentionPlan,
) -> Array[ArchivePurgeCandidate] {
  self.candidates.filter(fn(item) { item.action == ArchiveReview })
}

///|
pub fn ArchiveRetentionPlan::is_safe(self : ArchiveRetentionPlan) -> Bool {
  self.candidates.all(fn(item) {
    !item.record.legal_hold &&
    item.record.checksum.length() > 0 &&
    item.authorization_checksum.length() > 0
  })
}

///|
pub fn ArchiveRetentionPlan::summary(self : ArchiveRetentionPlan) -> String {
  [
    "plan_id=" + self.plan_id,
    "as_of=" + self.as_of,
    "candidates=" + self.candidates.length().to_string(),
    "purge=" + self.purge_candidates().length().to_string(),
    "archive=" + self.archive_candidates().length().to_string(),
    "review=" + self.review_candidates().length().to_string(),
    "held=" + self.held_records.length().to_string(),
    "unknown=" + self.unknown_records.length().to_string(),
    "safe=" + self.is_safe().to_string(),
    "checksum=" + self.checksum,
  ].join("\n")
}

///|
pub fn ArchiveRetentionPlan::to_json(self : ArchiveRetentionPlan) -> String {
  "{" +
  "\"plan_id\":\"" +
  json_escape(self.plan_id) +
  "\"," +
  "\"as_of\":\"" +
  json_escape(self.as_of) +
  "\"," +
  "\"candidates\":[" +
  self.candidates
  .map(fn(item) {
    "{\"record_id\":\"" +
    json_escape(item.record.record_id) +
    "\",\"action\":\"" +
    archive_retention_action_name(item.action) +
    "\",\"authorization_checksum\":\"" +
    json_escape(item.authorization_checksum) +
    "\"}"
  })
  .join(",") +
  "]," +
  "\"held\":" +
  self.held_records.length().to_string() +
  "," +
  "\"unknown\":" +
  self.unknown_records.length().to_string() +
  "," +
  "\"checksum\":\"" +
  json_escape(self.checksum) +
  "\"}"
}

///|
pub fn archive_plan_is_reproducible(
  left : ArchiveRetentionPlan,
  right : ArchiveRetentionPlan,
) -> Bool {
  left.plan_id == right.plan_id &&
  left.as_of == right.as_of &&
  left.checksum == right.checksum
}

///|
pub fn archive_plan_requires_authorization(plan : ArchiveRetentionPlan) -> Bool {
  !plan.purge_candidates().is_empty() || !plan.archive_candidates().is_empty()
}

///|
pub fn archive_plan_has_unknowns(plan : ArchiveRetentionPlan) -> Bool {
  !plan.unknown_records.is_empty()
}

///|
pub fn archive_plan_record_ids(plan : ArchiveRetentionPlan) -> Array[String] {
  plan.candidates.map(fn(item) { item.record.record_id })
}

///|
pub fn archive_plan_authorization_checksum(
  plan : ArchiveRetentionPlan,
) -> String {
  stable_hash(
    plan.candidates.map(fn(item) { item.authorization_checksum }).join("\n"),
  )
}

///|
pub fn archive_records_by_owner(
  records : Array[ArchiveRecord],
  owner : String,
) -> Array[ArchiveRecord] {
  records.filter(fn(record) { record.owner == owner })
}

///|
pub fn archive_records_with_hold(
  records : Array[ArchiveRecord],
) -> Array[ArchiveRecord] {
  records.filter(fn(record) { record.legal_hold })
}

///|
pub fn archive_retention_class_counts(
  records : Array[ArchiveRecord],
) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for record in records {
    let key = archive_retention_class_name(record.class)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn archive_plan_is_monotonic(
  before : ArchiveRetentionPlan,
  after : ArchiveRetentionPlan,
) -> Bool {
  after.candidates.length() >= before.candidates.length() &&
  before.held_records.all(fn(record_id) {
    after.held_records.contains(record_id)
  })
}