use crate::video_support::env_u32;

use super::mjpeg_visual_guard::{MjpegVisualArtifactReason, SampledFrame};

const DEFAULT_PIXEL_GUARD_SEAM_DELTA: u32 = 34;
const DEFAULT_PIXEL_GUARD_SEAM_COVERAGE_PCT: u32 = 42;
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_DELTA: u32 = 28;
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT: u32 = 32;
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT: u32 = 12;
const DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV: u32 = 14;
const DEFAULT_PIXEL_GUARD_CHROMA_DELTA: u32 = 28;
const DEFAULT_PIXEL_GUARD_CHROMA_COVERAGE_PCT: u32 = 34;

/// Resolve the full-width luma seam threshold.
///
/// Inputs: optional pixel-guard tuning. Output: luma delta threshold. Why:
/// hard horizontal jumps are useful evidence only when tuned above normal
/// camera texture, otherwise synthetic grids look like damage.
fn seam_delta_threshold() -> i16 {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_SEAM_DELTA",
        DEFAULT_PIXEL_GUARD_SEAM_DELTA,
    )
    .clamp(8, 96) as i16
}

/// Resolve the required full-width seam coverage.
///
/// Inputs: optional pixel-guard tuning. Output: percent of sampled row. Why:
/// isolated noisy pixels should not freeze the frame, but a broad tear should.
fn seam_coverage_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_SEAM_COVERAGE_PCT",
        DEFAULT_PIXEL_GUARD_SEAM_COVERAGE_PCT,
    )
    .clamp(10, 90) as usize
}

/// Resolve the partial-seam luma threshold.
///
/// Inputs: optional pixel-guard tuning. Output: luma delta threshold. Why:
/// partial-width tears are subtler than full seams, so they get separate
/// thresholds instead of weakening the full-width detector.
fn partial_seam_delta_threshold() -> i16 {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_DELTA",
        DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_DELTA,
    )
    .clamp(8, 96) as i16
}

/// Resolve the required partial-seam row coverage.
///
/// Inputs: optional pixel-guard tuning. Output: percent of sampled row. Why:
/// the detector should catch a torn block without treating small edge detail
/// as conference-breaking corruption.
fn partial_seam_coverage_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT",
        DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT,
    )
    .clamp(10, 90) as usize
}

/// Resolve the contiguous partial-seam run threshold.
///
/// Inputs: optional pixel-guard tuning. Output: percent of sampled row. Why:
/// requiring a contiguous run separates block tears from scattered texture.
fn partial_seam_run_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT",
        DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT,
    )
    .clamp(4, 60) as usize
}

/// Resolve the partial-seam block-texture threshold.
///
/// Inputs: optional pixel-guard tuning. Output: luma stddev threshold. Why:
/// natural shelf/chair/table edges can look like a partial seam on one row,
/// but damaged codec slabs usually keep both sides of the tear low-texture.
fn partial_seam_block_stddev_threshold() -> f64 {
    f64::from(
        env_u32(
            "LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV",
            DEFAULT_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV,
        )
        .clamp(2, 60),
    )
}

/// Resolve the chroma-seam threshold.
///
/// Inputs: optional pixel-guard tuning. Output: chroma delta threshold. Why:
/// some damaged MJPEG frames keep plausible luma while their color planes jump
/// across a tile boundary.
fn chroma_delta_threshold() -> i16 {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_CHROMA_DELTA",
        DEFAULT_PIXEL_GUARD_CHROMA_DELTA,
    )
    .clamp(8, 96) as i16
}

/// Resolve the required chroma-seam row coverage.
///
/// Inputs: optional pixel-guard tuning. Output: percent of sampled row. Why:
/// broad chroma shifts are visible corruption; narrow color edges are normal
/// scene content.
fn chroma_coverage_pct_threshold() -> usize {
    env_u32(
        "LESAVKA_UVC_MJPEG_PIXEL_GUARD_CHROMA_COVERAGE_PCT",
        DEFAULT_PIXEL_GUARD_CHROMA_COVERAGE_PCT,
    )
    .clamp(10, 90) as usize
}

/// Convert a row/column count into a rounded percentage.
///
/// Inputs: numerator and denominator counts. Output: 0..=100 percent. Why:
/// artifact reasons are telemetry payloads, so they should be stable and easy
/// to read rather than raw sampled-row counts.
fn pct(numerator: usize, denominator: usize) -> u8 {
    if denominator == 0 {
        return 0;
    }
    ((numerator.saturating_mul(100) + denominator / 2) / denominator).min(100) as u8
}

/// Return the sampled rows considered by seam detection.
///
/// Inputs: sampled frame. Output: row range. Why: the top setup rows and very
/// bottom edge are noisy on several cameras, so the detector focuses on the
/// stable body of the image.
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)
}

/// Measure sampled luma variation inside a bounded rectangle.
///
/// Inputs: sampled frame and exclusive x/y bounds. Output: luma standard
/// deviation, or `None` for empty regions. Why: partial seam detection should
/// reject smooth fake tile blocks without punishing naturally textured edges.
fn region_luma_stddev(
    frame: &SampledFrame,
    row_start: usize,
    row_end: usize,
    col_start: usize,
    col_end: usize,
) -> Option<f64> {
    if row_start >= row_end || col_start >= col_end || row_end > frame.height || col_end > frame.width
    {
        return None;
    }
    let mut sum = 0f64;
    let mut sum_sq = 0f64;
    let mut count = 0usize;
    for row in row_start..row_end {
        let start = row.saturating_mul(frame.width);
        for col in col_start..col_end {
            let value = f64::from(frame.pixels[start + col].luma);
            sum += value;
            sum_sq += value * value;
            count += 1;
        }
    }
    if count == 0 {
        return None;
    }
    let count_f = count as f64;
    let mean = sum / count_f;
    Some((sum_sq / count_f - mean * mean).max(0.0).sqrt())
}

/// Measure texture around a candidate partial-width seam.
///
/// Inputs: sampled frame, seam row, and candidate horizontal run. Output:
/// standard deviation for the least slab-like side of the suspect block. Why:
/// real wall/furniture edges often have one smooth side, but damaged UVC tiles
/// tend to create broad low-texture slabs on both sides of the tear.
fn partial_seam_block_stddev(
    frame: &SampledFrame,
    row: usize,
    col_start: usize,
    col_end: usize,
) -> Option<f64> {
    let window = frame.height.saturating_div(12).max(3);
    let above_start = row.saturating_sub(window);
    let above = region_luma_stddev(frame, above_start, row, col_start, col_end);
    let below_end = row.saturating_add(window).min(frame.height);
    let below = region_luma_stddev(frame, row, below_end, col_start, col_end);
    match (above, below) {
        (Some(above), Some(below)) => Some(above.max(below)),
        (Some(_), None) | (None, Some(_)) | (None, None) => None,
    }
}

/// Detect visible luma/chroma tile seams in a sampled MJPEG frame.
///
/// Inputs: decoded sampled frame. Output: the strongest seam reason, if any.
/// Why: the UVC path can receive syntactically valid JPEGs that are visibly
/// torn, and the safer user experience is to repeat the last good frame.
pub(super) fn seam_reason(frame: &SampledFrame) -> Option<MjpegVisualArtifactReason> {
    if frame.height < 4 || frame.width < 8 {
        return None;
    }
    let luma_threshold = seam_delta_threshold();
    let luma_coverage_threshold = seam_coverage_pct_threshold();
    let partial_luma_threshold = partial_seam_delta_threshold();
    let partial_coverage_threshold = partial_seam_coverage_pct_threshold();
    let partial_run_threshold = partial_seam_run_pct_threshold();
    let partial_block_stddev_threshold = partial_seam_block_stddev_threshold();
    let chroma_threshold = chroma_delta_threshold();
    let chroma_coverage_threshold = chroma_coverage_pct_threshold();
    let mut best_luma: Option<(usize, usize, u16)> = None;
    let mut best_partial: Option<(usize, usize, usize, u16)> = None;
    let mut best_chroma: Option<(usize, usize, u16)> = None;
    let mut luma_candidate_rows = Vec::new();
    let mut partial_candidate_rows = Vec::new();
    let mut chroma_candidate_rows = Vec::new();

    for row in row_bounds(frame).skip(1) {
        let prev_start = (row - 1) * frame.width;
        let row_start = row * frame.width;
        let mut luma_hits = 0usize;
        let mut partial_luma_hits = 0usize;
        let mut partial_run = 0usize;
        let mut partial_run_start = 0usize;
        let mut longest_partial_run_start = 0usize;
        let mut longest_partial_run_end = 0usize;
        let mut longest_partial_run = 0usize;
        let mut chroma_hits = 0usize;
        let mut luma_total = 0u32;
        let mut partial_luma_total = 0u32;
        let mut chroma_total = 0u32;
        for col in 0..frame.width {
            let before = frame.pixels[prev_start + col];
            let after = frame.pixels[row_start + col];
            let luma_delta = (after.luma - before.luma).unsigned_abs();
            let chroma_delta =
                ((after.cb - before.cb).unsigned_abs() + (after.cr - before.cr).unsigned_abs()) / 2;
            luma_total += u32::from(luma_delta);
            chroma_total += u32::from(chroma_delta);
            if luma_delta >= luma_threshold as u16 {
                luma_hits += 1;
            }
            if luma_delta >= partial_luma_threshold as u16 {
                partial_luma_hits += 1;
                partial_luma_total += u32::from(luma_delta);
                if partial_run == 0 {
                    partial_run_start = col;
                }
                partial_run += 1;
                if partial_run > longest_partial_run {
                    longest_partial_run = partial_run;
                    longest_partial_run_start = partial_run_start;
                    longest_partial_run_end = col + 1;
                }
            } else {
                partial_run = 0;
            }
            if chroma_delta >= chroma_threshold as u16 {
                chroma_hits += 1;
            }
        }
        let luma_coverage = luma_hits.saturating_mul(100) / frame.width;
        let partial_luma_coverage = partial_luma_hits.saturating_mul(100) / frame.width;
        let partial_luma_run_pct = longest_partial_run.saturating_mul(100) / frame.width;
        let chroma_coverage = chroma_hits.saturating_mul(100) / frame.width;
        let luma_avg = (luma_total / frame.width.max(1) as u32) as u16;
        let partial_luma_avg = (partial_luma_total / partial_luma_hits.max(1) as u32) as u16;
        let chroma_avg = (chroma_total / frame.width.max(1) as u32) as u16;

        if luma_coverage >= luma_coverage_threshold && luma_avg >= luma_threshold as u16 {
            luma_candidate_rows.push(row);
            match best_luma {
                Some((_, best_coverage, best_avg))
                    if best_coverage > luma_coverage
                        || (best_coverage == luma_coverage && best_avg >= luma_avg) => {}
                _ => best_luma = Some((row, luma_coverage, luma_avg)),
            }
        }
        if row >= frame.height / 3
            && partial_luma_coverage >= partial_coverage_threshold
            && partial_luma_run_pct >= partial_run_threshold
            && partial_luma_avg >= partial_luma_threshold as u16
            && partial_seam_block_stddev(
                frame,
                row,
                longest_partial_run_start,
                longest_partial_run_end,
            )
            .is_some_and(|stddev| stddev <= partial_block_stddev_threshold)
        {
            partial_candidate_rows.push(row);
            match best_partial {
                Some((_, best_coverage, best_run, best_avg))
                    if best_coverage > partial_luma_coverage
                        || (best_coverage == partial_luma_coverage && best_run > partial_luma_run_pct)
                        || (best_coverage == partial_luma_coverage
                            && best_run == partial_luma_run_pct
                            && best_avg >= partial_luma_avg) => {}
                _ => {
                    best_partial = Some((
                        row,
                        partial_luma_coverage,
                        partial_luma_run_pct,
                        partial_luma_avg,
                    ));
                }
            }
        }
        if chroma_coverage >= chroma_coverage_threshold && chroma_avg >= chroma_threshold as u16 {
            chroma_candidate_rows.push(row);
            match best_chroma {
                Some((_, best_coverage, best_avg))
                    if best_coverage > chroma_coverage
                        || (best_coverage == chroma_coverage && best_avg >= chroma_avg) => {}
                _ => best_chroma = Some((row, chroma_coverage, chroma_avg)),
            }
        }
    }

    if let Some((row, coverage, delta)) = best_chroma {
        if regular_texture_seam_rows(frame, &chroma_candidate_rows) {
            return None;
        }
        return Some(MjpegVisualArtifactReason::ChromaTileSeam {
            row_pct: pct(row, frame.height),
            coverage_pct: coverage.min(100) as u8,
            delta,
        });
    }
    if let Some((row, coverage, run_pct, delta)) = best_partial {
        if regular_texture_seam_rows(frame, &partial_candidate_rows) {
            return None;
        }
        return Some(MjpegVisualArtifactReason::PartialTileSeam {
            row_pct: pct(row, frame.height),
            coverage_pct: coverage.min(100) as u8,
            run_pct: run_pct.min(100) as u8,
            delta,
        });
    }
    if regular_texture_seam_rows(frame, &luma_candidate_rows) {
        return None;
    }
    best_luma.map(|(row, coverage, delta)| MjpegVisualArtifactReason::HorizontalTileSeam {
        row_pct: pct(row, frame.height),
        coverage_pct: coverage.min(100) as u8,
        delta,
    })
}

/// Decide whether seam rows are regular scene texture rather than corruption.
///
/// Inputs: sampled frame and candidate seam rows. Output: true for regular
/// repeated grid/texture edges. Why: calibration grids create many evenly
/// spaced hard edges; a damaged frame usually has isolated tear lines.
fn regular_texture_seam_rows(frame: &SampledFrame, rows: &[usize]) -> bool {
    if rows.len() < 6 {
        return false;
    }
    let mut unique_rows = rows.to_vec();
    unique_rows.sort_unstable();
    unique_rows.dedup();
    if unique_rows.len() < 6 {
        return false;
    }

    let bounds = row_bounds(frame);
    let row_span = unique_rows
        .last()
        .copied()
        .unwrap_or(0)
        .saturating_sub(unique_rows[0]);
    let guard_span = bounds.end.saturating_sub(bounds.start).max(1);
    if row_span.saturating_mul(100) < guard_span.saturating_mul(30) {
        return false;
    }

    let mut gaps: Vec<usize> = unique_rows
        .windows(2)
        .map(|pair| pair[1].saturating_sub(pair[0]))
        .filter(|gap| *gap > 0)
        .collect();
    if gaps.len() < 5 {
        return false;
    }
    gaps.sort_unstable();
    let median_gap = gaps[gaps.len() / 2];
    if median_gap == 0 || median_gap > frame.height.saturating_div(8).max(2) {
        return false;
    }

    let tolerance = median_gap.saturating_div(3).max(1);
    let consistent_gaps = gaps
        .iter()
        .filter(|gap| gap.abs_diff(median_gap) <= tolerance)
        .count();
    consistent_gaps.saturating_mul(100) >= gaps.len().saturating_mul(70)
}
