//! Collection storage model: user-owned item collections with bitmap membership.
//!
//! Each collection is a named set of item IDs owned by a user. Membership is
//! tracked via `RoaringBitmap` for O(1) lookup during filter evaluation.
//! Collections are persisted to fjall with `Tag::Collection` keys and rebuilt
//! on database open.

use std::sync::atomic::{AtomicU64, Ordering};

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

/// Newtype wrapping u64 for collection identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CollectionId(pub u64);

impl CollectionId {
    /// Create a new `CollectionId`.
    #[must_use]
    pub const fn new(id: u64) -> Self {
        Self(id)
    }

    /// Get the raw u64 value.
    #[must_use]
    pub const fn as_u64(self) -> u64 {
        self.0
    }
}

/// Visibility controls who can see a collection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
    Private,
    Shared,
    Public,
}

impl Visibility {
    /// Serialize to a single byte.
    #[must_use]
    pub const fn as_byte(self) -> u8 {
        match self {
            Self::Private => 0,
            Self::Shared => 1,
            Self::Public => 2,
        }
    }

    /// Parse from a single byte.
    #[must_use]
    pub const fn from_byte(b: u8) -> Option<Self> {
        match b {
            0 => Some(Self::Private),
            1 => Some(Self::Shared),
            2 => Some(Self::Public),
            _ => None,
        }
    }
}

/// Full collection record stored in fjall.
#[derive(Debug, Clone)]
pub struct Collection {
    pub id: CollectionId,
    pub owner_id: u64,
    pub name: String,
    pub visibility: Visibility,
    pub created_at_ns: u64,
    /// Item IDs in this collection (for serialization to fjall).
    pub item_ids: Vec<u32>,
}

/// Summary record returned by `list_collections`.
#[derive(Debug, Clone)]
pub struct CollectionInfo {
    pub id: CollectionId,
    pub name: String,
    pub visibility: Visibility,
    pub item_count: usize,
    pub created_at_ns: u64,
}

/// In-memory index: `CollectionId` -> `RoaringBitmap` of item IDs.
///
/// O(1) membership check via `DashMap`. Rebuilt from fjall on open.
pub struct CollectionIndex {
    /// Collection bitmaps by ID.
    inner: DashMap<CollectionId, RoaringBitmap>,
    /// Collection metadata by ID.
    meta: DashMap<CollectionId, Collection>,
    /// Owner -> set of `CollectionId`s.
    by_owner: DashMap<u64, Vec<CollectionId>>,
    /// Monotonically increasing ID counter.
    next_id: AtomicU64,
}

impl CollectionIndex {
    /// Create a new empty collection index.
    #[must_use]
    pub fn new() -> Self {
        Self {
            inner: DashMap::new(),
            meta: DashMap::new(),
            by_owner: DashMap::new(),
            next_id: AtomicU64::new(1),
        }
    }

    /// Allocate a new `CollectionId`.
    pub fn next_collection_id(&self) -> CollectionId {
        CollectionId::new(self.next_id.fetch_add(1, Ordering::Relaxed))
    }

    /// Set the next ID counter (used during rebuild from storage).
    pub fn set_next_id(&self, id: u64) {
        self.next_id.store(id, Ordering::Relaxed);
    }

    /// Create a collection (in-memory only; caller persists to fjall).
    pub fn create(&self, collection: Collection) {
        let id = collection.id;
        let owner_id = collection.owner_id;

        // Build initial bitmap from item_ids.
        let mut bm = RoaringBitmap::new();
        for &item_id in &collection.item_ids {
            bm.insert(item_id);
        }
        self.inner.insert(id, bm);
        self.meta.insert(id, collection);
        self.by_owner.entry(owner_id).or_default().push(id);
    }

    /// Add item to a collection's bitmap.
    pub fn add_item(&self, id: CollectionId, item_id: u32) {
        self.inner.entry(id).or_default().insert(item_id);
        // Update the metadata's item_ids list.
        if let Some(mut meta) = self.meta.get_mut(&id)
            && !meta.item_ids.contains(&item_id)
        {
            meta.item_ids.push(item_id);
        }
    }

    /// Remove item from a collection's bitmap.
    pub fn remove_item(&self, id: CollectionId, item_id: u32) {
        if let Some(mut bm) = self.inner.get_mut(&id) {
            bm.remove(item_id);
        }
        if let Some(mut meta) = self.meta.get_mut(&id) {
            meta.item_ids.retain(|&x| x != item_id);
        }
    }

    /// Check if an item is in a collection.
    #[must_use]
    pub fn contains(&self, id: CollectionId, item_id: u32) -> bool {
        self.inner.get(&id).is_some_and(|bm| bm.contains(item_id))
    }

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

    /// List collections for an owner.
    #[must_use]
    pub fn list_by_owner(&self, owner_id: u64) -> Vec<CollectionInfo> {
        let cids = self
            .by_owner
            .get(&owner_id)
            .map(|r| r.clone())
            .unwrap_or_default();
        let mut result = Vec::with_capacity(cids.len());
        for cid in cids {
            if let Some(meta) = self.meta.get(&cid) {
                let item_count = self.inner.get(&cid).map_or(0, |bm| bm.len() as usize);
                result.push(CollectionInfo {
                    id: cid,
                    name: meta.name.clone(),
                    visibility: meta.visibility,
                    item_count,
                    created_at_ns: meta.created_at_ns,
                });
            }
        }
        result
    }

    /// Get collection metadata.
    #[must_use]
    pub fn get_meta(&self, id: CollectionId) -> Option<Collection> {
        self.meta.get(&id).map(|r| r.clone())
    }
}

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

// ── Serialization ────────────────────────────────────────────────────────────

/// Serialize a collection record for fjall storage.
///
/// Format: `[id: 8 LE][owner_id: 8 LE][visibility: 1][created_at_ns: 8 LE]`
///         `[name_len: 4 LE][name bytes][item_count: 4 LE][item_id: 4 LE each]`
#[must_use]
pub fn serialize_collection(c: &Collection) -> Vec<u8> {
    #[allow(clippy::cast_possible_truncation)]
    let name_len = c.name.len() as u32;
    #[allow(clippy::cast_possible_truncation)]
    let item_count = c.item_ids.len() as u32;

    let capacity = 8 + 8 + 1 + 8 + 4 + c.name.len() + 4 + c.item_ids.len() * 4;
    let mut buf = Vec::with_capacity(capacity);

    buf.extend_from_slice(&c.id.as_u64().to_le_bytes());
    buf.extend_from_slice(&c.owner_id.to_le_bytes());
    buf.push(c.visibility.as_byte());
    buf.extend_from_slice(&c.created_at_ns.to_le_bytes());
    buf.extend_from_slice(&name_len.to_le_bytes());
    buf.extend_from_slice(c.name.as_bytes());
    buf.extend_from_slice(&item_count.to_le_bytes());
    for &item_id in &c.item_ids {
        buf.extend_from_slice(&item_id.to_le_bytes());
    }

    buf
}

/// Deserialize a collection record from fjall storage.
///
/// Returns `None` if the bytes are malformed.
#[must_use]
pub fn deserialize_collection(bytes: &[u8]) -> Option<Collection> {
    // Minimum: 8 + 8 + 1 + 8 + 4 + 0 + 4 = 33 bytes
    if bytes.len() < 33 {
        return None;
    }

    let mut pos = 0;

    let id = u64::from_le_bytes([
        bytes[pos],
        bytes[pos + 1],
        bytes[pos + 2],
        bytes[pos + 3],
        bytes[pos + 4],
        bytes[pos + 5],
        bytes[pos + 6],
        bytes[pos + 7],
    ]);
    pos += 8;

    let owner_id = u64::from_le_bytes([
        bytes[pos],
        bytes[pos + 1],
        bytes[pos + 2],
        bytes[pos + 3],
        bytes[pos + 4],
        bytes[pos + 5],
        bytes[pos + 6],
        bytes[pos + 7],
    ]);
    pos += 8;

    let visibility = Visibility::from_byte(bytes[pos])?;
    pos += 1;

    let created_at_ns = u64::from_le_bytes([
        bytes[pos],
        bytes[pos + 1],
        bytes[pos + 2],
        bytes[pos + 3],
        bytes[pos + 4],
        bytes[pos + 5],
        bytes[pos + 6],
        bytes[pos + 7],
    ]);
    pos += 8;

    if pos + 4 > bytes.len() {
        return None;
    }
    let name_len =
        u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
    pos += 4;

    if pos + name_len > bytes.len() {
        return None;
    }
    let name = String::from_utf8_lossy(&bytes[pos..pos + name_len]).to_string();
    pos += name_len;

    if pos + 4 > bytes.len() {
        return None;
    }
    let item_count =
        u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) as usize;
    pos += 4;

    let mut item_ids = Vec::with_capacity(item_count);
    for _ in 0..item_count {
        if pos + 4 > bytes.len() {
            return None;
        }
        let item_id =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
        pos += 4;
        item_ids.push(item_id);
    }

    Some(Collection {
        id: CollectionId::new(id),
        owner_id,
        name,
        visibility,
        created_at_ns,
        item_ids,
    })
}

// ── Unit tests ────────────────────────────────────────────────────────────────

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

    #[test]
    fn collection_create_and_list() {
        let idx = CollectionIndex::new();
        let id = idx.next_collection_id();
        let c = Collection {
            id,
            owner_id: 42,
            name: "favorites".into(),
            visibility: Visibility::Private,
            created_at_ns: 1_000_000,
            item_ids: vec![],
        };
        idx.create(c);

        let list = idx.list_by_owner(42);
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].name, "favorites");
        assert_eq!(list[0].item_count, 0);
    }

    #[test]
    fn collection_add_remove_contains() {
        let idx = CollectionIndex::new();
        let id = idx.next_collection_id();
        idx.create(Collection {
            id,
            owner_id: 1,
            name: "test".into(),
            visibility: Visibility::Public,
            created_at_ns: 0,
            item_ids: vec![],
        });

        assert!(!idx.contains(id, 10));
        idx.add_item(id, 10);
        assert!(idx.contains(id, 10));

        idx.remove_item(id, 10);
        assert!(!idx.contains(id, 10));
    }

    #[test]
    fn collection_bitmap() {
        let idx = CollectionIndex::new();
        let id = idx.next_collection_id();
        idx.create(Collection {
            id,
            owner_id: 1,
            name: "test".into(),
            visibility: Visibility::Shared,
            created_at_ns: 0,
            item_ids: vec![5, 10, 15],
        });

        let bm = idx.bitmap(id).unwrap();
        assert!(bm.contains(5));
        assert!(bm.contains(10));
        assert!(bm.contains(15));
        assert!(!bm.contains(20));
    }

    #[test]
    fn serialize_deserialize_roundtrip() {
        let c = Collection {
            id: CollectionId::new(42),
            owner_id: 7,
            name: "watchlist".into(),
            visibility: Visibility::Shared,
            created_at_ns: 999_999,
            item_ids: vec![1, 2, 3, 100],
        };
        let bytes = serialize_collection(&c);
        let d = deserialize_collection(&bytes).unwrap();
        assert_eq!(d.id, c.id);
        assert_eq!(d.owner_id, c.owner_id);
        assert_eq!(d.name, c.name);
        assert_eq!(d.visibility, c.visibility);
        assert_eq!(d.created_at_ns, c.created_at_ns);
        assert_eq!(d.item_ids, c.item_ids);
    }

    #[test]
    fn deserialize_too_short() {
        assert!(deserialize_collection(&[0u8; 10]).is_none());
    }

    #[test]
    fn visibility_roundtrip() {
        for v in [Visibility::Private, Visibility::Shared, Visibility::Public] {
            assert_eq!(Visibility::from_byte(v.as_byte()), Some(v));
        }
        assert_eq!(Visibility::from_byte(0xFF), None);
    }

    #[test]
    fn add_item_idempotent() {
        let idx = CollectionIndex::new();
        let id = idx.next_collection_id();
        idx.create(Collection {
            id,
            owner_id: 1,
            name: "test".into(),
            visibility: Visibility::Private,
            created_at_ns: 0,
            item_ids: vec![],
        });

        idx.add_item(id, 10);
        idx.add_item(id, 10); // idempotent
        let bm = idx.bitmap(id).unwrap();
        assert_eq!(bm.len(), 1);
    }

    #[test]
    fn remove_from_nonexistent_collection_is_noop() {
        let idx = CollectionIndex::new();
        let fake_id = CollectionId::new(999);
        idx.remove_item(fake_id, 10); // should not panic
    }
}
