use std::path::PathBuf;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BlobHash(blake3::Hash);

impl BlobHash {
    #[must_use]
    pub fn of(bytes: &[u8]) -> Self {
        Self(blake3::hash(bytes))
    }

    #[must_use]
    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
        Self(blake3::Hash::from_bytes(bytes))
    }

    #[must_use]
    pub fn bytes(self) -> [u8; 32] {
        *self.0.as_bytes()
    }

    #[must_use]
    pub fn full_hex(self) -> String {
        self.0.to_hex().to_string()
    }

    #[must_use]
    pub fn truncated_128_hex(self) -> String {
        self.0.to_hex()[..32].to_string()
    }

    #[must_use]
    pub fn relative_path(self, kind: BlobKind) -> PathBuf {
        let hex = self.truncated_128_hex();
        let (aa, rest) = hex.split_at(2);
        PathBuf::from(aa).join(format!("{rest}.{ext}", ext = kind.as_str()))
    }
}

impl core::fmt::Display for BlobHash {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", self.0.to_hex())
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BlobKind(&'static str);

impl BlobKind {
    pub const STEP: Self = Self("step");
    pub const TESS: Self = Self("tess");
    pub const THUMB: Self = Self("thumb");

    #[must_use]
    pub fn new(ext: &'static str) -> Self {
        assert!(
            !ext.is_empty()
                && ext
                    .bytes()
                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()),
            "BlobKind extension must be non-empty lowercase ASCII alphanumeric",
        );
        Self(ext)
    }

    #[must_use]
    pub const fn as_str(self) -> &'static str {
        self.0
    }
}

impl core::fmt::Display for BlobKind {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.0)
    }
}

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

    #[test]
    fn of_matches_blake3_reference() {
        let hash = BlobHash::of(b"");
        assert_eq!(
            hash.full_hex(),
            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
        );
    }

    #[test]
    fn truncated_is_first_32_hex_chars() {
        let hash = BlobHash::of(b"example");
        let full = hash.full_hex();
        let short = hash.truncated_128_hex();
        assert_eq!(short.len(), 32);
        assert_eq!(short, full[..32]);
    }

    #[test]
    fn relative_path_is_fanout_then_rest_dot_kind() {
        let hash = BlobHash::of(b"example");
        let path = hash.relative_path(BlobKind::STEP);
        let parts: Vec<String> = path
            .components()
            .map(|c| c.as_os_str().to_string_lossy().into_owned())
            .collect();
        let Some([fanout, file]) = parts
            .get(..2)
            .and_then(|s| <&[String; 2]>::try_from(s).ok())
        else {
            panic!("two components, got {parts:?}");
        };
        assert_eq!(fanout.len(), 2);
        let Some(stem) = file.strip_suffix(".step") else {
            panic!("ends with .step: {file}");
        };
        assert_eq!(stem.len(), 30);
        let hex = hash.truncated_128_hex();
        assert_eq!(fanout, &hex[..2]);
        assert_eq!(stem, &hex[2..]);
    }

    #[test]
    fn from_bytes_roundtrips_through_bytes() {
        let bytes = [7u8; 32];
        let hash = BlobHash::from_bytes(bytes);
        assert_eq!(hash.bytes(), bytes);
    }

    #[test]
    fn blob_kind_presets_match_adr_examples() {
        assert_eq!(BlobKind::STEP.as_str(), "step");
        assert_eq!(BlobKind::TESS.as_str(), "tess");
        assert_eq!(BlobKind::THUMB.as_str(), "thumb");
    }

    #[test]
    fn blob_kind_new_accepts_lowercase_alphanumeric() {
        assert_eq!(BlobKind::new("gltf").as_str(), "gltf");
        assert_eq!(BlobKind::new("v1").as_str(), "v1");
    }

    #[test]
    #[should_panic = "BlobKind extension"]
    fn blob_kind_new_rejects_empty() {
        let _ = BlobKind::new("");
    }

    #[test]
    #[should_panic = "BlobKind extension"]
    fn blob_kind_new_rejects_uppercase() {
        let _ = BlobKind::new("STEP");
    }

    #[test]
    #[should_panic = "BlobKind extension"]
    fn blob_kind_new_rejects_path_separator() {
        let _ = BlobKind::new("bad/ext");
    }

    #[test]
    #[should_panic = "BlobKind extension"]
    fn blob_kind_new_rejects_dot() {
        let _ = BlobKind::new("step.bak");
    }
}
