use std::path::PathBuf;

use serde::de::{Error as _, Unexpected};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

#[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 fn of_pair(first: &[u8], second: &[u8]) -> Self {
        let Ok(first_len) = u64::try_from(first.len()) else {
            unreachable!("slice length exceeds u64 only above 64-bit platforms");
        };
        let mut hasher = blake3::Hasher::new();
        hasher.update(&first_len.to_le_bytes());
        hasher.update(first);
        hasher.update(second);
        Self(hasher.finalize())
    }

    #[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())
    }
}

impl Serialize for BlobHash {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.full_hex())
    }
}

impl<'de> Deserialize<'de> for BlobHash {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let hex = String::deserialize(deserializer)?;
        blake3::Hash::from_hex(&hex).map(Self).map_err(|_| {
            D::Error::invalid_value(Unexpected::Str(&hex), &"a 64-char BLAKE3 hex digest")
        })
    }
}

#[derive(Debug, thiserror::Error)]
#[error("blob kind extension must be non-empty lowercase ascii alphanumeric")]
pub struct BlobKindError;

#[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");
    pub const LABELS: Self = Self("labels");
    pub const BREP: Self = Self("brep");

    pub fn new(ext: &'static str) -> Result<Self, BlobKindError> {
        let valid = !ext.is_empty()
            && ext
                .bytes()
                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit());
        valid.then_some(Self(ext)).ok_or(BlobKindError)
    }

    #[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 of_pair_frames_the_boundary_between_parts() {
        assert_ne!(
            BlobHash::of_pair(b"AB", b"C"),
            BlobHash::of_pair(b"A", b"BC"),
            "a different split of the same concatenation must not collide"
        );
        assert_ne!(
            BlobHash::of_pair(b"AB", b"C"),
            BlobHash::of(b"ABC"),
            "framed pair must not collide with the bare concatenation"
        );
        assert_ne!(
            BlobHash::of_pair(b"", b"X"),
            BlobHash::of_pair(b"X", b""),
            "an empty leading part is distinguishable from an empty trailing part"
        );
    }

    #[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");
        assert_eq!(BlobKind::LABELS.as_str(), "labels");
    }

    #[test]
    fn serde_round_trips_through_hex() {
        let hash = BlobHash::of(b"nel");
        let Ok(text) = crate::io::ron_io::to_string(&hash) else {
            panic!("serialize blob hash");
        };
        assert!(text.contains(&hash.full_hex()));
        let Ok(back) = crate::io::ron_io::from_str::<BlobHash>(&text) else {
            panic!("deserialize blob hash");
        };
        assert_eq!(hash, back);
    }

    #[test]
    fn deserialize_rejects_non_hex() {
        assert!(crate::io::ron_io::from_str::<BlobHash>("\"not a digest\"").is_err());
    }

    #[test]
    fn blob_kind_new_accepts_lowercase_alphanumeric() {
        let Ok(gltf) = BlobKind::new("gltf") else {
            panic!("gltf is a valid extension");
        };
        let Ok(v1) = BlobKind::new("v1") else {
            panic!("v1 is a valid extension");
        };
        assert_eq!(gltf.as_str(), "gltf");
        assert_eq!(v1.as_str(), "v1");
    }

    #[test]
    fn blob_kind_new_rejects_empty() {
        assert!(BlobKind::new("").is_err());
    }

    #[test]
    fn blob_kind_new_rejects_uppercase() {
        assert!(BlobKind::new("STEP").is_err());
    }

    #[test]
    fn blob_kind_new_rejects_path_separator() {
        assert!(BlobKind::new("bad/ext").is_err());
    }

    #[test]
    fn blob_kind_new_rejects_dot() {
        assert!(BlobKind::new("step.bak").is_err());
    }
}
