//! Operator-controlled policy for LLM-driven voice overrides.
//!
//! Mirrors OpenClaw's `SpeechModelOverridePolicy` 1:1 (see
//! `research/src/config/types.tts.ts:7`). Every override the LLM
//! could request via inline `[[tts:...]]` directives is gated by
//! one boolean here — operator decides whether each dimension is
//! safe to delegate.

use serde::{Deserialize, Serialize};

/// Per-dimension allow/deny for LLM-driven overrides. All-`false`
/// by default; operator opts in explicitly. The directive parser
/// silently drops overrides whose dimension is denied.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct ModelOverridesPolicy {
    /// Master switch. `false` ⇒ every dimension below is denied
    /// regardless of its individual setting (defence-in-depth so
    /// flipping the master off doesn't require touching each
    /// dimension).
    pub enabled: bool,
    /// Allow `[[tts:text]]...[[/tts:text]]` body override (LLM
    /// substitutes a different prose for synthesis vs the visible
    /// transcript).
    pub allow_text: bool,
    /// Allow `[[tts:provider=elevenlabs]]` selection.
    pub allow_provider: bool,
    /// Allow `[[tts:voice=es-CO-SalomeNeural]]` per-utterance
    /// voice selection.
    pub allow_voice: bool,
    /// Allow `[[tts:model=tts-1-hd]]` provider model override.
    pub allow_model_id: bool,
    /// Allow `[[tts:stability=0.6 similarity_boost=0.8]]` etc —
    /// per-provider voice settings.
    pub allow_voice_settings: bool,
    /// Allow `[[tts:lang=es-CO]]` text normalisation hint.
    pub allow_normalization: bool,
    /// Allow `[[tts:seed=42]]` deterministic synthesis seed.
    pub allow_seed: bool,
    /// Allow LLM to call `voice_set` / `voice_unset` tools. When
    /// `false` only UI / admin RPC can change the toggle.
    pub allow_toggle: bool,
}

impl Default for ModelOverridesPolicy {
    fn default() -> Self {
        Self {
            enabled: true,
            allow_text: false,
            allow_provider: false,
            allow_voice: true,
            allow_model_id: false,
            allow_voice_settings: false,
            allow_normalization: false,
            allow_seed: false,
            allow_toggle: true,
        }
    }
}

impl ModelOverridesPolicy {
    /// Convenience: returns `true` only when the master switch is
    /// on AND the specified dimension is allowed.
    pub fn dimension_allowed(&self, dim: PolicyDimension) -> bool {
        if !self.enabled {
            return false;
        }
        match dim {
            PolicyDimension::Text => self.allow_text,
            PolicyDimension::Provider => self.allow_provider,
            PolicyDimension::Voice => self.allow_voice,
            PolicyDimension::ModelId => self.allow_model_id,
            PolicyDimension::VoiceSettings => self.allow_voice_settings,
            PolicyDimension::Normalization => self.allow_normalization,
            PolicyDimension::Seed => self.allow_seed,
            PolicyDimension::Toggle => self.allow_toggle,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyDimension {
    Text,
    Provider,
    Voice,
    ModelId,
    VoiceSettings,
    Normalization,
    Seed,
    Toggle,
}

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

    #[test]
    fn default_policy_locks_dangerous_dimensions() {
        let p = ModelOverridesPolicy::default();
        assert!(p.enabled);
        // Safe by default: voice + toggle on; everything else off.
        assert!(p.dimension_allowed(PolicyDimension::Voice));
        assert!(p.dimension_allowed(PolicyDimension::Toggle));
        assert!(!p.dimension_allowed(PolicyDimension::Text));
        assert!(!p.dimension_allowed(PolicyDimension::Provider));
        assert!(!p.dimension_allowed(PolicyDimension::ModelId));
        assert!(!p.dimension_allowed(PolicyDimension::VoiceSettings));
        assert!(!p.dimension_allowed(PolicyDimension::Normalization));
        assert!(!p.dimension_allowed(PolicyDimension::Seed));
    }

    #[test]
    fn master_switch_off_denies_every_dimension() {
        let mut p = ModelOverridesPolicy::default();
        p.enabled = false;
        // Even though allow_voice = true, master switch denies.
        assert!(!p.dimension_allowed(PolicyDimension::Voice));
        assert!(!p.dimension_allowed(PolicyDimension::Toggle));
    }

    #[test]
    fn yaml_round_trip() {
        let yaml = r#"
enabled: true
allow_provider: true
allow_voice_settings: true
"#;
        let p: ModelOverridesPolicy = serde_yaml::from_str(yaml).unwrap();
        assert!(p.enabled);
        assert!(p.allow_provider);
        assert!(p.allow_voice_settings);
        // Omitted fields inherit Default — allow_voice on, allow_seed off.
        assert!(p.allow_voice);
        assert!(!p.allow_seed);
    }
}
