use std::io::Cursor;

use jpeg_decoder::{Decoder as JpegDecoder, PixelFormat};

use crate::video_support::env_u32;

use super::mjpeg_visual_seams::seam_reason;

const DEFAULT_PIXEL_GUARD_SAMPLE_WIDTH: u32 = 160;
const DEFAULT_PIXEL_GUARD_FLAT_STDDEV: u32 = 6;
const DEFAULT_PIXEL_GUARD_FLAT_RUN_PCT: u32 = 16;
const DEFAULT_PIXEL_GUARD_FLAT_MIN_START_PCT: u32 = 45;
const DEFAULT_PIXEL_GUARD_FLAT_MIN_END_PCT: u32 = 72;
const DEFAULT_PIXEL_GUARD_MIN_PIXELS: u32 = 160 * 90;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::video_sinks) enum MjpegVisualArtifactReason {
    FlatBlock {
        start_row_pct: u8,
        end_row_pct: u8,
        run_pct: u8,
    },
    HorizontalTileSeam {
        row_pct: u8,
        coverage_pct: u8,
        delta: u16,
    },
    PartialTileSeam {
        row_pct: u8,
        coverage_pct: u8,
        run_pct: u8,
        delta: u16,
    },
    ChromaTileSeam {
        row_pct: u8,
        coverage_pct: u8,
        delta: u16,
    },
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) struct PixelSample {
    pub(super) luma: i16,
    pub(super) cb: i16,
    pub(super) cr: i16,
}

#[derive(Debug)]
pub(super) struct SampledFrame {
    pub(super) width: usize,
    pub(super) height: usize,
    pub(super) pixels: Vec<PixelSample>,
}

/// Decide whether decoded-pixel artifact filtering is active.
///
/// Inputs: optional `LESAVKA_UVC_MJPEG_PIXEL_GUARD`. Output: true unless the
/// operator explicitly disables it. Why: compressed JPEG entropy catches blank
/// slabs, but the field failures also include tiled/chroma-corrupt images that
/// only become visible after decoding the JPEG pixels.
pub(super) fn mjpeg_pixel_guard_enabled() -> bool {
    std::env::var("LESAVKA_UVC_MJPEG_PIXEL_GUARD")
        .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 sample_width() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_SAMPLE_WIDTH",
        DEFAULT_PIXEL_GUARD_SAMPLE_WIDTH,
    )
    .clamp(64, 320) as usize
}

fn flat_stddev_threshold() -> f64 {
    f64::from(env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_FLAT_STDDEV",
        DEFAULT_PIXEL_GUARD_FLAT_STDDEV,
    )
    .clamp(1, 30))
}

fn flat_run_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_FLAT_RUN_PCT",
        DEFAULT_PIXEL_GUARD_FLAT_RUN_PCT,
    )
    .clamp(5, 60) as usize
}

fn flat_min_start_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_FLAT_MIN_START_PCT",
        DEFAULT_PIXEL_GUARD_FLAT_MIN_START_PCT,
    )
    .min(100) as usize
}

fn flat_min_end_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_FLAT_MIN_END_PCT",
        DEFAULT_PIXEL_GUARD_FLAT_MIN_END_PCT,
    )
    .min(100) as usize
}

fn min_pixels_for_guard() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_MIN_PIXELS",
        DEFAULT_PIXEL_GUARD_MIN_PIXELS,
    )
    .clamp(4096, 3840 * 2160) as usize
}

fn pct(numerator: usize, denominator: usize) -> u8 {
    if denominator == 0 {
        return 0;
    }
    ((numerator.saturating_mul(100) + denominator / 2) / denominator).min(100) as u8
}

fn rgb_to_sample(red: u8, green: u8, blue: u8) -> PixelSample {
    let r = i32::from(red);
    let g = i32::from(green);
    let b = i32::from(blue);
    PixelSample {
        luma: ((77 * r + 150 * g + 29 * b) >> 8) as i16,
        cb: (((-43 * r - 85 * g + 128 * b) >> 8) + 128) as i16,
        cr: (((128 * r - 107 * g - 21 * b) >> 8) + 128) as i16,
    }
}

fn sampled_dimensions(width: usize, height: usize) -> (usize, usize) {
    let sampled_width = sample_width().min(width).max(1);
    let sampled_height = ((height.saturating_mul(sampled_width) + width / 2) / width.max(1))
        .clamp(1, height.max(1));
    (sampled_width, sampled_height)
}

fn sample_rgb_frame(width: usize, height: usize, pixels: &[u8]) -> Option<SampledFrame> {
    if width == 0 || height == 0 || pixels.len() < width.saturating_mul(height).saturating_mul(3) {
        return None;
    }
    let (sampled_width, sampled_height) = sampled_dimensions(width, height);
    let mut sampled = Vec::with_capacity(sampled_width.saturating_mul(sampled_height));
    for sy in 0..sampled_height {
        let src_y = sy.saturating_mul(height) / sampled_height;
        for sx in 0..sampled_width {
            let src_x = sx.saturating_mul(width) / sampled_width;
            let offset = (src_y.saturating_mul(width).saturating_add(src_x)).saturating_mul(3);
            let red = *pixels.get(offset)?;
            let green = *pixels.get(offset + 1)?;
            let blue = *pixels.get(offset + 2)?;
            sampled.push(rgb_to_sample(red, green, blue));
        }
    }
    Some(SampledFrame {
        width: sampled_width,
        height: sampled_height,
        pixels: sampled,
    })
}

fn sample_luma_frame(width: usize, height: usize, pixels: &[u8]) -> Option<SampledFrame> {
    if width == 0 || height == 0 || pixels.len() < width.saturating_mul(height) {
        return None;
    }
    let (sampled_width, sampled_height) = sampled_dimensions(width, height);
    let mut sampled = Vec::with_capacity(sampled_width.saturating_mul(sampled_height));
    for sy in 0..sampled_height {
        let src_y = sy.saturating_mul(height) / sampled_height;
        for sx in 0..sampled_width {
            let src_x = sx.saturating_mul(width) / sampled_width;
            let luma = i16::from(*pixels.get(src_y.saturating_mul(width).saturating_add(src_x))?);
            sampled.push(PixelSample {
                luma,
                cb: 128,
                cr: 128,
            });
        }
    }
    Some(SampledFrame {
        width: sampled_width,
        height: sampled_height,
        pixels: sampled,
    })
}

fn sample_cmyk_frame(width: usize, height: usize, pixels: &[u8]) -> Option<SampledFrame> {
    if width == 0 || height == 0 || pixels.len() < width.saturating_mul(height).saturating_mul(4) {
        return None;
    }
    let mut rgb = Vec::with_capacity(width.saturating_mul(height).saturating_mul(3));
    for chunk in pixels.chunks_exact(4) {
        let c = u16::from(chunk[0]);
        let m = u16::from(chunk[1]);
        let y = u16::from(chunk[2]);
        let k = u16::from(chunk[3]);
        rgb.push(255u16.saturating_sub((c + k).min(255)) as u8);
        rgb.push(255u16.saturating_sub((m + k).min(255)) as u8);
        rgb.push(255u16.saturating_sub((y + k).min(255)) as u8);
    }
    sample_rgb_frame(width, height, &rgb)
}

fn decode_jpeg_sample(bytes: &[u8]) -> Option<SampledFrame> {
    let mut decoder = JpegDecoder::new(Cursor::new(bytes));
    let pixels = decoder.decode().ok()?;
    let info = decoder.info()?;
    let width = usize::from(info.width);
    let height = usize::from(info.height);
    if width.saturating_mul(height) < min_pixels_for_guard() {
        return None;
    }
    match info.pixel_format {
        PixelFormat::L8 => sample_luma_frame(width, height, &pixels),
        PixelFormat::RGB24 => sample_rgb_frame(width, height, &pixels),
        PixelFormat::CMYK32 => sample_cmyk_frame(width, height, &pixels),
        _ => None,
    }
}

fn row_bounds(frame: &SampledFrame) -> std::ops::Range<usize> {
    let start = frame.height / 6;
    let end = (frame.height.saturating_mul(19) / 20).max(start + 1);
    start..end.min(frame.height)
}

fn row_stats(frame: &SampledFrame, row: usize) -> (f64, f64) {
    let start = row.saturating_mul(frame.width);
    let end = start + frame.width;
    let mut sum = 0f64;
    let mut sum_sq = 0f64;
    for pixel in &frame.pixels[start..end] {
        let value = f64::from(pixel.luma);
        sum += value;
        sum_sq += value * value;
    }
    let count = frame.width.max(1) as f64;
    let mean = sum / count;
    let variance = (sum_sq / count - mean * mean).max(0.0);
    (mean, variance.sqrt())
}

fn flat_block_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
    let threshold = flat_stddev_threshold();
    let min_run = (frame.height.saturating_mul(flat_run_pct_threshold()) / 100).max(2);
    let min_start_pct = flat_min_start_pct_threshold();
    let min_end_pct = flat_min_end_pct_threshold();
    let mut best_start = 0usize;
    let mut best_len = 0usize;
    let mut current_start = 0usize;
    let mut current_len = 0usize;
    for row in row_bounds(frame) {
        let (mean, stddev) = row_stats(frame, row);
        let mid_gray = (58.0..=205.0).contains(&mean);
        if mid_gray && stddev <= threshold {
            if current_len == 0 {
                current_start = row;
            }
            current_len += 1;
            if current_len > best_len {
                best_len = current_len;
                best_start = current_start;
            }
        } else {
            current_len = 0;
        }
    }
    if best_len >= min_run {
        let start_row_pct = pct(best_start, frame.height);
        let end_row_pct = pct(best_start + best_len, frame.height);
        if usize::from(start_row_pct) < min_start_pct || usize::from(end_row_pct) < min_end_pct {
            return None;
        }
        return Some(MjpegVisualArtifactReason::FlatBlock {
            start_row_pct,
            end_row_pct,
            run_pct: pct(best_len, frame.height),
        });
    }
    None
}

fn inspect_sampled_frame_for_artifacts(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
    flat_block_reason(frame).or_else(|| seam_reason(frame))
}

/// Decode one MJPEG image and classify visible UVC-bound corruption.
///
/// Inputs: compressed MJPEG bytes. Output: a concrete artifact reason when the
/// decoded pixels contain a gray slab, a full-width tile seam, or a chroma-tile
/// seam. Why: this catches the real field failures where JPEG syntax and byte
/// size are healthy but the picture is visibly torn before it reaches the RCT.
pub(super) fn mjpeg_visual_artifact_reason(bytes: &[u8]) -> Option<MjpegVisualArtifactReason> {
    if !mjpeg_pixel_guard_enabled() {
        return None;
    }
    let frame = decode_jpeg_sample(bytes)?;
    inspect_sampled_frame_for_artifacts(&frame)
}

#[cfg(test)]
pub(super) fn sampled_rgb_frame_for_test(
    width: usize,
    height: usize,
    pixels: &[u8],
) -> Option<MjpegVisualArtifactReason> {
    let frame = sample_rgb_frame(width, height, pixels)?;
    inspect_sampled_frame_for_artifacts(&frame)
}
