use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvalCaseReport {
    pub id: String,
    pub input: String,
    pub expected_sources: Vec<String>,
    pub actual_sources: Vec<String>,
    pub retrieval_recall: f32,
    pub citation_precision: f32,
    pub refusal_expected: bool,
    pub refusal_actual: bool,
    pub refusal_accuracy: f32,
    pub schema_valid: bool,
    pub schema_validity: f32,
    pub latency_ms: f32,
    pub notes: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EvalReport {
    pub dataset_path: String,
    pub total_cases: usize,
    pub retrieval_recall: f32,
    pub citation_precision: f32,
    pub schema_validity: f32,
    pub refusal_accuracy: f32,
    pub avg_latency_ms: f32,
    pub cases: Vec<EvalCaseReport>,
}

impl EvalReport {
    pub fn from_cases(dataset_path: impl Into<String>, cases: Vec<EvalCaseReport>) -> Self {
        let total_cases = cases.len();
        if total_cases == 0 {
            return Self::empty();
        }

        let retrieval_recall =
            cases.iter().map(|case| case.retrieval_recall).sum::<f32>() / total_cases as f32;
        let citation_precision = cases
            .iter()
            .map(|case| case.citation_precision)
            .sum::<f32>()
            / total_cases as f32;
        let schema_validity =
            cases.iter().map(|case| case.schema_validity).sum::<f32>() / total_cases as f32;
        let refusal_accuracy =
            cases.iter().map(|case| case.refusal_accuracy).sum::<f32>() / total_cases as f32;
        let avg_latency_ms =
            cases.iter().map(|case| case.latency_ms).sum::<f32>() / total_cases as f32;

        Self {
            dataset_path: dataset_path.into(),
            total_cases,
            retrieval_recall,
            citation_precision,
            schema_validity,
            refusal_accuracy,
            avg_latency_ms,
            cases,
        }
    }

    pub fn empty() -> Self {
        Self {
            dataset_path: String::new(),
            total_cases: 0,
            retrieval_recall: 0.0,
            citation_precision: 0.0,
            schema_validity: 0.0,
            refusal_accuracy: 0.0,
            avg_latency_ms: 0.0,
            cases: Vec::new(),
        }
    }

    pub fn to_markdown(&self) -> String {
        let mut markdown = String::new();
        markdown.push_str("# Fierros Eval Report\n\n");
        markdown.push_str(&format!("Dataset: `{}`\n\n", self.dataset_path));
        markdown.push_str("## Summary\n\n");
        markdown.push_str("| Metric | Value |\n");
        markdown.push_str("| --- | --- |\n");
        markdown.push_str(&format!("| Total cases | {} |\n", self.total_cases));
        markdown.push_str(&format!(
            "| Retrieval recall | {:.4} |\n",
            self.retrieval_recall
        ));
        markdown.push_str(&format!(
            "| Citation precision | {:.4} |\n",
            self.citation_precision
        ));
        markdown.push_str(&format!(
            "| Refusal accuracy | {:.4} |\n",
            self.refusal_accuracy
        ));
        markdown.push_str(&format!(
            "| Schema validity | {:.4} |\n",
            self.schema_validity
        ));
        markdown.push_str(&format!(
            "| Average latency (ms) | {:.2} |\n",
            self.avg_latency_ms
        ));
        markdown.push('\n');

        markdown.push_str("## Per-case results\n\n");
        markdown
            .push_str("| Case | Recall | Precision | Refusal | Schema | Latency (ms) | Notes |\n");
        markdown.push_str("| --- | --- | --- | --- | --- | --- | --- |\n");
        for case in &self.cases {
            let refusal = format!(
                "{} / {}",
                bool_text(case.refusal_expected),
                bool_text(case.refusal_actual)
            );
            let schema = bool_text(case.schema_valid);
            let notes = if case.notes.is_empty() {
                "-".to_string()
            } else {
                case.notes.join("; ")
            };
            markdown.push_str(&format!(
                "| {} | {:.4} | {:.4} | {} | {} | {:.2} | {} |\n",
                case.id,
                case.retrieval_recall,
                case.citation_precision,
                refusal,
                schema,
                case.latency_ms,
                notes
            ));
        }

        markdown
    }
}

fn bool_text(value: bool) -> &'static str {
    if value {
        "yes"
    } else {
        "no"
    }
}

#[cfg(test)]
mod tests {
    use super::{EvalCaseReport, EvalReport};

    #[test]
    fn empty_report_has_zero_cases() {
        assert_eq!(EvalReport::empty().total_cases, 0);
        assert!(EvalReport::empty().cases.is_empty());
    }

    #[test]
    fn report_aggregates_case_metrics() {
        let report = EvalReport::from_cases(
            "dataset.jsonl",
            vec![
                EvalCaseReport {
                    id: "one".into(),
                    input: "q1".into(),
                    expected_sources: vec!["a".into()],
                    actual_sources: vec!["a".into()],
                    retrieval_recall: 1.0,
                    citation_precision: 0.5,
                    refusal_expected: false,
                    refusal_actual: false,
                    refusal_accuracy: 1.0,
                    schema_valid: true,
                    schema_validity: 1.0,
                    latency_ms: 10.0,
                    notes: Vec::new(),
                },
                EvalCaseReport {
                    id: "two".into(),
                    input: "q2".into(),
                    expected_sources: vec![],
                    actual_sources: vec!["b".into()],
                    retrieval_recall: 0.0,
                    citation_precision: 0.0,
                    refusal_expected: true,
                    refusal_actual: false,
                    refusal_accuracy: 0.0,
                    schema_valid: false,
                    schema_validity: 0.0,
                    latency_ms: 30.0,
                    notes: vec!["missing required phrase".into()],
                },
            ],
        );

        assert_eq!(report.total_cases, 2);
        assert_eq!(report.retrieval_recall, 0.5);
        assert_eq!(report.citation_precision, 0.25);
        assert_eq!(report.refusal_accuracy, 0.5);
        assert_eq!(report.schema_validity, 0.5);
        assert_eq!(report.avg_latency_ms, 20.0);
    }

    #[test]
    fn report_renders_markdown() {
        let report = EvalReport::from_cases(
            "dataset.jsonl",
            vec![EvalCaseReport {
                id: "one".into(),
                input: "q1".into(),
                expected_sources: vec!["a".into()],
                actual_sources: vec!["a".into()],
                retrieval_recall: 1.0,
                citation_precision: 1.0,
                refusal_expected: false,
                refusal_actual: false,
                refusal_accuracy: 1.0,
                schema_valid: true,
                schema_validity: 1.0,
                latency_ms: 10.0,
                notes: Vec::new(),
            }],
        );
        let markdown = report.to_markdown();
        assert!(markdown.contains("# Fierros Eval Report"));
        assert!(markdown.contains("| one |"));
    }
}
