//! 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};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};

/// Errors from signature verification.
///
/// Error messages are written for API consumers who may not understand Ed25519
/// cryptography, explaining what fields must contain and common mistakes.
#[derive(Debug)]
pub enum SignatureError {
    /// No signatures present on the assertion.
    Empty,

    /// Unknown signature version.
    UnknownVersion {
        /// The unrecognized version number.
        version: u8,
        /// Which signature in the list.
        index: usize,
    },

    /// The agent_id bytes are not a valid Ed25519 public key.
    InvalidPublicKey {
        /// Which signature in the list.
        index: usize,
        /// The underlying error detail.
        detail: String,
    },

    /// The signature does not verify against the message.
    VerificationFailed {
        /// Which signature in the list.
        index: usize,
        /// The signature version.
        version: u8,
        /// The underlying error detail.
        detail: String,
    },
}

impl std::fmt::Display for SignatureError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Empty => write!(
                f,
                "Assertion must have at least one signature. \
                Each signature requires: agent_id (32-byte Ed25519 public key, hex-encoded as 64 chars), \
                signature (64-byte Ed25519 signature, hex-encoded as 128 chars). \
                SHA-256/SHA-512 hashes cannot be used as agent_id or signature"
            ),
            Self::UnknownVersion { version, index } => write!(
                f,
                "Signature {index}: unknown version {version}. \
                Supported versions: 1 (signs '{{subject}}:{{predicate}}'), 2 (signs BLAKE3 content hash)"
            ),
            Self::InvalidPublicKey { index, detail } => write!(
                f,
                "Signature {index}: agent_id is not a valid Ed25519 public key ({detail}). \
                agent_id must be a 32-byte Ed25519 public key (hex-encoded as 64 chars). \
                Common mistake: using SHA-256 or other hashes as agent_id. \
                Generate a keypair with Ed25519 (e.g., ed25519-dalek, crypto/ed25519, or openssl)"
            ),
            Self::VerificationFailed { index, version, detail } => {
                let message_desc = match version {
                    1 => "'{subject}:{predicate}' (UTF-8 bytes)",
                    2 => "the BLAKE3 content hash of the assertion",
                    _ => "unknown",
                };
                write!(
                    f,
                    "Signature {index}: Ed25519 verification failed ({detail}). \
                    For v{version} signatures, signature must be Ed25519_sign(private_key, {message_desc}). \
                    Common mistakes: (1) using SHA hashes instead of Ed25519 signatures, \
                    (2) signing the wrong message, (3) agent_id doesn't match the signing key"
                )
            }
        }
    }
}

impl std::error::Error for SignatureError {}

/// Verify all Ed25519 signatures on an assertion.
///
/// Supports two signature versions:
/// - **Version 1 (legacy):** signs `"{subject}:{predicate}"` — only protects those fields
/// - **Version 2 (enterprise):** signs the BLAKE3 content hash — protects ALL fields
///
/// All signatures must be valid for the assertion to be accepted.
///
/// This function is used at both the API boundary (fail fast with 400) and in the
/// IngestWorker (defense in depth). Keeping it in `stemedb-core` avoids duplication.
pub fn verify_assertion_signatures(
    assertion: &Assertion,
) -> std::result::Result<(), SignatureError> {
    if assertion.signatures.is_empty() {
        return Err(SignatureError::Empty);
    }

    // Pre-compute v1 message (subject:predicate) — only used if v1 signatures exist
    let v1_message = format!("{}:{}", assertion.subject, assertion.predicate);

    // Pre-compute v2 content hash — only if any v2 signature exists
    let v2_content_hash: Option<[u8; 32]> = if assertion.signatures.iter().any(|s| s.version == 2) {
        Some(compute_content_hash_v2(assertion))
    } else {
        None
    };

    for (idx, sig_entry) in assertion.signatures.iter().enumerate() {
        let message_bytes: &[u8] = match sig_entry.version {
            1 => v1_message.as_bytes(),
            2 => v2_content_hash
                .as_ref()
                .ok_or(SignatureError::UnknownVersion { version: 2, index: idx })?,
            v => {
                return Err(SignatureError::UnknownVersion { version: v, index: idx });
            }
        };

        let verifying_key = VerifyingKey::from_bytes(&sig_entry.agent_id)
            .map_err(|e| SignatureError::InvalidPublicKey { index: idx, detail: e.to_string() })?;

        let signature = Signature::from_bytes(&sig_entry.signature);

        verifying_key.verify(message_bytes, &signature).map_err(|e| {
            SignatureError::VerificationFailed {
                index: idx,
                version: sig_entry.version,
                detail: e.to_string(),
            }
        })?;
    }

    Ok(())
}

/// 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
///
/// **Narrative IS included** because it is content-bearing (methodology, limitations).
/// Changing the narrative changes the assertion's meaning. When `None`, no bytes
/// are added, preserving backward compatibility with pre-narrative hashes.
///
/// # 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());

    // Narrative (only when present, so None preserves backward-compat hash)
    if let Some(ref narrative) = assertion.narrative {
        hasher.update(b":narrative:");
        hasher.update(narrative.as_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,
            narrative: 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);
            }
        }
    }

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

        assertion.narrative =
            Some("This drug carries a boxed warning for thyroid C-cell tumors.".to_string());
        let hash_some = compute_content_hash_v2(&assertion);

        assert_ne!(hash_none, hash_some, "Narrative should change the content hash");
    }

    #[test]
    fn test_content_hash_backward_compat_narrative_none() {
        // Capture a hash with narrative: None
        let assertion = test_assertion();
        let hash1 = compute_content_hash_v2(&assertion);

        // Build the same assertion again independently
        let assertion2 = 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,
            narrative: None,
            lifecycle: LifecycleStage::Approved,
            signatures: vec![],
            confidence: 0.95,
            timestamp: 1704067200,
            hlc_timestamp: HlcTimestamp::default(),
            vector: None,
        };
        let hash2 = compute_content_hash_v2(&assertion2);

        assert_eq!(hash1, hash2, "narrative: None must produce identical hash for backward compat");
    }
}
