///|
/// Purpose-drift checks for retained data capsules.
///
/// A capsule can be retained correctly but still be misused later for a purpose,
/// actor, region, or sharing mode outside the reason it was collected. This
/// module checks structured use events against explicit purpose rules.
pub(all) enum UseAction {
  AllowUse
  RequireReconsent
  BlockUse
  EscalateUse
} derive(Eq, Debug)

///|
pub(all) enum UseIssueKind {
  UnknownCapsule
  MissingPurposeRule
  PurposeMismatch
  ActorDenied
  UseRegionDenied
  ConsentForUseInvalid
  UseSharingDenied
  SensitivePublicUse
} derive(Eq, Debug)

///|
pub(all) struct PurposeRule {
  id : String
  source_purpose : String
  allowed_use_purposes : Array[String]
  allowed_actors : Array[String]
  allowed_regions : Array[String]
  allow_processor : Bool
  allow_public : Bool
  requires_fresh_consent : Bool
  owner : String
  note : String
} derive(Eq, Debug)

///|
pub(all) struct CapsuleUseEvent {
  id : String
  capsule_id : String
  day : Int
  use_purpose : String
  actor : String
  region : String
  sharing : SharingMode
  note : String
} derive(Eq, Debug)

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

///|
pub(all) struct UseDecision {
  event : CapsuleUseEvent
  capsule_id : String
  rule_id : String
  action : UseAction
  severity : RetentionSeverity
  issues : Array[UseIssue]
  owner : String
} derive(Eq, Debug)

///|
pub(all) struct PurposeDriftReport {
  generated_day : Int
  decisions : Array[UseDecision]
  total_events : Int
  allowed_count : Int
  reconsent_count : Int
  blocked_count : Int
  escalated_count : Int
} derive(Eq, Debug)

///|
pub fn UseAction::label(self : UseAction) -> String {
  match self {
    AllowUse => "allow"
    RequireReconsent => "require-reconsent"
    BlockUse => "block"
    EscalateUse => "escalate"
  }
}

///|
pub fn UseAction::priority(self : UseAction) -> Int {
  match self {
    AllowUse => 0
    RequireReconsent => 20
    EscalateUse => 35
    BlockUse => 50
  }
}

///|
pub fn UseIssueKind::label(self : UseIssueKind) -> String {
  match self {
    UnknownCapsule => "unknown-capsule"
    MissingPurposeRule => "missing-purpose-rule"
    PurposeMismatch => "purpose-mismatch"
    ActorDenied => "actor-denied"
    UseRegionDenied => "use-region-denied"
    ConsentForUseInvalid => "consent-for-use-invalid"
    UseSharingDenied => "use-sharing-denied"
    SensitivePublicUse => "sensitive-public-use"
  }
}

///|
pub fn purpose_rule(
  id : String,
  source_purpose : String,
  allowed_use_purposes : Array[String],
  allowed_actors : Array[String],
  allowed_regions : Array[String],
  allow_processor : Bool,
  allow_public : Bool,
  requires_fresh_consent : Bool,
  owner : String,
  note : String,
) -> PurposeRule {
  {
    id,
    source_purpose,
    allowed_use_purposes,
    allowed_actors,
    allowed_regions,
    allow_processor,
    allow_public,
    requires_fresh_consent,
    owner,
    note,
  }
}

///|
pub fn capsule_use_event(
  id : String,
  capsule_id : String,
  day : Int,
  use_purpose : String,
  actor : String,
  region : String,
  sharing : SharingMode,
  note : String,
) -> CapsuleUseEvent {
  { id, capsule_id, day, use_purpose, actor, region, sharing, note }
}

///|
pub fn evaluate_use_event(
  event : CapsuleUseEvent,
  capsules : Array[DataCapsule],
  rules : Array[PurposeRule],
) -> UseDecision {
  match find_capsule_by_id(capsules, event.capsule_id) {
    None =>
      build_use_decision(event, event.capsule_id, "", "privacy-team", [
        {
          kind: UnknownCapsule,
          severity: RetentionHigh,
          message: "use event references an unknown capsule",
          remedy: "drop the event or add the capsule inventory before processing",
        },
      ])
    Some(capsule) =>
      match find_purpose_rule(capsule, rules) {
        None =>
          build_use_decision(event, capsule.id, "", "privacy-team", [
            {
              kind: MissingPurposeRule,
              severity: RetentionHigh,
              message: "no purpose rule exists for source purpose '" +
              capsule.purpose +
              "'",
              remedy: "add a purpose rule before allowing secondary use",
            },
          ])
        Some(rule) => {
          let issues : Array[UseIssue] = []
          if !purpose_allowed(rule, event.use_purpose) {
            issues.push({
              kind: PurposeMismatch,
              severity: if rule.requires_fresh_consent {
                RetentionHigh
              } else {
                RetentionMedium
              },
              message: "use purpose '" +
              event.use_purpose +
              "' is not allowed for source purpose '" +
              capsule.purpose +
              "'",
              remedy: "collect fresh consent or create a narrower derived capsule",
            })
          }
          if !actor_allowed(rule, event.actor) {
            issues.push({
              kind: ActorDenied,
              severity: RetentionHigh,
              message: "actor '" +
              event.actor +
              "' is not allowed by purpose rule",
              remedy: "route the request to an approved actor or add an explicit rule",
            })
          }
          if !use_region_allowed(rule, event.region) {
            issues.push({
              kind: UseRegionDenied,
              severity: RetentionHigh,
              message: "use region '" + event.region + "' is not allowed",
              remedy: "run the use in an approved region or quarantine the result",
            })
          }
          if rule.requires_fresh_consent && !capsule.consent.allows_processing() {
            issues.push({
              kind: ConsentForUseInvalid,
              severity: RetentionCritical,
              message: "capsule consent does not allow this use",
              remedy: "block the use and remove downstream artifacts",
            })
          }
          if event.sharing == ProcessorShared && !rule.allow_processor {
            issues.push({
              kind: UseSharingDenied,
              severity: RetentionHigh,
              message: "processor sharing is denied for this purpose rule",
              remedy: "keep processing internal or obtain a processor-approved rule",
            })
          }
          if event.sharing == PublicShared && !rule.allow_public {
            issues.push({
              kind: UseSharingDenied,
              severity: RetentionHigh,
              message: "public sharing is denied for this purpose rule",
              remedy: "do not publish this derived result",
            })
          }
          if capsule.sensitivity.requires_encryption() &&
            event.sharing == PublicShared {
            issues.push({
              kind: SensitivePublicUse,
              severity: RetentionCritical,
              message: "sensitive capsule is used in a public sharing path",
              remedy: "block public output and create an aggregated anonymous result",
            })
          }
          build_use_decision(event, capsule.id, rule.id, rule.owner, issues)
        }
      }
  }
}

///|
pub fn analyze_purpose_drift(
  events : Array[CapsuleUseEvent],
  capsules : Array[DataCapsule],
  rules : Array[PurposeRule],
  today : Int,
) -> PurposeDriftReport {
  let decisions : Array[UseDecision] = []
  for event in events {
    decisions.push(evaluate_use_event(event, capsules, rules))
  }
  let decisions = sort_use_decisions(decisions)
  {
    generated_day: today,
    total_events: events.length(),
    allowed_count: decisions.count_if(decision => decision.action == AllowUse),
    reconsent_count: decisions.count_if(decision => {
      decision.action == RequireReconsent
    }),
    blocked_count: decisions.count_if(decision => decision.action == BlockUse),
    escalated_count: decisions.count_if(decision => {
      decision.action == EscalateUse
    }),
    decisions,
  }
}

///|
pub fn sample_purpose_rules() -> Array[PurposeRule] {
  [
    purpose_rule(
      "purpose.analytics",
      "analytics",
      ["analytics", "product-quality"],
      ["product", "privacy-analytics"],
      ["cn", "eu"],
      false,
      false,
      true,
      "privacy-analytics",
      "analytics capsules may be reused for product quality only",
    ),
    purpose_rule(
      "purpose.support",
      "support",
      ["support", "quality-review"],
      ["support", "support-ops"],
      ["cn", "sg"],
      true,
      false,
      false,
      "support-ops",
      "support tickets may be reviewed by approved processors",
    ),
    purpose_rule(
      "purpose.billing",
      "billing",
      ["billing", "tax"],
      ["finance"],
      ["cn"],
      false,
      false,
      false,
      "finance",
      "billing capsules stay local and finance-owned",
    ),
  ]
}

///|
pub fn sample_use_events() -> Array[CapsuleUseEvent] {
  [
    capsule_use_event(
      "use.allowed",
      "cap.active-analytics",
      221,
      "product-quality",
      "product",
      "cn",
      InternalOnly,
      "allowed quality improvement use",
    ),
    capsule_use_event(
      "use.ads-drift",
      "cap.active-analytics",
      222,
      "ads",
      "growth",
      "us",
      ProcessorShared,
      "purpose drift into advertising",
    ),
    capsule_use_event(
      "use.public-ticket",
      "cap.unencrypted-ticket",
      223,
      "quality-review",
      "support",
      "sg",
      PublicShared,
      "public sharing should be blocked",
    ),
    capsule_use_event(
      "use.unknown",
      "cap.missing",
      224,
      "analytics",
      "product",
      "cn",
      InternalOnly,
      "unknown capsule id",
    ),
  ]
}

///|
pub fn sample_purpose_drift_report() -> PurposeDriftReport {
  analyze_purpose_drift(
    sample_use_events(),
    sample_capsules(),
    sample_purpose_rules(),
    225,
  )
}

///|
pub fn PurposeDriftReport::is_ready(self : PurposeDriftReport) -> Bool {
  self.blocked_count == 0 && self.escalated_count == 0
}

///|
pub fn PurposeDriftReport::decisions_for_action(
  self : PurposeDriftReport,
  action : UseAction,
) -> Array[UseDecision] {
  self.decisions.filter(decision => decision.action == action)
}

///|
pub fn PurposeDriftReport::decision(
  self : PurposeDriftReport,
  event_id : StringView,
) -> UseDecision? {
  let expected = event_id.trim().to_owned()
  self.decisions.iter().find_first(decision => decision.event.id == expected)
}

///|
pub fn PurposeDriftReport::to_markdown(self : PurposeDriftReport) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Purpose Drift Report")
  lines.push("")
  lines.push("- Generated day: " + self.generated_day.to_string())
  lines.push("- Events: " + self.total_events.to_string())
  lines.push("- Allowed: " + self.allowed_count.to_string())
  lines.push("- Require reconsent: " + self.reconsent_count.to_string())
  lines.push("- Escalated: " + self.escalated_count.to_string())
  lines.push("- Blocked: " + self.blocked_count.to_string())
  lines.push("- Ready: " + bool_label(self.is_ready()))
  lines.push("")
  lines.push(
    "| Event | Capsule | Use Purpose | Actor | Region | Action | Severity | Issues |",
  )
  lines.push("| --- | --- | --- | --- | --- | --- | --- | --- |")
  if self.decisions.is_empty() {
    lines.push("| - | - | - | - | - | allow | info | no events |")
  } else {
    for decision in self.decisions {
      lines.push(
        "| " +
        markdown_cell(decision.event.id) +
        " | " +
        markdown_cell(decision.capsule_id) +
        " | " +
        markdown_cell(decision.event.use_purpose) +
        " | " +
        markdown_cell(decision.event.actor) +
        " | " +
        markdown_cell(decision.event.region) +
        " | " +
        decision.action.label() +
        " | " +
        decision.severity.label() +
        " | " +
        markdown_cell(decision.issue_summary()) +
        " |",
      )
    }
  }
  lines.join("\n")
}

///|
pub fn PurposeDriftReport::to_json_string(self : PurposeDriftReport) -> String {
  Json::object(
    Map([
      ("generated_day", json_int(self.generated_day)),
      ("ready", Json::boolean(self.is_ready())),
      ("total_events", json_int(self.total_events)),
      ("allowed_count", json_int(self.allowed_count)),
      ("reconsent_count", json_int(self.reconsent_count)),
      ("blocked_count", json_int(self.blocked_count)),
      ("escalated_count", json_int(self.escalated_count)),
      ("decisions", use_decisions_to_json(self.decisions)),
    ]),
  ).stringify(indent=2)
}

///|
pub fn UseDecision::issue_summary(self : UseDecision) -> String {
  if self.issues.is_empty() {
    "within purpose rule"
  } else {
    let labels : Array[String] = []
    for issue in self.issues {
      labels.push(issue.kind.label())
    }
    labels.join(", ")
  }
}

///|
fn build_use_decision(
  event : CapsuleUseEvent,
  capsule_id : String,
  rule_id : String,
  owner : String,
  issues : Array[UseIssue],
) -> UseDecision {
  {
    event,
    capsule_id,
    rule_id,
    action: action_for_use_issues(issues),
    severity: severity_for_use_issues(issues),
    issues,
    owner,
  }
}

///|
fn action_for_use_issues(issues : Array[UseIssue]) -> UseAction {
  let mut action = AllowUse
  for issue in issues {
    let next = action_for_use_issue(issue)
    if next.priority() > action.priority() {
      action = next
    }
  }
  action
}

///|
fn action_for_use_issue(issue : UseIssue) -> UseAction {
  match issue.kind {
    UnknownCapsule => EscalateUse
    MissingPurposeRule => EscalateUse
    PurposeMismatch =>
      if issue.severity == RetentionHigh || issue.severity == RetentionCritical {
        RequireReconsent
      } else {
        EscalateUse
      }
    ActorDenied => EscalateUse
    UseRegionDenied => EscalateUse
    ConsentForUseInvalid => BlockUse
    UseSharingDenied => EscalateUse
    SensitivePublicUse => BlockUse
  }
}

///|
fn severity_for_use_issues(issues : Array[UseIssue]) -> RetentionSeverity {
  let mut severity = RetentionInfo
  for issue in issues {
    if issue.severity.weight() > severity.weight() {
      severity = issue.severity
    }
  }
  severity
}

///|
fn find_capsule_by_id(
  capsules : Array[DataCapsule],
  id : String,
) -> DataCapsule? {
  capsules.iter().find_first(capsule => capsule.id == id)
}

///|
fn find_purpose_rule(
  capsule : DataCapsule,
  rules : Array[PurposeRule],
) -> PurposeRule? {
  let purpose = normalize_token(capsule.purpose)
  rules
  .iter()
  .find_first(rule => normalize_token(rule.source_purpose) == purpose)
}

///|
fn purpose_allowed(rule : PurposeRule, purpose : String) -> Bool {
  list_contains_normalized(rule.allowed_use_purposes, purpose)
}

///|
fn actor_allowed(rule : PurposeRule, actor : String) -> Bool {
  list_contains_normalized(rule.allowed_actors, actor)
}

///|
fn use_region_allowed(rule : PurposeRule, region : String) -> Bool {
  list_contains_normalized(rule.allowed_regions, region)
}

///|
fn list_contains_normalized(items : Array[String], value : String) -> Bool {
  let expected = normalize_token(value)
  items.any(item => normalize_token(item) == expected)
}

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

///|
fn compare_use_decision(a : UseDecision, b : UseDecision) -> Int {
  let action_delta = b.action.priority() - a.action.priority()
  if action_delta != 0 {
    action_delta
  } else {
    a.event.id.lexical_compare(b.event.id)
  }
}

///|
fn use_decisions_to_json(items : Array[UseDecision]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("event_id", Json::string(item.event.id)),
          ("capsule_id", Json::string(item.capsule_id)),
          ("rule_id", Json::string(item.rule_id)),
          ("use_purpose", Json::string(item.event.use_purpose)),
          ("actor", Json::string(item.event.actor)),
          ("region", Json::string(item.event.region)),
          ("sharing", Json::string(item.event.sharing.label())),
          ("action", Json::string(item.action.label())),
          ("severity", Json::string(item.severity.label())),
          ("owner", Json::string(item.owner)),
          ("issues", use_issues_to_json(item.issues)),
        ]),
      ),
    )
  }
  Json::array(values)
}

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