//! Cryptographic signing utilities for assertions.
//!
//! This module provides canonical content hashing and signature verification
//! for StemeDB assertions. All signing code should use these utilities to
//! ensure consistency across the codebase.
//!
//! # Signature Versions
//!
//! - **Version 1 (legacy):** Signs `"{subject}:{predicate}"` only. Used in simulation.
//! - **Version 2 (enterprise):** Signs BLAKE3 hash of full assertion content.
//!   Protects all fields from tampering.
//!
//! # Example
//!
//! ```ignore
//! use stemedb_core::signing::{compute_content_hash_v2, sign_assertion_v2};
//! use ed25519_dalek::SigningKey;
//!
//! let content_hash = compute_content_hash_v2(&assertion);
//! let signature = sign_assertion_v2(&assertion, &signing_key);
//! ```

use crate::types::{Assertion, ObjectValue};

/// Compute the canonical content hash for v2 (enterprise) signing.
///
/// This hash includes all content-bearing fields of the assertion:
/// - subject, predicate, object
/// - source_hash, source_class
/// - confidence, timestamp
///
/// Fields NOT included (and why):
/// - `signatures`: Would create circular dependency
/// - `hlc_timestamp`: May be set after content is finalized
/// - `vector`: Embedding, not part of the claim itself
/// - `parent_hash`, `epoch`, `visual_hash`: Lineage, not content
/// - `source_metadata`: Variable-length, domain-specific
/// - `lifecycle`: Can change over time
///
/// # Format
///
/// The hash is computed over:
/// ```text
/// subject:predicate:{type_tag}:{object_bytes}:source_hash:source_class:confidence:timestamp
/// ```
pub fn compute_content_hash_v2(assertion: &Assertion) -> [u8; 32] {
    let mut hasher = blake3::Hasher::new();

    // Subject and predicate
    hasher.update(assertion.subject.as_bytes());
    hasher.update(b":");
    hasher.update(assertion.predicate.as_bytes());
    hasher.update(b":");

    // Object value with type tag for disambiguation
    match &assertion.object {
        ObjectValue::Text(t) => {
            hasher.update(b"T:");
            hasher.update(t.as_bytes());
        }
        ObjectValue::Number(n) => {
            hasher.update(b"N:");
            // Round to 10 decimal places for reproducibility across JSON round-trips.
            // JSON serialization/deserialization can change the exact f64 bit pattern
            // for numbers that aren't exactly representable in decimal.
            // 10 decimal places is more than enough for real-world values while
            // surviving the decimal→binary→decimal conversion in JSON parsing.
            let s = format!("{:.10}", n);
            hasher.update(s.as_bytes());
        }
        ObjectValue::Boolean(b) => {
            hasher.update(b"B:");
            hasher.update(&[if *b { 1 } else { 0 }]);
        }
        ObjectValue::Reference(r) => {
            hasher.update(b"R:");
            hasher.update(r.as_bytes());
        }
    }

    // Source provenance
    hasher.update(b":");
    hasher.update(&assertion.source_hash);
    hasher.update(b":");
    hasher.update(&(assertion.source_class as u8).to_le_bytes());

    // Confidence and timestamp
    // Use string format for confidence (f32) to survive JSON round-trips.
    // f32 has ~7 significant decimal digits, so 6 decimal places is sufficient.
    hasher.update(b":");
    let confidence_str = format!("{:.6}", assertion.confidence);
    hasher.update(confidence_str.as_bytes());
    hasher.update(b":");
    hasher.update(&assertion.timestamp.to_le_bytes());

    *hasher.finalize().as_bytes()
}

/// Compute the canonical message for v1 (legacy) signing.
///
/// This is a simple concatenation of subject and predicate.
/// Used for backward compatibility with simulation code.
///
/// **Note:** v1 signing does NOT protect other fields from tampering.
/// Use v2 for production assertions.
pub fn compute_content_hash_v1(subject: &str, predicate: &str) -> Vec<u8> {
    format!("{}:{}", subject, predicate).into_bytes()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{HlcTimestamp, LifecycleStage, SourceClass};

    fn test_assertion() -> Assertion {
        Assertion {
            subject: "Semaglutide".to_string(),
            predicate: "has_boxed_warning".to_string(),
            object: ObjectValue::Boolean(true),
            parent_hash: None,
            source_hash: [1u8; 32],
            source_class: SourceClass::Regulatory,
            visual_hash: None,
            epoch: None,
            source_metadata: None,
            lifecycle: LifecycleStage::Approved,
            signatures: vec![],
            confidence: 0.95,
            timestamp: 1704067200,
            hlc_timestamp: HlcTimestamp::default(),
            vector: None,
        }
    }

    #[test]
    fn test_content_hash_deterministic() {
        let assertion = test_assertion();

        let hash1 = compute_content_hash_v2(&assertion);
        let hash2 = compute_content_hash_v2(&assertion);

        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_changes_with_subject() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.subject = "Tirzepatide".to_string();
        let hash2 = compute_content_hash_v2(&assertion);

        assert_ne!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_changes_with_predicate() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.predicate = "has_indication".to_string();
        let hash2 = compute_content_hash_v2(&assertion);

        assert_ne!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_changes_with_object() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.object = ObjectValue::Boolean(false);
        let hash2 = compute_content_hash_v2(&assertion);

        assert_ne!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_changes_with_confidence() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.confidence = 0.5;
        let hash2 = compute_content_hash_v2(&assertion);

        assert_ne!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_ignores_lifecycle() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.lifecycle = LifecycleStage::Deprecated;
        let hash2 = compute_content_hash_v2(&assertion);

        // Lifecycle is NOT part of content hash
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_ignores_hlc_timestamp() {
        let mut assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        assertion.hlc_timestamp = HlcTimestamp::new(999999, [1u8; 16]);
        let hash2 = compute_content_hash_v2(&assertion);

        // HLC timestamp is NOT part of content hash
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_v1_hash_format() {
        let hash = compute_content_hash_v1("Semaglutide", "has_boxed_warning");
        assert_eq!(hash, b"Semaglutide:has_boxed_warning");
    }

    #[test]
    fn test_different_object_types_produce_different_hashes() {
        let base = test_assertion();

        let mut text_assertion = base.clone();
        text_assertion.object = ObjectValue::Text("true".to_string());

        let mut bool_assertion = base.clone();
        bool_assertion.object = ObjectValue::Boolean(true);

        let mut num_assertion = base.clone();
        num_assertion.object = ObjectValue::Number(1.0);

        let mut ref_assertion = base;
        ref_assertion.object = ObjectValue::Reference("true".to_string());

        let hashes = [
            compute_content_hash_v2(&text_assertion),
            compute_content_hash_v2(&bool_assertion),
            compute_content_hash_v2(&num_assertion),
            compute_content_hash_v2(&ref_assertion),
        ];

        // All should be different
        for i in 0..hashes.len() {
            for j in (i + 1)..hashes.len() {
                assert_ne!(hashes[i], hashes[j], "Hashes {} and {} should differ", i, j);
            }
        }
    }
}
