//! Execute stage — the 8-stage SEARCH pipeline.

use std::collections::{HashMap, HashSet};
use std::time::Instant;

use roaring::RoaringBitmap;

use super::super::executor_helpers::{
    build_user_context, evaluate_metadata_filter, extract_user_state_filters,
    has_creator_metadata_filter,
};
use super::super::scope::ScopeResolver;
use super::super::types::{Search, SearchResultItem, SearchResults};
use crate::db::deserialize_metadata as deserialize_item_metadata;
use crate::query::fusion::{HybridFusion, RetrievalMode, ann_to_ranked, route_results};
use crate::query::retrieve::{Cursor, QueryError, Signal};
use crate::query::stats::QueryStats;
use crate::ranking::diversity::DiversitySelector;
use crate::ranking::executor::ProfileExecutor;
use crate::schema::{EntityId, EntityKind, Timestamp};
use crate::storage::indexes::bitmap::BitmapIndex;
use crate::storage::indexes::filter::{FilterEvaluator, FilterExpr, FilterResult};
use crate::storage::indexes::range::RangeIndex;
use crate::storage::{Tag, encode_key};
use crate::text::collectors::AllScoresCollector;

use super::SearchExecutor;

impl SearchExecutor<'_> {
    /// Execute a SEARCH query through the 8-stage pipeline.
    ///
    /// # Errors
    ///
    /// Returns `QueryError` on validation failure, missing index, profile not
    /// found, or storage error during BM25/ANN retrieval.
    #[allow(clippy::too_many_lines)]
    pub fn execute(&self, query: &Search) -> Result<SearchResults, QueryError> {
        let query_start = Instant::now();
        let mut stats = QueryStats::new(query.profile.name.clone());
        stats.degradation_level = self.degradation_level as u8;

        let mode =
            RetrievalMode::determine(query.query_text.is_some(), query.query_vector.is_some())
                .ok_or_else(|| QueryError::InvalidFilter {
                    field: "query".into(),
                    reason: "must provide at least one of query_text or query_vector".into(),
                })?;

        // Resolve the profile.
        #[allow(clippy::option_if_let_else)]
        let profile = match query.profile.version {
            Some(v) => self
                .profile_registry
                .get_version(&query.profile.name, v)
                .map_err(|_| QueryError::ProfileNotFound(query.profile.name.clone()))?,
            None => self
                .profile_registry
                .get(&query.profile.name)
                .map_err(|_| QueryError::ProfileNotFound(query.profile.name.clone()))?,
        };

        let mut warnings: Vec<String> = Vec::new();

        // ── Stage 0: Scope Pre-Filter ─────────────────────────────────────
        let scope_bitmap: Option<RoaringBitmap> = if let Some(ref scope) = query.within_scope {
            let universe = self.universe.map_or_else(RoaringBitmap::new, |u| {
                u.read().map(|g| g.clone()).unwrap_or_default()
            });

            let mut resolver = ScopeResolver::new(self.ledger, universe);

            if let (Some(cl), Some(cr)) = (self.cohort_ledger, self.cohort_registry) {
                resolver = resolver.with_cohort(cl, cr);
            }
            if let (Some(us), Some(ci)) = (self.user_state, self.creator_items) {
                resolver = resolver.with_user_context(us, ci);
            }
            if let Some(cat) = self.category_index {
                resolver = resolver.with_category_index(cat);
            }
            if let Some(coll) = self.collection_index {
                resolver = resolver.with_collection_index(coll);
            }

            Some(resolver.resolve(scope, query.for_user)?)
        } else {
            None
        };

        // ── Stage 1a: Text Retrieval (BM25) ────────────────────────────────
        let mut bm25_results: Vec<(EntityId, f32)> = Vec::new();

        // Route to creator or item text index based on entity_kind.
        let effective_text_index = match query.entity_kind {
            EntityKind::Creator => self.creator_text_index,
            _ => self.text_index,
        };

        if let Some(ref query_text) = query.query_text {
            match effective_text_index {
                None => {
                    warnings.push("text_index unavailable; text retrieval skipped".to_string());
                }
                Some(idx) => {
                    let parser = idx.query_parser();
                    let tq = parser
                        .parse(query_text)
                        .map_err(|e| QueryError::StorageError(format!("text query parse: {e}")))?;
                    let searcher = idx.searcher();
                    let collector = AllScoresCollector {
                        entity_id_field: idx.fields().entity_id,
                    };
                    let mut raw = searcher
                        .search(tq.as_ref(), &collector)
                        .map_err(|e| QueryError::StorageError(format!("BM25 search: {e}")))?;
                    // Sort descending by BM25 score (AllScoresCollector does not sort).
                    raw.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
                    bm25_results = raw;
                }
            }
        }

        // ── Stage 1b: ANN Retrieval ─────────────────────────────────────────
        let mut ann_results: Vec<(EntityId, f32)> = Vec::new();

        if let Some(ref query_vector) = query.query_vector {
            match self.embedding_registry {
                None => {
                    warnings
                        .push("embedding_registry unavailable; ANN retrieval skipped".to_string());
                }
                Some(registry_lock) => {
                    let registry = registry_lock.read().map_err(|_| {
                        QueryError::StorageError("embedding registry lock poisoned".to_string())
                    })?;
                    match registry.get(query.entity_kind, "content") {
                        None => {
                            warnings.push(format!(
                                "no 'content' embedding slot for {:?}; ANN retrieval skipped",
                                query.entity_kind
                            ));
                        }
                        Some(slot) => {
                            let k = if self.degradation_level.reduces_candidates() {
                                // Reduced: cap at 200 regardless of query limit.
                                // Cuts ANN search work by ~60% for typical limit=20 queries.
                                (query.limit as usize * 10).clamp(100, 200)
                            } else {
                                (query.limit as usize * 20).max(200)
                            };
                            // When a scope bitmap is set, use the predicate-filtered
                            // ANN search so the filter is applied during graph traversal
                            // rather than post-hoc. This ensures all k returned candidates
                            // are within scope and avoids exhausting the over-fetch budget
                            // on out-of-scope vectors.
                            let raw = if let Some(ref bitmap) = scope_bitmap {
                                #[allow(clippy::cast_possible_truncation)]
                                slot.index
                                    .filtered_search(
                                        query_vector,
                                        k,
                                        slot.params.ef_search,
                                        &|id| bitmap.contains(id as u32),
                                    )
                                    .map_err(|e| {
                                        QueryError::StorageError(format!(
                                            "ANN filtered search: {e}"
                                        ))
                                    })?
                            } else {
                                slot.index
                                    .search(query_vector, k, slot.params.ef_search)
                                    .map_err(|e| {
                                        QueryError::StorageError(format!("ANN search: {e}"))
                                    })?
                            };
                            ann_results = ann_to_ranked(&raw);
                        }
                    }
                }
            }
        }

        // Apply scope pre-filter to BM25 results (BM25 has no predicate callback API).
        if let Some(ref bitmap) = scope_bitmap {
            #[allow(clippy::cast_possible_truncation)]
            bm25_results.retain(|(id, _)| bitmap.contains(id.as_u64() as u32));
        }
        // ANN results are already scope-filtered via filtered_search above;
        // no post-filter needed for that path.

        // M7p2: ReducedCandidates — truncate BM25 to cap downstream scoring work.
        if self.degradation_level.reduces_candidates() {
            let bm25_cap = (query.limit as usize * 10).max(100);
            bm25_results.truncate(bm25_cap);
        }

        // Build per-entity score maps for explainability in Stage 5.
        let bm25_map: HashMap<u64, f32> = bm25_results
            .iter()
            .map(|(id, s)| (id.as_u64(), *s))
            .collect();
        let ann_map: HashMap<u64, f32> = ann_results
            .iter()
            .map(|(id, d)| (id.as_u64(), *d))
            .collect();

        // ── Stage 1c: Fusion ────────────────────────────────────────────────
        let fusion = HybridFusion::default();
        let fused = route_results(mode, &bm25_results, &ann_results, &fusion);
        let mut candidates: Vec<EntityId> = fused.iter().map(|(id, _)| *id).collect();

        // Apply exclude list.
        if !query.exclude.is_empty() {
            let exclude_set: HashSet<u64> = query.exclude.iter().map(|id| id.as_u64()).collect();
            candidates.retain(|id| !exclude_set.contains(&id.as_u64()));
        }

        stats.candidates_considered = candidates.len();

        tracing::trace!(
            bm25_count = bm25_results.len(),
            ann_count = ann_results.len(),
            fused_count = candidates.len(),
            mode = ?mode,
            "search stage 1: retrieval complete"
        );

        // ── Stage 2: Metadata Filter ─────────────────────────────────────────
        if let Some(filter_expr) = query.combined_filter() {
            let empty_bitmap = BitmapIndex::new("_empty");
            let empty_dur = RangeIndex::<u32>::new("_empty");
            let empty_ts = RangeIndex::<u64>::new("_empty");
            let empty_universe = RoaringBitmap::new();

            let cat = self.category_index.unwrap_or(&empty_bitmap);
            let fmt = self.format_index.unwrap_or(&empty_bitmap);
            let cre = self.creator_index.unwrap_or(&empty_bitmap);
            let tag = self.tag_index.unwrap_or(&empty_bitmap);
            let dur = self.duration_index.unwrap_or(&empty_dur);
            let ts = self.created_at_index.unwrap_or(&empty_ts);

            let universe_guard;
            #[allow(clippy::option_if_let_else)]
            let universe_ref = match self.universe {
                Some(u) => match u.read() {
                    Ok(guard) => {
                        universe_guard = guard;
                        &*universe_guard
                    }
                    Err(_) => &empty_universe,
                },
                None => &empty_universe,
            };

            let evaluator = FilterEvaluator::new(cat, fmt, cre, tag, dur, ts, universe_ref);
            let filter_result = evaluator.evaluate(&filter_expr);

            match filter_result {
                FilterResult::Bitmap(bitmap) => {
                    #[allow(clippy::cast_possible_truncation)]
                    candidates.retain(|id| bitmap.contains(id.as_u64() as u32));
                }
                FilterResult::Predicate(pred) => {
                    candidates.retain(|id| pred(id.as_u64()));
                }
            }
        }

        tracing::trace!(
            candidates = candidates.len(),
            "search stage 2: filter applied"
        );

        // ── Stage 2b: Creator Metadata Post-Filter ──────────────────────────
        // For creator searches, evaluate metadata-based filter predicates against
        // actual creator metadata from storage. This handles filters like
        // verified=true, language=en that aren't in bitmap indexes for creators.
        if query.entity_kind == EntityKind::Creator
            && let Some(combined) = query.combined_filter()
            && has_creator_metadata_filter(&combined)
            && !candidates.is_empty()
            && let Some(storage) = self.creators_storage
        {
            candidates.retain(|&id| {
                let key = encode_key(id, Tag::Meta, b"");
                let meta = storage
                    .get(&key)
                    .ok()
                    .flatten()
                    .map(|bytes| {
                        let (_emb, meta) = crate::entities::deserialize_entity(&bytes);
                        meta
                    })
                    .unwrap_or_default();
                evaluate_metadata_filter(&combined, &meta)
            });
        }

        tracing::trace!(
            candidates = candidates.len(),
            "search stage 2b: creator metadata filter applied"
        );

        // ── Stage 2.5: User-Context Filtering ────────────────────────────────
        if let Some(user_id) = query.for_user {
            if let Some(user_state) = self.user_state {
                let seen = user_state.seen_bitmap(user_id);
                #[allow(clippy::cast_possible_truncation)]
                candidates.retain(|id| !seen.contains(id.as_u64() as u32));

                let hidden = user_state.hidden_items(user_id);
                #[allow(clippy::cast_possible_truncation)]
                candidates.retain(|id| !hidden.contains(id.as_u64() as u32));

                if let Some(creator_items) = self.creator_items {
                    let blocked_creators = user_state.blocked_creators(user_id);
                    if !blocked_creators.is_empty() {
                        let mut blocked_items = RoaringBitmap::new();
                        for &cid in &blocked_creators {
                            if let Some(bm) = creator_items.get(cid) {
                                blocked_items |= &bm;
                            }
                        }
                        #[allow(clippy::cast_possible_truncation)]
                        candidates.retain(|id| !blocked_items.contains(id.as_u64() as u32));
                    }
                }

                // User-state inclusion filters (Saved, Liked, InProgress).
                if let Some(ref filter_expr) = query.combined_filter() {
                    let user_filters = extract_user_state_filters(filter_expr);
                    for uf in &user_filters {
                        match uf {
                            FilterExpr::Saved(uid) => {
                                let saved = user_state.saved_bitmap(*uid);
                                #[allow(clippy::cast_possible_truncation)]
                                candidates.retain(|id| saved.contains(id.as_u64() as u32));
                            }
                            FilterExpr::Liked(uid) => {
                                let liked = user_state.liked_bitmap(*uid);
                                #[allow(clippy::cast_possible_truncation)]
                                candidates.retain(|id| liked.contains(id.as_u64() as u32));
                            }
                            FilterExpr::InProgress {
                                user_id: uid,
                                threshold,
                            } => {
                                candidates.retain(|id| {
                                    user_state.is_in_progress(*uid, id.as_u64(), *threshold)
                                });
                            }
                            _ => {}
                        }
                    }
                }
            }

            if let Some(hard_neg) = self.hard_negatives {
                let neg_bitmap = hard_neg.bitmap(user_id);
                if !neg_bitmap.is_empty() {
                    #[allow(clippy::cast_possible_truncation)]
                    candidates.retain(|id| !neg_bitmap.contains(id.as_u64() as u32));
                }
            }

            tracing::trace!(
                candidates = candidates.len(),
                user_id,
                "search stage 2.5: user-context filter applied"
            );
        }

        stats.candidates_after_filter = candidates.len();
        stats.filters_applied = query.filters.len();

        // ── Stage 3: Profile Scoring ─────────────────────────────────────────
        let scoring_start = Instant::now();
        let now = Timestamp::now();
        let executor =
            ProfileExecutor::new(self.ledger).with_degradation_level(self.degradation_level);

        // Pre-load item metadata for keyword hint matching when session is active.
        let item_metadata: HashMap<u64, HashMap<String, String>> = if self.session_context.is_some()
        {
            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()
        };

        #[allow(clippy::option_if_let_else)]
        let scored = if let Some(user_id) = query.for_user {
            let user_ctx =
                build_user_context(self.interaction_ledger, self.creator_items, user_id, now);
            executor.score_personalized(
                &candidates,
                profile,
                now,
                self.session_context.as_ref(),
                &user_ctx,
                &item_metadata,
            )
        } else {
            executor.score_with_session(
                &candidates,
                profile,
                now,
                self.session_context.as_ref(),
                &item_metadata,
            )
        };

        let total_scored = scored.len();
        stats.scoring_time_us = scoring_start.elapsed().as_micros() as u64;

        tracing::trace!(scored = scored.len(), "search stage 3: scored");

        // ── Stage 4: Diversity Enforcement ───────────────────────────────────
        // M7p2: NoDiversity — skip entirely to reduce per-query work under load.
        let diversity_start = Instant::now();
        let (final_candidates, constraints_satisfied) = if self.degradation_level.skips_diversity()
        {
            warnings.push("diversity enforcement skipped due to load degradation".to_string());
            (scored, true)
        } else if let Some(ref diversity) = query.diversity {
            let result = DiversitySelector::select(&scored, diversity, scored.len());
            let satisfied = result.constraints_satisfied;
            if !satisfied {
                for v in &result.violations {
                    warnings.push(format!(
                        "diversity constraint '{}' relaxed: {}",
                        v.constraint, v.detail
                    ));
                }
            }
            (result.selected, satisfied)
        } else {
            (scored, true)
        };

        stats.diversity_time_us = diversity_start.elapsed().as_micros() as u64;
        stats.candidates_after_diversity = final_candidates.len();

        tracing::trace!(
            final_count = final_candidates.len(),
            "search stage 4: diversity enforced"
        );

        // ── Stage 5: Result Assembly ──────────────────────────────────────────
        let offset = query.cursor.as_ref().map_or(0, Cursor::offset);
        let limit = query.limit as usize;
        let end = (offset + limit).min(final_candidates.len());
        let page = if offset < final_candidates.len() {
            &final_candidates[offset..end]
        } else {
            &[]
        };

        // Load creator metadata for result enrichment when searching creators.
        let creator_meta: HashMap<u64, HashMap<String, String>> =
            if query.entity_kind == EntityKind::Creator {
                self.creators_storage.map_or_else(HashMap::new, |storage| {
                    page.iter()
                        .filter_map(|c| {
                            let key = encode_key(c.entity_id, Tag::Meta, b"");
                            storage.get(&key).ok().flatten().map(|bytes| {
                                let (_emb, meta) = crate::entities::deserialize_entity(&bytes);
                                (c.entity_id.as_u64(), meta)
                            })
                        })
                        .collect()
                })
            } else {
                HashMap::new()
            };

        let items: Vec<SearchResultItem> = page
            .iter()
            .enumerate()
            .map(|(i, c)| {
                let signals: Vec<Signal> = c
                    .signal_snapshot
                    .iter()
                    .map(|(name, value)| Signal {
                        name: name.clone(),
                        value: *value,
                        source: "decay_score".to_string(),
                    })
                    .collect();

                SearchResultItem {
                    entity_id: c.entity_id,
                    score: c.score,
                    rank: offset + i + 1,
                    bm25_score: bm25_map.get(&c.entity_id.as_u64()).copied(),
                    semantic_score: ann_map.get(&c.entity_id.as_u64()).copied(),
                    signals,
                    metadata: creator_meta.get(&c.entity_id.as_u64()).cloned(),
                }
            })
            .collect();

        let next_cursor = if end < final_candidates.len() {
            Some(Cursor::from_offset(end))
        } else {
            None
        };

        tracing::trace!(
            returned = items.len(),
            has_next_cursor = next_cursor.is_some(),
            "search stage 5: results assembled"
        );

        stats.total_time_us = query_start.elapsed().as_micros() as u64;

        Ok(SearchResults {
            items,
            next_cursor,
            total_candidates: total_scored,
            constraints_satisfied,
            warnings,
            session_snapshot: self.session_snapshot.clone(),
            degradation_level: self.degradation_level,
            stats,
        })
    }
}
