///|
/// Decision regression snapshots.
///
/// The snapshot operation runs a rule set over a fact batch and
/// returns a compact fingerprint per record: the decision value, the
/// sorted matched-rule IDs and the conflict flag, hashed into a short
/// digest. The compare operation re-decides and diffs against a
/// stored snapshot, reporting exactly which records changed and what
/// the old and new decisions were. This is the regression gate for
/// rule edits: capture before, compare after, review every diff.

///|

///|
fn fingerprint_of(result : Value) -> String raise {
  let matched_ids : Array[String] = []
  match get(result, "matched") {
    Array(matched) =>
      for entry in matched {
        matched_ids.push(str(get(entry, "id")))
      }
    _ => ()
  }
  matched_ids.sort_by(fn(a, b) { stable_string_compare(a, b) })
  let conflict = get(result, "conflict") is Bool(true)
  let fingerprint : Map[String, Value] = Map([])
  fingerprint["decision"] = get(result, "decision")
  fingerprint["matched"] = strings(matched_ids)
  fingerprint["conflict"] = Bool(conflict)
  digest(canonical(Object(fingerprint)))
}

///|
fn snapshot_request(request : Value) -> Value raise {
  let mode = optional_str(get(request, "mode"), "first")
  let decided = run(
    record([
      ("operation", String("decide")),
      ("rules", get(request, "rules")),
      ("facts", get(request, "facts")),
      ("mode", String(mode)),
      ("default", get(request, "default")),
      ("budget", get(request, "budget")),
    ]),
  )
  match get(decided, "results") {
    Array(results) => {
      let fingerprints : Array[Value] = []
      for entry in results {
        let fp = match get(entry, "result") {
          Object(_) => String(fingerprint_of(get(entry, "result")))
          _ => String("error")
        }
        fingerprints.push(
          record([("index", get(entry, "index")), ("fingerprint", fp)]),
        )
      }
      record([
        ("snapshots", Array(fingerprints)),
        ("count", Number(fingerprints.length().to_double())),
      ])
    }
    _ => raise input_error("E3002", "decide produced no results")
  }
}

///|
fn compare_request(request : Value) -> Value raise {
  let current = get(snapshot_request(request), "snapshots")
  let stored = arr(get(request, "snapshots"))
  if stored.length() == 0 {
    raise input_error("E3002", "compare needs a stored snapshot")
  }
  let stored_by_index : Map[Int32, String] = Map([])
  for entry in stored {
    let index = num(get(entry, "index")).to_int()
    stored_by_index[index] = str(get(entry, "fingerprint"))
  }
  let diffs : Array[Value] = []
  let mut same = 0
  let mut missing = 0
  let mut added = 0
  match current {
    Array(entries) =>
      for entry in entries {
        let index = num(get(entry, "index")).to_int()
        let now = str(get(entry, "fingerprint"))
        match stored_by_index.get(index) {
          None => {
            added += 1
            diffs.push(
              record([
                ("index", Number(index.to_double())),
                ("kind", String("added")),
                ("fingerprint", String(now)),
              ]),
            )
          }
          Some(before) =>
            if before != now {
              diffs.push(
                record([
                  ("index", Number(index.to_double())),
                  ("kind", String("changed")),
                  ("before", String(before)),
                  ("after", String(now)),
                ]),
              )
            } else {
              same += 1
            }
        }
      }
    _ => ()
  }
  for index, _ in stored_by_index {
    let mut found = false
    match current {
      Array(entries) =>
        for entry in entries {
          if num(get(entry, "index")).to_int() == index {
            found = true
          }
        }
      _ => ()
    }
    if !found {
      missing += 1
      diffs.push(
        record([
          ("index", Number(index.to_double())),
          ("kind", String("removed")),
        ]),
      )
    }
  }
  // Sort outside the raising context: precompute indices first.
  let indexed : Array[(Double, Value)] = []
  for diff in diffs {
    let index_value = match get(diff, "index") {
      Number(n) => n
      _ => 0.0
    }
    indexed.push((index_value, diff))
  }
  indexed.sort_by(fn(a, b) { a.0.compare(b.0) })
  let sorted_diffs : Array[Value] = []
  for pair in indexed {
    sorted_diffs.push(pair.1)
  }
  let diffs = sorted_diffs
  record([
    ("ok", Bool(diffs.is_empty())),
    ("unchanged", Number(same.to_double())),
    ("changed", Number((diffs.length() - added - missing).to_double())),
    ("added", Number(added.to_double())),
    ("removed", Number(missing.to_double())),
    ("diffs", Array(diffs)),
  ])
}