///|
fn value_kind(value : Json) -> String {
  match value {
    Null => "null"
    True | False => "boolean"
    Number(_) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
fn[T] eval_fail(
  code : String,
  message : String,
  span : Span,
  hint? : String,
) -> T raise RuleFailure {
  raise RuleFailure(Diagnostic::new(Evaluate, code, message, span, hint?))
}

///|
fn expect_bool(value : Json, span : Span) -> Bool raise RuleFailure {
  match value {
    True => true
    False => false
    other =>
      eval_fail(
        "E001",
        "expected boolean, found \{value_kind(other)}",
        span,
        hint="Boolean operators only accept true or false.",
      )
  }
}

///|
fn expect_number(value : Json, span : Span) -> Double raise RuleFailure {
  match value {
    Number(number, ..) => number
    other =>
      eval_fail("E002", "expected number, found \{value_kind(other)}", span)
  }
}

///|
fn expect_string(value : Json, span : Span) -> String raise RuleFailure {
  match value {
    String(text) => text
    other =>
      eval_fail("E003", "expected string, found \{value_kind(other)}", span)
  }
}

///|
fn compare_values(
  left : Json,
  right : Json,
  span : Span,
) -> Int raise RuleFailure {
  match (left, right) {
    (Number(a, ..), Number(b, ..)) =>
      if a < b {
        -1
      } else if a > b {
        1
      } else {
        0
      }
    (String(a), String(b)) => a.lexical_compare(b)
    _ =>
      eval_fail(
        "E004",
        "values of type \{value_kind(left)} and \{value_kind(right)} cannot be ordered",
        span,
        hint="Ordering comparisons require two numbers or two strings.",
      )
  }
}

///|
fn evaluate_member(
  target : Json,
  name : String,
  span : Span,
) -> Json raise RuleFailure {
  match target {
    Object(fields) =>
      match fields.get(name) {
        Some(value) => value
        None =>
          eval_fail(
            "E005",
            "field `\{name}` does not exist",
            span,
            hint="Check the rule path and the input JSON shape.",
          )
      }
    other =>
      eval_fail(
        "E006",
        "cannot read field `\{name}` from \{value_kind(other)}",
        span,
      )
  }
}

///|
fn evaluate_optional_variable(
  context : Json,
  name : String,
  span : Span,
) -> Json raise RuleFailure {
  match context {
    Object(fields) => fields.get(name).unwrap_or(Json::null())
    other =>
      eval_fail(
        "E006",
        "cannot read optional field `\{name}` from \{value_kind(other)}",
        span,
      )
  }
}

///|
fn evaluate_optional_member(
  target : Json,
  name : String,
  span : Span,
) -> Json raise RuleFailure {
  match target {
    Null => Json::null()
    Object(fields) => fields.get(name).unwrap_or(Json::null())
    other =>
      eval_fail(
        "E006",
        "cannot read optional field `\{name}` from \{value_kind(other)}",
        span,
        hint="Optional access suppresses missing and null values, not type errors.",
      )
  }
}

///|
fn evaluate_index(
  target : Json,
  index : Json,
  span : Span,
) -> Json raise RuleFailure {
  match (target, index) {
    (Array(values), Number(number, ..)) => {
      let position = number.to_int()
      if Double::from_int(position) != number {
        eval_fail("E007", "array index must be an integer", span)
      }
      match values.get(position) {
        Some(value) => value
        None =>
          eval_fail("E008", "array index \{position} is out of bounds", span)
      }
    }
    (Object(fields), String(key)) =>
      match fields.get(key) {
        Some(value) => value
        None => eval_fail("E005", "field `\{key}` does not exist", span)
      }
    (Array(_), other) =>
      eval_fail(
        "E007",
        "array index must be a number, found \{value_kind(other)}",
        span,
      )
    (Object(_), other) =>
      eval_fail(
        "E009",
        "object index must be a string, found \{value_kind(other)}",
        span,
      )
    (other, _) =>
      eval_fail("E010", "cannot index a \{value_kind(other)} value", span)
  }
}

///|
fn evaluate_optional_index(
  target : Json,
  index : Json,
  span : Span,
) -> Json raise RuleFailure {
  match (target, index) {
    (Null, _) => Json::null()
    (Array(values), Number(number, ..)) => {
      let position = number.to_int()
      if Double::from_int(position) != number {
        eval_fail("E007", "array index must be an integer", span)
      }
      values.get(position).unwrap_or(Json::null())
    }
    (Object(fields), String(key)) => fields.get(key).unwrap_or(Json::null())
    (Array(_), other) =>
      eval_fail(
        "E007",
        "array index must be a number, found \{value_kind(other)}",
        span,
      )
    (Object(_), other) =>
      eval_fail(
        "E009",
        "object index must be a string, found \{value_kind(other)}",
        span,
      )
    (other, _) =>
      eval_fail(
        "E010",
        "cannot optionally index a \{value_kind(other)} value",
        span,
        hint="Optional access suppresses missing and null values, not type errors.",
      )
  }
}

///|
fn evaluate_optional_base(
  expression : Expr,
  context : Json,
  budget : EvaluationBudget,
) -> Json raise RuleFailure {
  match expression {
    Variable(name, span) => {
      budget.consume(span)
      evaluate_optional_variable(context, name, span)
    }
    other => evaluate_expr(other, context, budget)
  }
}

///|
fn require_arity(
  name : String,
  arguments : Array[Json],
  expected : Int,
  span : Span,
) -> Unit raise RuleFailure {
  if arguments.length() != expected {
    eval_fail(
      "E011",
      "function `\{name}` expects \{expected} argument(s), received \{arguments.length()}",
      span,
    )
  }
}

///|
fn expect_object(
  value : Json,
  span : Span,
) -> Map[String, Json] raise RuleFailure {
  match value {
    Object(fields) => fields
    other =>
      eval_fail("E025", "expected object, found \{value_kind(other)}", span)
  }
}

///|
fn evaluate_number_array(
  name : String,
  value : Json,
  span : Span,
) -> Array[Json] raise RuleFailure {
  match value {
    Array(values) => values
    other =>
      eval_fail(
        "E026",
        "function `\{name}` expects an array, found \{value_kind(other)}",
        span,
      )
  }
}

///|
fn evaluate_boolean_array(
  name : String,
  value : Json,
  span : Span,
) -> Array[Json] raise RuleFailure {
  match value {
    Array(values) => values
    other =>
      eval_fail(
        "E027",
        "function `\{name}` expects an array, found \{value_kind(other)}",
        span,
      )
  }
}

///|
fn evaluate_call(
  name : String,
  arguments : Array[Json],
  span : Span,
) -> Json raise RuleFailure {
  match name {
    "len" => {
      require_arity(name, arguments, 1, span)
      let length = match arguments[0] {
        String(value) => value.char_length()
        Array(values) => values.length()
        Object(fields) => fields.length()
        other =>
          eval_fail("E012", "len() does not accept \{value_kind(other)}", span)
      }
      Json::number(Double::from_int(length), repr=length.to_string())
    }
    "contains" => {
      require_arity(name, arguments, 2, span)
      let result = match (arguments[0], arguments[1]) {
        (String(text), String(part)) => text.contains(part)
        (Array(values), needle) => values.contains(needle)
        (Object(fields), String(key)) => fields.contains(key)
        (container, _) =>
          eval_fail(
            "E013",
            "contains() does not support \{value_kind(container)} with this value",
            span,
          )
      }
      Json::boolean(result)
    }
    "has" => {
      require_arity(name, arguments, 2, span)
      let fields = expect_object(arguments[0], span)
      let key = expect_string(arguments[1], span)
      Json::boolean(fields.contains(key))
    }
    "get" => {
      require_arity(name, arguments, 3, span)
      let fields = expect_object(arguments[0], span)
      let key = expect_string(arguments[1], span)
      match fields.get(key) {
        Some(value) => value
        None => arguments[2]
      }
    }
    "starts_with" => {
      require_arity(name, arguments, 2, span)
      let text = expect_string(arguments[0], span)
      let prefix = expect_string(arguments[1], span)
      Json::boolean(text.has_prefix(prefix))
    }
    "ends_with" => {
      require_arity(name, arguments, 2, span)
      let text = expect_string(arguments[0], span)
      let suffix = expect_string(arguments[1], span)
      Json::boolean(text.has_suffix(suffix))
    }
    "lower" => {
      require_arity(name, arguments, 1, span)
      Json::string(expect_string(arguments[0], span).to_lower())
    }
    "upper" => {
      require_arity(name, arguments, 1, span)
      Json::string(expect_string(arguments[0], span).to_upper())
    }
    "abs" => {
      require_arity(name, arguments, 1, span)
      Json::number(expect_number(arguments[0], span).abs())
    }
    "min" => {
      require_arity(name, arguments, 2, span)
      let left = expect_number(arguments[0], span)
      let right = expect_number(arguments[1], span)
      Json::number(if left < right { left } else { right })
    }
    "max" => {
      require_arity(name, arguments, 2, span)
      let left = expect_number(arguments[0], span)
      let right = expect_number(arguments[1], span)
      Json::number(if left > right { left } else { right })
    }
    "clamp" => {
      require_arity(name, arguments, 3, span)
      let value = expect_number(arguments[0], span)
      let minimum = expect_number(arguments[1], span)
      let maximum = expect_number(arguments[2], span)
      if minimum > maximum {
        eval_fail(
          "E028",
          "clamp() minimum must not exceed maximum",
          span,
          hint="Swap the range bounds or correct the configured values.",
        )
      }
      Json::number(
        if value < minimum {
          minimum
        } else if value > maximum {
          maximum
        } else {
          value
        },
      )
    }
    "sum" => {
      require_arity(name, arguments, 1, span)
      let values = evaluate_number_array(name, arguments[0], span)
      let mut total = 0.0
      for value in values {
        total = total + expect_number(value, span)
      }
      Json::number(total)
    }
    "matches" => {
      require_arity(name, arguments, 2, span)
      let text = expect_string(arguments[0], span)
      let pattern = expect_string(arguments[1], span)
      if pattern.char_length() > 512 {
        eval_fail(
          "E029",
          "regular expression exceeds the 512-character safety limit",
          span,
          hint="Use a shorter pattern or split the validation into multiple rules.",
        )
      }
      if text.char_length() > 65536 {
        eval_fail(
          "E030",
          "regular-expression input exceeds the 65536-character safety limit",
          span,
          hint="Validate or truncate large input before applying matches().",
        )
      }
      let regex = @string.Regex::Regex(pattern) catch {
        _ =>
          eval_fail(
            "E023",
            "invalid regular expression",
            span,
            hint="MoonRule uses MoonBit's regular expression syntax.",
          )
      }
      Json::boolean(regex.execute(text) is Some(_))
    }
    "is_date" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(is_iso_date_string(expect_string(arguments[0], span)))
    }
    "is_datetime" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(
        is_rfc3339_datetime_string(expect_string(arguments[0], span)),
      )
    }
    "is_ipv4" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(is_ipv4_string(expect_string(arguments[0], span)))
    }
    "is_email" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(is_email_string(expect_string(arguments[0], span)))
    }
    "is_uuid" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(is_uuid_string(expect_string(arguments[0], span)))
    }
    "is_semver" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(is_semver_string(expect_string(arguments[0], span)))
    }
    "exists" => {
      require_arity(name, arguments, 1, span)
      Json::boolean(arguments[0] != Json::null())
    }
    "type_of" => {
      require_arity(name, arguments, 1, span)
      Json::string(value_kind(arguments[0]))
    }
    "coalesce" => {
      if arguments.is_empty() || arguments.length() > 32 {
        eval_fail(
          "E011",
          "function `coalesce` expects 1..32 argument(s), received \{arguments.length()}",
          span,
        )
      }
      let mut result = Json::null()
      for value in arguments {
        if value != Json::null() {
          result = value
          break
        }
      }
      result
    }
    "all" => {
      require_arity(name, arguments, 1, span)
      let values = evaluate_boolean_array(name, arguments[0], span)
      let mut result = true
      for value in values {
        if !expect_bool(value, span) {
          result = false
          break
        }
      }
      Json::boolean(result)
    }
    "any" => {
      require_arity(name, arguments, 1, span)
      let values = evaluate_boolean_array(name, arguments[0], span)
      let mut result = false
      for value in values {
        if expect_bool(value, span) {
          result = true
          break
        }
      }
      Json::boolean(result)
    }
    _ =>
      eval_fail(
        "E014",
        "unknown function `\{name}`",
        span,
        hint="Inspect builtin_functions() for the complete supported function catalog.",
      )
  }
}

///|
fn evaluate_binary(
  left : Json,
  operator : BinaryOp,
  right : Json,
  span : Span,
) -> Json raise RuleFailure {
  match operator {
    Equal => Json::boolean(left == right)
    NotEqual => Json::boolean(left != right)
    Less => Json::boolean(compare_values(left, right, span) < 0)
    LessEqual => Json::boolean(compare_values(left, right, span) <= 0)
    Greater => Json::boolean(compare_values(left, right, span) > 0)
    GreaterEqual => Json::boolean(compare_values(left, right, span) >= 0)
    In => {
      let result = match right {
        Array(values) => values.contains(left)
        Object(fields) =>
          match left {
            String(key) => fields.contains(key)
            other =>
              eval_fail(
                "E015",
                "object membership requires a string key, found \{value_kind(other)}",
                span,
              )
          }
        String(text) =>
          match left {
            String(part) => text.contains(part)
            other =>
              eval_fail(
                "E016",
                "string membership requires a string, found \{value_kind(other)}",
                span,
              )
          }
        other =>
          eval_fail(
            "E017",
            "right side of `in` must be an array, object, or string; found \{value_kind(other)}",
            span,
          )
      }
      Json::boolean(result)
    }
    Add =>
      match (left, right) {
        (Number(a, ..), Number(b, ..)) => Json::number(a + b)
        (String(a), String(b)) => Json::string(a + b)
        _ => eval_fail("E018", "`+` requires two numbers or two strings", span)
      }
    Subtract =>
      Json::number(expect_number(left, span) - expect_number(right, span))
    Multiply =>
      Json::number(expect_number(left, span) * expect_number(right, span))
    Divide => {
      let divisor = expect_number(right, span)
      if divisor == 0.0 {
        eval_fail("E019", "division by zero", span)
      }
      Json::number(expect_number(left, span) / divisor)
    }
    Remainder => {
      let a = expect_number(left, span)
      let b = expect_number(right, span)
      if b == 0.0 {
        eval_fail("E020", "remainder by zero", span)
      }
      Json::number(a - (a / b).trunc() * b)
    }
    Or | And => eval_fail("E021", "internal boolean evaluation error", span)
  }
}

///|
fn evaluate_expr(
  expr : Expr,
  context : Json,
  budget : EvaluationBudget,
) -> Json raise RuleFailure {
  budget.consume(expr.span())
  match expr {
    Literal(value, _) => value
    Variable(name, span) => evaluate_member(context, name, span)
    ArrayLiteral(expressions, _) =>
      Json::array(
        expressions.map(value => evaluate_expr(value, context, budget)),
      )
    Member(target, name, span) =>
      evaluate_member(evaluate_expr(target, context, budget), name, span)
    Index(target, index, span) =>
      evaluate_index(
        evaluate_expr(target, context, budget),
        evaluate_expr(index, context, budget),
        span,
      )
    OptionalMember(target, name, span) =>
      evaluate_optional_member(
        evaluate_optional_base(target, context, budget),
        name,
        span,
      )
    OptionalIndex(target, index, span) => {
      let target_value = evaluate_optional_base(target, context, budget)
      if target_value == Json::null() {
        Json::null()
      } else {
        evaluate_optional_index(
          target_value,
          evaluate_expr(index, context, budget),
          span,
        )
      }
    }
    Call(name, arguments, span) =>
      evaluate_call(
        name,
        arguments.map(argument => evaluate_expr(argument, context, budget)),
        span,
      )
    Unary(operator, operand, span) => {
      let value = evaluate_expr(operand, context, budget)
      match operator {
        Not => Json::boolean(!expect_bool(value, span))
        Negate => Json::number(-expect_number(value, span))
      }
    }
    Binary(left, Or, right, _) => {
      let left_value = expect_bool(
        evaluate_expr(left, context, budget),
        left.span(),
      )
      if left_value {
        Json::boolean(true)
      } else {
        Json::boolean(
          expect_bool(evaluate_expr(right, context, budget), right.span()),
        )
      }
    }
    Binary(left, And, right, _) => {
      let left_value = expect_bool(
        evaluate_expr(left, context, budget),
        left.span(),
      )
      if !left_value {
        Json::boolean(false)
      } else {
        Json::boolean(
          expect_bool(evaluate_expr(right, context, budget), right.span()),
        )
      }
    }
    Binary(left, operator, right, span) =>
      evaluate_binary(
        evaluate_expr(left, context, budget),
        operator,
        evaluate_expr(right, context, budget),
        span,
      )
  }
}