///|
/// An explicit atomic update to current review decisions.
pub(all) enum ReviewEdit {
  Set(Decision)
  Revoke(Target)
} derive(Eq, Debug)

///|
pub extend ReviewEdit with Eq::{equal, not_equal}

///|
pub extend ReviewEdit with @debug.Debug::{to_repr}

///|
fn ReviewEdit::target(self : ReviewEdit) -> Target {
  match self {
    Set(d) => d.target
    Revoke(target) => target
  }
}

///|
fn validate_edit_target(target : Target) -> Unit raise MergeError {
  fn column(name : String) -> Unit raise MergeError {
    validate_unicode(name)
    if name.is_empty() {
      raise InvalidDecision("review column must be nonempty")
    }
  }
  fn key(key : RowKey) -> Unit raise MergeError {
    if key.parts.is_empty() || key.parts.length() > max_columns {
      raise InvalidDecision("review row key requires 1..512 components")
    }
    for part in key.parts {
      validate_unicode(part)
      if part.is_empty() {
        raise InvalidDecision("review key components must be nonempty")
      }
    }
  }
  match target {
    Column(name) => column(name)
    Row(id) => key(id)
    Cell(id, name) => {
      key(id)
      column(name)
    }
  }
}

///|
/// Replaces/revokes choices without modifying this plan. Every retained choice
/// must remain applicable; revoke dependencies explicitly in the same batch.
pub fn MergePlan::revise(
  self : MergePlan,
  edits : Array[ReviewEdit],
) -> MergePlan raise MergeError {
  if edits.length() > max_cells {
    raise LimitExceeded("too many review edits")
  }
  let existing : Map[String, Bool] = Map([])
  let pending : Map[String, Bool] = Map([])
  for decision in self.decisions_data {
    existing[decision.target.to_json().stringify()] = true
  }
  for conflict in self.pending {
    pending[conflict.target.to_json().stringify()] = true
  }
  let updates : Map[String, ReviewEdit] = Map([])
  for edit in edits {
    let target = edit.target()
    validate_edit_target(target)
    let token = target.to_json().stringify()
    if updates.contains(token) {
      raise InvalidDecision(
        "duplicate review edit target; use one set/revoke per target",
      )
    }
    match edit {
      Set(_) =>
        if !existing.contains(token) && !pending.contains(token) {
          raise InvalidDecision(
            "cannot set unknown or newly hidden target \{token}; inspect current conflicts and resolve structure first",
          )
        }
      Revoke(_) =>
        if !existing.contains(token) {
          raise InvalidDecision(
            "cannot revoke unrecorded target \{token}; inspect decisions()",
          )
        }
    }
    updates[token] = edit
  }
  let choices : Array[Decision] = []
  // Replacements retain their position, unaffected decisions retain order.
  for decision in self.decisions_data {
    match updates.get(decision.target.to_json().stringify()) {
      Some(Set(replacement)) => choices.push(replacement)
      Some(Revoke(_)) => ()
      None => choices.push(decision)
    }
  }
  // Choices for current pending targets append in supplied edit order.
  for edit in edits {
    if edit is Set(decision) &&
      !existing.contains(decision.target.to_json().stringify()) {
      choices.push(decision)
    }
  }
  calculate(
    self.base,
    self.ours,
    self.theirs,
    self.key_columns,
    choices,
    self.ordering,
    lineage=self.lineage_data,
    rows=self.row_lineage_data,
    evolution=self.evolution_data,
  ) catch {
    InvalidDecision(reason) =>
      raise InvalidDecision(
        reason +
        "; revision rejected: explicitly revoke decisions that the new structure makes inapplicable, then inspect newly exposed conflicts",
      )
    error => raise error
  }
}

///|
pub fn ReviewBundle::revise(
  self : ReviewBundle,
  edits : Array[ReviewEdit],
) -> ReviewBundle raise MergeError {
  ReviewBundle::from_plan(self.plan().revise(edits))
}

///|
/// Strict export: refuses invalid Unicode and oversized edit documents.
pub fn review_edits_json(edits : Array[ReviewEdit]) -> Json raise MergeError {
  if edits.length() > max_cells {
    raise LimitExceeded("too many review edits")
  }
  let mut total = 0
  for edit in edits {
    let target = edit.target()
    let strings = match target {
      Column(name) => [name]
      Row(key) => key.components()
      Cell(key, name) => {
        let parts = key.components()
        parts.push(name)
        parts
      }
    }
    if edit is Set({ choice: Value(value), .. }) {
      strings.push(value)
    }
    for value in strings {
      if value.length() > max_chars - total {
        raise LimitExceeded(
          "review edit text exceeds 16000000 UTF-16 code units",
        )
      }
      total += value.length()
    }
  }
  let value = Json::array(
    edits.map(edit => {
      validate_edit_target(edit.target())
      match edit {
        Revoke(target) => { "op": "revoke", "target": target.to_json() }
        Set(decision) => {
          if decision.choice is Value(value) {
            validate_unicode(value)
          }
          {
            "op": "set",
            "target": decision.target.to_json(),
            "choice": decision.choice.to_json(),
          }
        }
      }
    }),
  )
  if value.stringify().length() > max_chars {
    raise LimitExceeded("review edit JSON exceeds 16000000 UTF-16 code units")
  }
  value
}

///|
pub fn parse_review_edits(
  source : String,
) -> Array[ReviewEdit] raise MergeError {
  guard checked_json(source) is Array(items) else {
    raise InvalidDecision("review edits must be an array")
  }
  if items.length() > max_cells {
    raise LimitExceeded("too many review edits")
  }
  items.map(item => {
    guard item is Object(fields) && fields.get("target") is Some(target) else {
      raise InvalidDecision("each review edit requires op and target")
    }
    let target = parse_target(target)
    validate_edit_target(target)
    match fields.get("op") {
      Some(String("revoke")) if fields.length() == 2 => Revoke(target)
      Some(String("set")) if fields.length() == 3 &&
        fields.get("choice") is Some(choice) =>
        Set({ target, choice: parse_choice(choice), })
      _ =>
        raise InvalidDecision(
          "use exactly {op: set, target, choice} or {op: revoke, target}; unknown fields/operations are rejected",
        )
    }
  })
}