//! Filter evaluation result type.
//!
//! `FilterResult` wraps the output of evaluating a `FilterExpr` -- either a
//! `RoaringBitmap` of matching entity IDs or a predicate closure for
//! per-candidate evaluation.

use roaring::RoaringBitmap;

use crate::storage::indexes::IndexError;

/// The result of evaluating a filter expression.
pub enum FilterResult {
    /// A bitmap of matching entity IDs.
    Bitmap(RoaringBitmap),
    /// A predicate closure for per-candidate evaluation.
    Predicate(Box<dyn Fn(u64) -> bool + Send + Sync>),
}

impl FilterResult {
    /// Extract the bitmap representation, if this result is materialized.
    ///
    /// `FilterResult::Predicate` cannot be losslessly converted to a bitmap
    /// without enumerating the entity universe, so this returns
    /// [`IndexError::ExpectedBitmap`] for the predicate variant rather than
    /// panicking. Both variants are publicly constructible, so a panic here
    /// would be reachable from outside the crate.
    ///
    /// # Errors
    ///
    /// Returns [`IndexError::ExpectedBitmap`] if called on a
    /// `FilterResult::Predicate`.
    pub fn try_into_bitmap(self) -> Result<RoaringBitmap, IndexError> {
        match self {
            Self::Bitmap(bm) => Ok(bm),
            Self::Predicate(_) => Err(IndexError::ExpectedBitmap),
        }
    }

    /// Extract the bitmap representation, degrading the predicate variant to
    /// an empty bitmap.
    ///
    /// This is the infallible convenience over
    /// [`try_into_bitmap`](Self::try_into_bitmap). Because a
    /// `Predicate` cannot be materialized without enumerating the universe, the
    /// predicate variant is logged and treated as the empty set rather than
    /// panicking -- both variants are publicly constructible, so a panic would
    /// be reachable from outside the crate. Callers that must distinguish the
    /// predicate path should use [`try_into_bitmap`](Self::try_into_bitmap).
    #[must_use]
    pub fn into_bitmap(self) -> RoaringBitmap {
        if let Ok(bm) = self.try_into_bitmap() {
            bm
        } else {
            tracing::warn!(
                "FilterResult::into_bitmap called on a Predicate; \
                 returning empty set (use try_into_bitmap to handle explicitly)"
            );
            RoaringBitmap::new()
        }
    }

    /// Convert to a predicate closure checking bitmap containment.
    ///
    /// # u32 item-universe bound
    ///
    /// `RoaringBitmap` keys entities by `u32`, so the item universe is bounded
    /// to `u32::MAX` (~4 billion). That bound is **enforced at the write
    /// boundary**: `TidalDb::write_item_with_metadata` (and the sibling signal /
    /// relationship / collection write paths) reject any `EntityId` whose
    /// `u64` value exceeds `u32::MAX` with `TidalError::invalid_input` *before*
    /// any durable write, so an out-of-range id can never reach a bitmap index.
    ///
    /// This closure honors that bound defensively: a `u64` id that does not fit
    /// in `u32` can never be present in a `u32`-keyed bitmap, so it returns
    /// `false` rather than truncating `id as u32` (which would wrap a huge id
    /// down onto a real lower id and falsely report a match). The `debug_assert`
    /// fires in debug builds to surface any id that slipped past the write
    /// boundary; release builds degrade safely to a non-match.
    #[must_use]
    pub fn into_predicate(self) -> Box<dyn Fn(u64) -> bool + Send + Sync> {
        match self {
            Self::Bitmap(bitmap) => Box::new(move |id: u64| {
                let id32 = u32::try_from(id);
                // The write boundary rejects ids above u32::MAX, so this should
                // always succeed; assert it in debug to surface any id that
                // slipped past. In release, an out-of-range id is reported as a
                // non-match (it can never be present in a u32-keyed bitmap),
                // never truncated onto a colliding lower id.
                debug_assert!(
                    id32.is_ok(),
                    "EntityId {id} out of u32 range reached bitmap predicate"
                );
                id32.is_ok_and(|i| bitmap.contains(i))
            }),
            Self::Predicate(f) => f,
        }
    }

    /// Number of entities matching the filter, when knowable.
    ///
    /// Returns `Some(n)` for the materialized [`Bitmap`](Self::Bitmap) variant
    /// and `None` for [`Predicate`](Self::Predicate): a predicate's match count
    /// cannot be known without enumerating the entity universe. Returning
    /// `Option` instead of a sentinel `0` forces callers (buffer sizing, plan
    /// selection) to handle the unknown case explicitly rather than silently
    /// treating a predicate as matching nothing.
    #[must_use]
    pub fn cardinality(&self) -> Option<u64> {
        match self {
            Self::Bitmap(bm) => Some(bm.len()),
            Self::Predicate(_) => None,
        }
    }

    /// Whether no entities match, when knowable.
    ///
    /// Returns `Some(true)`/`Some(false)` for the materialized
    /// [`Bitmap`](Self::Bitmap) variant and `None` for
    /// [`Predicate`](Self::Predicate): emptiness cannot be decided without
    /// evaluating the predicate over the universe. Returning `Option` instead
    /// of a definitive `false` prevents a short-circuit caller from wrongly
    /// concluding a predicate result is non-empty.
    #[must_use]
    pub fn is_empty(&self) -> Option<bool> {
        match self {
            Self::Bitmap(bm) => Some(bm.is_empty()),
            Self::Predicate(_) => None,
        }
    }
}

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

    #[test]
    fn into_predicate_checks_bitmap_containment() {
        let mut bitmap = RoaringBitmap::new();
        bitmap.insert(1);
        bitmap.insert(42);
        bitmap.insert(999);

        let result = FilterResult::Bitmap(bitmap);
        let predicate = result.into_predicate();

        assert!(predicate(1));
        assert!(predicate(42));
        assert!(predicate(999));
        assert!(!predicate(0));
        assert!(!predicate(2));
        assert!(!predicate(1000));
    }

    #[test]
    fn try_into_bitmap_returns_bitmap_for_bitmap_variant() {
        let mut bitmap = RoaringBitmap::new();
        bitmap.insert(7);
        let result = FilterResult::Bitmap(bitmap);
        let bm = result
            .try_into_bitmap()
            .expect("bitmap variant must succeed");
        assert!(bm.contains(7));
    }

    #[test]
    fn try_into_bitmap_errors_on_predicate_variant() {
        // Predicate is publicly constructible; converting must error, not panic.
        let result = FilterResult::Predicate(Box::new(|id| id == 1));
        let err = result
            .try_into_bitmap()
            .expect_err("predicate variant must error");
        assert!(matches!(err, IndexError::ExpectedBitmap));
    }

    #[test]
    fn predicate_handles_u32_max_in_range() {
        // u32::MAX is the largest legal item id (the write boundary rejects
        // anything above it). It must round-trip through the predicate without
        // truncation, and an id one below must not match.
        let mut bitmap = RoaringBitmap::new();
        bitmap.insert(u32::MAX);
        let predicate = FilterResult::Bitmap(bitmap).into_predicate();
        assert!(predicate(u64::from(u32::MAX)));
        assert!(!predicate(u64::from(u32::MAX) - 1));
    }

    #[test]
    fn cardinality_is_known_for_bitmap_unknown_for_predicate() {
        let mut bitmap = RoaringBitmap::new();
        bitmap.insert(1);
        bitmap.insert(2);
        bitmap.insert(3);
        assert_eq!(FilterResult::Bitmap(bitmap).cardinality(), Some(3));

        // Predicate cardinality is unknowable without enumerating the universe.
        let pred = FilterResult::Predicate(Box::new(|id| id == 1));
        assert_eq!(pred.cardinality(), None);
    }

    #[test]
    fn is_empty_is_known_for_bitmap_unknown_for_predicate() {
        assert_eq!(
            FilterResult::Bitmap(RoaringBitmap::new()).is_empty(),
            Some(true)
        );
        let mut bitmap = RoaringBitmap::new();
        bitmap.insert(7);
        assert_eq!(FilterResult::Bitmap(bitmap).is_empty(), Some(false));

        // Predicate emptiness is unknowable; must be None, never a definitive false.
        let pred = FilterResult::Predicate(Box::new(|_| true));
        assert_eq!(pred.is_empty(), None);
    }
}
