//! Tool-surface token efficiency: what each correct tool selection cost in
//! tokens and dollars, plus a letter grade for the selection quality.
//!
//! A large tool surface (many tools, verbose descriptions) costs input tokens
//! on every call and can drown the model in distractors. The headline question
//! is not just "how accurate was selection" (the F1) but "how much did each
//! correct selection cost" (tokens and dollars per true positive), so a fat
//! catalog that scores well but burns tokens reads as inefficient. This folds
//! the F1, the tool-surface token count, and the run cost into one report with
//! assertable `token_efficiency.*` targets, the same dot-path shape the
//! [`reliability`](crate::eval::reliability) and context-rot reports use.
//!
//! The math is deterministic. The grade keys on the selection F1 alone (A at
//! 90, B at 80, C at 70, D at 60, F below); the efficiency numbers ride
//! alongside as the cost-per-correct signals a reviewer gates on.

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::compliance::Grade;
use crate::suite::{Assertion, Matcher};

/// Default selection-F1 floor a `token_efficiency:` gate applies when it
/// declares no `expect:`. Mirrors the equal-function gate's floor: the grade and
/// per-correct figures are reported, but the only thing gated by default is that
/// selection did not collapse.
const DEFAULT_MIN_F1_PERCENT: u32 = 50;

/// The measurements a token-efficiency report folds together.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TokenEfficiencyInput {
    /// Tokens it takes to describe the tool surface presented to the model (the
    /// serialized tools array), the per-call input-token cost of the catalog.
    pub tool_surface_tokens: u32,
    /// Selection F1, an integer percent in `0..=100`.
    pub f1_percent: u32,
    /// Number of correct selections (true positives) across the run.
    pub correct_selections: u32,
    /// Dollars spent on the selection run.
    pub cost_usd: f64,
}

/// A token-efficiency report.
///
/// Serializable so reporters and the audit chain can persist the efficiency
/// numbers next to the headline F1.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TokenEfficiency {
    /// Tokens describing the tool surface.
    pub tool_surface_tokens: u32,
    /// Selection F1, integer percent.
    pub f1_percent: u32,
    /// Correct selections (true positives).
    pub correct_selections: u32,
    /// Dollars spent.
    pub cost_usd: f64,
    /// Letter grade for the selection quality, derived from the F1.
    pub grade: Grade,
}

impl TokenEfficiency {
    /// Fold the measurements into a report, assigning the grade from the F1.
    pub fn from_input(input: TokenEfficiencyInput) -> Self {
        Self {
            tool_surface_tokens: input.tool_surface_tokens,
            f1_percent: input.f1_percent,
            correct_selections: input.correct_selections,
            cost_usd: input.cost_usd,
            grade: grade_from_f1(input.f1_percent),
        }
    }

    /// Tool-surface tokens per correct selection: the per-correct input-token
    /// price of the catalog. `None` when nothing was selected correctly (the
    /// figure is undefined), so a reporter omits it rather than dividing by zero.
    pub fn tokens_per_correct(&self) -> Option<f64> {
        if self.correct_selections == 0 {
            return None;
        }
        Some(f64::from(self.tool_surface_tokens) / f64::from(self.correct_selections))
    }

    /// Dollars per correct selection. `None` when nothing was selected correctly
    /// or nothing was spent, so a free or fully-wrong run prints no figure.
    pub fn cost_per_correct(&self) -> Option<f64> {
        if self.correct_selections == 0 || self.cost_usd <= 0.0 {
            return None;
        }
        Some(self.cost_usd / f64::from(self.correct_selections))
    }

    /// One-line human-readable summary.
    pub fn line(&self) -> String {
        let tpc = self
            .tokens_per_correct()
            .map(|t| format!("{t:.0} tok"))
            .unwrap_or_else(|| "n/a".to_string());
        let cpc = self
            .cost_per_correct()
            .map(|c| format!("${c:.4}"))
            .unwrap_or_else(|| "n/a".to_string());
        format!(
            "token efficiency: {} (F1 {}%), {}/correct, {}/correct over {} tool-surface tokens",
            self.grade.as_str(),
            self.f1_percent,
            tpc,
            cpc,
            self.tool_surface_tokens
        )
    }

    /// Resolve a `token_efficiency.<field>` dot-path target to a number,
    /// mirroring the resolver the other eval reports expose. The grade is a
    /// letter, so it is not a numeric target; gate on `f1` instead. An undefined
    /// per-correct figure resolves to `None`.
    pub fn resolve_target(&self, target: &str) -> Option<f64> {
        match target {
            "token_efficiency.tool_surface_tokens" => Some(f64::from(self.tool_surface_tokens)),
            "token_efficiency.f1" => Some(f64::from(self.f1_percent)),
            "token_efficiency.correct_selections" => Some(f64::from(self.correct_selections)),
            "token_efficiency.cost" => Some(self.cost_usd),
            "token_efficiency.tokens_per_correct" => self.tokens_per_correct(),
            "token_efficiency.cost_per_correct" => self.cost_per_correct(),
            _ => None,
        }
    }

    /// Build the targets object the assertions resolve against.
    pub fn metrics_object(&self) -> Value {
        json!({
            "token_efficiency": {
                "tool_surface_tokens": self.tool_surface_tokens,
                "f1": self.f1_percent,
                "grade": self.grade.as_str(),
                "correct_selections": self.correct_selections,
                "cost": self.cost_usd,
                "tokens_per_correct": self.tokens_per_correct(),
                "cost_per_correct": self.cost_per_correct(),
            }
        })
    }
}

/// Default `expect:` for a `token_efficiency:` gate that declared none: gate
/// only that selection F1 stays at or above the floor. The efficiency numbers
/// (tokens and dollars per correct selection, the letter grade) ride alongside
/// for the reviewer to read, exactly as the equal-function gate does.
pub fn default_expect() -> Vec<Assertion> {
    vec![Assertion {
        target: "token_efficiency.f1".to_string(),
        matcher: Matcher::Schema(json!({ "minimum": DEFAULT_MIN_F1_PERCENT })),
        weight: 1.0,
        name: None,
        transform: None,
    }]
}

/// Map a selection F1 percent to a letter grade. A at 90, B at 80, C at 70, D
/// at 60, F below. `Grade::APlus` is a compliance concept (a passing MAY rule)
/// that does not apply to selection, so the ceiling here is `A`.
fn grade_from_f1(f1_percent: u32) -> Grade {
    match f1_percent {
        90..=u32::MAX => Grade::A,
        80..=89 => Grade::B,
        70..=79 => Grade::C,
        60..=69 => Grade::D,
        _ => Grade::F,
    }
}

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

    fn input(tokens: u32, f1: u32, correct: u32, cost: f64) -> TokenEfficiencyInput {
        TokenEfficiencyInput {
            tool_surface_tokens: tokens,
            f1_percent: f1,
            correct_selections: correct,
            cost_usd: cost,
        }
    }

    #[test]
    fn the_grade_keys_on_the_f1() {
        assert_eq!(
            TokenEfficiency::from_input(input(0, 95, 1, 0.0)).grade,
            Grade::A
        );
        assert_eq!(
            TokenEfficiency::from_input(input(0, 85, 1, 0.0)).grade,
            Grade::B
        );
        assert_eq!(
            TokenEfficiency::from_input(input(0, 72, 1, 0.0)).grade,
            Grade::C
        );
        assert_eq!(
            TokenEfficiency::from_input(input(0, 61, 1, 0.0)).grade,
            Grade::D
        );
        assert_eq!(
            TokenEfficiency::from_input(input(0, 40, 1, 0.0)).grade,
            Grade::F
        );
    }

    #[test]
    fn per_correct_figures_divide_by_the_true_positives() {
        // Exactly-representable values (0.5/2) so the f64 compares cleanly.
        let te = TokenEfficiency::from_input(input(2000, 90, 2, 0.5));
        assert_eq!(te.tokens_per_correct(), Some(1000.0));
        assert_eq!(te.cost_per_correct(), Some(0.25));
    }

    #[test]
    fn nothing_correct_leaves_the_per_correct_figures_undefined() {
        let te = TokenEfficiency::from_input(input(3000, 0, 0, 0.30));
        assert_eq!(te.tokens_per_correct(), None);
        assert_eq!(te.cost_per_correct(), None);
        assert_eq!(te.grade, Grade::F);
    }

    #[test]
    fn a_free_run_reports_no_cost_per_correct() {
        let te = TokenEfficiency::from_input(input(1000, 90, 2, 0.0));
        assert_eq!(te.cost_per_correct(), None);
        // Tokens-per-correct is still defined on a free run.
        assert_eq!(te.tokens_per_correct(), Some(500.0));
    }

    #[test]
    fn targets_resolve_for_assertions() {
        let te = TokenEfficiency::from_input(input(2000, 80, 4, 1.0));
        assert_eq!(te.resolve_target("token_efficiency.f1"), Some(80.0));
        assert_eq!(
            te.resolve_target("token_efficiency.tokens_per_correct"),
            Some(500.0)
        );
        assert_eq!(
            te.resolve_target("token_efficiency.cost_per_correct"),
            Some(0.25)
        );
        assert_eq!(te.resolve_target("token_efficiency.unknown"), None);
    }

    #[test]
    fn default_expect_gates_only_the_f1_floor() {
        let expect = default_expect();
        assert_eq!(expect.len(), 1);
        assert_eq!(expect[0].target, "token_efficiency.f1");
    }

    #[test]
    fn metrics_object_has_the_dot_path_shape() {
        let te = TokenEfficiency::from_input(input(2000, 80, 4, 1.0));
        let m = te.metrics_object();
        assert_eq!(m["token_efficiency"]["grade"], json!("B"));
        assert_eq!(m["token_efficiency"]["cost_per_correct"], json!(0.25));
        assert_eq!(m["token_efficiency"]["tokens_per_correct"], json!(500.0));
    }
}
