//! Content address of an ontology (blake3 over its sorted head OpIds).

use std::fmt;

use ontoref_types::OpId;
use serde::{Deserialize, Serialize};

/// 32-byte blake3 content address of an ontology.
///
/// Computed deterministically from the ontology's current set of DAG
/// heads — the multi-head case is hashed in sorted byte-lex order so
/// identical-state ontologies on different actors produce the same id.
/// An empty head set yields the canonical empty-ontology address.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct OntologyId(pub [u8; 32]);

impl OntologyId {
    /// Lower-case hex representation (64 chars).
    pub fn to_hex(&self) -> String {
        let mut s = String::with_capacity(64);
        for byte in self.0 {
            use std::fmt::Write as _;
            let _ = write!(s, "{byte:02x}");
        }
        s
    }

    /// Parse a 64-char lower-case hex string into an [`OntologyId`].
    pub fn from_hex(hex: &str) -> Option<Self> {
        if hex.len() != 64 {
            return None;
        }
        let mut bytes = [0u8; 32];
        for i in 0..32 {
            bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
        }
        Some(Self(bytes))
    }
}

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

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

/// Compute the content address of an ontology from its current DAG heads.
///
/// Heads are sorted byte-lex internally so the caller's ordering does
/// not affect the result.
pub fn compute_ontology_id(heads: &[OpId]) -> OntologyId {
    let mut sorted: Vec<&OpId> = heads.iter().collect();
    sorted.sort_by_key(|h| h.0);

    let mut hasher = blake3::Hasher::new();
    // Domain separator so the empty-heads case produces a known address
    // and so head bytes cannot be confused with arbitrary blob bytes.
    hasher.update(b"ontology-id-v1");
    for h in sorted {
        hasher.update(&h.0);
    }
    OntologyId(*hasher.finalize().as_bytes())
}

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

    #[test]
    fn hex_roundtrip() {
        let id = compute_ontology_id(&[]);
        let hex = id.to_hex();
        assert_eq!(hex.len(), 64);
        let parsed = OntologyId::from_hex(&hex).expect("hex parse");
        assert_eq!(parsed, id);
    }

    #[test]
    fn empty_heads_yields_stable_id() {
        let a = compute_ontology_id(&[]);
        let b = compute_ontology_id(&[]);
        assert_eq!(a, b);
    }

    #[test]
    fn head_order_does_not_affect_id() {
        let h1 = OpId([1u8; 32]);
        let h2 = OpId([2u8; 32]);
        let a = compute_ontology_id(&[h1, h2]);
        let b = compute_ontology_id(&[h2, h1]);
        assert_eq!(a, b, "head order must not influence the content address");
    }

    #[test]
    fn different_heads_yield_different_ids() {
        let a = compute_ontology_id(&[OpId([1u8; 32])]);
        let b = compute_ontology_id(&[OpId([2u8; 32])]);
        assert_ne!(a, b);
    }
}
