//! Filter expression AST.
//!
//! Defines the composable filter expression tree that the query parser
//! produces from `FILTER` clauses and that `FilterEvaluator` evaluates
//! against bitmap and range indexes.

/// A filter expression AST node.
///
/// Built by the query parser from the `FILTER` clause and evaluated by
/// `FilterEvaluator` against bitmap and range indexes.
#[derive(Debug, Clone, PartialEq)]
pub enum FilterExpr {
    /// Exact equality on the category field.
    CategoryEq(String),
    /// Exact equality on the format field.
    FormatEq(String),
    /// Exact equality on creator (stored by creator ID as string).
    CreatorEq(u32),
    /// Tag match (multi-value field).
    Tag(String),
    /// Minimum duration in seconds.
    DurationMin(u32),
    /// Maximum duration in seconds.
    DurationMax(u32),
    /// Created after a timestamp (nanoseconds).
    CreatedAfter(u64),
    /// Created before a timestamp (nanoseconds).
    CreatedBefore(u64),
    /// AND: all sub-expressions must match.
    And(Vec<FilterExpr>),
    /// OR: at least one sub-expression must match.
    Or(Vec<FilterExpr>),
    /// NOT: the sub-expression must NOT match.
    Not(Box<FilterExpr>),
    // -- M3 User State Filters -----------------------------------------------
    // These variants are evaluated by the query executor's Stage 2.5 (user-context
    // filtering) which has access to UserStateIndex. At the FilterEvaluator level
    // they return the full universe. They will be constructed by the query parser
    // from FOR USER clauses in M4; currently they are only reachable programmatically.
    /// Only items the user has not seen. Requires FOR USER context.
    Unseen(u64),
    /// Only items not from blocked creators. Requires FOR USER context.
    Unblocked(u64),
    /// Only items the user has saved. Requires FOR USER context.
    Saved(u64),
    /// Only items the user has liked. Requires FOR USER context.
    Liked(u64),
    /// Only items the user has partially consumed (0 < completion < threshold).
    /// Requires FOR USER context.
    InProgress { user_id: u64, threshold: f64 },
    /// Social graph filter: items from creators the user follows (depth=1)
    /// or items engaged by users in the resolved social subgraph (depth=2).
    ///
    /// Requires FOR USER context and `CreatorItemsBitmap` in executor.
    /// At the `FilterEvaluator` level this returns the full universe;
    /// the actual resolution happens in executor Stage 2.5 where
    /// `UserStateIndex` and `CreatorItemsBitmap` are available.
    SocialGraph { user_id: u64, depth: u8 },

    // -- M6p3 Engagement Threshold Filters ────────────────────────────────
    /// Minimum engagement threshold: `AllTime` windowed count for the named
    /// signal must be >= `threshold`. Evaluated as a post-filter in Stage 2.3
    /// of the executor (needs ledger access).
    MinSignal { signal: String, threshold: f64 },
    /// Maximum engagement threshold: `AllTime` windowed count for the named
    /// signal must be <= `threshold`. Evaluated as a post-filter in Stage 2.3
    /// of the executor (needs ledger access).
    MaxSignal { signal: String, threshold: f64 },

    // -- M6p3 Geographic Post-Filter ──────────────────────────────────────
    /// Geographic proximity filter: Haversine distance from `(lat, lng)` must
    /// be <= `radius_km`. Evaluated against item "latitude"/"longitude" metadata
    /// as a post-filter in Stage 2.4 (not index-backed).
    NearLocation { lat: f64, lng: f64, radius_km: f64 },

    // -- M6p4 Collection Filter ──────────────────────────────────────────
    /// Only items in the specified collection. Evaluated as post-filter
    /// Stage 2.2 in the executor. Requires `CollectionIndex`.
    InCollection(crate::entities::CollectionId),
}

impl FilterExpr {
    /// Construct a social graph filter.
    ///
    /// - `depth=1`: items from creators the user follows.
    /// - `depth=2`: depth-1 items plus items engaged by users in the
    ///   resolved social subgraph (followers of the same creators).
    #[must_use]
    pub const fn social_graph(user_id: u64, depth: u8) -> Self {
        Self::SocialGraph { user_id, depth }
    }

    /// Construct a minimum signal threshold filter.
    ///
    /// Items with `AllTime` windowed count for `signal` below `threshold` are excluded.
    #[must_use]
    pub fn min_signal(signal: impl Into<String>, threshold: f64) -> Self {
        Self::MinSignal {
            signal: signal.into(),
            threshold,
        }
    }

    /// Construct a maximum signal threshold filter.
    ///
    /// Items with `AllTime` windowed count for `signal` above `threshold` are excluded.
    #[must_use]
    pub fn max_signal(signal: impl Into<String>, threshold: f64) -> Self {
        Self::MaxSignal {
            signal: signal.into(),
            threshold,
        }
    }

    /// Construct a geographic proximity filter.
    ///
    /// Items beyond `radius_km` from `(lat, lng)` are excluded. Uses Haversine
    /// distance against item "latitude"/"longitude" metadata fields.
    #[must_use]
    pub const fn near_location(lat: f64, lng: f64, radius_km: f64) -> Self {
        Self::NearLocation {
            lat,
            lng,
            radius_km,
        }
    }

    /// Construct an in-collection filter.
    ///
    /// Only items that belong to the specified collection are retained.
    #[must_use]
    pub const fn in_collection(id: crate::entities::CollectionId) -> Self {
        Self::InCollection(id)
    }

    /// Returns the total number of nodes in this filter expression tree.
    ///
    /// Used to guard against deeply nested or oversized filter expressions
    /// that could cause excessive recursion or memory usage during evaluation.
    #[must_use]
    pub fn node_count(&self) -> usize {
        match self {
            Self::And(children) | Self::Or(children) => {
                1 + children.iter().map(Self::node_count).sum::<usize>()
            }
            Self::Not(inner) => 1 + inner.node_count(),
            _ => 1,
        }
    }

    /// Construct an equality filter by field name.
    ///
    /// Routes known field names to their typed variants:
    /// - `"category"` -> `CategoryEq`
    /// - `"format"` -> `FormatEq`
    ///
    /// Other field names fall back to `CategoryEq` as a best-effort default.
    /// M3+ will formalize arbitrary field equality via a generic `Eq` variant.
    #[must_use]
    pub fn eq(field: &str, value: &str) -> Self {
        match field {
            "category" => Self::CategoryEq(value.to_string()),
            "format" => Self::FormatEq(value.to_string()),
            _ => {
                tracing::warn!(
                    field = %field,
                    "unknown filter field; defaulting to CategoryEq -- M3+ will add a generic Eq variant"
                );
                Self::CategoryEq(value.to_string())
            }
        }
    }
}

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

    #[test]
    fn node_count_leaf() {
        assert_eq!(FilterExpr::CategoryEq("music".into()).node_count(), 1);
    }

    #[test]
    fn node_count_and() {
        let filter = FilterExpr::And(vec![
            FilterExpr::CategoryEq("music".into()),
            FilterExpr::FormatEq("video".into()),
        ]);
        // 1 (And) + 2 (leaves) = 3
        assert_eq!(filter.node_count(), 3);
    }

    #[test]
    fn node_count_nested() {
        let filter = FilterExpr::Or(vec![
            FilterExpr::And(vec![
                FilterExpr::CategoryEq("a".into()),
                FilterExpr::FormatEq("b".into()),
            ]),
            FilterExpr::Not(Box::new(FilterExpr::Tag("c".into()))),
        ]);
        // Or(1) + And(1) + Cat(1) + Fmt(1) + Not(1) + Tag(1) = 6
        assert_eq!(filter.node_count(), 6);
    }

    #[test]
    fn node_count_large_flat_and() {
        let children: Vec<FilterExpr> = (0..300)
            .map(|i| FilterExpr::CategoryEq(format!("cat_{i}")))
            .collect();
        let filter = FilterExpr::And(children);
        // 1 (And) + 300 (leaves) = 301
        assert_eq!(filter.node_count(), 301);
        assert!(
            filter.node_count() > 256,
            "this filter should exceed the 256-node limit"
        );
    }
}
