//! TTS provider abstraction.
//!
//! Every implementor returns raw audio bytes plus a [`ReasonCode`]
//! describing the outcome. The pipeline above wraps these calls
//! with retry + fallback + circuit breaker + cache; providers
//! themselves only worry about their endpoint's quirks.
//!
//! ## Feature flags
//!
//! Each provider lives behind its own Cargo feature so a microapp
//! that only needs Edge doesn't pull in `reqwest`, `sherpa-rs`,
//! `ort`, or any cloud SDK. The default build is `provider-edge`
//! only — opt in to others via `--features provider-elevenlabs`,
//! `--features provider-azure`, etc.

use async_trait::async_trait;

use crate::error::{ReasonCode, Result};

#[cfg(feature = "provider-edge")]
pub mod edge;

// ─── Stubs (feature-gated, each module's `synthesize` returns
//      NotSupported when the cargo feature is missing real deps) ───

#[cfg(feature = "provider-elevenlabs")]
pub mod elevenlabs;

#[cfg(feature = "provider-azure")]
pub mod azure;

#[cfg(feature = "provider-openai-tts")]
pub mod openai_tts;

#[cfg(feature = "provider-cartesia")]
pub mod cartesia;

#[cfg(feature = "provider-piper")]
pub mod piper;

#[cfg(feature = "provider-kokoro")]
pub mod kokoro;

/// Output format the provider produces BEFORE the transcode step.
/// The pipeline normalises everything to canonical OGG/Opus after.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioFormat {
    OggOpus24k,
    Mp3_24k,
    WavPcm16,
}

impl AudioFormat {
    /// MIME hint for the raw bytes the provider returns.
    pub fn raw_mime(self) -> &'static str {
        match self {
            Self::OggOpus24k => "audio/ogg; codecs=opus",
            Self::Mp3_24k => "audio/mpeg",
            Self::WavPcm16 => "audio/wav",
        }
    }
}

/// Coarse cost class — drives operator filters and the
/// `parallel-cheapest-wins` strategy. Mirrors `voices.toml`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CostClass {
    Free,
    Cheap,
    Premium,
}

impl CostClass {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Free => "free",
            Self::Cheap => "cheap",
            Self::Premium => "premium",
        }
    }
}

/// Per-call request shape. Providers borrow everything to avoid
/// cloning the body on every retry.
#[derive(Debug)]
pub struct SynthesizeRequest<'a> {
    /// SSML-prepared body. Already through the markers / markdown /
    /// auto_detect / emoji-strip / collapse pipeline.
    pub body_ssml: &'a str,
    /// Voice id to synthesise with. Pipeline resolves catalog →
    /// per-provider native id before calling.
    pub voice_id: &'a str,
    /// Optional speed multiplier `0.5..=2.0`. `None` ⇒ provider
    /// default cadence.
    pub speed: Option<f32>,
    /// Optional pitch percentage `-50..=50`. `None` ⇒ provider
    /// default.
    pub pitch_pct: Option<i32>,
    /// Optional provider-specific style id.
    pub style: Option<&'a str>,
    /// Optional model override (`tts-1` vs `tts-1-hd`, etc).
    pub model_id: Option<&'a str>,
    /// Format we want the provider to emit. Pipeline transcodes
    /// to canonical OGG/Opus after.
    pub format: AudioFormat,
}

/// Successful output: raw bytes + classification metadata.
#[derive(Debug, Clone)]
pub struct SynthesizeOutput {
    pub audio_bytes: Vec<u8>,
    pub raw_mime: &'static str,
    pub latency_ms: u64,
    pub reason_code: ReasonCode,
}

/// TTS backend. Implementors handle endpoint-specific quirks
/// (Edge WS empty-body retry, ElevenLabs rate-limit headers,
/// Cartesia streaming chunks) inside `synthesize`. The pipeline
/// adds retry + fallback + circuit breaker on top.
#[async_trait]
pub trait TtsProvider: Send + Sync {
    /// Provider id matching `voices.toml::provider` + the operator
    /// config key. Stable string; do not localise.
    fn id(&self) -> &'static str;

    /// Human-readable label for diagnostics + admin dashboards.
    fn label(&self) -> &str;

    /// Aliases for legacy `voice_set { provider: ... }` calls.
    /// E.g. `"microsoft"` ↔ `"edge"`. Default = no aliases.
    fn aliases(&self) -> &[&'static str] {
        &[]
    }

    /// Order operator's UI dropdown / wizard picker presents this
    /// provider. Lower = earlier. Default = 100 (mid-pack).
    fn auto_select_order(&self) -> u32 {
        100
    }

    /// Coarse cost class for filter operations.
    fn cost_class(&self) -> CostClass;

    /// `true` when the provider has every credential / binary it
    /// needs to call out today. The fallback executor skips
    /// providers that return `false` instead of attempting a
    /// failed synth.
    fn is_configured(&self) -> bool;

    /// Locales this provider can synthesise. Empty slice ⇒ no
    /// locale gating. The pipeline never calls a provider whose
    /// `supported_locales` excludes the request's locale.
    fn supported_locales(&self) -> &[&'static str] {
        &[]
    }

    /// Actual synthesis call. Borrows the request to avoid cloning
    /// on retry.
    async fn synthesize(&self, req: SynthesizeRequest<'_>) -> Result<SynthesizeOutput>;
}
