//! Metrics computation for LLM prompt evaluation.
//!
//! Computes precision, recall, F1, and cost metrics from fixture results.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use super::fixture::BaselineMetrics;

/// Aggregate metrics from an evaluation run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metrics {
    /// True positives: expected claims that were extracted.
    pub true_positives: usize,

    /// False positives: extracted claims that weren't expected.
    pub false_positives: usize,

    /// False negatives: expected claims that weren't extracted.
    pub false_negatives: usize,

    /// Precision = TP / (TP + FP).
    pub precision: f64,

    /// Recall = TP / (TP + FN).
    pub recall: f64,

    /// F1 = 2 * (P * R) / (P + R).
    pub f1: f64,

    /// Total fixtures evaluated.
    pub total_fixtures: usize,

    /// Fixtures that passed (all expectations met).
    pub passed: usize,

    /// Fixtures that failed (some expectations not met).
    pub failed: usize,

    /// Fixtures that errored (LLM call failed, parse failed).
    pub errored: usize,

    /// Total tokens used (input + output).
    pub total_tokens: u64,

    /// Estimated cost (USD).
    pub estimated_cost_usd: f64,

    /// Average latency in milliseconds.
    pub avg_latency_ms: f64,

    /// Parse success rate (successful parses / total).
    pub parse_success_rate: f64,

    /// Per-category breakdown.
    pub by_category: HashMap<String, CategoryMetrics>,
}

impl Default for Metrics {
    fn default() -> Self {
        Self {
            true_positives: 0,
            false_positives: 0,
            false_negatives: 0,
            precision: 0.0,
            recall: 0.0,
            f1: 0.0,
            total_fixtures: 0,
            passed: 0,
            failed: 0,
            errored: 0,
            total_tokens: 0,
            estimated_cost_usd: 0.0,
            avg_latency_ms: 0.0,
            parse_success_rate: 0.0,
            by_category: HashMap::new(),
        }
    }
}

impl Metrics {
    /// Compute aggregate metrics from fixture results.
    pub fn compute(results: &[FixtureResult]) -> Self {
        let mut tp = 0;
        let mut fp = 0;
        let mut fn_ = 0;
        let mut passed = 0;
        let mut failed = 0;
        let mut errored = 0;
        let mut total_tokens = 0u64;
        let mut total_cost = 0.0;
        let mut total_latency = 0u64;
        let mut parse_successes = 0;
        let mut by_category: HashMap<String, CategoryMetricsBuilder> = HashMap::new();

        for result in results {
            match result.status {
                FixtureStatus::Passed => passed += 1,
                FixtureStatus::Failed => failed += 1,
                FixtureStatus::Errored => errored += 1,
            }

            tp += result.true_positives;
            fp += result.false_positives;
            fn_ += result.false_negatives;
            total_tokens += result.tokens_used as u64;
            total_cost += result.cost_usd;
            total_latency += result.latency_ms;

            if result.parse_success {
                parse_successes += 1;
            }

            // Update category metrics
            let category = by_category.entry(result.category.clone()).or_default();
            category.add(result);
        }

        let total = results.len();
        let precision = if tp + fp > 0 { tp as f64 / (tp + fp) as f64 } else { 0.0 };
        let recall = if tp + fn_ > 0 { tp as f64 / (tp + fn_) as f64 } else { 0.0 };
        let f1 = if precision + recall > 0.0 {
            2.0 * precision * recall / (precision + recall)
        } else {
            0.0
        };

        let avg_latency = if total > 0 { total_latency as f64 / total as f64 } else { 0.0 };

        let parse_success_rate =
            if total > 0 { parse_successes as f64 / total as f64 } else { 0.0 };

        Self {
            true_positives: tp,
            false_positives: fp,
            false_negatives: fn_,
            precision,
            recall,
            f1,
            total_fixtures: total,
            passed,
            failed,
            errored,
            total_tokens,
            estimated_cost_usd: total_cost,
            avg_latency_ms: avg_latency,
            parse_success_rate,
            by_category: by_category.into_iter().map(|(k, v)| (k, v.build())).collect(),
        }
    }
}

/// Metrics for a single category.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryMetrics {
    /// Total fixtures in this category.
    pub fixtures: usize,

    /// Passed fixtures.
    pub passed: usize,

    /// Failed fixtures.
    pub failed: usize,

    /// Precision for this category.
    pub precision: f64,

    /// Recall for this category.
    pub recall: f64,

    /// F1 for this category.
    pub f1: f64,
}

/// Builder for accumulating category metrics.
#[derive(Default)]
struct CategoryMetricsBuilder {
    fixtures: usize,
    passed: usize,
    failed: usize,
    tp: usize,
    fp: usize,
    fn_: usize,
}

impl CategoryMetricsBuilder {
    fn add(&mut self, result: &FixtureResult) {
        self.fixtures += 1;
        match result.status {
            FixtureStatus::Passed => self.passed += 1,
            FixtureStatus::Failed => self.failed += 1,
            FixtureStatus::Errored => self.failed += 1,
        }
        self.tp += result.true_positives;
        self.fp += result.false_positives;
        self.fn_ += result.false_negatives;
    }

    fn build(self) -> CategoryMetrics {
        let precision =
            if self.tp + self.fp > 0 { self.tp as f64 / (self.tp + self.fp) as f64 } else { 0.0 };
        let recall =
            if self.tp + self.fn_ > 0 { self.tp as f64 / (self.tp + self.fn_) as f64 } else { 0.0 };
        let f1 = if precision + recall > 0.0 {
            2.0 * precision * recall / (precision + recall)
        } else {
            0.0
        };

        CategoryMetrics {
            fixtures: self.fixtures,
            passed: self.passed,
            failed: self.failed,
            precision,
            recall,
            f1,
        }
    }
}

/// Result of evaluating a single fixture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureResult {
    /// Fixture ID.
    pub fixture_id: String,

    /// Fixture category.
    pub category: String,

    /// Pass/fail/error status.
    pub status: FixtureStatus,

    /// True positives (matched must_contain).
    pub true_positives: usize,

    /// False positives (unexpected claims).
    pub false_positives: usize,

    /// False negatives (unmatched must_contain).
    pub false_negatives: usize,

    /// Must_not_contain violations.
    pub violations: usize,

    /// Tokens used for this fixture.
    pub tokens_used: usize,

    /// Cost in USD for this fixture.
    pub cost_usd: f64,

    /// Latency in milliseconds.
    pub latency_ms: u64,

    /// Whether JSON parsing succeeded.
    pub parse_success: bool,

    /// Error message if any.
    pub error: Option<String>,

    /// Details about unmatched expectations (for reporting).
    pub unmatched_expectations: Vec<UnmatchedExpectation>,

    /// Details about violations (for reporting).
    pub violation_details: Vec<ViolationDetail>,
}

impl FixtureResult {
    /// Create a result for a successful evaluation.
    #[allow(clippy::too_many_arguments)]
    pub fn success(
        fixture_id: String,
        category: String,
        tp: usize,
        fp: usize,
        fn_: usize,
        violations: usize,
        tokens: usize,
        cost: f64,
        latency: u64,
    ) -> Self {
        let status =
            if fn_ == 0 && violations == 0 { FixtureStatus::Passed } else { FixtureStatus::Failed };

        Self {
            fixture_id,
            category,
            status,
            true_positives: tp,
            false_positives: fp,
            false_negatives: fn_,
            violations,
            tokens_used: tokens,
            cost_usd: cost,
            latency_ms: latency,
            parse_success: true,
            error: None,
            unmatched_expectations: Vec::new(),
            violation_details: Vec::new(),
        }
    }

    /// Create a result for a failed evaluation (error).
    pub fn error(fixture_id: String, category: String, error: String) -> Self {
        Self {
            fixture_id,
            category,
            status: FixtureStatus::Errored,
            true_positives: 0,
            false_positives: 0,
            false_negatives: 0,
            violations: 0,
            tokens_used: 0,
            cost_usd: 0.0,
            latency_ms: 0,
            parse_success: false,
            error: Some(error),
            unmatched_expectations: Vec::new(),
            violation_details: Vec::new(),
        }
    }

    /// Add unmatched expectation details.
    pub fn with_unmatched(mut self, unmatched: Vec<UnmatchedExpectation>) -> Self {
        self.unmatched_expectations = unmatched;
        self
    }

    /// Add violation details.
    pub fn with_violations(mut self, violations: Vec<ViolationDetail>) -> Self {
        self.violation_details = violations;
        self
    }
}

/// Status of a fixture evaluation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FixtureStatus {
    /// All expectations met.
    Passed,
    /// Some expectations not met.
    Failed,
    /// Error during evaluation.
    Errored,
}

/// Details about an unmatched expectation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnmatchedExpectation {
    /// Subject that was expected.
    pub subject: String,
    /// Predicate that was expected.
    pub predicate: String,
    /// Value that was expected.
    pub expected_value: serde_json::Value,
    /// Rationale for this expectation.
    pub rationale: Option<String>,
}

/// Details about a must_not_contain violation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViolationDetail {
    /// Subject that violated.
    pub subject: String,
    /// Predicate that violated.
    pub predicate: String,
    /// Value that was found (but shouldn't have been).
    pub found_value: String,
}

/// Comparison of current metrics against a baseline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselineComparison {
    /// Current metrics.
    pub current: MetricsSummary,

    /// Baseline metrics.
    pub baseline: MetricsSummary,

    /// Precision delta (current - baseline).
    pub precision_delta: f64,

    /// Recall delta (current - baseline).
    pub recall_delta: f64,

    /// F1 delta (current - baseline).
    pub f1_delta: f64,

    /// Regression threshold used.
    pub regression_threshold: f64,

    /// Whether a regression was detected.
    pub has_regression: bool,

    /// Fixtures that regressed (passed before, failed now).
    pub regressed_fixtures: Vec<String>,

    /// Fixtures that improved (failed before, passed now).
    pub improved_fixtures: Vec<String>,
}

/// Summary of metrics for comparison.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSummary {
    /// Precision score.
    pub precision: f64,
    /// Recall score.
    pub recall: f64,
    /// F1 score.
    pub f1: f64,
    /// Total fixtures evaluated.
    pub total_fixtures: usize,
    /// Fixtures that passed.
    pub passed: usize,
}

impl BaselineComparison {
    /// Create a comparison between current metrics and a baseline.
    pub fn compare(current: &Metrics, baseline: &BaselineMetrics, threshold: f64) -> Self {
        let precision_delta = current.precision - baseline.precision;
        let recall_delta = current.recall - baseline.recall;
        let f1_delta = current.f1 - baseline.f1;

        let has_regression =
            precision_delta < -threshold || recall_delta < -threshold || f1_delta < -threshold;

        Self {
            current: MetricsSummary {
                precision: current.precision,
                recall: current.recall,
                f1: current.f1,
                total_fixtures: current.total_fixtures,
                passed: current.passed,
            },
            baseline: MetricsSummary {
                precision: baseline.precision,
                recall: baseline.recall,
                f1: baseline.f1,
                total_fixtures: baseline.total_fixtures,
                passed: 0, // Not tracked in baseline
            },
            precision_delta,
            recall_delta,
            f1_delta,
            regression_threshold: threshold,
            has_regression,
            regressed_fixtures: Vec::new(),
            improved_fixtures: Vec::new(),
        }
    }
}

/// Cost per 1K input tokens (USD).
pub const COST_PER_1K_INPUT_TOKENS: f64 = 0.00025;
/// Cost per 1K output tokens (USD).
pub const COST_PER_1K_OUTPUT_TOKENS: f64 = 0.0005;

/// Estimate cost from token counts.
pub fn estimate_cost(input_tokens: usize, output_tokens: usize) -> f64 {
    let input_cost = (input_tokens as f64 / 1000.0) * COST_PER_1K_INPUT_TOKENS;
    let output_cost = (output_tokens as f64 / 1000.0) * COST_PER_1K_OUTPUT_TOKENS;
    input_cost + output_cost
}

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

    fn make_fixture_result(
        id: &str,
        category: &str,
        passed: bool,
        tp: usize,
        fp: usize,
        fn_: usize,
    ) -> FixtureResult {
        let violations = 0;
        let mut result = FixtureResult::success(
            id.to_string(),
            category.to_string(),
            tp,
            fp,
            fn_,
            violations,
            1000,
            0.01,
            100,
        );
        if !passed {
            result.status = FixtureStatus::Failed;
        }
        result
    }

    #[test]
    fn test_metrics_computation() {
        let results = vec![
            make_fixture_result("tls-001", "tls", true, 2, 0, 0),
            make_fixture_result("tls-002", "tls", false, 1, 1, 1),
            make_fixture_result("jwt-001", "jwt", true, 1, 0, 0),
        ];

        let metrics = Metrics::compute(&results);

        assert_eq!(metrics.total_fixtures, 3);
        assert_eq!(metrics.passed, 2);
        assert_eq!(metrics.failed, 1);
        assert_eq!(metrics.true_positives, 4); // 2 + 1 + 1
        assert_eq!(metrics.false_positives, 1);
        assert_eq!(metrics.false_negatives, 1);

        // Precision = 4 / (4 + 1) = 0.8
        assert!((metrics.precision - 0.8).abs() < 0.01);

        // Recall = 4 / (4 + 1) = 0.8
        assert!((metrics.recall - 0.8).abs() < 0.01);
    }

    #[test]
    fn test_category_metrics() {
        let results = vec![
            make_fixture_result("tls-001", "tls", true, 2, 0, 0),
            make_fixture_result("tls-002", "tls", true, 1, 0, 0),
            make_fixture_result("jwt-001", "jwt", false, 0, 0, 1),
        ];

        let metrics = Metrics::compute(&results);

        let tls_metrics = metrics.by_category.get("tls").expect("tls category");
        assert_eq!(tls_metrics.fixtures, 2);
        assert_eq!(tls_metrics.passed, 2);

        let jwt_metrics = metrics.by_category.get("jwt").expect("jwt category");
        assert_eq!(jwt_metrics.fixtures, 1);
        assert_eq!(jwt_metrics.failed, 1);
    }

    #[test]
    fn test_baseline_comparison() {
        let current = Metrics {
            precision: 0.85,
            recall: 0.76, // -0.04 delta, less than threshold
            f1: 0.80,
            total_fixtures: 10,
            passed: 8,
            ..Default::default()
        };

        let baseline = BaselineMetrics {
            precision: 0.80,
            recall: 0.80,
            f1: 0.80,
            total_fixtures: 10,
            prompt_version: "1.0.0".to_string(),
            model: "gemini-2.0-flash".to_string(),
            measured_at: "2026-02-05".to_string(),
        };

        let comparison = BaselineComparison::compare(&current, &baseline, 0.05);

        assert!((comparison.precision_delta - 0.05).abs() < 0.01);
        assert!((comparison.recall_delta - (-0.04)).abs() < 0.01);
        assert!(!comparison.has_regression); // Below threshold, no regression
    }

    #[test]
    fn test_regression_detection() {
        let current = Metrics { precision: 0.70, recall: 0.80, f1: 0.75, ..Default::default() };

        let baseline = BaselineMetrics {
            precision: 0.80,
            recall: 0.80,
            f1: 0.80,
            total_fixtures: 10,
            prompt_version: "1.0.0".to_string(),
            model: "gemini-2.0-flash".to_string(),
            measured_at: "2026-02-05".to_string(),
        };

        let comparison = BaselineComparison::compare(&current, &baseline, 0.05);

        assert!(comparison.has_regression); // Precision dropped by 0.10 > 0.05 threshold
    }

    #[test]
    fn test_cost_estimation() {
        let cost = estimate_cost(10000, 2000);
        // 10K input = $0.0025, 2K output = $0.001
        assert!((cost - 0.0035).abs() < 0.0001);
    }
}
