//! Selectivity estimation for adaptive query planning.
//!
//! The [`SelectivityEstimator`] trait provides a pluggable interface for
//! estimating what fraction of vectors pass a filter predicate.
//! [`FixedSelectivityEstimator`] is the simplest implementation -- a
//! caller-provided constant.

use super::super::VectorId;

/// Estimates the fraction of vectors that pass a given filter predicate.
///
/// Selectivity is in `[0.0, 1.0]` where `1.0` means all vectors pass and
/// `0.0` means none pass. The planner uses this estimate to select the
/// optimal ANN strategy.
///
/// Implementations range from simple (caller-provided constant) to
/// sophisticated (metadata index cardinality estimation). The trait
/// abstraction allows the planner to work with any estimation approach.
pub trait SelectivityEstimator: Send + Sync {
    /// Estimate the selectivity of the given filter predicate.
    ///
    /// The `filter` argument is provided for implementations that sample
    /// a subset of vector IDs to estimate pass rate. Simple implementations
    /// (e.g., [`FixedSelectivityEstimator`]) ignore it.
    fn estimate_selectivity(&self, filter: &dyn Fn(VectorId) -> bool) -> f64;
}

/// A selectivity estimator that always returns a caller-provided constant.
///
/// Useful when the caller already knows the selectivity (e.g., from a
/// metadata index cardinality query) and does not need runtime estimation.
pub struct FixedSelectivityEstimator {
    selectivity: f64,
}

impl FixedSelectivityEstimator {
    /// Create a new estimator with the given selectivity, clamped to `[0.0, 1.0]`.
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // clamp is not const
    pub fn new(selectivity: f64) -> Self {
        Self {
            selectivity: selectivity.clamp(0.0, 1.0),
        }
    }

    /// Update the selectivity value, clamped to `[0.0, 1.0]`.
    #[allow(clippy::missing_const_for_fn)] // clamp is not const
    pub fn set_selectivity(&mut self, selectivity: f64) {
        self.selectivity = selectivity.clamp(0.0, 1.0);
    }
}

impl SelectivityEstimator for FixedSelectivityEstimator {
    fn estimate_selectivity(&self, _filter: &dyn Fn(VectorId) -> bool) -> f64 {
        self.selectivity
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn fixed_selectivity_estimator() {
        // Normal value
        let est = FixedSelectivityEstimator::new(0.5);
        assert!((est.estimate_selectivity(&|_| true) - 0.5).abs() < f64::EPSILON);

        // Clamped above 1.0
        let est = FixedSelectivityEstimator::new(1.5);
        assert!((est.estimate_selectivity(&|_| true) - 1.0).abs() < f64::EPSILON);

        // Clamped below 0.0
        let est = FixedSelectivityEstimator::new(-0.5);
        assert!((est.estimate_selectivity(&|_| true) - 0.0).abs() < f64::EPSILON);

        // set_selectivity also clamps
        let mut est = FixedSelectivityEstimator::new(0.5);
        est.set_selectivity(2.0);
        assert!((est.estimate_selectivity(&|_| true) - 1.0).abs() < f64::EPSILON);
        est.set_selectivity(-1.0);
        assert!((est.estimate_selectivity(&|_| true) - 0.0).abs() < f64::EPSILON);
    }
}
