///|
/// Decision-table compilation.
///
/// Business authors think in tables: rows of mutually exclusive value
/// ranges mapping to outcomes. The decision_table operation compiles
/// such a table into engine rules — each row becomes one rule whose
/// condition is the conjunction of its range predicates. Priority
/// derives from row order. Compilation does not prove disjointness;
/// callers must inspect conflicts and select an explicit hit policy.

///|
fn compile_cell(column : String, cell : Value) -> String raise {
  match cell {
    String(text) => {
      let operators = ["<=", ">=", "==", "!=", "<", ">"]
      let mut matched = ""
      let mut rest = ""
      for op in operators {
        if text.has_prefix(op) {
          matched = op
          rest = text[op.length():].to_owned().trim().to_owned()
          break
        }
      }
      if matched == "" {
        rest = text.trim().to_owned()
        matched = "=="
      }
      let literal : Value? = Some(parse_value(rest)) catch { _ => None }
      let rendered = match literal {
        Some(Number(value)) => {
          if value.is_nan() || value.is_inf() {
            raise input_error(
              "E3004",
              "cell " + column + ": " + text + " is not finite",
            )
          }
          rest
        }
        Some(Bool(_)) => rest
        Some(String(_)) | None => Json::string(rest).stringify()
        Some(other) =>
          raise input_error(
            "E3001",
            "cell " +
            column +
            ": unsupported literal " +
            Value::stringify(other),
          )
      }
      column + " " + matched + " " + rendered
    }
    Bool(flag) => column + " == " + (if flag { "true" } else { "false" })
    Number(value) => column + " == " + Json::number(value).stringify()
    _ =>
      raise input_error(
        "E3001",
        "cell in column " + column + " must be a string, bool or number",
      )
  }
}

///|
fn decision_table_request(request : Value) -> Value raise {
  let table = obj(get(request, "table"))
  let columns_value = arr(get(Object(table), "columns"))
  let columns : Array[String] = []
  let seen_columns : Map[String, Bool] = Map([])
  for column in columns_value {
    let name = str(column)
    if name == "" || seen_columns.contains(name) {
      raise input_error("E4001", "duplicate or empty decision-table column")
    }
    for part in name.split(".") {
      let chars = part.to_owned().to_array()
      if chars.is_empty() ||
        !((chars[0] >= 'a' && chars[0] <= 'z') ||
        (chars[0] >= 'A' && chars[0] <= 'Z') ||
        chars[0] == '_') ||
        !chars.all(identifier_char) {
        raise input_error("E3002", "decision-table columns must be fact paths")
      }
    }
    seen_columns[name] = true
    columns.push(name)
  }
  if columns.is_empty() {
    raise input_error("E3002", "decision table needs columns")
  }
  let rows = arr(get(Object(table), "rows"))
  let outcomes : Map[String, Bool] = Map([])
  let rules : Array[Value] = []
  let mut row_number = 0
  let row_count = rows.length()
  for row in rows {
    let row_obj = obj(row)
    let when_parts : Array[String] = []
    for column in columns {
      let cell = get(Object(row_obj), column)
      if cell is Null {
        raise input_error(
          "E4006",
          "row " + row_number.to_string() + " is missing column " + column,
        )
      }
      when_parts.push(compile_cell(column, cell))
    }
    let outcome = get(Object(row_obj), "outcome")
    if outcome is Null {
      raise input_error(
        "E4007",
        "row " + row_number.to_string() + " has no outcome",
      )
    }
    match outcome {
      String(name) => {
        if outcomes.contains(name) {
          raise input_error("E4001", "duplicate outcome: " + name)
        }
        outcomes[name] = true
      }
      _ => ()
    }
    let priority = (row_count - row_number).to_double()
    let condition = when_parts.join(" && ")
    ignore(compile(condition))
    let rule_id = match outcome {
      String(name) => name
      other => "row_" + row_number.to_string() + "_" + Value::stringify(other)
    }
    rules.push(
      record([
        ("id", String(rule_id)),
        ("priority", Number(priority)),
        ("when", String(condition)),
        ("output", outcome),
      ]),
    )
    row_number += 1
  }
  record([
    ("rules", Array(rules)),
    ("rule_count", Number(rules.length().to_double())),
    ("columns", strings(columns)),
  ])
}

///|
/// Compiles the table and immediately decides a fact through it,
/// the single-call convenience for callers who do not need the
/// intermediate rules.
fn decide_table_request(request : Value) -> Value raise {
  let compiled = decision_table_request(request)
  let decide_request = record([
    ("operation", String("decide")),
    ("rules", get(compiled, "rules")),
    ("facts", get(request, "facts")),
    ("mode", get(request, "mode")),
    ("default", get(request, "default")),
    ("budget", get(request, "budget")),
  ])
  record([
    ("results", get(run(decide_request), "results")),
    ("rule_count", get(compiled, "rule_count")),
    ("rules", get(compiled, "rules")),
    ("columns", get(compiled, "columns")),
  ])
}