//! Adaptive query planner: strategy selection and execution.
//!
//! The [`AdaptiveQueryPlanner`] selects the optimal ANN strategy based on
//! estimated filter selectivity, then dispatches to the appropriate
//! [`VectorIndex`] method.

use std::time::Instant;

use super::super::{VectorError, VectorId, VectorIndex, VectorSearchResult};
use super::config::PlannerConfig;
use super::stats::AnnQueryStats;
use super::strategy::AnnStrategy;

/// Adaptive query planner for ANN search.
///
/// Selects the optimal [`AnnStrategy`] based on estimated filter selectivity,
/// then dispatches to the appropriate [`VectorIndex`] method. The planner is
/// stateless -- it does not cache results or self-tune. Configuration is
/// immutable after construction.
///
/// # Usage
///
/// ```rust,no_run
/// # use tidaldb::storage::vector::planner::*;
/// # use tidaldb::storage::vector::*;
/// let planner = AdaptiveQueryPlanner::with_defaults();
///
/// // The planner selects the strategy and dispatches the search.
/// // `selectivity` is provided by the caller (e.g., from a metadata index).
/// // let (results, stats) = planner.execute(
/// //     &index, &query, 100, Some(&filter), 0.15, None,
/// // ).unwrap();
/// ```
pub struct AdaptiveQueryPlanner {
    config: PlannerConfig,
}

impl AdaptiveQueryPlanner {
    /// Create a new planner with the given configuration.
    #[must_use]
    pub const fn new(config: PlannerConfig) -> Self {
        Self { config }
    }

    /// Create a new planner with default configuration.
    #[must_use]
    pub fn with_defaults() -> Self {
        Self {
            config: PlannerConfig::default(),
        }
    }

    /// Return a reference to the planner's configuration.
    #[must_use]
    pub const fn config(&self) -> &PlannerConfig {
        &self.config
    }

    /// Select the optimal ANN strategy for the given filter selectivity.
    ///
    /// # Decision tree
    ///
    /// | Selectivity          | Strategy                                    |
    /// |----------------------|---------------------------------------------|
    /// | 1.0 (or > 1.0)      | [`AnnStrategy::Unfiltered`]                 |
    /// | >= 0.20              | [`AnnStrategy::InGraphFilter`]              |
    /// | >= 0.05              | [`AnnStrategy::WidenedFilter`] (2x ef)      |
    /// | >= 0.01              | [`AnnStrategy::WidenedFilter`] (3x ef)      |
    /// | < 0.01              | [`AnnStrategy::PreFilterBruteForce`]         |
    #[must_use]
    pub fn select_strategy(&self, selectivity: f64) -> AnnStrategy {
        if (selectivity - 1.0).abs() < f64::EPSILON || selectivity > 1.0 {
            return AnnStrategy::Unfiltered;
        }

        if selectivity >= self.config.in_graph_min_selectivity {
            return AnnStrategy::InGraphFilter;
        }

        if selectivity >= self.config.brute_force_max_selectivity {
            let multiplier = if selectivity >= 0.05 {
                self.config.ef_search_multiplier_moderate
            } else {
                self.config.ef_search_multiplier_low
            };
            #[allow(
                clippy::cast_possible_truncation,
                clippy::cast_sign_loss,
                clippy::cast_precision_loss
            )]
            let ef = (self.config.default_ef_search as f64 * multiplier) as usize;
            return AnnStrategy::WidenedFilter { ef_search: ef };
        }

        AnnStrategy::PreFilterBruteForce
    }

    /// Execute an ANN query with automatic strategy selection.
    ///
    /// When `filter` is `None`, the strategy is always [`AnnStrategy::Unfiltered`]
    /// regardless of the `selectivity` parameter.
    ///
    /// When the selected strategy is [`AnnStrategy::PreFilterBruteForce`] but no
    /// `brute_force_index` is provided, the planner falls back to
    /// [`AnnStrategy::WidenedFilter`] with the low-selectivity multiplier (3x ef)
    /// and logs a warning.
    ///
    /// # Errors
    ///
    /// Returns [`VectorError`] if the underlying index search fails (e.g.,
    /// dimension mismatch, backend error).
    pub fn execute(
        &self,
        index: &dyn VectorIndex,
        query: &[f32],
        k: usize,
        filter: Option<&dyn Fn(VectorId) -> bool>,
        selectivity: f64,
        brute_force_index: Option<&dyn VectorIndex>,
    ) -> Result<(Vec<VectorSearchResult>, AnnQueryStats), VectorError> {
        let strategy = match &filter {
            None => AnnStrategy::Unfiltered,
            Some(_) => self.select_strategy(selectivity),
        };

        tracing::debug!(
            strategy = ?strategy,
            selectivity = selectivity,
            k = k,
            "adaptive query planner: executing ANN query"
        );

        let start = Instant::now();

        let results = match (&strategy, filter) {
            (AnnStrategy::Unfiltered, _) => {
                index.search(query, k, self.config.default_ef_search)?
            }
            (AnnStrategy::InGraphFilter, Some(f)) => {
                index.filtered_search(query, k, self.config.default_ef_search, f)?
            }
            (AnnStrategy::WidenedFilter { ef_search }, Some(f)) => {
                index.filtered_search(query, k, *ef_search, f)?
            }
            (AnnStrategy::PreFilterBruteForce, Some(f)) => {
                if let Some(bf) = brute_force_index {
                    bf.filtered_search(query, k, 0, f)?
                } else {
                    // Fallback: use widened filter with low-selectivity multiplier.
                    // This is a degraded path -- the caller should provide a brute-force
                    // index for optimal recall at extreme selectivity.
                    tracing::warn!(
                        "PreFilterBruteForce selected but no brute-force index available; \
                         falling back to WidenedFilter"
                    );
                    #[allow(
                        clippy::cast_possible_truncation,
                        clippy::cast_sign_loss,
                        clippy::cast_precision_loss
                    )]
                    let ef = (self.config.default_ef_search as f64
                        * self.config.ef_search_multiplier_low)
                        as usize;
                    index.filtered_search(query, k, ef, f)?
                }
            }
            // Defensive: if strategy is InGraphFilter/WidenedFilter but filter is None
            // (should never happen given the match above), fall back to unfiltered.
            _ => index.search(query, k, self.config.default_ef_search)?,
        };

        let latency = start.elapsed();

        let stats = AnnQueryStats {
            estimated_selectivity: selectivity,
            strategy,
            results_returned: results.len(),
            requested_k: k,
            latency,
        };

        Ok((results, stats))
    }
}

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

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::time::Duration;

    use super::*;
    use crate::storage::vector::{
        BruteForceIndex, DistanceMetric, QuantizationLevel, VectorIndexConfig,
    };

    /// Helper: create a 4-dimensional brute-force index with `n` vectors.
    fn make_brute_index(n: u64) -> BruteForceIndex {
        let config = VectorIndexConfig {
            dimensions: 4,
            metric: DistanceMetric::L2,
            quantization: QuantizationLevel::F32,
            connectivity: 16,
            ef_construction: 200,
            ef_search: 200,
        };
        let index = BruteForceIndex::new(config);
        for i in 0..n {
            #[allow(clippy::cast_precision_loss)]
            let v = [i as f32 * 0.1, 0.0, 0.0, 0.0];
            index.insert(i, &v).unwrap();
        }
        index
    }

    // -------------------------------------------------------------------
    // select_strategy() tests
    // -------------------------------------------------------------------

    #[test]
    fn strategy_unfiltered_at_100pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        assert_eq!(planner.select_strategy(1.0), AnnStrategy::Unfiltered);
    }

    #[test]
    fn strategy_in_graph_above_20pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        assert_eq!(planner.select_strategy(0.50), AnnStrategy::InGraphFilter);
        assert_eq!(planner.select_strategy(0.25), AnnStrategy::InGraphFilter);
        assert_eq!(planner.select_strategy(0.20), AnnStrategy::InGraphFilter);
    }

    #[test]
    fn strategy_widened_moderate_5_to_20pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        assert_eq!(
            planner.select_strategy(0.10),
            AnnStrategy::WidenedFilter { ef_search: 400 }
        );
    }

    #[test]
    fn strategy_widened_low_1_to_5pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        assert_eq!(
            planner.select_strategy(0.03),
            AnnStrategy::WidenedFilter { ef_search: 600 }
        );
    }

    #[test]
    fn strategy_brute_force_below_1pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        assert_eq!(
            planner.select_strategy(0.005),
            AnnStrategy::PreFilterBruteForce
        );
        assert_eq!(
            planner.select_strategy(0.001),
            AnnStrategy::PreFilterBruteForce
        );
        assert_eq!(
            planner.select_strategy(0.0),
            AnnStrategy::PreFilterBruteForce
        );
    }

    #[test]
    fn strategy_boundary_at_20pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        // Exactly 0.20 => InGraphFilter
        assert_eq!(planner.select_strategy(0.20), AnnStrategy::InGraphFilter);
        // Just below 0.20 => WidenedFilter
        assert_eq!(
            planner.select_strategy(0.19),
            AnnStrategy::WidenedFilter { ef_search: 400 }
        );
    }

    #[test]
    fn strategy_boundary_at_1pct() {
        let planner = AdaptiveQueryPlanner::with_defaults();
        // Exactly 0.01 => WidenedFilter (3x)
        assert_eq!(
            planner.select_strategy(0.01),
            AnnStrategy::WidenedFilter { ef_search: 600 }
        );
        // Just below 0.01 => PreFilterBruteForce
        assert_eq!(
            planner.select_strategy(0.009),
            AnnStrategy::PreFilterBruteForce
        );
    }

    #[test]
    fn custom_thresholds() {
        let config = PlannerConfig {
            in_graph_min_selectivity: 0.30,
            brute_force_max_selectivity: 0.05,
            ef_search_multiplier_moderate: 1.5,
            ef_search_multiplier_low: 4.0,
            default_ef_search: 100,
        };
        let planner = AdaptiveQueryPlanner::new(config);

        // Above 30% => InGraphFilter
        assert_eq!(planner.select_strategy(0.35), AnnStrategy::InGraphFilter);

        // 5-30% with >= 5% => moderate multiplier
        assert_eq!(
            planner.select_strategy(0.10),
            AnnStrategy::WidenedFilter { ef_search: 150 }
        );

        // Below 5% => PreFilterBruteForce
        assert_eq!(
            planner.select_strategy(0.04),
            AnnStrategy::PreFilterBruteForce
        );
    }

    // -------------------------------------------------------------------
    // execute() tests
    // -------------------------------------------------------------------

    #[test]
    fn execute_unfiltered() {
        let index = make_brute_index(10);
        let planner = AdaptiveQueryPlanner::with_defaults();
        let query = [0.5, 0.0, 0.0, 0.0];

        let (results, stats) = planner.execute(&index, &query, 2, None, 1.0, None).unwrap();

        assert_eq!(results.len(), 2);
        assert_eq!(stats.strategy, AnnStrategy::Unfiltered);
        assert_eq!(stats.requested_k, 2);
        assert_eq!(stats.results_returned, 2);
    }

    #[test]
    fn execute_filtered_in_graph() {
        let index = make_brute_index(10);
        let planner = AdaptiveQueryPlanner::with_defaults();
        let query = [0.5, 0.0, 0.0, 0.0];

        // 50% selectivity => InGraphFilter. Only even IDs pass.
        let filter = |id: VectorId| id.is_multiple_of(2);
        let (results, stats) = planner
            .execute(&index, &query, 10, Some(&filter), 0.50, None)
            .unwrap();

        assert_eq!(stats.strategy, AnnStrategy::InGraphFilter);
        // All results must be even IDs.
        for r in &results {
            assert_eq!(r.id % 2, 0, "odd ID {} in even-only filtered search", r.id);
        }
    }

    #[test]
    fn execute_brute_force_fallback_without_brute_index() {
        let index = make_brute_index(10);
        let planner = AdaptiveQueryPlanner::with_defaults();
        let query = [0.5, 0.0, 0.0, 0.0];

        // 0.5% selectivity => PreFilterBruteForce, but no brute_force_index.
        // Should fall back to WidenedFilter.
        let filter = |id: VectorId| id.is_multiple_of(2);
        let (results, stats) = planner
            .execute(&index, &query, 10, Some(&filter), 0.005, None)
            .unwrap();

        // The stats still report PreFilterBruteForce (the selected strategy),
        // but the execution fell back to WidenedFilter internally.
        assert_eq!(stats.strategy, AnnStrategy::PreFilterBruteForce);
        assert!(!results.is_empty());
    }

    #[test]
    fn execute_brute_force_with_brute_index() {
        let index = make_brute_index(10);
        let brute = make_brute_index(10);
        let planner = AdaptiveQueryPlanner::with_defaults();
        let query = [0.5, 0.0, 0.0, 0.0];

        // 0.5% selectivity => PreFilterBruteForce, with brute_force_index.
        let filter = |id: VectorId| id.is_multiple_of(2);
        let (results, stats) = planner
            .execute(
                &index,
                &query,
                10,
                Some(&filter),
                0.005,
                Some(&brute as &dyn VectorIndex),
            )
            .unwrap();

        assert_eq!(stats.strategy, AnnStrategy::PreFilterBruteForce);
        for r in &results {
            assert_eq!(r.id % 2, 0, "odd ID {} in even-only filtered search", r.id);
        }
    }

    #[test]
    fn ann_query_stats_captures_latency() {
        let index = make_brute_index(10);
        let planner = AdaptiveQueryPlanner::with_defaults();
        let query = [0.5, 0.0, 0.0, 0.0];

        let (_, stats) = planner.execute(&index, &query, 5, None, 1.0, None).unwrap();

        // Latency must be non-zero (even brute-force takes some time).
        assert!(stats.latency > Duration::ZERO);
    }
}
