//! `qexpr`: typed query expressions (query algebra).
//!
//! Goal: provide a small, stable AST for common retrieval query operators without
//! committing to any particular index backend or scoring model.
//!
//! This is intentionally **not** a parser. Parsing (syntax) is product-specific.
//! This crate is about a shared, typed *meaning* that multiple systems can compile
//! down to their preferred execution plan.

#![warn(missing_docs)]

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// A query expression for retrieval.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum QExpr {
    /// A single term.
    Term(Term),
    /// A phrase (ordered sequence of terms).
    ///
    /// Semantics require positional information in the target index to evaluate exactly.
    Phrase(Phrase),
    /// Proximity query: terms must occur within a window.
    ///
    /// This is the semantic payload behind operators like `NEAR/k`.
    /// Evaluation requires positional information in the target index (or a verifier stage).
    Near(Near),
    /// Conjunction: all children must match.
    And(Vec<QExpr>),
    /// Disjunction: any child may match.
    Or(Vec<QExpr>),
    /// Negation: exclude matches of inner expression.
    Not(Box<QExpr>),
    /// Field scoping (e.g. `title:term`).
    ///
    /// Evaluation requires field-aware indexing (or a compiler that rewrites into field-specific terms).
    Field(FieldName, Box<QExpr>),
}

/// A normalized term token.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Term(pub String);

impl Term {
    /// Create a term (caller is responsible for normalization/tokenization policy).
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    /// Returns true if the term is empty or whitespace.
    pub fn is_blank(&self) -> bool {
        self.0.trim().is_empty()
    }
}

/// A phrase of ordered terms.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Phrase {
    /// Ordered terms.
    pub terms: Vec<Term>,
}

impl Phrase {
    /// Create a phrase.
    pub fn new(terms: Vec<Term>) -> Self {
        Self { terms }
    }

    /// Returns true if the phrase has no terms (or all terms are blank).
    pub fn is_blank(&self) -> bool {
        self.terms.is_empty() || self.terms.iter().all(|t| t.is_blank())
    }

    /// Count of non-blank terms.
    pub fn non_blank_count(&self) -> usize {
        self.terms.iter().filter(|t| !t.is_blank()).count()
    }
}

/// A proximity query over ordered terms.
///
/// This represents constraints like “the terms occur within `window` tokens”.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Near {
    /// Terms participating in the proximity constraint.
    ///
    /// Must have length >= 2.
    pub terms: Vec<Term>,
    /// Window size in tokens.
    ///
    /// Interpretation: there exists an assignment of positions (one per term occurrence)
    /// such that `max(pos) - min(pos) <= window`.
    pub window: u32,
    /// If true, enforce term order (like an ordered NEAR / “WITHIN k in order”).
    ///
    /// If false, order is ignored (unordered NEAR/k).
    pub ordered: bool,
}

impl Near {
    /// Create a proximity query.
    pub fn new(terms: Vec<Term>, window: u32, ordered: bool) -> Self {
        Self {
            terms,
            window,
            ordered,
        }
    }

    /// Returns true if the constraint is structurally blank/invalid.
    pub fn is_blank(&self) -> bool {
        self.terms.len() < 2 || self.terms.iter().all(|t| t.is_blank()) || self.window == 0
    }
}

/// A field name.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FieldName(pub String);

impl FieldName {
    /// Create a field name.
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    /// Returns true if the field name is empty or whitespace.
    pub fn is_blank(&self) -> bool {
        self.0.trim().is_empty()
    }
}

/// Structural validation errors for `QExpr`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidateError {
    /// A `Term` node contained a blank term.
    BlankTerm,
    /// A `Phrase` node contained no usable terms.
    BlankPhrase,
    /// A `Phrase` node contained fewer than 2 non-blank terms.
    ///
    /// A single-term "phrase" is semantically equivalent to a bare term and should
    /// be represented as `QExpr::Term` instead.
    InsufficientPhraseTerms,
    /// A `Near` node contained fewer than 2 usable terms or an invalid window.
    BlankNear,
    /// A `Near` node contained fewer than 2 non-blank terms after filtering blanks.
    InsufficientNearTerms,
    /// An `And`/`Or` node had no children.
    EmptyJunction,
    /// A `Field` node had a blank field name.
    BlankFieldName,
}

/// Validate a query expression for basic structural invariants.
///
/// This does **not** attempt semantic checks like "is phrase supported by the target index".
pub fn validate(expr: &QExpr) -> Result<(), ValidateError> {
    match expr {
        QExpr::Term(t) => {
            if t.is_blank() {
                Err(ValidateError::BlankTerm)
            } else {
                Ok(())
            }
        }
        QExpr::Phrase(p) => {
            if p.is_blank() {
                Err(ValidateError::BlankPhrase)
            } else if p.non_blank_count() < 2 {
                Err(ValidateError::InsufficientPhraseTerms)
            } else {
                Ok(())
            }
        }
        QExpr::Near(n) => {
            if n.is_blank() {
                Err(ValidateError::BlankNear)
            } else if n.terms.iter().filter(|t| !t.is_blank()).count() < 2 {
                Err(ValidateError::InsufficientNearTerms)
            } else {
                Ok(())
            }
        }
        QExpr::And(xs) | QExpr::Or(xs) => {
            if xs.is_empty() {
                return Err(ValidateError::EmptyJunction);
            }
            for x in xs {
                validate(x)?;
            }
            Ok(())
        }
        QExpr::Not(x) => validate(x),
        QExpr::Field(name, inner) => {
            if name.is_blank() {
                return Err(ValidateError::BlankFieldName);
            }
            validate(inner)
        }
    }
}

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

    // -- Arbitrary QExpr generators --

    fn arb_non_blank_term() -> impl Strategy<Value = Term> {
        "[a-z]{1,8}".prop_map(Term::new)
    }

    fn arb_blank_term() -> impl Strategy<Value = Term> {
        prop_oneof![Just(Term::new("")), Just(Term::new("  ")),]
    }

    fn arb_term() -> impl Strategy<Value = Term> {
        prop_oneof![
            8 => arb_non_blank_term(),
            2 => arb_blank_term(),
        ]
    }

    fn arb_qexpr(depth: u32) -> impl Strategy<Value = QExpr> {
        let leaf = prop_oneof![
            arb_term().prop_map(QExpr::Term),
            prop::collection::vec(arb_term(), 0..5).prop_map(|ts| QExpr::Phrase(Phrase::new(ts))),
            (
                prop::collection::vec(arb_term(), 0..5),
                0..10u32,
                any::<bool>()
            )
                .prop_map(|(ts, w, o)| QExpr::Near(Near::new(ts, w, o))),
        ];
        leaf.prop_recursive(depth, 32, 4, |inner| {
            prop_oneof![
                prop::collection::vec(inner.clone(), 0..4).prop_map(QExpr::And),
                prop::collection::vec(inner.clone(), 0..4).prop_map(QExpr::Or),
                inner.clone().prop_map(|e| QExpr::Not(Box::new(e))),
                ("[a-z]{1,6}".prop_map(FieldName::new), inner)
                    .prop_map(|(f, e)| QExpr::Field(f, Box::new(e))),
            ]
        })
    }

    /// Build a QExpr that is guaranteed to be valid (well-formed).
    fn arb_valid_qexpr(depth: u32) -> impl Strategy<Value = QExpr> {
        let leaf = prop_oneof![
            arb_non_blank_term().prop_map(QExpr::Term),
            prop::collection::vec(arb_non_blank_term(), 2..5)
                .prop_map(|ts| QExpr::Phrase(Phrase::new(ts))),
            (
                prop::collection::vec(arb_non_blank_term(), 2..5),
                1..10u32,
                any::<bool>()
            )
                .prop_map(|(ts, w, o)| QExpr::Near(Near::new(ts, w, o))),
        ];
        leaf.prop_recursive(depth, 32, 4, |inner| {
            prop_oneof![
                prop::collection::vec(inner.clone(), 1..4).prop_map(QExpr::And),
                prop::collection::vec(inner.clone(), 1..4).prop_map(QExpr::Or),
                inner.clone().prop_map(|e| QExpr::Not(Box::new(e))),
                ("[a-z]{1,6}".prop_map(FieldName::new), inner,)
                    .prop_map(|(f, e)| QExpr::Field(f, Box::new(e))),
            ]
        })
    }

    // -- Property tests --

    proptest! {
        #[test]
        fn valid_exprs_pass_validation(expr in arb_valid_qexpr(3)) {
            prop_assert!(validate(&expr).is_ok());
        }

        #[test]
        fn blank_term_always_rejected(s in "\\s{0,4}") {
            let expr = QExpr::Term(Term::new(s));
            prop_assert_eq!(validate(&expr).unwrap_err(), ValidateError::BlankTerm);
        }

        #[test]
        fn empty_and_always_rejected(_ in Just(())) {
            let expr = QExpr::And(vec![]);
            prop_assert_eq!(validate(&expr).unwrap_err(), ValidateError::EmptyJunction);
        }

        #[test]
        fn empty_or_always_rejected(_ in Just(())) {
            let expr = QExpr::Or(vec![]);
            prop_assert_eq!(validate(&expr).unwrap_err(), ValidateError::EmptyJunction);
        }

        #[test]
        fn and_associativity(
            a in arb_valid_qexpr(1),
            b in arb_valid_qexpr(1),
            c in arb_valid_qexpr(1),
        ) {
            let left = QExpr::And(vec![QExpr::And(vec![a.clone(), b.clone()]), c.clone()]);
            let right = QExpr::And(vec![a, QExpr::And(vec![b, c])]);
            let left_ok = validate(&left).is_ok();
            let right_ok = validate(&right).is_ok();
            prop_assert_eq!(left_ok, right_ok, "And associativity: both must validate the same way");
        }

        #[test]
        fn phrase_with_n_ge2_non_blank_passes(
            terms in prop::collection::vec(arb_non_blank_term(), 2..6),
        ) {
            let expr = QExpr::Phrase(Phrase::new(terms));
            prop_assert!(validate(&expr).is_ok());
        }

        #[test]
        fn near_with_n_ge2_and_positive_window_passes(
            terms in prop::collection::vec(arb_non_blank_term(), 2..6),
            window in 1..100u32,
            ordered in any::<bool>(),
        ) {
            let expr = QExpr::Near(Near::new(terms, window, ordered));
            prop_assert!(validate(&expr).is_ok());
        }

        #[test]
        fn random_tree_validate_never_panics(expr in arb_qexpr(4)) {
            // validate should return Ok or Err, never panic.
            let _ = validate(&expr);
        }
    }

    #[test]
    fn validate_rejects_blank_term() {
        assert_eq!(
            validate(&QExpr::Term(Term::new("  "))).unwrap_err(),
            ValidateError::BlankTerm
        );
    }

    #[test]
    fn validate_rejects_blank_phrase() {
        assert_eq!(
            validate(&QExpr::Phrase(Phrase::new(vec![]))).unwrap_err(),
            ValidateError::BlankPhrase
        );
        assert_eq!(
            validate(&QExpr::Phrase(Phrase::new(vec![Term::new("  ")]))).unwrap_err(),
            ValidateError::BlankPhrase
        );
    }

    #[test]
    fn validate_rejects_blank_near() {
        assert_eq!(
            validate(&QExpr::Near(Near::new(
                vec![Term::new("a"), Term::new("b")],
                0,
                false
            )))
            .unwrap_err(),
            ValidateError::BlankNear
        );
        assert_eq!(
            validate(&QExpr::Near(Near::new(vec![Term::new("a")], 5, false))).unwrap_err(),
            ValidateError::BlankNear
        );
    }

    #[test]
    fn validate_rejects_empty_junctions() {
        assert_eq!(
            validate(&QExpr::And(vec![])).unwrap_err(),
            ValidateError::EmptyJunction
        );
        assert_eq!(
            validate(&QExpr::Or(vec![])).unwrap_err(),
            ValidateError::EmptyJunction
        );
    }

    #[test]
    fn validate_rejects_blank_field_name() {
        let q = QExpr::Field(FieldName::new(" "), Box::new(QExpr::Term(Term::new("x"))));
        assert_eq!(validate(&q).unwrap_err(), ValidateError::BlankFieldName);
    }

    #[test]
    fn validate_accepts_non_blank_tree() {
        let q = QExpr::And(vec![
            QExpr::Term(Term::new("alpha")),
            QExpr::Phrase(Phrase::new(vec![Term::new("new"), Term::new("york")])),
            // Blank terms are tolerated as long as >= 2 non-blank terms remain.
            QExpr::Phrase(Phrase::new(vec![
                Term::new("  "),
                Term::new("x"),
                Term::new("y"),
            ])),
            QExpr::Near(Near::new(
                vec![Term::new("deep"), Term::new("learning")],
                5,
                false,
            )),
        ]);
        validate(&q).unwrap();
    }

    #[test]
    fn validate_rejects_single_non_blank_phrase() {
        // A phrase with 1 blank + 1 non-blank has only 1 usable term -- not a real phrase.
        assert_eq!(
            validate(&QExpr::Phrase(Phrase::new(vec![
                Term::new("  "),
                Term::new("x"),
            ])))
            .unwrap_err(),
            ValidateError::InsufficientPhraseTerms
        );
        // A phrase with exactly 1 term.
        assert_eq!(
            validate(&QExpr::Phrase(Phrase::new(vec![Term::new("solo")]))).unwrap_err(),
            ValidateError::InsufficientPhraseTerms
        );
    }

    #[test]
    fn validate_rejects_single_non_blank_near() {
        assert_eq!(
            validate(&QExpr::Near(Near::new(
                vec![Term::new("  "), Term::new("only")],
                5,
                false,
            )))
            .unwrap_err(),
            ValidateError::InsufficientNearTerms
        );
    }
}
