//! RETRIEVE query executor.
//!
//! Implements the 6-stage pipeline that turns a `Retrieve` AST into `Results`:
//!
//! 1. **Candidate generation**: sources entity IDs via the profile's candidate strategy
//! 2. **Filter evaluation**: applies bitmap/predicate filters to narrow candidates
//! 3. **Signal scoring**: scores and ranks candidates via `ProfileExecutor`
//! 4. **Diversity enforcement**: applies per-creator and format-mix constraints
//! 5. **Notification cap enforcement**: applied when `notification_caps` is set; limits
//!    per-creator and total daily notifications using `NotificationTracker`
//! 6. **Result assembly**: slices, paginates, and builds the final `Results`
//!
//! The executor borrows all infrastructure by reference and is constructed
//! per-query by `TidalDb::retrieve()`.

pub mod candidate_gen;
pub mod personalization;
pub mod social_filter;
pub mod user_filter;

mod helpers;
mod pipeline;

#[cfg(test)]
mod tests;

#[cfg(test)]
mod tests_part2;

use std::sync::RwLock;

use roaring::RoaringBitmap;

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

// ── Executor ────────────────────────────────────────────────────────────────

/// Executes RETRIEVE queries against the database's live state.
///
/// Constructed per-query by `TidalDb::retrieve()`. Holds borrowed references
/// to all infrastructure needed for the 6-stage pipeline.
///
/// Index references are `Option` because the database may not have all indexes
/// available (e.g., no items written yet). The executor degrades gracefully
/// when indexes are absent.
pub struct RetrieveExecutor<'a> {
    ledger: &'a SignalLedger,
    profile_registry: &'a ProfileRegistry,
    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>>,
    #[allow(dead_code)]
    embedding_registry: Option<&'a RwLock<EmbeddingSlotRegistry>>,
    // ── M3 personalization context ────────────────────────────────────
    user_state: Option<&'a UserStateIndex>,
    hard_negatives: Option<&'a HardNegIndex>,
    interaction_ledger: Option<&'a InteractionLedger>,
    creator_items: Option<&'a CreatorItemsBitmap>,
    /// Per-user preference vectors for future embedding-similarity scoring.
    ///
    /// TODO: Use in Stage 3 to compute cosine similarity between user preference
    /// vector and per-item embeddings, adding as an additive boost to the score.
    /// Requires O(1) per-item embedding lookup (M5 work -- currently embeddings
    /// are only stored in the HNSW index, not in a flat lookup table).
    #[allow(dead_code)]
    preference_vectors: Option<&'a PreferenceVectors>,
    // ── M4 session context ────────────────────────────────────────────
    session_context: Option<SessionContext>,
    session_snapshot: Option<SessionSnapshot>,
    /// Items storage engine for metadata lookup during session-boosted scoring.
    items_storage: Option<&'a dyn StorageEngine>,
    // ── M6 cohort context ─────────────────────────────────────────────
    cohort_ledger: Option<&'a crate::cohort::CohortSignalLedger>,
    cohort_registry: Option<&'a crate::cohort::CohortRegistry>,
    // ── M6 co-engagement + social graph ──────────────────────────────
    co_engagement: Option<&'a crate::entities::CoEngagementIndex>,
    user_signal_index: Option<&'a crate::entities::UserSignalIndex>,
    // ── M6p4 collection index ─────────────────────────────────────────
    collection_index: Option<&'a crate::entities::CollectionIndex>,
    // ── M6p6 notification tracker ─────────────────────────────────────
    notification_tracker:
        Option<&'a std::sync::Arc<crate::db::notification_tracker::NotificationTracker>>,
    // ── M7p2 load degradation ─────────────────────────────────────────
    degradation_level: crate::load::DegradationLevel,
}

impl<'a> RetrieveExecutor<'a> {
    /// Create a new executor with all borrowed infrastructure.
    #[must_use]
    #[allow(clippy::too_many_arguments)]
    pub const fn new(
        ledger: &'a SignalLedger,
        profile_registry: &'a ProfileRegistry,
        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>>,
        embedding_registry: Option<&'a RwLock<EmbeddingSlotRegistry>>,
    ) -> Self {
        Self {
            ledger,
            profile_registry,
            category_index,
            format_index,
            creator_index,
            tag_index,
            duration_index,
            created_at_index,
            universe,
            embedding_registry,
            user_state: None,
            hard_negatives: None,
            interaction_ledger: None,
            creator_items: None,
            preference_vectors: None,
            session_context: None,
            session_snapshot: None,
            items_storage: None,
            cohort_ledger: None,
            cohort_registry: None,
            co_engagement: None,
            user_signal_index: None,
            collection_index: None,
            notification_tracker: None,
            degradation_level: crate::load::DegradationLevel::Full,
        }
    }

    /// Set the degradation level for this query 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 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 M3 personalization context to the executor.
    #[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 for future embedding-similarity scoring.
    #[must_use]
    pub const fn with_preference_vectors(
        mut self,
        preference_vectors: &'a PreferenceVectors,
    ) -> Self {
        self.preference_vectors = Some(preference_vectors);
        self
    }

    /// Attach M6 co-engagement index for `related` profile scoring.
    #[must_use]
    pub const fn with_co_engagement(
        mut self,
        co_engagement: &'a crate::entities::CoEngagementIndex,
    ) -> Self {
        self.co_engagement = Some(co_engagement);
        self
    }

    /// Attach M6 per-user signal index for social-graph-scoped trending.
    #[must_use]
    pub const fn with_user_signal_index(
        mut self,
        user_signal_index: &'a crate::entities::UserSignalIndex,
    ) -> Self {
        self.user_signal_index = Some(user_signal_index);
        self
    }

    /// Attach M6 cohort context for cohort-scoped signal reads.
    #[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 `InCollection` filter evaluation.
    #[must_use]
    pub const fn with_collection_index(
        mut self,
        collection_index: &'a crate::entities::CollectionIndex,
    ) -> Self {
        self.collection_index = Some(collection_index);
        self
    }

    /// Attach M6p6 notification tracker for daily notification capping.
    #[must_use]
    pub const fn with_notification_tracker(
        mut self,
        tracker: &'a std::sync::Arc<crate::db::notification_tracker::NotificationTracker>,
    ) -> Self {
        self.notification_tracker = Some(tracker);
        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
    }

    /// Extract and execute Stage 3 (signal scoring) of the pipeline.
    ///
    /// Scores all candidates using the ranking profile, cohort context, and session,
    /// returning the scored candidates and their pre-loaded metadata.
    ///
    /// Called from `pipeline::execute()`.
    #[allow(clippy::too_many_lines, clippy::type_complexity)]
    pub(super) fn stage3_score(
        &self,
        candidates: &[crate::schema::EntityId],
        profile: &crate::ranking::profile::RankingProfile,
        query: &crate::query::retrieve::Retrieve,
        now: crate::schema::Timestamp,
        _stats: &mut crate::query::stats::QueryStats,
        warnings: &mut Vec<String>,
    ) -> Result<
        (
            Vec<crate::ranking::executor::ScoredCandidate>,
            std::collections::HashMap<u64, std::collections::HashMap<String, String>>,
        ),
        crate::query::retrieve::QueryError,
    > {
        use crate::db::deserialize_metadata as deserialize_item_metadata;
        use crate::ranking::executor::ProfileExecutor;
        use crate::ranking::profile::Sort;
        use crate::storage::{Tag, encode_key};
        use std::collections::HashMap;

        let mut executor =
            ProfileExecutor::new(self.ledger).with_degradation_level(self.degradation_level);

        // M6: wire co-engagement context when seed_item (similar_to) is available.
        if let Some(co_eng) = self.co_engagement {
            executor = executor.with_co_engagement(co_eng);
        }
        if let Some(seed) = query.similar_to {
            executor = executor.with_seed(seed);
        }

        // M6: wire social-graph-scoped trending when a SocialGraph filter is active
        // and a per-user signal index is available.
        if let Some(usi) = self.user_signal_index {
            executor = executor.with_user_signal_index(usi);

            // Resolve social subgraph users from the SocialGraph filter.
            if let (Some(user_state), Some(ref filter_expr)) =
                (self.user_state, query.combined_filter())
                && let Some((uid, depth)) = extract_social_graph_params(filter_expr)
            {
                let subgraph_users =
                    social_filter::resolve_social_subgraph_users(uid, depth, user_state);
                if !subgraph_users.is_empty() {
                    executor = executor.with_social_graph_users(subgraph_users);
                }
            }
        }

        // Pre-load item metadata for keyword hint matching (session),
        // metadata-based sorts (alphabetical, duration), or diversity / notification
        // cap enforcement (both need creator_id from metadata to group candidates).
        let needs_metadata_for_sort = matches!(
            profile.sort,
            Some(Sort::AlphabeticalAsc | Sort::AlphabeticalDesc | Sort::Shortest | Sort::Longest)
        );
        let needs_metadata_for_creator_grouping =
            profile.diversity.max_per_creator.is_some() || query.notification_caps.is_some();
        let item_metadata: HashMap<u64, HashMap<String, String>> = if self.session_context.is_some()
            || needs_metadata_for_sort
            || needs_metadata_for_creator_grouping
        {
            self.items_storage.map_or_else(HashMap::new, |storage| {
                candidates
                    .iter()
                    .filter_map(|&eid| {
                        let key = encode_key(eid, Tag::Meta, b"");
                        storage
                            .get(&key)
                            .ok()
                            .flatten()
                            .map(|bytes| (eid.as_u64(), deserialize_item_metadata(&bytes)))
                    })
                    .collect()
            })
        } else {
            HashMap::new()
        };

        // Wire metadata for metadata-based sorts.
        if !item_metadata.is_empty() {
            executor = executor.with_item_metadata(&item_metadata);
        }

        // Wire DateSaved context.
        if matches!(profile.sort, Some(Sort::DateSaved))
            && let (Some(user_id), Some(user_state)) = (query.for_user, self.user_state)
        {
            executor = executor.with_date_saved_context(user_state, user_id);
        }

        #[allow(clippy::option_if_let_else)]
        let mut scored = if let Some(user_id) = query.for_user {
            // Personalized scoring: build UserContext from the interaction ledger
            // and creator-items bitmap, then call score_personalized().
            let user_ctx = personalization::build_user_context(
                user_id,
                now,
                self.interaction_ledger,
                self.creator_items,
            );
            executor.score_personalized(
                candidates,
                profile,
                now,
                self.session_context.as_ref(),
                &user_ctx,
                &item_metadata,
            )
        } else {
            // Population-level profiles: no user context needed.
            executor.score_with_session(
                candidates,
                profile,
                now,
                self.session_context.as_ref(),
                &item_metadata,
            )
        };

        // ── Stage 3b: Cohort-scoped signal override (M6) ────────────────
        // When a named cohort or ad-hoc cohort predicate is specified,
        // re-score candidates using the cohort signal ledger instead of the
        // global ledger. The profile's boosts and sort mode are the same,
        // but signal values come from cohort-specific state.
        //
        // Resolution order:
        //   1. `query.cohort` -- direct named cohort.
        //   2. `query.cohort_predicate` + `query.for_user` -- resolve the
        //      user's metadata, evaluate the predicate, then find the first
        //      registered cohort the user belongs to.
        #[allow(clippy::option_if_let_else)]
        let resolved_cohort: Option<String> = if let Some(ref name) = query.cohort {
            Some(name.clone())
        } else if let (Some(pred), Some(user_id)) = (&query.cohort_predicate, query.for_user) {
            // Ad-hoc cohort predicate: read user metadata, evaluate the
            // predicate, and find matching registered cohorts.
            self.resolve_adhoc_cohort(pred, user_id, warnings)?
        } else if query.cohort_predicate.is_some() {
            return Err(QueryError::InvalidFilter {
                field: "cohort_predicate".to_string(),
                reason: "cohort_predicate requires FOR USER context (set for_user on the query)"
                    .to_string(),
            });
        } else {
            None
        };

        if let Some(ref cohort_name) = resolved_cohort
            && let Some(cohort_ledger) = self.cohort_ledger
        {
            // Validate the cohort exists (for named cohorts).
            let cohort_exists = self
                .cohort_registry
                .is_some_and(|reg| reg.get(cohort_name).is_some());
            if !cohort_exists && query.cohort.is_some() {
                return Err(QueryError::InvalidFilter {
                    field: "cohort".to_string(),
                    reason: format!("cohort '{cohort_name}' not registered"),
                });
            }
            if cohort_exists {
                self.rescore_with_cohort(&mut scored, cohort_name, cohort_ledger, &profile.boosts);
            }
        }

        // Exploration budget: inject random candidates for discovery.
        if profile.exploration > 0.0 {
            candidate_gen::inject_exploration(&mut scored, candidates, profile.exploration);
        }

        // Populate creator_id in scored candidates from pre-loaded metadata.
        // The ranking executor always leaves creator_id = None; this enrichment
        // enables Stage 4 diversity enforcement (DiversitySelector checks creator_id)
        // and Stage 4.5 notification cap enforcement (resolve_creator_id falls back
        // to items_storage, but having it pre-populated is more reliable).
        if needs_metadata_for_creator_grouping && !item_metadata.is_empty() {
            for candidate in &mut scored {
                if candidate.creator_id.is_none()
                    && let Some(meta) = item_metadata.get(&candidate.entity_id.as_u64())
                    && let Some(cid_str) = meta.get("creator_id")
                    && let Ok(cid) = cid_str.parse::<u64>()
                {
                    candidate.creator_id = Some(EntityId::new(cid));
                }
            }
        }

        let total_scored = scored.len();
        // timing recorded in pipeline.rs execute() after this call returns
        let _ = total_scored; // used by caller

        Ok((scored, item_metadata))
    }
}

// ── Helpers ──────────────────────────────────────────────────────────────────

/// Extract `SocialGraph { user_id, depth }` parameters from a filter expression.
///
/// Walks the top-level expression (including `And` lists) and returns the first
/// `SocialGraph` variant found. Returns `None` if no social graph filter is present.
fn extract_social_graph_params(expr: &FilterExpr) -> Option<(u64, u8)> {
    match expr {
        FilterExpr::SocialGraph { user_id, depth } => Some((*user_id, *depth)),
        FilterExpr::And(exprs) => exprs.iter().find_map(extract_social_graph_params),
        _ => None,
    }
}
