//! Inter-juror agreement metrics.
//!
//! Two metrics per evaluation:
//!
//! 1. **Krippendorff's alpha** (Krippendorff, 2011) for ordinal data on
//!    the closed `[0.0, 1.0]` jury score scale, simplified for the
//!    single-unit case (one evaluation, N raters).
//! 2. **Sample variance** (n-1 denominator) as a simpler diagnostic
//!    that pairs well with alpha for users who do not want to memorize
//!    the alpha thresholds.
//!
//! See ADR 0030 for why we pick Krippendorff over Cohen's kappa
//! (multi-rater and ordinal-native) and why we report variance alongside.
//!
//! The single-unit formulation: for jury scores on the bounded interval
//! `[0, 1]`, we compute
//!
//! ```text
//! alpha = 1 - 4 * sample_variance
//! ```
//!
//! which is the closed-form Krippendorff interval alpha when the
//! reference distribution is uniform bimodal at the scale endpoints
//! (the maximum-disagreement reference for a bounded scale). It pins
//! the metric to `[1.0, -inf)` with the published interpretation
//! thresholds (Krippendorff 2011): high agreement above 0.8, tentative
//! between 0.667 and 0.8, low below.

use serde::{Deserialize, Serialize};

/// Per-evaluation agreement output the runner attaches to each jury
/// verdict.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agreement {
    /// Krippendorff's alpha for the juror scores. `None` when too few
    /// jurors (single juror) made the metric undefined.
    pub krippendorff_alpha: Option<f64>,
    /// Sample variance of the juror scores with `n - 1` denominator.
    pub score_variance: f64,
    /// Max minus min of the juror scores.
    pub score_range: f64,
    /// Bucketed interpretation per Krippendorff's published thresholds.
    pub interpretation: AgreementInterpretation,
    /// Threshold below which alpha counts as low agreement. Surfaced so
    /// the reporter can render it next to the metric.
    pub low_agreement_threshold: f64,
}

/// Interpretation bucket for Krippendorff's alpha.
///
/// Thresholds follow Krippendorff (2011): 0.8 for high, 0.667 for
/// tentative.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgreementInterpretation {
    /// Alpha at or above 0.8. Consensus is trustworthy.
    HighAgreement,
    /// Alpha between 0.667 and 0.8. Use with caution.
    TentativeAgreement,
    /// Alpha below 0.667. Consensus is unreliable.
    LowAgreement,
}

/// Decision-grade confidence band attached to a jury verdict.
///
/// This is a one-to-one relabel of [`AgreementInterpretation`] for the
/// people reading the verdict rather than the agreement math. It carries
/// no new statistic: a high-agreement jury reports `High`, a tentative
/// jury `Medium`, and a low-agreement jury `Low`. Reporters and the
/// scorecard surface the band next to the pass or fail so a reader can
/// tell a confident verdict from a coin flip.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfidenceBand {
    /// Derived from `HighAgreement`. The consensus is trustworthy.
    High,
    /// Derived from `TentativeAgreement`. Use the verdict with caution.
    Medium,
    /// Derived from `LowAgreement`. The verdict is a split the engine
    /// did not average away; pair it with the escalation flag.
    Low,
}

impl From<AgreementInterpretation> for ConfidenceBand {
    fn from(interpretation: AgreementInterpretation) -> Self {
        match interpretation {
            AgreementInterpretation::HighAgreement => ConfidenceBand::High,
            AgreementInterpretation::TentativeAgreement => ConfidenceBand::Medium,
            AgreementInterpretation::LowAgreement => ConfidenceBand::Low,
        }
    }
}

impl Agreement {
    /// Map this agreement onto the verdict's confidence band.
    ///
    /// A straight relabel of [`Agreement::interpretation`]; see
    /// [`ConfidenceBand`]. No new statistic enters here.
    pub fn confidence_band(&self) -> ConfidenceBand {
        self.interpretation.into()
    }

    /// True when the jury landed in `LowAgreement` and the verdict should
    /// go to a human rather than be reported as a settled pass or fail.
    ///
    /// The threshold is the existing `low_agreement_threshold` (0.667),
    /// already encoded in `interpretation`; we read the bucket rather
    /// than re-deriving it so the escalation flag and the confidence band
    /// can never disagree.
    pub fn needs_escalation(&self) -> bool {
        self.interpretation == AgreementInterpretation::LowAgreement
    }
}

/// Compute Krippendorff's alpha for a single jury evaluation.
///
/// Returns `None` for fewer than two scores because alpha is undefined
/// without inter-rater pairs. Returns `Some(1.0)` when every score is
/// identical (zero observed disagreement). Otherwise applies the
/// closed-form bounded-interval simplification documented on the module.
pub fn krippendorff_alpha(scores: &[f64]) -> Option<f64> {
    if scores.len() < 2 {
        return None;
    }
    let variance = score_variance(scores);
    if variance == 0.0 {
        return Some(1.0);
    }
    Some(1.0 - 4.0 * variance)
}

/// Sample variance of the juror scores with `n - 1` denominator.
///
/// Returns 0.0 when the slice has fewer than two elements (no
/// meaningful variance) so the function never panics on the runner's
/// edge cases.
pub fn score_variance(scores: &[f64]) -> f64 {
    if scores.len() < 2 {
        return 0.0;
    }
    let n = scores.len() as f64;
    let mean: f64 = scores.iter().copied().sum::<f64>() / n;
    let sum_sq: f64 = scores.iter().map(|s| (s - mean).powi(2)).sum();
    sum_sq / (n - 1.0)
}

/// Max minus min of the juror scores.
///
/// Returns 0.0 on an empty slice. Useful as a quick "is this jury split"
/// signal alongside alpha and variance.
pub fn score_range(scores: &[f64]) -> f64 {
    if scores.is_empty() {
        return 0.0;
    }
    let mut min = f64::INFINITY;
    let mut max = f64::NEG_INFINITY;
    for s in scores {
        if *s < min {
            min = *s;
        }
        if *s > max {
            max = *s;
        }
    }
    max - min
}

/// Bucket an alpha value into the published interpretation tiers.
///
/// Thresholds match Krippendorff (2011): >= 0.8 high, >= 0.667
/// tentative, otherwise low.
pub fn interpret_alpha(alpha: f64) -> AgreementInterpretation {
    if alpha >= 0.8 {
        AgreementInterpretation::HighAgreement
    } else if alpha >= 0.667 {
        AgreementInterpretation::TentativeAgreement
    } else {
        AgreementInterpretation::LowAgreement
    }
}

/// Compose an [`Agreement`] from a juror score slice.
///
/// Convenience helper so the runner does not repeat the three sub-calls
/// at every evaluation site. When fewer than two scores are supplied,
/// `krippendorff_alpha` is `None` and the interpretation defaults to
/// `LowAgreement` so callers see the diagnostic gap.
pub fn compute_agreement(scores: &[f64]) -> Agreement {
    let alpha = krippendorff_alpha(scores);
    let interpretation = match alpha {
        Some(a) => interpret_alpha(a),
        None => AgreementInterpretation::LowAgreement,
    };
    Agreement {
        krippendorff_alpha: alpha,
        score_variance: score_variance(scores),
        score_range: score_range(scores),
        interpretation,
        low_agreement_threshold: 0.667,
    }
}

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

    fn approx(a: f64, b: f64, tol: f64) -> bool {
        (a - b).abs() < tol
    }

    #[test]
    fn alpha_is_one_for_unanimous_jurors() {
        let scores = vec![0.8, 0.8, 0.8];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined for 3 jurors");
        assert!(approx(alpha, 1.0, 1e-9));
    }

    #[test]
    fn alpha_is_one_for_all_identical_scores_at_zero() {
        let scores = vec![0.0, 0.0, 0.0];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined for 3 jurors");
        assert!(approx(alpha, 1.0, 1e-9));
    }

    #[test]
    fn alpha_is_high_for_narrow_disagreement() {
        let scores = vec![0.85, 0.80, 0.82];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined");
        assert!(alpha > 0.95);
        assert_eq!(
            interpret_alpha(alpha),
            AgreementInterpretation::HighAgreement
        );
    }

    #[test]
    fn alpha_is_tentative_for_moderate_disagreement() {
        let scores = vec![0.5, 0.8, 0.75];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined");
        assert!(alpha > 0.667 && alpha < 0.95);
    }

    #[test]
    fn alpha_drops_into_low_for_wide_disagreement() {
        let scores = vec![0.2, 0.7, 0.9];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined");
        assert_eq!(
            interpret_alpha(alpha),
            AgreementInterpretation::LowAgreement
        );
    }

    #[test]
    fn alpha_is_very_low_for_polarized_split() {
        let scores = vec![0.05, 0.95, 0.1, 0.9];
        let alpha = krippendorff_alpha(&scores).expect("alpha defined");
        assert!(alpha < 0.2);
    }

    #[test]
    fn alpha_is_none_for_single_juror() {
        assert!(krippendorff_alpha(&[0.5]).is_none());
        assert!(krippendorff_alpha(&[]).is_none());
    }

    #[test]
    fn variance_uses_n_minus_one_denominator() {
        let scores = vec![0.0, 1.0];
        let var = score_variance(&scores);
        assert!(approx(var, 0.5, 1e-9));
    }

    #[test]
    fn variance_is_zero_for_identical_scores() {
        let scores = vec![0.5, 0.5, 0.5, 0.5];
        assert!(approx(score_variance(&scores), 0.0, 1e-9));
    }

    #[test]
    fn range_returns_max_minus_min() {
        assert!(approx(score_range(&[0.2, 0.7, 0.9]), 0.7, 1e-9));
        assert!(approx(score_range(&[0.5]), 0.0, 1e-9));
        assert!(approx(score_range(&[]), 0.0, 1e-9));
    }

    #[test]
    fn interpret_alpha_uses_published_thresholds() {
        assert_eq!(interpret_alpha(0.9), AgreementInterpretation::HighAgreement);
        assert_eq!(interpret_alpha(0.8), AgreementInterpretation::HighAgreement);
        assert_eq!(
            interpret_alpha(0.75),
            AgreementInterpretation::TentativeAgreement
        );
        assert_eq!(
            interpret_alpha(0.667),
            AgreementInterpretation::TentativeAgreement
        );
        assert_eq!(interpret_alpha(0.5), AgreementInterpretation::LowAgreement);
        assert_eq!(interpret_alpha(-0.5), AgreementInterpretation::LowAgreement);
    }

    #[test]
    fn compute_agreement_for_single_juror_marks_low_agreement() {
        let agreement = compute_agreement(&[0.7]);
        assert!(agreement.krippendorff_alpha.is_none());
        assert_eq!(
            agreement.interpretation,
            AgreementInterpretation::LowAgreement
        );
    }

    #[test]
    fn compute_agreement_for_unanimous_jury_marks_high_agreement() {
        let agreement = compute_agreement(&[0.9, 0.9, 0.9]);
        assert_eq!(agreement.krippendorff_alpha, Some(1.0));
        assert_eq!(
            agreement.interpretation,
            AgreementInterpretation::HighAgreement
        );
    }

    #[test]
    fn confidence_band_maps_each_interpretation_tier() {
        assert_eq!(
            ConfidenceBand::from(AgreementInterpretation::HighAgreement),
            ConfidenceBand::High
        );
        assert_eq!(
            ConfidenceBand::from(AgreementInterpretation::TentativeAgreement),
            ConfidenceBand::Medium
        );
        assert_eq!(
            ConfidenceBand::from(AgreementInterpretation::LowAgreement),
            ConfidenceBand::Low
        );
    }

    #[test]
    fn high_agreement_jury_reports_high_band_and_no_escalation() {
        let agreement = compute_agreement(&[0.9, 0.9, 0.9]);
        assert_eq!(agreement.confidence_band(), ConfidenceBand::High);
        assert!(!agreement.needs_escalation());
    }

    #[test]
    fn split_jury_reports_low_band_and_escalates() {
        let agreement = compute_agreement(&[0.05, 0.95, 0.1, 0.9]);
        assert_eq!(agreement.confidence_band(), ConfidenceBand::Low);
        assert!(agreement.needs_escalation());
    }

    #[test]
    fn confidence_band_serializes_snake_case() {
        let v = serde_json::to_value(ConfidenceBand::High).unwrap();
        assert_eq!(v, serde_json::json!("high"));
        let v = serde_json::to_value(ConfidenceBand::Medium).unwrap();
        assert_eq!(v, serde_json::json!("medium"));
        let v = serde_json::to_value(ConfidenceBand::Low).unwrap();
        assert_eq!(v, serde_json::json!("low"));
    }
}
