//! `TranscriptionProvider` is the async-native trait every transcription
//! backend implements, regardless of whether the work happens locally
//! (Whisper, Parakeet, Moonshine via the `LocalProviderAdapter` in
//! `lumotia-transcription`) or remotely (OpenAI Whisper, Groq,
//! Deepgram, etc.).
//!
//! Living in `lumotia-cloud-providers` is deliberate: the AGPL OEM
//! exception (≥£2k/yr) requires a clean trait surface that an OEM
//! licensee can implement without depending on Lumotia's transcription
//! internals. The trait crate stays small; provider implementations
//! sit alongside.
//!
//! Object-safety discipline: no generic methods, no `Self`-returning
//! methods. The compile-time witness in `tests` enforces this.

use std::fmt;

use async_trait::async_trait;
use lumotia_core::error::Result;
use lumotia_core::types::{AudioSamples, ModelId, Transcript, TranscriptionOptions};
use serde::{Deserialize, Serialize};

/// Stable, lower-kebab-case identifier for a provider. Used in user
/// profiles, settings storage, and logs. Examples: `local-whisper`,
/// `local-parakeet`, `openai-whisper`, `groq-whisper-v3`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ProviderId(String);

impl ProviderId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for ProviderId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

/// Whether a provider runs locally or over the network. The orchestrator
/// inspects this to decide whether to honour an offline-only profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProviderKind {
    Local,
    Cloud(NetworkRequirement),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkRequirement {
    /// Provider requires a live network connection on every call.
    Online,
    /// Provider can fall back to a cached or queued path when offline.
    OnlineWithFallback,
}

/// Indicative cost class for UI surfacing. Not a billing source of truth.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CostClass {
    /// No marginal cost beyond the user's local hardware.
    Free,
    /// Per-call cost; user supplies their own API key (BYOK).
    PaidByok,
    /// Per-call cost; provider billed by CORBEL on the user's behalf.
    PaidManaged,
}

/// Capabilities a provider advertises to the orchestrator and the UI.
/// Superset of `lumotia_transcription::TranscriberCapabilities` for
/// local providers, with extra fields cloud providers populate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCapabilities {
    pub sample_rate: u32,
    pub channels: u16,
    pub initial_prompt_supported: bool,
    pub language_hint_supported: bool,
    pub streaming_supported: bool,
    pub cost_class: CostClass,
}

/// User-selectable engine configuration. The orchestrator resolves
/// `engine_id` against the `EngineRegistry`, then derives
/// `TranscriptionOptions` from the remaining fields.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EngineProfile {
    pub engine_id: ProviderId,
    pub model_id: Option<ModelId>,
    pub language: Option<String>,
    pub initial_prompt: Option<String>,
}

impl EngineProfile {
    pub fn new(engine_id: ProviderId) -> Self {
        Self {
            engine_id,
            model_id: None,
            language: None,
            initial_prompt: None,
        }
    }

    pub fn to_options(&self) -> TranscriptionOptions {
        TranscriptionOptions {
            language: self.language.clone(),
            initial_prompt: self.initial_prompt.clone(),
        }
    }
}

/// Result returned by `TranscriptionProvider::transcribe`. Carries the
/// transcript plus inference timing so the orchestrator can surface
/// latency without losing it across the trait boundary.
#[derive(Debug, Clone)]
pub struct ProviderTranscript {
    pub transcript: Transcript,
    pub inference_ms: u64,
}

/// Async-native unified interface for transcription providers.
///
/// `Send + Sync` supertraits: providers live in an `Arc<dyn
/// TranscriptionProvider>` shared across tokio tasks. `async_trait`
/// macro converts the async methods into boxed futures so the trait
/// stays object-safe.
#[async_trait]
pub trait TranscriptionProvider: Send + Sync {
    fn provider_id(&self) -> ProviderId;

    fn kind(&self) -> ProviderKind;

    fn capabilities(&self) -> ProviderCapabilities;

    /// Idempotent pre-flight. Local providers verify the model is
    /// loaded into memory; cloud providers validate credentials and
    /// reach the endpoint. Called before the first `transcribe` of a
    /// session, and again after a profile or settings change that
    /// invalidates the previous prepare.
    async fn prepare(&self, profile: &EngineProfile) -> Result<()>;

    /// Transcribe a chunk of audio. The orchestrator passes raw audio
    /// already resampled to the provider's `capabilities().sample_rate`.
    async fn transcribe(
        &self,
        audio: AudioSamples,
        options: TranscriptionOptions,
    ) -> Result<ProviderTranscript>;
}

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

    #[test]
    fn provider_trait_is_object_safe() {
        // Compile-time witness: if the trait stops being object-safe
        // (generic method, Self-returning method, missing async_trait
        // attribute) this declaration fails to build. No runtime work.
        let _: Option<std::sync::Arc<dyn TranscriptionProvider>> = None;
    }

    #[test]
    fn provider_id_round_trips_display_and_str() {
        let id = ProviderId::new("local-whisper");
        assert_eq!(id.as_str(), "local-whisper");
        assert_eq!(id.to_string(), "local-whisper");
    }

    #[test]
    fn engine_profile_derives_options() {
        let profile = EngineProfile {
            engine_id: ProviderId::new("local-whisper"),
            model_id: None,
            language: Some("en".to_string()),
            initial_prompt: Some("Lumotia".to_string()),
        };
        let opts = profile.to_options();
        assert_eq!(opts.language, Some("en".to_string()));
        assert_eq!(opts.initial_prompt, Some("Lumotia".to_string()));
    }
}
