//! Windows system-audio capture via WASAPI loopback.
//!
//! On Windows, cpal's WASAPI host transparently enables loopback mode
//! when you build an *input* stream from an *output* device — so we
//! get the post-mix system audio with no Windows-specific code beyond
//! grabbing the default output device.
//!
//! See:
//! <https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording>
//!
//! ## Coverage caveat
//!
//! This module is gated on `cfg(target_os = "windows")` and was
//! authored on macOS — it compiles via cross-platform cpal but has not
//! been exercised on real Windows hardware. The cpal API contract
//! (loopback on output devices) is documented and stable, so the
//! integration should be straightforward, but expect first-run rough
//! edges around device selection (multiple output devices) and sample
//! format negotiation.

#![cfg(target_os = "windows")]

use crate::enrollment::TranscriptRole;
use crate::provider::build_stt_provider;
use crate::vad::VadState;
use crate::{Listener, Result, VoiceConfig, VoiceError, VoiceEvent};
use async_trait::async_trait;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;

const EVENT_CHANNEL_CAPACITY: usize = 64;
const AUDIO_CHANNEL_CAPACITY: usize = 8;
const MIN_SEGMENT_MS: u64 = 300;

/// Windows system-audio listener using WASAPI loopback on the default
/// output device. Mirrors [`crate::CpalListener`]'s structure — the
/// only difference is the device selection.
pub struct WindowsLoopbackListener {
    running: Arc<AtomicBool>,
    stt_task: Option<tokio::task::JoinHandle<()>>,
    capture_thread: Option<std::thread::JoinHandle<()>>,
}

impl WindowsLoopbackListener {
    pub fn new() -> Self {
        Self {
            running: Arc::new(AtomicBool::new(false)),
            stt_task: None,
            capture_thread: None,
        }
    }
}

impl Default for WindowsLoopbackListener {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for WindowsLoopbackListener {
    fn drop(&mut self) {
        if self.running.load(Ordering::SeqCst) {
            self.running.store(false, Ordering::SeqCst);
            if let Some(handle) = self.capture_thread.take() {
                let _ = handle.join();
            }
            if let Some(task) = self.stt_task.take() {
                task.abort();
            }
        }
    }
}

#[async_trait]
impl Listener for WindowsLoopbackListener {
    async fn start(&mut self, config: VoiceConfig) -> Result<mpsc::Receiver<VoiceEvent>> {
        if self.running.load(Ordering::SeqCst) {
            return Err(VoiceError::AlreadyRunning);
        }
        let stt_provider = build_stt_provider(&config)?;
        self.running.store(true, Ordering::SeqCst);

        let (event_tx, event_rx) = mpsc::channel::<VoiceEvent>(EVENT_CHANNEL_CAPACITY);
        let (audio_tx, mut audio_rx) = mpsc::channel::<AudioSegment>(AUDIO_CHANNEL_CAPACITY);

        let event_tx_for_stt = event_tx.clone();
        let stt_handle = tokio::spawn(async move {
            while let Some(segment) = audio_rx.recv().await {
                match stt_provider
                    .transcribe(&segment.samples, segment.sample_rate)
                    .await
                {
                    Ok(text) if !text.is_empty() => {
                        let _ = event_tx_for_stt
                            .send(VoiceEvent::Transcript {
                                text,
                                duration_ms: segment.duration_ms,
                                role: TranscriptRole::Unknown,
                            })
                            .await;
                    }
                    Ok(_) => {}
                    Err(e) => {
                        tracing::error!("[wasapi-loopback] STT failed: {e}");
                    }
                }
            }
        });
        self.stt_task = Some(stt_handle);

        let running = self.running.clone();
        let cfg = config.clone();
        let capture_handle = std::thread::spawn(move || {
            if let Err(e) = run_loopback(running.clone(), cfg, event_tx, audio_tx) {
                tracing::error!("[wasapi-loopback] capture thread error: {e}");
                running.store(false, Ordering::SeqCst);
            }
        });
        self.capture_thread = Some(capture_handle);
        Ok(event_rx)
    }

    async fn stop(&mut self) -> Result<()> {
        if !self.running.swap(false, Ordering::SeqCst) {
            return Err(VoiceError::NotRunning);
        }
        if let Some(task) = self.stt_task.take() {
            task.abort();
        }
        if let Some(handle) = self.capture_thread.take() {
            tokio::task::spawn_blocking(move || {
                let _ = handle.join();
            })
            .await
            .map_err(|e| VoiceError::Capture(format!("join task failed: {e}")))?;
        }
        Ok(())
    }

    fn is_running(&self) -> bool {
        self.running.load(Ordering::SeqCst)
    }
}

struct AudioSegment {
    samples: Vec<f32>,
    sample_rate: u32,
    duration_ms: u64,
}

fn run_loopback(
    running: Arc<AtomicBool>,
    config: VoiceConfig,
    event_tx: mpsc::Sender<VoiceEvent>,
    audio_tx: mpsc::Sender<AudioSegment>,
) -> Result<()> {
    let host = cpal::default_host();
    // The Windows-specific bit: use the default OUTPUT device. cpal's
    // WASAPI host will quietly enable loopback when we then build an
    // INPUT stream from it. This is the documented contract:
    // https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
    let device = host
        .default_output_device()
        .ok_or_else(|| VoiceError::Device("no default output device for loopback".into()))?;
    let stream_config = device
        .default_output_config()
        .map_err(|e| VoiceError::Device(format!("default output config: {e}")))?;
    let sample_rate = stream_config.sample_rate().0;
    tracing::info!(
        "[wasapi-loopback] capturing from output device '{}' at {}Hz",
        device.name().unwrap_or_default(),
        sample_rate
    );

    let recording: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(Vec::new()));
    let is_recording = Arc::new(AtomicBool::new(false));
    let vad: Arc<Mutex<VadState>> =
        Arc::new(Mutex::new(VadState::from_config(sample_rate, &config)));

    let rec = recording.clone();
    let rec_flag = is_recording.clone();
    let v = vad.clone();
    let event_tx_cb = event_tx.clone();
    let audio_tx_cb = audio_tx.clone();
    let running_cb = running.clone();

    // Per cpal docs, build_input_stream on an output device gives loopback.
    let stream = device
        .build_input_stream(
            &stream_config.into(),
            move |data: &[f32], _: &cpal::InputCallbackInfo| {
                if !running_cb.load(Ordering::Relaxed) {
                    return;
                }
                let mut vad_lock = v.lock().unwrap();
                vad_lock.process_samples(data);
                let speech = vad_lock.is_speech_active();
                let recording_now = rec_flag.load(Ordering::Relaxed);

                if speech && !recording_now {
                    rec_flag.store(true, Ordering::Relaxed);
                    let mut r = rec.lock().unwrap();
                    r.clear();
                    r.extend_from_slice(data);
                    let _ = event_tx_cb.try_send(VoiceEvent::SpeechStart);
                } else if recording_now {
                    rec.lock().unwrap().extend_from_slice(data);
                    if !speech && vad_lock.turn_ended() {
                        rec_flag.store(false, Ordering::Relaxed);
                        let audio: Vec<f32> = std::mem::take(&mut *rec.lock().unwrap());
                        drop(vad_lock);
                        let duration_ms = if sample_rate > 0 {
                            (audio.len() as u64 * 1000) / sample_rate as u64
                        } else {
                            0
                        };
                        let _ = event_tx_cb.try_send(VoiceEvent::SpeechEnd);
                        if duration_ms >= MIN_SEGMENT_MS {
                            if let Err(e) = audio_tx_cb.try_send(AudioSegment {
                                samples: audio,
                                sample_rate,
                                duration_ms,
                            }) {
                                tracing::error!(
                                    "[wasapi-loopback] dropped {}ms segment: {}",
                                    duration_ms,
                                    e
                                );
                            }
                        }
                    }
                }
            },
            |err| tracing::error!("[wasapi-loopback] stream error: {err}"),
            None,
        )
        .map_err(|e| VoiceError::Device(format!("build loopback stream: {e}")))?;

    stream
        .play()
        .map_err(|e| VoiceError::Device(format!("play loopback stream: {e}")))?;
    while running.load(Ordering::Relaxed) {
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    drop(stream);
    Ok(())
}
