//! Random and content-addressed identifiers for items, fields, and attachments.
//!
//! - `ItemId` and `FieldId` are random 16-char hex strings (64 bits of entropy)
//!   generated via `OsRng` (audit M8: bumped from the v1 8-char/32-bit format).
//! - `AttachmentId` is the first 32 hex chars of `sha256(plaintext)` (128 bits) —
//!   content-addressed so identical plaintext blobs deduplicate naturally in git.
//!   (audit I2/B4: bumped from 8-byte/64-bit format to prevent birthday collisions)

use rand::rngs::OsRng;
use rand::RngCore;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ItemId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FieldId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AttachmentId(pub String);

impl ItemId {
    pub fn new() -> Self {
        let mut bytes = [0u8; 8];
        OsRng.fill_bytes(&mut bytes);
        Self(hex::encode(bytes))
    }
    pub fn as_str(&self) -> &str { &self.0 }

    /// Returns true if this ID is valid for filesystem paths.
    /// Valid ItemIds are 16 lowercase hex chars.
    pub fn is_valid(&self) -> bool {
        self.0.len() == 16 && self.0.chars().all(|c| c.is_ascii_hexdigit())
    }
}

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

impl FieldId {
    pub fn new() -> Self {
        let mut bytes = [0u8; 8];
        OsRng.fill_bytes(&mut bytes);
        Self(hex::encode(bytes))
    }
    pub fn as_str(&self) -> &str { &self.0 }
}

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

impl AttachmentId {
    pub fn from_plaintext(plaintext: &[u8]) -> Self {
        let digest = Sha256::digest(plaintext);
        Self(hex::encode(&digest[..16])) // 16 bytes = 128 bits
    }
    pub fn as_str(&self) -> &str { &self.0 }

    /// Returns true if this ID is valid for filesystem paths.
    /// Valid AttachmentIds are 32 lowercase hex chars.
    pub fn is_valid(&self) -> bool {
        self.0.len() == 32 && self.0.chars().all(|c| c.is_ascii_hexdigit())
    }
}

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

    #[test]
    fn item_id_is_16_hex_chars() {
        let id = ItemId::new();
        assert_eq!(id.0.len(), 16);
        assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn item_ids_are_unique() {
        let mut seen = std::collections::HashSet::new();
        for _ in 0..10_000 {
            assert!(seen.insert(ItemId::new().0));
        }
    }

    #[test]
    fn field_id_is_16_hex_chars() {
        let id = FieldId::new();
        assert_eq!(id.0.len(), 16);
        assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn field_ids_are_unique() {
        let mut seen = std::collections::HashSet::new();
        for _ in 0..10_000 {
            assert!(seen.insert(FieldId::new().0));
        }
    }

    #[test]
    fn attachment_id_is_deterministic() {
        let plaintext = b"hello world";
        let a = AttachmentId::from_plaintext(plaintext);
        let b = AttachmentId::from_plaintext(plaintext);
        assert_eq!(a, b);
    }

    #[test]
    fn attachment_id_changes_with_plaintext() {
        let a = AttachmentId::from_plaintext(b"hello");
        let b = AttachmentId::from_plaintext(b"world");
        assert_ne!(a, b);
    }

    #[test]
    fn attachment_id_is_32_hex_chars() {
        let id = AttachmentId::from_plaintext(b"any bytes");
        assert_eq!(id.0.len(), 32); // 16 bytes = 32 hex chars = 128 bits
        assert!(id.0.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn item_id_is_valid_for_normal_ids() {
        let id = ItemId::new();
        assert!(id.is_valid());
    }

    #[test]
    fn item_id_is_invalid_for_traversal() {
        let bad = ItemId("../../../etc".to_string());
        assert!(!bad.is_valid());
    }

    #[test]
    fn attachment_id_is_valid_for_normal_ids() {
        let id = AttachmentId::from_plaintext(b"test");
        assert!(id.is_valid());
    }

    #[test]
    fn attachment_id_is_invalid_for_traversal() {
        let bad = AttachmentId("../../passwd".to_string());
        assert!(!bad.is_valid());
    }

    #[test]
    fn ids_serialize_as_bare_strings() {
        let item = ItemId("abcdef0123456789".to_string());
        let json = serde_json::to_string(&item).unwrap();
        assert_eq!(json, "\"abcdef0123456789\"");

        let parsed: ItemId = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, item);
    }
}
