use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

pub const UVC_CONTRACT_SCHEMA: &str = "lesavka.uvc-payload-contract.v1";
pub const DEFAULT_UVC_CONTRACT_PATH: &str = "/run/lesavka-uvc-contract.json";
pub const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
pub const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
pub const MAX_MJPEG_FRAME_BYTES: u32 = 8 * 1024 * 1024;
pub const HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC: u32 = 8_000;
const LESAVKA_APP4_MAGIC: &[u8; 4] = b"LSVK";
static CONTRACT_MISSING_READS: AtomicU64 = AtomicU64::new(0);
static CONTRACT_DIVERGENT_READS: AtomicU64 = AtomicU64::new(0);

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct UvcContractReadStats {
    pub missing_or_invalid: u64,
    pub divergent: u64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UvcPayloadContract {
    pub schema: String,
    pub generated_unix_ms: u64,
    pub configfs_available: bool,
    pub commit_observed: bool,
    pub transport: String,
    pub transport_source: String,
    pub streaming_bulk_value: Option<u32>,
    pub width: u32,
    pub height: u32,
    pub fps: u32,
    pub frame_interval_100ns: u32,
    pub streaming_maxpacket: u32,
    pub streaming_maxburst: u32,
    pub configured_budget_bytes_per_sec: u32,
    pub transport_budget_bytes_per_sec: u32,
    pub advertised_frame_bytes: u32,
    pub committed_frame_bytes: u32,
    pub committed_payload_bytes: u32,
    pub requested_frame_cap_bytes: Option<u32>,
    pub enforced_frame_cap_bytes: u32,
    pub divergent: bool,
    pub detail: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UvcContractInput {
    pub configfs_available: bool,
    pub commit_observed: bool,
    pub bulk: bool,
    pub streaming_bulk_value: Option<u32>,
    pub width: u32,
    pub height: u32,
    pub fps: u32,
    pub frame_interval_100ns: u32,
    pub streaming_maxpacket: u32,
    pub streaming_maxburst: u32,
    pub configured_budget_bytes_per_sec: u32,
    pub isochronous_limit_pct: u32,
    pub advertised_frame_bytes: u32,
    pub committed_frame_bytes: u32,
    pub committed_payload_bytes: u32,
    pub requested_frame_cap_bytes: Option<u32>,
}

impl UvcPayloadContract {
    pub fn from_input(input: UvcContractInput) -> Self {
        let fps = input.fps.max(1);
        let maxpacket = input.streaming_maxpacket.max(1);
        let configured_budget = input.configured_budget_bytes_per_sec.max(1);
        let transport_budget = effective_mjpeg_budget_bytes_per_sec(
            configured_budget,
            maxpacket,
            input.bulk,
            input.isochronous_limit_pct,
        );
        let derived_frame_cap = frame_cap_from_budget(transport_budget, fps);
        let advertised = nonzero_or(input.advertised_frame_bytes, derived_frame_cap);
        let committed = nonzero_or(input.committed_frame_bytes, advertised);
        let requested = input.requested_frame_cap_bytes.filter(|value| *value > 0);
        let enforced = requested
            .unwrap_or(derived_frame_cap)
            .min(derived_frame_cap)
            .min(advertised)
            .min(committed)
            .min(MAX_MJPEG_FRAME_BYTES)
            .max(1);
        let bulk_value_matches = if input.bulk {
            input.streaming_bulk_value == Some(1)
        } else {
            input.streaming_bulk_value != Some(1)
        };
        let divergent = !input.configfs_available
            || !input.commit_observed
            || !bulk_value_matches
            || input.advertised_frame_bytes == 0
            || input.committed_frame_bytes == 0
            || input.committed_payload_bytes == 0
            || input.committed_payload_bytes > maxpacket;
        let detail = if divergent {
            format!(
                "live UVC state differs: configfs_available={} commit_observed={} bulk_value={:?} advertised={} committed_frame={} committed_payload={} maxpacket={}",
                input.configfs_available,
                input.commit_observed,
                input.streaming_bulk_value,
                input.advertised_frame_bytes,
                input.committed_frame_bytes,
                input.committed_payload_bytes,
                maxpacket
            )
        } else {
            "live configfs and committed UVC controls agree".to_string()
        };

        Self {
            schema: UVC_CONTRACT_SCHEMA.to_string(),
            generated_unix_ms: unix_time_ms(),
            configfs_available: input.configfs_available,
            commit_observed: input.commit_observed,
            transport: if input.bulk { "bulk" } else { "isochronous" }.to_string(),
            transport_source: "configfs.streaming_bulk.value".to_string(),
            streaming_bulk_value: input.streaming_bulk_value,
            width: input.width,
            height: input.height,
            fps,
            frame_interval_100ns: input.frame_interval_100ns,
            streaming_maxpacket: maxpacket,
            streaming_maxburst: input.streaming_maxburst,
            configured_budget_bytes_per_sec: configured_budget,
            transport_budget_bytes_per_sec: transport_budget,
            advertised_frame_bytes: advertised,
            committed_frame_bytes: committed,
            committed_payload_bytes: input.committed_payload_bytes,
            requested_frame_cap_bytes: requested,
            enforced_frame_cap_bytes: enforced,
            divergent,
            detail,
        }
    }

    pub fn is_usable(&self) -> bool {
        self.schema == UVC_CONTRACT_SCHEMA
            && self.enforced_frame_cap_bytes > 0
            && self.enforced_frame_cap_bytes <= MAX_MJPEG_FRAME_BYTES
            && self.enforced_frame_cap_bytes <= self.advertised_frame_bytes
            && self.enforced_frame_cap_bytes <= self.committed_frame_bytes
    }
}

pub fn contract_path() -> PathBuf {
    std::env::var("LESAVKA_UVC_CONTRACT_PATH")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_UVC_CONTRACT_PATH))
}

pub fn read_contract(path: &Path) -> anyhow::Result<UvcPayloadContract> {
    let bytes = std::fs::read(path)?;
    let contract: UvcPayloadContract = serde_json::from_slice(&bytes)?;
    anyhow::ensure!(contract.is_usable(), "unusable UVC payload contract");
    Ok(contract)
}

pub fn record_missing_contract_read() -> u64 {
    CONTRACT_MISSING_READS.fetch_add(1, Ordering::Relaxed) + 1
}

pub fn record_divergent_contract_read() -> u64 {
    CONTRACT_DIVERGENT_READS.fetch_add(1, Ordering::Relaxed) + 1
}

pub fn contract_read_stats() -> UvcContractReadStats {
    UvcContractReadStats {
        missing_or_invalid: CONTRACT_MISSING_READS.load(Ordering::Relaxed),
        divergent: CONTRACT_DIVERGENT_READS.load(Ordering::Relaxed),
    }
}

pub fn write_contract(path: &Path, contract: &UvcPayloadContract) -> anyhow::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
    let mut bytes = serde_json::to_vec_pretty(contract)?;
    bytes.push(b'\n');
    std::fs::write(&tmp, bytes)?;
    std::fs::rename(&tmp, path)?;
    Ok(())
}

pub fn read_streaming_bulk_value(base: &Path) -> Option<u32> {
    std::fs::read_to_string(base.join("streaming_bulk"))
        .ok()
        .and_then(|value| value.trim().parse::<u32>().ok())
}

pub fn bulk_enabled_from_configfs(base: &Path, requested: bool) -> bool {
    requested && read_streaming_bulk_value(base) == Some(1)
}

pub fn isochronous_budget_bytes_per_sec(maxpacket: u32, pct: u32) -> u32 {
    let bytes = u64::from(maxpacket.max(1))
        .saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
        .saturating_mul(u64::from(pct.clamp(1, 100)))
        / 100;
    bytes.min(u64::from(u32::MAX)) as u32
}

pub fn effective_mjpeg_budget_bytes_per_sec(
    configured: u32,
    maxpacket: u32,
    bulk: bool,
    isochronous_limit_pct: u32,
) -> u32 {
    if bulk {
        configured.max(1)
    } else {
        configured
            .max(1)
            .min(isochronous_budget_bytes_per_sec(
                maxpacket,
                isochronous_limit_pct,
            ))
            .max(1)
    }
}

pub fn frame_cap_from_budget(budget_bytes_per_sec: u32, fps: u32) -> u32 {
    (budget_bytes_per_sec / fps.max(1))
        .max(1)
        .min(MAX_MJPEG_FRAME_BYTES)
}

pub fn clamp_frame_cap(
    requested: Option<u32>,
    advertised: u32,
    budget_bytes_per_sec: u32,
    fps: u32,
) -> u32 {
    let derived = frame_cap_from_budget(budget_bytes_per_sec, fps);
    requested
        .filter(|value| *value > 0)
        .unwrap_or(derived)
        .min(derived)
        .min(nonzero_or(advertised, derived))
        .min(MAX_MJPEG_FRAME_BYTES)
        .max(1)
}

/// Validate the complete marker and entropy structure of one JPEG frame.
///
/// This is intentionally stricter than checking SOI/EOI bytes: a truncated
/// entropy scan can still have those sentinels after a bad splice.
pub fn validate_jpeg_structure(frame: &[u8]) -> bool {
    if frame.len() < 4 || !frame.starts_with(&[0xff, 0xd8]) {
        return false;
    }
    let mut pos = 2usize;
    let mut saw_scan = false;
    while pos < frame.len() {
        if frame[pos] != 0xff {
            return false;
        }
        let marker_start = pos;
        while pos < frame.len() && frame[pos] == 0xff {
            pos += 1;
        }
        if pos >= frame.len() {
            return false;
        }
        let marker = frame[pos];
        pos += 1;
        match marker {
            0xd9 => return saw_scan && pos == frame.len(),
            0xd8 | 0x00 => return false,
            0x01 | 0xd0..=0xd7 => continue,
            _ => {}
        }
        if pos + 2 > frame.len() {
            return false;
        }
        let segment_len = usize::from(u16::from_be_bytes([frame[pos], frame[pos + 1]]));
        if segment_len < 2 || pos + segment_len > frame.len() {
            return false;
        }
        pos += segment_len;
        if marker != 0xda {
            continue;
        }

        saw_scan = true;
        loop {
            if pos >= frame.len() {
                return false;
            }
            if frame[pos] != 0xff {
                pos += 1;
                continue;
            }
            let entropy_marker_start = pos;
            while pos < frame.len() && frame[pos] == 0xff {
                pos += 1;
            }
            if pos >= frame.len() {
                return false;
            }
            match frame[pos] {
                0x00 => pos += 1,
                0xd0..=0xd7 => pos += 1,
                _ => {
                    pos = entropy_marker_start;
                    break;
                }
            }
        }
        debug_assert!(pos >= marker_start);
    }
    false
}

pub fn lesavka_app4_sequence(frame: &[u8]) -> Option<u64> {
    if !frame.starts_with(&[0xff, 0xd8]) {
        return None;
    }
    let mut pos = 2usize;
    while pos + 4 <= frame.len() && frame[pos] == 0xff {
        while pos < frame.len() && frame[pos] == 0xff {
            pos += 1;
        }
        let marker = *frame.get(pos)?;
        pos += 1;
        if marker == 0xda || marker == 0xd9 {
            return None;
        }
        if marker == 0x01 || (0xd0..=0xd7).contains(&marker) {
            continue;
        }
        let segment_len = usize::from(u16::from_be_bytes([*frame.get(pos)?, *frame.get(pos + 1)?]));
        if segment_len < 2 || pos + segment_len > frame.len() {
            return None;
        }
        let payload = &frame[pos + 2..pos + segment_len];
        if marker == 0xe4 && payload.starts_with(LESAVKA_APP4_MAGIC) && payload.len() >= 17 {
            return Some(u64::from_be_bytes(payload[5..13].try_into().ok()?));
        }
        pos += segment_len;
    }
    None
}

pub fn lesavka_app4_integrity_valid(frame: &[u8]) -> Option<bool> {
    if !frame.starts_with(&[0xff, 0xd8]) {
        return None;
    }
    let mut pos = 2usize;
    while pos + 4 <= frame.len() && frame[pos] == 0xff {
        let marker_start = pos;
        while pos < frame.len() && frame[pos] == 0xff {
            pos += 1;
        }
        let marker = *frame.get(pos)?;
        pos += 1;
        if marker == 0xda || marker == 0xd9 {
            return None;
        }
        if marker == 0x01 || (0xd0..=0xd7).contains(&marker) {
            continue;
        }
        let segment_len = usize::from(u16::from_be_bytes([*frame.get(pos)?, *frame.get(pos + 1)?]));
        if segment_len < 2 || pos + segment_len > frame.len() {
            return None;
        }
        let segment_end = pos + segment_len;
        let payload = &frame[pos + 2..segment_end];
        if marker == 0xe4 && payload.starts_with(LESAVKA_APP4_MAGIC) && payload.len() >= 17 {
            let expected = u32::from_be_bytes(payload[13..17].try_into().ok()?);
            let mut original = Vec::with_capacity(frame.len() - (segment_end - marker_start));
            original.extend_from_slice(&frame[..marker_start]);
            original.extend_from_slice(&frame[segment_end..]);
            return Some(crc32_ieee(&original) == expected);
        }
        pos = segment_end;
    }
    None
}

pub fn add_lesavka_app4_integrity(frame: &[u8], sequence: u64) -> Option<Vec<u8>> {
    if frame.len() < 4 || !frame.starts_with(&[0xff, 0xd8]) || !frame.ends_with(&[0xff, 0xd9]) {
        return None;
    }
    let mut payload = Vec::with_capacity(17);
    payload.extend_from_slice(LESAVKA_APP4_MAGIC);
    payload.push(1);
    payload.extend_from_slice(&sequence.to_be_bytes());
    payload.extend_from_slice(&crc32_ieee(frame).to_be_bytes());
    let segment_len = u16::try_from(payload.len() + 2).ok()?;
    let mut marked = Vec::with_capacity(frame.len() + payload.len() + 4);
    marked.extend_from_slice(&frame[..2]);
    marked.extend_from_slice(&[0xff, 0xe4]);
    marked.extend_from_slice(&segment_len.to_be_bytes());
    marked.extend_from_slice(&payload);
    marked.extend_from_slice(&frame[2..]);
    Some(marked)
}

pub fn crc32_ieee(data: &[u8]) -> u32 {
    let mut crc = 0xffff_ffffu32;
    for byte in data {
        crc ^= u32::from(*byte);
        for _ in 0..8 {
            crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
        }
    }
    !crc
}

fn nonzero_or(value: u32, fallback: u32) -> u32 {
    if value == 0 { fallback } else { value }
}

fn unix_time_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
        .min(u128::from(u64::MAX)) as u64
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bulk_requires_live_attribute_value_one() {
        let dir = tempfile::tempdir().expect("tempdir");
        assert!(!bulk_enabled_from_configfs(dir.path(), true));
        std::fs::write(dir.path().join("streaming_bulk"), "0\n").expect("write zero");
        assert!(!bulk_enabled_from_configfs(dir.path(), true));
        std::fs::write(dir.path().join("streaming_bulk"), "1\n").expect("write one");
        assert!(bulk_enabled_from_configfs(dir.path(), true));
        assert!(!bulk_enabled_from_configfs(dir.path(), false));
    }

    #[test]
    fn explicit_frame_cap_cannot_exceed_wire_or_descriptor_contract() {
        assert_eq!(
            clamp_frame_cap(Some(900_000), 200_000, 4_500_000, 30),
            150_000
        );
        assert_eq!(
            clamp_frame_cap(Some(100_000), 200_000, 4_500_000, 30),
            100_000
        );
        assert_eq!(
            clamp_frame_cap(Some(149_999), 200_000, 4_500_000, 30),
            149_999
        );
        assert_eq!(
            clamp_frame_cap(Some(150_001), 200_000, 4_500_000, 30),
            150_000
        );
        assert_eq!(clamp_frame_cap(None, 100_000, 30_000, 30), 1_000);
    }

    #[test]
    fn contract_round_trips_and_bounds_requested_override() {
        let contract = UvcPayloadContract::from_input(UvcContractInput {
            configfs_available: true,
            commit_observed: true,
            bulk: false,
            streaming_bulk_value: Some(0),
            width: 1280,
            height: 720,
            fps: 30,
            frame_interval_100ns: 333_333,
            streaming_maxpacket: 1024,
            streaming_maxburst: 0,
            configured_budget_bytes_per_sec: 10_000_000,
            isochronous_limit_pct: 85,
            advertised_frame_bytes: 232_106,
            committed_frame_bytes: 232_106,
            committed_payload_bytes: 1024,
            requested_frame_cap_bytes: Some(999_999),
        });
        assert!(contract.configfs_available);
        assert!(contract.commit_observed);
        assert_eq!(contract.transport, "isochronous");
        assert_eq!(contract.enforced_frame_cap_bytes, 232_106);
        assert!(!contract.divergent);

        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("contract.json");
        write_contract(&path, &contract).expect("write contract");
        assert_eq!(read_contract(&path).expect("read contract"), contract);
    }

    #[test]
    fn contract_marks_missing_configfs_and_unobserved_commit_as_divergent() {
        let contract = UvcPayloadContract::from_input(UvcContractInput {
            configfs_available: false,
            commit_observed: false,
            bulk: false,
            streaming_bulk_value: None,
            width: 1280,
            height: 720,
            fps: 30,
            frame_interval_100ns: 333_333,
            streaming_maxpacket: 1024,
            streaming_maxburst: 0,
            configured_budget_bytes_per_sec: 4_500_000,
            isochronous_limit_pct: 85,
            advertised_frame_bytes: 150_000,
            committed_frame_bytes: 150_000,
            committed_payload_bytes: 1024,
            requested_frame_cap_bytes: None,
        });

        assert!(contract.divergent);
        assert!(contract.detail.contains("configfs_available=false"));
        assert!(contract.detail.contains("commit_observed=false"));
    }

    #[test]
    fn strict_jpeg_validation_rejects_truncation_and_trailing_splices() {
        let valid = include_bytes!("bin/lesavka_uvc/idle_1280x720_black.jpg");
        assert!(validate_jpeg_structure(valid));
        assert!(!validate_jpeg_structure(&valid[..valid.len() - 2]));

        let mut trailing = valid.to_vec();
        trailing.extend_from_slice(&[0xff, 0xd9]);
        assert!(!validate_jpeg_structure(&trailing));

        let mut broken_segment = valid.to_vec();
        broken_segment[4] = 0xff;
        broken_segment[5] = 0xff;
        assert!(!validate_jpeg_structure(&broken_segment));
    }

    #[test]
    fn app4_integrity_marker_round_trips_sequence_and_keeps_jpeg_valid() {
        let valid = include_bytes!("bin/lesavka_uvc/idle_1280x720_black.jpg");
        let marked = add_lesavka_app4_integrity(valid, 0x0102_0304_0506_0708).expect("mark JPEG");
        assert_eq!(lesavka_app4_sequence(&marked), Some(0x0102_0304_0506_0708));
        assert_eq!(lesavka_app4_integrity_valid(&marked), Some(true));
        assert!(validate_jpeg_structure(&marked));

        let mut corrupted = marked;
        let index = corrupted.len() - 10;
        corrupted[index] ^= 1;
        assert_eq!(lesavka_app4_integrity_valid(&corrupted), Some(false));
    }

    #[test]
    fn downstream_uvc_consumers_use_shared_budget_math() {
        let spool = include_str!("video_sinks/mjpeg_spool.rs");
        let helper = include_str!("bin/lesavka-uvc.real.inc");

        assert!(spool.contains("uvc_contract::effective_mjpeg_budget_bytes_per_sec"));
        assert!(spool.contains("uvc_contract::clamp_frame_cap"));
        assert!(helper.contains("uvc_contract::effective_mjpeg_budget_bytes_per_sec"));
        assert!(helper.contains("uvc_contract::frame_cap_from_budget"));
        assert!(!spool.contains("HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC"));
        assert!(!helper.contains("saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS"));
    }
}
