//! Filter evaluation engine.
//!
//! `FilterEvaluator` evaluates boolean combinations of metadata predicates
//! against bitmap and range indexes, producing a `FilterResult`.

use std::ops::Bound;

use roaring::RoaringBitmap;

use super::expr::FilterExpr;
use super::result::FilterResult;
use crate::storage::indexes::bitmap::BitmapIndex;
use crate::storage::indexes::range::RangeIndex;

/// Evaluates filter expressions against bitmap and range indexes.
///
/// Constructed once per query with references to all indexes. Borrows
/// all indexes by reference; does not outlive the query execution.
pub struct FilterEvaluator<'a> {
    category_index: &'a BitmapIndex,
    format_index: &'a BitmapIndex,
    creator_index: &'a BitmapIndex,
    tag_index: &'a BitmapIndex,
    duration_index: &'a RangeIndex<u32>,
    created_at_index: &'a RangeIndex<u64>,
    /// Universe bitmap: all entity IDs currently in the database.
    /// Required for NOT (complement) operations.
    universe: &'a RoaringBitmap,
}

impl<'a> FilterEvaluator<'a> {
    /// Create a new evaluator with references to all indexes.
    #[must_use]
    pub const fn new(
        category_index: &'a BitmapIndex,
        format_index: &'a BitmapIndex,
        creator_index: &'a BitmapIndex,
        tag_index: &'a BitmapIndex,
        duration_index: &'a RangeIndex<u32>,
        created_at_index: &'a RangeIndex<u64>,
        universe: &'a RoaringBitmap,
    ) -> Self {
        Self {
            category_index,
            format_index,
            creator_index,
            tag_index,
            duration_index,
            created_at_index,
            universe,
        }
    }

    /// Evaluate a filter expression and return the matching entity set.
    ///
    /// AND nodes sort children by ascending selectivity and short-circuit
    /// on empty intersection (Spec 08, Section 7.4).
    ///
    /// User-state filters (Unseen, Unblocked, Saved, Liked, `InProgress`)
    /// return `FilterResult::Bitmap` by evaluating against the universe.
    /// The actual user-state filtering happens in the query executor's
    /// Stage 2.5 (user-context filtering).
    #[must_use]
    pub fn evaluate(&self, expr: &FilterExpr) -> FilterResult {
        FilterResult::Bitmap(self.eval_to_bitmap(expr))
    }

    fn eval_to_bitmap(&self, expr: &FilterExpr) -> RoaringBitmap {
        match expr {
            FilterExpr::CategoryEq(v) => self.category_index.get(v).unwrap_or_default(),
            FilterExpr::FormatEq(v) => self.format_index.get(v).unwrap_or_default(),
            FilterExpr::CreatorEq(id) => {
                self.creator_index.get(&id.to_string()).unwrap_or_default()
            }
            FilterExpr::Tag(v) => self.tag_index.get(v).unwrap_or_default(),
            FilterExpr::DurationMin(secs) => self.duration_index.gte(secs),
            FilterExpr::DurationMax(secs) => self.duration_index.lte(secs),
            FilterExpr::CreatedAfter(ns) => self.created_at_index.gt(ns),
            FilterExpr::CreatedBefore(ns) => self.created_at_index.lt(ns),
            FilterExpr::And(children) => self.evaluate_and(children),
            FilterExpr::Or(children) => self.evaluate_or(children),
            FilterExpr::Not(inner) => self.evaluate_not(inner),
            // User-state filters: these are evaluated at the FilterEvaluator
            // level by returning the full universe -- the actual filtering
            // happens in the query executor's Stage 2.5 (user-context filtering)
            // which has access to UserStateIndex. This design avoids coupling
            // the filter evaluator to user-state infrastructure.
            FilterExpr::Unseen(_)
            | FilterExpr::Unblocked(_)
            | FilterExpr::Saved(_)
            | FilterExpr::Liked(_)
            | FilterExpr::InProgress { .. }
            | FilterExpr::SocialGraph { .. }
            | FilterExpr::MinSignal { .. }
            | FilterExpr::MaxSignal { .. }
            | FilterExpr::NearLocation { .. }
            | FilterExpr::InCollection(_) => {
                // Return full universe; executor Stage 2.2/2.3/2.4/2.5 does the real filtering.
                self.universe.clone()
            }
        }
    }

    fn evaluate_and(&self, children: &[FilterExpr]) -> RoaringBitmap {
        if children.is_empty() {
            // AND of nothing = everything (identity element for intersection)
            return self.universe.clone();
        }

        // Sort children by estimated selectivity (ascending) for early termination.
        let mut ordered: Vec<_> = children.iter().map(|c| (self.selectivity(c), c)).collect();
        ordered.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));

        let mut result = self.eval_to_bitmap(ordered[0].1);
        for &(_, child) in &ordered[1..] {
            // Short-circuit: empty ∩ X = empty.
            if result.is_empty() {
                return result;
            }
            let child_bitmap = self.eval_to_bitmap(child);
            result &= &child_bitmap;
        }
        result
    }

    fn evaluate_or(&self, children: &[FilterExpr]) -> RoaringBitmap {
        if children.is_empty() {
            return RoaringBitmap::new();
        }
        let mut result = RoaringBitmap::new();
        for child in children {
            result |= &self.eval_to_bitmap(child);
        }
        result
    }

    fn evaluate_not(&self, inner: &FilterExpr) -> RoaringBitmap {
        let inner_bitmap = self.eval_to_bitmap(inner);
        let mut complement = self.universe.clone();
        complement -= &inner_bitmap;
        complement
    }

    /// Estimate the fraction of entities matching a filter expression.
    ///
    /// Returns a value in [0.0, 1.0]. Uses index cardinalities and the
    /// independence assumption for compound predicates.
    #[must_use]
    pub fn selectivity(&self, expr: &FilterExpr) -> f64 {
        #[allow(clippy::cast_precision_loss)]
        let total = self.universe.len() as f64;
        if total == 0.0 {
            return 0.0;
        }

        #[allow(clippy::cast_precision_loss)]
        match expr {
            FilterExpr::CategoryEq(v) => self.category_index.cardinality(v) as f64 / total,
            FilterExpr::FormatEq(v) => self.format_index.cardinality(v) as f64 / total,
            FilterExpr::CreatorEq(id) => {
                self.creator_index.cardinality(&id.to_string()) as f64 / total
            }
            FilterExpr::Tag(v) => self.tag_index.cardinality(v) as f64 / total,
            FilterExpr::DurationMin(secs) => self.duration_index.selectivity(
                Bound::Included(secs),
                Bound::Unbounded,
                self.universe.len(),
            ),
            FilterExpr::DurationMax(secs) => self.duration_index.selectivity(
                Bound::Unbounded,
                Bound::Included(secs),
                self.universe.len(),
            ),
            FilterExpr::CreatedAfter(ns) => self.created_at_index.selectivity(
                Bound::Excluded(ns),
                Bound::Unbounded,
                self.universe.len(),
            ),
            FilterExpr::CreatedBefore(ns) => self.created_at_index.selectivity(
                Bound::Unbounded,
                Bound::Excluded(ns),
                self.universe.len(),
            ),
            FilterExpr::And(children) => {
                // Independence assumption: P(A AND B) ~ P(A) * P(B)
                children
                    .iter()
                    .map(|c| self.selectivity(c))
                    .product::<f64>()
                    .clamp(0.0, 1.0)
            }
            FilterExpr::Or(children) => {
                // Inclusion-exclusion: P(A OR B) ~ 1 - (1-P(A))(1-P(B))
                let complement_product: f64 =
                    children.iter().map(|c| 1.0 - self.selectivity(c)).product();
                (1.0 - complement_product).clamp(0.0, 1.0)
            }
            FilterExpr::Not(inner) => (1.0 - self.selectivity(inner)).clamp(0.0, 1.0),
            // User-state filters: selectivity is unknown without user state.
            // Estimate 0.9 for Unseen (most items are unseen for most users),
            // 0.95 for Unblocked, and low for Saved/Liked/InProgress.
            FilterExpr::Unseen(_) => 0.9,
            FilterExpr::Unblocked(_) => 0.95,
            FilterExpr::Saved(_) | FilterExpr::InProgress { .. } | FilterExpr::InCollection(_) => {
                0.05
            }
            FilterExpr::Liked(_) => 0.1,
            // Social graph: moderate selectivity -- constrains to followed-creator items.
            FilterExpr::SocialGraph { .. } => 0.2,
            // Signal threshold filters: moderate selectivity assumption.
            FilterExpr::MinSignal { .. } | FilterExpr::MaxSignal { .. } => 0.5,
            // Geo filter: moderate selectivity assumption.
            FilterExpr::NearLocation { .. } => 0.3,
        }
    }
}

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

    use super::*;

    fn now_ns() -> u64 {
        1_708_000_000_000_000_000u64
    }

    fn setup_test_indexes() -> (
        BitmapIndex,
        BitmapIndex,
        BitmapIndex,
        BitmapIndex,
        RangeIndex<u32>,
        RangeIndex<u64>,
        RoaringBitmap,
    ) {
        let cat = BitmapIndex::new("category");
        let fmt = BitmapIndex::new("format");
        let creator = BitmapIndex::new("creator");
        let tags = BitmapIndex::new("tags");
        let dur: RangeIndex<u32> = RangeIndex::new("duration");
        let ts: RangeIndex<u64> = RangeIndex::new("created_at");
        let mut universe = RoaringBitmap::new();

        // Populate: 10 items total
        for i in 0u32..10 {
            universe.insert(i);
            if i < 5 {
                cat.insert(i, "jazz");
            } else {
                cat.insert(i, "blues");
            }
            if i % 2 == 0 {
                fmt.insert(i, "video");
            } else {
                fmt.insert(i, "audio");
            }
            creator.insert(i, (i % 3).to_string());
            dur.insert(i, (i + 1) * 60); // durations: 60, 120, ... 600
            ts.insert(i, now_ns() - u64::from(i) * 86_400_000_000_000);
        }

        (cat, fmt, creator, tags, dur, ts, universe)
    }

    fn make_evaluator_full<'a>(
        cat: &'a BitmapIndex,
        fmt: &'a BitmapIndex,
        creator: &'a BitmapIndex,
        tags: &'a BitmapIndex,
        dur: &'a RangeIndex<u32>,
        ts: &'a RangeIndex<u64>,
        universe: &'a RoaringBitmap,
    ) -> FilterEvaluator<'a> {
        FilterEvaluator::new(cat, fmt, creator, tags, dur, ts, universe)
    }

    #[test]
    fn evaluate_single_category() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::CategoryEq("jazz".into()));
        assert!(!result.into_bitmap().is_empty());
    }

    #[test]
    fn evaluate_and_two_filters() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::And(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::FormatEq("video".into()),
        ]));
        let bm = result.into_bitmap();
        // jazz = 0..5, video = even = 0,2,4,6,8 -> intersection = 0,2,4
        assert_eq!(bm.len(), 3);
    }

    #[test]
    fn evaluate_or_two_categories() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::Or(vec![
            FilterExpr::CategoryEq("jazz".into()),
            FilterExpr::CategoryEq("blues".into()),
        ]));
        assert_eq!(result.into_bitmap().len(), 10);
    }

    #[test]
    fn evaluate_not_creator() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::Not(Box::new(FilterExpr::CreatorEq(0))));
        let bm = result.into_bitmap();
        // Creator 0 has items 0, 3, 6, 9 -> NOT = 1,2,4,5,7,8
        assert_eq!(bm.len(), 6);
    }

    #[test]
    fn evaluate_duration_min() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::DurationMin(300)); // >= 5min = items 4..=9
        let bm = result.into_bitmap();
        assert_eq!(bm.len(), 6);
    }

    #[test]
    fn evaluate_created_after() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let seven_days_ago = now_ns() - 7 * 86_400_000_000_000;
        let result = ev.evaluate(&FilterExpr::CreatedAfter(seven_days_ago));
        let bm = result.into_bitmap();
        // Items 0..6 are within 7 days (0=now, 6=6 days ago), item 7+ is older
        assert!(!bm.is_empty());
    }

    #[test]
    fn evaluate_nonexistent_category_short_circuits() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::And(vec![
            FilterExpr::CategoryEq("nonexistent".into()),
            FilterExpr::FormatEq("video".into()),
        ]));
        assert!(result.is_empty());
    }

    #[test]
    fn empty_and_returns_universe() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::And(vec![]));
        assert!(!result.into_bitmap().is_empty());
    }

    #[test]
    fn empty_or_returns_empty() {
        let (cat, fmt, creator, tags, dur, ts, universe) = setup_test_indexes();
        let ev = make_evaluator_full(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let result = ev.evaluate(&FilterExpr::Or(vec![]));
        assert!(result.is_empty());
    }

    #[test]
    fn selectivity_empty_universe() {
        let cat = BitmapIndex::new("category");
        let fmt = BitmapIndex::new("format");
        let creator = BitmapIndex::new("creator");
        let tags = BitmapIndex::new("tags");
        let dur: RangeIndex<u32> = RangeIndex::new("duration");
        let ts: RangeIndex<u64> = RangeIndex::new("created_at");
        let universe = RoaringBitmap::new();
        let ev = FilterEvaluator::new(&cat, &fmt, &creator, &tags, &dur, &ts, &universe);
        let sel = ev.selectivity(&FilterExpr::CategoryEq("jazz".into()));
        assert!((sel - 0.0).abs() < f64::EPSILON);
    }

    // === Property tests ===

    proptest! {
        #[test]
        fn and_equals_intersection(
            a_ids in prop::collection::hash_set(0u32..10_000, 1..200),
            b_ids in prop::collection::hash_set(0u32..10_000, 1..200),
        ) {
            let cat_index = BitmapIndex::new("category");
            let fmt_index = BitmapIndex::new("format");
            for &id in &a_ids { cat_index.insert(id, "jazz"); }
            for &id in &b_ids { fmt_index.insert(id, "video"); }

            let mut universe = RoaringBitmap::new();
            for &id in a_ids.iter().chain(b_ids.iter()) { universe.insert(id); }

            let creator = BitmapIndex::new("creator");
            let tags = BitmapIndex::new("tags");
            let dur: RangeIndex<u32> = RangeIndex::new("duration");
            let ts: RangeIndex<u64> = RangeIndex::new("created_at");
            let ev = FilterEvaluator::new(&cat_index, &fmt_index, &creator, &tags, &dur, &ts, &universe);

            let result = ev.evaluate(&FilterExpr::And(vec![
                FilterExpr::CategoryEq("jazz".into()),
                FilterExpr::FormatEq("video".into()),
            ]));

            let expected = {
                let a: RoaringBitmap = a_ids.iter().copied().collect();
                let b: RoaringBitmap = b_ids.iter().copied().collect();
                a & b
            };
            prop_assert_eq!(result.into_bitmap(), expected);
        }

        #[test]
        fn or_equals_union(
            a_ids in prop::collection::hash_set(0u32..10_000, 1..200),
            b_ids in prop::collection::hash_set(0u32..10_000, 1..200),
        ) {
            let cat_index = BitmapIndex::new("category");
            for &id in &a_ids { cat_index.insert(id, "jazz"); }
            for &id in &b_ids { cat_index.insert(id, "blues"); }

            let mut universe = RoaringBitmap::new();
            for &id in a_ids.iter().chain(b_ids.iter()) { universe.insert(id); }

            let fmt = BitmapIndex::new("format");
            let creator = BitmapIndex::new("creator");
            let tags = BitmapIndex::new("tags");
            let dur: RangeIndex<u32> = RangeIndex::new("duration");
            let ts: RangeIndex<u64> = RangeIndex::new("created_at");
            let ev = FilterEvaluator::new(&cat_index, &fmt, &creator, &tags, &dur, &ts, &universe);

            let result = ev.evaluate(&FilterExpr::Or(vec![
                FilterExpr::CategoryEq("jazz".into()),
                FilterExpr::CategoryEq("blues".into()),
            ]));

            let expected = {
                let a: RoaringBitmap = a_ids.iter().copied().collect();
                let b: RoaringBitmap = b_ids.iter().copied().collect();
                a | b
            };
            prop_assert_eq!(result.into_bitmap(), expected);
        }

        #[test]
        fn not_is_complement(
            all_ids in prop::collection::hash_set(0u32..10_000, 50..500),
            match_ids in prop::collection::hash_set(0u32..10_000, 1..200),
        ) {
            let cat_index = BitmapIndex::new("category");
            for &id in &match_ids { cat_index.insert(id, "jazz"); }

            let universe: RoaringBitmap = all_ids.iter().copied().collect();

            let fmt = BitmapIndex::new("format");
            let creator = BitmapIndex::new("creator");
            let tags = BitmapIndex::new("tags");
            let dur: RangeIndex<u32> = RangeIndex::new("duration");
            let ts: RangeIndex<u64> = RangeIndex::new("created_at");
            let ev = FilterEvaluator::new(&cat_index, &fmt, &creator, &tags, &dur, &ts, &universe);

            let result = ev.evaluate(&FilterExpr::Not(
                Box::new(FilterExpr::CategoryEq("jazz".into()))
            ));
            let result_bitmap = result.into_bitmap();

            let jazz_bitmap: RoaringBitmap = match_ids.iter()
                .filter(|id| all_ids.contains(id))
                .copied()
                .collect();
            let expected = &universe - &jazz_bitmap;

            prop_assert_eq!(result_bitmap, expected);
        }

        #[test]
        fn selectivity_in_unit_range(
            n_items in 1u32..10_000,
            n_jazz in 0u32..5_000,
            n_video in 0u32..5_000,
        ) {
            let cat_index = BitmapIndex::new("category");
            let fmt_index = BitmapIndex::new("format");
            let n_jazz = n_jazz.min(n_items);
            let n_video = n_video.min(n_items);
            for id in 0..n_jazz { cat_index.insert(id, "jazz"); }
            for id in 0..n_video { fmt_index.insert(id, "video"); }

            let universe: RoaringBitmap = (0..n_items).collect();
            let creator = BitmapIndex::new("creator");
            let tags = BitmapIndex::new("tags");
            let dur: RangeIndex<u32> = RangeIndex::new("duration");
            let ts: RangeIndex<u64> = RangeIndex::new("created_at");
            let ev = FilterEvaluator::new(&cat_index, &fmt_index, &creator, &tags, &dur, &ts, &universe);

            let exprs = vec![
                FilterExpr::CategoryEq("jazz".into()),
                FilterExpr::FormatEq("video".into()),
                FilterExpr::And(vec![
                    FilterExpr::CategoryEq("jazz".into()),
                    FilterExpr::FormatEq("video".into()),
                ]),
                FilterExpr::Or(vec![
                    FilterExpr::CategoryEq("jazz".into()),
                    FilterExpr::FormatEq("video".into()),
                ]),
                FilterExpr::Not(Box::new(FilterExpr::CategoryEq("jazz".into()))),
            ];

            for expr in &exprs {
                let sel = ev.selectivity(expr);
                prop_assert!((0.0..=1.0).contains(&sel),
                    "selectivity {sel} out of range for {expr:?}");
            }
        }

        #[test]
        fn predicate_matches_bitmap(
            ids in prop::collection::hash_set(0u32..10_000, 1..200),
            test_ids in prop::collection::vec(0u32..10_000, 1..100),
        ) {
            let cat_index = BitmapIndex::new("category");
            for &id in &ids { cat_index.insert(id, "jazz"); }

            let universe: RoaringBitmap = (0..10_000u32).collect();
            let fmt = BitmapIndex::new("format");
            let creator = BitmapIndex::new("creator");
            let tags = BitmapIndex::new("tags");
            let dur: RangeIndex<u32> = RangeIndex::new("duration");
            let ts: RangeIndex<u64> = RangeIndex::new("created_at");
            let ev = FilterEvaluator::new(&cat_index, &fmt, &creator, &tags, &dur, &ts, &universe);

            let result = ev.evaluate(&FilterExpr::CategoryEq("jazz".into()));
            let bitmap = result.into_bitmap();
            let bitmap_clone = bitmap.clone();
            let predicate = FilterResult::Bitmap(bitmap).into_predicate();

            for &test_id in &test_ids {
                prop_assert_eq!(
                    predicate(u64::from(test_id)),
                    bitmap_clone.contains(test_id),
                    "predicate disagreed with bitmap for id {}",
                    test_id,
                );
            }
        }
    }
}
