//! JSON query parser — converts ES-compatible JSON to [`QueryAST`].
//!
//! Supports both the full object form and shorthand forms:
//! - `{"term": {"status": {"value": "active"}}}` (full)
//! - `{"term": {"status": "active"}}` (shorthand)
//! - `{"match": {"title": {"query": "search engine"}}}` (full)
//! - `{"match": {"title": "search engine"}}` (shorthand)
//!
//! See [[query-dsl]] and [[architecture-query-execution#Step 5]].

use luci_core::{LuciError, Result};
use serde_json::Value;

use super::ast::{
    QueryAST, ScoreFunction, FunctionScoreMode, FunctionBoostMode, FieldValueModifier,
    SpatialRelation, GeoShapeValue,
};

/// Parse a JSON query into a QueryAST.
///
/// Accepts either `{"query": {...}}` wrapper or a bare query object.
pub fn parse_query(json: &Value) -> Result<QueryAST> {
    // Unwrap optional "query" wrapper
    let query_obj = if let Some(q) = json.get("query") {
        q
    } else {
        json
    };

    parse_query_node(query_obj)
}

fn parse_query_node(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("query must be a JSON object".into())
    })?;

    if obj.is_empty() {
        return Err(LuciError::InvalidQuery("empty query object".into()));
    }

    // Each query type is identified by its key
    if let Some(v) = obj.get("term") {
        return parse_term_query(v);
    }
    if let Some(v) = obj.get("terms") {
        return parse_terms_query(v);
    }
    if let Some(v) = obj.get("match") {
        return parse_match_query(v);
    }
    if let Some(v) = obj.get("match_phrase") {
        return parse_match_phrase_query(v);
    }
    if let Some(v) = obj.get("match_bool_prefix") {
        return parse_match_bool_prefix_query(v);
    }
    if let Some(v) = obj.get("multi_match") {
        return parse_multi_match_query(v);
    }
    if let Some(v) = obj.get("bool") {
        return parse_bool_query(v);
    }
    if let Some(v) = obj.get("dis_max") {
        return parse_dis_max_query(v);
    }
    if let Some(v) = obj.get("exists") {
        return parse_exists_query(v);
    }
    if let Some(v) = obj.get("prefix") {
        return parse_prefix_query(v);
    }
    if let Some(v) = obj.get("script_score") {
        return parse_script_score_query(v);
    }
    if let Some(v) = obj.get("function_score") {
        return parse_function_score_query(v);
    }
    if let Some(v) = obj.get("boosting") {
        return parse_boosting_query(v);
    }
    if let Some(v) = obj.get("fuzzy") {
        return parse_fuzzy_query(v);
    }
    if let Some(v) = obj.get("regexp") {
        return parse_regexp_query(v);
    }
    if let Some(v) = obj.get("wildcard") {
        return parse_wildcard_query(v);
    }
    if let Some(v) = obj.get("range") {
        return parse_range_query(v);
    }
    if let Some(v) = obj.get("span_term") {
        return parse_span_term_query(v);
    }
    if let Some(v) = obj.get("span_near") {
        return parse_span_near_query(v);
    }
    if let Some(v) = obj.get("span_not") {
        return parse_span_not_query(v);
    }
    if let Some(v) = obj.get("span_first") {
        return parse_span_first_query(v);
    }
    if let Some(v) = obj.get("constant_score") {
        return parse_constant_score_query(v);
    }
    if let Some(v) = obj.get("nested") {
        return parse_nested_query(v);
    }
    if let Some(v) = obj.get("geo_distance") {
        return parse_geo_distance_query(v);
    }
    if let Some(v) = obj.get("geo_bounding_box") {
        return parse_geo_bbox_query(v);
    }
    if let Some(v) = obj.get("geo_shape") {
        return parse_geo_shape_query(v);
    }
    if obj.contains_key("match_all") {
        return Ok(QueryAST::MatchAll);
    }
    if obj.contains_key("match_none") {
        return Ok(QueryAST::MatchNone);
    }

    let key = obj.keys().next().unwrap();
    Err(LuciError::InvalidQuery(format!(
        "unknown query type: {key}"
    )))
}

/// Wrap a query AST with a Boost if boost is Some and != 1.0.
fn maybe_boost(ast: QueryAST, boost: Option<f64>) -> QueryAST {
    match boost {
        Some(b) if (b - 1.0).abs() > f64::EPSILON => QueryAST::Boost {
            query: Box::new(ast),
            boost: b as f32,
        },
        _ => ast,
    }
}

/// Extract boost from a field-level object (e.g., {"value": "x", "boost": 2.0}).
fn extract_boost(obj: &serde_json::Map<String, Value>) -> Option<f64> {
    obj.get("boost").and_then(|v| v.as_f64())
}

/// Parse `{"field": "value"}` or `{"field": {"value": "..."}}`.
fn parse_term_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("term query must be an object".into()))?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("term query: missing field".into())
    })?;

    let (value, boost) = match field_val {
        Value::String(s) => (s.clone(), None),
        Value::Number(n) => (n.to_string(), None),
        Value::Bool(b) => (b.to_string(), None),
        Value::Object(inner) => {
            let v = inner
                .get("value")
                .and_then(|v| match v {
                    Value::String(s) => Some(s.clone()),
                    Value::Number(n) => Some(n.to_string()),
                    Value::Bool(b) => Some(b.to_string()),
                    _ => None,
                })
                .ok_or_else(|| {
                    LuciError::InvalidQuery("term query: missing 'value' field".into())
                })?;
            (v, extract_boost(inner))
        }
        _ => {
            return Err(LuciError::InvalidQuery(
                "term query: invalid value type".into(),
            ))
        }
    };

    Ok(maybe_boost(QueryAST::Term {
        field: field.clone(),
        value,
    }, boost))
}

fn parse_terms_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("terms query must be an object".into()))?;

    let (field, values_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("terms query: missing field".into())
    })?;

    let arr = values_val
        .as_array()
        .ok_or_else(|| LuciError::InvalidQuery("terms query: values must be an array".into()))?;

    let values: Vec<String> = arr
        .iter()
        .map(|v| match v {
            Value::String(s) => Ok(s.clone()),
            Value::Number(n) => Ok(n.to_string()),
            Value::Bool(b) => Ok(b.to_string()),
            _ => Err(LuciError::InvalidQuery(
                "terms query: invalid value type in array".into(),
            )),
        })
        .collect::<Result<_>>()?;

    Ok(QueryAST::Terms {
        field: field.clone(),
        values,
    })
}

/// Parse `{"field": "query"}` or `{"field": {"query": "...", "analyzer": "..."}}`.
fn parse_match_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("match query must be an object".into()))?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("match query: missing field".into())
    })?;

    let (query, analyzer, boost) = match field_val {
        Value::String(s) => (s.clone(), None, None),
        Value::Object(inner) => {
            let q = inner
                .get("query")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    LuciError::InvalidQuery("match query: missing 'query' field".into())
                })?
                .to_string();
            let a = inner.get("analyzer").and_then(|v| v.as_str()).map(String::from);
            (q, a, extract_boost(inner))
        }
        _ => {
            return Err(LuciError::InvalidQuery(
                "match query: invalid field value".into(),
            ))
        }
    };

    Ok(maybe_boost(QueryAST::Match {
        field: field.clone(),
        query,
        analyzer,
    }, boost))
}

fn parse_match_phrase_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("match_phrase query must be an object".into()))?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("match_phrase query: missing field".into())
    })?;

    let (query, analyzer) = match field_val {
        Value::String(s) => (s.clone(), None),
        Value::Object(inner) => {
            let q = inner
                .get("query")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    LuciError::InvalidQuery("match_phrase: missing 'query'".into())
                })?
                .to_string();
            let a = inner.get("analyzer").and_then(|v| v.as_str()).map(String::from);
            (q, a)
        }
        _ => {
            return Err(LuciError::InvalidQuery(
                "match_phrase: invalid field value".into(),
            ))
        }
    };

    Ok(QueryAST::MatchPhrase {
        field: field.clone(),
        query,
        analyzer,
    })
}

fn parse_match_bool_prefix_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("match_bool_prefix must be an object".into())
    })?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("match_bool_prefix: missing field".into())
    })?;

    let (query, analyzer) = match field_val {
        Value::String(s) => (s.clone(), None),
        Value::Object(inner) => {
            let q = inner.get("query").and_then(|v| v.as_str())
                .ok_or_else(|| LuciError::InvalidQuery("match_bool_prefix: missing 'query'".into()))?
                .to_string();
            let a = inner.get("analyzer").and_then(|v| v.as_str()).map(String::from);
            (q, a)
        }
        _ => return Err(LuciError::InvalidQuery("match_bool_prefix: invalid field value".into())),
    };

    Ok(QueryAST::MatchBoolPrefix {
        field: field.clone(),
        query,
        analyzer,
    })
}

fn parse_bool_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("bool query must be an object".into()))?;

    let parse_clauses = |key: &str| -> Result<Vec<QueryAST>> {
        match obj.get(key) {
            None => Ok(Vec::new()),
            Some(Value::Array(arr)) => arr.iter().map(parse_query_node).collect(),
            Some(single) => Ok(vec![parse_query_node(single)?]),
        }
    };

    let boost = extract_boost(obj);
    Ok(maybe_boost(QueryAST::Bool {
        must: parse_clauses("must")?,
        should: parse_clauses("should")?,
        must_not: parse_clauses("must_not")?,
        filter: parse_clauses("filter")?,
    }, boost))
}

fn parse_dis_max_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("dis_max must be an object".into())
    })?;

    let queries = match obj.get("queries") {
        Some(Value::Array(arr)) => arr.iter().map(parse_query_node).collect::<Result<Vec<_>>>()?,
        _ => return Err(LuciError::InvalidQuery("dis_max: missing 'queries' array".into())),
    };

    let tie_breaker = obj.get("tie_breaker")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0) as f32;

    let boost = extract_boost(obj);
    Ok(maybe_boost(QueryAST::DisMax { queries, tie_breaker }, boost))
}

fn parse_exists_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("exists query must be an object".into()))?;

    let field = obj
        .get("field")
        .and_then(|v| v.as_str())
        .ok_or_else(|| LuciError::InvalidQuery("exists query: missing 'field'".into()))?;

    Ok(QueryAST::Exists {
        field: field.to_string(),
    })
}

fn parse_prefix_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("prefix query must be an object".into()))?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("prefix query: missing field".into())
    })?;

    let value = match field_val {
        Value::String(s) => s.clone(),
        Value::Object(inner) => inner
            .get("value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| LuciError::InvalidQuery("prefix: missing 'value'".into()))?
            .to_string(),
        _ => {
            return Err(LuciError::InvalidQuery(
                "prefix query: invalid value type".into(),
            ))
        }
    };

    Ok(QueryAST::Prefix {
        field: field.clone(),
        value,
    })
}

fn parse_range_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("range query must be an object".into()))?;

    let (field, field_val) = obj.iter().next().ok_or_else(|| {
        LuciError::InvalidQuery("range query: missing field".into())
    })?;

    let range_obj = field_val.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("range query: field value must be an object".into())
    })?;

    let gte = range_obj.get("gte").and_then(|v| v.as_f64());
    let gt = range_obj.get("gt").and_then(|v| v.as_f64());
    let lte = range_obj.get("lte").and_then(|v| v.as_f64());
    let lt = range_obj.get("lt").and_then(|v| v.as_f64());

    Ok(QueryAST::Range {
        field: field.clone(),
        gte,
        gt,
        lte,
        lt,
    })
}

fn parse_script_score_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("script_score must be an object".into()))?;
    let query = obj.get("query")
        .map(|q| parse_query_node(q))
        .unwrap_or_else(|| Ok(QueryAST::MatchAll))?;
    let script_obj = obj.get("script")
        .and_then(|v| v.as_object())
        .ok_or_else(|| LuciError::InvalidQuery("script_score: missing 'script' object".into()))?;
    let source = script_obj.get("source")
        .and_then(|v| v.as_str())
        .ok_or_else(|| LuciError::InvalidQuery("script_score: missing 'source'".into()))?
        .to_string();
    let mut params = std::collections::HashMap::new();
    if let Some(p) = script_obj.get("params").and_then(|v| v.as_object()) {
        for (k, v) in p {
            if let Some(n) = v.as_f64() {
                params.insert(k.clone(), n);
            }
        }
    }
    Ok(QueryAST::ScriptScore { query: Box::new(query), script: source, params })
}

fn parse_function_score_query(node: &Value) -> Result<QueryAST> {

    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("function_score must be an object".into()))?;

    let query = obj.get("query")
        .map(|q| parse_query_node(q))
        .unwrap_or_else(|| Ok(QueryAST::MatchAll))?;

    let mut functions = Vec::new();

    // Parse "functions" array
    if let Some(Value::Array(funcs)) = obj.get("functions") {
        for func_obj in funcs {
            if let Some(f) = parse_score_function(func_obj)? {
                functions.push(f);
            }
        }
    }

    // Parse top-level shorthand functions (e.g., "field_value_factor" directly)
    if let Some(f) = parse_score_function(node)? {
        functions.push(f);
    }

    // Parse weight at top level
    if let Some(w) = obj.get("weight").and_then(|v| v.as_f64()) {
        functions.push(ScoreFunction::Weight(w as f32));
    }

    let score_mode = match obj.get("score_mode").and_then(|v| v.as_str()) {
        Some("multiply") | None => FunctionScoreMode::Multiply,
        Some("sum") => FunctionScoreMode::Sum,
        Some("avg") => FunctionScoreMode::Avg,
        Some("first") => FunctionScoreMode::First,
        Some("max") => FunctionScoreMode::Max,
        Some("min") => FunctionScoreMode::Min,
        _ => FunctionScoreMode::Multiply,
    };

    let boost_mode = match obj.get("boost_mode").and_then(|v| v.as_str()) {
        Some("multiply") | None => FunctionBoostMode::Multiply,
        Some("replace") => FunctionBoostMode::Replace,
        Some("sum") => FunctionBoostMode::Sum,
        Some("avg") => FunctionBoostMode::Avg,
        Some("max") => FunctionBoostMode::Max,
        Some("min") => FunctionBoostMode::Min,
        _ => FunctionBoostMode::Multiply,
    };

    Ok(QueryAST::FunctionScore {
        query: Box::new(query),
        functions,
        score_mode,
        boost_mode,
    })
}

fn parse_score_function(node: &Value) -> Result<Option<ScoreFunction>> {

    let obj = match node.as_object() {
        Some(o) => o,
        None => return Ok(None),
    };

    if let Some(fvf) = obj.get("field_value_factor") {
        let fvf_obj = fvf.as_object()
            .ok_or_else(|| LuciError::InvalidQuery("field_value_factor must be object".into()))?;
        let field = fvf_obj.get("field").and_then(|v| v.as_str())
            .ok_or_else(|| LuciError::InvalidQuery("field_value_factor: missing 'field'".into()))?
            .to_string();
        let factor = fvf_obj.get("factor").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
        let modifier = match fvf_obj.get("modifier").and_then(|v| v.as_str()) {
            Some("log1p") => FieldValueModifier::Log1p,
            Some("log2p") => FieldValueModifier::Log2p,
            Some("ln1p") => FieldValueModifier::Ln1p,
            Some("ln2p") => FieldValueModifier::Ln2p,
            Some("sqrt") => FieldValueModifier::Sqrt,
            Some("square") => FieldValueModifier::Square,
            Some("reciprocal") => FieldValueModifier::Reciprocal,
            _ => FieldValueModifier::None,
        };
        let missing = fvf_obj.get("missing").and_then(|v| v.as_f64()).unwrap_or(1.0);
        return Ok(Some(ScoreFunction::FieldValueFactor { field, factor, modifier, missing }));
    }

    if let Some(rs) = obj.get("random_score") {
        let seed = rs.as_object()
            .and_then(|o| o.get("seed"))
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        return Ok(Some(ScoreFunction::RandomScore { seed }));
    }

    if let Some(w) = obj.get("weight").and_then(|v| v.as_f64()) {
        return Ok(Some(ScoreFunction::Weight(w as f32)));
    }

    Ok(None)
}

fn parse_boosting_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("boosting must be an object".into()))?;
    let positive = obj.get("positive")
        .ok_or_else(|| LuciError::InvalidQuery("boosting: missing 'positive'".into()))?;
    let negative = obj.get("negative")
        .ok_or_else(|| LuciError::InvalidQuery("boosting: missing 'negative'".into()))?;
    let negative_boost = obj.get("negative_boost")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.5) as f32;
    Ok(QueryAST::Boosting {
        positive: Box::new(parse_query_node(positive)?),
        negative: Box::new(parse_query_node(negative)?),
        negative_boost,
    })
}

fn parse_fuzzy_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("fuzzy query must be an object".into()))?;
    let (field, field_val) = obj.iter().next()
        .ok_or_else(|| LuciError::InvalidQuery("fuzzy query: missing field".into()))?;
    let (value, fuzziness) = match field_val {
        Value::String(s) => (s.clone(), 2), // default fuzziness=2
        Value::Object(inner) => {
            let v = inner.get("value").and_then(|v| v.as_str())
                .ok_or_else(|| LuciError::InvalidQuery("fuzzy: missing 'value'".into()))?
                .to_string();
            let f = inner.get("fuzziness").and_then(|v| v.as_u64()).unwrap_or(2) as u32;
            (v, f)
        }
        other => (other.to_string(), 2),
    };
    Ok(QueryAST::Fuzzy { field: field.clone(), value, fuzziness })
}

fn parse_regexp_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("regexp query must be an object".into()))?;
    let (field, field_val) = obj.iter().next()
        .ok_or_else(|| LuciError::InvalidQuery("regexp query: missing field".into()))?;
    let value = match field_val {
        Value::String(s) => s.clone(),
        Value::Object(inner) => inner.get("value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| LuciError::InvalidQuery("regexp: missing 'value'".into()))?
            .to_string(),
        other => other.to_string(),
    };
    Ok(QueryAST::Regexp { field: field.clone(), value })
}

fn parse_wildcard_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("wildcard query must be an object".into()))?;
    let (field, field_val) = obj.iter().next()
        .ok_or_else(|| LuciError::InvalidQuery("wildcard query: missing field".into()))?;
    let value = match field_val {
        Value::String(s) => s.clone(),
        Value::Object(inner) => inner.get("value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| LuciError::InvalidQuery("wildcard: missing 'value'".into()))?
            .to_string(),
        other => other.to_string(),
    };
    Ok(QueryAST::Wildcard { field: field.clone(), value })
}

fn parse_multi_match_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("multi_match must be an object".into()))?;
    let query = obj.get("query").and_then(|v| v.as_str())
        .ok_or_else(|| LuciError::InvalidQuery("multi_match: missing 'query'".into()))?
        .to_string();
    let fields = obj.get("fields").and_then(|v| v.as_array())
        .ok_or_else(|| LuciError::InvalidQuery("multi_match: missing 'fields' array".into()))?
        .iter()
        .filter_map(|v| v.as_str().map(String::from))
        .collect();
    let analyzer = obj.get("analyzer").and_then(|v| v.as_str()).map(String::from);
    Ok(QueryAST::MultiMatch { fields, query, analyzer })
}

fn parse_span_term_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("span_term must be an object".into()))?;
    let (field, field_val) = obj.iter().next()
        .ok_or_else(|| LuciError::InvalidQuery("span_term: missing field".into()))?;
    let value = match field_val {
        Value::String(s) => s.clone(),
        Value::Object(inner) => inner.get("value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| LuciError::InvalidQuery("span_term: missing 'value'".into()))?
            .to_string(),
        other => other.to_string(),
    };
    Ok(QueryAST::SpanTerm { field: field.clone(), value })
}

fn parse_span_near_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("span_near must be an object".into()))?;
    let clauses = obj.get("clauses").and_then(|v| v.as_array())
        .ok_or_else(|| LuciError::InvalidQuery("span_near: missing 'clauses'".into()))?;

    let mut field: Option<String> = None;
    let mut terms = Vec::new();
    for clause in clauses {
        let ast = parse_query_node(clause)?;
        match ast {
            QueryAST::SpanTerm { field: f, value } => {
                if let Some(ref existing) = field {
                    if *existing != f {
                        return Err(LuciError::InvalidQuery(
                            "span_near: all clauses must be on the same field".into()));
                    }
                } else {
                    field = Some(f);
                }
                terms.push(value);
            }
            _ => return Err(LuciError::InvalidQuery(
                "span_near: clauses must be span_term queries".into())),
        }
    }
    let field = field.ok_or_else(|| LuciError::InvalidQuery("span_near: no clauses".into()))?;
    let slop = obj.get("slop").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
    let in_order = obj.get("in_order").and_then(|v| v.as_bool()).unwrap_or(true);
    Ok(QueryAST::SpanNear { field, terms, slop, in_order })
}

fn parse_span_not_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("span_not must be an object".into()))?;
    let include = obj.get("include")
        .ok_or_else(|| LuciError::InvalidQuery("span_not: missing 'include'".into()))?;
    let exclude = obj.get("exclude")
        .ok_or_else(|| LuciError::InvalidQuery("span_not: missing 'exclude'".into()))?;
    Ok(QueryAST::SpanNot {
        include: Box::new(parse_query_node(include)?),
        exclude: Box::new(parse_query_node(exclude)?),
    })
}

fn parse_span_first_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object()
        .ok_or_else(|| LuciError::InvalidQuery("span_first must be an object".into()))?;
    let match_query = obj.get("match")
        .ok_or_else(|| LuciError::InvalidQuery("span_first: missing 'match'".into()))?;
    let end = obj.get("end").and_then(|v| v.as_u64())
        .ok_or_else(|| LuciError::InvalidQuery("span_first: missing 'end'".into()))? as u32;
    Ok(QueryAST::SpanFirst {
        query: Box::new(parse_query_node(match_query)?),
        end,
    })
}

fn parse_constant_score_query(node: &Value) -> Result<QueryAST> {
    let obj = node
        .as_object()
        .ok_or_else(|| LuciError::InvalidQuery("constant_score must be an object".into()))?;

    let filter = obj
        .get("filter")
        .ok_or_else(|| LuciError::InvalidQuery("constant_score: missing 'filter'".into()))?;

    let query = parse_query_node(filter)?;
    let boost = obj
        .get("boost")
        .and_then(|v| v.as_f64())
        .map(|f| f as f32)
        .unwrap_or(1.0);

    Ok(QueryAST::ConstantScore {
        query: Box::new(query),
        boost,
    })
}

fn parse_nested_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("nested must be an object".into())
    })?;
    let path = obj.get("path").and_then(|v| v.as_str())
        .ok_or_else(|| LuciError::InvalidQuery("nested: missing 'path'".into()))?;
    let query = obj.get("query")
        .ok_or_else(|| LuciError::InvalidQuery("nested: missing 'query'".into()))?;
    let inner_hits = obj.get("inner_hits").map(|ih| {
        let ih_obj = ih.as_object();
        crate::query::ast::InnerHitsConfig {
            name: ih_obj.and_then(|o| o.get("name"))
                .and_then(|v| v.as_str()).map(String::from),
            size: ih_obj.and_then(|o| o.get("size"))
                .and_then(|v| v.as_u64()).map(|v| v as usize)
                .unwrap_or(3),
            from: ih_obj.and_then(|o| o.get("from"))
                .and_then(|v| v.as_u64()).map(|v| v as usize)
                .unwrap_or(0),
        }
    });
    Ok(QueryAST::Nested {
        path: path.to_string(),
        query: Box::new(parse_query_node(query)?),
        inner_hits,
    })
}

fn parse_geo_distance_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("geo_distance must be an object".into())
    })?;

    let distance = obj.get("distance").and_then(|v| v.as_str())
        .ok_or_else(|| LuciError::InvalidQuery("geo_distance: missing 'distance'".into()))?
        .to_string();

    // Find the field (any key that's not "distance")
    for (key, val) in obj {
        if key == "distance" { continue; }
        let point = crate::spatial::geo::GeoPoint::from_json(val)
            .ok_or_else(|| LuciError::InvalidQuery(format!(
                "geo_distance: invalid geo point for field '{key}'"
            )))?;
        return Ok(QueryAST::GeoDistance {
            field: key.clone(),
            lat: point.lat,
            lon: point.lon,
            distance,
        });
    }
    Err(LuciError::InvalidQuery("geo_distance: missing field".into()))
}

fn parse_geo_bbox_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("geo_bounding_box must be an object".into())
    })?;

    for (key, val) in obj {
        let bbox = val.as_object().ok_or_else(|| {
            LuciError::InvalidQuery("geo_bounding_box: field value must be an object".into())
        })?;
        let tl = bbox.get("top_left").ok_or_else(|| {
            LuciError::InvalidQuery("geo_bounding_box: missing 'top_left'".into())
        })?;
        let br = bbox.get("bottom_right").ok_or_else(|| {
            LuciError::InvalidQuery("geo_bounding_box: missing 'bottom_right'".into())
        })?;
        let tl_point = crate::spatial::geo::GeoPoint::from_json(tl)
            .ok_or_else(|| LuciError::InvalidQuery("invalid top_left".into()))?;
        let br_point = crate::spatial::geo::GeoPoint::from_json(br)
            .ok_or_else(|| LuciError::InvalidQuery("invalid bottom_right".into()))?;

        return Ok(QueryAST::GeoBoundingBox {
            field: key.clone(),
            top_left_lat: tl_point.lat,
            top_left_lon: tl_point.lon,
            bottom_right_lat: br_point.lat,
            bottom_right_lon: br_point.lon,
        });
    }
    Err(LuciError::InvalidQuery("geo_bounding_box: missing field".into()))
}

fn parse_geo_shape_query(node: &Value) -> Result<QueryAST> {
    let obj = node.as_object().ok_or_else(|| {
        LuciError::InvalidQuery("geo_shape must be an object".into())
    })?;

    for (key, val) in obj {
        let field_obj = val.as_object().ok_or_else(|| {
            LuciError::InvalidQuery(format!("geo_shape: field '{key}' must be an object"))
        })?;

        let shape_val = field_obj.get("shape").ok_or_else(|| {
            LuciError::InvalidQuery("geo_shape: missing 'shape'".into())
        })?;

        let relation_str = field_obj.get("relation")
            .and_then(|v| v.as_str())
            .unwrap_or("intersects");

        let relation = match relation_str {
            "intersects" | "INTERSECTS" => SpatialRelation::Intersects,
            "within" | "WITHIN" => SpatialRelation::Within,
            "contains" | "CONTAINS" => SpatialRelation::Contains,
            "disjoint" | "DISJOINT" => SpatialRelation::Disjoint,
            "touches" | "TOUCHES" => SpatialRelation::Touches,
            "crosses" | "CROSSES" => SpatialRelation::Crosses,
            "overlaps" | "OVERLAPS" => SpatialRelation::Overlaps,
            "equals" | "EQUALS" => SpatialRelation::Equals,
            "covers" | "COVERS" => SpatialRelation::Covers,
            "coveredby" | "COVEREDBY" => SpatialRelation::CoveredBy,
            "contains_properly" | "CONTAINS_PROPERLY" => SpatialRelation::ContainsProperly,
            other => return Err(LuciError::InvalidQuery(
                format!("geo_shape: unknown relation '{other}'")
            )),
        };

        return Ok(QueryAST::GeoShape {
            field: key.clone(),
            shape: GeoShapeValue { json: shape_val.clone() },
            relation,
        });
    }
    Err(LuciError::InvalidQuery("geo_shape: missing field".into()))
}

/// Parse distance string like "10km", "5mi", "1000m". Returns km.
pub fn parse_distance_km(s: &str) -> f64 {
    let s = s.trim();
    if let Some(n) = s.strip_suffix("km") {
        n.trim().parse().unwrap_or(0.0)
    } else if let Some(n) = s.strip_suffix("mi") {
        n.trim().parse::<f64>().unwrap_or(0.0) * 1.60934
    } else if let Some(n) = s.strip_suffix('m') {
        n.trim().parse::<f64>().unwrap_or(0.0) / 1000.0
    } else {
        // default: meters
        s.parse::<f64>().unwrap_or(0.0) / 1000.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn parse_term_shorthand() {
        let q = parse_query(&json!({"term": {"status": "active"}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Term {
                field: "status".into(),
                value: "active".into()
            }
        );
    }

    #[test]
    fn parse_term_full_form() {
        let q =
            parse_query(&json!({"term": {"status": {"value": "active"}}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Term {
                field: "status".into(),
                value: "active".into()
            }
        );
    }

    #[test]
    fn parse_term_numeric() {
        let q = parse_query(&json!({"term": {"age": 25}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Term {
                field: "age".into(),
                value: "25".into()
            }
        );
    }

    #[test]
    fn parse_terms() {
        let q = parse_query(&json!({"terms": {"status": ["a", "b", "c"]}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Terms {
                field: "status".into(),
                values: vec!["a".into(), "b".into(), "c".into()]
            }
        );
    }

    #[test]
    fn parse_match_shorthand() {
        let q = parse_query(&json!({"match": {"title": "search engine"}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Match {
                field: "title".into(),
                query: "search engine".into(),
                analyzer: None
            }
        );
    }

    #[test]
    fn parse_match_full_form() {
        let q = parse_query(&json!({
            "match": {"title": {"query": "search", "analyzer": "standard"}}
        }))
        .unwrap();
        assert_eq!(
            q,
            QueryAST::Match {
                field: "title".into(),
                query: "search".into(),
                analyzer: Some("standard".into())
            }
        );
    }

    #[test]
    fn parse_match_phrase_shorthand() {
        let q = parse_query(&json!({"match_phrase": {"body": "quick brown fox"}})).unwrap();
        assert_eq!(
            q,
            QueryAST::MatchPhrase {
                field: "body".into(),
                query: "quick brown fox".into(),
                analyzer: None
            }
        );
    }

    #[test]
    fn parse_match_phrase_full_form() {
        let q = parse_query(&json!({
            "match_phrase": {"body": {"query": "quick brown"}}
        }))
        .unwrap();
        if let QueryAST::MatchPhrase { query, .. } = &q {
            assert_eq!(query, "quick brown");
        } else {
            panic!("expected MatchPhrase");
        }
    }

    #[test]
    fn parse_bool_basic() {
        let q = parse_query(&json!({
            "bool": {
                "must": [{"term": {"status": "active"}}],
                "filter": [{"exists": {"field": "title"}}]
            }
        }))
        .unwrap();

        if let QueryAST::Bool {
            must,
            should,
            must_not,
            filter,
        } = &q
        {
            assert_eq!(must.len(), 1);
            assert!(should.is_empty());
            assert!(must_not.is_empty());
            assert_eq!(filter.len(), 1);
        } else {
            panic!("expected Bool");
        }
    }

    #[test]
    fn parse_bool_all_clauses() {
        let q = parse_query(&json!({
            "bool": {
                "must": [{"match": {"title": "search"}}],
                "should": [{"term": {"tag": "hot"}}],
                "must_not": [{"term": {"status": "deleted"}}],
                "filter": [{"exists": {"field": "body"}}]
            }
        }))
        .unwrap();

        if let QueryAST::Bool {
            must,
            should,
            must_not,
            filter,
        } = &q
        {
            assert_eq!(must.len(), 1);
            assert_eq!(should.len(), 1);
            assert_eq!(must_not.len(), 1);
            assert_eq!(filter.len(), 1);
        }
    }

    #[test]
    fn parse_bool_single_clause_not_array() {
        // ES accepts a single query object without wrapping in array
        let q = parse_query(&json!({
            "bool": {
                "must": {"term": {"status": "active"}}
            }
        }))
        .unwrap();

        if let QueryAST::Bool { must, .. } = &q {
            assert_eq!(must.len(), 1);
        }
    }

    #[test]
    fn parse_exists() {
        let q = parse_query(&json!({"exists": {"field": "title"}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Exists {
                field: "title".into()
            }
        );
    }

    #[test]
    fn parse_prefix_shorthand() {
        let q = parse_query(&json!({"prefix": {"title": "sea"}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Prefix {
                field: "title".into(),
                value: "sea".into()
            }
        );
    }

    #[test]
    fn parse_prefix_full_form() {
        let q = parse_query(&json!({"prefix": {"title": {"value": "sea"}}})).unwrap();
        assert_eq!(
            q,
            QueryAST::Prefix {
                field: "title".into(),
                value: "sea".into()
            }
        );
    }

    #[test]
    fn parse_constant_score() {
        let q = parse_query(&json!({
            "constant_score": {
                "filter": {"term": {"status": "active"}},
                "boost": 1.5
            }
        }))
        .unwrap();

        if let QueryAST::ConstantScore { boost, query } = &q {
            assert_eq!(*boost, 1.5);
            assert!(matches!(query.as_ref(), QueryAST::Term { .. }));
        } else {
            panic!("expected ConstantScore");
        }
    }

    #[test]
    fn parse_match_all() {
        let q = parse_query(&json!({"match_all": {}})).unwrap();
        assert_eq!(q, QueryAST::MatchAll);
    }

    #[test]
    fn parse_match_none() {
        let q = parse_query(&json!({"match_none": {}})).unwrap();
        assert_eq!(q, QueryAST::MatchNone);
    }

    #[test]
    fn parse_with_query_wrapper() {
        let q = parse_query(&json!({
            "query": {"term": {"status": "active"}}
        }))
        .unwrap();
        assert!(matches!(q, QueryAST::Term { .. }));
    }

    #[test]
    fn parse_unknown_query_type() {
        let r = parse_query(&json!({"unknown_type": {"field": "val"}}));
        assert!(r.is_err());
    }

    #[test]
    fn parse_empty_object() {
        let r = parse_query(&json!({}));
        assert!(r.is_err());
    }

    #[test]
    fn parse_nested_bool() {
        let q = parse_query(&json!({
            "bool": {
                "must": [{
                    "bool": {
                        "should": [
                            {"term": {"a": "1"}},
                            {"term": {"b": "2"}}
                        ]
                    }
                }]
            }
        }))
        .unwrap();

        if let QueryAST::Bool { must, .. } = &q {
            assert!(matches!(&must[0], QueryAST::Bool { .. }));
        }
    }

    #[test]
    fn parse_deeply_nested() {
        let q = parse_query(&json!({
            "bool": {
                "filter": [{
                    "constant_score": {
                        "filter": {"term": {"x": "y"}},
                        "boost": 2.0
                    }
                }]
            }
        }))
        .unwrap();

        if let QueryAST::Bool { filter, .. } = &q {
            assert!(matches!(&filter[0], QueryAST::ConstantScore { .. }));
        }
    }
}
