//! Built-in ranking profiles.
//!
//! `TidalDB` ships 25 default profiles covering the most common content ranking
//! patterns: trending, hot, new, top (by window), hidden gems, controversial,
//! most viewed, most liked, shuffle, four personalized profiles added in M3
//! (`for_you`, `following`, `related`, `notification`), a search profile (M5),
//! a cohort-scoped trending profile (M6, `cohort_trending`), and full sort mode
//! coverage profiles (M6p3: `live`, `alphabetical_asc`, `alphabetical_desc`,
//! `shortest`, `longest`, `most_commented`, `most_shared`, `date_saved`).
//!
//! Population-level profiles use `CandidateStrategy::Scan` with `sort_field =
//! "created_at"`. Personalized profiles use `Relationship` strategy.
//! All are registered at version 1 with `is_builtin = true`.

use crate::schema::Window;

use super::profile::{Boost, CandidateStrategy, DiversitySpec, RankingProfile, SignalAgg, Sort};
use super::registry::{ProfileError, ProfileRegistry};

/// Default candidate strategy for built-in profiles.
fn default_strategy() -> CandidateStrategy {
    CandidateStrategy::Scan {
        sort_field: "created_at".into(),
    }
}

/// Build a profile skeleton with common defaults.
fn skeleton(name: &str) -> RankingProfile {
    RankingProfile {
        name: name.to_owned(),
        version: 1,
        candidate_strategy: default_strategy(),
        boosts: vec![],
        decay: None,
        gates: vec![],
        penalties: vec![],
        excludes: vec![],
        diversity: DiversitySpec::default(),
        exploration: 0.0,
        sort: None,
        is_builtin: true,
    }
}

// ── Profile tuning constants ──────────────────────────────────────────────

/// Age-decay gravity for the hot sort (Reddit-style HN algorithm).
/// Higher values decay older content faster.
const HOT_GRAVITY: f64 = 1.8;

/// Exploration fraction injected into shuffle results.
/// 0.5 = 50% random exploration, 50% signal-ranked.
const SHUFFLE_EXPLORATION: f64 = 0.5;

/// Signal weight multiplier for share events in the trending score.
const TRENDING_SHARE_WEIGHT: f64 = 2.0;

/// Maximum items per creator in trending results (content diversity).
const TRENDING_MAX_PER_CREATOR: usize = 1;

/// Maximum items per creator in hot results.
const HOT_MAX_PER_CREATOR: usize = 2;

/// Register all 25 built-in ranking profiles into the given registry.
///
/// # Errors
///
/// Returns `ProfileError` if any profile fails validation. This should never
/// happen for built-in profiles -- if it does, it is a bug in the profile
/// definitions.
pub fn register_builtins(registry: &mut ProfileRegistry) -> Result<(), ProfileError> {
    // Population-level profiles (M2).
    registry.register(trending())?;
    registry.register(hot())?;
    registry.register(new())?;
    registry.register(top_week())?;
    registry.register(top_month())?;
    registry.register(top_all_time())?;
    registry.register(hidden_gems())?;
    registry.register(controversial())?;
    registry.register(most_viewed())?;
    registry.register(most_liked())?;
    registry.register(shuffle())?;
    // Personalized profiles (M3).
    registry.register(for_you())?;
    registry.register(following())?;
    registry.register(related())?;
    registry.register(notification())?;
    // Search profile (M5).
    registry.register(search())?;
    // Cohort profile (M6).
    registry.register(cohort_trending())?;
    // M6p3 profiles: live content + full sort mode coverage.
    registry.register(live())?;
    registry.register(alphabetical_asc())?;
    registry.register(alphabetical_desc())?;
    registry.register(shortest())?;
    registry.register(longest())?;
    registry.register(most_commented())?;
    registry.register(most_shared())?;
    registry.register(date_saved())?;
    Ok(())
}

fn trending() -> RankingProfile {
    let mut p = skeleton("trending");
    p.sort = Some(Sort::Trending);
    p.boosts = vec![
        Boost {
            signal: "share".into(),
            agg: SignalAgg::Velocity,
            window: Window::TwentyFourHours,
            weight: TRENDING_SHARE_WEIGHT,
        },
        Boost {
            signal: "view".into(),
            agg: SignalAgg::Velocity,
            window: Window::TwentyFourHours,
            weight: 1.0,
        },
    ];
    // Gate on engagement_ratio deferred to application-level profiles.
    // Built-in profiles avoid gates on signals that may not be in the schema.
    p.diversity = DiversitySpec {
        max_per_creator: Some(TRENDING_MAX_PER_CREATOR),
        ..DiversitySpec::default()
    };
    p
}

fn hot() -> RankingProfile {
    let mut p = skeleton("hot");
    p.sort = Some(Sort::Hot {
        gravity: HOT_GRAVITY,
    });
    p.boosts = vec![Boost {
        signal: "view".into(),
        agg: SignalAgg::Velocity,
        window: Window::OneHour,
        weight: 1.0,
    }];
    p.diversity = DiversitySpec {
        max_per_creator: Some(HOT_MAX_PER_CREATOR),
        ..DiversitySpec::default()
    };
    p
}

fn new() -> RankingProfile {
    let mut p = skeleton("new");
    p.sort = Some(Sort::New);
    p
}

fn top_week() -> RankingProfile {
    let mut p = skeleton("top_week");
    p.sort = Some(Sort::TopWindow {
        window: Window::SevenDays,
    });
    p
}

fn top_month() -> RankingProfile {
    let mut p = skeleton("top_month");
    // TODO(m8): Use Window::ThirtyDays once hourly rollups are implemented.
    // ThirtyDays currently returns 0 (not implemented), which produces arbitrary
    // ordering. SevenDays is the best available approximation until M8 rollups.
    p.sort = Some(Sort::TopWindow {
        window: Window::SevenDays,
    });
    p
}

fn top_all_time() -> RankingProfile {
    let mut p = skeleton("top_all_time");
    p.sort = Some(Sort::TopWindow {
        window: Window::AllTime,
    });
    p
}

fn hidden_gems() -> RankingProfile {
    let mut p = skeleton("hidden_gems");
    p.sort = Some(Sort::HiddenGems);
    // Gates on completion/view deferred to application-level profiles.
    // Built-in profiles avoid gates on signals that may not be in the schema.
    p
}

fn controversial() -> RankingProfile {
    let mut p = skeleton("controversial");
    p.sort = Some(Sort::Controversial);
    // Gates on like/dislike deferred to application-level profiles.
    // Built-in profiles avoid gates on signals that may not be in the schema.
    p
}

fn most_viewed() -> RankingProfile {
    let mut p = skeleton("most_viewed");
    p.sort = Some(Sort::MostViewed {
        window: Window::SevenDays,
    });
    p
}

fn most_liked() -> RankingProfile {
    let mut p = skeleton("most_liked");
    p.sort = Some(Sort::MostLiked {
        window: Window::SevenDays,
    });
    p
}

fn shuffle() -> RankingProfile {
    let mut p = skeleton("shuffle");
    p.sort = Some(Sort::Shuffle);
    p.exploration = SHUFFLE_EXPLORATION;
    p
}

// ── M3 Personalized Profiles ────────────────────────────────────────────────

/// Maximum items per creator in `for_you` results.
const FOR_YOU_MAX_PER_CREATOR: usize = 2;

/// Exploration fraction for the `for_you` profile.
/// 10% random exploration to prevent filter bubbles.
const FOR_YOU_EXPLORATION: f64 = 0.1;

/// Maximum items per creator in following results.
const FOLLOWING_MAX_PER_CREATOR: usize = 3;

/// `for_you`: personalized home feed ranking.
///
/// Combines interaction-weighted decay scores with exploration injection.
/// Uses `Scan` strategy (M3 user-context filtering in Stage 2.5 handles
/// unseen/unblocked). The `for_user` clause triggers preference-aware
/// scoring in the executor.
fn for_you() -> RankingProfile {
    let mut p = skeleton("for_you");
    p.sort = Some(Sort::Hot { gravity: 1.5 });
    p.boosts = vec![
        Boost {
            signal: "view".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: 1.0,
        },
        Boost {
            signal: "like".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: 2.0,
        },
        Boost {
            signal: "share".into(),
            agg: SignalAgg::Velocity,
            window: Window::TwentyFourHours,
            weight: 1.5,
        },
    ];
    p.diversity = DiversitySpec {
        max_per_creator: Some(FOR_YOU_MAX_PER_CREATOR),
        format_mix_max_fraction: Some(0.4),
    };
    p.exploration = FOR_YOU_EXPLORATION;
    p
}

/// following: content from followed creators, ranked by interaction strength.
///
/// Uses `Relationship` candidate strategy -- the executor sources
/// candidates from the user's relationship graph. Ranked by recency
/// (`Sort::New`) with a view decay boost.
fn following() -> RankingProfile {
    let mut p = skeleton("following");
    p.candidate_strategy = CandidateStrategy::Relationship;
    p.sort = Some(Sort::New);
    p.boosts = vec![Boost {
        signal: "view".into(),
        agg: SignalAgg::Velocity,
        window: Window::OneHour,
        weight: 0.5,
    }];
    p.diversity = DiversitySpec {
        max_per_creator: Some(FOLLOWING_MAX_PER_CREATOR),
        ..DiversitySpec::default()
    };
    p
}

/// related: "more like this" ranking for a seed item.
///
/// Uses `Scan` strategy. When `similar_to` is set in the query, the
/// executor will use the seed item's embedding for ANN in Stage 1
/// (when vector indexes are wired). For M3, falls back to scan with
/// content-type boosting.
fn related() -> RankingProfile {
    let mut p = skeleton("related");
    p.sort = Some(Sort::Hot { gravity: 1.2 });
    p.boosts = vec![
        Boost {
            signal: "view".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: 1.0,
        },
        Boost {
            signal: "completion".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: 1.5,
        },
    ];
    p.diversity = DiversitySpec {
        max_per_creator: Some(2),
        ..DiversitySpec::default()
    };
    p
}

/// notification: items a user should be notified about.
///
/// Prioritizes high-velocity content from creators the user follows.
/// Strict diversity ensures no single creator dominates the notification
/// tray.
fn notification() -> RankingProfile {
    let mut p = skeleton("notification");
    p.candidate_strategy = CandidateStrategy::Relationship;
    p.sort = Some(Sort::Trending);
    p.boosts = vec![
        Boost {
            signal: "view".into(),
            agg: SignalAgg::Velocity,
            window: Window::OneHour,
            weight: 2.0,
        },
        Boost {
            signal: "like".into(),
            agg: SignalAgg::Velocity,
            window: Window::OneHour,
            weight: 1.0,
        },
    ];
    p.diversity = DiversitySpec {
        max_per_creator: Some(1),
        ..DiversitySpec::default()
    };
    p
}

// ── M5 Search Profile ───────────────────────────────────────────────────────

/// Weight for view decay score in the search profile.
///
/// Lower than personalized profiles to let text relevance dominate.
const SEARCH_VIEW_WEIGHT: f64 = 0.5;

/// Weight for like decay score in the search profile.
///
/// Captures quality signal: items frequently liked tend to be good results.
const SEARCH_LIKE_WEIGHT: f64 = 0.8;

/// `search`: text and vector relevance plus light signal re-ranking.
///
/// Default profile for the SEARCH query type. The heavy lifting is done by
/// RRF fusion (BM25 + ANN) in Stage 1c of the `SearchExecutor`. This profile
/// provides a lightweight signal overlay: a view-decay and like-decay boost to
/// surface quality content without overriding text relevance signals.
///
/// - No exploration injection (`exploration = 0.0`): search results must be
///   deterministic for a given query.
/// - No diversity enforcement: callers specify diversity explicitly via
///   `SearchBuilder::diversity()`.
/// - `sort = None`: the fused RRF score from Stage 1c is the primary ordering
///   signal; the profile adds a small quality overlay on top.
fn search() -> RankingProfile {
    let mut p = skeleton("search");
    p.boosts = vec![
        Boost {
            signal: "view".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: SEARCH_VIEW_WEIGHT,
        },
        Boost {
            signal: "like".into(),
            agg: SignalAgg::DecayScore,
            window: Window::AllTime,
            weight: SEARCH_LIKE_WEIGHT,
        },
    ];
    // No diversity: callers control diversity via SearchBuilder.
    // No exploration: search results are deterministic.
    p.exploration = 0.0;
    p.sort = None;
    p
}

// ── M6 Cohort Profiles ───────────────────────────────────────────────────────

/// `cohort_trending`: trending content scoped to a named cohort.
///
/// Identical boosts and sort mode to the global `trending` profile, but
/// intended for use with `RetrieveBuilder::cohort("my_cohort")`. The executor
/// reads signal values from the cohort signal ledger instead of the global
/// ledger. Without a `cohort` clause, this profile behaves identically to
/// `trending`.
fn cohort_trending() -> RankingProfile {
    let mut p = skeleton("cohort_trending");
    p.sort = Some(Sort::Trending);
    p.boosts = vec![
        Boost {
            signal: "share".into(),
            agg: SignalAgg::Velocity,
            window: Window::TwentyFourHours,
            weight: TRENDING_SHARE_WEIGHT,
        },
        Boost {
            signal: "view".into(),
            agg: SignalAgg::Velocity,
            window: Window::TwentyFourHours,
            weight: 1.0,
        },
    ];
    p.diversity = DiversitySpec {
        max_per_creator: Some(TRENDING_MAX_PER_CREATOR),
        ..DiversitySpec::default()
    };
    p
}

// ── M6p3 Live Content Profile ───────────────────────────────────────────────

/// Weight for the relationship-preference boost in the live profile.
///
/// Lower than the `following` profile's 0.5 because the primary sort key
/// (`LiveViewerCount`) already dominates ordering. The boost lifts content
/// from creators the user actively views (social circle), making relationship
/// weight "dominant" without overriding the viewer-count signal entirely.
const LIVE_RELATIONSHIP_BOOST_WEIGHT: f64 = 0.3;

/// `live`: live content ranking by current viewer count.
///
/// Sorts by `LiveViewerCount` (decayed `viewer_count` signal) with strict
/// per-creator diversity (max 1 per creator). A `view` velocity boost gives
/// preference to content from creators the user's social circle actively
/// watches (relationship-weight dominant, per UC-12).
fn live() -> RankingProfile {
    let mut p = skeleton("live");
    p.sort = Some(Sort::LiveViewerCount);
    p.boosts = vec![Boost {
        signal: "view".into(),
        agg: SignalAgg::Velocity,
        window: Window::OneHour,
        weight: LIVE_RELATIONSHIP_BOOST_WEIGHT,
    }];
    p.diversity = DiversitySpec {
        max_per_creator: Some(1),
        ..DiversitySpec::default()
    };
    p
}

/// `alphabetical_asc`: sort by item title A-Z (case-insensitive).
fn alphabetical_asc() -> RankingProfile {
    let mut p = skeleton("alphabetical_asc");
    p.sort = Some(Sort::AlphabeticalAsc);
    p
}

/// `alphabetical_desc`: sort by item title Z-A (case-insensitive).
fn alphabetical_desc() -> RankingProfile {
    let mut p = skeleton("alphabetical_desc");
    p.sort = Some(Sort::AlphabeticalDesc);
    p
}

/// `shortest`: sort by item duration, shortest first.
fn shortest() -> RankingProfile {
    let mut p = skeleton("shortest");
    p.sort = Some(Sort::Shortest);
    p
}

/// `longest`: sort by item duration, longest first.
fn longest() -> RankingProfile {
    let mut p = skeleton("longest");
    p.sort = Some(Sort::Longest);
    p
}

/// `most_commented`: sort by comment count (`AllTime` window).
fn most_commented() -> RankingProfile {
    let mut p = skeleton("most_commented");
    p.sort = Some(Sort::MostCommented {
        window: Window::AllTime,
    });
    p
}

/// `most_shared`: sort by share count (`AllTime` window).
fn most_shared() -> RankingProfile {
    let mut p = skeleton("most_shared");
    p.sort = Some(Sort::MostShared {
        window: Window::AllTime,
    });
    p
}

/// `date_saved`: sort by when the querying user saved the item (latest first).
///
/// Requires `FOR USER` context in the query. Without it, the executor
/// returns `QueryError::InvalidFilter`.
fn date_saved() -> RankingProfile {
    let mut p = skeleton("date_saved");
    p.sort = Some(Sort::DateSaved);
    p
}

// ── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests;
