//! RETRIEVE and SEARCH query execution on `TidalDb`.

use crate::query::executor::RetrieveExecutor;
use crate::query::retrieve::{QueryError, Results, Retrieve};
use crate::query::search::{Search, SearchExecutor, SearchResults};
use crate::schema::{EntityKind, TidalError};
use crate::session as session_mod;

use super::TidalDb;
use super::storage_box::StorageBox;

impl TidalDb {
    /// Execute a RETRIEVE query against the database.
    ///
    /// Constructs a per-query `RetrieveExecutor` with borrowed references to
    /// all infrastructure, then runs the 5-stage pipeline:
    ///
    /// 1. Candidate generation (scan universe or signal-ranked)
    /// 2. Filter evaluation (bitmap + range indexes)
    /// 3. Signal scoring (profile executor)
    /// 4. Diversity enforcement (per-creator, format-mix)
    /// 5. Result assembly (pagination, cursor, explain)
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Query` on validation failure, missing profile,
    /// or unsupported candidate strategy.
    #[tracing::instrument(skip_all, fields(profile = %query.profile.name, limit = query.limit))]
    pub fn retrieve(&self, query: &Retrieve) -> crate::Result<Results> {
        // Guard against oversized filter expressions.
        const MAX_FILTER_NODES: usize = 256;
        if let Some(ref filter) = query.combined_filter() {
            let count = filter.node_count();
            if count > MAX_FILTER_NODES {
                return Err(TidalError::invalid_input(format!(
                    "filter expression too complex: {count} nodes exceeds max {MAX_FILTER_NODES}"
                )));
            }
        }

        // Record query latency from this point forward.
        #[cfg(feature = "metrics")]
        let query_start = std::time::Instant::now();

        // Enter the load detector. The guard decrements the in-flight counter
        // on drop (method return, success or error).
        let (degradation_level, _inflight_guard) = self.load_detector.enter();
        #[cfg(feature = "metrics")]
        self.metrics.degradation_level.store(
            degradation_level as u64,
            std::sync::atomic::Ordering::Relaxed,
        );

        tracing::debug!(
            degradation = %degradation_level,
            in_flight = self.load_detector.in_flight(),
            "retrieve: query entry"
        );

        let ledger = self.ledger()?;

        // Attach items storage for keyword-hint metadata lookup (session scoring).
        let items_storage_opt = self.storage.as_ref().map(StorageBox::items_engine);

        let mut base_executor = RetrieveExecutor::new(
            ledger,
            &self.profile_registry,
            Some(&self.category_index),
            Some(&self.format_index),
            Some(&self.creator_index),
            Some(&self.tag_index),
            Some(&self.duration_index),
            Some(&self.created_at_index),
            Some(&self.universe),
            Some(&self.embedding_registry),
        )
        .with_user_context(
            &self.user_state,
            &self.hard_negatives,
            &self.interaction_ledger,
            &self.creator_items,
        )
        .with_preference_vectors(&self.preference_vectors);

        if let Some(storage) = items_storage_opt {
            base_executor = base_executor.with_items_storage(storage);
        }

        // M6: wire co-engagement for related profile scoring.
        base_executor = base_executor.with_co_engagement(&self.co_engagement);

        // M6p6: wire notification tracker for notification capping.
        base_executor = base_executor.with_notification_tracker(&self.notification_tracker);

        // M6p4: wire collection index for InCollection filter evaluation.
        base_executor = base_executor.with_collection_index(&self.collection_index);

        // M6: wire per-user signal index for social-graph-scoped trending.
        base_executor = base_executor.with_user_signal_index(&self.user_signal_index);

        // M6: wire cohort context for cohort-scoped signal reads.
        base_executor =
            base_executor.with_cohort_context(&self.cohort_ledger, &self.cohort_registry);

        // M7p2: wire degradation level into the executor.
        base_executor = base_executor.with_degradation_level(degradation_level);

        // M4: wire in session context when FOR SESSION is specified.
        let executor = if let Some(session_id) = query.for_session {
            match self.session_snapshot(session_id) {
                Ok(snapshot) => {
                    let ctx = session_mod::SessionContext::from_snapshot(&snapshot);
                    base_executor.with_session(ctx, snapshot)
                }
                Err(_) => {
                    return Err(TidalError::Query(QueryError::SessionNotFound(format!(
                        "{session_id}"
                    ))));
                }
            }
        } else {
            base_executor
        };

        let result = executor.execute(query).map_err(TidalError::from);
        #[cfg(feature = "metrics")]
        {
            let elapsed_us = query_start.elapsed().as_micros() as u64;
            self.metrics.retrieve_latency.observe(elapsed_us);
        }
        result
    }

    /// Execute a SEARCH query -- text and/or vector retrieval with RRF fusion,
    /// signal-based profile scoring, filtering, and optional diversity.
    ///
    /// The 8-stage pipeline is:
    /// 1a. BM25 text retrieval (when `query_text` is set)
    /// 1b. ANN vector retrieval (when `query_vector` is set)
    /// 1c. Fusion via Reciprocal Rank Fusion
    /// 2. Metadata filter evaluation
    /// 2.5. User-context filtering (seen, blocked, hard negatives)
    /// 3. Profile scoring (signal-weighted, personalized when `for_user` is set)
    /// 4. Diversity enforcement (when `diversity` is set)
    /// 5. Result assembly with BM25 and semantic score explainability
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Query` on validation failure, missing profile,
    /// or storage error during retrieval.
    #[allow(clippy::too_many_lines)]
    #[tracing::instrument(skip_all, fields(query = ?query.query_text, kind = ?query.entity_kind))]
    pub fn search(&self, query: &Search) -> crate::Result<SearchResults> {
        // Record query latency from this point forward.
        #[cfg(feature = "metrics")]
        let query_start = std::time::Instant::now();

        // Guard against oversized filter expressions.
        const MAX_FILTER_NODES: usize = 256;
        if let Some(ref filter) = query.combined_filter() {
            let count = filter.node_count();
            if count > MAX_FILTER_NODES {
                return Err(TidalError::invalid_input(format!(
                    "filter expression too complex: {count} nodes exceeds max {MAX_FILTER_NODES}"
                )));
            }
        }

        // Enter the load detector. The guard decrements the in-flight counter
        // on drop (method return, success or error).
        let (degradation_level, _inflight_guard) = self.load_detector.enter();
        #[cfg(feature = "metrics")]
        self.metrics.degradation_level.store(
            degradation_level as u64,
            std::sync::atomic::Ordering::Relaxed,
        );

        tracing::debug!(
            degradation = %degradation_level,
            in_flight = self.load_detector.in_flight(),
            "search: query entry"
        );

        let ledger = self.ledger()?;

        // Resolve similar_to: read stored embedding and inject as query_vector.
        let mut query_owned;
        let query = if let (Some(similar_id), None) = (query.similar_to, &query.query_vector) {
            let emb = match query.entity_kind {
                EntityKind::Creator => self.read_creator_embedding(similar_id)?,
                EntityKind::Item => self.read_item_embedding(similar_id)?,
                EntityKind::User => None,
            };
            if let Some(embedding) = emb {
                query_owned = query.clone();
                query_owned.query_vector = Some(embedding);
                // Exclude the source entity from results.
                if !query_owned.exclude.contains(&similar_id) {
                    query_owned.exclude.push(similar_id);
                }
                &query_owned
            } else {
                query
            }
        } else {
            query
        };

        let text_index_ref = self.text_index.as_ref();
        let items_storage_opt = self.storage.as_ref().map(StorageBox::items_engine);

        let mut base_executor = SearchExecutor::new(
            ledger,
            &self.profile_registry,
            text_index_ref, // item text index (None for Creator queries, but executor routes)
            Some(&self.embedding_registry),
            Some(&self.category_index),
            Some(&self.format_index),
            Some(&self.creator_index),
            Some(&self.tag_index),
            Some(&self.duration_index),
            Some(&self.created_at_index),
            Some(&self.universe),
        )
        .with_user_context(
            &self.user_state,
            &self.hard_negatives,
            &self.interaction_ledger,
            &self.creator_items,
        )
        .with_preference_vectors(&self.preference_vectors);

        if let Some(storage) = items_storage_opt {
            base_executor = base_executor.with_items_storage(storage);
        }

        // M6p5: wire cohort context for WithinScope::CohortTrending.
        base_executor =
            base_executor.with_cohort_context(&self.cohort_ledger, &self.cohort_registry);

        // M6p5: wire collection index for WithinScope::Collection.
        base_executor = base_executor.with_collection_index(&self.collection_index);

        // Route creator text index and creators storage for creator searches.
        if query.entity_kind == EntityKind::Creator {
            if let Some(idx) = self.creator_text_index.as_ref() {
                base_executor = base_executor.with_creator_text_index(idx);
            }
            if let Ok(storage) = self.storage() {
                base_executor = base_executor.with_creators_storage(storage.creators_engine());
            }
        }

        // M7p2: wire degradation level into the executor.
        base_executor = base_executor.with_degradation_level(degradation_level);

        // Wire in session context when FOR SESSION is specified.
        let executor = if let Some(session_id) = query.for_session {
            match self.session_snapshot(session_id) {
                Ok(snapshot) => {
                    let ctx = session_mod::SessionContext::from_snapshot(&snapshot);
                    base_executor.with_session(ctx, snapshot)
                }
                Err(e) => {
                    tracing::warn!(
                        session_id = %session_id,
                        error = %e,
                        "FOR SESSION: session not found; executing without session boost"
                    );
                    base_executor
                }
            }
        } else {
            base_executor
        };

        let result = executor.execute(query).map_err(TidalError::from);

        #[cfg(feature = "metrics")]
        {
            let elapsed_us = query_start.elapsed().as_micros() as u64;
            self.metrics.search_latency.observe(elapsed_us);
        }

        let result = result?;

        // M6p5: record query text for trending autocomplete suggestions.
        if let Some(ref text) = query.query_text {
            self.suggestion_index.record_query(text);
        }

        Ok(result)
    }

    /// Autocomplete suggestions for a query prefix.
    ///
    /// Returns matching title terms if `prefix` is non-empty, or trending search
    /// queries if `prefix` is empty.
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Query` if `limit` is 0 or greater than 50.
    pub fn suggest(
        &self,
        req: &crate::query::suggest::Suggest,
    ) -> crate::Result<Vec<crate::query::suggest::Suggestion>> {
        if req.limit == 0 || req.limit > 50 {
            return Err(TidalError::Query(
                crate::query::retrieve::QueryError::InvalidLimit {
                    requested: req.limit as usize,
                    min: 1,
                    max: 50,
                },
            ));
        }
        let results = self
            .suggestion_index
            .suggest(&req.prefix, req.limit as usize);
        Ok(results)
    }
}
