use crate::color::{Lab, delta_e00, lab_to_lch, michelson_lightness_contrast};
use crate::types::{GoalVector, MeasurementMode, QualityCheck};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const MODE_FACTOR_ICC_NORMALIZED: f32 = 1.00;
pub const MODE_FACTOR_SRGB_ASSUMED: f32 = 0.94;
pub const MODE_FACTOR_PROFILE_UNSUPPORTED: f32 = 0.78;
pub const MODE_FACTOR_ALLOWED_APPARENT_FALLBACK: f32 = 0.70;
pub const HARSHNESS_MAX: f32 = 0.15;
pub const REDNESS_AMPLIFICATION_MAX: f32 = 0.10;
pub const SALLOWNESS_AMPLIFICATION_MAX: f32 = 0.10;
pub const CONTRAST_COLLAPSE_MAX: f32 = 0.10;
pub const ARTIFICIALITY_MAX: f32 = 0.05;
pub const TOTAL_PENALTY_MAX: f32 = 0.50;

#[derive(Debug, Error, Clone, PartialEq)]
pub enum ScoreError {
    #[error("{field} must be finite")]
    NonFinite { field: &'static str },
    #[error("{field} interval must have distinct finite bounds")]
    InvalidInterval { field: &'static str },
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ScoreComponents {
    pub skin_quality: f32,
    pub feature_readability: f32,
    pub eye_support: f32,
    pub lip_skin_harmony: f32,
    pub hair_brow_coherence: f32,
    pub goal_alignment: f32,
    pub total_penalty: f32,
}

#[derive(Debug, Clone)]
pub struct CandidateScoreInput {
    pub skin_lab: Lab,
    pub candidate_lab: Lab,
    pub lip_lab: Option<Lab>,
    pub iris_lab: Option<Lab>,
    pub sclera_lab: Option<Lab>,
    pub hair_lab: Option<Lab>,
    pub brow_lab: Option<Lab>,
    pub baseline_feature_michelson: Option<f32>,
    pub goal_vector: GoalVector,
    pub confidence: f32,
}

pub fn try_clamp01(x: f32) -> Result<f32, ScoreError> {
    finite_value("clamp01", x).map(|value| value.clamp(0.0, 1.0))
}
pub fn try_ramp(value: f32, start: f32, end: f32) -> Result<f32, ScoreError> {
    let value = finite_value("ramp value", value)?;
    let start = finite_value("ramp start", start)?;
    let end = finite_value("ramp end", end)?;
    if start == end {
        return Err(ScoreError::InvalidInterval { field: "ramp" });
    }
    try_clamp01((value - start) / (end - start))
}
pub fn try_inverse_ramp(value: f32, start: f32, end: f32) -> Result<f32, ScoreError> {
    Ok(1.0 - try_ramp(value, start, end)?)
}
pub fn try_target_score(
    value: f32,
    target: f32,
    tolerance: f32,
    max_distance: f32,
) -> Result<f32, ScoreError> {
    finite_value("target value", value)?;
    finite_value("target target", target)?;
    try_inverse_ramp((value - target).abs(), tolerance, max_distance)
}
pub fn try_interval_score(
    value: f32,
    low: f32,
    high: f32,
    falloff_low: f32,
    falloff_high: f32,
) -> Result<f32, ScoreError> {
    finite_value("interval value", value)?;
    finite_value("interval low", low)?;
    finite_value("interval high", high)?;
    finite_value("interval falloff_low", falloff_low)?;
    finite_value("interval falloff_high", falloff_high)?;
    if value >= low && value <= high {
        return Ok(1.0);
    }
    if value < low {
        return try_ramp(value, falloff_low, low);
    }
    try_inverse_ramp(value, high, falloff_high)
}

pub fn try_confidence(
    mode: MeasurementMode,
    quality_factor: f32,
    region_factor: f32,
    sample_factor: f32,
) -> Result<f32, ScoreError> {
    let quality_factor = finite_value("quality_factor", quality_factor)?;
    let region_factor = finite_value("region_factor", region_factor)?;
    let sample_factor = finite_value("sample_factor", sample_factor)?;
    try_clamp01(mode.factor() * quality_factor * region_factor * sample_factor)
}

pub fn quality_factor(report: &crate::types::CaptureQualityReport) -> f32 {
    let exposure = exposure_deduction(report.over_clip_fraction.max(report.under_clip_fraction));
    let white = measured_deduction(&report.white_balance);
    let blur = measured_deduction(&report.blur);
    let shadow = measured_deduction(&report.shadow);
    1.0 - 0.30_f32.min(exposure + white + blur + shadow)
}

pub fn exposure_deduction(fraction: f32) -> f32 {
    if fraction >= 0.10 {
        0.15
    } else if fraction >= 0.05 {
        0.08
    } else if fraction >= 0.02 {
        0.03
    } else {
        0.0
    }
}
pub fn white_balance_deduction(imbalance: f32) -> f32 {
    if imbalance >= 0.20 {
        0.10
    } else if imbalance >= 0.12 {
        0.05
    } else if imbalance >= 0.08 {
        0.02
    } else {
        0.0
    }
}
pub fn blur_deduction(blur_score: f32) -> f32 {
    if blur_score < 0.08 {
        0.10
    } else if blur_score < 0.15 {
        0.05
    } else {
        0.0
    }
}
pub fn shadow_deduction(unevenness: f32) -> f32 {
    if unevenness >= 0.25 {
        0.08
    } else if unevenness >= 0.15 {
        0.04
    } else {
        0.0
    }
}

pub fn region_factor(available_region_weight: f32, applicable_region_weight: f32) -> f32 {
    (available_region_weight / applicable_region_weight.max(0.001)).max(0.50)
}

pub fn sample_factor(sample_counts: &[(&str, usize)]) -> f32 {
    let mut any_below_min = false;
    let mut any_below_half = false;
    for (region, count) in sample_counts {
        let minimum = if *region == "skin" { 50 } else { 20 };
        if *count == 0 {
            continue;
        }
        any_below_min |= *count < minimum;
        any_below_half |= *count < minimum / 2;
    }
    if any_below_half {
        return 0.70;
    }
    if any_below_min {
        return 0.85;
    }
    1.00
}

pub fn score_candidate(input: CandidateScoreInput) -> Result<(f32, ScoreComponents), ScoreError> {
    validate_score_input(&input)?;
    let candidate_skin_delta_e00 = delta_e00(input.candidate_lab, input.skin_lab);
    let candidate_skin_michelson =
        michelson_lightness_contrast(input.candidate_lab, input.skin_lab);
    let harshness = harshness(candidate_skin_delta_e00)?;
    let redness = redness_amplification(input.skin_lab, input.candidate_lab)?;
    let sallowness = sallowness_amplification(input.skin_lab, input.candidate_lab)?;
    let contrast_collapse =
        contrast_collapse(candidate_skin_michelson, input.baseline_feature_michelson)?;
    let candidate_chroma = lab_to_lch(input.candidate_lab).c;
    let artificiality = artificiality(
        candidate_chroma,
        candidate_skin_delta_e00,
        input.goal_vector.artificiality_tolerance,
    )?;
    let skin_quality = try_clamp01(
        1.0 - 0.40 * (harshness / HARSHNESS_MAX)
            - 0.30 * (redness / REDNESS_AMPLIFICATION_MAX)
            - 0.30 * (sallowness / SALLOWNESS_AMPLIFICATION_MAX),
    )?;
    let feature_readability = try_interval_score(candidate_skin_michelson, 0.10, 0.45, 0.03, 0.75)?
        * try_target_score(
            candidate_skin_michelson,
            input.goal_vector.feature_readability_target,
            0.10,
            0.45,
        )?;
    let eye_support = eye_support(input.candidate_lab, input.iris_lab, input.sclera_lab)?;
    let lip_skin_harmony = lip_skin_harmony(input.candidate_lab, input.lip_lab)?;
    let hair_brow_coherence =
        hair_brow_coherence(input.candidate_lab, input.hair_lab, input.brow_lab)?;
    let goal_alignment = goal_alignment(
        input.candidate_lab,
        candidate_skin_delta_e00,
        candidate_skin_michelson,
        input.goal_vector,
    )?;
    let weighted = 0.20 * skin_quality
        + 0.20 * feature_readability
        + 0.15 * eye_support
        + 0.15 * lip_skin_harmony
        + 0.10 * hair_brow_coherence
        + 0.20 * goal_alignment;
    let total_penalty = (harshness + redness + sallowness + contrast_collapse + artificiality)
        .min(TOTAL_PENALTY_MAX);
    let score = (100.0 * weighted * input.confidence - 100.0 * total_penalty).clamp(0.0, 100.0);
    Ok((
        score,
        ScoreComponents {
            skin_quality,
            feature_readability,
            eye_support,
            lip_skin_harmony,
            hair_brow_coherence,
            goal_alignment,
            total_penalty,
        },
    ))
}

pub(crate) fn clamp01(x: f32) -> f32 {
    try_clamp01(x).expect("internal scoring input must be finite")
}

pub fn harshness(candidate_skin_delta_e00: f32) -> Result<f32, ScoreError> {
    Ok(HARSHNESS_MAX * try_ramp(candidate_skin_delta_e00, 28.0, 55.0)?)
}
pub fn redness_amplification(skin: Lab, candidate: Lab) -> Result<f32, ScoreError> {
    finite_lab("skin", skin)?;
    finite_lab("candidate", candidate)?;
    if skin.a >= 18.0 && candidate.a > skin.a {
        Ok(REDNESS_AMPLIFICATION_MAX * try_ramp(candidate.a - skin.a, 6.0, 18.0)?)
    } else {
        Ok(0.0)
    }
}
pub fn sallowness_amplification(skin: Lab, candidate: Lab) -> Result<f32, ScoreError> {
    finite_lab("skin", skin)?;
    finite_lab("candidate", candidate)?;
    if skin.b >= 22.0 && candidate.b > skin.b {
        Ok(SALLOWNESS_AMPLIFICATION_MAX * try_ramp(candidate.b - skin.b, 8.0, 22.0)?)
    } else {
        Ok(0.0)
    }
}
pub fn contrast_collapse(
    candidate_skin_michelson: f32,
    baseline_feature_michelson: Option<f32>,
) -> Result<f32, ScoreError> {
    finite_value("candidate_skin_michelson", candidate_skin_michelson)?;
    let Some(baseline) = baseline_feature_michelson else {
        return Ok(0.0);
    };
    finite_value("baseline_feature_michelson", baseline)?;
    Ok(CONTRAST_COLLAPSE_MAX
        * ((0.65 - candidate_skin_michelson / baseline.max(0.001)) / 0.35).clamp(0.0, 1.0))
}
pub fn artificiality(
    candidate_chroma: f32,
    candidate_skin_delta_e00: f32,
    tolerance: f32,
) -> Result<f32, ScoreError> {
    finite_value("candidate_chroma", candidate_chroma)?;
    finite_value("candidate_skin_delta_e00", candidate_skin_delta_e00)?;
    finite_value("tolerance", tolerance)?;
    if tolerance <= 0.30 {
        Ok(ARTIFICIALITY_MAX
            * try_ramp(candidate_chroma, 70.0, 100.0)?.max(try_ramp(
                candidate_skin_delta_e00,
                45.0,
                70.0,
            )?))
    } else {
        Ok(0.0)
    }
}

fn goal_alignment(
    candidate_lab: Lab,
    candidate_skin_delta_e00: f32,
    candidate_skin_michelson: f32,
    goal: GoalVector,
) -> Result<f32, ScoreError> {
    let candidate_lch = lab_to_lch(candidate_lab);
    let candidate_warmth = (candidate_lab.b / 60.0).clamp(-1.0, 1.0);
    let candidate_chroma_normalized = try_clamp01(candidate_lch.c / 100.0)?;
    let artificiality_proxy = try_ramp(candidate_lch.c, 70.0, 100.0)?.max(try_ramp(
        candidate_skin_delta_e00,
        45.0,
        70.0,
    )?);
    Ok(
        0.35 * try_target_score(candidate_skin_michelson, goal.contrast_target, 0.10, 0.50)?
            + 0.25 * try_target_score(candidate_warmth, goal.warmth_target, 0.20, 1.00)?
            + 0.25 * try_target_score(candidate_chroma_normalized, goal.chroma_target, 0.15, 1.00)?
            + 0.15
                * try_target_score(
                    artificiality_proxy,
                    goal.artificiality_tolerance,
                    0.15,
                    1.00,
                )?,
    )
}

fn eye_support(candidate: Lab, iris: Option<Lab>, sclera: Option<Lab>) -> Result<f32, ScoreError> {
    finite_lab("candidate", candidate)?;
    match (iris, sclera) {
        (Some(iris), Some(sclera)) => {
            finite_lab("iris", iris)?;
            finite_lab("sclera", sclera)?;
            Ok(0.60 * try_ramp(delta_e00(candidate, iris), 5.0, 24.0)?
                + 0.40 * try_inverse_ramp((candidate.b - sclera.b).abs(), 4.0, 16.0)?)
        }
        (Some(iris), None) => {
            finite_lab("iris", iris)?;
            try_ramp(delta_e00(candidate, iris), 5.0, 24.0)
        }
        _ => Ok(0.50),
    }
}
fn lip_skin_harmony(candidate: Lab, lip: Option<Lab>) -> Result<f32, ScoreError> {
    finite_lab("candidate", candidate)?;
    let Some(lip) = lip else {
        return Ok(0.50);
    };
    finite_lab("lip", lip)?;
    Ok(
        0.50 * try_target_score(delta_e00(candidate, lip), 18.0, 8.0, 35.0)?
            + 0.50 * try_inverse_ramp((candidate.a - lip.a).abs(), 4.0, 18.0)?,
    )
}
fn hair_brow_coherence(
    candidate: Lab,
    hair: Option<Lab>,
    brow: Option<Lab>,
) -> Result<f32, ScoreError> {
    finite_lab("candidate", candidate)?;
    let mut deltas = Vec::new();
    for (name, lab) in [("hair", hair), ("brow", brow)] {
        if let Some(lab) = lab {
            finite_lab(name, lab)?;
            deltas.push(delta_e00(candidate, lab));
        }
    }
    let Some(min_delta) = deltas.into_iter().min_by(f32::total_cmp) else {
        return Ok(0.50);
    };
    try_interval_score(min_delta, 8.0, 38.0, 2.0, 65.0)
}
fn measured_deduction<T>(check: &QualityCheck<T>) -> f32 {
    match check {
        QualityCheck::Measured { deduction, .. } | QualityCheck::NotMeasured { deduction, .. } => {
            *deduction
        }
    }
}
fn validate_score_input(input: &CandidateScoreInput) -> Result<(), ScoreError> {
    finite_lab("skin_lab", input.skin_lab)?;
    finite_lab("candidate_lab", input.candidate_lab)?;
    if let Some(value) = input.baseline_feature_michelson {
        finite_value("baseline_feature_michelson", value)?;
    }
    finite_value("confidence", input.confidence)?;
    input
        .goal_vector
        .clone()
        .parse()
        .map_err(|_| ScoreError::NonFinite {
            field: "goal_vector",
        })?;
    Ok(())
}

fn finite_lab(field: &'static str, lab: Lab) -> Result<(), ScoreError> {
    finite_value(field, lab.l)?;
    finite_value(field, lab.a)?;
    finite_value(field, lab.b)?;
    Ok(())
}

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