use crate::color::{Lab, directional_delta, srgb_to_lab};
use crate::quality::ImageError;
use crate::score::{
    ScoreError, harshness, quality_factor, region_factor, sample_factor, score_candidate,
    try_confidence,
};
use crate::types::{
    CandidateColor, CandidateRanking, CaptureQualityReport, ContrastMap, GoalVector,
    MeasurementStatus, SdkError, SubjectColorProfile,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum MeasureError {
    #[error(transparent)]
    Image(#[from] ImageError),
    #[error("region '{0}' has zero area or no accepted pixels")]
    EmptyRegion(&'static str),
    #[error("region '{0}' is out of bounds")]
    RegionOutOfBounds(&'static str),
    #[error("skin measurement is required for ranking")]
    MissingSkin,
    #[error(transparent)]
    Validation(#[from] SdkError),
    #[error("quality report contains non-finite value in {0}")]
    NonFiniteQuality(&'static str),
    #[error(transparent)]
    Score(#[from] ScoreError),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum RegionMeasurement {
    Measured { lab: Lab, accepted_pixels: usize },
    Missing { reason: String },
}

#[derive(Clone)]
pub struct ManualRegionSamples {
    pub skin: Option<Vec<[u8; 3]>>,
    pub brow: Option<Vec<[u8; 3]>>,
    pub iris: Option<Vec<[u8; 3]>>,
    pub sclera: Option<Vec<[u8; 3]>>,
    pub lip: Option<Vec<[u8; 3]>>,
    pub hair: Option<Vec<[u8; 3]>>,
    pub beard: Option<Vec<[u8; 3]>>,
    pub clothing: Option<Vec<[u8; 3]>>,
}

impl fmt::Debug for ManualRegionSamples {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ManualRegionSamples")
            .field("skin", &redacted_sample_count(&self.skin))
            .field("brow", &redacted_sample_count(&self.brow))
            .field("iris", &redacted_sample_count(&self.iris))
            .field("sclera", &redacted_sample_count(&self.sclera))
            .field("lip", &redacted_sample_count(&self.lip))
            .field("hair", &redacted_sample_count(&self.hair))
            .field("beard", &redacted_sample_count(&self.beard))
            .field("clothing", &redacted_sample_count(&self.clothing))
            .finish()
    }
}

#[derive(Clone)]
pub struct MeasurementInput {
    pub quality: CaptureQualityReport,
    pub mode: crate::types::MeasurementMode,
    pub goal_vector: GoalVector,
    pub candidates: Vec<CandidateColor>,
    pub samples: ManualRegionSamples,
}

impl fmt::Debug for MeasurementInput {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("MeasurementInput")
            .field("quality", &self.quality)
            .field("mode", &self.mode)
            .field("goal_vector", &self.goal_vector)
            .field("candidates", &self.candidates)
            .field("samples", &self.samples)
            .finish()
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct MeasurementReport {
    pub status: MeasurementStatus,
    pub quality: CaptureQualityReport,
    pub subject: Option<SubjectColorProfile>,
    pub contrast_map: ContrastMap,
    pub rankings: Vec<CandidateRanking>,
}

pub struct MeasurementEngine;

impl MeasurementEngine {
    pub fn measure(input: &MeasurementInput) -> Result<MeasurementReport, MeasureError> {
        validate_measurement_input(input)?;
        let skin = measure_region("skin", input.samples.skin.as_deref(), true)?;
        let brow = measure_region("brow", input.samples.brow.as_deref(), false)?;
        let iris = measure_region("iris", input.samples.iris.as_deref(), false)?;
        let sclera = measure_region("sclera", input.samples.sclera.as_deref(), false)?;
        let lip = measure_region("lip", input.samples.lip.as_deref(), false)?;
        let hair = measure_region("hair", input.samples.hair.as_deref(), false)?;
        let beard = measure_region("beard", input.samples.beard.as_deref(), false)?;
        let clothing = measure_region("clothing", input.samples.clothing.as_deref(), false)?;
        let RegionMeasurement::Measured {
            lab: skin_lab,
            accepted_pixels: skin_count,
        } = skin
        else {
            return Ok(MeasurementReport {
                status: MeasurementStatus::InsufficientData,
                quality: input.quality.clone(),
                subject: None,
                contrast_map: ContrastMap {
                    contrasts: Vec::new(),
                },
                rankings: Vec::new(),
            });
        };
        let mut contrasts = Vec::new();
        for (name, region) in [
            ("brow", &brow),
            ("iris", &iris),
            ("sclera", &sclera),
            ("lip", &lip),
            ("hair", &hair),
            ("beard", &beard),
            ("clothing", &clothing),
        ] {
            if let RegionMeasurement::Measured { lab, .. } = region {
                contrasts.push(directional_delta("skin", skin_lab, name, *lab));
            }
        }
        let baseline_feature_michelson = contrasts
            .iter()
            .map(|d| d.michelson_lightness_contrast)
            .max_by(f32::total_cmp);
        let available_weight =
            0.40 + if !input.candidates.is_empty() {
                0.20
            } else {
                0.0
            } + if matches!(iris, RegionMeasurement::Measured { .. }) {
                0.10
            } else {
                0.0
            } + if matches!(lip, RegionMeasurement::Measured { .. }) {
                0.10
            } else {
                0.0
            } + if matches!(hair, RegionMeasurement::Measured { .. })
                || matches!(brow, RegionMeasurement::Measured { .. })
                || matches!(beard, RegionMeasurement::Measured { .. })
            {
                0.10
            } else {
                0.0
            } + if matches!(clothing, RegionMeasurement::Measured { .. }) {
                0.10
            } else {
                0.0
            };
        let confidence_value = try_confidence(
            input.mode,
            quality_factor(&input.quality),
            region_factor(available_weight, 1.0),
            sample_factor(&[
                ("skin", skin_count),
                ("brow", count(&brow)),
                ("iris", count(&iris)),
                ("lip", count(&lip)),
                ("hair", count(&hair)),
                ("beard", count(&beard)),
                ("clothing", count(&clothing)),
            ]),
        )?;
        let mut rankings = input
            .candidates
            .iter()
            .enumerate()
            .map(|(index, candidate)| {
                let candidate_lab = srgb_to_lab(candidate.srgb);
                let candidate_harshness =
                    harshness(crate::color::delta_e00(candidate_lab, skin_lab))?;
                let (score, components) = score_candidate(crate::score::CandidateScoreInput {
                    skin_lab,
                    candidate_lab,
                    lip_lab: lab(&lip),
                    iris_lab: lab(&iris),
                    sclera_lab: lab(&sclera),
                    hair_lab: lab(&hair),
                    brow_lab: lab(&brow),
                    baseline_feature_michelson,
                    goal_vector: input.goal_vector.clone(),
                    confidence: confidence_value,
                })?;
                Ok((
                    index,
                    CandidateRanking {
                        name: candidate.name.clone(),
                        score,
                        confidence: confidence_value,
                        harshness: candidate_harshness,
                        label: "Goal-specific fit with measured uncertainty".to_string(),
                        components,
                    },
                ))
            })
            .collect::<Result<Vec<_>, MeasureError>>()?;
        sort_rankings_by_contract(&mut rankings);
        let rankings = rankings.into_iter().map(|(_, ranking)| ranking).collect();
        Ok(MeasurementReport {
            status: MeasurementStatus::Complete,
            quality: input.quality.clone(),
            subject: Some(SubjectColorProfile {
                skin_lab,
                skin_ita: crate::color::ita_degrees(skin_lab),
                skin_depth_proxy: crate::color::depth_proxy(skin_lab),
            }),
            contrast_map: ContrastMap { contrasts },
            rankings,
        })
    }
}

fn validate_measurement_input(input: &MeasurementInput) -> Result<(), MeasureError> {
    input.goal_vector.clone().parse()?;
    if input.candidates.is_empty() {
        return Err(SdkError::EmptyCandidates.into());
    }
    for candidate in &input.candidates {
        if candidate.name.trim().is_empty() {
            return Err(SdkError::InvalidCandidateColor {
                name: candidate.name.clone(),
            }
            .into());
        }
    }
    validate_quality_report(&input.quality)
}

fn sort_rankings_by_contract(rankings: &mut [(usize, CandidateRanking)]) {
    rankings.sort_by(|left, right| {
        right
            .1
            .score
            .total_cmp(&left.1.score)
            .then_with(|| right.1.confidence.total_cmp(&left.1.confidence))
            .then_with(|| left.1.harshness.total_cmp(&right.1.harshness))
            .then_with(|| left.0.cmp(&right.0))
    });
}

fn validate_quality_report(report: &CaptureQualityReport) -> Result<(), MeasureError> {
    finite_quality("over_clip_fraction", report.over_clip_fraction)?;
    finite_quality("under_clip_fraction", report.under_clip_fraction)?;
    finite_quality_check("white_balance", &report.white_balance)?;
    finite_quality_check("blur", &report.blur)?;
    finite_quality_check("shadow", &report.shadow)?;
    finite_quality_check("face_angle", &report.face_angle)?;
    finite_bool_quality_check("filters_or_makeup", &report.filters_or_makeup)?;
    finite_bool_quality_check("occlusion", &report.occlusion)?;
    finite_bool_quality_check("calibration_card", &report.calibration_card)?;
    Ok(())
}

fn finite_quality(field: &'static str, value: f32) -> Result<(), MeasureError> {
    if value.is_finite() {
        return Ok(());
    }
    Err(MeasureError::NonFiniteQuality(field))
}

fn finite_quality_check(
    field: &'static str,
    check: &crate::types::QualityCheck<f32>,
) -> Result<(), MeasureError> {
    match check {
        crate::types::QualityCheck::Measured { value, deduction } => {
            finite_quality(field, *value)?;
            finite_quality(field, *deduction)
        }
        crate::types::QualityCheck::NotMeasured { deduction, .. } => {
            finite_quality(field, *deduction)
        }
    }
}

fn finite_bool_quality_check(
    field: &'static str,
    check: &crate::types::QualityCheck<bool>,
) -> Result<(), MeasureError> {
    match check {
        crate::types::QualityCheck::Measured { deduction, .. }
        | crate::types::QualityCheck::NotMeasured { deduction, .. } => {
            finite_quality(field, *deduction)
        }
    }
}

fn redacted_sample_count(samples: &Option<Vec<[u8; 3]>>) -> String {
    samples.as_ref().map_or_else(
        || "[MISSING]".to_string(),
        |values| format!("[REDACTED; count={}]", values.len()),
    )
}

fn measure_region(
    name: &'static str,
    samples: Option<&[[u8; 3]]>,
    required: bool,
) -> Result<RegionMeasurement, MeasureError> {
    let Some(samples) = samples else {
        return Ok(RegionMeasurement::Missing {
            reason: "not_provided".to_string(),
        });
    };
    if samples.is_empty() && required {
        return Err(MeasureError::EmptyRegion(name));
    }
    if samples.is_empty() {
        return Ok(RegionMeasurement::Missing {
            reason: "zero_accepted_pixels".to_string(),
        });
    }
    let labs: Vec<_> = samples.iter().map(|rgb| srgb_to_lab(*rgb)).collect();
    if labs
        .iter()
        .any(|lab| !lab.l.is_finite() || !lab.a.is_finite() || !lab.b.is_finite())
    {
        return Err(MeasureError::EmptyRegion(name));
    }
    let count = labs.len();
    let (l, a, b) = labs.iter().fold((0.0, 0.0, 0.0), |acc, lab| {
        (acc.0 + lab.l, acc.1 + lab.a, acc.2 + lab.b)
    });
    Ok(RegionMeasurement::Measured {
        lab: Lab {
            l: l / count as f32,
            a: a / count as f32,
            b: b / count as f32,
        },
        accepted_pixels: count,
    })
}
fn count(region: &RegionMeasurement) -> usize {
    if let RegionMeasurement::Measured {
        accepted_pixels, ..
    } = region
    {
        *accepted_pixels
    } else {
        0
    }
}
fn lab(region: &RegionMeasurement) -> Option<Lab> {
    if let RegionMeasurement::Measured { lab, .. } = region {
        Some(*lab)
    } else {
        None
    }
}

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

    fn ranking(name: &str, score: f32, confidence: f32, harshness: f32) -> CandidateRanking {
        CandidateRanking {
            name: name.to_string(),
            score,
            confidence,
            harshness,
            label: "test".to_string(),
            components: ScoreComponents {
                skin_quality: 0.5,
                feature_readability: 0.5,
                eye_support: 0.5,
                lip_skin_harmony: 0.5,
                hair_brow_coherence: 0.5,
                goal_alignment: 0.5,
                total_penalty: 0.0,
            },
        }
    }

    #[test]
    fn ranking_sort_uses_lower_harshness_then_stable_order() {
        let mut rankings = vec![
            (0, ranking("higher_harshness", 42.0, 0.8, 0.10)),
            (1, ranking("lower_harshness", 42.0, 0.8, 0.01)),
            (2, ranking("stable_a", 40.0, 0.8, 0.01)),
            (3, ranking("stable_b", 40.0, 0.8, 0.01)),
        ];
        sort_rankings_by_contract(&mut rankings);
        let names: Vec<_> = rankings
            .iter()
            .map(|(_, ranking)| ranking.name.as_str())
            .collect();
        assert_eq!(
            names,
            vec![
                "lower_harshness",
                "higher_harshness",
                "stable_a",
                "stable_b"
            ]
        );
    }
}
