use anyhow::Context;
use gstreamer as gst;
use gstreamer::prelude::*;
use gstreamer_app as gst_app;
use std::sync::atomic::{AtomicBool, AtomicU64};

use super::hevc_mjpeg_guard;
use super::mjpeg_spool::{
    mjpeg_spool_enabled, mjpeg_spool_frame_max_bytes, mjpeg_spool_path,
};
use super::*;
use crate::camera::{CameraCodec, CameraConfig};
use crate::video_support::require_h264_decoder;

impl WebcamSink {
    /// Build a new webcam sink pipeline.
    ///
    /// Inputs: the target UVC device plus the selected camera profile.
    /// Outputs: a sink ready to receive `VideoPacket`s.
    /// Why: UVC output has its own caps and decoder chain that differs from the
    /// HDMI sink, so it lives in a dedicated constructor.
    #[cfg(coverage)]
    pub fn new(_uvc_dev: &str, cfg: &CameraConfig) -> anyhow::Result<Self> {
        gst::init()?;

        let pipeline = gst::Pipeline::new();
        let clock_align_enabled = uvc_sink_session_clock_align_enabled();
        let src = gst::ElementFactory::make("appsrc")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(true);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", false);
        configure_uvc_appsrc(&src);

        let sink = gst::ElementFactory::make("fakesink")
            .build()
            .context("building fakesink")?;
        if clock_align_enabled {
            crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
            crate::media_timing::enable_sink_clock_sync(&sink);
        }
        pipeline.add_many([src.upcast_ref(), &sink])?;
        gst::Element::link_many([src.upcast_ref(), &sink])?;
        pipeline.set_state(gst::State::Playing)?;

        let frame_step_us = (1_000_000u64 / u64::from(cfg.fps.max(1))).max(1);
        Ok(Self {
            appsrc: src,
            pipe: pipeline,
            clock_aligned: AtomicBool::new(!clock_align_enabled),
            next_pts_us: AtomicU64::new(0),
            frame_step_us,
            mjpeg_spool_path: None,
            direct_mjpeg_appsrc: None,
            normalized_mjpeg_sink: None,
            hevc_mjpeg_appsrc: None,
            decoded_mjpeg_sink: None,
            last_mjpeg_passthrough_bytes: AtomicU64::new(0),
            direct_mjpeg_max_bytes: mjpeg_spool_frame_max_bytes(cfg.fps),
            uvc_width: cfg.width.min(u32::from(u16::MAX)) as u16,
            uvc_height: cfg.height.min(u32::from(u16::MAX)) as u16,
            uvc_fps: cfg.fps.max(1),
            direct_mjpeg_profile_mismatch_seen: AtomicBool::new(false),
            unexpected_mjpeg_in_hevc_seen: AtomicBool::new(false),
            last_decoded_mjpeg_bytes: AtomicU64::new(0),
            direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
            normalized_mjpeg_miss_count: AtomicU64::new(0),
            normalized_mjpeg_memory_check_count: AtomicU64::new(0),
            decoded_mjpeg_miss_count: AtomicU64::new(0),
            decode_recovery_needs_irap: AtomicBool::new(false),
        })
    }

    #[cfg(not(coverage))]
    pub fn new(uvc_dev: &str, cfg: &CameraConfig) -> anyhow::Result<Self> {
        gst::init()?;

        let pipeline = gst::Pipeline::new();
        let clock_align_enabled = uvc_sink_session_clock_align_enabled();

        let width = cfg.width as i32;
        let height = cfg.height as i32;
        let fps = cfg.fps.max(1) as i32;
        let use_mjpeg = matches!(cfg.codec, CameraCodec::Mjpeg);
        let use_hevc = matches!(cfg.codec, CameraCodec::Hevc);

        let src = gst::ElementFactory::make("appsrc")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(true);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", false);
        configure_uvc_appsrc(&src);
        if clock_align_enabled {
            crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
        }

        let mut mjpeg_spool_file = None;
        let mut direct_mjpeg_appsrc = None;
        let mut normalized_mjpeg_sink = None;
        let mut hevc_mjpeg_appsrc = None;
        let mut decoded_mjpeg_sink = None;

        if use_mjpeg && mjpeg_spool_enabled() {
            let caps_mjpeg = gst::Caps::builder("image/jpeg")
                .field("parsed", true)
                .field("width", width)
                .field("height", height)
                .field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
                .build();
            src.set_caps(Some(&caps_mjpeg));

            let sink = gst::ElementFactory::make("fakesink")
                .build()
                .context("building fakesink for MJPEG UVC spool")?;
            if clock_align_enabled {
                crate::media_timing::enable_sink_clock_sync(&sink);
            } else if sink.has_property("sync", None) {
                sink.set_property("sync", false);
            }
            pipeline.add_many([src.upcast_ref(), &sink])?;
            gst::Element::link_many([src.upcast_ref(), &sink])?;
            mjpeg_spool_file = Some(mjpeg_spool_path());
            if hevc_mjpeg_guard::direct_mjpeg_normalize_enabled() {
                match build_direct_mjpeg_normalize_branch(&pipeline, width, height, fps) {
                    Ok((normalize_src, normalize_sink)) => {
                        direct_mjpeg_appsrc = Some(normalize_src);
                        normalized_mjpeg_sink = Some(normalize_sink);
                        tracing::info!(
                            target: "lesavka_server::video",
                            quality = hevc_mjpeg_guard::direct_mjpeg_jpeg_quality(),
                            pull_timeout_ms = hevc_mjpeg_guard::direct_mjpeg_normalize_pull_timeout_ms(),
                            "direct MJPEG UVC spool will normalize frames through jpegdec/jpegenc"
                        );
                    }
                    Err(err) => {
                        tracing::warn!(
                            target: "lesavka_server::video",
                            %err,
                            "direct MJPEG normalization unavailable; falling back to guarded passthrough"
                        );
                    }
                }
            }
            match add_hevc_mjpeg_spool_branch(&pipeline, width, height, fps) {
                Ok((hevc_src, hevc_sink)) => {
                    hevc_mjpeg_appsrc = Some(hevc_src);
                    decoded_mjpeg_sink = Some(hevc_sink);
                    tracing::info!(
                        target: "lesavka_server::video",
                        "MJPEG UVC spool will also accept live HEVC uplink packets"
                    );
                }
                Err(err) => {
                    tracing::warn!(
                        target: "lesavka_server::video",
                        %err,
                        "dynamic HEVC->MJPEG branch unavailable; MJPEG UVC spool will accept MJPEG only"
                    );
                }
            }
        } else if use_mjpeg {
            let caps_mjpeg = gst::Caps::builder("image/jpeg")
                .field("parsed", true)
                .field("width", width)
                .field("height", height)
                .field("framerate", gst::Fraction::new(fps, 1))
                .field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
                .build();
            src.set_caps(Some(&caps_mjpeg));

            let queue = gst::ElementFactory::make("queue").build()?;
            let capsfilter = gst::ElementFactory::make("capsfilter")
                .property("caps", &caps_mjpeg)
                .build()?;
            let sink = gst::ElementFactory::make("v4l2sink")
                .property("device", uvc_dev)
                .build()?;
            sink.set_property_from_str("io-mode", &uvc_mjpeg_v4l2sink_io_mode());
            if clock_align_enabled {
                crate::media_timing::enable_sink_clock_sync(&sink);
            } else if sink.has_property("sync", None) {
                sink.set_property("sync", false);
            }

            pipeline.add_many([src.upcast_ref(), &queue, &capsfilter, &sink])?;
            gst::Element::link_many([src.upcast_ref(), &queue, &capsfilter, &sink])?;
        } else if use_hevc {
            let caps_hevc = gst::Caps::builder("video/x-h265")
                .field("stream-format", "byte-stream")
                .field("alignment", "au")
                .build();
            let caps_mjpeg = gst::Caps::builder("image/jpeg")
                .field("parsed", true)
                .field("width", width)
                .field("height", height)
                .field("framerate", gst::Fraction::new(fps, 1))
                .field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
                .build();
            src.set_caps(Some(&caps_hevc));

            let h265parse = gst::ElementFactory::make("h265parse")
                .property("disable-passthrough", true)
                .property("config-interval", -1i32)
                .build()?;
            let decoder_name = require_hevc_decoder()?;
            let decoder = gst::ElementFactory::make(decoder_name)
                .build()
                .with_context(|| format!("building HEVC decoder element {decoder_name}"))?;
            configure_hevc_decoder(&decoder);
            let decoded_queue = build_hevc_freshness_queue("hevc_mjpeg_decoded_queue")?;
            let convert = gst::ElementFactory::make("videoconvert").build()?;
            let scale = gst::ElementFactory::make("videoscale").build()?;
            let raw_caps = gst::Caps::builder("video/x-raw")
                .field("width", width)
                .field("height", height)
                .build();
            let raw_capsfilter = gst::ElementFactory::make("capsfilter")
                .property("caps", &raw_caps)
                .build()?;
            let encoder = gst::ElementFactory::make("jpegenc")
                .property("quality", hevc_mjpeg_guard::hevc_jpeg_quality() as i32)
                .build()?;
            let jpegparse = gst::ElementFactory::make("jpegparse")
                .name("hevc_mjpeg_output_parse")
                .build()?;
            let caps = gst::ElementFactory::make("capsfilter")
                .property("caps", &caps_mjpeg)
                .build()?;
            let encoded_queue = build_hevc_freshness_queue("hevc_mjpeg_encoded_queue")?;

            tracing::info!(
                target: "lesavka_server::video",
                decoder = decoder_name,
                "HEVC camera uplink will be decoded and emitted as MJPEG/UVC"
            );

            if mjpeg_spool_enabled() {
                if hevc_mjpeg_guard::direct_mjpeg_normalize_enabled() {
                    match build_direct_mjpeg_normalize_branch(&pipeline, width, height, fps) {
                        Ok((normalize_src, normalize_sink)) => {
                            direct_mjpeg_appsrc = Some(normalize_src);
                            normalized_mjpeg_sink = Some(normalize_sink);
                            tracing::info!(
                                target: "lesavka_server::video",
                                quality = hevc_mjpeg_guard::direct_mjpeg_jpeg_quality(),
                                pull_timeout_ms = hevc_mjpeg_guard::direct_mjpeg_normalize_pull_timeout_ms(),
                                "HEVC UVC spool will normalize unexpected direct MJPEG packets"
                            );
                        }
                        Err(err) => {
                            tracing::warn!(
                                target: "lesavka_server::video",
                                %err,
                                "direct MJPEG normalization unavailable for HEVC UVC spool fallback"
                            );
                        }
                    }
                }
                let sink = gst::ElementFactory::make("appsink")
                    .name("hevc_mjpeg_spool_sink")
                    .property("sync", false)
                    .property("enable-last-sample", false)
                    .property("emit-signals", false)
                    .property("max-buffers", 1u32)
                    .property("drop", true)
                    .build()?
                    .downcast::<gst_app::AppSink>()
                    .expect("appsink");
                pipeline.add_many([
                    src.upcast_ref(),
                    &h265parse,
                    &decoder,
                    &decoded_queue,
                    &convert,
                    &scale,
                    &raw_capsfilter,
                    &encoder,
                    &jpegparse,
                    &caps,
                    &encoded_queue,
                    sink.upcast_ref(),
                ])?;
                gst::Element::link_many([
                    src.upcast_ref(),
                    &h265parse,
                    &decoder,
                    &decoded_queue,
                    &convert,
                    &scale,
                    &raw_capsfilter,
                    &encoder,
                    &jpegparse,
                    &caps,
                    &encoded_queue,
                    sink.upcast_ref(),
                ])?;
                mjpeg_spool_file = Some(mjpeg_spool_path());
                decoded_mjpeg_sink = Some(sink);
            } else {
                let sink = gst::ElementFactory::make("v4l2sink")
                    .property("device", uvc_dev)
                    .build()?;
                sink.set_property_from_str("io-mode", &uvc_mjpeg_v4l2sink_io_mode());
                if clock_align_enabled {
                    crate::media_timing::enable_sink_clock_sync(&sink);
                } else if sink.has_property("sync", None) {
                    sink.set_property("sync", false);
                }

                pipeline.add_many([
                    src.upcast_ref(),
                    &h265parse,
                    &decoder,
                    &convert,
                    &scale,
                    &raw_capsfilter,
                    &encoder,
                    &jpegparse,
                    &caps,
                    &sink,
                ])?;
                gst::Element::link_many([
                    src.upcast_ref(),
                    &h265parse,
                    &decoder,
                    &convert,
                    &scale,
                    &raw_capsfilter,
                    &encoder,
                    &jpegparse,
                    &caps,
                    &sink,
                ])?;
            }
        } else {
            let caps_h264 = gst::Caps::builder("video/x-h264")
                .field("stream-format", "byte-stream")
                .field("alignment", "au")
                .build();
            let raw_caps = gst::Caps::builder("video/x-raw")
                .field("format", "YUY2")
                .field("width", width)
                .field("height", height)
                .field("framerate", gst::Fraction::new(fps, 1))
                .build();
            src.set_caps(Some(&caps_h264));

            let h264parse = gst::ElementFactory::make("h264parse").build()?;
            let decoder_name = require_h264_decoder()?;
            let decoder = gst::ElementFactory::make(decoder_name)
                .build()
                .with_context(|| format!("building decoder element {decoder_name}"))?;
            let convert = gst::ElementFactory::make("videoconvert").build()?;
            let scale = gst::ElementFactory::make("videoscale").build()?;
            let caps = gst::ElementFactory::make("capsfilter")
                .property("caps", &raw_caps)
                .build()?;
            let sink = gst::ElementFactory::make("v4l2sink")
                .property("device", uvc_dev)
                .build()?;
            if clock_align_enabled {
                crate::media_timing::enable_sink_clock_sync(&sink);
            } else if sink.has_property("sync", None) {
                sink.set_property("sync", false);
            }

            pipeline.add_many([
                src.upcast_ref(),
                &h264parse,
                &decoder,
                &convert,
                &scale,
                &caps,
                &sink,
            ])?;
            gst::Element::link_many([
                src.upcast_ref(),
                &h264parse,
                &decoder,
                &convert,
                &scale,
                &caps,
                &sink,
            ])?;
        }
        pipeline.set_state(gst::State::Playing)?;
        let bus_watch = spawn_webcam_bus_logger(&pipeline, "uvc-webcam");

        let frame_step_us = (1_000_000u64 / u64::from(cfg.fps.max(1))).max(1);
        Ok(Self {
            appsrc: src,
            pipe: pipeline,
            clock_aligned: AtomicBool::new(!clock_align_enabled),
            next_pts_us: AtomicU64::new(0),
            frame_step_us,
            mjpeg_spool_path: mjpeg_spool_file,
            direct_mjpeg_appsrc,
            normalized_mjpeg_sink,
            hevc_mjpeg_appsrc,
            decoded_mjpeg_sink,
            last_mjpeg_passthrough_bytes: AtomicU64::new(0),
            direct_mjpeg_max_bytes: mjpeg_spool_frame_max_bytes(cfg.fps),
            uvc_width: cfg.width.min(u32::from(u16::MAX)) as u16,
            uvc_height: cfg.height.min(u32::from(u16::MAX)) as u16,
            uvc_fps: cfg.fps.max(1),
            direct_mjpeg_profile_mismatch_seen: AtomicBool::new(false),
            unexpected_mjpeg_in_hevc_seen: AtomicBool::new(false),
            last_decoded_mjpeg_bytes: AtomicU64::new(0),
            direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
            normalized_mjpeg_miss_count: AtomicU64::new(0),
            normalized_mjpeg_memory_check_count: AtomicU64::new(0),
            decoded_mjpeg_miss_count: AtomicU64::new(0),
            decode_recovery_needs_irap: AtomicBool::new(false),
            _bus_watch: bus_watch,
        })
    }
}
