//! Entity types and relationship graph for personalized ranking.
//!
//! This module introduces `UserEntity`, `CreatorEntity`, the relationship
//! graph, user-state bitmap indexes, preference vectors, interaction weights,
//! and hard negatives. Together they form the foundation for M3 personalized
//! ranking: the system can track who a user follows, what they have seen,
//! what they have blocked, and how their taste evolves.

pub mod co_engagement;
pub mod collection;
pub mod creator;
pub mod hard_neg;
pub mod interaction;
pub mod preference;
pub mod relationship;
pub mod user;
pub mod user_signal;
pub mod user_state;

pub use co_engagement::CoEngagementIndex;
pub use collection::{
    Collection, CollectionId, CollectionIndex, CollectionInfo, Visibility, deserialize_collection,
    serialize_collection,
};
pub use creator::CreatorEntity;
pub use hard_neg::HardNegIndex;
pub use interaction::InteractionLedger;
pub use preference::PreferenceVectors;
pub use relationship::{RelationshipEdge, RelationshipType};
pub use user::UserEntity;
pub use user_signal::UserSignalIndex;
pub use user_state::UserStateIndex;

use std::collections::HashMap;

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

// ── Creator-Items Bitmap ────────────────────────────────────────────────────

/// Maps each creator to the set of item IDs they have produced.
///
/// Used by the `following` profile to quickly build the candidate set
/// from a user's followed creators, and by `unblocked` to exclude all
/// items from a blocked creator.
///
/// Thread-safe via `DashMap` -- concurrent reads and writes are lock-free
/// on disjoint keys.
pub struct CreatorItemsBitmap {
    inner: DashMap<u64, RoaringBitmap>,
}

impl CreatorItemsBitmap {
    #[must_use]
    pub fn new() -> Self {
        Self {
            inner: DashMap::new(),
        }
    }

    /// Add an item to a creator's bitmap.
    pub fn add_item(&self, creator_id: u64, item_id: u32) {
        self.inner.entry(creator_id).or_default().insert(item_id);
    }

    /// Get the bitmap for a creator (cloned).
    #[must_use]
    pub fn get(&self, creator_id: u64) -> Option<RoaringBitmap> {
        self.inner.get(&creator_id).map(|r| r.clone())
    }

    /// Get a reference to the bitmap for a creator without cloning.
    ///
    /// Returns a `Ref` guard that borrows the bitmap. Prefer this over `get()`
    /// on hot paths where the caller only needs to read the bitmap.
    #[must_use]
    pub fn get_ref(
        &self,
        creator_id: u64,
    ) -> Option<dashmap::mapref::one::Ref<'_, u64, RoaringBitmap>> {
        self.inner.get(&creator_id)
    }

    /// Build a union bitmap of all items from the given creator IDs.
    #[must_use]
    pub fn union_for(&self, creator_ids: &[u64]) -> RoaringBitmap {
        let mut result = RoaringBitmap::new();
        for &cid in creator_ids {
            if let Some(bm) = self.inner.get(&cid) {
                result |= bm.value();
            }
        }
        result
    }
}

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

// ── Entity serialization codec ──────────────────────────────────────────────

/// Serialize an entity with optional embedding and metadata.
///
/// This is the **user/creator entity** format, which includes an embedding header
/// before the metadata. For items-only metadata (no embedding), see
/// [`db::serialize_metadata`](crate::db::mod::serialize_metadata) in `db/mod.rs`.
///
/// Format:
/// ```text
/// [has_embedding: 1 byte (0 or 1)]
/// if has_embedding:
///   [emb_len: 4 bytes LE][emb bytes: emb_len * 4 f32 LE]
/// [meta_count: 4 bytes LE]
/// for each entry:
///   [key_len: 4 bytes LE][key bytes]
///   [val_len: 4 bytes LE][val bytes]
/// ```
pub(crate) fn serialize_entity(
    embedding: Option<&[f32]>,
    metadata: &HashMap<String, String>,
) -> Vec<u8> {
    let mut buf = Vec::new();
    match embedding {
        Some(emb) => {
            buf.push(1u8);
            #[allow(clippy::cast_possible_truncation)]
            let emb_len = emb.len() as u32;
            buf.extend_from_slice(&emb_len.to_le_bytes());
            for &v in emb {
                buf.extend_from_slice(&v.to_le_bytes());
            }
        }
        None => {
            buf.push(0u8);
        }
    }
    #[allow(clippy::cast_possible_truncation)]
    let meta_count = metadata.len() as u32;
    buf.extend_from_slice(&meta_count.to_le_bytes());
    for (k, v) in metadata {
        #[allow(clippy::cast_possible_truncation)]
        let key_len = k.len() as u32;
        buf.extend_from_slice(&key_len.to_le_bytes());
        buf.extend_from_slice(k.as_bytes());
        #[allow(clippy::cast_possible_truncation)]
        let val_len = v.len() as u32;
        buf.extend_from_slice(&val_len.to_le_bytes());
        buf.extend_from_slice(v.as_bytes());
    }
    buf
}

/// Deserialize an entity with optional embedding and metadata.
///
/// This is the **user/creator entity** format. For items-only metadata,
/// see `db::deserialize_metadata` in `db/mod.rs`.
///
/// Returns `(Option<Vec<f32>>, HashMap<String, String>)`.
/// Tolerant of malformed data -- returns empty results rather than panicking.
pub(crate) fn deserialize_entity(bytes: &[u8]) -> (Option<Vec<f32>>, HashMap<String, String>) {
    let mut pos = 0;
    if bytes.is_empty() {
        return (None, HashMap::new());
    }

    // has_embedding
    let has_emb = bytes[pos];
    pos += 1;

    let embedding = if has_emb == 1 {
        if pos + 4 > bytes.len() {
            return (None, HashMap::new());
        }
        let emb_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;
        let byte_len = emb_len * 4;
        if pos + byte_len > bytes.len() {
            return (None, HashMap::new());
        }
        let mut emb = Vec::with_capacity(emb_len);
        for i in 0..emb_len {
            let off = pos + i * 4;
            emb.push(f32::from_le_bytes([
                bytes[off],
                bytes[off + 1],
                bytes[off + 2],
                bytes[off + 3],
            ]));
        }
        pos += byte_len;
        Some(emb)
    } else {
        None
    };

    // metadata
    let mut metadata = HashMap::new();
    if pos + 4 > bytes.len() {
        return (embedding, metadata);
    }
    let meta_count =
        u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
    pos += 4;
    for _ in 0..meta_count {
        if pos + 4 > bytes.len() {
            break;
        }
        let key_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;
        if pos + key_len > bytes.len() {
            break;
        }
        let key = String::from_utf8_lossy(&bytes[pos..pos + key_len]).to_string();
        pos += key_len;
        if pos + 4 > bytes.len() {
            break;
        }
        let val_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;
        if pos + val_len > bytes.len() {
            break;
        }
        let val = String::from_utf8_lossy(&bytes[pos..pos + val_len]).to_string();
        pos += val_len;
        metadata.insert(key, val);
    }

    (embedding, metadata)
}

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

    #[test]
    fn creator_items_bitmap_add_and_get() {
        let bm = CreatorItemsBitmap::new();
        bm.add_item(1, 10);
        bm.add_item(1, 20);
        bm.add_item(2, 30);

        let c1 = bm.get(1).unwrap();
        assert!(c1.contains(10));
        assert!(c1.contains(20));
        assert!(!c1.contains(30));

        let c2 = bm.get(2).unwrap();
        assert!(c2.contains(30));
    }

    #[test]
    fn creator_items_bitmap_union() {
        let bm = CreatorItemsBitmap::new();
        bm.add_item(1, 10);
        bm.add_item(2, 20);
        bm.add_item(3, 30);

        let union = bm.union_for(&[1, 2]);
        assert!(union.contains(10));
        assert!(union.contains(20));
        assert!(!union.contains(30));
    }

    #[test]
    fn serialize_deserialize_entity_no_embedding() {
        let mut meta = HashMap::new();
        meta.insert("name".to_string(), "alice".to_string());
        meta.insert("age".to_string(), "30".to_string());

        let bytes = serialize_entity(None, &meta);
        let (emb, deserialized_meta) = deserialize_entity(&bytes);
        assert!(emb.is_none());
        assert_eq!(deserialized_meta.get("name").unwrap(), "alice");
        assert_eq!(deserialized_meta.get("age").unwrap(), "30");
    }

    #[test]
    fn serialize_deserialize_entity_with_embedding() {
        let emb = vec![1.0f32, 2.0, 3.0];
        let meta = HashMap::new();

        let bytes = serialize_entity(Some(&emb), &meta);
        let (deserialized_emb, deserialized_meta) = deserialize_entity(&bytes);
        assert_eq!(deserialized_emb.unwrap(), emb);
        assert!(deserialized_meta.is_empty());
    }

    #[test]
    fn deserialize_empty_bytes() {
        let (emb, meta) = deserialize_entity(&[]);
        assert!(emb.is_none());
        assert!(meta.is_empty());
    }

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            /// serialize_entity / deserialize_entity roundtrip: metadata survives.
            #[test]
            fn entity_serialization_roundtrip(
                keys in proptest::collection::vec("[a-z]{1,20}", 0..10),
                vals in proptest::collection::vec("[a-z0-9]{0,50}", 0..10),
                has_emb in any::<bool>(),
                emb_vals in proptest::collection::vec(-1.0f32..1.0f32, 0..64),
            ) {
                let count = keys.len().min(vals.len());
                let mut meta = HashMap::new();
                for (k, v) in keys.into_iter().zip(vals.into_iter()).take(count) {
                    meta.insert(k, v);
                }
                let emb = if has_emb && !emb_vals.is_empty() {
                    Some(emb_vals)
                } else {
                    None
                };

                let bytes = serialize_entity(emb.as_deref(), &meta);
                let (dec_emb, dec_meta) = deserialize_entity(&bytes);

                // Metadata roundtrip.
                prop_assert_eq!(&dec_meta, &meta);

                // Embedding roundtrip.
                match (&emb, &dec_emb) {
                    (Some(orig), Some(dec)) => {
                        prop_assert_eq!(orig.len(), dec.len());
                        for (a, b) in orig.iter().zip(dec.iter()) {
                            prop_assert!((a - b).abs() < f32::EPSILON);
                        }
                    }
                    (None, None) => {}
                    _ => prop_assert!(false, "embedding presence mismatch"),
                }
            }
        }
    }
}
