//! Operator-tunable plugin configuration.
//!
//! Mirrors the manifest's `[plugin.config_schema]` JSON shape.
//! The daemon's `plugin.configure` JSON-RPC delivers the YAML
//! slice at `cfg.plugins.voice`; the SDK adapter's `on_configure`
//! callback hands it here for parsing.
//!
//! Today (v0.1.0) `hot_reload = true` is declared but only
//! partially honoured: new settings persist into a global lock
//! and boot reads them, so a restart applies the latest YAML.
//! True in-place application without restart is a follow-up
//! (`81.33.x.voice-hot-reload`).

use std::path::PathBuf;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

use crate::breaker::BreakerSettings;
use crate::cache::AudioCache;
use crate::fallback::{CostClassFilter, FallbackConfig, FallbackStrategyKind, RetryPolicy};
use crate::policy::ModelOverridesPolicy;

/// Top-level config slice for ONE tenant. Each subsection is
/// optional — when absent the daemon's defaults apply.
///
/// Top-level `enabled: false` opts the tenant out without removing
/// it from `plugins.discovery.search_paths`. Mirrors the
/// nexo-rs-plugin-email pattern operators expect.
///
/// Multi-instance (0.2.0): the operator may declare an array of
/// these blocks. The `instance` label identifies the tenant; the
/// optional `allow_agents` whitelist routes tool invocations from
/// matching agents to this tenant's pipeline. A config without
/// `instance` and `allow_agents` becomes the implicit `default`
/// catch-all.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct VoicePluginConfig {
    /// Per-tenant label. `None` → `default` (single-tenant boot or
    /// catch-all entry in a multi-tenant array). Sanitised to
    /// `[a-z0-9_-]{1,64}` by the boot helper.
    pub instance: Option<String>,
    /// Agent ids whose tool invocations route to this tenant.
    /// Empty = catch-all (used when no other tenant claims the
    /// agent_id; only one catch-all per declaration is allowed).
    pub allow_agents: Vec<String>,
    pub enabled: bool,
    pub auto_mode: AutoMode,
    pub default_voice: Option<String>,
    pub primary_provider: Option<String>,
    pub fallback: FallbackSection,
    pub retry: RetrySection,
    pub text: TextSection,
    pub cache: CacheSection,
    pub audio: AudioSection,
    pub model_overrides_policy: ModelOverridesPolicy,
    pub providers: ProvidersSection,
    pub breakers: BreakerSettings,
}

impl Default for VoicePluginConfig {
    fn default() -> Self {
        Self {
            instance: None,
            allow_agents: Vec::new(),
            enabled: true,
            auto_mode: AutoMode::default(),
            default_voice: None,
            primary_provider: None,
            fallback: FallbackSection::default(),
            retry: RetrySection::default(),
            text: TextSection::default(),
            cache: CacheSection::default(),
            audio: AudioSection::default(),
            model_overrides_policy: ModelOverridesPolicy::default(),
            providers: ProvidersSection::default(),
            breakers: BreakerSettings::default(),
        }
    }
}

/// `plugin.configure` YAML payload shape. Operators may declare the
/// plugin as a single object (legacy 0.1.x → implicit `default`
/// tenant) or an array of per-tenant blocks (0.2.0). The boot
/// helper [`crate::boot::apply_configure`] flattens both into the
/// same `Vec<VoicePluginConfig>` before factory invocation.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum VoicePluginShape {
    Single(VoicePluginConfig),
    Many(Vec<VoicePluginConfig>),
}

/// Auto-mode enum mirroring spec's 5 variants. `PerConversation`
/// is the default — voice mode only fires when the
/// `VoiceModeStore` row says it's active for the given chat.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AutoMode {
    Off,
    Always,
    Inbound,
    Tagged,
    PerConversation,
}

impl Default for AutoMode {
    fn default() -> Self {
        Self::PerConversation
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FallbackSection {
    pub strategy: FallbackStrategyKind,
    pub chain: Vec<String>,
    pub max_cost: CostClassFilter,
}

impl Default for FallbackSection {
    fn default() -> Self {
        Self {
            strategy: FallbackStrategyKind::Chain,
            chain: vec!["edge".to_string()],
            max_cost: CostClassFilter::Free,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RetrySection {
    pub max_attempts: u32,
    pub backoff_ms_initial: u64,
    pub backoff_ms_max: u64,
    pub backoff_multiplier: f64,
}

impl Default for RetrySection {
    fn default() -> Self {
        let d = RetryPolicy::default();
        Self {
            max_attempts: d.max_attempts,
            backoff_ms_initial: d.backoff_ms_initial,
            backoff_ms_max: d.backoff_ms_max,
            backoff_multiplier: d.backoff_multiplier,
        }
    }
}

impl From<RetrySection> for RetryPolicy {
    fn from(s: RetrySection) -> Self {
        Self {
            max_attempts: s.max_attempts,
            backoff_ms_initial: s.backoff_ms_initial,
            backoff_ms_max: s.backoff_ms_max,
            backoff_multiplier: s.backoff_multiplier,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TextSection {
    pub max_length: usize,
    pub summarize_when_over: bool,
}

impl Default for TextSection {
    fn default() -> Self {
        Self {
            max_length: 1500,
            summarize_when_over: false,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CacheSection {
    pub enabled: bool,
    pub max_bytes: usize,
    pub ttl_seconds: u64,
}

impl Default for CacheSection {
    fn default() -> Self {
        Self {
            enabled: true,
            max_bytes: 100 * 1024 * 1024, // 100 MB
            ttl_seconds: 3600,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AudioSection {
    pub format: String,
    pub transcode: TranscodeKind,
}

impl Default for AudioSection {
    fn default() -> Self {
        Self {
            format: "ogg_opus".to_string(),
            transcode: TranscodeKind::PureRust,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TranscodeKind {
    PureRust,
    // Ffmpeg kept as enum slot for forward-compat; the plugin only
    // ships pure-rust today and ignores this field at runtime.
    Ffmpeg,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProvidersSection {
    pub edge: Option<EdgeConfig>,
    pub elevenlabs: Option<ElevenLabsConfig>,
    pub azure: Option<AzureConfig>,
    pub openai_tts: Option<OpenAiTtsConfig>,
    pub cartesia: Option<CartesiaConfig>,
    pub piper: Option<PiperConfig>,
    pub kokoro: Option<KokoroConfig>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct EdgeConfig {
    pub pool_size: Option<usize>,
    pub pool_ttl_seconds: Option<u64>,
    pub base_rate_pct: Option<i32>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ElevenLabsConfig {
    pub model_id: Option<String>,
    pub voice_settings: Option<serde_yaml::Value>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AzureConfig {
    pub region: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct OpenAiTtsConfig {
    pub model: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CartesiaConfig {}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PiperConfig {
    pub models_dir: Option<PathBuf>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct KokoroConfig {
    pub model_path: Option<PathBuf>,
}

impl VoicePluginConfig {
    /// Convert the parsed `fallback` + `retry` slices into the
    /// runtime `FallbackConfig`. Used by the boot path to seed
    /// the initial context.
    pub fn to_fallback_config(&self) -> FallbackConfig {
        FallbackConfig {
            strategy: self.fallback.strategy,
            chain: self.fallback.chain.clone(),
            max_cost: self.fallback.max_cost,
            retry: self.retry.clone().into(),
        }
    }

    /// Build the audio cache the [`crate::pipeline`] uses,
    /// honouring `enabled = false` by zero-sizing the buffer.
    pub fn build_cache(&self) -> AudioCache {
        let max_bytes = if self.cache.enabled {
            self.cache.max_bytes
        } else {
            0
        };
        AudioCache::new(max_bytes, std::time::Duration::from_secs(self.cache.ttl_seconds))
    }
}

/// Process-wide settings handle. Populated lazily on the first
/// `plugin.configure` JSON-RPC; reads the [`VoicePluginConfig::default()`]
/// before the host calls configure so boot has a sane baseline.
pub fn settings() -> &'static Arc<RwLock<VoicePluginConfig>> {
    static SETTINGS: once_cell::sync::OnceCell<Arc<RwLock<VoicePluginConfig>>> =
        once_cell::sync::OnceCell::new();
    SETTINGS.get_or_init(|| Arc::new(RwLock::new(VoicePluginConfig::default())))
}

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

    #[test]
    fn default_parses_to_per_conversation_with_edge_chain() {
        let c = VoicePluginConfig::default();
        assert_eq!(c.auto_mode, AutoMode::PerConversation);
        assert_eq!(c.fallback.chain, vec!["edge".to_string()]);
        assert_eq!(c.text.max_length, 1500);
        assert!(c.cache.enabled);
        assert_eq!(c.cache.max_bytes, 100 * 1024 * 1024);
    }

    #[test]
    fn round_trip_yaml() {
        let yaml = r#"
auto_mode: always
primary_provider: edge
fallback:
  strategy: chain
  chain: ["edge", "elevenlabs"]
  max_cost: cheap
retry:
  max_attempts: 5
  backoff_ms_initial: 100
  backoff_ms_max: 5000
  backoff_multiplier: 2.0
text:
  max_length: 2000
  summarize_when_over: false
cache:
  enabled: false
  max_bytes: 0
  ttl_seconds: 60
audio:
  format: ogg_opus
  transcode: pure_rust
model_overrides_policy:
  enabled: true
  allow_voice: true
  allow_toggle: true
providers:
  edge:
    pool_size: 8
  elevenlabs:
    model_id: eleven_multilingual_v2
"#;
        let parsed: VoicePluginConfig = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(parsed.auto_mode, AutoMode::Always);
        assert_eq!(parsed.primary_provider.as_deref(), Some("edge"));
        assert_eq!(parsed.fallback.chain.len(), 2);
        assert_eq!(parsed.retry.max_attempts, 5);
        assert_eq!(parsed.text.max_length, 2000);
        assert!(!parsed.cache.enabled);
        assert_eq!(parsed.providers.edge.as_ref().unwrap().pool_size, Some(8));
        assert_eq!(
            parsed
                .providers
                .elevenlabs
                .as_ref()
                .unwrap()
                .model_id
                .as_deref(),
            Some("eleven_multilingual_v2")
        );
    }

    #[test]
    fn unknown_field_rejected() {
        let yaml = "auto_mode: per_conversation\nbogus_field: 42\n";
        let err = serde_yaml::from_str::<VoicePluginConfig>(yaml).unwrap_err();
        assert!(err.to_string().contains("bogus_field"));
    }

    #[test]
    fn to_fallback_config_threads_retry() {
        let mut c = VoicePluginConfig::default();
        c.retry.max_attempts = 7;
        c.fallback.chain = vec!["edge".into(), "openai_tts".into()];
        let fb = c.to_fallback_config();
        assert_eq!(fb.retry.max_attempts, 7);
        assert_eq!(fb.chain, vec!["edge".to_string(), "openai_tts".to_string()]);
    }

    #[test]
    fn build_cache_disabled_yields_zero_max_bytes() {
        let mut c = VoicePluginConfig::default();
        c.cache.enabled = false;
        c.cache.max_bytes = 999;
        let cache = c.build_cache();
        assert_eq!(cache.current_bytes(), 0);
    }

    #[tokio::test]
    async fn settings_handle_singleton() {
        let a = settings();
        let b = settings();
        assert!(Arc::ptr_eq(a, b));
        let read = a.read().await;
        assert_eq!(read.text.max_length, 1500);
    }
}
