///|
/// Compare two retention plans by capsule id.
///
/// This is useful when an application changes retention rules or imports a new
/// inventory snapshot and wants to know whether operator work increased,
/// decreased, or stayed stable.
pub(all) enum PlanShiftKind {
  CapsuleAdded
  CapsuleRemoved
  ActionChanged
  SeverityChanged
  DecisionStable
} derive(Eq, Debug)

///|
pub(all) enum PlanShiftImpact {
  RiskIncreased
  RiskReduced
  ReviewNeeded
  NoImpact
} derive(Eq, Debug)

///|
pub(all) struct PlanShift {
  capsule_id : String
  kind : PlanShiftKind
  impact : PlanShiftImpact
  before_action : String
  after_action : String
  before_severity : String
  after_severity : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct PlanDelta {
  before_day : Int
  after_day : Int
  shifts : Array[PlanShift]
  increased_count : Int
  reduced_count : Int
  review_count : Int
  stable_count : Int
} derive(Eq, Debug)

///|
pub fn PlanShiftKind::label(self : PlanShiftKind) -> String {
  match self {
    CapsuleAdded => "capsule-added"
    CapsuleRemoved => "capsule-removed"
    ActionChanged => "action-changed"
    SeverityChanged => "severity-changed"
    DecisionStable => "stable"
  }
}

///|
pub fn PlanShiftImpact::label(self : PlanShiftImpact) -> String {
  match self {
    RiskIncreased => "risk-increased"
    RiskReduced => "risk-reduced"
    ReviewNeeded => "review-needed"
    NoImpact => "no-impact"
  }
}

///|
pub fn compare_retention_plans(
  before : RetentionPlan,
  after : RetentionPlan,
) -> PlanDelta {
  let shifts : Array[PlanShift] = []
  for old in before.decisions {
    match after.decision(old.capsule.id) {
      Some(current) => shifts.push(compare_decision_pair(old, current))
      None =>
        shifts.push({
          capsule_id: old.capsule.id,
          kind: CapsuleRemoved,
          impact: if old.needs_operator_action() {
            RiskReduced
          } else {
            ReviewNeeded
          },
          before_action: old.action.label(),
          after_action: "-",
          before_severity: old.severity.label(),
          after_severity: "-",
          message: "capsule disappeared from the after plan",
        })
    }
  }
  for current in after.decisions {
    if before.decision(current.capsule.id) is None {
      shifts.push({
        capsule_id: current.capsule.id,
        kind: CapsuleAdded,
        impact: if current.needs_operator_action() {
          RiskIncreased
        } else {
          ReviewNeeded
        },
        before_action: "-",
        after_action: current.action.label(),
        before_severity: "-",
        after_severity: current.severity.label(),
        message: "capsule is new in the after plan",
      })
    }
  }
  let shifts = sort_plan_shifts(shifts)
  {
    before_day: before.generated_day,
    after_day: after.generated_day,
    increased_count: shifts.count_if(shift => shift.impact == RiskIncreased),
    reduced_count: shifts.count_if(shift => shift.impact == RiskReduced),
    review_count: shifts.count_if(shift => shift.impact == ReviewNeeded),
    stable_count: shifts.count_if(shift => shift.impact == NoImpact),
    shifts,
  }
}

///|
pub fn PlanDelta::is_empty(self : PlanDelta) -> Bool {
  self.shifts.all(shift => shift.kind == DecisionStable)
}

///|
pub fn PlanDelta::has_risk_increase(self : PlanDelta) -> Bool {
  self.increased_count > 0
}

///|
pub fn PlanDelta::changed_shifts(self : PlanDelta) -> Array[PlanShift] {
  self.shifts.filter(shift => shift.kind != DecisionStable)
}

///|
pub fn PlanDelta::to_markdown(self : PlanDelta) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Plan Delta")
  lines.push("")
  lines.push("- Before day: " + self.before_day.to_string())
  lines.push("- After day: " + self.after_day.to_string())
  lines.push("- Risk increased: " + self.increased_count.to_string())
  lines.push("- Risk reduced: " + self.reduced_count.to_string())
  lines.push("- Review needed: " + self.review_count.to_string())
  lines.push("- Stable: " + self.stable_count.to_string())
  lines.push("")
  lines.push("| Capsule | Change | Impact | Before | After | Message |")
  lines.push("| --- | --- | --- | --- | --- | --- |")
  if self.shifts.is_empty() {
    lines.push("| - | stable | no-impact | - | - | no decisions |")
  } else {
    for shift in self.shifts {
      lines.push(
        "| " +
        markdown_cell(shift.capsule_id) +
        " | " +
        shift.kind.label() +
        " | " +
        shift.impact.label() +
        " | " +
        markdown_cell(shift.before_action + "/" + shift.before_severity) +
        " | " +
        markdown_cell(shift.after_action + "/" + shift.after_severity) +
        " | " +
        markdown_cell(shift.message) +
        " |",
      )
    }
  }
  lines.join("\n")
}

///|
pub fn PlanDelta::to_json_string(self : PlanDelta) -> String {
  Json::object(
    Map([
      ("before_day", json_int(self.before_day)),
      ("after_day", json_int(self.after_day)),
      ("risk_increased", Json::boolean(self.has_risk_increase())),
      ("increased_count", json_int(self.increased_count)),
      ("reduced_count", json_int(self.reduced_count)),
      ("review_count", json_int(self.review_count)),
      ("stable_count", json_int(self.stable_count)),
      ("shifts", plan_shifts_to_json(self.shifts)),
    ]),
  ).stringify(indent=2)
}

///|
pub fn sample_plan_delta() -> PlanDelta {
  let before = plan_retention(sample_capsules(), sample_rules(), 220)
  let after_capsules = sample_capsules()
  after_capsules.push(
    data_capsule(
      "cap.new-public-export",
      "visitor",
      "analytics",
      "behavior",
      219,
      219,
      ConsentGranted,
      RestrictedData,
      "cn",
      true,
      PublicShared,
      "new export that should be quarantined",
    ),
  )
  let after = plan_retention(after_capsules, sample_rules(), 230)
  compare_retention_plans(before, after)
}

///|
fn compare_decision_pair(
  before : CapsuleDecision,
  after : CapsuleDecision,
) -> PlanShift {
  if before.action != after.action {
    {
      capsule_id: before.capsule.id,
      kind: ActionChanged,
      impact: impact_for_actions(before, after),
      before_action: before.action.label(),
      after_action: after.action.label(),
      before_severity: before.severity.label(),
      after_severity: after.severity.label(),
      message: "action changed from " +
      before.action.label() +
      " to " +
      after.action.label(),
    }
  } else if before.severity != after.severity {
    {
      capsule_id: before.capsule.id,
      kind: SeverityChanged,
      impact: impact_for_severity(before.severity, after.severity),
      before_action: before.action.label(),
      after_action: after.action.label(),
      before_severity: before.severity.label(),
      after_severity: after.severity.label(),
      message: "severity changed from " +
      before.severity.label() +
      " to " +
      after.severity.label(),
    }
  } else {
    {
      capsule_id: before.capsule.id,
      kind: DecisionStable,
      impact: NoImpact,
      before_action: before.action.label(),
      after_action: after.action.label(),
      before_severity: before.severity.label(),
      after_severity: after.severity.label(),
      message: "decision stayed stable",
    }
  }
}

///|
fn impact_for_actions(
  before : CapsuleDecision,
  after : CapsuleDecision,
) -> PlanShiftImpact {
  if after.action.priority() > before.action.priority() ||
    after.severity.weight() > before.severity.weight() {
    RiskIncreased
  } else if after.action.priority() < before.action.priority() ||
    after.severity.weight() < before.severity.weight() {
    RiskReduced
  } else {
    ReviewNeeded
  }
}

///|
fn impact_for_severity(
  before : RetentionSeverity,
  after : RetentionSeverity,
) -> PlanShiftImpact {
  if after.weight() > before.weight() {
    RiskIncreased
  } else if after.weight() < before.weight() {
    RiskReduced
  } else {
    NoImpact
  }
}

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

///|
fn compare_plan_shift(a : PlanShift, b : PlanShift) -> Int {
  let impact_delta = shift_impact_weight(b.impact) -
    shift_impact_weight(a.impact)
  if impact_delta != 0 {
    impact_delta
  } else {
    a.capsule_id.lexical_compare(b.capsule_id)
  }
}

///|
fn shift_impact_weight(impact : PlanShiftImpact) -> Int {
  match impact {
    RiskIncreased => 4
    ReviewNeeded => 3
    RiskReduced => 2
    NoImpact => 1
  }
}

///|
fn plan_shifts_to_json(items : Array[PlanShift]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("capsule_id", Json::string(item.capsule_id)),
          ("kind", Json::string(item.kind.label())),
          ("impact", Json::string(item.impact.label())),
          ("before_action", Json::string(item.before_action)),
          ("after_action", Json::string(item.after_action)),
          ("before_severity", Json::string(item.before_severity)),
          ("after_severity", Json::string(item.after_severity)),
          ("message", Json::string(item.message)),
        ]),
      ),
    )
  }
  Json::array(values)
}