///|
pub(all) struct Comparison {
  schema_version : Int32
  baseline : Audit
  candidate : Audit
  retained_pairs : Int64
  lost_pairs : Int64
  gained_pairs : Int64
  newly_unmatched_left : Array[String]
  newly_unmatched_right : Array[String]
  recovered_left : Array[String]
  recovered_right : Array[String]
} derive(ToJson)

///|
fn list_minus(a : Array[String], b : Array[String]) -> Array[String] {
  let lookup : Map[String, Bool] = Map([])
  for id in b {
    lookup[id] = true
  }
  a.filter(id => !lookup.contains(id))
}

///|
/// Exact pair-set changes. Reduced output does not prove a business-correct key.
pub fn compare(
  left : Table,
  right : Table,
  baseline : Plan,
  candidate : Plan,
) -> Comparison raise JoinError {
  if baseline.kind != candidate.kind || baseline.nulls != candidate.nulls {
    invalid("comparison requires identical join kind and null semantics")
  }
  let before = audit(left, right, baseline)
  let after = audit(left, right, candidate)
  let li = indexes(left, baseline.keys, true)
  let ri = indexes(right, baseline.keys, false)
  for pos in indexes(left, candidate.keys, true) {
    li.push(pos)
  }
  for pos in indexes(right, candidate.keys, false) {
    ri.push(pos)
  }
  // Conjunction of both match predicates equals intersection of their pair sets.
  let retained = inventory_keys(left, right, li, ri, baseline.nulls, "inner").matched_pairs
  {
    schema_version: 1,
    baseline: before,
    candidate: after,
    retained_pairs: retained,
    lost_pairs: before.inventory.matched_pairs - retained,
    gained_pairs: after.inventory.matched_pairs - retained,
    newly_unmatched_left: list_minus(
      after.inventory.unmatched_left,
      before.inventory.unmatched_left,
    ),
    newly_unmatched_right: list_minus(
      after.inventory.unmatched_right,
      before.inventory.unmatched_right,
    ),
    recovered_left: list_minus(
      before.inventory.unmatched_left,
      after.inventory.unmatched_left,
    ),
    recovered_right: list_minus(
      before.inventory.unmatched_right,
      after.inventory.unmatched_right,
    ),
  }
}