//! Fast, unique ID generation, with a focus on small string representations.
//!
//! This crate provides a nanoid-style ID generator, but with IDs of length 16
//! instead of 21. We use the same 64-character alphabet as nanoid,
//! which means that our IDs have 96 bits of entropy.
//!
//! The `serde` feature enables serialization and deserialization of IDs as strings.

use std::fmt;
use std::ops::Deref;
use std::str::FromStr;

use rand::RngCore as _;

const ALPHABET: [u8; 64] = [
    b'_', b'-', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd',
    b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't',
    b'u', b'v', b'w', b'x', b'y', b'z', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J',
    b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z',
];
const MASK: u8 = 63;

/// A unique identifier generated by this crate.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Id([u8; 16]);

impl Deref for Id {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        // SAFETY: every `Id` constructor (`secure`, `insecure`, `FromStr`, and
        // transitively `serde::Deserialize`) guarantees the bytes are drawn from
        // `ALPHABET`, which is entirely ASCII. The bytes are therefore valid UTF-8.
        #[allow(unsafe_code)]
        unsafe {
            std::str::from_utf8_unchecked(&self.0)
        }
    }
}

impl fmt::Debug for Id {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("Id").field(&&**self).finish()
    }
}

impl FromStr for Id {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.as_bytes()
            .as_array()
            .copied()
            .filter(|bytes| bytes.iter().all(|byte| ALPHABET.contains(byte)))
            .map(Self)
            .ok_or_else(|| format!("invalid ID: {s:?} (must be 16 ID characters in the alphabet)"))
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for Id {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Serialize as a string.
        self.deref().serialize(serializer)
    }
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Id {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        use std::borrow::Cow;
        let s = <Cow<'_, str>>::deserialize(deserializer)?;
        Self::from_str(&s).map_err(serde::de::Error::custom)
    }
}

impl Id {
    /// Generate an [`Id`] from a cryptographically secure RNG.
    ///
    /// The resulting ID is suitable for use in contexts where uniqueness is needed and
    /// the risk of an adversarial collision is non-negligible.
    pub fn secure() -> Self {
        let mut raw = [0u8; 16];
        let mut rng = rand::rng();
        rng.fill_bytes(raw.as_mut());
        Self(Self::reduce(&raw))
    }

    /// Generate an [`Id`] from a fast, non-cryptographically secure RNG.
    ///
    /// The resulting ID is suitable for use in contexts where uniqueness is needed
    /// but the risk of an adversarial collision is negligible (such as local cache keys).
    pub fn insecure() -> Self {
        let mut raw = [0u8; 16];
        fastrand::fill(&mut raw);
        Self(Self::reduce(&raw))
    }

    /// Reduce a 16-byte array of random bytes to a 16-byte array of ASCII characters in our ID alphabet.
    fn reduce(raw: &[u8; 16]) -> [u8; 16] {
        raw.map(|byte| ALPHABET[(byte & MASK) as usize])
    }
}

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

    #[test]
    fn test_reduce_kat() {
        for (raw, expected) in [
            ([0u8; 16], "________________"),
            ([255u8; 16], "ZZZZZZZZZZZZZZZZ"),
            (
                [
                    0x00, 0x41, 0x80, 0xc1, 0x00, 0x41, 0x80, 0xc1, 0x00, 0x41, 0x80, 0xc1, 0x00,
                    0x41, 0x80, 0xc1,
                ],
                "_-_-_-_-_-_-_-_-",
            ),
            (
                [
                    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
                    0x0d, 0x0e, 0x0f,
                ],
                "_-0123456789abcd",
            ),
        ] {
            let bytes = Id::reduce(&raw);
            let actual = std::str::from_utf8(&bytes).expect("reduce produces ASCII");
            assert_eq!(actual, expected, "reduce({raw:?}) should be {expected}");
        }
    }

    #[test]
    fn test_secure() {
        let id = Id::secure();
        assert_eq!(id.len(), 16);
        assert!(id.bytes().all(|byte| ALPHABET.contains(&byte)));
    }

    #[test]
    fn test_insecure() {
        let id = Id::insecure();
        assert_eq!(id.len(), 16);
        assert!(id.bytes().all(|byte| ALPHABET.contains(&byte)));
    }

    #[test]
    fn test_from_str() {
        for tc in ["", "short", "0123456789abcdefg", "invalid!"] {
            assert!(Id::from_str(tc).is_err(),);
        }

        for tc in ["________________", "ZZZZZZZZZZZZZZZZ", "_-0123456789abcd"] {
            let id = Id::from_str(tc).expect("should parse valid ID");
            assert_eq!(&*id, tc, "parsed ID should match input");
        }
    }

    #[cfg(feature = "serde")]
    mod serde {
        use super::*;

        #[test]
        fn round_trip() {
            let id = Id::secure();
            let json = serde_json::to_string(&id).unwrap();
            let parsed: Id = serde_json::from_str(&json).unwrap();
            assert_eq!(id, parsed);

            let id = Id::insecure();
            let json = serde_json::to_string(&id).unwrap();
            let parsed: Id = serde_json::from_str(&json).unwrap();
            assert_eq!(id, parsed);
        }

        #[test]
        fn rejects_wrong_length() {
            for tc in ["\"\"", "\"short\"", "\"0123456789abcdefg\""] {
                assert!(
                    serde_json::from_str::<Id>(tc).is_err(),
                    "should reject {tc}"
                );
            }
        }

        #[test]
        fn rejects_wrong_alphabet() {
            for tc in ["\"0123456789abcde!\"", "\"0123456789abcdeé\""] {
                assert!(
                    serde_json::from_str::<Id>(tc).is_err(),
                    "should reject {tc}"
                );
            }
        }
    }
}
