
use anyhow::{Context, anyhow};
use futures_util::Stream;
use gst::ElementFactory;
use gst::MessageView::*;
use gst::prelude::*;
use gstreamer as gst;
use gstreamer_app as gst_app;
use std::fs;
use std::sync::{
    Arc, Mutex, OnceLock,
    atomic::{AtomicU64, Ordering},
};
use std::time::{Duration, Instant};
use tokio_stream::wrappers::ReceiverStream;
use tonic::Status;
use tracing::{debug, error, info, warn};

use lesavka_common::lesavka::AudioPacket;

#[path = "ear_capture/source_watchdog.rs"]
mod source_watchdog;
#[cfg(not(coverage))]
use source_watchdog::{AudioSourceHealth, spawn_audio_source_watchdog};

/// “Speaker” stream coming **from** the remote host (UAC2‑gadget playback
/// endpoint) **towards** the client.
pub struct AudioStream {
    _pipeline: gst::Pipeline,
    inner: ReceiverStream<Result<AudioPacket, Status>>,
    active_generation: Option<u64>,
}

impl Stream for AudioStream {
    type Item = Result<AudioPacket, Status>;
    fn poll_next(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Option<Self::Item>> {
        std::pin::Pin::new(&mut self.inner).poll_next(cx)
    }
}

impl Drop for AudioStream {
    fn drop(&mut self) {
        let _ = self._pipeline.set_state(gst::State::Null);
        clear_active_audio_capture(self.active_generation);
    }
}

#[cfg(not(coverage))]
static NEXT_AUDIO_CAPTURE_GENERATION: AtomicU64 = AtomicU64::new(1);

#[cfg(not(coverage))]
static ACTIVE_AUDIO_CAPTURE: OnceLock<Mutex<Option<(u64, gst::Pipeline)>>> = OnceLock::new();

#[cfg(not(coverage))]
fn active_audio_capture() -> &'static Mutex<Option<(u64, gst::Pipeline)>> {
    ACTIVE_AUDIO_CAPTURE.get_or_init(|| Mutex::new(None))
}

#[cfg(not(coverage))]
fn next_audio_capture_generation() -> u64 {
    NEXT_AUDIO_CAPTURE_GENERATION.fetch_add(1, Ordering::Relaxed)
}

#[cfg(not(coverage))]
fn retire_active_audio_capture(reason: &'static str) {
    let Ok(mut active) = active_audio_capture().lock() else {
        return;
    };
    if let Some((generation, pipeline)) = active.take() {
        warn!(generation, reason, "🔊 retiring previous downstream audio capture");
        let _ = pipeline.set_state(gst::State::Null);
    }
}

#[cfg(not(coverage))]
fn remember_active_audio_capture(generation: u64, pipeline: &gst::Pipeline) {
    let Ok(mut active) = active_audio_capture().lock() else {
        return;
    };
    if let Some((previous_generation, previous_pipeline)) =
        active.replace((generation, pipeline.clone()))
        && previous_generation != generation
    {
        warn!(
            previous_generation,
            generation, "🔊 replacing overlapping downstream audio capture"
        );
        let _ = previous_pipeline.set_state(gst::State::Null);
    }
}

#[cfg(not(coverage))]
fn clear_active_audio_capture(generation: Option<u64>) {
    let Some(generation) = generation else {
        return;
    };
    let Ok(mut active) = active_audio_capture().lock() else {
        return;
    };
    if active
        .as_ref()
        .is_some_and(|(active_generation, _)| *active_generation == generation)
    {
        active.take();
    }
}

#[cfg(coverage)]
fn clear_active_audio_capture(_generation: Option<u64>) {}

/// Start a GStreamer pipeline and reset it to NULL if startup fails.
#[cfg(not(coverage))]
pub(crate) fn start_pipeline_or_reset(
    pipeline: &gst::Pipeline,
    context: &'static str,
) -> anyhow::Result<()> {
    match pipeline.set_state(gst::State::Playing) {
        Ok(_) => Ok(()),
        Err(error) => {
            let details = pipeline_start_failure_details(pipeline);
            let _ = pipeline.set_state(gst::State::Null);
            Err(anyhow!("{error}{details}")).context(context)
        }
    }
}

#[cfg(not(coverage))]
fn pipeline_start_failure_details(pipeline: &gst::Pipeline) -> String {
    let Some(bus) = pipeline.bus() else {
        return String::new();
    };
    let mut details = Vec::new();
    let deadline = Instant::now() + Duration::from_millis(200);
    while Instant::now() < deadline && details.len() < 3 {
        let Some(msg) = bus.timed_pop(gst::ClockTime::from_mseconds(20)) else {
            continue;
        };
        match msg.view() {
            Error(error) => details.push(format!(
                "error from {:?}: {} ({})",
                msg.src().map(gst::prelude::GstObjectExt::path_string),
                error.error(),
                error.debug().unwrap_or_default()
            )),
            Warning(warning) => details.push(format!(
                "warning from {:?}: {} ({})",
                msg.src().map(gst::prelude::GstObjectExt::path_string),
                warning.error(),
                warning.debug().unwrap_or_default()
            )),
            _ => {}
        }
    }
    if details.is_empty() {
        String::new()
    } else {
        format!("; {}", details.join("; "))
    }
}

/// Start a coverage pipeline with a deterministic forced-failure hook.
#[cfg(coverage)]
pub(crate) fn start_pipeline_or_reset(
    pipeline: &gst::Pipeline,
    context: &'static str,
) -> anyhow::Result<()> {
    if std::env::var("LESAVKA_TEST_FORCE_PIPELINE_START_ERROR").is_ok() {
        let _ = pipeline.set_state(gst::State::Null);
        return Err(anyhow!("{context}: forced test failure"));
    }
    pipeline
        .set_state(gst::State::Playing)
        .map(|_| ())
        .context(context)
}

#[cfg(not(coverage))]
fn spawn_pipeline_bus_logger(bus: gst::Bus, label: &'static str, playing_message: &'static str) {
    std::thread::spawn(move || {
        for msg in bus.iter_timed(gst::ClockTime::NONE) {
            match msg.view() {
                Error(e) => error!(
                    "💥 {label} pipeline from {:?}: {} ({})",
                    msg.src().map(gst::prelude::GstObjectExt::path_string),
                    e.error(),
                    e.debug().unwrap_or_default()
                ),
                Warning(w) => warn!(
                    "⚠️  {label} pipeline from {:?}: {} ({})",
                    msg.src().map(gst::prelude::GstObjectExt::path_string),
                    w.error(),
                    w.debug().unwrap_or_default()
                ),
                StateChanged(s)
                    if s.current() == gst::State::Playing
                        && msg.src().map(|s| s.is::<gst::Pipeline>()).unwrap_or(false) =>
                {
                    debug!("{playing_message}")
                }
                Element(e) => {
                    if let Some(structure) = e.structure() {
                        if structure.name() == "level" {
                            let source = msg
                                .src()
                                .map(gst::prelude::GstObjectExt::path_string)
                                .unwrap_or_else(|| "<unknown>".into());
                            info!("🔊 {label} audio level src={source} {}", structure);
                            if label == "audio" && level_message_looks_like_digital_silence(structure) {
                                log_remote_speaker_silence_hint();
                            }
                        } else {
                            debug!("🔎 audio element message: {}", structure);
                        }
                    }
                }
                _ => {}
            }
        }
    });
}

#[cfg(not(coverage))]
fn level_message_looks_like_digital_silence(structure: &gst::StructureRef) -> bool {
    let text = structure.to_string();
    text.contains("-699.") && text.contains("-349.")
}

#[cfg(not(coverage))]
fn log_remote_speaker_silence_hint() {
    static SILENCE_MESSAGES: AtomicU64 = AtomicU64::new(0);
    let count = SILENCE_MESSAGES.fetch_add(1, Ordering::Relaxed) + 1;
    if count <= 3 || count.is_multiple_of(30) {
        warn!(
            count,
            "🔇 downstream UAC speaker capture is digital silence; Lesavka audio path is open, but the RCT/host is not currently feeding audible audio into the USB speaker endpoint"
        );
    }
}

/*───────────────────────────────────────────────────────────────────────────*/
/*  ear() - capture from ALSA (“speaker”) and push AAC AUs via gRPC          */
/*───────────────────────────────────────────────────────────────────────────*/

#[cfg(coverage)]
/// Build a deterministic remote speaker stream without requiring ALSA hardware.
pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
    let _ = id;
    if alsa_dev.contains('"') {
        return Err(anyhow!("invalid ALSA device string"));
    }
    if alsa_dev.contains("UAC2Gadget") || alsa_dev.contains("DefinitelyMissing") {
        return Err(anyhow!("ALSA source not available"));
    }

    let _ = gst::init();
    let pipeline = gst::Pipeline::new();
    let (_tx, rx) = tokio::sync::mpsc::channel(1);
    Ok(AudioStream {
        _pipeline: pipeline,
        inner: ReceiverStream::new(rx),
        active_generation: None,
    })
}

#[cfg(not(coverage))]
/// Capture remote speaker audio from the UAC ALSA source and stream AAC packets.
pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
    //   NB: one *logical* speaker → id==0.  A 2nd logical stream could be
    //   added later (for multi‑channel) without changing the client.
    gst::init().context("gst init")?;
    ensure_remote_usb_audio_ready(alsa_dev)?;
    let active_generation = next_audio_capture_generation();
    retire_active_audio_capture("new downstream audio stream requested");

    /*──────────── pipeline description ────────────
     *
     *    ALSA (UAC2 gadget)       AAC+ADTS            AppSink
     *   ┌───────────┐ raw 48 kHz ┌─────────┐ AU/ADTS ┌──────────┐
     *   │  alsasrc  │────────────► voaacenc │────────► appsink   │
     *   └───────────┘            └─────────┘         └──────────┘
     */
    let desc = build_pipeline_desc(alsa_dev)?;

    let pipeline: gst::Pipeline = gst::parse::launch(&desc)?.downcast().expect("pipeline");

    let sink: gst_app::AppSink = pipeline
        .by_name("asink")
        .expect("asink")
        .downcast()
        .expect("appsink");

    let tap = Arc::new(Mutex::new(ClipTap::new(
        "🎧 - ear",
        Duration::from_secs(60),
    )));
    // sink.connect("underrun", false, |_| {
    //     tracing::warn!("⚠️  USB playback underrun – host muted or not reading");
    //     None
    // });

    let (tx, rx) = tokio::sync::mpsc::channel(8192);
    let source_health = Arc::new(AudioSourceHealth::new());

    let bus = pipeline.bus().expect("bus");

    /*──────────── callbacks ────────────*/
    sink.set_callbacks(
        gst_app::AppSinkCallbacks::builder()
            .new_sample({
                let tap = tap.clone();
                let source_health = source_health.clone();
                let tx = tx.clone();
                move |s| {
                    if source_health.is_closed() {
                        return Err(gst::FlowError::Flushing);
                    }
                    let sample = s.pull_sample().map_err(|_| gst::FlowError::Eos)?;
                    let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
                    let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
                    source_health.mark_sample();

                    // -------- clip‑tap (minute dumps) ------------
                    tap.lock().unwrap().feed(map.as_slice());

                    static CNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
                    let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                    if n < 10 || n.is_multiple_of(300) {
                        debug!("🎧 ear #{n}: {} bytes", map.len());
                    }

                    let pts_us = buffer.pts().unwrap_or(gst::ClockTime::ZERO).nseconds() / 1_000;

                    // push non‑blocking; drop oldest on overflow
                    if tx
                        .try_send(Ok(AudioPacket {
                            id,
                            pts: pts_us,
                            data: map.as_slice().to_vec(),
                            ..Default::default()
                        }))
                        .is_err()
                    {
                        static DROPS: std::sync::atomic::AtomicU64 =
                            std::sync::atomic::AtomicU64::new(0);
                        let d = DROPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                        if d.is_multiple_of(300) {
                            warn!("🎧💔 dropped {d} audio AUs (client too slow)");
                        }
                    }
                    Ok(gst::FlowSuccess::Ok)
                }
            })
            .build(),
    );

    start_pipeline_or_reset(&pipeline, "starting audio pipeline")?;
    remember_active_audio_capture(active_generation, &pipeline);
    spawn_pipeline_bus_logger(bus, "audio", "🎶 audio pipeline PLAYING");

    spawn_audio_source_watchdog(
        pipeline.clone(),
        source_health,
        tx.clone(),
        alsa_dev.to_string(),
    );

    Ok(AudioStream {
        _pipeline: pipeline,
        inner: ReceiverStream::new(rx),
        active_generation: Some(active_generation),
    })
}

/*────────────────────────── build_pipeline_desc ───────────────────────────*/
#[cfg(not(coverage))]
fn build_pipeline_desc(dev: &str) -> anyhow::Result<String> {
    let reg = gst::Registry::get();

    // first available encoder
    let enc = ["fdkaacenc", "voaacenc", "avenc_aac"]
        .into_iter()
        .find(|&e| {
            reg.find_plugin(e).is_some()
                || reg.find_feature(e, ElementFactory::static_type()).is_some()
        })
        .ok_or_else(|| anyhow!("no AAC encoder plugin available"))?;

    Ok(format!(
        concat!(
            "alsasrc device=\"{dev}\" do-timestamp=true ! ",
            "audio/x-raw,format=S16LE,channels=2,rate=48000 ! ",
            "level name=source_level interval=1000000000 message=true ! ",
            "audioconvert ! audioresample ! {enc} bitrate=192000 ! ",
            "aacparse ! ",
            "capsfilter caps=audio/mpeg,stream-format=adts,channels=2,rate=48000 ! ",
            "tee name=t ",
            "t. ! queue ! appsink name=asink emit-signals=true ",
            "t. ! queue ! appsink name=debugtap emit-signals=true max-buffers=500 drop=true"
        ),
        dev = dev,
        enc = enc
    ))
}

#[cfg(not(coverage))]
fn ensure_remote_usb_audio_ready(alsa_dev: &str) -> anyhow::Result<()> {
    if !alsa_dev_uses_remote_uac_gadget(alsa_dev) {
        return Ok(());
    }

    let Some((controller, state)) = current_usb_gadget_state()? else {
        return Ok(());
    };
    if state == "not attached" {
        return Err(anyhow!(
            "remote USB gadget is not attached (UDC {controller} state={state}); remote speaker audio cannot stream until the controlled PC enumerates Lesavka USB"
        ));
    }
    Ok(())
}

#[cfg(not(coverage))]
fn alsa_dev_uses_remote_uac_gadget(alsa_dev: &str) -> bool {
    matches!(alsa_dev, "hw:UAC2Gadget,0" | "hw:UAC2_Gadget,0")
        || alsa_dev.contains("UAC2Gadget")
        || alsa_dev.contains("UAC2_Gadget")
}

#[cfg(not(coverage))]
fn current_usb_gadget_state() -> anyhow::Result<Option<(String, String)>> {
    let configfs_root = std::env::var("LESAVKA_GADGET_CONFIGFS_ROOT")
        .unwrap_or_else(|_| "/sys/kernel/config/usb_gadget".to_string());
    let sysfs_root = std::env::var("LESAVKA_GADGET_SYSFS_ROOT").unwrap_or_else(|_| "/sys".into());
    let udc = fs::read_to_string(format!("{configfs_root}/lesavka/UDC"))
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty());
    let Some(controller) = udc else {
        return Ok(None);
    };
    let state = fs::read_to_string(format!("{sysfs_root}/class/udc/{controller}/state"))
        .with_context(|| format!("reading UDC state for {controller}"))?
        .trim()
        .to_string();
    Ok(Some((controller, state)))
}

// ────────────────────── minute‑clip helper ───────────────────────────────
