//! Social graph filter resolution for RETRIEVE queries.
//!
//! Builds a `RoaringBitmap` of item IDs reachable from a user's social graph.
//!
//! - **depth=1:** Items from creators the user follows.
//! - **depth=2:** depth-1 items UNION items seen by users who follow the same creators.
//!
//! # Fan-out cap
//!
//! At depth=2 the subgraph resolver expands through co-followers and their
//! followed creators. Popular creators with large follower sets can produce
//! combinatorial fan-out. `MAX_SUBGRAPH_USERS` caps the resolved user set
//! to prevent unbounded query-time expansion.

/// Maximum number of users to include in a resolved social subgraph.
///
/// Limits depth=2 fan-out: if more than this many co-followers are found,
/// the first `MAX_SUBGRAPH_USERS` encountered are used (insertion order in
/// the `HashSet`). This is a best-effort bound — the exact subset retained
/// is not deterministic but prevents O(followers²) expansion.
pub const MAX_SUBGRAPH_USERS: usize = 500;

use std::collections::HashSet;

use roaring::RoaringBitmap;

use crate::entities::{CreatorItemsBitmap, UserStateIndex};

/// Build the bitmap of items reachable from a user's social graph.
///
/// # Arguments
///
/// - `user_id` -- the requesting user.
/// - `depth` -- 1 for direct follows, 2 to expand to follower community.
/// - `user_state` -- user state index (follows, seen bitmaps, reverse index).
/// - `creator_items` -- creator-to-items bitmap.
///
/// # Depth semantics
///
/// - **depth=1:** Union of items from all creators the user follows.
/// - **depth=2:** depth-1 UNION (for each followed creator, the `seen_bitmap`
///   of every other follower of that creator). This captures "items that the
///   follower community has engaged with."
#[must_use]
pub fn social_graph_bitmap(
    user_id: u64,
    depth: u8,
    user_state: &UserStateIndex,
    creator_items: &CreatorItemsBitmap,
) -> RoaringBitmap {
    let followed_creators = user_state.followed_creators_vec(user_id);
    let mut result = creator_items.union_for(&followed_creators);

    if depth >= 2 {
        // Expand to items seen by users who follow the same creators
        // (the "follower community seen items" expansion).
        // Capped at MAX_SUBGRAPH_USERS to prevent fan-out on popular creators.
        let mut visited_users = HashSet::new();
        'outer: for &creator_id in &followed_creators {
            for follower_uid in user_state
                .followers_bitmap(creator_id)
                .iter()
                .map(u64::from)
            {
                // Skip self.
                if follower_uid == user_id {
                    continue;
                }
                // Enforce fan-out cap.
                if visited_users.len() >= MAX_SUBGRAPH_USERS {
                    break 'outer;
                }
                if visited_users.insert(follower_uid) {
                    let seen = user_state.seen_bitmap(follower_uid);
                    result |= &seen;
                }
            }
        }
    }

    result
}

/// Resolve the set of user IDs in the social subgraph for trending scoping.
///
/// Returns all users who follow any of the same creators as `user_id`,
/// excluding `user_id` itself. For depth=2, also includes users that
/// those users follow (2-hop expansion).
#[must_use]
pub fn resolve_social_subgraph_users(
    user_id: u64,
    depth: u8,
    user_state: &UserStateIndex,
) -> Vec<u64> {
    let followed_creators = user_state.followed_creators_vec(user_id);
    let mut user_ids = HashSet::new();
    for &creator_id in &followed_creators {
        for uid in user_state.follower_ids(creator_id) {
            if uid != user_id {
                user_ids.insert(uid);
            }
        }
    }

    if depth >= 2 {
        // 2-hop: also include users followed by depth-1 users.
        // Capped at MAX_SUBGRAPH_USERS total to prevent combinatorial fan-out.
        let depth1_users: Vec<u64> = user_ids.iter().copied().collect();
        'outer2: for uid in depth1_users {
            let their_creators = user_state.followed_creators_vec(uid);
            for &creator_id in &their_creators {
                for uid2 in user_state
                    .followers_bitmap(creator_id)
                    .iter()
                    .map(u64::from)
                {
                    if uid2 != user_id {
                        if user_ids.len() >= MAX_SUBGRAPH_USERS {
                            break 'outer2;
                        }
                        user_ids.insert(uid2);
                    }
                }
            }
        }
    }

    user_ids.into_iter().collect()
}

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

    #[test]
    fn depth1_returns_items_from_followed_creators() {
        let user_state = UserStateIndex::new();
        let creator_items = CreatorItemsBitmap::new();

        // User 1 follows creators 10, 20.
        user_state.add_follow(1, 10);
        user_state.add_follow(1, 20);
        user_state.add_creator_follower(10, 1);
        user_state.add_creator_follower(20, 1);

        // Creator 10 has items 100, 101.
        creator_items.add_item(10, 100);
        creator_items.add_item(10, 101);
        // Creator 20 has items 200, 201.
        creator_items.add_item(20, 200);
        creator_items.add_item(20, 201);
        // Creator 30 has items 300 (not followed).
        creator_items.add_item(30, 300);

        let bm = social_graph_bitmap(1, 1, &user_state, &creator_items);
        assert_eq!(bm.len(), 4);
        assert!(bm.contains(100));
        assert!(bm.contains(101));
        assert!(bm.contains(200));
        assert!(bm.contains(201));
        assert!(!bm.contains(300)); // not followed
    }

    #[test]
    fn depth2_expands_to_follower_community_seen_items() {
        let user_state = UserStateIndex::new();
        let creator_items = CreatorItemsBitmap::new();

        // User 1 follows creator 10.
        user_state.add_follow(1, 10);
        user_state.add_creator_follower(10, 1);

        // User 2 also follows creator 10.
        user_state.add_follow(2, 10);
        user_state.add_creator_follower(10, 2);

        // Creator 10 has item 100.
        creator_items.add_item(10, 100);

        // User 2 has seen items 500, 600 (not from creator 10).
        user_state.mark_seen(2, 500);
        user_state.mark_seen(2, 600);

        let bm = social_graph_bitmap(1, 2, &user_state, &creator_items);
        // depth-1: item 100 (from creator 10)
        // depth-2: items 500, 600 (seen by user 2, co-follower of creator 10)
        assert!(bm.contains(100));
        assert!(bm.contains(500));
        assert!(bm.contains(600));
        assert_eq!(bm.len(), 3);
    }

    #[test]
    fn resolve_subgraph_users_depth1() {
        let user_state = UserStateIndex::new();

        // User 1 follows creators 10, 20.
        user_state.add_follow(1, 10);
        user_state.add_follow(1, 20);
        user_state.add_creator_follower(10, 1);
        user_state.add_creator_follower(20, 1);

        // User 2 follows creator 10.
        user_state.add_follow(2, 10);
        user_state.add_creator_follower(10, 2);

        // User 3 follows creator 20.
        user_state.add_follow(3, 20);
        user_state.add_creator_follower(20, 3);

        let users = resolve_social_subgraph_users(1, 1, &user_state);
        // Should include users 2 and 3, but NOT user 1 (self).
        assert_eq!(users.len(), 2);
        assert!(users.contains(&2));
        assert!(users.contains(&3));
    }

    #[test]
    fn resolve_subgraph_empty_for_no_follows() {
        let user_state = UserStateIndex::new();
        let users = resolve_social_subgraph_users(1, 1, &user_state);
        assert!(users.is_empty());
    }
}
