//! Audit firehose events published to the broker.
//!
//! Three event topics:
//!   - `agent.event.voice.synth.attempt` — every synth call result.
//!   - `agent.event.voice.toggle.changed` — voice_set / voice_unset.
//!   - `agent.event.voice.cache.hit` — pipeline cache hit.
//!
//! Audio bytes are NEVER included — only the sha256 fingerprint
//! + length. Operators inspect patterns (latency distribution,
//! provider preference, fallback frequency) without storing
//! voice notes in the firehose.

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::error::ReasonCode;

pub const TOPIC_SYNTH_ATTEMPT: &str = "agent.event.voice.synth.attempt";
pub const TOPIC_TOGGLE_CHANGED: &str = "agent.event.voice.toggle.changed";
pub const TOPIC_CACHE_HIT: &str = "agent.event.voice.cache.hit";

/// One row per synth attempt the pipeline returned.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynthAttemptEvent {
    pub ts_ms: i64,
    pub conversation_key: String,
    pub provider: String,
    pub voice_id: String,
    pub text_sha256: String,
    pub text_length: usize,
    pub latency_ms: u64,
    pub reason_code: ReasonCode,
    pub fallback_chain: Vec<String>,
    pub attempt: u32,
}

/// Persisted state change emitted by voice_set / voice_unset.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToggleChangedEvent {
    pub ts_ms: i64,
    pub conversation_key: String,
    pub enabled: bool,
    pub voice_id: String,
    /// `"ui" | "llm" | "admin"` — who flipped the toggle.
    pub source: String,
}

/// Cache hit notification (pipeline short-circuited a provider
/// call). Useful for operator dashboards to spot repeat queries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheHitEvent {
    pub ts_ms: i64,
    pub conversation_key: String,
    pub text_sha256: String,
    pub age_seconds: u64,
}

/// Convenience hash helper. Hex sha256.
pub fn text_sha256(text: &str) -> String {
    let mut h = Sha256::new();
    h.update(text.as_bytes());
    format!("{:x}", h.finalize())
}

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

    #[test]
    fn topic_strings_stable() {
        assert_eq!(TOPIC_SYNTH_ATTEMPT, "agent.event.voice.synth.attempt");
        assert_eq!(TOPIC_TOGGLE_CHANGED, "agent.event.voice.toggle.changed");
        assert_eq!(TOPIC_CACHE_HIT, "agent.event.voice.cache.hit");
    }

    #[test]
    fn synth_attempt_round_trips_serde() {
        let e = SynthAttemptEvent {
            ts_ms: 1_700_000_000_000,
            conversation_key: "ana:s:1".into(),
            provider: "edge".into(),
            voice_id: "es-CO-SalomeNeural".into(),
            text_sha256: text_sha256("hola"),
            text_length: 4,
            latency_ms: 850,
            reason_code: ReasonCode::Success,
            fallback_chain: vec!["edge".into()],
            attempt: 1,
        };
        let json = serde_json::to_string(&e).unwrap();
        let back: SynthAttemptEvent = serde_json::from_str(&json).unwrap();
        assert_eq!(back.provider, "edge");
        assert!(!json.contains("audio_bytes"), "audit must not leak audio");
    }

    #[test]
    fn text_sha256_stable_for_same_input() {
        assert_eq!(text_sha256("hola"), text_sha256("hola"));
        assert_ne!(text_sha256("hola"), text_sha256("Hola"));
    }
}
