//! Per-user state bitmap indexes.
//!
//! Tracks seen items, blocked creators/items, saved items, liked items,
//! and completion progress. All state is in-memory and rebuilt from
//! relationships + signals on demand.

use std::collections::HashSet;

use dashmap::DashMap;
use roaring::RoaringBitmap;

use super::CreatorItemsBitmap;

/// Blocked state: both individual items and all items from blocked creators.
#[derive(Debug, Default)]
pub struct BlockedState {
    pub creators: HashSet<u64>,
    pub items: RoaringBitmap,
}

/// Per-user state bitmaps for filtering during ranking.
///
/// Thread-safe via `DashMap` -- no mutex on the hot path. Each user's
/// state is independent, so concurrent updates to different users never
/// contend.
pub struct UserStateIndex {
    seen: DashMap<u64, RoaringBitmap>,
    blocked: DashMap<u64, BlockedState>,
    saved: DashMap<u64, RoaringBitmap>,
    liked: DashMap<u64, RoaringBitmap>,
    completion: DashMap<(u64, u64), f64>,
    /// `user_id` -> set of followed creator IDs.
    /// Populated from `RelationshipType::Follows` edges on startup and
    /// maintained in real-time by `write_relationship` / `delete_relationship`.
    follows: DashMap<u64, HashSet<u64>>,
    /// Reverse index: `creator_id` -> set of follower user IDs (as u32 in bitmap).
    ///
    /// Populated from `RelationshipType::Follows` edges. Updated on every
    /// `write_relationship` / `delete_relationship`. Rebuilt from durable
    /// relationship storage on open -- no separate persistence needed.
    ///
    /// Assumption: user IDs fit in u32 for bitmap storage, matching the
    /// pattern used throughout `UserStateIndex` for item IDs.
    creator_followers: DashMap<u64, RoaringBitmap>,
    /// M6p3: save timestamps for `DateSaved` sort.
    /// Maps `(user_id, item_id_u64)` -> `saved_at_ns`.
    save_timestamps: DashMap<(u64, u64), u64>,
}

impl UserStateIndex {
    #[must_use]
    pub fn new() -> Self {
        Self {
            seen: DashMap::new(),
            blocked: DashMap::new(),
            saved: DashMap::new(),
            liked: DashMap::new(),
            completion: DashMap::new(),
            follows: DashMap::new(),
            creator_followers: DashMap::new(),
            save_timestamps: DashMap::new(),
        }
    }

    // ── Seen ────────────────────────────────────────────────────────────

    pub fn mark_seen(&self, user_id: u64, item_id: u32) {
        self.seen.entry(user_id).or_default().insert(item_id);
    }

    #[must_use]
    pub fn is_seen(&self, user_id: u64, item_id: u32) -> bool {
        self.seen
            .get(&user_id)
            .is_some_and(|bm| bm.contains(item_id))
    }

    /// Get the seen bitmap for a user (cloned).
    #[must_use]
    pub fn seen_bitmap(&self, user_id: u64) -> RoaringBitmap {
        self.seen
            .get(&user_id)
            .map(|r| r.clone())
            .unwrap_or_default()
    }

    // ── Blocked ─────────────────────────────────────────────────────────

    /// Hide a specific item for a user.
    pub fn add_hide(&self, user_id: u64, item_id: u32) {
        self.blocked
            .entry(user_id)
            .or_default()
            .items
            .insert(item_id);
    }

    /// Block an entire creator for a user.
    pub fn add_block_creator(&self, user_id: u64, creator_id: u64) {
        self.blocked
            .entry(user_id)
            .or_default()
            .creators
            .insert(creator_id);
    }

    /// Get hidden items bitmap for a user.
    #[must_use]
    pub fn hidden_items(&self, user_id: u64) -> RoaringBitmap {
        self.blocked
            .get(&user_id)
            .map(|state| state.items.clone())
            .unwrap_or_default()
    }

    /// Get blocked creator IDs for a user.
    #[must_use]
    pub fn blocked_creators(&self, user_id: u64) -> HashSet<u64> {
        self.blocked
            .get(&user_id)
            .map(|state| state.creators.clone())
            .unwrap_or_default()
    }

    /// Returns a predicate that checks whether an item ID is unseen for a user.
    #[must_use]
    pub fn unseen_predicate(&self, user_id: u64) -> Box<dyn Fn(u32) -> bool + Send + Sync> {
        let seen = self.seen_bitmap(user_id);
        let hidden = self.hidden_items(user_id);
        Box::new(move |item_id: u32| !seen.contains(item_id) && !hidden.contains(item_id))
    }

    /// Returns a predicate that checks whether an item is not from a blocked creator.
    ///
    /// Requires the `CreatorItemsBitmap` to expand creator blocks into item blocks.
    #[must_use]
    pub fn unblocked_predicate(
        &self,
        user_id: u64,
        creator_items: &CreatorItemsBitmap,
    ) -> Box<dyn Fn(u32) -> bool + Send + Sync> {
        let blocked_creators = self.blocked_creators(user_id);
        let hidden = self.hidden_items(user_id);

        // Build a bitmap of all items from blocked creators.
        let mut blocked_items = hidden;
        for &cid in &blocked_creators {
            if let Some(bm) = creator_items.get(cid) {
                blocked_items |= &bm;
            }
        }

        Box::new(move |item_id: u32| !blocked_items.contains(item_id))
    }

    /// Build a bitmap of all items from creators the user follows.
    #[must_use]
    pub fn follows_bitmap(
        &self,
        _user_id: u64,
        followed_creator_ids: &[u64],
        creator_items: &CreatorItemsBitmap,
    ) -> RoaringBitmap {
        creator_items.union_for(followed_creator_ids)
    }

    /// Remove a creator block for a user.
    pub fn remove_block_creator(&self, user_id: u64, creator_id: u64) {
        if let Some(mut state) = self.blocked.get_mut(&user_id) {
            state.creators.remove(&creator_id);
        }
    }

    /// Remove a hidden item for a user.
    pub fn remove_hide(&self, user_id: u64, item_id: u32) {
        if let Some(mut bm) = self.seen.get_mut(&user_id) {
            bm.remove(item_id);
        }
        if let Some(mut state) = self.blocked.get_mut(&user_id) {
            state.items.remove(item_id);
        }
    }

    // ── Follows ─────────────────────────────────────────────────────────

    /// Record that a user follows a creator.
    pub fn add_follow(&self, user_id: u64, creator_id: u64) {
        self.follows.entry(user_id).or_default().insert(creator_id);
    }

    /// Remove a follow relationship.
    pub fn remove_follow(&self, user_id: u64, creator_id: u64) {
        if let Some(mut set) = self.follows.get_mut(&user_id) {
            set.remove(&creator_id);
        }
    }

    /// Get the set of creator IDs this user follows.
    #[must_use]
    pub fn followed_creators(&self, user_id: u64) -> HashSet<u64> {
        self.follows
            .get(&user_id)
            .map(|r| r.clone())
            .unwrap_or_default()
    }

    /// Get the followed creator IDs as a Vec (for convenience).
    #[must_use]
    pub fn followed_creators_vec(&self, user_id: u64) -> Vec<u64> {
        self.follows
            .get(&user_id)
            .map(|r| r.iter().copied().collect())
            .unwrap_or_default()
    }

    // ── Creator Followers (reverse index) ────────────────────────────────

    /// Add a follower to a creator's follower set.
    ///
    /// `creator_id` is the entity being followed; `follower_user_id` is the
    /// user doing the following. Stored as `u32` in a `RoaringBitmap`.
    pub fn add_creator_follower(&self, creator_id: u64, follower_user_id: u64) {
        #[allow(clippy::cast_possible_truncation)]
        self.creator_followers
            .entry(creator_id)
            .or_default()
            .insert(follower_user_id as u32);
    }

    /// Remove a follower from a creator's follower set.
    pub fn remove_creator_follower(&self, creator_id: u64, follower_user_id: u64) {
        #[allow(clippy::cast_possible_truncation)]
        if let Some(mut bm) = self.creator_followers.get_mut(&creator_id) {
            bm.remove(follower_user_id as u32);
        }
    }

    /// Get the set of follower user IDs for a creator (as a bitmap, cloned).
    #[must_use]
    pub fn followers_bitmap(&self, creator_id: u64) -> RoaringBitmap {
        self.creator_followers
            .get(&creator_id)
            .map(|r| r.clone())
            .unwrap_or_default()
    }

    /// Get follower user IDs for a creator as a `Vec<u64>`.
    #[must_use]
    pub fn follower_ids(&self, creator_id: u64) -> Vec<u64> {
        self.creator_followers
            .get(&creator_id)
            .map(|bm| bm.iter().map(u64::from).collect())
            .unwrap_or_default()
    }

    // ── Saved / Liked ───────────────────────────────────────────────────

    pub fn add_save(&self, user_id: u64, item_id: u32) {
        self.saved.entry(user_id).or_default().insert(item_id);
    }

    /// Save an item with a timestamp (for `DateSaved` sort).
    ///
    /// Also adds the item to the saved bitmap for inclusion filters.
    pub fn add_save_timestamped(&self, user_id: u64, item_id: u32, timestamp_ns: u64) {
        self.saved.entry(user_id).or_default().insert(item_id);
        self.save_timestamps
            .insert((user_id, u64::from(item_id)), timestamp_ns);
    }

    /// Get the save timestamp for a (user, item) pair.
    ///
    /// Returns `None` if the item was not saved via `add_save_timestamped` or
    /// was saved via the older `add_save` (which does not record timestamps).
    #[must_use]
    pub fn get_save_timestamp(&self, user_id: u64, item_id: u64) -> Option<u64> {
        self.save_timestamps.get(&(user_id, item_id)).map(|r| *r)
    }

    #[must_use]
    pub fn is_saved(&self, user_id: u64, item_id: u32) -> bool {
        self.saved
            .get(&user_id)
            .is_some_and(|bm| bm.contains(item_id))
    }

    /// Get the saved-items bitmap for a user (cloned).
    #[must_use]
    pub fn saved_bitmap(&self, user_id: u64) -> RoaringBitmap {
        self.saved
            .get(&user_id)
            .map(|r| r.clone())
            .unwrap_or_default()
    }

    pub fn add_like(&self, user_id: u64, item_id: u32) {
        self.liked.entry(user_id).or_default().insert(item_id);
    }

    #[must_use]
    pub fn is_liked(&self, user_id: u64, item_id: u32) -> bool {
        self.liked
            .get(&user_id)
            .is_some_and(|bm| bm.contains(item_id))
    }

    /// Get the liked-items bitmap for a user (cloned).
    #[must_use]
    pub fn liked_bitmap(&self, user_id: u64) -> RoaringBitmap {
        self.liked
            .get(&user_id)
            .map(|r| r.clone())
            .unwrap_or_default()
    }

    // ── Completion ──────────────────────────────────────────────────────

    pub fn record_completion(&self, user_id: u64, item_id: u64, ratio: f64) {
        self.completion.insert((user_id, item_id), ratio);
    }

    #[must_use]
    pub fn get_completion(&self, user_id: u64, item_id: u64) -> Option<f64> {
        self.completion.get(&(user_id, item_id)).map(|r| *r)
    }

    /// Check if an item is "in progress" (0.0 < completion < threshold).
    #[must_use]
    pub fn is_in_progress(&self, user_id: u64, item_id: u64, threshold: f64) -> bool {
        self.completion
            .get(&(user_id, item_id))
            .is_some_and(|r| *r > 0.0 && *r < threshold)
    }

    /// List all item IDs with partial completion for a user (0 < v < threshold).
    ///
    /// Returns a `RoaringBitmap` of item IDs (truncated to u32) that satisfy
    /// the in-progress predicate. Used by Stage 2.5 of the executor to build
    /// the inclusion set for `FilterExpr::InProgress`.
    #[must_use]
    pub fn in_progress_items(&self, user_id: u64, threshold: f64) -> RoaringBitmap {
        let mut bm = RoaringBitmap::new();
        for entry in &self.completion {
            let &(uid, iid) = entry.key();
            if uid == user_id && *entry.value() > 0.0 && *entry.value() < threshold {
                #[allow(clippy::cast_possible_truncation)]
                bm.insert(iid as u32);
            }
        }
        bm
    }
}

impl Default for UserStateIndex {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
    use super::*;

    #[test]
    fn seen_mark_and_check() {
        let idx = UserStateIndex::new();
        assert!(!idx.is_seen(1, 10));
        idx.mark_seen(1, 10);
        assert!(idx.is_seen(1, 10));
        assert!(!idx.is_seen(1, 20));
        assert!(!idx.is_seen(2, 10)); // different user
    }

    #[test]
    fn hide_item() {
        let idx = UserStateIndex::new();
        idx.add_hide(1, 100);
        let hidden = idx.hidden_items(1);
        assert!(hidden.contains(100));
        assert!(!hidden.contains(200));
    }

    #[test]
    fn block_creator() {
        let idx = UserStateIndex::new();
        idx.add_block_creator(1, 77);
        let blocked = idx.blocked_creators(1);
        assert!(blocked.contains(&77));
    }

    #[test]
    fn unseen_predicate_excludes_seen_and_hidden() {
        let idx = UserStateIndex::new();
        idx.mark_seen(1, 10);
        idx.add_hide(1, 20);

        let pred = idx.unseen_predicate(1);
        assert!(!pred(10)); // seen
        assert!(!pred(20)); // hidden
        assert!(pred(30)); // neither
    }

    #[test]
    fn unblocked_predicate_excludes_blocked_creator_items() {
        let idx = UserStateIndex::new();
        let creator_items = CreatorItemsBitmap::new();
        creator_items.add_item(77, 100);
        creator_items.add_item(77, 101);

        idx.add_block_creator(1, 77);
        idx.add_hide(1, 200);

        let pred = idx.unblocked_predicate(1, &creator_items);
        assert!(!pred(100)); // from blocked creator
        assert!(!pred(101)); // from blocked creator
        assert!(!pred(200)); // hidden
        assert!(pred(300)); // ok
    }

    #[test]
    fn save_and_like() {
        let idx = UserStateIndex::new();
        idx.add_save(1, 10);
        idx.add_like(1, 20);

        assert!(idx.is_saved(1, 10));
        assert!(!idx.is_saved(1, 20));
        assert!(idx.is_liked(1, 20));
        assert!(!idx.is_liked(1, 10));
    }

    #[test]
    fn saved_bitmap_returns_all_saved_items() {
        let idx = UserStateIndex::new();
        idx.add_save(1, 10);
        idx.add_save(1, 20);
        idx.add_save(1, 30);
        idx.add_save(2, 99); // different user

        let bm = idx.saved_bitmap(1);
        assert_eq!(bm.len(), 3);
        assert!(bm.contains(10));
        assert!(bm.contains(20));
        assert!(bm.contains(30));
        assert!(!bm.contains(99)); // belongs to user 2

        // Unknown user returns empty bitmap.
        assert!(idx.saved_bitmap(999).is_empty());
    }

    #[test]
    fn liked_bitmap_returns_all_liked_items() {
        let idx = UserStateIndex::new();
        idx.add_like(1, 5);
        idx.add_like(1, 15);
        idx.add_like(2, 25); // different user

        let bm = idx.liked_bitmap(1);
        assert_eq!(bm.len(), 2);
        assert!(bm.contains(5));
        assert!(bm.contains(15));
        assert!(!bm.contains(25));

        // Unknown user returns empty bitmap.
        assert!(idx.liked_bitmap(999).is_empty());
    }

    // ── Creator Followers Tests ──────────────────────────────────────────

    #[test]
    fn add_creator_follower_and_query() {
        let idx = UserStateIndex::new();
        idx.add_creator_follower(100, 1); // creator 100, follower user 1
        idx.add_creator_follower(100, 2);
        idx.add_creator_follower(200, 3);

        let bm = idx.followers_bitmap(100);
        assert_eq!(bm.len(), 2);
        assert!(bm.contains(1));
        assert!(bm.contains(2));

        let ids = idx.follower_ids(100);
        assert_eq!(ids.len(), 2);
        assert!(ids.contains(&1));
        assert!(ids.contains(&2));

        // Creator 200 has only user 3.
        assert_eq!(idx.follower_ids(200), vec![3]);

        // Unknown creator returns empty.
        assert!(idx.followers_bitmap(999).is_empty());
        assert!(idx.follower_ids(999).is_empty());
    }

    #[test]
    fn remove_creator_follower() {
        let idx = UserStateIndex::new();
        idx.add_creator_follower(100, 1);
        idx.add_creator_follower(100, 2);

        idx.remove_creator_follower(100, 1);
        let bm = idx.followers_bitmap(100);
        assert_eq!(bm.len(), 1);
        assert!(!bm.contains(1));
        assert!(bm.contains(2));

        // Removing from unknown creator is a no-op.
        idx.remove_creator_follower(999, 1);
    }

    #[test]
    fn completion_tracking() {
        let idx = UserStateIndex::new();
        idx.record_completion(1, 10, 0.5);
        assert_eq!(idx.get_completion(1, 10), Some(0.5));
        assert!(idx.is_in_progress(1, 10, 0.8));
        assert!(!idx.is_in_progress(1, 10, 0.4)); // 0.5 >= 0.4

        idx.record_completion(1, 10, 0.9);
        assert!(!idx.is_in_progress(1, 10, 0.8)); // 0.9 >= 0.8
    }

    #[test]
    fn in_progress_items_bitmap() {
        let idx = UserStateIndex::new();
        idx.record_completion(1, 10, 0.3); // in progress
        idx.record_completion(1, 20, 0.9); // complete
        idx.record_completion(1, 30, 0.0); // not started
        idx.record_completion(1, 40, 0.5); // in progress
        idx.record_completion(2, 50, 0.4); // different user

        let bm = idx.in_progress_items(1, 0.8);
        assert_eq!(bm.len(), 2);
        assert!(bm.contains(10));
        assert!(bm.contains(40));
        assert!(!bm.contains(20)); // >= threshold
        assert!(!bm.contains(30)); // == 0.0
        assert!(!bm.contains(50)); // different user
    }
}
