// lesavka-uvc - minimal UVC control handler for the gadget node.

use anyhow::{Context, Result};
use lesavka_server::uvc_contract::{self, UvcContractInput, UvcPayloadContract};
use std::collections::VecDeque;
use std::env;
use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, RawFd};
use std::thread;
use std::time::{Duration, Instant, SystemTime};

const STREAM_CTRL_SIZE_11: usize = 26;
const STREAM_CTRL_SIZE_15: usize = 34;
const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15;
const UVC_DATA_SIZE: usize = 60;

const V4L2_EVENT_PRIVATE_START: u32 = 0x0800_0000;
const UVC_EVENT_CONNECT: u32 = V4L2_EVENT_PRIVATE_START;
const UVC_EVENT_DISCONNECT: u32 = V4L2_EVENT_PRIVATE_START + 1;
const UVC_EVENT_STREAMON: u32 = V4L2_EVENT_PRIVATE_START + 2;
const UVC_EVENT_STREAMOFF: u32 = V4L2_EVENT_PRIVATE_START + 3;
const UVC_EVENT_SETUP: u32 = V4L2_EVENT_PRIVATE_START + 4;
const UVC_EVENT_DATA: u32 = V4L2_EVENT_PRIVATE_START + 5;

const UVC_STRING_CONTROL_IDX: u8 = 0;
const UVC_STRING_STREAMING_IDX: u8 = 1;

const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0";

const USB_DIR_IN: u8 = 0x80;

const UVC_SET_CUR: u8 = 0x01;
const UVC_GET_CUR: u8 = 0x81;
const UVC_GET_MIN: u8 = 0x82;
const UVC_GET_MAX: u8 = 0x83;
const UVC_GET_RES: u8 = 0x84;
const UVC_GET_LEN: u8 = 0x85;
const UVC_GET_INFO: u8 = 0x86;
const UVC_GET_DEF: u8 = 0x87;

const UVC_VS_PROBE_CONTROL: u8 = 0x01;
const UVC_VS_COMMIT_CONTROL: u8 = 0x02;
const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02;

const V4L2_BUF_TYPE_VIDEO_OUTPUT: u32 = 2;
const V4L2_MEMORY_MMAP: u32 = 1;
const V4L2_FIELD_NONE: u32 = 1;
const V4L2_PIX_FMT_MJPEG: u32 = u32::from_le_bytes(*b"MJPG");
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("lesavka_uvc/idle_1280x720_black.jpg");
// Keep enough output buffers queued that the USB gadget is not starved while a
// browser is consuming high-speed isochronous MJPEG.
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
const DEFAULT_UVC_QUEUE_PACING: bool = false;
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
const DEFAULT_UVC_IDLE_AFTER_STALE_MS: u64 = 2_000;
const DEFAULT_UVC_HANDOFF_AUDIT_PATH: &str = "/run/lesavka-uvc-handoff.jsonl";
const DEFAULT_UVC_HANDOFF_AUDIT_MAX_RECORDS: usize = 1_024;
const DEFAULT_UVC_KERNEL_STATS_PATH: &str = "/run/lesavka-uvc-kernel-stats.json";
const V4L2_BUF_FLAG_ERROR: u32 = 0x0040;

#[repr(C)]
struct V4l2EventSubscription {
    type_: u32,
    id: u32,
    flags: u32,
    reserved: [u32; 5],
}

#[repr(C)]
union V4l2EventUnion {
    data: [u8; 64],
    _align: u64,
}

#[repr(C)]
struct V4l2Event {
    type_: u32,
    u: V4l2EventUnion,
    pending: u32,
    sequence: u32,
    timestamp: libc::timespec,
    id: u32,
    reserved: [u32; 8],
}

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

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

#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2PixFormat {
    width: u32,
    height: u32,
    pixelformat: u32,
    field: u32,
    bytesperline: u32,
    sizeimage: u32,
    colorspace: u32,
    priv_: u32,
    flags: u32,
    ycbcr_enc: u32,
    quantization: u32,
    xfer_func: u32,
}

#[repr(C)]
union V4l2FormatUnion {
    pix: V4l2PixFormat,
    raw_data: [u8; 200],
}

#[repr(C)]
struct V4l2Format {
    type_: u32,
    // v4l2_format's union is 8-byte aligned on aarch64; this pad keeps
    // our ioctl request size at the kernel's expected 208 bytes.
    _padding: u32,
    fmt: V4l2FormatUnion,
}

#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2RequestBuffers {
    count: u32,
    type_: u32,
    memory: u32,
    capabilities: u32,
    flags: u8,
    reserved: [u8; 3],
}

#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2Timecode {
    type_: u32,
    flags: u32,
    frames: u8,
    seconds: u8,
    minutes: u8,
    hours: u8,
    userbits: [u8; 4],
}

#[repr(C)]
union V4l2BufferM {
    offset: u32,
    userptr: libc::c_ulong,
    planes: *mut libc::c_void,
    fd: i32,
}

#[repr(C)]
union V4l2BufferRequest {
    request_fd: i32,
    reserved: u32,
}

#[repr(C)]
struct V4l2Buffer {
    index: u32,
    type_: u32,
    bytesused: u32,
    flags: u32,
    field: u32,
    timestamp: libc::timeval,
    timecode: V4l2Timecode,
    sequence: u32,
    memory: u32,
    m: V4l2BufferM,
    length: u32,
    reserved2: u32,
    request: V4l2BufferRequest,
}

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

struct PayloadCap {
    limit: u32,
    pct: u32,
    source: &'static str,
    periodic_dw: Option<u32>,
    non_periodic_dw: Option<u32>,
}

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,
}

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

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

#[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>,
}

struct MmapBuffer {
    ptr: *mut u8,
    len: usize,
}

struct UvcVideoStream {
    fd: RawFd,
    buffers: Vec<MmapBuffer>,
    frame_path: std::path::PathBuf,
    latest_frame: Vec<u8>,
    frame_max_bytes: usize,
    frame_period: Option<Duration>,
    next_queue_at: Option<Instant>,
    streaming: bool,
    transport_bulk: bool,
    max_packet: u32,
    has_verified_frame: bool,
    last_verified_mtime: Option<SystemTime>,
    last_spool_mtime_ns: Option<u128>,
    last_refresh_disposition: &'static str,
    handoff_sequence: u64,
    handoff_audit: VecDeque<String>,
    stats: UvcVideoStats,
}

#[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<Instant>,
}

impl UvcVideoStream {
    fn new(fd: RawFd) -> Self {
        Self {
            fd,
            buffers: Vec::new(),
            frame_path: frame_spool_path(),
            latest_frame: IDLE_MJPEG_FRAME.to_vec(),
            frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
            frame_period: None,
            next_queue_at: None,
            streaming: false,
            transport_bulk: false,
            max_packet: 0,
            has_verified_frame: false,
            last_verified_mtime: None,
            last_spool_mtime_ns: None,
            last_refresh_disposition: "idle_startup",
            handoff_sequence: 0,
            handoff_audit: VecDeque::new(),
            stats: UvcVideoStats::default(),
        }
    }

    fn start(&mut self, cfg: UvcConfig) -> Result<()> {
        self.stop();
        self.frame_max_bytes = uvc_frame_max_bytes(cfg);
        self.frame_period = uvc_queue_period(cfg.fps);
        self.next_queue_at = None;
        self.transport_bulk = cfg.bulk;
        self.max_packet = cfg.max_packet;
        self.set_format(cfg)?;
        self.request_buffers(uvc_buffer_count())?;
        for index in 0..self.buffers.len() {
            self.queue_buffer(index as u32, 0)?;
        }
        let req = ioctl_write::<libc::c_int>(b'V', 18);
        let mut type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT as libc::c_int;
        let rc = unsafe { libc::ioctl(self.fd, req, &mut type_) };
        if rc < 0 {
            return Err(std::io::Error::last_os_error()).context("VIDIOC_STREAMON");
        }
        self.streaming = true;
        let transport = if self.transport_bulk {
            "bulk"
        } else {
            "isochronous"
        };
        eprintln!(
            "[lesavka-uvc] video stream started with {} buffers; transport={} max_packet={} frame_cap={} frame_path={}",
            self.buffers.len(),
            transport,
            self.max_packet,
            self.frame_payload_limit(),
            self.frame_path.display()
        );
        Ok(())
    }

    fn stop(&mut self) {
        if self.streaming {
            let req = ioctl_write::<libc::c_int>(b'V', 19);
            let mut type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT as libc::c_int;
            let rc = unsafe { libc::ioctl(self.fd, req, &mut type_) };
            if rc < 0 {
                eprintln!(
                    "[lesavka-uvc] VIDIOC_STREAMOFF failed: {}",
                    std::io::Error::last_os_error()
                );
            }
            self.streaming = false;
        }
        if !self.buffers.is_empty() {
            for buffer in self.buffers.drain(..) {
                unsafe {
                    libc::munmap(buffer.ptr.cast(), buffer.len);
                }
            }
            let _ = self.request_buffers(0);
        }
    }

    fn pump(&mut self) -> Result<()> {
        if !self.streaming {
            return Ok(());
        }
        loop {
            let mut buf = empty_v4l2_buffer();
            let req = ioctl_readwrite::<V4l2Buffer>(b'V', 17);
            let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) };
            if rc < 0 {
                let err = std::io::Error::last_os_error();
                match err.raw_os_error() {
                    Some(libc::EAGAIN) | Some(libc::EINTR) => return Ok(()),
                    _ => {
                        self.stats.dqbuf_ioctl_errors += 1;
                        return Err(err).context("VIDIOC_DQBUF");
                    }
                }
            }
            if buf.flags & V4L2_BUF_FLAG_ERROR != 0 {
                self.stats.dqbuf_flag_errors += 1;
            }
            self.queue_buffer(buf.index, buf.flags)?;
        }
    }

    fn set_format(&self, cfg: UvcConfig) -> Result<()> {
        let mut fmt = V4l2Format {
            type_: V4L2_BUF_TYPE_VIDEO_OUTPUT,
            _padding: 0,
            fmt: V4l2FormatUnion {
                pix: V4l2PixFormat {
                    width: cfg.width,
                    height: cfg.height,
                    pixelformat: V4L2_PIX_FMT_MJPEG,
                    field: V4L2_FIELD_NONE,
                    bytesperline: 0,
                    sizeimage: cfg.frame_size.max(cfg.width * cfg.height),
                    colorspace: 8,
                    priv_: 0,
                    flags: 0,
                    ycbcr_enc: 0,
                    quantization: 0,
                    xfer_func: 0,
                },
            },
        };
        let req = ioctl_readwrite::<V4l2Format>(b'V', 5);
        let rc = unsafe { libc::ioctl(self.fd, req, &mut fmt) };
        if rc < 0 {
            return Err(std::io::Error::last_os_error()).context("VIDIOC_S_FMT");
        }
        Ok(())
    }

    fn request_buffers(&mut self, count: u32) -> Result<()> {
        let mut rb = V4l2RequestBuffers {
            count,
            type_: V4L2_BUF_TYPE_VIDEO_OUTPUT,
            memory: V4L2_MEMORY_MMAP,
            capabilities: 0,
            flags: 0,
            reserved: [0; 3],
        };
        let req = ioctl_readwrite::<V4l2RequestBuffers>(b'V', 8);
        let rc = unsafe { libc::ioctl(self.fd, req, &mut rb) };
        if rc < 0 {
            return Err(std::io::Error::last_os_error()).context("VIDIOC_REQBUFS");
        }
        if count == 0 {
            return Ok(());
        }

        self.buffers.clear();
        for index in 0..rb.count {
            let mut buf = empty_v4l2_buffer();
            buf.index = index;
            let req = ioctl_readwrite::<V4l2Buffer>(b'V', 9);
            let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) };
            if rc < 0 {
                return Err(std::io::Error::last_os_error()).context("VIDIOC_QUERYBUF");
            }
            let offset = unsafe { buf.m.offset };
            let ptr = unsafe {
                libc::mmap(
                    std::ptr::null_mut(),
                    buf.length as usize,
                    libc::PROT_READ | libc::PROT_WRITE,
                    libc::MAP_SHARED,
                    self.fd,
                    offset as libc::off_t,
                )
            };
            if ptr == libc::MAP_FAILED {
                return Err(std::io::Error::last_os_error()).context("mmap UVC buffer");
            }
            self.buffers.push(MmapBuffer {
                ptr: ptr.cast(),
                len: buf.length as usize,
            });
        }
        Ok(())
    }

    fn queue_buffer(&mut self, index: u32, dqbuf_flags: u32) -> Result<()> {
        if self.streaming {
            self.pace_queue_if_needed();
        }
        self.refresh_latest_frame();
        let Some(buffer) = self.buffers.get(index as usize) else {
            return Ok(());
        };
        let frame = self.frame_for_buffer(buffer.len);
        let bytes = frame.len();
        let frame_hash = fnv1a64(frame);
        let frame_sequence = uvc_contract::lesavka_app4_sequence(frame);
        if bytes > 0 {
            unsafe {
                std::ptr::copy_nonoverlapping(frame.as_ptr(), buffer.ptr, bytes);
            }
            if bytes < buffer.len {
                unsafe {
                    std::ptr::write_bytes(buffer.ptr.add(bytes), 0, buffer.len - bytes);
                }
            }
        } else {
            unsafe {
                std::ptr::write_bytes(buffer.ptr, 0, buffer.len);
            }
        }

        let mut buf = empty_v4l2_buffer();
        buf.index = index;
        buf.bytesused = bytes as u32;
        buf.length = buffer.len as u32;
        let req = ioctl_readwrite::<V4l2Buffer>(b'V', 15);
        let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) };
        if rc < 0 {
            self.stats.qbuf_ioctl_errors += 1;
            return Err(std::io::Error::last_os_error()).context("VIDIOC_QBUF");
        }
        self.stats.queued += 1;
        self.stats.latest_bytes = bytes;
        self.record_handoff(index, dqbuf_flags, bytes, frame_hash, frame_sequence);
        self.report_stats_if_due();
        Ok(())
    }

    fn pace_queue_if_needed(&mut self) {
        let Some(period) = self.frame_period else {
            return;
        };
        let now = Instant::now();
        if let Some(deadline) = self.next_queue_at
            && let Some(delay) = deadline.checked_duration_since(now)
            && !delay.is_zero()
        {
            thread::sleep(delay);
            self.stats.paced_sleeps += 1;
            self.stats.paced_sleep_ms += delay.as_millis().min(u128::from(u64::MAX)) as u64;
        }
        self.next_queue_at = Some(Instant::now() + period);
    }

    fn refresh_latest_frame(&mut self) {
        self.last_spool_mtime_ns = frame_spool_mtime_ns(&self.frame_path);
        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("hold_stale", "idle_stale");
            return;
        }
        let max_frame_bytes = self.frame_payload_limit();
        match std::fs::read(&self.frame_path) {
            Ok(frame) if !looks_like_mjpeg_frame(&frame) => {
                self.stats.rejected_invalid += 1;
                self.hold_last_good_or_idle("hold_invalid", "idle_invalid");
            }
            Ok(frame) if frame.len() > max_frame_bytes => {
                self.stats.rejected_oversize += 1;
                self.stats.last_rejected_oversize_bytes = frame.len();
                self.stats.last_rejected_oversize_cap = max_frame_bytes;
                self.hold_last_good_or_idle("hold_oversize", "idle_oversize");
            }
            Ok(frame) => {
                if strict_validation_due(self.stats.reloaded)
                    && (!uvc_contract::validate_jpeg_structure(&frame)
                        || uvc_contract::lesavka_app4_integrity_valid(&frame) == Some(false))
                {
                    self.stats.rejected_invalid += 1;
                    self.stats.strict_validation_failures += 1;
                    self.hold_last_good_or_idle("hold_strict_invalid", "idle_strict_invalid");
                    return;
                }
                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);
                self.last_refresh_disposition = "fresh";
            }
            Err(_) => {
                self.stats.read_errors += 1;
                self.hold_last_good_or_idle("hold_read_error", "idle_read_error");
            }
        }
        if !looks_like_mjpeg_frame(&self.latest_frame) {
            self.replace_latest_with_idle();
            self.last_refresh_disposition = "idle_invalid_cache";
        }
    }

    fn hold_last_good_or_idle(&mut self, hold_reason: &'static str, idle_reason: &'static str) {
        if self.has_verified_frame && !verified_frame_idle_timeout_elapsed(self.last_verified_mtime) {
            self.stats.held_last_good += 1;
            self.last_refresh_disposition = hold_reason;
            return;
        }
        self.replace_latest_with_idle();
        self.last_refresh_disposition = idle_reason;
    }

    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 record_handoff(
        &mut self,
        buffer_index: u32,
        dqbuf_flags: u32,
        bytes: usize,
        hash: u64,
        frame_sequence: Option<u64>,
    ) {
        let Some(path) = uvc_handoff_audit_path() else {
            return;
        };
        self.handoff_sequence += 1;
        let record = format!(
            "{{\"schema\":\"lesavka.uvc-handoff.v1\",\"sequence\":{},\"frame_sequence\":{},\"unix_ns\":{},\"spool_mtime_ns\":{},\"bytes\":{},\"fnv1a64\":\"{:016x}\",\"frame_cap\":{},\"buffer_index\":{},\"dqbuf_flags\":{},\"v4l2_buffer_error\":{},\"disposition\":\"{}\"}}\n",
            self.handoff_sequence,
            frame_sequence
                .map(|value| value.to_string())
                .unwrap_or_else(|| "null".to_string()),
            unix_now_ns(),
            self.last_spool_mtime_ns
                .map(|value| value.to_string())
                .unwrap_or_else(|| "null".to_string()),
            bytes,
            hash,
            self.frame_payload_limit(),
            buffer_index,
            dqbuf_flags,
            dqbuf_flags & V4L2_BUF_FLAG_ERROR != 0,
            self.last_refresh_disposition,
        );
        let max_records = uvc_handoff_audit_max_records();
        if max_records == 0 {
            return;
        }
        if self.handoff_audit.len() == max_records {
            self.handoff_audit.pop_front();
        }
        self.handoff_audit.push_back(record);
        let payload = self.handoff_audit.iter().map(String::as_str).collect::<String>();
        if let Err(err) = write_atomic_text(&path, &payload) {
            eprintln!(
                "[lesavka-uvc] failed to write handoff audit {}: {err:#}",
                path.display()
            );
        }
    }

    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)
    }

    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
        }
    }

    fn report_stats_if_due(&mut self) {
        let Some(interval) = uvc_stats_interval() else {
            return;
        };
        let now = Instant::now();
        if self
            .stats
            .last_report
            .is_some_and(|last| now.duration_since(last) < interval)
        {
            return;
        }
        self.stats.last_report = Some(now);
        eprintln!(
            "[lesavka-uvc] video stats queued={} reloaded={} stale_replay={} rejected_oversize={} rejected_invalid={} fallback_idle={} held_last_good={} read_errors={} strict_validation_failures={} dqbuf_ioctl_errors={} dqbuf_flag_errors={} qbuf_ioctl_errors={} latest_bytes={} frame_cap={} last_rejected_oversize_bytes={} last_rejected_oversize_cap={} paced_sleeps={} paced_sleep_ms={}",
            self.stats.queued,
            self.stats.reloaded,
            self.stats.replayed_stale,
            self.stats.rejected_oversize,
            self.stats.rejected_invalid,
            self.stats.fallback_idle,
            self.stats.held_last_good,
            self.stats.read_errors,
            self.stats.strict_validation_failures,
            self.stats.dqbuf_ioctl_errors,
            self.stats.dqbuf_flag_errors,
            self.stats.qbuf_ioctl_errors,
            self.stats.latest_bytes,
            self.frame_payload_limit(),
            self.stats.last_rejected_oversize_bytes,
            self.stats.last_rejected_oversize_cap,
            self.stats.paced_sleeps,
            self.stats.paced_sleep_ms
        );
        if let Some(path) = uvc_stats_path() {
            let _ = write_atomic_text(
                &path,
                &uvc_stats_snapshot_json(&self.stats, self.frame_payload_limit()),
            );
        }
    }
}

impl Drop for UvcVideoStream {
    fn drop(&mut self) {
        self.stop();
    }
}

fn empty_v4l2_buffer() -> V4l2Buffer {
    V4l2Buffer {
        index: 0,
        type_: V4L2_BUF_TYPE_VIDEO_OUTPUT,
        bytesused: 0,
        flags: 0,
        field: V4L2_FIELD_NONE,
        timestamp: libc::timeval {
            tv_sec: 0,
            tv_usec: 0,
        },
        timecode: V4l2Timecode {
            type_: 0,
            flags: 0,
            frames: 0,
            seconds: 0,
            minutes: 0,
            hours: 0,
            userbits: [0; 4],
        },
        sequence: 0,
        memory: V4L2_MEMORY_MMAP,
        m: V4l2BufferM { offset: 0 },
        length: 0,
        reserved2: 0,
        request: V4l2BufferRequest { reserved: 0 },
    }
}

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"))
}

fn uvc_stats_path() -> Option<std::path::PathBuf> {
    match env::var("LESAVKA_UVC_STATS_PATH") {
        Ok(value) => {
            let trimmed = value.trim();
            if trimmed.is_empty() || trimmed == "0" {
                None
            } else {
                Some(std::path::PathBuf::from(trimmed))
            }
        }
        Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)),
    }
}

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

fn uvc_buffer_count() -> u32 {
    env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
}

fn uvc_idle_pump_sleep() -> Duration {
    Duration::from_millis(env_u64(
        "LESAVKA_UVC_IDLE_PUMP_MS",
        DEFAULT_UVC_IDLE_PUMP_MS,
    ))
}

fn uvc_queue_period(fps: u32) -> Option<Duration> {
    if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", DEFAULT_UVC_QUEUE_PACING) {
        return None;
    }
    let fps = fps.max(1);
    Some(Duration::from_nanos(1_000_000_000 / u64::from(fps)))
}

/// Bound MJPEG frames before they enter the USB UVC helper.
///
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
/// byte length. Why: oversized frames are the most common cause of browser
/// half-frame grey smears, and freezing the last good frame is preferable to
/// queueing a frame that is likely to arrive incomplete.
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
    if let Ok(contract) = uvc_contract::read_contract(&uvc_contract::contract_path()) {
        return contract
            .enforced_frame_cap_bytes
            .min(cfg.frame_size)
            .min(MAX_MJPEG_FRAME_BYTES as u32) as usize;
    }
    let derived = derived_uvc_frame_max_bytes(cfg).min(cfg.frame_size as usize);
    env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES")
        .filter(|limit| *limit > 0)
        .map(|limit| derived.min(limit as usize))
        .unwrap_or(derived)
}

fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
    derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
}

fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
    let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
    uvc_contract::frame_cap_from_budget(budget_per_sec, fps) as usize
}

fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
    let configured = env_u32(
        "LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
        DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
    )
    .max(1);
    let pct = env_u32(
        "LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
        DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
    );
    uvc_contract::effective_mjpeg_budget_bytes_per_sec(configured, max_packet, bulk, pct)
}

fn env_flag_enabled(name: &str, default: bool) -> bool {
    env::var(name)
        .ok()
        .map(|value| {
            let trimmed = value.trim();
            if trimmed.eq_ignore_ascii_case("0")
                || trimmed.eq_ignore_ascii_case("false")
                || trimmed.eq_ignore_ascii_case("no")
                || trimmed.eq_ignore_ascii_case("off")
            {
                false
            } else if trimmed.eq_ignore_ascii_case("1")
                || trimmed.eq_ignore_ascii_case("true")
                || trimmed.eq_ignore_ascii_case("yes")
                || trimmed.eq_ignore_ascii_case("on")
            {
                true
            } else {
                default
            }
        })
        .unwrap_or(default)
}

fn uvc_bulk_transfer_enabled() -> bool {
    uvc_bulk_transfer_enabled_for_base(std::path::Path::new(CONFIGFS_UVC_BASE))
}

fn uvc_bulk_transfer_enabled_for_base(base: &std::path::Path) -> bool {
    let requested = env_flag_enabled("LESAVKA_UVC_BULK", true);
    let bulk = uvc_contract::bulk_enabled_from_configfs(base, requested);
    if requested && !bulk {
        eprintln!(
            "[lesavka-uvc] UVC bulk requested but live configfs streaming_bulk is not 1; using isochronous payload sizing"
        );
    }
    bulk
}

fn uvc_frame_size_for_active_mode(
    width: u32,
    height: u32,
    fps: u32,
    max_packet: u32,
    bulk: bool,
) -> u32 {
    let derived = derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
        .min(u32::MAX as usize) as u32;
    let requested = env_u32_opt("LESAVKA_UVC_FRAME_SIZE").filter(|value| *value > 0);
    let frame_size = requested.unwrap_or(derived).min(derived);
    let minimum = (width.saturating_mul(height) / 32).max(64 * 1024);
    frame_size.max(minimum.min(derived))
}

fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
    std::fs::write(&tmp, text)?;
    std::fs::rename(&tmp, path)?;
    Ok(())
}

fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
    let kernel = uvc_kernel_stats_json();
    format!(
        "{{\"generated_unix_ms\":{},\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"held_last_good\":{},\"read_errors\":{},\"strict_validation_failures\":{},\"dqbuf_ioctl_errors\":{},\"dqbuf_flag_errors\":{},\"qbuf_ioctl_errors\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{},\"kernel\":{}}}\n",
        unix_now_ns() / 1_000_000,
        stats.queued,
        stats.reloaded,
        stats.replayed_stale,
        stats.rejected_oversize,
        stats.rejected_invalid,
        stats.fallback_idle,
        stats.held_last_good,
        stats.read_errors,
        stats.strict_validation_failures,
        stats.dqbuf_ioctl_errors,
        stats.dqbuf_flag_errors,
        stats.qbuf_ioctl_errors,
        stats.latest_bytes,
        frame_cap,
        stats.last_rejected_oversize_bytes,
        stats.last_rejected_oversize_cap,
        stats.paced_sleeps,
        stats.paced_sleep_ms,
        kernel
    )
}

fn uvc_kernel_stats_json() -> String {
    let path = env::var("LESAVKA_UVC_KERNEL_STATS_PATH")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from(DEFAULT_UVC_KERNEL_STATS_PATH));
    std::fs::read_to_string(path)
        .ok()
        .and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok())
        .map(|value| value.to_string())
        .unwrap_or_else(|| "null".to_string())
}

fn uvc_stats_interval() -> Option<Duration> {
    match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) {
        0 => None,
        value => Some(Duration::from_millis(value)),
    }
}

fn frame_spool_max_age() -> Option<Duration> {
    match env_u64(
        "LESAVKA_UVC_FRAME_MAX_AGE_MS",
        DEFAULT_UVC_FRAME_MAX_AGE_MS,
    ) {
        0 => None,
        value => Some(Duration::from_millis(value)),
    }
}

fn uvc_idle_after_stale() -> Duration {
    let value = env::var("LESAVKA_UVC_IDLE_AFTER_MS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .or_else(|| {
            env::var("LESAVKA_UVC_IDLE_AFTER_STALE_MS")
                .ok()
                .and_then(|value| value.parse::<u64>().ok())
        })
        .unwrap_or(DEFAULT_UVC_IDLE_AFTER_STALE_MS);
    Duration::from_millis(value)
}

fn verified_frame_idle_timeout_elapsed(modified: Option<SystemTime>) -> bool {
    let Some(modified) = modified else {
        return true;
    };
    SystemTime::now()
        .duration_since(modified)
        .map(|age| age >= uvc_idle_after_stale())
        .unwrap_or(false)
}

fn frame_spool_modified(path: &std::path::Path) -> Option<SystemTime> {
    std::fs::metadata(path).ok()?.modified().ok()
}

fn frame_spool_mtime_ns(path: &std::path::Path) -> Option<u128> {
    frame_spool_modified(path)?
        .duration_since(SystemTime::UNIX_EPOCH)
        .ok()
        .map(|duration| duration.as_nanos())
}

fn strict_validation_due(reloaded: u64) -> bool {
    match env_u64("LESAVKA_UVC_STRICT_VALIDATE_EVERY", 30) {
        0 => false,
        every => reloaded % every == 0,
    }
}

fn uvc_handoff_audit_path() -> Option<std::path::PathBuf> {
    if let Ok(value) = env::var("LESAVKA_UVC_HANDOFF_AUDIT_PATH") {
        let value = value.trim();
        return (!value.is_empty() && value != "0").then(|| std::path::PathBuf::from(value));
    }
    env_flag_enabled("LESAVKA_UVC_HANDOFF_AUDIT", false)
        .then(|| std::path::PathBuf::from(DEFAULT_UVC_HANDOFF_AUDIT_PATH))
}

fn uvc_handoff_audit_max_records() -> usize {
    env_u64(
        "LESAVKA_UVC_HANDOFF_AUDIT_MAX_RECORDS",
        DEFAULT_UVC_HANDOFF_AUDIT_MAX_RECORDS as u64,
    )
    .min(16_384) as usize
}

fn fnv1a64(data: &[u8]) -> u64 {
    let mut hash = 0xcbf2_9ce4_8422_2325u64;
    for byte in data {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

fn unix_now_ns() -> u128 {
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos()
}

fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<Duration>) -> bool {
    let Some(max_age) = max_age else {
        return false;
    };
    let Ok(metadata) = std::fs::metadata(path) else {
        return true;
    };
    let Ok(modified) = metadata.modified() else {
        return false;
    };
    SystemTime::now()
        .duration_since(modified)
        .map(|age| age > max_age)
        .unwrap_or(false)
}

fn main() -> Result<()> {
    let (dev, cfg) = parse_args()?;
    let _singleton = acquire_singleton_lock()?;
    let interfaces = load_interfaces();
    eprintln!("[lesavka-uvc] starting (dev={dev})");
    eprintln!(
        "[lesavka-uvc] interfaces control={} streaming={}",
        interfaces.control, interfaces.streaming
    );

    let debug = env::var("LESAVKA_UVC_DEBUG").is_ok();
    let idle_sleep = uvc_idle_pump_sleep();
    eprintln!("[lesavka-uvc] nonblock=1");
    eprintln!(
        "[lesavka-uvc] freshness buffers={} idle_pump_ms={} frame_max_age_ms={}",
        uvc_buffer_count(),
        idle_sleep.as_millis(),
        frame_spool_max_age()
            .map(|duration| duration.as_millis().to_string())
            .unwrap_or_else(|| "disabled".to_string())
    );
    let mut setup_seen: u64 = 0;
    let mut data_seen: u64 = 0;
    let mut dq_err_seen: u64 = 0;
    let mut dq_err_last: Option<i32> = None;

    loop {
        let file = match open_with_retry(&dev) {
            Ok(file) => file,
            Err(err) => {
                eprintln!("[lesavka-uvc] open failed: {err:#}");
                thread::sleep(Duration::from_millis(250));
                continue;
            }
        };
        let fd = file.as_raw_fd();
        let vidioc_subscribe = ioctl_write::<V4l2EventSubscription>(b'V', 90);
        let vidioc_dqevent = ioctl_read::<V4l2Event>(b'V', 89);
        let uvc_send_response = ioctl_write::<UvcRequestData>(b'U', 1);

        if let Err(err) = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_SETUP)
            .and_then(|_| subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DATA))
        {
            if should_retry_uvc_error(&err) {
                eprintln!("[lesavka-uvc] subscribe failed: {err:#}");
                thread::sleep(Duration::from_millis(250));
                continue;
            }
            return Err(err);
        }
        let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_CONNECT).map_err(|err| {
            eprintln!("[lesavka-uvc] connect-subscribe failed: {err:#}");
        });
        let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DISCONNECT).map_err(|err| {
            eprintln!("[lesavka-uvc] disconnect-subscribe failed: {err:#}");
        });
        let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMON).map_err(|err| {
            eprintln!("[lesavka-uvc] streamon-subscribe failed: {err:#}");
        });
        let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMOFF).map_err(|err| {
            eprintln!("[lesavka-uvc] streamoff-subscribe failed: {err:#}");
        });

        let mut state = UvcState::new(cfg);
        let mut pending: Option<PendingRequest> = None;
        let mut video = UvcVideoStream::new(fd);

        loop {
            if let Err(err) = video.pump() {
                eprintln!("[lesavka-uvc] video pump failed: {err:#}");
            }

            let mut ev = unsafe { std::mem::zeroed::<V4l2Event>() };
            let rc = unsafe { libc::ioctl(fd, vidioc_dqevent, &mut ev) };
            if rc < 0 {
                let err = std::io::Error::last_os_error();
                match err.raw_os_error() {
                    Some(libc::EAGAIN) | Some(libc::EINTR) | Some(libc::ENOENT) => {
                        if debug {
                            let code = err.raw_os_error();
                            if dq_err_seen < 10 || code != dq_err_last {
                                eprintln!("[lesavka-uvc] dqevent idle: {err}");
                                dq_err_seen += 1;
                                dq_err_last = code;
                            }
                        }
                        thread::sleep(idle_sleep);
                        continue;
                    }
                    Some(libc::ENODEV) | Some(libc::EBADF) | Some(libc::EIO) => {
                        eprintln!("[lesavka-uvc] device reset ({err}); reopening");
                        video.stop();
                        break;
                    }
                    _ => {
                        eprintln!("[lesavka-uvc] dqevent failed: {err}");
                        thread::sleep(Duration::from_millis(100));
                        continue;
                    }
                }
            }

            match ev.type_ {
                UVC_EVENT_CONNECT => {
                    let speed = u32::from_le_bytes(event_bytes(&ev)[0..4].try_into().unwrap());
                    eprintln!("[lesavka-uvc] UVC connect (speed={speed})");
                }
                UVC_EVENT_DISCONNECT => {
                    eprintln!("[lesavka-uvc] UVC disconnect");
                    video.stop();
                }
                UVC_EVENT_STREAMON => {
                    eprintln!("[lesavka-uvc] stream on");
                    if let Err(err) = video.start(state.cfg) {
                        eprintln!("[lesavka-uvc] stream start failed: {err:#}");
                    }
                }
                UVC_EVENT_STREAMOFF => {
                    eprintln!("[lesavka-uvc] stream off");
                    video.stop();
                }
                UVC_EVENT_SETUP => {
                    let req = parse_ctrl_request(event_bytes(&ev));
                    setup_seen += 1;
                    if debug || setup_seen <= 10 {
                        eprintln!(
                            "[lesavka-uvc] setup #{setup_seen} rt=0x{:02x} rq=0x{:02x} val=0x{:04x} idx=0x{:04x} len={}",
                            req.b_request_type,
                            req.b_request,
                            req.w_value,
                            req.w_index,
                            req.w_length
                        );
                    }
                    handle_setup(
                        fd,
                        uvc_send_response,
                        &mut state,
                        &mut pending,
                        interfaces,
                        req,
                        debug,
                    );
                }
                UVC_EVENT_DATA => {
                    let data = parse_request_data(event_bytes(&ev));
                    data_seen += 1;
                    if debug || data_seen <= 10 {
                        eprintln!("[lesavka-uvc] data #{data_seen} len={}", data.length);
                    }
                    handle_data(
                        fd,
                        uvc_send_response,
                        &mut state,
                        &mut pending,
                        interfaces,
                        data,
                        debug,
                    );
                }
                _ => {
                    if debug {
                        eprintln!("[lesavka-uvc] event type=0x{:08x}", ev.type_);
                    }
                }
            }
        }
    }
}

fn should_retry_uvc_error(err: &anyhow::Error) -> bool {
    err.chain()
        .find_map(|cause| {
            cause
                .downcast_ref::<std::io::Error>()
                .and_then(|error| error.raw_os_error())
        })
        .is_some_and(|code| {
            matches!(
                code,
                libc::ENOENT | libc::ENODEV | libc::EIO | libc::EBADF | libc::EAGAIN
            )
        })
}

fn parse_args() -> Result<(String, UvcConfig)> {
    let mut args = env::args().skip(1);
    let mut dev: Option<String> = None;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--device" | "-d" => dev = args.next(),
            _ if arg.starts_with('/') => dev = Some(arg),
            _ => {}
        }
    }

    let dev = dev
        .or_else(|| env::var("LESAVKA_UVC_DEV").ok())
        .context("missing --device (or LESAVKA_UVC_DEV)")?;

    Ok((dev, UvcConfig::from_env()))
}

impl UvcConfig {
    fn from_env() -> Self {
        let mut width = env_u32("LESAVKA_UVC_WIDTH", 1280);
        let mut height = env_u32("LESAVKA_UVC_HEIGHT", 720);
        let mut fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
        let requested_interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
        let mut interval = if requested_interval == 0 {
            10_000_000 / fps
        } else {
            requested_interval
        };
        let live_snapshot = read_configfs_snapshot();
        if let Some(snapshot) = live_snapshot {
            let live_interval = if snapshot.default_interval == 0 {
                snapshot.frame_interval
            } else {
                snapshot.default_interval
            };
            if live_interval > 0 {
                let live_fps = (10_000_000 / live_interval).max(1);
                if (width, height, fps, interval)
                    != (snapshot.width, snapshot.height, live_fps, live_interval)
                {
                    eprintln!(
                        "[lesavka-uvc] live descriptor differs from configured profile; honoring attached gadget {}x{}@{}fps interval={} until rebuild (configured {}x{}@{}fps interval={})",
                        snapshot.width,
                        snapshot.height,
                        live_fps,
                        live_interval,
                        width,
                        height,
                        fps,
                        interval
                    );
                }
                width = snapshot.width;
                height = snapshot.height;
                fps = live_fps;
                interval = live_interval;
            }
        }
        let bulk = uvc_bulk_transfer_enabled();
        let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
        if let Some(cap) = compute_payload_cap(bulk) {
            if max_packet > cap.limit {
                eprintln!(
                    "[lesavka-uvc] payload cap {}B ({}% from {}): clamp max_packet {} -> {} (periodic_dw={:?} non_periodic_dw={:?})",
                    cap.limit,
                    cap.pct,
                    cap.source,
                    max_packet,
                    cap.limit,
                    cap.periodic_dw,
                    cap.non_periodic_dw
                );
                max_packet = cap.limit;
            } else {
                eprintln!(
                    "[lesavka-uvc] payload cap {}B ({}% from {}): max_packet {} (periodic_dw={:?} non_periodic_dw={:?})",
                    cap.limit,
                    cap.pct,
                    cap.source,
                    max_packet,
                    cap.periodic_dw,
                    cap.non_periodic_dw
                );
            }
        } else {
            eprintln!(
                "[lesavka-uvc] payload cap unavailable; using max_packet {}",
                max_packet
            );
        }
        if let Some(cfg_max) = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket")) {
            if max_packet > cfg_max {
                eprintln!(
                    "[lesavka-uvc] configfs maxpacket {}: clamp max_packet {} -> {}",
                    cfg_max, max_packet, cfg_max
                );
                max_packet = cfg_max;
            } else {
                eprintln!(
                    "[lesavka-uvc] configfs maxpacket {}: max_packet {}",
                    cfg_max, max_packet
                );
            }
        }
        if bulk {
            max_packet = max_packet.min(512);
        } else {
            max_packet = max_packet.min(1024);
        }
        let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk)
            .min(
                live_snapshot
                    .map(|snapshot| snapshot.frame_size)
                    .filter(|frame_size| *frame_size > 0)
                    .unwrap_or(u32::MAX),
            );

        Self {
            width,
            height,
            fps,
            interval,
            max_packet,
            frame_size,
            bulk,
        }
    }
}

impl UvcState {
    fn new(cfg: UvcConfig) -> Self {
        let _profile_hint = (cfg.width, cfg.height, cfg.fps);
        let ctrl_len = stream_ctrl_len();
        let default = build_streaming_control(&cfg, ctrl_len);
        let state = Self {
            cfg,
            ctrl_len,
            default,
            probe: default,
            commit: default,
            cfg_snapshot: None,
            commit_observed: false,
        };
        state.publish_payload_contract("startup");
        state
    }

    fn payload_contract(&self) -> UvcPayloadContract {
        let snapshot = read_configfs_snapshot();
        let streaming_bulk_value = snapshot
            .and_then(|snapshot| snapshot.streaming_bulk)
            .or_else(|| {
                uvc_contract::read_streaming_bulk_value(std::path::Path::new(CONFIGFS_UVC_BASE))
            });
        UvcPayloadContract::from_input(UvcContractInput {
            configfs_available: snapshot.is_some(),
            commit_observed: self.commit_observed,
            bulk: self.cfg.bulk,
            streaming_bulk_value,
            width: self.cfg.width,
            height: self.cfg.height,
            fps: self.cfg.fps,
            frame_interval_100ns: read_le32(&self.commit, 4).max(self.cfg.interval),
            streaming_maxpacket: snapshot
                .map(|value| value.maxpacket)
                .unwrap_or(self.cfg.max_packet),
            streaming_maxburst: snapshot.map(|value| value.maxburst).unwrap_or(0),
            configured_budget_bytes_per_sec: env_u32(
                "LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
                DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
            ),
            isochronous_limit_pct: env_u32(
                "LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
                DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
            ),
            advertised_frame_bytes: snapshot
                .map(|value| value.frame_size)
                .unwrap_or(self.cfg.frame_size),
            committed_frame_bytes: read_le32(&self.commit, 18),
            committed_payload_bytes: read_le32(&self.commit, 22),
            requested_frame_cap_bytes: env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES"),
        })
    }

    fn publish_payload_contract(&self, reason: &str) {
        let contract = self.payload_contract();
        let path = uvc_contract::contract_path();
        if let Err(err) = uvc_contract::write_contract(&path, &contract) {
            eprintln!(
                "[lesavka-uvc] failed to publish UVC payload contract {}: {err:#}",
                path.display()
            );
            return;
        }
        eprintln!(
            "[lesavka-uvc] payload contract reason={} transport={} frame_cap={} advertised={} committed={} payload={} divergent={} path={}",
            reason,
            contract.transport,
            contract.enforced_frame_cap_bytes,
            contract.advertised_frame_bytes,
            contract.committed_frame_bytes,
            contract.committed_payload_bytes,
            contract.divergent,
            path.display()
        );
    }
}

fn load_interfaces() -> UvcInterfaces {
    let control = env_u8("LESAVKA_UVC_CTRL_INTF")
        .or_else(|| {
            read_interface(
                "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/control/bInterfaceNumber",
            )
        })
        .unwrap_or(UVC_STRING_CONTROL_IDX);
    let streaming = env_u8("LESAVKA_UVC_STREAM_INTF")
        .or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming/bInterfaceNumber"))
        .unwrap_or(UVC_STRING_STREAMING_IDX);
    UvcInterfaces { control, streaming }
}

fn read_interface(path: &str) -> Option<u8> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|v| v.trim().parse::<u8>().ok())
}

fn open_with_retry(path: &str) -> Result<std::fs::File> {
    let read_only = uvc_control_read_only();
    for attempt in 1..=200 {
        let mut opts = OpenOptions::new();
        opts.read(true);
        if !read_only {
            opts.write(true);
        }
        opts.custom_flags(libc::O_NONBLOCK);
        match opts.open(path) {
            Ok(f) => {
                let mode = if read_only { "ro" } else { "rw" };
                eprintln!("[lesavka-uvc] opened {path} mode={mode} (attempt {attempt})");
                return Ok(f);
            }
            Err(err) if err.raw_os_error() == Some(libc::ENOENT) => {
                thread::sleep(Duration::from_millis(50));
            }
            Err(err) => return Err(err).with_context(|| format!("open {path}")),
        }
    }
    Err(anyhow::anyhow!("timeout opening {path}"))
}

fn uvc_control_read_only() -> bool {
    env::var("LESAVKA_UVC_CONTROL_READ_ONLY")
        .ok()
        .map(|value| {
            let trimmed = value.trim();
            !(trimmed.eq_ignore_ascii_case("0")
                || trimmed.eq_ignore_ascii_case("false")
                || trimmed.eq_ignore_ascii_case("no")
                || trimmed.eq_ignore_ascii_case("off"))
        })
        .unwrap_or(true)
}

fn acquire_singleton_lock() -> Result<File> {
    let path =
        env::var("LESAVKA_UVC_LOCK_PATH").unwrap_or_else(|_| "/run/lesavka-uvc.lock".to_string());
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(&path)
        .with_context(|| format!("open singleton lock {path}"))?;
    let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        match err.raw_os_error() {
            Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => {
                eprintln!("[lesavka-uvc] another helper already owns {path}; exiting");
                std::process::exit(0);
            }
            _ => return Err(err).with_context(|| format!("lock singleton {path}")),
        }
    }
    Ok(file)
}

fn subscribe_event(fd: i32, req: libc::c_ulong, event: u32) -> Result<()> {
    let mut sub = V4l2EventSubscription {
        type_: event,
        id: 0,
        flags: 0,
        reserved: [0; 5],
    };
    let rc = unsafe { libc::ioctl(fd, req, &mut sub) };
    if rc < 0 {
        return Err(std::io::Error::last_os_error())
            .with_context(|| format!("subscribe event {event:#x} (fd={fd})"));
    }
    Ok(())
}

fn handle_setup(
    fd: i32,
    uvc_send_response: libc::c_ulong,
    state: &mut UvcState,
    pending: &mut Option<PendingRequest>,
    interfaces: UvcInterfaces,
    req: UsbCtrlRequest,
    debug: bool,
) {
    let selector = (req.w_value >> 8) as u8;
    let interface_lo = (req.w_index & 0xff) as u8;
    let interface_hi = (req.w_index >> 8) as u8;
    let interface_raw =
        if interface_hi == interfaces.streaming || interface_hi == interfaces.control {
            interface_hi
        } else if interface_lo == interfaces.streaming || interface_lo == interfaces.control {
            interface_lo
        } else {
            interface_hi
        };
    let is_in = (req.b_request_type & USB_DIR_IN) != 0;
    if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) {
        maybe_update_ctrl_len(state, req.w_length, debug);
    }
    let interface = map_interface(interface_raw, selector, interfaces, debug);

    if !is_in && req.b_request == UVC_SET_CUR {
        let len = req.w_length as usize;
        if interface == interfaces.control {
            // Accept SET_CUR on VC controls by replying with a zeroed payload.
            let payload = vec![0u8; len.min(UVC_DATA_SIZE)];
            let _ = send_response(fd, uvc_send_response, &payload);
            if debug {
                eprintln!(
                    "[lesavka-uvc] VC SET_CUR ack len={} iface={} sel={}",
                    req.w_length, interface, selector
                );
            }
            return;
        }
        if interface != interfaces.streaming {
            let _ = send_stall(fd, uvc_send_response);
            return;
        }
        if len > UVC_DATA_SIZE {
            eprintln!(
                "[lesavka-uvc] SET_CUR too large len={} (max={}); stalling",
                len, UVC_DATA_SIZE
            );
            let _ = send_stall(fd, uvc_send_response);
            return;
        }
        *pending = Some(PendingRequest {
            interface,
            selector,
            expected_len: len,
        });
        let payload = vec![0u8; len];
        let _ = send_response(fd, uvc_send_response, &payload);
        if debug {
            eprintln!(
                "[lesavka-uvc] SET_CUR queued len={} iface={} sel={}",
                req.w_length, interface, selector
            );
        }
        return;
    }

    if !is_in {
        let _ = send_stall(fd, uvc_send_response);
        return;
    }

    let payload = build_in_response(
        state,
        interfaces,
        interface,
        selector,
        req.b_request,
        req.w_length,
    );
    match payload {
        Some(bytes) => {
            if debug {
                eprintln!(
                    "[lesavka-uvc] send IN response rq=0x{:02x} sel={} len={}",
                    req.b_request,
                    selector,
                    bytes.len()
                );
            }
            let _ = send_response(fd, uvc_send_response, &bytes);
        }
        None => {
            let _ = send_stall(fd, uvc_send_response);
        }
    }
}

fn map_interface(raw: u8, selector: u8, interfaces: UvcInterfaces, debug: bool) -> u8 {
    let mapped = if selector == UVC_VS_PROBE_CONTROL {
        interfaces.streaming
    } else if selector == UVC_VS_COMMIT_CONTROL || selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL {
        if raw == interfaces.control {
            interfaces.control
        } else {
            interfaces.streaming
        }
    } else {
        raw
    };

    if debug && mapped != raw {
        eprintln!("[lesavka-uvc] remapped interface {raw} -> {mapped} for selector {selector}");
    }

    mapped
}

fn maybe_update_ctrl_len(state: &mut UvcState, w_length: u16, debug: bool) {
    let want = w_length as usize;
    if !(want == STREAM_CTRL_SIZE_11 || want == STREAM_CTRL_SIZE_15) {
        return;
    }
    if state.ctrl_len == want {
        return;
    }

    state.ctrl_len = want;
    state.default = build_streaming_control(&state.cfg, state.ctrl_len);
    state.probe = state.default;
    state.commit = state.default;

    if debug {
        eprintln!("[lesavka-uvc] ctrl_len set to {}", state.ctrl_len);
    }
}

fn handle_data(
    _fd: i32,
    _uvc_send_response: libc::c_ulong,
    state: &mut UvcState,
    pending: &mut Option<PendingRequest>,
    interfaces: UvcInterfaces,
    data: UvcRequestData,
    debug: bool,
) {
    let Some(p) = pending.take() else {
        if debug {
            eprintln!("[lesavka-uvc] DATA with no pending request; ignoring");
        }
        return;
    };

    if data.length < 0 {
        return;
    }

    let len = data.length as usize;
    if debug && p.expected_len != 0 && len != p.expected_len {
        eprintln!(
            "[lesavka-uvc] DATA len mismatch: expected={} got={}",
            p.expected_len, len
        );
    }
    let slice = &data.data[..len.min(data.data.len())];
    if debug && slice.len() >= STREAM_CTRL_SIZE_11 {
        let interval = read_le32(slice, 4);
        let payload = read_le32(slice, 22);
        eprintln!(
            "[lesavka-uvc] data ctrl fmt={} frame={} interval={} payload={}",
            slice[2], slice[3], interval, payload
        );
    }

    if p.interface == interfaces.streaming
        && matches!(p.selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL)
    {
        let sanitized = sanitize_streaming_control(slice, state);
        if p.selector == UVC_VS_PROBE_CONTROL {
            state.probe = sanitized;
            state.publish_payload_contract("probe");
            if debug {
                let interval = read_le32(&state.probe, 4);
                let payload = read_le32(&state.probe, 22);
                eprintln!(
                    "[lesavka-uvc] probe set interval={} payload={}",
                    interval, payload
                );
                log_configfs_snapshot(state, "probe");
            }
        } else {
            state.commit = sanitized;
            state.commit_observed = true;
            state.publish_payload_contract("commit");
            if debug {
                let interval = read_le32(&state.commit, 4);
                let payload = read_le32(&state.commit, 22);
                eprintln!(
                    "[lesavka-uvc] commit set interval={} payload={}",
                    interval, payload
                );
                log_configfs_snapshot(state, "commit");
            }
        }
    }
}

fn build_in_response(
    state: &UvcState,
    interfaces: UvcInterfaces,
    interface: u8,
    selector: u8,
    request: u8,
    w_length: u16,
) -> Option<Vec<u8>> {
    let payload = match interface {
        _ if interface == interfaces.streaming => {
            build_streaming_response(state, selector, request)
        }
        _ if interface == interfaces.control => build_control_response(selector, request),
        _ => None,
    }?;

    Some(adjust_length(payload, w_length))
}

fn build_streaming_response(state: &UvcState, selector: u8, request: u8) -> Option<Vec<u8>> {
    let current = match selector {
        UVC_VS_PROBE_CONTROL => state.probe,
        UVC_VS_COMMIT_CONTROL => state.commit,
        _ => return None,
    };

    match request {
        UVC_GET_INFO => Some(vec![0x03]), // support GET/SET
        UVC_GET_LEN => Some((state.ctrl_len as u16).to_le_bytes().to_vec()),
        UVC_GET_CUR => Some(current[..state.ctrl_len].to_vec()),
        UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => {
            Some(state.default[..state.ctrl_len].to_vec())
        }
        _ => None,
    }
}

fn build_control_response(selector: u8, request: u8) -> Option<Vec<u8>> {
    match request {
        UVC_GET_INFO => Some(vec![0x03]), // indicate both GET/SET supported
        UVC_GET_LEN => Some(1u16.to_le_bytes().to_vec()),
        UVC_GET_CUR | UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => {
            if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL {
                // reset error code to “no error”
                Some(vec![0x00])
            } else {
                // simple 1‑byte placeholder for unhandled VC controls
                Some(vec![0x00])
            }
        }
        _ => None,
    }
}

fn sanitize_streaming_control(data: &[u8], state: &UvcState) -> [u8; STREAM_CTRL_SIZE_MAX] {
    let mut out = state.default;
    if data.len() >= STREAM_CTRL_SIZE_11 {
        let format_index = data[2];
        let frame_index = data[3];
        let interval = read_le32(data, 4);
        let host_payload = read_le32(data, 22);

        if format_index == 1 {
            out[2] = 1;
        }
        let frame_index = uvc_frame_index_for_request(frame_index, &state.cfg);
        out[3] = frame_index;
        write_le32(
            &mut out[18..22],
            uvc_frame_size_for_index(frame_index, state.cfg.frame_size),
        );
        let _host_requested_interval = interval;
        write_le32(&mut out[4..8], state.cfg.interval);
        if host_payload > 0 {
            let payload = host_payload.min(state.cfg.max_packet);
            write_le32(&mut out[22..26], payload);
        }
    }
    out
}

fn send_response(fd: i32, req: libc::c_ulong, payload: &[u8]) -> Result<()> {
    let mut resp = UvcRequestData {
        length: payload.len() as i32,
        data: [0u8; UVC_DATA_SIZE],
    };
    let n = payload.len().min(UVC_DATA_SIZE);
    resp.data[..n].copy_from_slice(&payload[..n]);

    let rc = unsafe { libc::ioctl(fd, req, &resp) };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        eprintln!("[lesavka-uvc] send_response failed: {err}");
        return Err(err).context("UVCIOC_SEND_RESPONSE");
    }
    Ok(())
}

fn send_stall(fd: i32, req: libc::c_ulong) -> Result<()> {
    let resp = UvcRequestData {
        length: -1,
        data: [0u8; UVC_DATA_SIZE],
    };
    let rc = unsafe { libc::ioctl(fd, req, &resp) };
    if rc < 0 {
        let err = std::io::Error::last_os_error();
        eprintln!("[lesavka-uvc] send_stall failed: {err}");
        return Err(err).context("UVCIOC_SEND_RESPONSE(stall)");
    }
    Ok(())
}

fn build_streaming_control(cfg: &UvcConfig, ctrl_len: usize) -> [u8; STREAM_CTRL_SIZE_MAX] {
    let mut buf = [0u8; STREAM_CTRL_SIZE_MAX];

    write_le16(&mut buf[0..2], 1); // bmHint: dwFrameInterval
    buf[2] = 1; // bFormatIndex
    buf[3] = uvc_frame_index_for_mode(cfg.width, cfg.height); // bFrameIndex
    write_le32(&mut buf[4..8], cfg.interval);
    write_le16(&mut buf[8..10], 0);
    write_le16(&mut buf[10..12], 0);
    write_le16(&mut buf[12..14], 0);
    write_le16(&mut buf[14..16], 0);
    write_le16(&mut buf[16..18], 0);
    write_le32(&mut buf[18..22], cfg.frame_size);
    write_le32(&mut buf[22..26], cfg.max_packet);
    if ctrl_len >= STREAM_CTRL_SIZE_15 {
        write_le32(&mut buf[26..30], 48_000_000);
        buf[30] = 0x03; // bmFramingInfo: FID + EOF supported
        buf[31] = 0x01; // bPreferedVersion
        buf[32] = 0x01; // bMinVersion
        buf[33] = 0x01; // bMaxVersion
    }

    buf
}

fn uvc_frame_index_for_mode(width: u32, height: u32) -> u8 {
    if !uvc_advertises_multiple_mjpeg_frames() {
        return 1;
    }
    match (width, height) {
        (1920, 1080) => 1,
        (1280, 720) => 2,
        _ => 2,
    }
}

fn uvc_frame_index_for_request(_requested: u8, cfg: &UvcConfig) -> u8 {
    uvc_frame_index_for_mode(cfg.width, cfg.height)
}

fn uvc_advertises_multiple_mjpeg_frames() -> bool {
    let base = std::path::Path::new(CONFIGFS_UVC_BASE).join("streaming/mjpeg/m");
    base.join("1080p").exists() && base.join("720p").exists()
}

fn uvc_frame_size_for_index(frame_index: u8, fallback: u32) -> u32 {
    match frame_index {
        1 | 2 => fallback,
        _ => fallback,
    }
}

fn event_bytes(ev: &V4l2Event) -> [u8; 64] {
    unsafe { ev.u.data }
}

fn parse_ctrl_request(data: [u8; 64]) -> UsbCtrlRequest {
    UsbCtrlRequest {
        b_request_type: data[0],
        b_request: data[1],
        w_value: u16::from_le_bytes([data[2], data[3]]),
        w_index: u16::from_le_bytes([data[4], data[5]]),
        w_length: u16::from_le_bytes([data[6], data[7]]),
    }
}

fn parse_request_data(data: [u8; 64]) -> UvcRequestData {
    let length = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
    let mut out = [0u8; UVC_DATA_SIZE];
    out.copy_from_slice(&data[4..64]);
    UvcRequestData { length, data: out }
}

fn stream_ctrl_len() -> usize {
    let value = env_u32("LESAVKA_UVC_CTRL_LEN", STREAM_CTRL_SIZE_15 as u32) as usize;
    match value {
        STREAM_CTRL_SIZE_11 | STREAM_CTRL_SIZE_15 => value,
        _ => STREAM_CTRL_SIZE_11,
    }
}

fn env_u32(name: &str, default: u32) -> u32 {
    env::var(name)
        .ok()
        .and_then(|v| v.parse::<u32>().ok())
        .unwrap_or(default)
}

fn env_u64(name: &str, default: u64) -> u64 {
    env::var(name)
        .ok()
        .and_then(|v| v.parse::<u64>().ok())
        .unwrap_or(default)
}

fn env_u8(name: &str) -> Option<u8> {
    env::var(name).ok().and_then(|v| v.parse::<u8>().ok())
}

fn env_u32_opt(name: &str) -> Option<u32> {
    env::var(name).ok().and_then(|v| v.parse::<u32>().ok())
}

fn read_u32_file(path: &str) -> Option<u32> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|v| v.trim().parse::<u32>().ok())
}

fn read_u32_first(path: &str) -> Option<u32> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|v| v.split_whitespace().next()?.parse::<u32>().ok())
}

fn read_configfs_snapshot() -> Option<ConfigfsSnapshot> {
    let frame = configfs_frame_name_from_env();
    let frame_root = format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/{frame}");
    let width = read_u32_file(&format!("{frame_root}/wWidth"))?;
    let height = read_u32_file(&format!("{frame_root}/wHeight"))?;
    let default_interval = read_u32_file(&format!(
        "{frame_root}/dwDefaultFrameInterval"
    ))?;
    let frame_interval = read_u32_first(&format!("{frame_root}/dwFrameInterval")).unwrap_or(0);
    let frame_size = read_u32_file(&format!("{frame_root}/dwMaxVideoFrameBufferSize"))?;
    let maxpacket = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket"))?;
    let maxburst = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxburst")).unwrap_or(0);
    let streaming_bulk =
        uvc_contract::read_streaming_bulk_value(std::path::Path::new(CONFIGFS_UVC_BASE));
    Some(ConfigfsSnapshot {
        width,
        height,
        default_interval,
        frame_interval,
        frame_size,
        maxpacket,
        maxburst,
        streaming_bulk,
    })
}

fn configfs_frame_name_from_env() -> &'static str {
    match (
        env_u32("LESAVKA_UVC_WIDTH", 1280),
        env_u32("LESAVKA_UVC_HEIGHT", 720),
    ) {
        (1920, 1080) => "1080p",
        _ => "720p",
    }
}

fn log_configfs_snapshot(state: &mut UvcState, label: &str) {
    let Some(current) = read_configfs_snapshot() else {
        eprintln!("[lesavka-uvc] configfs {label}: unavailable");
        return;
    };
    if state.cfg_snapshot == Some(current) {
        return;
    }
    eprintln!(
        "[lesavka-uvc] configfs {label}: {}x{} default_interval={} frame_interval={} frame_size={} maxpacket={} maxburst={} streaming_bulk={:?}",
        current.width,
        current.height,
        current.default_interval,
        current.frame_interval,
        current.frame_size,
        current.maxpacket,
        current.maxburst,
        current.streaming_bulk
    );
    state.cfg_snapshot = Some(current);
}

fn adjust_length(mut bytes: Vec<u8>, w_length: u16) -> Vec<u8> {
    let want = (w_length as usize).min(UVC_DATA_SIZE);
    if bytes.len() > want {
        bytes.truncate(want);
    } else if bytes.len() < want {
        bytes.resize(want, 0);
    }
    bytes
}

fn write_le16(dst: &mut [u8], val: u16) {
    let bytes = val.to_le_bytes();
    dst[0] = bytes[0];
    dst[1] = bytes[1];
}

fn write_le32(dst: &mut [u8], val: u32) {
    let bytes = val.to_le_bytes();
    dst[0] = bytes[0];
    dst[1] = bytes[1];
    dst[2] = bytes[2];
    dst[3] = bytes[3];
}

fn read_le32(src: &[u8], offset: usize) -> u32 {
    u32::from_le_bytes([
        src[offset],
        src[offset + 1],
        src[offset + 2],
        src[offset + 3],
    ])
}

fn compute_payload_cap(bulk: bool) -> Option<PayloadCap> {
    if let Some(limit) = env_u32_opt("LESAVKA_UVC_MAXPAYLOAD_LIMIT") {
        return Some(PayloadCap {
            limit,
            pct: 100,
            source: "env",
            periodic_dw: None,
            non_periodic_dw: None,
        });
    }

    let mut periodic =
        read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size").map(|v| (v, "dwc2.params"));
    let mut non_periodic =
        read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size").map(|v| (v, "dwc2.params"));
    if (periodic.is_none() || non_periodic.is_none())
        && let Some((p, np)) = read_debugfs_fifos()
    {
        if periodic.is_none() {
            periodic = p.map(|v| (v, "debugfs.params"));
        }
        if non_periodic.is_none() {
            non_periodic = np.map(|v| (v, "debugfs.params"));
        }
    }
    let periodic_dw = periodic.map(|(v, _)| v);
    let non_periodic_dw = non_periodic.map(|(v, _)| v);

    let (fifo_dw, source) = if bulk {
        if let Some((np, src)) = non_periodic {
            (np, src)
        } else if let Some((p, src)) = periodic {
            (p, src)
        } else {
            return None;
        }
    } else if let Some((p, src)) = periodic {
        (p, src)
    } else if let Some((np, src)) = non_periodic {
        (np, src)
    } else {
        return None;
    };

    let mut pct = env_u32("LESAVKA_UVC_LIMIT_PCT", 95);
    if pct == 0 {
        pct = 1;
    } else if pct > 100 {
        pct = 100;
    }
    let fifo_bytes = fifo_dw.saturating_mul(4);
    let limit = fifo_bytes.saturating_mul(pct) / 100;
    if limit == 0 {
        return None;
    }

    Some(PayloadCap {
        limit,
        pct,
        source,
        periodic_dw,
        non_periodic_dw,
    })
}

fn read_fifo_min(path: &str) -> Option<u32> {
    let raw = std::fs::read_to_string(path).ok()?;
    raw.split(|c: char| c == ',' || c.is_whitespace())
        .filter_map(|v| v.trim().parse::<u32>().ok())
        .filter(|v| *v > 0)
        .min()
}

fn read_debugfs_fifos() -> Option<(Option<u32>, Option<u32>)> {
    let udc = std::fs::read_dir("/sys/class/udc")
        .ok()?
        .filter_map(|e| e.ok())
        .filter_map(|e| e.file_name().into_string().ok())
        .next()?;
    let path = format!("/sys/kernel/debug/usb/{udc}/params");
    let text = std::fs::read_to_string(path).ok()?;
    let mut periodic: Option<u32> = None;
    let mut non_periodic: Option<u32> = None;
    for line in text.lines() {
        let mut parts = line.splitn(2, ':');
        let key = match parts.next() {
            Some(v) => v.trim(),
            None => continue,
        };
        let val = match parts.next().and_then(|v| v.trim().parse::<u32>().ok()) {
            Some(v) => v,
            None => continue,
        };
        if key == "g_np_tx_fifo_size" {
            non_periodic = Some(val);
        } else if key.starts_with("g_tx_fifo_size[") && val > 0 {
            periodic = Some(match periodic {
                Some(prev) => prev.min(val),
                None => val,
            });
        }
    }
    if periodic.is_none() && non_periodic.is_none() {
        None
    } else {
        Some((periodic, non_periodic))
    }
}

const IOC_NRBITS: u8 = 8;
const IOC_TYPEBITS: u8 = 8;
const IOC_SIZEBITS: u8 = 14;
const IOC_NRSHIFT: u8 = 0;
const IOC_TYPESHIFT: u8 = IOC_NRSHIFT + IOC_NRBITS;
const IOC_SIZESHIFT: u8 = IOC_TYPESHIFT + IOC_TYPEBITS;
const IOC_DIRSHIFT: u8 = IOC_SIZESHIFT + IOC_SIZEBITS;
const IOC_READ: u8 = 2;
const IOC_WRITE: u8 = 1;

fn ioctl_read<T>(type_: u8, nr: u8) -> libc::c_ulong {
    ioc(IOC_READ, type_, nr, std::mem::size_of::<T>() as u16)
}

fn ioctl_write<T>(type_: u8, nr: u8) -> libc::c_ulong {
    ioc(IOC_WRITE, type_, nr, std::mem::size_of::<T>() as u16)
}

fn ioctl_readwrite<T>(type_: u8, nr: u8) -> libc::c_ulong {
    ioc(
        IOC_READ | IOC_WRITE,
        type_,
        nr,
        std::mem::size_of::<T>() as u16,
    )
}

fn ioc(dir: u8, type_: u8, nr: u8, size: u16) -> libc::c_ulong {
    let dir = (dir as u32) << IOC_DIRSHIFT;
    let ty = (type_ as u32) << IOC_TYPESHIFT;
    let nr = (nr as u32) << IOC_NRSHIFT;
    let size = (size as u32) << IOC_SIZESHIFT;
    (dir | ty | nr | size) as libc::c_ulong
}
