//! User-state filter extraction from filter expression trees.
//!
//! Walks a `FilterExpr` AST and collects references to user-state filter
//! variants (`Saved`, `Liked`, `InProgress`). These variants pass through
//! the `FilterEvaluator` as "return full universe" and must be applied as
//! inclusion filters in Stage 2.5 of the executor pipeline, where
//! `UserStateIndex` is available.
//!
//! # Negation of deferred post-filters
//!
//! The variants listed in [`is_deferred_post_filter`] (`Saved`, `Liked`,
//! `InProgress`, `SocialGraph`, `MinSignal`, `MaxSignal`, `NearLocation`,
//! `InCollection`) are *deferred*: the `FilterEvaluator` returns the full
//! universe for them in Stage 2, and the real predicate runs later in a
//! Stage 2.x post-filter. That two-phase design has no correct interpretation
//! under `Not`: Stage 2 would compute `universe - universe = empty` (dropping
//! every candidate), and the Stage 2.x extractors here would re-apply the inner
//! variant *positively*, returning the matched set instead of its complement.
//! Either way the result is the exact opposite of the requested set, silently.
//!
//! Until a negation-aware (polarity-tracking) evaluator exists, we fail loud:
//! the `collect_*` extractors do **not** recurse into `Not`, and
//! [`reject_negated_deferred_filters`] returns
//! [`QueryError::InvalidFilter`] for any deferred variant placed under an odd
//! number of `Not` nodes. Failing loudly beats returning the inverse set.

use crate::{query::retrieve::QueryError, storage::indexes::filter::FilterExpr};

/// Whether `expr` is a *deferred* post-filter variant — one that the
/// `FilterEvaluator` resolves to the full universe and a later executor stage
/// (2.2/2.3/2.4/2.5) actually applies. These have no sound negation at the
/// bitmap layer (see module docs), so they are rejected under `Not`.
#[must_use]
pub(crate) const fn is_deferred_post_filter(expr: &FilterExpr) -> bool {
    matches!(
        expr,
        FilterExpr::Saved(_)
            | FilterExpr::Liked(_)
            | FilterExpr::InProgress { .. }
            | FilterExpr::SocialGraph { .. }
            | FilterExpr::MinSignal { .. }
            | FilterExpr::MaxSignal { .. }
            | FilterExpr::NearLocation { .. }
            | FilterExpr::InCollection(_)
    )
}

/// Reject any deferred post-filter variant sitting under an odd number of `Not`
/// nodes, anywhere in the expression tree.
///
/// `negated` tracks the running polarity: `true` means an odd number of `Not`
/// ancestors. A deferred variant reached with `negated == true` cannot be
/// applied correctly (the bitmap-layer complement and the positive Stage 2.x
/// re-application both yield the wrong set), so the whole query is rejected.
///
/// Index-backed variants (`CategoryEq`, `DurationMin`, `CreatedAfter`, …) negate
/// correctly via the evaluator's bitmap complement and are always allowed.
///
/// # Errors
///
/// Returns [`QueryError::InvalidFilter`] naming the first negated deferred
/// variant encountered.
pub(crate) fn reject_negated_deferred_filters(expr: &FilterExpr) -> Result<(), QueryError> {
    fn walk(expr: &FilterExpr, negated: bool) -> Result<(), QueryError> {
        match expr {
            FilterExpr::And(children) | FilterExpr::Or(children) => {
                for child in children {
                    walk(child, negated)?;
                }
                Ok(())
            }
            FilterExpr::Not(inner) => walk(inner, !negated),
            other if negated && is_deferred_post_filter(other) => Err(QueryError::InvalidFilter {
                field: format!("NOT({other:?})"),
                reason: "negation of a deferred post-filter (Saved/Liked/InProgress/\
                             SocialGraph/MinSignal/MaxSignal/NearLocation/InCollection) is not \
                             supported: it would silently return the inverse set. Express the \
                             complement explicitly or use an index-backed filter instead."
                    .to_string(),
            }),
            _ => Ok(()),
        }
    }
    walk(expr, false)
}

/// Collect every leaf node for which `is_target` returns `true`, walking only
/// the positive part of the tree.
///
/// This is the single, load-bearing traversal rule shared by all of the
/// deferred-post-filter extractors below (user-state, signal threshold, geo,
/// collection). Centralizing it here means the rule lives in exactly one place
/// instead of being re-stated — and at risk of drifting — across four
/// near-identical walkers (W27):
///
/// - **And/Or children are recursed into.** Both combinators are positive
///   contexts for these extractors; an `Or` over a deferred variant is rejected
///   up front by [`reject_or_of_deferred_filters`], so by the time extraction
///   runs an `Or` can only hold safe (non-deferred) siblings alongside the
///   target — recursing is correct and matches the historical behavior.
/// - **`Not` is deliberately NOT recursed into.** A deferred variant under `Not`
///   has no positive application (see the module docs);
///   [`reject_negated_deferred_filters`] rejects such queries before extraction,
///   so the inner variant must never be pulled out as a *positive* filter (that
///   would invert the requested set).
/// - **Every other node contributes nothing** — non-deferred variants
///   (`CategoryEq`, `FormatEq`, …) are handled by the `FilterEvaluator`.
fn collect_matching<'a>(
    expr: &'a FilterExpr,
    is_target: &impl Fn(&FilterExpr) -> bool,
    out: &mut Vec<&'a FilterExpr>,
) {
    if is_target(expr) {
        out.push(expr);
        return;
    }
    match expr {
        FilterExpr::And(children) | FilterExpr::Or(children) => {
            for child in children {
                collect_matching(child, is_target, out);
            }
        }
        // `Not` and every other node: not a positive target context (see above).
        _ => {}
    }
}

/// Extract user-state filter variants from a filter expression tree.
///
/// AND/OR nodes are recursed into; `Not` is NOT (negated deferred filters are
/// rejected up front by [`reject_negated_deferred_filters`], so extracting the
/// inner variant positively would invert the requested set). Leaf nodes that
/// are not user-state filters are ignored (handled by the `FilterEvaluator`).
pub(crate) fn extract_user_state_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    let mut result = Vec::new();
    collect_matching(
        expr,
        &|e| {
            matches!(
                e,
                FilterExpr::Saved(_)
                    | FilterExpr::Liked(_)
                    | FilterExpr::InProgress { .. }
                    | FilterExpr::SocialGraph { .. }
            )
        },
        &mut result,
    );
    result
}

// ── M6p3 Signal Threshold + Geo Filter Extraction ────────────────────────────

/// Extract `MinSignal` and `MaxSignal` filter variants from a filter expression tree.
///
/// Walks AND/OR nodes (not `Not` — see [`reject_negated_deferred_filters`]).
/// These variants pass through the `FilterEvaluator` as "return full universe"
/// and are applied as post-filters in Stage 2.3 of the executor pipeline, where
/// the signal ledger is available. Thresholds are applied conjunctively, so
/// OR-of-thresholds is rejected up front by [`reject_or_of_deferred_filters`].
pub(crate) fn extract_signal_threshold_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    // All MinSignal/MaxSignal variants are applied CONJUNCTIVELY in Stage 2.3, so
    // an OR-of-threshold would be wrong semantics; `reject_or_of_deferred_filters`
    // rejects that up front (and `reject_negated_deferred_filters` rejects them
    // under `Not`). The shared `collect_matching` walker enforces the same
    // recurse-And/Or, skip-Not, skip-other rule (W27).
    let mut result = Vec::new();
    collect_matching(
        expr,
        &|e| {
            matches!(
                e,
                FilterExpr::MinSignal { .. } | FilterExpr::MaxSignal { .. }
            )
        },
        &mut result,
    );
    result
}

/// Reject any `OR` node that contains *any* deferred post-filter variant
/// (`Saved`, `Liked`, `InProgress`, `SocialGraph`, `MinSignal`, `MaxSignal`,
/// `NearLocation`, `InCollection` — see [`is_deferred_post_filter`]).
///
/// Deferred variants resolve to the full universe in Stage 2 (the
/// `FilterEvaluator` cannot apply them at the bitmap layer), so an
/// `Or([Saved(u), CategoryEq("jazz")])` evaluates to `universe ∪ jazz =
/// universe` — every candidate retained. The Stage 2.x extractors then recurse
/// INTO the `Or`, pull the deferred variant out, and apply it as an
/// unconditional `candidates.retain(...)` — an *intersection*. The net effect is
/// that `saved OR jazz` silently collapses to `saved` (the inverse of the
/// requested union). Thresholds are the same trap: Stage 2.3 applies them
/// conjunctively, so `OR` of thresholds is silently evaluated as `AND`.
///
/// Until a polarity/union-aware deferred evaluator exists, every such query is
/// rejected so the caller never silently gets `AND` (intersection) semantics in
/// place of the requested `OR`. Failing loudly beats returning the inverse set
/// (see the module-level contract).
///
/// Index-backed `Or` (e.g. `Or([CategoryEq, FormatEq])`) is fine: those variants
/// union correctly at the bitmap layer and are never deferred.
///
/// # Errors
///
/// Returns [`QueryError::InvalidFilter`] naming the first deferred variant found
/// inside any `OR` subtree.
pub(crate) fn reject_or_of_deferred_filters(expr: &FilterExpr) -> Result<(), QueryError> {
    /// First deferred variant anywhere in `expr`'s subtree, or `None`.
    fn first_deferred_in_subtree(expr: &FilterExpr) -> Option<&FilterExpr> {
        if is_deferred_post_filter(expr) {
            return Some(expr);
        }
        match expr {
            FilterExpr::And(children) | FilterExpr::Or(children) => {
                children.iter().find_map(first_deferred_in_subtree)
            }
            FilterExpr::Not(inner) => first_deferred_in_subtree(inner),
            _ => None,
        }
    }
    match expr {
        FilterExpr::Or(children) => {
            if let Some(offender) = children.iter().find_map(first_deferred_in_subtree) {
                return Err(QueryError::InvalidFilter {
                    field: format!("OR({offender:?})"),
                    reason: "OR over a deferred post-filter (Saved/Liked/InProgress/\
                             SocialGraph/MinSignal/MaxSignal/NearLocation/InCollection) is not \
                             supported: these filters resolve to the full universe in Stage 2 and \
                             are then applied conjunctively (AND) in a later stage, so an OR would \
                             silently collapse to AND (an intersection, the inverse of the \
                             requested union). Use AND-combined deferred filters, replace it with \
                             an index-backed filter, or split the query."
                        .to_string(),
                });
            }
            for child in children {
                reject_or_of_deferred_filters(child)?;
            }
            Ok(())
        }
        FilterExpr::And(children) => {
            for child in children {
                reject_or_of_deferred_filters(child)?;
            }
            Ok(())
        }
        FilterExpr::Not(inner) => reject_or_of_deferred_filters(inner),
        _ => Ok(()),
    }
}

/// Extract `NearLocation` filter variants from a filter expression tree.
///
/// Walks AND/OR nodes (not `Not`, like the other deferred extractors). Applied
/// as post-filters in Stage 2.4 of the executor pipeline, where item metadata
/// (latitude/longitude) is read from storage.
pub(crate) fn extract_geo_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    // Negated geo filters are rejected up front; the shared `collect_matching`
    // walker recurses And/Or, skips `Not`, and ignores every other node (W27).
    let mut result = Vec::new();
    collect_matching(
        expr,
        &|e| matches!(e, FilterExpr::NearLocation { .. }),
        &mut result,
    );
    result
}

// ── M6p4 Collection Filter Extraction ─────────────────────────────────────────

/// Extract `InCollection` filter variants from a filter expression tree.
///
/// Walks AND/OR nodes (not `Not`, like the other deferred extractors). These
/// variants pass through the `FilterEvaluator` as "return full universe" and are
/// applied as post-filters in Stage 2.2 of the executor pipeline, where the
/// `CollectionIndex` is available.
pub(crate) fn extract_collection_filters(expr: &FilterExpr) -> Vec<&FilterExpr> {
    // Negated collection filters are rejected up front; the shared
    // `collect_matching` walker recurses And/Or, skips `Not`, ignores other
    // nodes (W27).
    let mut result = Vec::new();
    collect_matching(
        expr,
        &|e| matches!(e, FilterExpr::InCollection(_)),
        &mut result,
    );
    result
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::storage::indexes::filter::FilterExpr;

    #[test]
    fn extract_user_state_filters_finds_saved_liked_in_progress() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::Saved(42),
            FilterExpr::Liked(42),
            FilterExpr::InProgress {
                user_id: 42,
                threshold: 0.8,
            },
        ]);
        let found = extract_user_state_filters(&expr);
        assert_eq!(found.len(), 3);
        assert!(matches!(found[0], FilterExpr::Saved(42)));
        assert!(matches!(found[1], FilterExpr::Liked(42)));
        assert!(matches!(
            found[2],
            FilterExpr::InProgress {
                user_id: 42,
                threshold,
            } if (*threshold - 0.8).abs() < f64::EPSILON
        ));
    }

    #[test]
    fn extract_user_state_filters_empty_for_plain_filters() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::FormatEq("video".into()),
        ]);
        let found = extract_user_state_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_user_state_filters_does_not_recurse_into_not() {
        // The positive Saved(1) is extracted; the negated Liked(2) is NOT
        // (extracting it positively would invert the requested set). The whole
        // query is rejected separately by `reject_negated_deferred_filters`.
        let expr = FilterExpr::Or(vec![
            FilterExpr::Saved(1),
            FilterExpr::Not(Box::new(FilterExpr::Liked(2))),
        ]);
        let found = extract_user_state_filters(&expr);
        assert_eq!(found.len(), 1);
        assert!(matches!(found[0], FilterExpr::Saved(1)));
    }

    // ── Signal threshold extraction ─────────────────────────────────────────

    #[test]
    fn extract_signal_threshold_finds_min_and_max() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::MinSignal {
                signal: "view".into(),
                threshold: 100.0,
            },
            FilterExpr::MaxSignal {
                signal: "hide".into(),
                threshold: 5.0,
            },
        ]);
        let found = extract_signal_threshold_filters(&expr);
        assert_eq!(found.len(), 2);
        assert!(matches!(found[0], FilterExpr::MinSignal { .. }));
        assert!(matches!(found[1], FilterExpr::MaxSignal { .. }));
    }

    #[test]
    fn extract_signal_threshold_empty_for_plain_filters() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("rock".into()),
            FilterExpr::FormatEq("audio".into()),
        ]);
        let found = extract_signal_threshold_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_signal_threshold_does_not_recurse_into_not() {
        // A negated threshold must NOT be extracted positively.
        let expr = FilterExpr::Not(Box::new(FilterExpr::MinSignal {
            signal: "like".into(),
            threshold: 10.0,
        }));
        let found = extract_signal_threshold_filters(&expr);
        assert!(found.is_empty());
    }

    // ── Geo filter extraction ───────────────────────────────────────────────

    #[test]
    fn extract_geo_finds_near_location() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("food".into()),
            FilterExpr::NearLocation {
                lat: 40.7128,
                lng: -74.0060,
                radius_km: 10.0,
            },
        ]);
        let found = extract_geo_filters(&expr);
        assert_eq!(found.len(), 1);
        assert!(matches!(found[0], FilterExpr::NearLocation { .. }));
    }

    #[test]
    fn extract_geo_empty_for_plain_filters() {
        let expr = FilterExpr::Tag("live".into());
        let found = extract_geo_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_geo_nested_in_or() {
        let expr = FilterExpr::Or(vec![
            FilterExpr::NearLocation {
                lat: 34.0522,
                lng: -118.2437,
                radius_km: 5.0,
            },
            FilterExpr::NearLocation {
                lat: 40.7128,
                lng: -74.0060,
                radius_km: 15.0,
            },
        ]);
        let found = extract_geo_filters(&expr);
        assert_eq!(found.len(), 2);
    }

    // ── Collection filter extraction ─────────────────────────────────────────

    #[test]
    fn extract_collection_finds_in_collection() {
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::InCollection(crate::entities::CollectionId::new(42)),
        ]);
        let found = extract_collection_filters(&expr);
        assert_eq!(found.len(), 1);
        assert!(matches!(found[0], FilterExpr::InCollection(_)));
    }

    #[test]
    fn extract_collection_empty_for_plain_filters() {
        let expr = FilterExpr::Tag("live".into());
        let found = extract_collection_filters(&expr);
        assert!(found.is_empty());
    }

    #[test]
    fn extract_collection_does_not_recurse_into_not() {
        let expr = FilterExpr::Not(Box::new(FilterExpr::InCollection(
            crate::entities::CollectionId::new(1),
        )));
        let found = extract_collection_filters(&expr);
        assert!(found.is_empty());
    }

    // ── Negated-deferred rejection ──────────────────────────────────────────

    #[test]
    fn reject_negated_saved_errors_cleanly() {
        // Not(Saved(u)) must be rejected, never silently return the saved set
        // or an empty-by-accident set.
        let expr = FilterExpr::Not(Box::new(FilterExpr::Saved(42)));
        let err = reject_negated_deferred_filters(&expr).expect_err("Not(Saved) must be rejected");
        assert!(matches!(err, QueryError::InvalidFilter { .. }));
    }

    #[test]
    fn reject_negated_deferred_covers_all_variants() {
        let cid = crate::entities::CollectionId::new(1);
        let deferred = [
            FilterExpr::Saved(1),
            FilterExpr::Liked(1),
            FilterExpr::InProgress {
                user_id: 1,
                threshold: 0.5,
            },
            FilterExpr::SocialGraph {
                user_id: 1,
                depth: 1,
            },
            FilterExpr::min_signal("view", 1.0),
            FilterExpr::max_signal("hide", 1.0),
            FilterExpr::near_location(0.0, 0.0, 1.0),
            FilterExpr::InCollection(cid),
        ];
        for v in deferred {
            let expr = FilterExpr::Not(Box::new(v.clone()));
            assert!(
                reject_negated_deferred_filters(&expr).is_err(),
                "Not({v:?}) must be rejected"
            );
        }
    }

    #[test]
    fn reject_negated_deferred_allows_index_backed_not() {
        // Index-backed filters negate correctly via the bitmap complement.
        let expr = FilterExpr::Not(Box::new(FilterExpr::CategoryEq("jazz".into())));
        assert!(reject_negated_deferred_filters(&expr).is_ok());
        let expr = FilterExpr::Not(Box::new(FilterExpr::DurationMin(60)));
        assert!(reject_negated_deferred_filters(&expr).is_ok());
    }

    #[test]
    fn reject_negated_deferred_double_not_is_positive() {
        // Not(Not(Saved)) sits under an EVEN number of Not -> positive polarity,
        // which is fine (it reduces to Saved).
        let expr = FilterExpr::Not(Box::new(FilterExpr::Not(Box::new(FilterExpr::Saved(1)))));
        assert!(reject_negated_deferred_filters(&expr).is_ok());
    }

    #[test]
    fn reject_negated_deferred_finds_nested_negation() {
        // A negated deferred filter buried inside an AND must still be caught.
        let expr = FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::Not(Box::new(FilterExpr::InCollection(
                crate::entities::CollectionId::new(7),
            ))),
        ]);
        assert!(reject_negated_deferred_filters(&expr).is_err());
    }

    #[test]
    fn positive_deferred_filters_are_allowed() {
        let expr = FilterExpr::And(vec![
            FilterExpr::Saved(1),
            FilterExpr::min_signal("view", 10.0),
        ]);
        assert!(reject_negated_deferred_filters(&expr).is_ok());
    }

    // ── OR-of-deferred rejection ────────────────────────────────────────────
    //
    // Thresholds are deferred filters, so the threshold cases below are now a
    // special case of the general `reject_or_of_deferred_filters` guard. The
    // `*_variant_*` tests extend the same coverage to every other deferred
    // variant (BLOCKER 5: OR over them silently collapsed to AND).

    #[test]
    fn reject_or_of_thresholds_errors() {
        let expr = FilterExpr::Or(vec![
            FilterExpr::min_signal("view", 10.0),
            FilterExpr::min_signal("like", 5.0),
        ]);
        let err =
            reject_or_of_deferred_filters(&expr).expect_err("OR-of-threshold must be rejected");
        assert!(matches!(err, QueryError::InvalidFilter { .. }));
    }

    #[test]
    fn reject_or_of_thresholds_allows_and_of_thresholds() {
        let expr = FilterExpr::And(vec![
            FilterExpr::min_signal("view", 10.0),
            FilterExpr::max_signal("hide", 5.0),
        ]);
        assert!(reject_or_of_deferred_filters(&expr).is_ok());
    }

    #[test]
    fn reject_or_of_deferred_allows_or_without_deferred() {
        // A legitimate index-backed `Or` (no deferred variant) unions correctly
        // at the bitmap layer and must still succeed.
        let expr = FilterExpr::Or(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::CategoryEq("blues".into()),
        ]);
        assert!(reject_or_of_deferred_filters(&expr).is_ok());
    }

    #[test]
    fn reject_or_of_deferred_covers_all_variants() {
        // BLOCKER 5: every deferred variant placed under an `Or` must be
        // rejected, not silently degraded to AND. Each is OR'd with a harmless
        // index-backed sibling (`CategoryEq`) — exactly the `saved OR jazz`
        // shape that previously collapsed to `saved`.
        let cid = crate::entities::CollectionId::new(1);
        let deferred = [
            FilterExpr::Saved(1),
            FilterExpr::Liked(1),
            FilterExpr::InProgress {
                user_id: 1,
                threshold: 0.5,
            },
            FilterExpr::SocialGraph {
                user_id: 1,
                depth: 1,
            },
            FilterExpr::min_signal("view", 1.0),
            FilterExpr::max_signal("hide", 1.0),
            FilterExpr::near_location(0.0, 0.0, 1.0),
            FilterExpr::InCollection(cid),
        ];
        for v in deferred {
            let expr = FilterExpr::Or(vec![v.clone(), FilterExpr::CategoryEq("jazz".into())]);
            assert!(
                matches!(
                    reject_or_of_deferred_filters(&expr),
                    Err(QueryError::InvalidFilter { .. })
                ),
                "Or([{v:?}, CategoryEq]) must be rejected"
            );
        }
    }

    #[test]
    fn reject_or_of_deferred_finds_nested_or() {
        // A deferred variant buried inside an `Or` nested under an `And` must
        // still be caught — the walk recurses through both combinators.
        let expr = FilterExpr::And(vec![
            FilterExpr::FormatEq("video".into()),
            FilterExpr::Or(vec![
                FilterExpr::Saved(7),
                FilterExpr::CategoryEq("jazz".into()),
            ]),
        ]);
        assert!(reject_or_of_deferred_filters(&expr).is_err());
    }

    #[test]
    fn reject_or_of_deferred_allows_and_of_deferred() {
        // Deferred filters combined with AND are honored (applied
        // conjunctively in Stage 2.x), so an `And` of them must succeed.
        let expr = FilterExpr::And(vec![
            FilterExpr::Saved(1),
            FilterExpr::InCollection(crate::entities::CollectionId::new(3)),
        ]);
        assert!(reject_or_of_deferred_filters(&expr).is_ok());
    }
}
