use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use serde_json::json;

/// Ledger: real-time cost estimate for the current session — tokens × model pricing → USD.
pub struct LedgerSkill;

/// Model pricing in USD per 1M tokens (input).
static MODEL_PRICES: &[(&str, f64)] = &[
    ("claude-opus-4", 15.0),
    ("claude-sonnet-4", 3.0),
    ("claude-haiku-4", 0.25),
    ("claude-opus-3-5", 15.0),
    ("claude-sonnet-3-5", 3.0),
    ("claude-haiku-3-5", 0.25),
    ("gpt-4o", 5.0),
    ("gpt-4o-mini", 0.15),
    ("gpt-4-turbo", 10.0),
    ("gpt-4", 30.0),
    ("o1", 15.0),
    ("o1-mini", 3.0),
];

fn price_for_model(model: &str) -> f64 {
    MODEL_PRICES
        .iter()
        .find(|(m, _)| model.contains(m))
        .map(|(_, p)| *p)
        .unwrap_or(3.0)
}

impl Skill for LedgerSkill {
    fn id(&self) -> SkillId {
        SkillId("ledger")
    }
    fn display_name(&self) -> &str {
        "Ledger"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "tokens_sent": { "type": "integer", "description": "Total tokens sent to the model this session" },
                "tokens_saved": { "type": "integer", "description": "Tokens saved by bctx compression this session" },
                "model": { "type": "string", "default": "claude-sonnet-4", "description": "Model ID for pricing" }
            },
            "required": ["tokens_sent"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "cost_usd": { "type": "number" },
                "cost_avoided_usd": { "type": "number" },
                "savings_pct": { "type": "number" },
                "model": { "type": "string" },
                "price_per_1m": { "type": "number" },
                "breakdown": { "type": "string" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let tokens_sent = input.get_u64("tokens_sent").unwrap_or(0) as f64;
        let tokens_saved = input.get_u64("tokens_saved").unwrap_or(0) as f64;
        let model = input
            .get_str("model")
            .unwrap_or("claude-sonnet-4")
            .to_string();

        let price_per_1m = price_for_model(&model);

        let cost_usd = tokens_sent / 1_000_000.0 * price_per_1m;
        let cost_avoided_usd = tokens_saved / 1_000_000.0 * price_per_1m;
        let total_without_bctx = tokens_sent + tokens_saved;
        let savings_pct = if total_without_bctx > 0.0 {
            tokens_saved / total_without_bctx * 100.0
        } else {
            0.0
        };

        let breakdown = format!(
            "{tokens_sent:.0} tokens sent · ${cost_usd:.4} cost · \
             {tokens_saved:.0} tokens saved · ${cost_avoided_usd:.4} avoided ({savings_pct:.1}%)"
        );

        Ok(SkillOutcome::success(
            "ledger",
            json!({
                "cost_usd": (cost_usd * 10000.0).round() / 10000.0,
                "cost_avoided_usd": (cost_avoided_usd * 10000.0).round() / 10000.0,
                "savings_pct": (savings_pct * 10.0).round() / 10.0,
                "model": model,
                "price_per_1m": price_per_1m,
                "breakdown": breakdown
            }),
        ))
    }
}

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

    #[test]
    fn claude_sonnet_4_price_is_3_usd() {
        assert_eq!(price_for_model("claude-sonnet-4"), 3.0);
    }

    #[test]
    fn claude_opus_4_price_is_15_usd() {
        assert_eq!(price_for_model("claude-opus-4"), 15.0);
    }

    #[test]
    fn claude_haiku_4_price_is_25_cents() {
        assert_eq!(price_for_model("claude-haiku-4"), 0.25);
    }

    #[test]
    fn gpt4o_price_is_5_usd() {
        assert_eq!(price_for_model("gpt-4o"), 5.0);
    }

    #[test]
    fn gpt4_turbo_price_is_10_usd() {
        assert_eq!(price_for_model("gpt-4-turbo"), 10.0);
    }

    #[test]
    fn unknown_model_defaults_to_3_usd() {
        assert_eq!(price_for_model("some-future-model-xyz"), 3.0);
    }

    #[test]
    fn cost_calculation_1m_tokens_sonnet() {
        let cost = 1_000_000_f64 / 1_000_000.0 * price_for_model("claude-sonnet-4");
        assert!((cost - 3.0).abs() < 0.001);
    }

    #[test]
    fn savings_pct_equal_split_is_50() {
        let sent = 500_000_f64;
        let saved = 500_000_f64;
        let pct = saved / (sent + saved) * 100.0;
        assert!((pct - 50.0).abs() < 0.001);
    }

    #[test]
    fn zero_tokens_no_savings() {
        let pct = if 0.0_f64 > 0.0 { 1.0 } else { 0.0 };
        assert_eq!(pct, 0.0);
    }
}
