///|
/// Convert retention decisions into owner-oriented operator tasks.
pub(all) enum OperatorTaskStatus {
  TaskTodo
  TaskBlocked
  TaskDone
} derive(Eq, Debug)

///|
pub(all) struct OperatorTask {
  id : String
  owner : String
  action : RetentionAction
  severity : RetentionSeverity
  due_day : Int
  capsule_ids : Array[String]
  reason_kinds : Array[String]
  checklist : Array[String]
  status : OperatorTaskStatus
} derive(Eq, Debug)

///|
pub(all) struct OperatorPlaybook {
  generated_day : Int
  tasks : Array[OperatorTask]
} derive(Eq, Debug)

///|
pub fn OperatorTaskStatus::label(self : OperatorTaskStatus) -> String {
  match self {
    TaskTodo => "todo"
    TaskBlocked => "blocked"
    TaskDone => "done"
  }
}

///|
pub fn build_operator_playbook(plan : RetentionPlan) -> OperatorPlaybook {
  let tasks : Array[OperatorTask] = []
  for decision in plan.decisions {
    if decision.needs_operator_action() {
      let due_day = operator_due_day(plan.generated_day, decision)
      match
        find_operator_task(
          tasks,
          decision.owner,
          decision.action,
          decision.severity,
          due_day,
        ) {
        Some(index) => merge_decision_into_task(tasks[index], decision)
        None => tasks.push(task_from_decision(plan.generated_day, decision))
      }
    }
  }
  { generated_day: plan.generated_day, tasks: sort_operator_tasks(tasks) }
}

///|
pub fn sample_playbook() -> OperatorPlaybook {
  build_operator_playbook(sample_plan())
}

///|
pub fn OperatorPlaybook::is_empty(self : OperatorPlaybook) -> Bool {
  self.tasks.is_empty()
}

///|
pub fn OperatorPlaybook::tasks_for_owner(
  self : OperatorPlaybook,
  owner : StringView,
) -> Array[OperatorTask] {
  let expected = owner.trim().to_owned()
  self.tasks.filter(task => task.owner == expected)
}

///|
pub fn OperatorPlaybook::urgent_tasks(
  self : OperatorPlaybook,
) -> Array[OperatorTask] {
  self.tasks.filter(task => {
    task.severity == RetentionCritical || task.severity == RetentionHigh
  })
}

///|
pub fn OperatorPlaybook::count_by_action(
  self : OperatorPlaybook,
  action : RetentionAction,
) -> Int {
  self.tasks.count_if(task => task.action == action)
}

///|
pub fn OperatorPlaybook::mark_done(
  self : OperatorPlaybook,
  task_id : StringView,
) -> OperatorPlaybook {
  change_task_status(self, task_id, TaskDone)
}

///|
pub fn OperatorPlaybook::mark_blocked(
  self : OperatorPlaybook,
  task_id : StringView,
) -> OperatorPlaybook {
  change_task_status(self, task_id, TaskBlocked)
}

///|
pub fn OperatorPlaybook::to_markdown(self : OperatorPlaybook) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Operator Playbook")
  lines.push("")
  lines.push("- Generated day: " + self.generated_day.to_string())
  lines.push("- Tasks: " + self.tasks.length().to_string())
  lines.push("- Urgent: " + self.urgent_tasks().length().to_string())
  lines.push("")
  lines.push("| Task | Owner | Action | Severity | Due | Capsules | Status |")
  lines.push("| --- | --- | --- | --- | --- | --- | --- |")
  if self.tasks.is_empty() {
    lines.push("| - | - | keep | info | - | - | done |")
  } else {
    for task in self.tasks {
      lines.push(
        "| " +
        markdown_cell(task.id) +
        " | " +
        markdown_cell(task.owner) +
        " | " +
        task.action.label() +
        " | " +
        task.severity.label() +
        " | " +
        task.due_day.to_string() +
        " | " +
        markdown_cell(sort_strings(task.capsule_ids).join(", ")) +
        " | " +
        task.status.label() +
        " |",
      )
    }
  }
  lines.push("")
  lines.push("## Checklists")
  lines.push("")
  for task in self.tasks {
    lines.push("### " + task.id)
    for item in task.checklist {
      lines.push("- [ ] " + item)
    }
    lines.push("")
  }
  lines.join("\n")
}

///|
pub fn OperatorPlaybook::to_json_string(self : OperatorPlaybook) -> String {
  Json::object(
    Map([
      ("generated_day", json_int(self.generated_day)),
      ("task_count", json_int(self.tasks.length())),
      ("urgent_count", json_int(self.urgent_tasks().length())),
      ("tasks", operator_tasks_to_json(self.tasks)),
    ]),
  ).stringify(indent=2)
}

///|
fn change_task_status(
  playbook : OperatorPlaybook,
  task_id : StringView,
  status : OperatorTaskStatus,
) -> OperatorPlaybook {
  let expected = task_id.trim().to_owned()
  let tasks : Array[OperatorTask] = []
  for task in playbook.tasks {
    if task.id == expected {
      tasks.push({
        id: task.id,
        owner: task.owner,
        action: task.action,
        severity: task.severity,
        due_day: task.due_day,
        capsule_ids: task.capsule_ids,
        reason_kinds: task.reason_kinds,
        checklist: task.checklist,
        status,
      })
    } else {
      tasks.push(task)
    }
  }
  { generated_day: playbook.generated_day, tasks }
}

///|
fn task_from_decision(today : Int, decision : CapsuleDecision) -> OperatorTask {
  let due_day = operator_due_day(today, decision)
  {
    id: task_id(decision.owner, decision.action, decision.severity, due_day),
    owner: decision.owner,
    action: decision.action,
    severity: decision.severity,
    due_day,
    capsule_ids: [decision.capsule.id],
    reason_kinds: reason_kind_labels(decision.reasons),
    checklist: checklist_for(decision.action),
    status: TaskTodo,
  }
}

///|
fn merge_decision_into_task(
  task : OperatorTask,
  decision : CapsuleDecision,
) -> Unit {
  if !task.capsule_ids.contains(decision.capsule.id) {
    task.capsule_ids.push(decision.capsule.id)
  }
  for reason in reason_kind_labels(decision.reasons) {
    if !task.reason_kinds.contains(reason) {
      task.reason_kinds.push(reason)
    }
  }
}

///|
fn find_operator_task(
  tasks : Array[OperatorTask],
  owner : String,
  action : RetentionAction,
  severity : RetentionSeverity,
  due_day : Int,
) -> Int? {
  for index, task in tasks.iter2() {
    if task.owner == owner &&
      task.action == action &&
      task.severity == severity &&
      task.due_day == due_day {
      return Some(index)
    }
  }
  None
}

///|
fn operator_due_day(today : Int, decision : CapsuleDecision) -> Int {
  match decision.action {
    DeleteData => today
    QuarantineData => today
    BlockCollection => today
    AnonymizeData => today
    KeepData => today + decision.days_until_delete
  }
}

///|
fn task_id(
  owner : String,
  action : RetentionAction,
  severity : RetentionSeverity,
  due_day : Int,
) -> String {
  owner +
  "." +
  action.label() +
  "." +
  severity.label() +
  "." +
  due_day.to_string()
}

///|
fn reason_kind_labels(reasons : Array[RetentionReason]) -> Array[String] {
  let labels : Array[String] = []
  for reason in reasons {
    let label = reason.kind.label()
    if !labels.contains(label) {
      labels.push(label)
    }
  }
  sort_strings(labels)
}

///|
fn checklist_for(action : RetentionAction) -> Array[String] {
  match action {
    KeepData => ["No operator work is required."]
    AnonymizeData =>
      [
        "Run the anonymization pipeline for listed capsules.", "Verify direct identifiers are removed or aggregated.",
        "Record the anonymization batch id in the application log.",
      ]
    DeleteData =>
      [
        "Delete listed capsules from active storage.", "Remove scheduled exports and derived caches.",
        "Record deletion confirmation with the responsible owner.",
      ]
    QuarantineData =>
      [
        "Move listed capsules out of normal processing.", "Fix rule, region, encryption, or sharing metadata.",
        "Re-run CapsuleTrace before returning data to active use.",
      ]
    BlockCollection =>
      [
        "Stop new collection for listed purpose/data-kind pairs.", "Delete any already-collected records if policy requires it.",
        "Update product or consent flow before enabling collection again.",
      ]
  }
}

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

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

///|
fn operator_tasks_to_json(items : Array[OperatorTask]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("id", Json::string(item.id)),
          ("owner", Json::string(item.owner)),
          ("action", Json::string(item.action.label())),
          ("severity", Json::string(item.severity.label())),
          ("due_day", json_int(item.due_day)),
          ("capsule_ids", string_array_to_json(item.capsule_ids)),
          ("reason_kinds", string_array_to_json(item.reason_kinds)),
          ("checklist", string_array_to_json(item.checklist)),
          ("status", Json::string(item.status.label())),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn string_array_to_json(items : Array[String]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(Json::string(item))
  }
  Json::array(values)
}