///|
/// Rule-set merging.
///
/// The merge operation combines multiple rule sets into one: rules
/// are matched by ID across sets, and every ID keeps a single entry
/// in the merged output. Non-conflicting duplicates collapse; when
/// two sets define the same ID differently the strategy decides —
/// `first` keeps the earlier set's version and `last` keeps the
/// later's, while `report` marks the ID as conflicted and keeps the
/// earliest. The output records each rule's origin so reviews can
/// trace provenance.

///|
fn rule_signature(item : Value) -> String raise {
  // A stable comparable form of a rule: canonical condition, compute,
  // priority and output through canonical serialization.
  let signature : Map[String, Value] = Map([])
  signature["when"] = String(compile(str(get(item, "when"))).render())
  match get(item, "priority") {
    Null => signature["priority"] = Number(0.0)
    other => signature["priority"] = other
  }
  match get(item, "compute") {
    Null => ()
    String(source) => signature["compute"] = String(compile(source).render())
    _ => signature["compute"] = get(item, "compute")
  }
  signature["output"] = get(item, "output")
  canonical(Object(signature))
}

///|
fn merge_request(request : Value) -> Value raise {
  let strategy = optional_str(get(request, "strategy"), "first")
  if strategy != "first" && strategy != "last" && strategy != "report" {
    raise input_error("E3005", "strategy must be first, last or report")
  }
  let sets = match get(request, "sets") {
    Array(items) => items
    _ => raise input_error("E3002", "merge needs a sets array")
  }
  if sets.is_empty() {
    raise input_error("E3002", "merge needs at least one rule set")
  }
  let by_id : Map[String, Value] = Map([])
  let origin : Map[String, String] = Map([])
  let conflicts : Map[String, Bool] = Map([])
  let mut set_index = 0
  for set in sets {
    let set_name = match get(set, "name") {
      String(name) => name
      _ => "set" + set_index.to_string()
    }
    for item in arr(get(set, "rules")) {
      let id = str(get(item, "id"))
      match by_id.get(id) {
        None => {
          by_id[id] = item
          origin[id] = set_name
        }
        Some(existing) => {
          let same = rule_signature(existing) == rule_signature(item)
          if !same {
            match strategy {
              "last" => {
                by_id[id] = item
                origin[id] = set_name
                conflicts[id] = true
              }
              "report" => conflicts[id] = true
              _ => conflicts[id] = true
            }
          }
        }
      }
    }
    set_index += 1
  }
  let ids : Array[String] = []
  for id, _ in by_id {
    ids.push(id)
  }
  ids.sort_by(fn(a, b) { stable_string_compare(a, b) })
  let merged : Array[Value] = []
  let conflicted_ids : Array[String] = []
  for id in ids {
    let item = by_id[id]
    merged.push(
      record([
        ("id", String(id)),
        ("origin", String(origin.get(id).unwrap_or("?"))),
        ("rule", item),
      ]),
    )
    if conflicts.contains(id) {
      conflicted_ids.push(id)
    }
  }
  conflicted_ids.sort_by(fn(a, b) { stable_string_compare(a, b) })
  let sources : Array[Value] = []
  let mut set_idx2 = 0
  for set in sets {
    let set_name = match get(set, "name") {
      String(name) => name
      _ => "set" + set_idx2.to_string()
    }
    let count = arr(get(set, "rules")).length()
    sources.push(
      record([("name", String(set_name)), ("rules", Number(count.to_double()))]),
    )
    set_idx2 += 1
  }
  record([
    ("rules", Array(merged)),
    ("rule_count", Number(merged.length().to_double())),
    ("conflict_count", Number(conflicted_ids.length().to_double())),
    ("conflicted_ids", strings(conflicted_ids)),
    ("sources", Array(sources)),
    ("strategy", String(strategy)),
  ])
}