///|
/// Human-readable decision reports.
///
/// The explain operation runs the normal decide pipeline over one record
/// and renders the structured result as deterministic text: one PASS or
/// FAIL line per evaluated rule with its canonical condition, the chosen
/// outputs, the final decision, conflict accounting and step counts.

///|
fn render_scalar_short(value : Value) -> String {
  match value {
    String(_) | Number(_) | Bool(_) | Null => value.stringify()
    other => {
      let text = other.stringify()
      if text.length() > 40 {
        text[0:37].to_owned() + "..."
      } else {
        text
      }
    }
  }
}

///|
fn explain_lines(result : Value, mode : String) -> Array[String] {
  let lines : Array[String] = []
  lines.push("mode: " + mode)
  lines.push("decision: " + render_scalar_short(get(result, "decision")))
  lines.push("conflict: " + get(result, "conflict").stringify())
  let conflict_ids = get(result, "conflict_ids")
  match conflict_ids {
    Array(items) =>
      if !items.is_empty() {
        lines.push(
          "conflicting rules: " +
          items
          .map(fn(v) {
            match v {
              String(s) => s
              _ => ""
            }
          })
          .join(", "),
        )
      }
    _ => ()
  }
  lines.push("")
  lines.push("evaluated rules:")
  match get(result, "evaluated") {
    Array(entries) =>
      for entry in entries {
        let id = match get(entry, "id") {
          String(name) => name
          _ => "?"
        }
        let matched = get(entry, "matched")
        let mark = match matched {
          Bool(true) => "[PASS]"
          _ => "[FAIL]"
        }
        lines.push("  " + mark + " " + id)
        match get(entry, "trace") {
          Array(trace) =>
            if !trace.is_empty() {
              let parts : Array[String] = []
              for step in trace {
                parts.push(render_scalar_short(step))
              }
              lines.push("        trace: " + parts.join(" | "))
            }
          _ => ()
        }
      }
    _ => ()
  }
  match get(result, "matched") {
    Array(matched) =>
      if !matched.is_empty() {
        lines.push("")
        lines.push("matched outputs:")
        for entry in matched {
          let id = match get(entry, "id") {
            String(name) => name
            _ => "?"
          }
          let priority = get(entry, "priority").stringify()
          lines.push(
            "  " +
            id +
            " (priority " +
            priority +
            ") -> " +
            render_scalar_short(get(entry, "value")),
          )
        }
      }
    _ => ()
  }
  lines.push("")
  lines.push("steps: " + get(result, "steps").stringify())
  lines
}

///|
fn explain_request(request : Value) -> Value raise {
  let mode = optional_str(get(request, "mode"), "first")
  if !["first", "all", "unique"].contains(mode) {
    raise input_error("E3006", "unknown decision mode")
  }
  let rules = load_rules(get(request, "rules"))
  let facts = match get(request, "facts") {
    Array(items) =>
      if items.length() != 1 {
        raise input_error("E3010", "explain needs exactly one fact record")
      } else {
        items[0]
      }
    other => other
  }
  let budget = optional_num(get(request, "budget"), 10000.0).to_int()
  if budget <= 0 || budget > 1000000 {
    raise input_error("E3007", "budget out of range")
  }
  let result = decide(rules, facts, mode, get(request, "default"), budget)
  let lines = explain_lines(result, mode)
  record([
    ("text", String(lines.join("\n"))),
    ("line_count", Number(lines.length().to_double())),
    ("result", result),
  ])
}