//! SEARCH query executor -- 8-stage pipeline.
//!
//! Implements the pipeline that turns a `Search` AST into `SearchResults`:
//! 1a. Text retrieval (BM25 via Tantivy), 1b. ANN retrieval, 1c. Fusion (RRF),
//! 2. Metadata filter, 2.5. User-context filter, 3. Profile scoring,
//! 4. Diversity enforcement, 5. Result assembly.

use std::sync::{Arc, RwLock};

use roaring::RoaringBitmap;

use crate::{
    entities::{
        CreatorItemsBitmap, HardNegIndex, InteractionLedger, PreferenceVectors, UserStateIndex,
    },
    ranking::registry::ProfileRegistry,
    session::{SessionContext, SessionSnapshot},
    signals::SignalLedger,
    storage::{
        StorageEngine,
        indexes::{bitmap::BitmapIndex, range::RangeIndex},
        vector::registry::EmbeddingSlotRegistry,
    },
};

// ── Candidate-cap tuning constants ────────────────────────────────────────────
//
// Retrieval over-fetches more candidates than the final `limit` so that
// downstream filtering, profile scoring, and diversity have a pool to work
// from. These constants name the previously-inline magic numbers and document
// the multipliers/floors/ceilings so the ANN and BM25 caps stay in sync.

/// ANN over-fetch multiplier at full fidelity: `k = limit * this`, floored at
/// [`ANN_K_FLOOR`]. Wider than the BM25 multiplier because HNSW recall improves
/// with a larger `k` before post-filtering trims the set.
pub(crate) const ANN_K_MULTIPLIER_FULL: usize = 20;

/// ANN over-fetch multiplier under `ReducedCandidates` degradation:
/// `k = (limit * this).clamp(ANN_K_FLOOR, ANN_K_CEILING_REDUCED)`. Cuts ANN
/// search work by ~60% for a typical `limit = 20` query.
pub(crate) const ANN_K_MULTIPLIER_REDUCED: usize = 10;

/// Minimum ANN `k` regardless of `limit`, so tiny-limit queries still retrieve
/// a usable candidate pool.
pub(crate) const ANN_K_FLOOR: usize = 200;

/// Maximum ANN `k` under `ReducedCandidates` degradation.
pub(crate) const ANN_K_CEILING_REDUCED: usize = 200;

/// Lower clamp for ANN `k` under `ReducedCandidates` degradation.
pub(crate) const ANN_K_FLOOR_REDUCED: usize = 100;

/// BM25 truncation multiplier under `ReducedCandidates` degradation:
/// `cap = (limit * this).max(BM25_CAP_FLOOR_REDUCED)`. Caps downstream scoring
/// work by trimming the BM25 list before fusion.
pub(crate) const BM25_CAP_MULTIPLIER_REDUCED: usize = 10;

/// Minimum BM25 cap under `ReducedCandidates` degradation.
pub(crate) const BM25_CAP_FLOOR_REDUCED: usize = 100;

// ── SearchExecutor ───────────────────────────────────────────────────────────

/// Executes SEARCH queries through the 8-stage pipeline.
///
/// Constructed per-query by `TidalDb::search()`. Holds borrowed references
/// to all infrastructure needed for BM25 retrieval, ANN retrieval, fusion,
/// filtering, signal scoring, and diversity enforcement.
///
/// Index references are `Option` because the database may not have all indexes
/// available. The executor degrades gracefully when indexes are absent.
pub struct SearchExecutor<'a> {
    ledger: &'a SignalLedger,
    profile_registry: &'a ProfileRegistry,
    /// Text index for BM25 retrieval (items). `None` when no text fields are declared.
    text_index: Option<&'a Arc<crate::text::TextIndex>>,
    /// Text index for BM25 retrieval (creators). `None` when no creator text fields are declared.
    creator_text_index: Option<&'a Arc<crate::text::TextIndex>>,
    /// Embedding slot registry for ANN retrieval. `None` when no embedding slots declared.
    embedding_registry: Option<&'a RwLock<EmbeddingSlotRegistry>>,
    category_index: Option<&'a BitmapIndex>,
    format_index: Option<&'a BitmapIndex>,
    creator_index: Option<&'a BitmapIndex>,
    tag_index: Option<&'a BitmapIndex>,
    duration_index: Option<&'a RangeIndex<u32>>,
    created_at_index: Option<&'a RangeIndex<u64>>,
    universe: Option<&'a RwLock<RoaringBitmap>>,
    user_state: Option<&'a UserStateIndex>,
    hard_negatives: Option<&'a HardNegIndex>,
    interaction_ledger: Option<&'a InteractionLedger>,
    creator_items: Option<&'a CreatorItemsBitmap>,
    /// Per-user preference vectors. Consumed in Stage 3 of the personalized
    /// scoring path: `compute_preference_boosts` reads each candidate's content
    /// embedding from `items_storage` and adds the cosine similarity to the
    /// user's preference vector as an additive ranking boost.
    preference_vectors: Option<&'a PreferenceVectors>,
    items_storage: Option<&'a dyn StorageEngine>,
    /// Item content embedding slot name (schema's first Item slot, fallback
    /// "content"), threaded from `TidalDb::item_embedding_slot()` so the
    /// preference boost reads embeddings from the slot they were written to.
    item_embedding_slot: Option<&'a str>,
    /// Creator content embedding slot name. Creator embeddings are written and
    /// auto-registered under `(Creator, "content")` by the db layer
    /// (`db::creators::write_creator_embedding`), so the default resolution is
    /// "content"; this field exists so a schema-driven creator slot resolver can
    /// be threaded in later without touching the ANN-retrieval logic.
    creator_embedding_slot: Option<&'a str>,
    /// Storage engine for creator entities, used to load metadata for creator
    /// search results and to evaluate metadata-based post-filters.
    creators_storage: Option<&'a dyn StorageEngine>,
    session_context: Option<SessionContext>,
    session_snapshot: Option<SessionSnapshot>,
    /// M6 cohort ledger for `WithinScope::CohortTrending` resolution.
    cohort_ledger: Option<&'a crate::cohort::CohortSignalLedger>,
    /// M6 cohort registry for cohort existence checks.
    cohort_registry: Option<&'a crate::cohort::CohortRegistry>,
    /// M6p4 collection index for `WithinScope::Collection` resolution.
    collection_index: Option<&'a crate::entities::CollectionIndex>,
    // M7p2 load degradation
    degradation_level: crate::load::DegradationLevel,
}

impl<'a> SearchExecutor<'a> {
    /// Create a new executor with the core infrastructure.
    #[must_use]
    #[allow(clippy::too_many_arguments)]
    pub const fn new(
        ledger: &'a SignalLedger,
        profile_registry: &'a ProfileRegistry,
        text_index: Option<&'a Arc<crate::text::TextIndex>>,
        embedding_registry: Option<&'a RwLock<EmbeddingSlotRegistry>>,
        category_index: Option<&'a BitmapIndex>,
        format_index: Option<&'a BitmapIndex>,
        creator_index: Option<&'a BitmapIndex>,
        tag_index: Option<&'a BitmapIndex>,
        duration_index: Option<&'a RangeIndex<u32>>,
        created_at_index: Option<&'a RangeIndex<u64>>,
        universe: Option<&'a RwLock<RoaringBitmap>>,
    ) -> Self {
        Self {
            ledger,
            profile_registry,
            text_index,
            creator_text_index: None,
            embedding_registry,
            category_index,
            format_index,
            creator_index,
            tag_index,
            duration_index,
            created_at_index,
            universe,
            user_state: None,
            hard_negatives: None,
            interaction_ledger: None,
            creator_items: None,
            preference_vectors: None,
            items_storage: None,
            item_embedding_slot: None,
            creator_embedding_slot: None,
            creators_storage: None,
            session_context: None,
            session_snapshot: None,
            cohort_ledger: None,
            cohort_registry: None,
            collection_index: None,
            degradation_level: crate::load::DegradationLevel::Full,
        }
    }

    /// Set the degradation level for this search execution.
    ///
    /// Computed by `LoadDetector::enter()` at query entry and threaded through
    /// the executor pipeline. Defaults to `Full` (no degradation).
    #[must_use]
    pub const fn with_degradation_level(mut self, level: crate::load::DegradationLevel) -> Self {
        self.degradation_level = level;
        self
    }

    /// Attach M3 user-context for personalization and user-state filtering.
    #[must_use]
    pub const fn with_user_context(
        mut self,
        user_state: &'a UserStateIndex,
        hard_negatives: &'a HardNegIndex,
        interaction_ledger: &'a InteractionLedger,
        creator_items: &'a CreatorItemsBitmap,
    ) -> Self {
        self.user_state = Some(user_state);
        self.hard_negatives = Some(hard_negatives);
        self.interaction_ledger = Some(interaction_ledger);
        self.creator_items = Some(creator_items);
        self
    }

    /// Attach preference vectors.
    #[must_use]
    pub const fn with_preference_vectors(
        mut self,
        preference_vectors: &'a PreferenceVectors,
    ) -> Self {
        self.preference_vectors = Some(preference_vectors);
        self
    }

    /// Attach the item content embedding slot name used by the preference boost.
    ///
    /// Resolved from `TidalDb::item_embedding_slot()`. Defaults to "content".
    #[must_use]
    pub const fn with_item_embedding_slot(mut self, slot: &'a str) -> Self {
        self.item_embedding_slot = Some(slot);
        self
    }

    /// Attach the creator content embedding slot name used by Creator ANN search.
    ///
    /// Defaults to "content" (the slot creators are auto-registered under by the
    /// db layer). Threaded separately from the item slot so item and creator
    /// vector search resolve independently.
    #[must_use]
    pub const fn with_creator_embedding_slot(mut self, slot: &'a str) -> Self {
        self.creator_embedding_slot = Some(slot);
        self
    }

    /// Resolve the embedding slot name to use for ANN retrieval of `kind`.
    ///
    /// Items use the schema-resolved item slot (threaded from
    /// `TidalDb::item_embedding_slot()`); creators (and any other kind) use the
    /// creator slot. Both default to "content" when unset. This replaces the
    /// previous hard-coded `"content"` literal that silently skipped ANN
    /// retrieval whenever an app named its first Item embedding slot anything
    /// other than "content" (writes used the schema slot, reads used the literal).
    pub(crate) fn embedding_slot_for(&self, kind: crate::schema::EntityKind) -> &str {
        match kind {
            crate::schema::EntityKind::Item => self.item_embedding_slot.unwrap_or("content"),
            _ => self.creator_embedding_slot.unwrap_or("content"),
        }
    }

    /// Attach a creator text index for BM25 retrieval when `entity_kind = Creator`.
    #[must_use]
    pub const fn with_creator_text_index(mut self, idx: &'a Arc<crate::text::TextIndex>) -> Self {
        self.creator_text_index = Some(idx);
        self
    }

    /// Attach items storage for metadata lookup during session-boosted scoring.
    #[must_use]
    pub fn with_items_storage(mut self, storage: &'a dyn StorageEngine) -> Self {
        self.items_storage = Some(storage);
        self
    }

    /// Attach creators storage for metadata lookup in creator search results
    /// and metadata-based post-filtering.
    #[must_use]
    pub fn with_creators_storage(mut self, storage: &'a dyn StorageEngine) -> Self {
        self.creators_storage = Some(storage);
        self
    }

    /// Attach M4 session context for FOR SESSION ranking boost.
    #[must_use]
    pub fn with_session(mut self, context: SessionContext, snapshot: SessionSnapshot) -> Self {
        self.session_context = Some(context);
        self.session_snapshot = Some(snapshot);
        self
    }

    /// Attach M6 cohort context for `WithinScope::CohortTrending` resolution.
    #[must_use]
    pub const fn with_cohort_context(
        mut self,
        cohort_ledger: &'a crate::cohort::CohortSignalLedger,
        cohort_registry: &'a crate::cohort::CohortRegistry,
    ) -> Self {
        self.cohort_ledger = Some(cohort_ledger);
        self.cohort_registry = Some(cohort_registry);
        self
    }

    /// Attach M6p4 collection index for `WithinScope::Collection` resolution.
    #[must_use]
    pub const fn with_collection_index(
        mut self,
        collection_index: &'a crate::entities::CollectionIndex,
    ) -> Self {
        self.collection_index = Some(collection_index);
        self
    }
}

mod pipeline;
