// Expr — filter expression tree with to_json / from_json / eval.

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

///|
pub suberror QueryError {
  EvalError(String)
} derive(ToJson)

// ---------------------------------------------------------------------------
// Value — runtime values
// ---------------------------------------------------------------------------

///|
pub(all) enum Value {
  Null
  Bool(Bool)
  Int(Int64)
  Float(Double)
  String(String)
} derive(Debug, Eq)

// ---------------------------------------------------------------------------
// ToValue trait — convert native MoonBit types into Value
// ---------------------------------------------------------------------------

///|
pub trait ToValue {
  fn to_value(Self) -> Value
}

///|
pub impl ToValue for Value with fn to_value(self) -> Value {
  self
}

///|
pub impl ToValue for Int with fn to_value(self) -> Value {
  Value::Int(self.to_int64())
}

///|
pub impl ToValue for Int64 with fn to_value(self) -> Value {
  Value::Int(self)
}

///|
pub impl ToValue for String with fn to_value(self) -> Value {
  Value::String(self)
}

///|
pub impl ToValue for Bool with fn to_value(self) -> Value {
  Value::Bool(self)
}

///|
pub impl ToValue for Double with fn to_value(self) -> Value {
  Value::Float(self)
}

// ---------------------------------------------------------------------------
// CmpOp / StrKind — comparison & string-match operators
// ---------------------------------------------------------------------------

///|
pub(all) enum CmpOp {
  Eq
  Ne
  Gt
  Lt
  Ge
  Le
} derive(Debug, Eq)

///|
pub(all) enum StrKind {
  Contains
  StartsWith
  EndsWith
} derive(Debug, Eq)

// ---------------------------------------------------------------------------
// Expr — filter expression tree
// ---------------------------------------------------------------------------

///|
pub(all) enum Expr {
  Lit(Value) // literal value
  Field(String) // field reference
  Compare(String, CmpOp, Value) // field OP value
  Between(String, Value, Value) // field BETWEEN lo AND hi
  StrMatch(String, StrKind, String) // field STR_KIND pattern
  And(Array[Expr]) // logical AND
  Or(Array[Expr]) // logical OR
  Not(Expr) // logical NOT
  Empty // matches all
} derive(Debug, Eq)

// ---------------------------------------------------------------------------
// Value :: ToJson / FromJson
// ---------------------------------------------------------------------------

///|
pub impl ToJson for Value with fn to_json(self : Value) -> Json {
  match self {
    Value::Null => Json::null()
    Value::Bool(b) => Json::boolean(b)
    Value::Int(n) => Json::number(n.to_double())
    Value::Float(f) => Json::number(f)
    Value::String(s) => Json::string(s)
  }
}

///|
pub impl @json.FromJson for Value with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> Value raise @json.JsonDecodeError {
  match json {
    Json::Null => Value::Null
    Json::True => Value::Bool(true)
    Json::False => Value::Bool(false)
    Json::Number(n, ..) =>
      if n == n.to_int().to_double() {
        Value::Int(n.to_int64())
      } else {
        Value::Float(n)
      }
    Json::String(s) => Value::String(s)
    _ =>
      raise @json.JsonDecodeError(
        (path, "unsupported JSON literal: \{json.stringify()}"),
      )
  }
}

// ---------------------------------------------------------------------------
// Expr :: ToJson / FromJson
// ---------------------------------------------------------------------------

///|
pub impl ToJson for Expr with fn to_json(self : Expr) -> Json {
  match self {
    Expr::Empty => Json::empty_object()
    Expr::Lit(_) => Json::empty_object()
    Expr::Field(_) => Json::empty_object()
    Expr::Compare(field, CmpOp::Eq, val) => {
      let map = Map([], capacity=1)
      map.set(field, val.to_json())
      Json::object(map)
    }
    Expr::Compare(field, op, val) => {
      let map = Map([], capacity=1)
      let ops = Map([], capacity=1)
      ops.set(op_to_name(op), val.to_json())
      map.set(field, Json::object(ops))
      Json::object(map)
    }
    Expr::Between(field, lo, hi) => {
      let map = Map([], capacity=1)
      let ops = Map([], capacity=1)
      ops.set("between", Json::array([lo.to_json(), hi.to_json()]))
      map.set(field, Json::object(ops))
      Json::object(map)
    }
    Expr::StrMatch(field, kind, pattern) => {
      let map = Map([], capacity=1)
      let ops = Map([], capacity=1)
      ops.set(strkind_to_name(kind), Json::string(pattern))
      map.set(field, Json::object(ops))
      Json::object(map)
    }
    Expr::And(sub_exprs) => and_exprs_to_json(sub_exprs)
    Expr::Or(sub_exprs) => {
      let arr = sub_exprs.map(fn(e) { e.to_json() })
      let map = Map([], capacity=1)
      map.set("or", Json::array(arr))
      Json::object(map)
    }
    Expr::Not(sub) => {
      let map = Map([], capacity=1)
      map.set("not", sub.to_json())
      Json::object(map)
    }
  }
}

///|
pub impl @json.FromJson for Expr with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> Expr raise @json.JsonDecodeError {
  match json {
    Json::Object(map) => parse_filter_object(map, path)
    _ =>
      raise @json.JsonDecodeError(
        (path, "filter must be a JSON object, got \{json.stringify()}"),
      )
  }
}

// ---------------------------------------------------------------------------
// Expr :: eval
// ---------------------------------------------------------------------------

///|
/// Evaluate the expression against a concrete map of field → value bindings.
pub fn Expr::eval(
  self : Expr,
  values : Map[String, Value],
) -> Bool raise QueryError {
  match self {
    Expr::Empty => true
    Expr::Lit(Value::Bool(b)) => b
    Expr::Lit(_) => raise EvalError("literal is not a boolean")
    Expr::Field(_) => raise EvalError("unresolved field")
    Expr::Compare(field, op, rhs) => {
      let lhs = match values.get(field) {
        Some(v) => v
        None => raise EvalError("missing value for field '\{field}'")
      }
      lhs.compare(op, rhs)
    }
    Expr::Between(field, lo, hi) => {
      let v = match values.get(field) {
        Some(x) => x
        None => raise EvalError("missing value for field '\{field}'")
      }
      v.compare(CmpOp::Ge, lo) && v.compare(CmpOp::Le, hi)
    }
    Expr::StrMatch(field, kind, pattern) => {
      let v = match values.get(field) {
        Some(Value::String(s)) => s
        Some(_) => raise EvalError("field '\{field}' is not a string")
        None => raise EvalError("missing value for field '\{field}'")
      }
      match kind {
        StrKind::Contains => v.contains(pattern)
        StrKind::StartsWith => v.has_prefix(pattern)
        StrKind::EndsWith => v.has_suffix(pattern)
      }
    }
    Expr::And(sub_exprs) => {
      for e in sub_exprs {
        if !e.eval(values) {
          return false
        }
      }
      true
    }
    Expr::Or(sub_exprs) => {
      for e in sub_exprs {
        if e.eval(values) {
          return true
        }
      }
      false
    }
    Expr::Not(sub) => !sub.eval(values)
  }
}

// ============================================================================
// Private helpers
// ============================================================================

// ---------------------------------------------------------------------------
// CmpOp ↔ operator name
// ---------------------------------------------------------------------------

///|
fn op_to_name(op : CmpOp) -> String {
  match op {
    CmpOp::Eq => "eq"
    CmpOp::Ne => "ne"
    CmpOp::Gt => "gt"
    CmpOp::Lt => "lt"
    CmpOp::Ge => "ge"
    CmpOp::Le => "le"
  }
}

// ---------------------------------------------------------------------------
// StrKind ↔ name
// ---------------------------------------------------------------------------

///|
fn strkind_to_name(kind : StrKind) -> String {
  match kind {
    StrKind::Contains => "contains"
    StrKind::StartsWith => "starts_with"
    StrKind::EndsWith => "ends_with"
  }
}

// ---------------------------------------------------------------------------
// Expr :: from_json helpers
// ---------------------------------------------------------------------------

///|
fn parse_filter_object(
  map : Map[String, Json],
  path : @json.JsonPath,
) -> Expr raise @json.JsonDecodeError {
  let exprs : Array[Expr] = []
  for key, value in map {
    match key {
      "or" =>
        match value {
          Json::Array(arr) => {
            let branches : Array[Expr] = []
            for v in arr {
              branches.push(@json.from_json(v))
            }
            exprs.push(Expr::Or(branches))
          }
          _ =>
            raise @json.JsonDecodeError(
              (path.add_key(key), "'or' value must be an array"),
            )
        }
      "and" =>
        match value {
          Json::Array(arr) => {
            let conjuncts : Array[Expr] = []
            for v in arr {
              conjuncts.push(@json.from_json(v))
            }
            exprs.push(Expr::And(conjuncts))
          }
          _ =>
            raise @json.JsonDecodeError(
              (path.add_key(key), "'and' value must be an array"),
            )
        }
      _ => exprs.push(parse_field_value(key, value, path.add_key(key)))
    }
  }
  match exprs.length() {
    0 => Expr::Empty
    1 => exprs[0]
    _ => Expr::And(exprs)
  }
}

///|
fn parse_field_value(
  field : String,
  value : Json,
  path : @json.JsonPath,
) -> Expr raise @json.JsonDecodeError {
  match value {
    Json::Object(ops_map) => {
      let conds : Array[Expr] = []
      for op_name, op_val in ops_map {
        conds.push(parse_field_operator(field, op_name, op_val, path))
      }
      match conds.length() {
        0 => Expr::Empty
        1 => conds[0]
        _ => Expr::And(conds)
      }
    }
    plain => Expr::Compare(field, CmpOp::Eq, @json.from_json(plain))
  }
}

///|
fn parse_field_operator(
  field : String,
  op_name : String,
  op_val : Json,
  path : @json.JsonPath,
) -> Expr raise @json.JsonDecodeError {
  match op_name {
    "eq" => Expr::Compare(field, CmpOp::Eq, @json.from_json(op_val))
    "ne" => Expr::Compare(field, CmpOp::Ne, @json.from_json(op_val))
    "gt" => Expr::Compare(field, CmpOp::Gt, @json.from_json(op_val))
    "lt" => Expr::Compare(field, CmpOp::Lt, @json.from_json(op_val))
    "ge" => Expr::Compare(field, CmpOp::Ge, @json.from_json(op_val))
    "le" => Expr::Compare(field, CmpOp::Le, @json.from_json(op_val))
    "contains" =>
      match op_val {
        Json::String(s) => Expr::StrMatch(field, StrKind::Contains, s)
        _ =>
          raise @json.JsonDecodeError(
            (path, "'contains' requires a string value"),
          )
      }
    "starts_with" =>
      match op_val {
        Json::String(s) => Expr::StrMatch(field, StrKind::StartsWith, s)
        _ =>
          raise @json.JsonDecodeError(
            (path, "'starts_with' requires a string value"),
          )
      }
    "ends_with" =>
      match op_val {
        Json::String(s) => Expr::StrMatch(field, StrKind::EndsWith, s)
        _ =>
          raise @json.JsonDecodeError(
            (path, "'ends_with' requires a string value"),
          )
      }
    "between" =>
      match op_val {
        Json::Array(arr) if arr.length() == 2 =>
          Expr::Between(field, @json.from_json(arr[0]), @json.from_json(arr[1]))
        _ =>
          raise @json.JsonDecodeError(
            (path, "'between' requires an array of exactly 2 values"),
          )
      }
    _ => raise @json.JsonDecodeError((path, "unknown operator '\{op_name}'"))
  }
}

// ---------------------------------------------------------------------------
// Expr :: to_json helpers
// ---------------------------------------------------------------------------

///|
fn and_exprs_to_json(sub_exprs : Array[Expr]) -> Json {
  let map = Map([])
  for e in sub_exprs {
    match e {
      Expr::Compare(field, CmpOp::Eq, val) =>
        merge_field_value(map, field, "eq", val.to_json())
      Expr::Compare(field, op, val) =>
        merge_field_value(map, field, op_to_name(op), val.to_json())
      Expr::Between(field, lo, hi) => {
        let ops = Map([], capacity=1)
        ops.set("between", Json::array([lo.to_json(), hi.to_json()]))
        map.set(field, Json::object(ops))
      }
      Expr::StrMatch(field, kind, pat) =>
        merge_field_value(map, field, strkind_to_name(kind), Json::string(pat))
      Expr::Or(sub) =>
        map.set("or", Json::array(sub.map(fn(s) { s.to_json() })))
      Expr::And(inner) =>
        match Expr::And(inner).to_json() {
          Json::Object(inner_map) =>
            for k, v in inner_map {
              merge_field_json(map, k, v)
            }
          _ => ()
        }
      Expr::Not(_) => map.set("not", e.to_json())
      _ => ()
    }
  }
  collapse_between_in_map(map)
  Json::object(map)
}

///|
/// Walk a filter map and collapse any ge+le / gt+lt pairs into `between`.
fn collapse_between_in_map(map : Map[String, Json]) -> Unit {
  for _, val in map {
    match val {
      Json::Object(ops) => {
        match (ops.get("ge"), ops.get("le")) {
          (Some(lo), Some(hi)) => {
            ops.remove("ge")
            ops.remove("le")
            ops.set("between", Json::array([lo, hi]))
          }
          _ => ()
        }
        match (ops.get("gt"), ops.get("lt")) {
          (Some(lo), Some(hi)) => {
            ops.remove("gt")
            ops.remove("lt")
            ops.set("between", Json::array([lo, hi]))
          }
          _ => ()
        }
        match (ops.get("gt"), ops.get("le")) {
          (Some(lo), Some(hi)) => {
            ops.remove("gt")
            ops.remove("le")
            ops.set("between", Json::array([lo, hi]))
          }
          _ => ()
        }
        match (ops.get("ge"), ops.get("lt")) {
          (Some(lo), Some(hi)) => {
            ops.remove("ge")
            ops.remove("lt")
            ops.set("between", Json::array([lo, hi]))
          }
          _ => ()
        }
      }
      _ => ()
    }
  }
}

///|
/// Merge a single (op_name, json_val) into the map for a field.
fn merge_field_value(
  map : Map[String, Json],
  field : String,
  op_name : String,
  val : Json,
) -> Unit {
  if op_name == "eq" {
    merge_field_json(map, field, val)
  } else {
    let ops = Map([], capacity=1)
    ops.set(op_name, val)
    merge_field_json(map, field, Json::object(ops))
  }
}

///|
fn merge_field_json(
  map : Map[String, Json],
  field : String,
  val : Json,
) -> Unit {
  match map.get(field) {
    None => map.set(field, val)
    Some(Json::Object(existing)) =>
      match val {
        Json::Object(v_ops) =>
          for op_k, op_v in v_ops {
            existing.set(op_k, op_v)
          }
        plain => existing.set("eq", plain)
      }
    Some(plain) => {
      let ops = Map([], capacity=2)
      ops.set("eq", plain)
      match val {
        Json::Object(v_ops) =>
          for op_k, op_v in v_ops {
            ops.set(op_k, op_v)
          }
        other => ops.set("eq", other)
      }
      map.set(field, Json::object(ops))
    }
  }
}

// ---------------------------------------------------------------------------
// Value comparison helpers (for eval)
// ---------------------------------------------------------------------------

///|
fn Value::compare(
  self : Value,
  op : CmpOp,
  other : Value,
) -> Bool raise QueryError {
  let ordering = value_ord(self, other)
  match op {
    CmpOp::Eq => ordering == 0
    CmpOp::Ne => ordering != 0
    CmpOp::Gt => ordering > 0
    CmpOp::Lt => ordering < 0
    CmpOp::Ge => ordering >= 0
    CmpOp::Le => ordering <= 0
  }
}

///|
fn value_ord(a : Value, b : Value) -> Int raise QueryError {
  match (a, b) {
    (Value::Int(x), Value::Int(y)) => x.compare(y)
    (Value::Float(x), Value::Float(y)) => x.compare(y)
    (Value::Int(x), Value::Float(y)) => x.to_double().compare(y)
    (Value::Float(x), Value::Int(y)) => x.compare(y.to_double())
    (Value::String(x), Value::String(y)) => x.compare(y)
    (Value::Bool(x), Value::Bool(y)) =>
      if x == y {
        0
      } else if x {
        1
      } else {
        -1
      }
    (Value::Null, Value::Null) => 0
    _ => raise EvalError("type mismatch: cannot compare the two values")
  }
}