/// Summarizes one compressed MJPEG frame without fully decoding pixels.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct MjpegFrameInspection {
    pub bytes: usize,
    pub complete: bool,
    pub width: Option<u16>,
    pub height: Option<u16>,
    pub entropy_bytes: usize,
    pub entropy_distinct_bytes: u16,
    pub entropy_dominant_pct: u8,
    pub entropy_max_run: usize,
}

/// Return whether a decoded buffer looks like one complete JPEG image.
///
/// Inputs: decoded MJPEG bytes. Output: true when SOI, SOS, and EOI markers
/// are present. Why: incomplete JPEGs can still be non-empty buffers; freezing
/// them is safer than letting the UVC helper expose a partially decoded frame.
pub(crate) fn looks_like_complete_jpeg(bytes: &[u8]) -> bool {
    bytes.len() >= 4
        && bytes.starts_with(&[0xff, 0xd8])
        && bytes.ends_with(&[0xff, 0xd9])
        && bytes.windows(2).any(|pair| pair == [0xff, 0xda])
}

fn marker_has_length(marker: u8) -> bool {
    !matches!(marker, 0x01 | 0xd0..=0xd9)
}

fn is_start_of_frame_marker(marker: u8) -> bool {
    matches!(
        marker,
        0xc0 | 0xc1 | 0xc2 | 0xc3 | 0xc5 | 0xc6 | 0xc7 | 0xc9 | 0xca | 0xcb | 0xcd | 0xce
            | 0xcf
    )
}

fn read_be_u16(bytes: &[u8], offset: usize) -> Option<u16> {
    bytes
        .get(offset..offset + 2)
        .map(|value| u16::from_be_bytes([value[0], value[1]]))
}

fn jpeg_entropy_range(bytes: &[u8]) -> Option<std::ops::Range<usize>> {
    let mut idx = 2;
    while idx + 4 <= bytes.len() {
        if bytes[idx] != 0xff {
            idx += 1;
            continue;
        }
        while idx < bytes.len() && bytes[idx] == 0xff {
            idx += 1;
        }
        let marker = *bytes.get(idx)?;
        idx += 1;
        if marker == 0xd9 {
            return None;
        }
        if !marker_has_length(marker) {
            continue;
        }
        let segment_len = usize::from(read_be_u16(bytes, idx)?);
        if marker == 0xda && (segment_len < 2 || idx + segment_len > bytes.len()) {
            let start = idx.min(bytes.len());
            let end = bytes.len().saturating_sub(2);
            return (end > start).then_some(start..end);
        }
        if segment_len < 2 || idx + segment_len > bytes.len() {
            return None;
        }
        if marker == 0xda {
            let start = idx + segment_len;
            let end = bytes.len().saturating_sub(2);
            return (end > start).then_some(start..end);
        }
        idx += segment_len;
    }
    None
}

/// Inspect one MJPEG payload without full pixel decode.
///
/// Inputs: compressed JPEG bytes. Output: dimensions and entropy-shape metrics.
/// Why: these metrics make the guard explainable and cheap enough to run on
/// every UVC-bound frame, while still catching black slabs, truncation, and
/// profile mismatches that byte-size checks alone cannot distinguish.
pub(crate) fn inspect_mjpeg_frame(bytes: &[u8]) -> MjpegFrameInspection {
    let complete = looks_like_complete_jpeg(bytes);
    let mut width = None;
    let mut height = None;
    let mut idx = 2;
    while idx + 4 <= bytes.len() {
        if bytes[idx] != 0xff {
            idx += 1;
            continue;
        }
        while idx < bytes.len() && bytes[idx] == 0xff {
            idx += 1;
        }
        let Some(marker) = bytes.get(idx).copied() else {
            break;
        };
        idx += 1;
        if marker == 0xda || marker == 0xd9 {
            break;
        }
        if !marker_has_length(marker) {
            continue;
        }
        let Some(segment_len) = read_be_u16(bytes, idx).map(usize::from) else {
            break;
        };
        if segment_len < 2 || idx + segment_len > bytes.len() {
            break;
        }
        if is_start_of_frame_marker(marker)
            && segment_len >= 7
            && let (Some(frame_height), Some(frame_width)) =
                (read_be_u16(bytes, idx + 3), read_be_u16(bytes, idx + 5))
        {
            height = Some(frame_height);
            width = Some(frame_width);
            break;
        }
        idx += segment_len;
    }

    let mut inspection = MjpegFrameInspection {
        bytes: bytes.len(),
        complete,
        width,
        height,
        ..MjpegFrameInspection::default()
    };
    let Some(range) = jpeg_entropy_range(bytes) else {
        return inspection;
    };
    let payload = &bytes[range];
    if payload.is_empty() {
        return inspection;
    }

    let mut counts = [0u32; 256];
    let mut max_run = 1usize;
    let mut current_run = 0usize;
    let mut previous = None;
    for byte in payload {
        counts[*byte as usize] += 1;
        if previous == Some(*byte) {
            current_run += 1;
        } else {
            current_run = 1;
            previous = Some(*byte);
        }
        max_run = max_run.max(current_run);
    }

    let distinct = counts.iter().filter(|count| **count > 0).count() as u16;
    let dominant = counts.iter().copied().max().unwrap_or(0) as u64;
    let total = payload.len() as u64;
    inspection.entropy_bytes = payload.len();
    inspection.entropy_distinct_bytes = distinct;
    inspection.entropy_dominant_pct =
        ((dominant.saturating_mul(100) + total.saturating_sub(1)) / total) as u8;
    inspection.entropy_max_run = max_run;
    inspection
}
