//! RETRIEVE query AST, builder, response types, and pagination cursor.

pub use super::cursor::Cursor;
use super::errors::QueryError;
use crate::query::stats::QueryStats;
use crate::ranking::diversity::DiversityConstraints;
use crate::ranking::profile::CandidateStrategy;
use crate::ranking::registry::ProfileRegistry;
use crate::schema::{EntityId, EntityKind};
use crate::session::{SessionId, SessionSnapshot};
use crate::storage::indexes::filter::FilterExpr;

// ── NotificationCaps ────────────────────────────────────────────────────────

/// Notification frequency caps for the `notification` profile.
///
/// Applied as a post-diversity pass in the executor. Limits how many
/// notifications a user receives per creator and in total per day.
#[derive(Debug, Clone, Copy)]
pub struct NotificationCaps {
    /// Maximum notifications from a single creator per day.
    pub max_per_creator_per_day: u32,
    /// Maximum total notifications delivered per day.
    pub max_total_per_day: u32,
}

impl NotificationCaps {
    /// Default caps (1 per creator, 3 total per day) matching UC-07 spec.
    pub const DEFAULT: Self = Self {
        max_per_creator_per_day: 1,
        max_total_per_day: 3,
    };
}

// ── ProfileRef ──────────────────────────────────────────────────────────────

/// Reference to a ranking profile by name and optional version.
///
/// When `version` is `None`, the registry resolves to the latest version.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileRef {
    /// Profile name (must match `[a-z0-9_]{1,64}`).
    pub name: String,
    /// Optional pinned version. `None` means "latest".
    pub version: Option<u32>,
}

impl ProfileRef {
    /// Create a profile reference that resolves to the latest version.
    #[must_use]
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            version: None,
        }
    }

    /// Create a profile reference pinned to a specific version.
    #[must_use]
    pub fn versioned(name: impl Into<String>, version: u32) -> Self {
        Self {
            name: name.into(),
            version: Some(version),
        }
    }
}

// ── Retrieve ────────────────────────────────────────────────────────────────

/// The typed AST for a RETRIEVE query.
///
/// Captures the full query intent: which entity kind to rank, which profile
/// to use, filters, diversity constraints, pagination, and context. Built
/// via `RetrieveBuilder` or (M3+) parsed from the query language.
#[derive(Debug, Clone)]
pub struct Retrieve {
    /// Which entity kind to retrieve (Item, User, Creator).
    pub entity_kind: EntityKind,
    /// Which ranking profile to apply.
    pub profile: ProfileRef,
    /// Zero or more filter expressions, AND-combined by `combined_filter()`.
    pub filters: Vec<FilterExpr>,
    /// Optional diversity constraints (max-per-creator, format-mix).
    pub diversity: Option<DiversityConstraints>,
    /// Maximum number of results to return.
    pub limit: usize,
    /// Entity IDs to exclude from results (e.g., already-seen items).
    pub exclude: Vec<EntityId>,
    /// Opaque cursor for keyset pagination.
    pub cursor: Option<Cursor>,
    /// User context for personalization (M3+).
    pub for_user: Option<u64>,
    /// Seed entity for "more like this" queries (M3+).
    pub similar_to: Option<EntityId>,
    /// Free-text context for hybrid search (M3+).
    pub context: Option<String>,
    /// Session ID for FOR SESSION ranking boost (M4+).
    pub for_session: Option<SessionId>,
    /// Cohort name for cohort-scoped signal reads (M6+).
    pub cohort: Option<String>,
    /// Ad-hoc cohort predicate for on-demand cohort queries (M6+).
    pub cohort_predicate: Option<crate::cohort::Predicate>,
    /// Notification frequency caps (applied post-diversity). Only meaningful
    /// with the `notification` profile, but not enforced to that profile.
    pub notification_caps: Option<NotificationCaps>,
    /// Constrain results to items from this creator (m6p6+).
    pub for_creator: Option<EntityId>,
}

impl Retrieve {
    /// Create a `RetrieveBuilder` with sensible defaults.
    ///
    /// Default: `entity_kind = Item`, `profile = "new"`, `limit = 20`.
    #[must_use]
    pub fn builder() -> RetrieveBuilder {
        RetrieveBuilder::with_defaults()
    }

    /// Combine all filter expressions into a single `FilterExpr`.
    ///
    /// - Zero filters: `None`
    /// - One filter: returns it directly
    /// - Multiple filters: wraps in `FilterExpr::And`
    #[must_use]
    pub fn combined_filter(&self) -> Option<FilterExpr> {
        match self.filters.len() {
            0 => None,
            1 => Some(self.filters[0].clone()),
            _ => Some(FilterExpr::And(self.filters.clone())),
        }
    }

    /// Validate the query against a profile registry.
    ///
    /// Checks:
    /// 1. Profile exists (by name, optionally by version)
    /// 2. Limit is in `[1, 500]`
    /// 3. `for_user` and `similar_to` are supported (M3)
    /// 4. Candidate strategy is supported
    ///
    /// # Errors
    ///
    /// Returns `QueryError` describing the first validation failure.
    // Used by the query executor in Task 02. Until then, only tests reference it.
    #[allow(dead_code)]
    pub(crate) fn validate(&self, registry: &ProfileRegistry) -> Result<(), QueryError> {
        // 1. Profile existence.
        let profile_name = &self.profile.name;
        let not_found = || QueryError::ProfileNotFound(profile_name.clone());
        #[allow(clippy::option_if_let_else)]
        let profile = match self.profile.version {
            Some(v) => registry
                .get_version(profile_name, v)
                .map_err(|_| not_found()),
            None => registry.get(profile_name).map_err(|_| not_found()),
        }?;

        // 2. Limit range (defense in depth).
        if self.limit == 0 || self.limit > 500 {
            return Err(QueryError::InvalidLimit {
                requested: self.limit,
                min: 1,
                max: 500,
            });
        }

        // 3. M3: for_user and similar_to are now supported.
        // No validation rejection -- the executor handles user context.

        // 4. Candidate strategy support check.
        // M3 adds support for Relationship strategy.
        match &profile.candidate_strategy {
            CandidateStrategy::Ann { .. }
            | CandidateStrategy::Scan { .. }
            | CandidateStrategy::SignalRanked { .. }
            | CandidateStrategy::Relationship => {}
            other => {
                return Err(QueryError::UnsupportedStrategy(format!(
                    "{other:?} requires M4+ infrastructure"
                )));
            }
        }

        Ok(())
    }
}

// ── RetrieveBuilder ─────────────────────────────────────────────────────────

/// Builder for `Retrieve` queries.
///
/// Provides a fluent API for constructing queries programmatically.
/// The only required fields are `entity_kind` and `profile` (set in `new()`);
/// everything else has sensible defaults.
///
/// # Examples
///
/// ```
/// use tidaldb::query::retrieve::{RetrieveBuilder, ProfileRef};
/// use tidaldb::schema::EntityKind;
///
/// let query = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("trending"))
///     .limit(20)
///     .build();
/// assert!(query.is_ok());
/// ```
pub struct RetrieveBuilder {
    entity_kind: EntityKind,
    profile: ProfileRef,
    filters: Vec<FilterExpr>,
    diversity: Option<DiversityConstraints>,
    limit: usize,
    exclude: Vec<EntityId>,
    cursor: Option<Cursor>,
    for_user: Option<u64>,
    similar_to: Option<EntityId>,
    context: Option<String>,
    for_session: Option<SessionId>,
    cohort: Option<String>,
    cohort_predicate: Option<crate::cohort::Predicate>,
    notification_caps: Option<NotificationCaps>,
    for_creator: Option<EntityId>,
}

impl RetrieveBuilder {
    /// Create a builder with sensible defaults (for `Retrieve::builder()`).
    ///
    /// Default: `entity_kind = Item`, `profile = "new"`, `limit = 20`.
    #[must_use]
    pub fn with_defaults() -> Self {
        Self::new(EntityKind::Item, ProfileRef::new("new"))
    }

    /// Create a new builder with the required entity kind and profile reference.
    ///
    /// Defaults: `limit = 20`, no filters, no diversity, no cursor, no exclusions.
    #[must_use]
    pub const fn new(entity_kind: EntityKind, profile: ProfileRef) -> Self {
        Self {
            entity_kind,
            profile,
            filters: Vec::new(),
            diversity: None,
            limit: 20,
            exclude: Vec::new(),
            cursor: None,
            for_user: None,
            similar_to: None,
            context: None,
            for_session: None,
            cohort: None,
            cohort_predicate: None,
            notification_caps: None,
            for_creator: None,
        }
    }

    /// Add a filter expression. Multiple filters are AND-combined.
    #[must_use]
    pub fn filter(mut self, expr: FilterExpr) -> Self {
        self.filters.push(expr);
        self
    }

    /// Set diversity constraints.
    #[must_use]
    pub const fn diversity(mut self, constraints: DiversityConstraints) -> Self {
        self.diversity = Some(constraints);
        self
    }

    /// Set the maximum number of results. Must be in `[1, 500]`.
    #[must_use]
    pub const fn limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Add entity IDs to exclude from results.
    #[must_use]
    pub fn exclude(mut self, ids: Vec<EntityId>) -> Self {
        self.exclude = ids;
        self
    }

    /// Set a pagination cursor from a previous response.
    #[must_use]
    pub const fn cursor(mut self, cursor: Cursor) -> Self {
        self.cursor = Some(cursor);
        self
    }

    /// Set the user ID for personalized ranking (M3+).
    #[must_use]
    pub const fn for_user(mut self, user_id: u64) -> Self {
        self.for_user = Some(user_id);
        self
    }

    /// Set a seed entity for "more like this" queries (M3+).
    #[must_use]
    pub const fn similar_to(mut self, entity_id: EntityId) -> Self {
        self.similar_to = Some(entity_id);
        self
    }

    /// Set free-text context for hybrid search (M3+).
    #[must_use]
    pub fn context(mut self, text: impl Into<String>) -> Self {
        self.context = Some(text.into());
        self
    }

    /// Apply FOR SESSION ranking boost using the given session ID (M4+).
    #[must_use]
    pub const fn for_session(mut self, session_id: SessionId) -> Self {
        self.for_session = Some(session_id);
        self
    }

    /// Scope signal reads to a named cohort (M6+).
    #[must_use]
    pub fn cohort(mut self, name: impl Into<String>) -> Self {
        self.cohort = Some(name.into());
        self
    }

    /// Set an ad-hoc cohort predicate for on-demand cohort queries (M6+).
    ///
    /// The predicate is evaluated against the query user's metadata (requires
    /// `for_user` to be set). If the user matches the predicate, the executor
    /// finds the first registered cohort whose own predicate also matches the
    /// user and uses that cohort's signal ledger.
    ///
    /// # Errors
    ///
    /// Returns `QueryError::InvalidFilter` when:
    /// - `for_user` is not set on the query
    /// - The user matches the predicate but no registered cohort matches
    ///
    /// Use [`RetrieveBuilder::cohort`] to query a named cohort directly, or
    /// call [`TidalDb::define_cohort`] to register a cohort before using this
    /// method.
    #[must_use]
    pub fn cohort_predicate(mut self, pred: crate::cohort::Predicate) -> Self {
        self.cohort_predicate = Some(pred);
        self
    }

    /// Apply notification frequency caps to limit results per creator and in total.
    #[must_use]
    pub const fn notification_caps(mut self, caps: NotificationCaps) -> Self {
        self.notification_caps = Some(caps);
        self
    }

    /// Constrain results to items from a specific creator.
    ///
    /// Adds a `CreatorEq` filter for the creator's ID and sets `for_creator`
    /// on the query for creator-bitmap-based candidate restriction.
    ///
    /// # Known Limitation
    ///
    /// `CreatorEq` stores the creator ID as `u32` (matching the `RoaringBitmap`
    /// item-ID space). Creator IDs > `u32::MAX` (4,294,967,295) will be silently
    /// truncated, producing incorrect filter results. This is a systemic constraint
    /// of the bitmap index design and will be addressed when the bitmap index is
    /// upgraded to 64-bit keys (planned post-M7).
    #[must_use]
    pub fn for_creator(mut self, creator_id: EntityId) -> Self {
        #[allow(clippy::cast_possible_truncation)]
        self.filters
            .push(FilterExpr::CreatorEq(creator_id.as_u64() as u32));
        self.for_creator = Some(creator_id);
        self
    }

    /// Set the entity kind to retrieve.
    #[must_use]
    pub const fn entity(mut self, kind: EntityKind) -> Self {
        self.entity_kind = kind;
        self
    }

    /// Set the ranking profile by name (shorthand for `ProfileRef::new`).
    #[must_use]
    pub fn profile(mut self, name: impl Into<String>) -> Self {
        self.profile = ProfileRef::new(name);
        self
    }

    /// Build the `Retrieve` query, validating the limit range.
    ///
    /// # Errors
    ///
    /// Returns `QueryError::InvalidLimit` if `limit` is 0 or greater than 500.
    pub fn build(self) -> Result<Retrieve, QueryError> {
        if self.limit == 0 || self.limit > 500 {
            return Err(QueryError::InvalidLimit {
                requested: self.limit,
                min: 1,
                max: 500,
            });
        }

        Ok(Retrieve {
            entity_kind: self.entity_kind,
            profile: self.profile,
            filters: self.filters,
            diversity: self.diversity,
            limit: self.limit,
            exclude: self.exclude,
            cursor: self.cursor,
            for_user: self.for_user,
            similar_to: self.similar_to,
            context: self.context,
            for_session: self.for_session,
            cohort: self.cohort,
            cohort_predicate: self.cohort_predicate,
            notification_caps: self.notification_caps,
            for_creator: self.for_creator,
        })
    }
}

// ── Results ─────────────────────────────────────────────────────────────────

/// The response from executing a RETRIEVE query.
///
/// Contains the ranked results, an optional cursor for pagination, and
/// the total candidate count before diversity enforcement.
#[derive(Debug)]
pub struct Results {
    /// Ranked results in descending score order.
    pub items: Vec<RetrieveResult>,
    /// Cursor for fetching the next page. `None` if no more results.
    pub next_cursor: Option<Cursor>,
    /// Total candidates that matched filters before limit/diversity.
    pub total_candidates: usize,
    /// Whether all diversity constraints were satisfied.
    pub constraints_satisfied: bool,
    /// Warnings generated during query execution (e.g., relaxed constraints).
    pub warnings: Vec<String>,
    /// Session snapshot at query time (populated when `for_session` is set).
    pub session_snapshot: Option<SessionSnapshot>,
    /// The degradation level under which this query was executed.
    ///
    /// `Full` means the query ran at full fidelity. Any other level
    /// indicates that quality was reduced due to load pressure. Callers
    /// should treat non-`Full` responses as best-effort: results are
    /// valid but may be less diverse, use coarser aggregation windows,
    /// or draw from a smaller candidate pool.
    pub degradation_level: crate::load::DegradationLevel,
    /// Per-query execution statistics for operational visibility.
    ///
    /// Populated by the executor pipeline. Contains timing, candidate
    /// counts, and the profile name used for scoring.
    pub stats: QueryStats,
}

impl Results {
    /// Number of results returned.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.items.len()
    }

    /// Whether the result set is empty.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

// ── RetrieveResult ──────────────────────────────────────────────────────────

/// A single ranked result from a RETRIEVE query.
///
/// Includes the entity ID, its normalized score, the 1-based rank,
/// and the signal values that contributed to the ranking -- enabling
/// explain-ability in API responses.
#[derive(Debug, Clone)]
pub struct RetrieveResult {
    /// The entity that was ranked.
    pub entity_id: EntityId,
    /// Normalized score in `[0.0, 1.0]`.
    pub score: f64,
    /// 1-based rank within the full result set (across pages).
    pub rank: usize,
    /// Signal values that contributed to this score, for explain-ability.
    pub signals: Vec<Signal>,
}

// ── Signal ──────────────────────────────────────────────────────────────────

/// A signal value snapshot attached to a ranked result.
///
/// Captures the signal name, aggregation value, and source for explain-ability.
#[derive(Debug, Clone)]
pub struct Signal {
    /// Signal type name (e.g., `"view"`, `"like"`).
    pub name: String,
    /// The aggregated value that contributed to the score.
    pub value: f64,
    /// Which aggregation produced this value (e.g., `"decay_score"`, `"velocity"`).
    pub source: String,
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests;
