///|
/// Versioned envelope retaining original inputs, ordinary policies and repairs.
pub fn UniqueReview::to_json(self : UniqueReview) -> Json raise MergeError {
  let value : Json = {
    "format": "mntablemerge/unique-review-1",
    "merge": self.review.to_json(),
    "unique_rules": unique_rules_json(self.rules_data),
    "repairs": decisions_json(self.repair_data),
  }
  if value.stringify().length() > max_chars {
    raise LimitExceeded("unique review exceeds 16000000 UTF-16 units")
  }
  value
}

///|
/// Rejects duplicate/unknown fields and recomputes every phase; cached results are not trusted.
pub fn UniqueReview::from_json(
  source : String,
) -> UniqueReview raise MergeError {
  guard checked_json(source) is Object(fields) &&
    fields.length() == 4 &&
    fields.get("format") == Some(Json::string("mntablemerge/unique-review-1")) &&
    fields.get("merge") is Some(original) &&
    fields.get("unique_rules") is Some(rules) &&
    fields.get("repairs") is Some(repairs) else {
    raise InvalidReview(
      "expected unique-review-1 with exactly format/merge/unique_rules/repairs; use uniqueness-aware commands",
    )
  }
  make_unique(
    ReviewBundle::from_json(original.stringify()),
    parse_unique_rules(rules.stringify()),
    parse_decisions(repairs.stringify()),
  )
}

///|
fn UniqueConflict::to_json(self : UniqueConflict) -> Json {
  {
    "rule": self.rule,
    "columns": Json::array(self.columns.map(Json::string)),
    "values": Json::array(self.values.map(Json::string)),
    "rows": Json::array(
      self.rows.map(k => Json::array(k.parts.map(Json::string))),
    ),
    "base": values_json(self.base),
    "ours": values_json(self.ours),
    "theirs": values_json(self.theirs),
    "merged": values_json(self.merged),
  }
}

///|
fn UniqueReview::repair_evidence(
  self : UniqueReview,
  decision : Decision,
) -> Json {
  let key = match decision.target {
    Row(k) | Cell(k, _) => k
    _ => abort("validated repair")
  }
  let before = unique_row_values(self.outputs[0], [key])
  let after = unique_row_values(self.outputs[1], [key])
  {
    "target": decision.target.to_json(),
    "choice": decision.choice.to_json(),
    "base": values_json(unique_row_values(self.sources[0], [key])),
    "ours": values_json(unique_row_values(self.sources[1], [key])),
    "theirs": values_json(unique_row_values(self.sources[2], [key])),
    "before": values_json(before),
    "after": values_json(after),
  }
}

///|
/// Full ordinary/unique evidence. Oversized reports fail without truncation.
pub fn UniqueReview::report(self : UniqueReview) -> Json raise MergeError {
  let report = self.ordinary.report()
  guard report is Object(fields) else { abort("report object") }
  fields["format"] = Json::string("mntablemerge/unique-report-1")
  fields["complete"] = Json::boolean(
    self.candidate != None && self.pending_unique.is_empty(),
  )
  fields["unique_rules"] = unique_rules_json(self.rules_data)
  fields["unique_conflicts"] = Json::array(
    self.pending_unique.map(c => c.to_json()),
  )
  fields["unique_repairs"] = Json::array(
    self.repair_data.map(d => self.repair_evidence(d)),
  )
  if report.stringify().length() > max_chars {
    raise LimitExceeded(
      "unique report exceeds 16000000 UTF-16 units; inspect typed conflicts instead",
    )
  }
  report
}

///|
/// Compares equal-input/equal-rule reviews with full before/after repair records.
pub fn UniqueReview::changes_report(
  self : UniqueReview,
  previous : UniqueReview,
) -> Json raise MergeError {
  self.review.require_same_inputs(previous.review)
  if self.rules_data != previous.rules_data {
    raise InvalidReview(
      "unique rules differ; compare reviews of the same policies",
    )
  }
  let ordinary = self.review.changes_report(previous.review)
  guard ordinary is Object(fields) &&
    fields.get("changes") is Some(Array(ordinary_changes)) else {
    abort("audit changes")
  }
  let before : Map[String, Json] = Map([])
  let after : Map[String, Json] = Map([])
  let targets : Map[String, Target] = Map([])
  for (source, values) in [(previous, before), (self, after)] {
    for d in source.repair_data {
      let token = d.target.to_json().stringify()
      values[token] = source.repair_evidence(d)
      targets[token] = d.target
    }
  }
  let ordered = targets.to_array()
  ordered.sort_by((a, b) => lexical_string(a.0, b.0))
  let changes = ordinary_changes.copy()
  for (token, target) in ordered {
    if before.get(token) != after.get(token) {
      changes.push({
        "phase": "unique",
        "target": target.to_json(),
        "before": before.get(token).unwrap_or(Json::null()),
        "after": after.get(token).unwrap_or(Json::null()),
      })
    }
  }
  let report : Json = {
    "format": "mntablemerge/unique-changes-1",
    "merge": ordinary,
    "unique_rules": unique_rules_json(self.rules_data),
    "before_complete": previous.candidate != None &&
    previous.pending_unique.is_empty(),
    "after_complete": self.candidate != None && self.pending_unique.is_empty(),
    "changes": Json::array(changes),
  }
  if report.stringify().length() > max_chars {
    raise LimitExceeded("unique audit exceeds 16000000 UTF-16 units")
  }
  report
}