//! Cluster-0.23 observability primitives — PII-safe principal
//! hashing + helpers for span attribute construction.
//!
//! Span fields use [`principal_hash`] for the
//! `klieo.principal_hash` attribute so no PII leaks into OTEL
//! backends. Cluster 0.22's `OwnershipRegistry` continues to
//! store the raw `sub` claim in KV so operators can map
//! hash → sub when investigating a specific trace.
//!
//! See ADR-023.

use sha2::{Digest, Sha256};

const HASH_HEX_LEN: usize = 16;
const HASH_BYTE_LEN: usize = HASH_HEX_LEN / 2;
const NIBBLE: &[u8; 16] = b"0123456789abcdef";

/// PII-safe principal identifier for span attributes.
///
/// Returns the first 16 hex characters (8 bytes / 64 bits) of
/// `SHA-256(sub)`. Stable for trace correlation; reversal
/// requires brute-force OR the cluster-0.22
/// [`crate::OwnershipRegistry`] lookup that maps hash → raw
/// sub.
///
/// Output is exactly 16 lowercase hex characters, regardless
/// of input length (empty string OK).
pub fn principal_hash(sub: &str) -> String {
    let digest = Sha256::digest(sub.as_bytes());
    let mut out = String::with_capacity(HASH_HEX_LEN);
    for byte in digest.iter().take(HASH_BYTE_LEN) {
        out.push(NIBBLE[(byte >> 4) as usize] as char);
        out.push(NIBBLE[(byte & 0x0f) as usize] as char);
    }
    out
}

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

    #[test]
    fn principal_hash_is_deterministic_and_16_hex_chars() {
        let a = principal_hash("alice@example.com");
        let b = principal_hash("alice@example.com");
        assert_eq!(a, b);
        assert_eq!(a.len(), HASH_HEX_LEN);
        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn principal_hash_differs_per_sub() {
        assert_ne!(principal_hash("alice"), principal_hash("bob"));
    }

    #[test]
    fn principal_hash_handles_empty_string() {
        let h = principal_hash("");
        assert_eq!(h.len(), HASH_HEX_LEN);
        assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
    }
}
