//! Engine-abstraction trait for speech-to-text backends.
//!
//! Replaces the previous `SpeechBackend` enum so new backends
//! (Moonshine, whisper-rs forks, cloud ASR shims, Windows non-AVX2
//! fallbacks) can drop in without adding a match arm in `LocalEngine`.
//!
//! Concrete implementers today: `SpeechModelAdapter` (wraps any
//! `transcribe-rs` model, currently used for Parakeet) and — behind the
//! `whisper` feature — `WhisperRsBackend` (direct whisper-rs, the only
//! path that pipes `initial_prompt`).

use std::sync::atomic::AtomicBool;
use std::sync::Arc;

use lumotia_core::error::Result;
use lumotia_core::types::{Segment, TranscriptionOptions};

/// Static capabilities a `Transcriber` advertises to callers.
///
/// `sample_rate` is load-bearing for the progressive WAV writer (#19)
/// which writes live capture samples to disk at the transcriber's
/// native rate. `supports_initial_prompt` lets the Settings surface
/// hide the initial-prompt field for backends that ignore it (Parakeet
/// today).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TranscriberCapabilities {
    pub sample_rate: u32,
    pub channels: u16,
    pub supports_initial_prompt: bool,
}

/// Unified interface for speech-to-text backends.
///
/// `Send` is a supertrait so `Box<dyn Transcriber + Send>` travels
/// across `spawn_blocking` boundaries without a per-site bound. All
/// inference is synchronous — async callers wrap a `tokio::spawn_blocking`
/// around `transcribe_sync`.
pub trait Transcriber: Send {
    fn capabilities(&self) -> TranscriberCapabilities;

    /// Synchronously transcribe 16 kHz mono f32 PCM (or whatever the
    /// backend's `capabilities().sample_rate` declares). `&mut self` so
    /// backends that keep per-call scratch state (whisper-rs's
    /// `WhisperState`, Parakeet's decoder buffers) can mutate them
    /// without interior-mutability gymnastics.
    fn transcribe_sync(
        &mut self,
        samples: &[f32],
        options: &TranscriptionOptions,
    ) -> Result<Vec<Segment>>;

    /// Variant of `transcribe_sync` that accepts an external abort flag.
    ///
    /// REQUIRED so every backend opts in to a cancellation story at
    /// compile time. Without this requirement a default impl that
    /// simply forwarded to `transcribe_sync` would silently re-introduce
    /// the wedge for any non-whisper backend: the live session's
    /// `drain_inference` timeout would set the flag, but the backend
    /// would ignore it, the orphan inference thread would keep holding
    /// the engine `Mutex`, and the next start/stop would deadlock on it.
    ///
    /// Implementer guidance:
    /// * Backends that can react to mid-inference cancellation
    ///   (whisper-rs via `set_abort_callback_safe`) wire the flag into
    ///   their decoder's abort hook so the wedged inference can be
    ///   unstuck by the live session's drain timeout.
    /// * Backends that genuinely cannot honour the flag mid-call
    ///   (synchronous external API calls, transcribe-rs adapters that
    ///   own opaque decoder state) MUST still implement this method —
    ///   even if the implementation is to check the flag at the safest
    ///   available boundary and otherwise dispatch to `transcribe_sync`.
    ///   Document the uncancellability with a `// SAFETY:` comment so a
    ///   future audit can find it.
    fn transcribe_sync_with_abort(
        &mut self,
        samples: &[f32],
        options: &TranscriptionOptions,
        abort_flag: Arc<AtomicBool>,
    ) -> Result<Vec<Segment>>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::Ordering;

    #[test]
    fn transcriber_trait_is_object_safe() {
        // Compile-time witness: if the trait stops being object-safe
        // (e.g. someone adds a generic method or a Self-returning
        // method) this declaration fails to build. No runtime work.
        let _: Option<Box<dyn Transcriber + Send>> = None;
    }

    /// Fake backend that records whether the abort flag was observed
    /// at dispatch time. Proves the compile-time-required
    /// `transcribe_sync_with_abort` actually receives the flag instead
    /// of silently falling back to a default impl that drops it on the
    /// floor (the Lifecycle-2 wedge).
    struct FlagSnoopingBackend {
        observed_abort: bool,
    }

    impl Transcriber for FlagSnoopingBackend {
        fn capabilities(&self) -> TranscriberCapabilities {
            TranscriberCapabilities {
                sample_rate: 16_000,
                channels: 1,
                supports_initial_prompt: false,
            }
        }

        fn transcribe_sync(
            &mut self,
            _samples: &[f32],
            _options: &TranscriptionOptions,
        ) -> Result<Vec<Segment>> {
            Ok(Vec::new())
        }

        fn transcribe_sync_with_abort(
            &mut self,
            _samples: &[f32],
            _options: &TranscriptionOptions,
            abort_flag: Arc<AtomicBool>,
        ) -> Result<Vec<Segment>> {
            self.observed_abort = abort_flag.load(Ordering::Relaxed);
            Ok(Vec::new())
        }
    }

    #[test]
    fn transcribe_sync_with_abort_is_required_and_flag_is_observed() {
        // Lifecycle-2 regression: removing the trait's default impl
        // forces every backend to receive the abort flag at the call
        // site. Without an explicit method on this fake backend the
        // file wouldn't compile; with it, asserting the snoop is the
        // runtime witness that the flag is actually piped through.
        let mut backend = FlagSnoopingBackend {
            observed_abort: false,
        };
        let flag = Arc::new(AtomicBool::new(true));
        let options = TranscriptionOptions::default();
        let segs = backend
            .transcribe_sync_with_abort(&[], &options, flag.clone())
            .expect("fake backend never errors");
        assert!(segs.is_empty());
        assert!(
            backend.observed_abort,
            "trait must hand the abort flag to the backend, not drop it via a default impl"
        );
    }
}
