///|
/// Human review lifecycle for findings that should not be silently discarded.
pub(all) enum ReviewStatus {
  Queued
  Assigned
  InReview
  Decided
  Cancelled
} derive(Debug, Eq)

///|
pub(all) enum ReviewDecision {
  ApproveRedaction
  KeepValue
  Escalate
  Defer
  NoDecision
} derive(Debug, Eq)

///|
pub(all) struct ReviewTask {
  id : String
  document_id : String
  finding : Finding
  risk : RiskLevel
  priority : Int
  status : ReviewStatus
  assignee : String
  decision : ReviewDecision
  notes : Array[String]
  created_sequence : Int
} derive(Debug, Eq)

///|
pub(all) struct ReviewPolicy {
  min_confidence : Int
  max_open_tasks : Int
  escalate_risk : RiskLevel
  assign_by_kind : Map[String, String]
  auto_keep_below : Int
} derive(Debug, Eq)

///|
pub(all) struct ReviewQueue {
  mut tasks : Array[ReviewTask]
  mut sequence : Int
  mut closed : Bool
  policy : ReviewPolicy
} derive(Debug)

///|
pub fn review_status_name(status : ReviewStatus) -> String {
  match status {
    Queued => "queued"
    Assigned => "assigned"
    InReview => "in_review"
    Decided => "decided"
    Cancelled => "cancelled"
  }
}

///|
pub fn review_decision_name(decision : ReviewDecision) -> String {
  match decision {
    ApproveRedaction => "approve_redaction"
    KeepValue => "keep_value"
    Escalate => "escalate"
    Defer => "defer"
    NoDecision => "no_decision"
  }
}

///|
pub fn ReviewPolicy::default() -> ReviewPolicy {
  {
    min_confidence: 45,
    max_open_tasks: 10000,
    escalate_risk: Critical,
    assign_by_kind: Map([]),
    auto_keep_below: 20,
  }
}

///|
pub fn ReviewPolicy::strict() -> ReviewPolicy {
  {
    ..ReviewPolicy::default(),
    min_confidence: 70,
    max_open_tasks: 1000,
    escalate_risk: High,
    auto_keep_below: 0,
  }
}

///|
pub fn review_policy_assign(
  policy : ReviewPolicy,
  kind : PhiKind,
  reviewer : String,
) -> ReviewPolicy {
  let assignments = policy.assign_by_kind.copy()
  assignments[phi_kind_name(kind)] = reviewer
  { ..policy, assign_by_kind: assignments }
}

///|
pub fn review_policy_for_kind(policy : ReviewPolicy, kind : PhiKind) -> String? {
  policy.assign_by_kind.get(phi_kind_name(kind))
}

///|
pub fn ReviewQueue::new(policy : ReviewPolicy) -> ReviewQueue {
  { tasks: [], sequence: 0, closed: false, policy }
}

///|
pub fn ReviewQueue::default() -> ReviewQueue {
  ReviewQueue::new(ReviewPolicy::default())
}

///|
pub fn ReviewQueue::is_closed(self : ReviewQueue) -> Bool {
  self.closed
}

///|
pub fn ReviewQueue::close(self : ReviewQueue) -> Unit {
  self.closed = true
}

///|
pub fn ReviewQueue::open(self : ReviewQueue) -> Unit {
  self.closed = false
}

///|
pub fn ReviewQueue::size(self : ReviewQueue) -> Int {
  self.tasks.length()
}

///|
pub fn ReviewQueue::open_count(self : ReviewQueue) -> Int {
  self.tasks
  .filter(fn(task) { task.status != Decided && task.status != Cancelled })
  .length()
}

///|
pub fn ReviewQueue::decided_count(self : ReviewQueue) -> Int {
  self.tasks.filter(fn(task) { task.status == Decided }).length()
}

///|
fn review_priority(finding : Finding, policy : ReviewPolicy) -> Int {
  let risk_score = match risk_level(finding) {
    Low => 10
    Medium => 30
    High => 60
    Critical => 90
  }
  let low_bonus = if finding.confidence < policy.min_confidence {
    20
  } else {
    0
  }
  risk_score + (100 - finding.confidence) + low_bonus
}

///|
pub fn review_task(
  document_id : String,
  finding : Finding,
  sequence : Int,
  policy : ReviewPolicy,
) -> ReviewTask {
  {
    id: "review-\{document_id}-\{sequence}",
    document_id,
    finding,
    risk: risk_level(finding),
    priority: review_priority(finding, policy),
    status: Queued,
    assignee: "",
    decision: NoDecision,
    notes: [],
    created_sequence: sequence,
  }
}

///|
pub fn ReviewQueue::enqueue(
  self : ReviewQueue,
  document_id : String,
  finding : Finding,
) -> ReviewTask? {
  if self.closed || self.open_count() >= self.policy.max_open_tasks {
    None
  } else {
    self.sequence += 1
    let task = review_task(document_id, finding, self.sequence, self.policy)
    self.tasks.push(task)
    Some(task)
  }
}

///|
pub fn ReviewQueue::enqueue_findings(
  self : ReviewQueue,
  document_id : String,
  findings : Array[Finding],
) -> Array[ReviewTask] {
  let tasks = []
  for finding in findings {
    match self.enqueue(document_id, finding) {
      Some(task) => tasks.push(task)
      None => ()
    }
  }
  tasks
}

///|
pub fn ReviewQueue::enqueue_result(
  self : ReviewQueue,
  document_id : String,
  result : DeidResult,
) -> Array[ReviewTask] {
  self.enqueue_findings(document_id, result.findings)
}

///|
pub fn ReviewQueue::get(self : ReviewQueue, id : String) -> ReviewTask? {
  let mut result : ReviewTask? = None
  for task in self.tasks {
    if task.id == id {
      result = Some(task)
    }
  }
  result
}

///|
pub fn ReviewQueue::update_task(
  self : ReviewQueue,
  id : String,
  update : ReviewTask,
) -> Bool {
  let mut updated = false
  for i in 0.. Bool {
  let mut updated = false
  for i in 0.. Bool {
  let mut updated = false
  for i in 0.. Bool {
  let mut updated = false
  for i in 0.. Bool {
  let mut updated = false
  for i in 0.. Bool {
  let mut updated = false
  for i in 0.. Array[ReviewTask] {
  self.tasks.filter(fn(task) {
    task.status == Queued && task.assignee.is_empty()
  })
}

///|
pub fn ReviewQueue::assigned_to(
  self : ReviewQueue,
  reviewer : String,
) -> Array[ReviewTask] {
  self.tasks.filter(fn(task) { task.assignee == reviewer })
}

///|
pub fn ReviewQueue::by_status(
  self : ReviewQueue,
  status : ReviewStatus,
) -> Array[ReviewTask] {
  self.tasks.filter(fn(task) { task.status == status })
}

///|
pub fn ReviewQueue::by_risk(
  self : ReviewQueue,
  risk : RiskLevel,
) -> Array[ReviewTask] {
  self.tasks.filter(fn(task) { task.risk == risk })
}

///|
pub fn ReviewQueue::by_document(
  self : ReviewQueue,
  document_id : String,
) -> Array[ReviewTask] {
  self.tasks.filter(fn(task) { task.document_id == document_id })
}

///|
pub fn ReviewQueue::top_priority(
  self : ReviewQueue,
  limit : Int,
) -> Array[ReviewTask] {
  let tasks = self.tasks.filter(fn(task) {
    task.status != Decided && task.status != Cancelled
  })
  tasks.sort_by(fn(left, right) { right.priority - left.priority })
  if limit <= 0 || limit >= tasks.length() {
    tasks
  } else {
    tasks[:limit].to_owned()
  }
}

///|
pub fn ReviewQueue::auto_assign(self : ReviewQueue, reviewer : String) -> Int {
  let mut assigned = 0
  for i in 0.. Int {
  let mut assigned = 0
  for i in 0.. {
        self.tasks[i] = { ..task, status: Assigned, assignee: reviewer }
        assigned += 1
      }
      _ => ()
    }
  }
  assigned
}

///|
pub fn ReviewQueue::auto_decide_low_confidence(self : ReviewQueue) -> Int {
  let mut count = 0
  for i in 0.. String {
  [
    "id=\{task.id}",
    "document=\{task.document_id}",
    "kind=\{phi_kind_name(task.finding.kind)}",
    "risk=\{task.risk}",
    "confidence=\{task.finding.confidence}",
    "priority=\{task.priority}",
    "status=\{review_status_name(task.status)}",
    "assignee=\{task.assignee}",
    "decision=\{review_decision_name(task.decision)}",
  ].join("\n")
}

///|
pub fn review_task_json(task : ReviewTask) -> String {
  let notes = task.notes.map(json_escape).join(",")
  "{" +
  "\"id\":\{json_escape(task.id)}," +
  "\"document_id\":\{json_escape(task.document_id)}," +
  "\"kind\":\{json_escape(phi_kind_name(task.finding.kind))}," +
  "\"risk\":\{json_escape("\{task.risk}")}," +
  "\"priority\":\{task.priority}," +
  "\"status\":\{json_escape(review_status_name(task.status))}," +
  "\"assignee\":\{json_escape(task.assignee)}," +
  "\"decision\":\{json_escape(review_decision_name(task.decision))}," +
  "\"notes\":[" +
  notes +
  "]}"
}

///|
pub fn ReviewQueue::to_json(self : ReviewQueue) -> String {
  "[" + self.tasks.map(review_task_json).join(",") + "]"
}

///|
pub fn ReviewQueue::to_csv(self : ReviewQueue) -> String {
  let lines = [
    "id,document_id,kind,risk,confidence,priority,status,assignee,decision",
  ]
  for task in self.tasks {
    lines.push(
      [
        csv_cell(task.id),
        csv_cell(task.document_id),
        csv_cell(phi_kind_name(task.finding.kind)),
        csv_cell("\{task.risk}"),
        "\{task.finding.confidence}",
        "\{task.priority}",
        review_status_name(task.status),
        csv_cell(task.assignee),
        review_decision_name(task.decision),
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
pub fn ReviewQueue::risk_counts(self : ReviewQueue) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for task in self.tasks {
    let key = "\{task.risk}"
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn ReviewQueue::status_counts(self : ReviewQueue) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for task in self.tasks {
    let key = review_status_name(task.status)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn ReviewQueue::decision_counts(self : ReviewQueue) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for task in self.tasks {
    let key = review_decision_name(task.decision)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn ReviewQueue::average_priority(self : ReviewQueue) -> Float {
  if self.tasks.is_empty() {
    0.0
  } else {
    Float::from_int(self.tasks.fold(init=0, (sum, task) => sum + task.priority)) /
    Float::from_int(self.tasks.length())
  }
}

///|
pub fn ReviewQueue::completion_ratio(self : ReviewQueue) -> Float {
  if self.tasks.is_empty() {
    1.0
  } else {
    Float::from_int(self.decided_count()) / Float::from_int(self.tasks.length())
  }
}

///|
pub fn ReviewQueue::has_critical_open(self : ReviewQueue) -> Bool {
  self.tasks.any(fn(task) {
    task.risk == Critical && task.status != Decided && task.status != Cancelled
  })
}

///|
pub fn ReviewQueue::is_ready(self : ReviewQueue) -> Bool {
  !self.has_critical_open() && self.open_count() == 0
}

///|
pub fn ReviewQueue::checksum(self : ReviewQueue) -> String {
  let values = self.tasks.map(fn(task) {
    task.id +
    ":" +
    review_status_name(task.status) +
    ":" +
    review_decision_name(task.decision)
  })
  values.sort()
  stable_hash(values.join("\n"))
}

///|
pub fn review_queue_summary(queue : ReviewQueue) -> String {
  [
    "tasks=\{queue.size()}",
    "open=\{queue.open_count()}",
    "decided=\{queue.decided_count()}",
    "completion=\{queue.completion_ratio()}",
    "critical_open=\{queue.has_critical_open()}",
    "checksum=\{queue.checksum()}",
  ].join("\n")
}