use anyhow::{Context, Result};
#[cfg(coverage)]
use lesavka_server::uvc_contract;
#[cfg(coverage)]
use std::env;
#[cfg(coverage)]
use std::fs::OpenOptions;
#[cfg(coverage)]
use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};

#[cfg(coverage)]
const STREAM_CTRL_SIZE_11: usize = 26;
#[cfg(coverage)]
const STREAM_CTRL_SIZE_15: usize = 34;
#[cfg(coverage)]
const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15;
#[cfg(coverage)]
const UVC_DATA_SIZE: usize = 60;

#[cfg(coverage)]
const UVC_STRING_CONTROL_IDX: u8 = 0;
#[cfg(coverage)]
const UVC_STRING_STREAMING_IDX: u8 = 1;
#[cfg(coverage)]
const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0";

#[cfg(coverage)]
const USB_DIR_IN: u8 = 0x80;

#[cfg(coverage)]
const UVC_SET_CUR: u8 = 0x01;
#[cfg(coverage)]
const UVC_GET_CUR: u8 = 0x81;
#[cfg(coverage)]
const UVC_GET_MIN: u8 = 0x82;
#[cfg(coverage)]
const UVC_GET_MAX: u8 = 0x83;
#[cfg(coverage)]
const UVC_GET_RES: u8 = 0x84;
#[cfg(coverage)]
const UVC_GET_LEN: u8 = 0x85;
#[cfg(coverage)]
const UVC_GET_INFO: u8 = 0x86;
#[cfg(coverage)]
const UVC_GET_DEF: u8 = 0x87;

#[cfg(coverage)]
const UVC_VS_PROBE_CONTROL: u8 = 0x01;
#[cfg(coverage)]
const UVC_VS_COMMIT_CONTROL: u8 = 0x02;
#[cfg(coverage)]
const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02;

#[cfg(coverage)]
// Keep coverage-mode defaults aligned with the real UVC helper.
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
#[cfg(coverage)]
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
#[cfg(coverage)]
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
#[cfg(coverage)]
const DEFAULT_UVC_QUEUE_PACING: bool = false;
#[cfg(coverage)]
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
#[cfg(coverage)]
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
#[cfg(coverage)]
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
#[cfg(coverage)]
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
#[cfg(coverage)]
const DEFAULT_UVC_IDLE_AFTER_STALE_MS: u64 = 2_000;
#[cfg(coverage)]
const DEFAULT_UVC_KERNEL_STATS_PATH: &str = "/run/lesavka-uvc-kernel-stats.json";
#[cfg(coverage)]
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
#[cfg(coverage)]
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
#[cfg(coverage)]
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("idle_1280x720_black.jpg");

#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2Event {
    _bytes: [u8; 64],
}

#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct UsbCtrlRequest {
    b_request_type: u8,
    b_request: u8,
    w_value: u16,
    w_index: u16,
    w_length: u16,
}

#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct UvcRequestData {
    length: i32,
    data: [u8; UVC_DATA_SIZE],
}

#[cfg(coverage)]
#[derive(Clone, Copy)]
struct UvcConfig {
    width: u32,
    height: u32,
    fps: u32,
    interval: u32,
    max_packet: u32,
    frame_size: u32,
    bulk: bool,
}

#[cfg(coverage)]
struct PayloadCap {
    limit: u32,
    pct: u32,
    source: &'static str,
    periodic_dw: Option<u32>,
    non_periodic_dw: Option<u32>,
}

#[cfg(coverage)]
struct UvcState {
    cfg: UvcConfig,
    ctrl_len: usize,
    default: [u8; STREAM_CTRL_SIZE_MAX],
    probe: [u8; STREAM_CTRL_SIZE_MAX],
    commit: [u8; STREAM_CTRL_SIZE_MAX],
    cfg_snapshot: Option<ConfigfsSnapshot>,
    commit_observed: bool,
}

#[cfg(coverage)]
#[derive(Clone, Copy)]
struct PendingRequest {
    interface: u8,
    selector: u8,
    expected_len: usize,
}

#[cfg(coverage)]
#[derive(Clone, Copy)]
struct UvcInterfaces {
    control: u8,
    streaming: u8,
}

#[cfg(coverage)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ConfigfsSnapshot {
    width: u32,
    height: u32,
    default_interval: u32,
    frame_interval: u32,
    frame_size: u32,
    maxpacket: u32,
    maxburst: u32,
    streaming_bulk: Option<u32>,
}

#[cfg(coverage)]
struct MmapBuffer {
    ptr: *mut u8,
    len: usize,
}

#[cfg(coverage)]
struct UvcVideoStream {
    buffers: Vec<MmapBuffer>,
    frame_path: std::path::PathBuf,
    latest_frame: Vec<u8>,
    stats: UvcVideoStats,
    frame_max_bytes: usize,
    frame_period: Option<std::time::Duration>,
    next_queue_at: Option<std::time::Instant>,
    transport_bulk: bool,
    max_packet: u32,
    has_verified_frame: bool,
    last_verified_mtime: Option<std::time::SystemTime>,
}

#[cfg(coverage)]
#[derive(Default)]
struct UvcVideoStats {
    queued: u64,
    reloaded: u64,
    replayed_stale: u64,
    rejected_oversize: u64,
    rejected_invalid: u64,
    fallback_idle: u64,
    held_last_good: u64,
    read_errors: u64,
    strict_validation_failures: u64,
    dqbuf_ioctl_errors: u64,
    dqbuf_flag_errors: u64,
    qbuf_ioctl_errors: u64,
    latest_bytes: usize,
    last_rejected_oversize_bytes: usize,
    last_rejected_oversize_cap: usize,
    paced_sleeps: u64,
    paced_sleep_ms: u64,
    last_report: Option<std::time::Instant>,
}

#[cfg(coverage)]
impl UvcVideoStream {
    /// Builds an inert coverage-mode stream seeded with the idle JPEG frame.
    ///
    /// Inputs: the already-open UVC file descriptor, currently unused in this
    /// harness. Output: stream state that mirrors production defaults. Why:
    /// coverage tests need the stream bookkeeping without touching hardware.
    fn new(_fd: i32) -> Self {
        Self {
            buffers: Vec::new(),
            frame_path: frame_spool_path(),
            latest_frame: IDLE_MJPEG_FRAME.to_vec(),
            stats: UvcVideoStats::default(),
            frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
            frame_period: None,
            next_queue_at: None,
            transport_bulk: false,
            max_packet: 0,
            has_verified_frame: false,
            last_verified_mtime: None,
        }
    }

    /// Reloads the latest spooled MJPEG frame when it is fresh and safe to use.
    ///
    /// Inputs: the configured frame path and current buffer limits. Output:
    /// updated cached frame plus rejection counters. Why: stale or oversized
    /// frames should degrade to known-safe idle content instead of freezing RCT.
    fn refresh_latest_frame(&mut self) {
        let stale = frame_spool_is_stale(&self.frame_path, frame_spool_max_age());
        if stale {
            self.stats.replayed_stale += 1;
            self.hold_last_good_or_idle();
            return;
        }
        let max_frame_bytes = self.frame_payload_limit();
        if let Ok(frame) = std::fs::read(&self.frame_path) {
            if frame.len() <= max_frame_bytes && looks_like_mjpeg_frame(&frame) {
                self.stats.reloaded += 1;
                self.stats.latest_bytes = frame.len();
                self.latest_frame = frame;
                self.has_verified_frame = true;
                self.last_verified_mtime = frame_spool_modified(&self.frame_path);
            } else {
                if frame.len() > max_frame_bytes && looks_like_mjpeg_frame(&frame) {
                    self.stats.rejected_oversize += 1;
                    self.stats.last_rejected_oversize_bytes = frame.len();
                    self.stats.last_rejected_oversize_cap = max_frame_bytes;
                } else {
                    self.stats.rejected_invalid += 1;
                }
                self.hold_last_good_or_idle();
            }
        } else {
            self.stats.read_errors += 1;
            self.hold_last_good_or_idle();
        }
        if !looks_like_mjpeg_frame(&self.latest_frame) {
            self.replace_latest_with_idle();
        }
    }

    fn hold_last_good_or_idle(&mut self) {
        if self.has_verified_frame && !verified_frame_idle_timeout_elapsed(self.last_verified_mtime) {
            self.stats.held_last_good += 1;
        } else {
            self.replace_latest_with_idle();
        }
    }

    fn replace_latest_with_idle(&mut self) {
        if self.latest_frame.as_slice() != IDLE_MJPEG_FRAME {
            self.stats.fallback_idle += 1;
        }
        self.latest_frame = IDLE_MJPEG_FRAME.to_vec();
    }

    fn frame_payload_limit(&self) -> usize {
        self.buffers
            .iter()
            .map(|buffer| buffer.len)
            .min()
            .unwrap_or(MAX_MJPEG_FRAME_BYTES)
            .min(self.frame_max_bytes)
    }

    /// Chooses the safest frame payload for the active UVC buffer.
    ///
    /// Inputs: one buffer length. Output: current, idle, or minimal JPEG bytes.
    /// Why: the gadget must always queue valid MJPEG, even with tiny buffers.
    fn frame_for_buffer(&self, buffer_len: usize) -> &[u8] {
        if self.latest_frame.len() <= buffer_len && looks_like_mjpeg_frame(&self.latest_frame) {
            &self.latest_frame
        } else if IDLE_MJPEG_FRAME.len() <= buffer_len {
            IDLE_MJPEG_FRAME
        } else {
            MINIMAL_MJPEG_FRAME
        }
    }
}

#[cfg(coverage)]
fn frame_spool_path() -> std::path::PathBuf {
    env::var("LESAVKA_UVC_FRAME_PATH")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg"))
}

#[cfg(coverage)]
fn looks_like_mjpeg_frame(frame: &[u8]) -> bool {
    frame.len() > MINIMAL_MJPEG_FRAME.len()
        && frame.starts_with(&[0xff, 0xd8])
        && frame.ends_with(&[0xff, 0xd9])
}
