//! Query AST types — the output of JSON parsing and input to execution.
//!
//! See [[query-dsl]] and [[architecture-query-execution#Step 3]].

/// Configuration for inner_hits on nested queries.
/// See [[feature-search-inner-hits]].
#[derive(Clone, Debug, PartialEq)]
pub struct InnerHitsConfig {
    pub name: Option<String>,
    pub size: usize,
    pub from: usize,
}

/// Typed query AST. Each variant maps to an ES-compatible query type.
#[derive(Clone, Debug, PartialEq)]
pub enum QueryAST {
    /// Exact term match on a field (no analysis).
    Term {
        field: String,
        value: String,
    },

    /// Match any of a set of exact values.
    Terms {
        field: String,
        values: Vec<String>,
    },

    /// Full-text match: analyze query text, build bool of term queries.
    Match {
        field: String,
        query: String,
        analyzer: Option<String>,
    },

    /// Phrase match: terms must appear in order at consecutive positions.
    MatchPhrase {
        field: String,
        query: String,
        analyzer: Option<String>,
    },

    /// Match bool prefix: analyzes text, all tokens become term queries
    /// except the last which becomes a prefix query. Rewrites to bool/should.
    MatchBoolPrefix {
        field: String,
        query: String,
        analyzer: Option<String>,
    },

    /// Boolean combination of sub-queries.
    Bool {
        must: Vec<QueryAST>,
        should: Vec<QueryAST>,
        must_not: Vec<QueryAST>,
        filter: Vec<QueryAST>,
    },

    /// Disjunction max: return the score from the best-matching sub-query,
    /// plus tie_breaker * sum of other matching scores.
    DisMax {
        queries: Vec<QueryAST>,
        tie_breaker: f32,
    },

    /// Field exists (has a non-null value).
    Exists {
        field: String,
    },

    /// Prefix match on a field's terms.
    Prefix {
        field: String,
        value: String,
    },

    /// Wrap a query in filter context with a constant score.
    ConstantScore {
        query: Box<QueryAST>,
        boost: f32,
    },

    /// Nested query: search within nested document arrays.
    Nested {
        path: String,
        query: Box<QueryAST>,
        inner_hits: Option<InnerHitsConfig>,
    },

    /// Geographic distance query.
    GeoDistance {
        field: String,
        lat: f64,
        lon: f64,
        distance: String, // e.g. "10km"
    },

    /// Geographic bounding box query.
    GeoBoundingBox {
        field: String,
        top_left_lat: f64,
        top_left_lon: f64,
        bottom_right_lat: f64,
        bottom_right_lon: f64,
    },

    /// Geographic shape query with spatial relation.
    ///
    /// See [[feature-geo-shape]] and [[de-9im]].
    GeoShape {
        field: String,
        shape: GeoShapeValue,
        relation: SpatialRelation,
    },

    /// Numeric range query on a field.
    Range {
        field: String,
        gte: Option<f64>,
        gt: Option<f64>,
        lte: Option<f64>,
        lt: Option<f64>,
    },

    /// Boost wrapper: multiplies inner query's score by a factor.
    Boost {
        query: Box<QueryAST>,
        boost: f32,
    },

    /// Script score: compiled expression-based custom scoring.
    ScriptScore {
        query: Box<QueryAST>,
        script: String,
        params: std::collections::HashMap<String, f64>,
    },

    /// Function score: modify query scores with scoring functions.
    FunctionScore {
        query: Box<QueryAST>,
        functions: Vec<ScoreFunction>,
        score_mode: FunctionScoreMode,
        boost_mode: FunctionBoostMode,
    },

    /// Boosting: positive query with score demotion for negative matches.
    Boosting {
        positive: Box<QueryAST>,
        negative: Box<QueryAST>,
        negative_boost: f32,
    },

    /// Fuzzy match: terms within edit distance.
    Fuzzy {
        field: String,
        value: String,
        fuzziness: u32,
    },

    /// Regexp match on term dictionary.
    Regexp {
        field: String,
        value: String,
    },

    /// Wildcard match: * (any chars) and ? (single char).
    Wildcard {
        field: String,
        value: String,
    },

    /// Multi-field match: search the same query across multiple fields.
    MultiMatch {
        fields: Vec<String>,
        query: String,
        analyzer: Option<String>,
    },

    /// Span term: single term with position awareness.
    SpanTerm { field: String, value: String },

    /// Span near: terms within slop positions, optionally ordered.
    SpanNear { field: String, terms: Vec<String>, slop: u32, in_order: bool },

    /// Span not: include query minus exclude query.
    SpanNot { include: Box<QueryAST>, exclude: Box<QueryAST> },

    /// Span first: match within first N positions.
    SpanFirst { query: Box<QueryAST>, end: u32 },

    /// Match all documents.
    MatchAll,

    /// Match no documents.
    MatchNone,
}

/// A scoring function for function_score queries.
#[derive(Clone, Debug, PartialEq)]
pub enum ScoreFunction {
    /// Multiply score by a constant weight.
    Weight(f32),
    /// Score based on a numeric field's value.
    FieldValueFactor {
        field: String,
        factor: f32,
        modifier: FieldValueModifier,
        missing: f64,
    },
    /// Random score (deterministic per seed + doc).
    RandomScore { seed: u64 },
}

/// Modifier for field_value_factor.
#[derive(Clone, Debug, PartialEq)]
pub enum FieldValueModifier {
    None,
    Log1p,
    Log2p,
    Ln1p,
    Ln2p,
    Sqrt,
    Square,
    Reciprocal,
}

/// How to combine multiple function scores.
#[derive(Clone, Debug, PartialEq)]
pub enum FunctionScoreMode {
    Multiply,
    Sum,
    Avg,
    First,
    Max,
    Min,
}

/// How to combine the function result with the query score.
#[derive(Clone, Debug, PartialEq)]
pub enum FunctionBoostMode {
    Multiply,
    Replace,
    Sum,
    Avg,
    Max,
    Min,
}

/// Spatial relation for geo_shape queries.
///
/// Supports the full [[ogc-simple-features]] / [[de-9im]] predicate set.
#[derive(Clone, Debug, PartialEq)]
pub enum SpatialRelation {
    /// Query and document shapes share any area or boundary (default).
    Intersects,
    /// Document shape is entirely inside the query shape.
    Within,
    /// Document shape entirely contains the query shape.
    Contains,
    /// Query and document shapes share no area or boundary.
    Disjoint,
    /// Shapes meet at boundary but don't overlap interiors.
    Touches,
    /// Interiors intersect but neither contains the other.
    Crosses,
    /// Shared interior area, same dimension, neither contains the other.
    Overlaps,
    /// Geometries are topologically identical.
    Equals,
    /// Like Contains but includes boundary contact.
    Covers,
    /// Like Within but includes boundary contact.
    CoveredBy,
    /// Interior of document shape contains all of query shape (strict).
    ContainsProperly,
}

/// A parsed GeoJSON shape for use in geo_shape queries.
#[derive(Clone, Debug, PartialEq)]
pub struct GeoShapeValue {
    /// The raw GeoJSON shape object (type + coordinates).
    pub json: serde_json::Value,
}

impl QueryAST {
    /// Convenience constructor for a bool query.
    pub fn bool_query(
        must: Vec<QueryAST>,
        should: Vec<QueryAST>,
        must_not: Vec<QueryAST>,
        filter: Vec<QueryAST>,
    ) -> Self {
        Self::Bool {
            must,
            should,
            must_not,
            filter,
        }
    }
}

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

    #[test]
    fn term_query() {
        let q = QueryAST::Term {
            field: "status".into(),
            value: "active".into(),
        };
        assert_eq!(
            q,
            QueryAST::Term {
                field: "status".into(),
                value: "active".into()
            }
        );
    }

    #[test]
    fn bool_query_construction() {
        let q = QueryAST::bool_query(
            vec![QueryAST::Term {
                field: "f".into(),
                value: "v".into(),
            }],
            vec![],
            vec![],
            vec![],
        );
        if let QueryAST::Bool { must, .. } = &q {
            assert_eq!(must.len(), 1);
        } else {
            panic!("expected Bool");
        }
    }

    #[test]
    fn nested_bool() {
        let inner = QueryAST::bool_query(
            vec![QueryAST::MatchAll],
            vec![],
            vec![],
            vec![],
        );
        let outer = QueryAST::bool_query(vec![inner], vec![], vec![], vec![]);
        if let QueryAST::Bool { must, .. } = &outer {
            assert!(matches!(&must[0], QueryAST::Bool { .. }));
        }
    }

    #[test]
    fn constant_score_wraps() {
        let q = QueryAST::ConstantScore {
            query: Box::new(QueryAST::Term {
                field: "f".into(),
                value: "v".into(),
            }),
            boost: 1.5,
        };
        if let QueryAST::ConstantScore { boost, .. } = q {
            assert_eq!(boost, 1.5);
        }
    }

    #[test]
    fn match_query() {
        let q = QueryAST::Match {
            field: "body".into(),
            query: "search engine".into(),
            analyzer: None,
        };
        if let QueryAST::Match { field, query, .. } = &q {
            assert_eq!(field, "body");
            assert_eq!(query, "search engine");
        }
    }

    #[test]
    fn debug_format() {
        let q = QueryAST::MatchAll;
        let s = format!("{q:?}");
        assert!(s.contains("MatchAll"));
    }

    #[test]
    fn clone_works() {
        let q = QueryAST::Term {
            field: "f".into(),
            value: "v".into(),
        };
        let q2 = q.clone();
        assert_eq!(q, q2);
    }

    #[test]
    fn terms_query() {
        let q = QueryAST::Terms {
            field: "tags".into(),
            values: vec!["a".into(), "b".into(), "c".into()],
        };
        if let QueryAST::Terms { values, .. } = &q {
            assert_eq!(values.len(), 3);
        }
    }
}
