///|
fn numeric_value(value : Json?) -> ExactDecimal? {
  match value {
    Some(Number(_, ..) as n) => Some(exact_decimal(canonical_json(n)))
    _ => None
  }
}

///|
fn NumericFieldRule::scalar_status(
  self : NumericFieldRule,
  number : ExactDecimal,
) -> String {
  if self.lower is Some(bound) {
    let c = number.compare(bound.value)
    if c < 0 || (c == 0 && !bound.inclusive) {
      return "below_minimum"
    }
  }
  if self.upper is Some(bound) {
    let c = number.compare(bound.value)
    if c > 0 || (c == 0 && !bound.inclusive) {
      return "above_maximum"
    }
  }
  "pass"
}

///|
// One bounded outcome per field: the first failed condition plus complete
// original value/history. No array element is removed or coerced.
fn NumericFieldRule::check(
  self : NumericFieldRule,
  value : Json?,
) -> (String, Int32?) {
  guard value is Some(value) else { return ("missing_numeric_field", None) }
  if !self.array {
    return match numeric_value(Some(value)) {
      Some(number) => (self.scalar_status(number), None)
      None => ("not_number", None)
    }
  }
  guard value is Array(items) else { return ("not_array", None) }
  if items.length() < self.minimum_items {
    return ("too_few_items", None)
  }
  if self.maximum_items is Some(maximum) && items.length() > maximum {
    return ("too_many_items", None)
  }
  let mut previous : ExactDecimal? = None
  for i, item in items {
    guard numeric_value(Some(item)) is Some(number) else {
      return ("not_number", Some(i))
    }
    let status = self.scalar_status(number)
    if status != "pass" {
      return (status, Some(i))
    }
    if self.increasing &&
      previous is Some(before) &&
      before.compare(number) >= 0 {
      return ("not_strictly_increasing", Some(i))
    }
    previous = Some(number)
  }
  ("pass", None)
}

///|
fn numeric_evidence(result : ResolvedMetadata, field : String) -> Json {
  let value = result.values.get(field)
  let doc : Map[String, Json] = {
    "field": Json::string(field),
    "present": Json::boolean(value is Some(_)),
  }
  if value is Some(value) {
    doc["actual"] = value
    let history = result.origins[field]
    doc["trace"] = history_value(history)
    doc["winning_source"] = Json::string(history[history.length() - 1].path)
  }
  Json::object(doc)
}

///|
fn numeric_relation_matches(order : Int32, op : String) -> Bool {
  match op {
    "lt" => order < 0
    "lte" => order <= 0
    "eq" => order == 0
    "gte" => order >= 0
    "gt" => order > 0
    _ => abort("validated numeric operator")
  }
}

///|
fn NumericRules::evaluate(
  self : NumericRules,
  result : ResolvedMetadata,
) -> (Array[Json], Int32) {
  let checks : Array[Json] = []
  let mut violations = 0
  for rule in self.fields {
    let (status, item_index) = rule.check(result.values.get(rule.field))
    if status != "pass" {
      violations += 1
    }
    checks.push(
      Json::object({
        "kind": Json::string("field"),
        "status": Json::string(status),
        "field": Json::string(rule.field),
        "rule": Json::object(rule.document),
        "evidence": numeric_evidence(result, rule.field),
        "item_index": item_index
        .map(i => Json::number(i.to_double()))
        .unwrap_or(Json::null()),
      }),
    )
  }
  for relation in self.relations {
    let left = result.values.get(relation.left)
    let right = result.values.get(relation.right)
    let status = match (numeric_value(left), numeric_value(right)) {
      (Some(a), Some(b)) =>
        if numeric_relation_matches(a.compare(b), relation.op) {
          "pass"
        } else {
          "relation_failed"
        }
      _ => "invalid_operands"
    }
    if status != "pass" {
      violations += 1
    }
    checks.push(
      Json::object({
        "kind": Json::string("relation"),
        "status": Json::string(status),
        "op": Json::string(relation.op),
        "left": numeric_evidence(result, relation.left),
        "right": numeric_evidence(result, relation.right),
      }),
    )
  }
  (checks, violations)
}