use gstreamer as gst;
use lesavka_common::lesavka::VideoPacket;
use std::path::Path;
use std::sync::atomic::Ordering;
use tracing::warn;

use super::hevc_mjpeg_guard;
#[cfg(not(coverage))]
use super::mjpeg_spool::freshest_mjpeg_sample;
use super::mjpeg_spool::{MjpegSpoolTiming, spool_mjpeg_frame_with_timing};
use super::*;
use crate::video_support::{contains_hevc_irap, reserve_local_pts};
#[cfg(not(coverage))]
use crate::uvc_contract::{add_lesavka_app4_integrity, lesavka_app4_sequence};

impl WebcamSink {
    /// Push one client frame into the UVC pipeline.
    ///
    /// Inputs: the next `VideoPacket` from the gRPC camera stream.
    /// Outputs: none; the frame is forwarded to the appsrc when possible.
    /// Why: UVC sinks use a locally monotonic timeline so presentation remains
    /// stable even when WAN packet timestamps arrive out of order.
    #[cfg(coverage)]
    pub fn push(&self, pkt: VideoPacket) {
        let buf = gst::Buffer::from_slice(pkt.data);
        let _ = self.appsrc.push_buffer(buf);
    }

    #[cfg(not(coverage))]
    pub fn push(&self, pkt: VideoPacket) {
        let packet_is_mjpeg = looks_like_mjpeg_frame(&pkt.data);
        if packet_is_mjpeg && self.decoded_mjpeg_sink.is_some() {
            if !self
                .unexpected_mjpeg_in_hevc_seen
                .swap(true, Ordering::Relaxed)
            {
                warn!(
                    target:"lesavka_server::video",
                    bytes = pkt.data.len(),
                    pts = pkt.pts,
                    "MJPEG packet received while HEVC UVC decoder is active; falling back to guarded direct MJPEG spool to avoid freezing output"
                );
            }
            if let Some(path) = &self.mjpeg_spool_path {
                self.spool_direct_mjpeg_frame(path, &pkt);
            }
            return;
        }

        if let Some(path) = &self.mjpeg_spool_path
            && packet_is_mjpeg
        {
            self.spool_direct_mjpeg_frame(path, &pkt);
            return;
        }

        let hevc_recovery_frame =
            self.decoded_mjpeg_sink.is_some() && contains_hevc_irap(&pkt.data);
        if self.decoded_mjpeg_sink.is_some()
            && self.decode_recovery_needs_irap.load(Ordering::Relaxed)
        {
            if !hevc_recovery_frame {
                return;
            }
            self.decode_recovery_needs_irap
                .store(false, Ordering::Relaxed);
            self.decoded_mjpeg_miss_count.store(0, Ordering::Relaxed);
            tracing::info!(
                target: "lesavka_server::video",
                pts = pkt.pts,
                "HEVC decoded-MJPEG handoff found a recovery keyframe"
            );
        }

        if self.mjpeg_spool_path.is_some()
            && self.decoded_mjpeg_sink.is_none()
            && !looks_like_mjpeg_frame(&pkt.data)
        {
            warn!(
                target:"lesavka_server::video",
                bytes = pkt.data.len(),
                hevc_annex_b = looks_like_annex_b_hevc(&pkt.data),
                "dropping non-MJPEG packet before UVC spool; no dynamic decoder is available"
            );
            return;
        }

        let mut buf = gst::Buffer::from_slice(pkt.data);
        if let Some(meta) = buf.get_mut() {
            let pts_us = reserve_local_pts(&self.next_pts_us, pkt.pts, self.frame_step_us);
            if !self.clock_aligned.swap(true, Ordering::SeqCst) {
                crate::media_timing::align_pipeline_to_session_clock(&self.pipe, pts_us);
            }
            let ts = gst::ClockTime::from_useconds(pts_us);
            meta.set_pts(Some(ts));
            meta.set_dts(Some(ts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)));
        }
        let hevc_appsrc = self.hevc_mjpeg_appsrc.as_ref().unwrap_or(&self.appsrc);
        if let Err(err) = hevc_appsrc.push_buffer(buf) {
            tracing::warn!(target:"lesavka_server::video", %err, "appsrc push failed");
            return;
        }

        if let (Some(path), Some(sink)) = (&self.mjpeg_spool_path, &self.decoded_mjpeg_sink)
            && let Some(sample) = freshest_mjpeg_sample(sink)
            && let Some(buffer) = sample.buffer()
            && let Ok(map) = buffer.map_readable()
        {
            self.decoded_mjpeg_miss_count.store(0, Ordering::Relaxed);
            let decoded_pts_us = buffer.pts().map(|pts| pts.nseconds() / 1_000);
            let timing = MjpegSpoolTiming::hevc_decoded_mjpeg(pkt.pts, decoded_pts_us)
                .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
            let previous_bytes = self.last_decoded_mjpeg_bytes.load(Ordering::Relaxed);
            let decoded_bytes = map.as_slice().len();
            if let Some(reason) =
                hevc_mjpeg_guard::decoded_mjpeg_reject_reason(previous_bytes, map.as_slice())
            {
                let rejected_timing =
                    MjpegSpoolTiming::rejected_hevc_decoded_mjpeg(pkt.pts, decoded_pts_us)
                        .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
                let reason_text = format!("{reason:?}");
                super::mjpeg_spool::audit_rejected_mjpeg_frame(
                    map.as_slice(),
                    rejected_timing,
                    &reason_text,
                );
                warn!(
                    target:"lesavka_server::video",
                    ?reason,
                    previous_bytes,
                    next_bytes = decoded_bytes,
                    "freezing suspicious decoded HEVC->MJPEG frame"
                );
                return;
            }
            if decoded_bytes > self.direct_mjpeg_max_bytes {
                warn!(
                    target:"lesavka_server::video",
                    previous_bytes,
                    next_bytes = decoded_bytes,
                    max_bytes = self.direct_mjpeg_max_bytes,
                    "freezing oversized decoded HEVC->MJPEG frame before UVC spool"
                );
                return;
            }
            if let Err(err) = spool_mjpeg_frame_with_timing(path, map.as_slice(), Some(timing)) {
                warn!(target:"lesavka_server::video", %err, "failed to spool decoded HEVC frame for UVC helper");
            } else {
                self.last_decoded_mjpeg_bytes
                    .store(decoded_bytes as u64, Ordering::Relaxed);
            }
        } else if self.decoded_mjpeg_sink.is_some() {
            let misses = self.decoded_mjpeg_miss_count.fetch_add(1, Ordering::Relaxed) + 1;
            let limit = uvc_hevc_decode_miss_limit();
            if misses >= limit {
                self.decode_recovery_needs_irap
                    .store(true, Ordering::Relaxed);
                self.decoded_mjpeg_miss_count.store(0, Ordering::Relaxed);
                warn!(
                    target: "lesavka_server::video",
                    misses,
                    limit,
                    "HEVC decoded-MJPEG handoff produced no frames; freezing output until the next recovery keyframe"
                );
            }
        }
    }

    #[cfg(not(coverage))]
    fn spool_direct_mjpeg_frame(&self, path: &Path, pkt: &VideoPacket) {
        let inspection = hevc_mjpeg_guard::inspect_mjpeg_frame(&pkt.data);
        if let (Some(width), Some(height)) = (inspection.width, inspection.height)
            && (width, height) != (self.uvc_width, self.uvc_height)
            && !self
                .direct_mjpeg_profile_mismatch_seen
                .swap(true, Ordering::Relaxed)
        {
            warn!(
                target:"lesavka_server::video",
                frame_width = width,
                frame_height = height,
                uvc_width = self.uvc_width,
                uvc_height = self.uvc_height,
                "direct MJPEG frame dimensions differ from the live UVC profile; this can make browser output unstable"
            );
        }
        if !hevc_mjpeg_guard::looks_like_complete_jpeg(&pkt.data) {
            self.reject_direct_mjpeg_frame(pkt, "Incomplete");
            return;
        }

        if self.direct_mjpeg_appsrc.is_some()
            && self.normalized_mjpeg_sink.is_some()
            && !self.direct_mjpeg_normalize_bypassed.load(Ordering::Relaxed)
        {
            self.spool_normalized_direct_mjpeg_frame(path, pkt);
            return;
        }

        self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
    }

    #[cfg(not(coverage))]
    fn spool_guarded_passthrough_direct_mjpeg_frame(&self, path: &Path, pkt: &VideoPacket) {
        let previous_bytes = self.last_mjpeg_passthrough_bytes.load(Ordering::Relaxed);
        let inspection = hevc_mjpeg_guard::inspect_mjpeg_frame(&pkt.data);
        if let Some(reason) = hevc_mjpeg_guard::direct_mjpeg_reject_reason(
            previous_bytes,
            Some(self.direct_mjpeg_max_bytes),
            Some((self.uvc_width, self.uvc_height)),
            &pkt.data,
        ) {
            warn!(
                target:"lesavka_server::video",
                ?reason,
                previous_bytes,
                next_bytes = pkt.data.len(),
                max_bytes = self.direct_mjpeg_max_bytes,
                frame_width = ?inspection.width,
                frame_height = ?inspection.height,
                entropy_bytes = inspection.entropy_bytes,
                entropy_distinct_bytes = inspection.entropy_distinct_bytes,
                entropy_dominant_pct = inspection.entropy_dominant_pct,
                entropy_max_run = inspection.entropy_max_run,
                "freezing suspicious direct MJPEG frame before UVC spool"
            );
            let reason_text = format!("{reason:?}");
            self.reject_direct_mjpeg_frame(pkt, &reason_text);
            return;
        }

        self.spool_passthrough_direct_mjpeg_frame(path, pkt);
    }

    #[cfg(not(coverage))]
    fn reject_direct_mjpeg_frame(&self, pkt: &VideoPacket, reason: &str) {
        let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts)
            .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
        super::mjpeg_spool::audit_rejected_mjpeg_frame(&pkt.data, timing, reason);
    }

    #[cfg(not(coverage))]
    fn spool_passthrough_direct_mjpeg_frame(&self, path: &Path, pkt: &VideoPacket) {
        let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts)
            .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
        if let Err(err) = spool_mjpeg_frame_with_timing(path, &pkt.data, Some(timing)) {
            warn!(target:"lesavka_server::video", %err, "failed to spool MJPEG frame for UVC helper");
        } else {
            self.last_mjpeg_passthrough_bytes
                .store(pkt.data.len() as u64, Ordering::Relaxed);
        }
    }

    #[cfg(not(coverage))]
    fn spool_normalized_direct_mjpeg_frame(&self, path: &Path, pkt: &VideoPacket) {
        let Some(src) = self.direct_mjpeg_appsrc.as_ref() else {
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        };
        let Some(sink) = self.normalized_mjpeg_sink.as_ref() else {
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        };

        if self
            .normalized_mjpeg_memory_check_count
            .fetch_add(1, Ordering::Relaxed)
            .is_multiple_of(150)
            && let Some(limit_kb) = hevc_mjpeg_guard::direct_mjpeg_normalize_rss_limit_kb()
            && let Some(rss_kb) = current_process_rss_kb()
            && rss_kb > limit_kb
        {
            self.direct_mjpeg_normalize_bypassed
                .store(true, Ordering::Relaxed);
            warn!(
                target:"lesavka_server::video",
                rss_kb,
                limit_kb,
                "direct MJPEG normalization disabled because server RSS exceeded its safety limit"
            );
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        }

        let mut buf = gst::Buffer::from_slice(pkt.data.clone());
        if let Some(meta) = buf.get_mut() {
            let ts = gst::ClockTime::from_useconds(pkt.pts);
            meta.set_pts(Some(ts));
            meta.set_dts(Some(ts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)));
        }
        if let Err(err) = src.push_buffer(buf) {
            tracing::warn!(
                target:"lesavka_server::video",
                %err,
                "direct MJPEG normalization appsrc push failed; falling back to guarded passthrough"
            );
            self.direct_mjpeg_normalize_bypassed
                .store(true, Ordering::Relaxed);
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        }

        let Some(sample) = freshest_direct_mjpeg_sample(sink) else {
            let misses = self.normalized_mjpeg_miss_count.fetch_add(1, Ordering::Relaxed) + 1;
            let limit = u64::from(hevc_mjpeg_guard::direct_mjpeg_normalize_miss_limit());
            if misses == 1 || misses.is_multiple_of(30) {
                warn!(
                    target:"lesavka_server::video",
                    misses,
                    limit,
                    "direct MJPEG normalization produced no fresh frame; freezing last good UVC frame until fallback threshold"
                );
            }
            if misses >= limit {
                self.direct_mjpeg_normalize_bypassed
                    .store(true, Ordering::Relaxed);
                warn!(
                    target:"lesavka_server::video",
                    misses,
                    limit,
                    "direct MJPEG normalization starved; falling back to guarded passthrough for this webcam session"
                );
                self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            }
            return;
        };
        let Some(buffer) = sample.buffer() else {
            self.direct_mjpeg_normalize_bypassed
                .store(true, Ordering::Relaxed);
            warn!(
                target:"lesavka_server::video",
                "direct MJPEG normalization returned an empty sample; falling back to guarded passthrough"
            );
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        };
        let Ok(map) = buffer.map_readable() else {
            self.direct_mjpeg_normalize_bypassed
                .store(true, Ordering::Relaxed);
            warn!(
                target:"lesavka_server::video",
                "direct MJPEG normalization returned an unreadable sample; falling back to guarded passthrough"
            );
            self.spool_guarded_passthrough_direct_mjpeg_frame(path, pkt);
            return;
        };
        let normalized_marked = lesavka_app4_sequence(&pkt.data)
            .and_then(|sequence| add_lesavka_app4_integrity(map.as_slice(), sequence));
        let normalized = normalized_marked.as_deref().unwrap_or_else(|| map.as_slice());
        let previous_bytes = self.last_mjpeg_passthrough_bytes.load(Ordering::Relaxed);
        if let Some(reason) = hevc_mjpeg_guard::direct_mjpeg_reject_reason(
            previous_bytes,
            Some(self.direct_mjpeg_max_bytes),
            Some((self.uvc_width, self.uvc_height)),
            normalized,
        ) {
            let inspection = hevc_mjpeg_guard::inspect_mjpeg_frame(normalized);
            let rejected_timing = MjpegSpoolTiming::mjpeg_normalized(pkt.pts)
                .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
            let reason_text = format!("{reason:?}");
            super::mjpeg_spool::audit_rejected_mjpeg_frame(
                normalized,
                rejected_timing,
                &reason_text,
            );
            warn!(
                target:"lesavka_server::video",
                ?reason,
                previous_bytes,
                next_bytes = normalized.len(),
                max_bytes = self.direct_mjpeg_max_bytes,
                frame_width = ?inspection.width,
                frame_height = ?inspection.height,
                entropy_bytes = inspection.entropy_bytes,
                entropy_distinct_bytes = inspection.entropy_distinct_bytes,
                entropy_dominant_pct = inspection.entropy_dominant_pct,
                entropy_max_run = inspection.entropy_max_run,
                "freezing suspicious normalized direct MJPEG frame before UVC spool"
            );
            return;
        }

        let timing = MjpegSpoolTiming::mjpeg_normalized(pkt.pts)
            .with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
        if let Err(err) = spool_mjpeg_frame_with_timing(path, normalized, Some(timing)) {
            warn!(target:"lesavka_server::video", %err, "failed to spool normalized direct MJPEG frame for UVC helper");
        } else {
            self.normalized_mjpeg_miss_count
                .store(0, Ordering::Relaxed);
            self.last_mjpeg_passthrough_bytes
                .store(normalized.len() as u64, Ordering::Relaxed);
        }
    }
}
