//! Retry-ladder bookkeeping for the router.
//!
//! Two surfaces:
//!   - `next_model(current_model_id, provider_id, registry)` — registry-backed
//!     escalation: returns the next non-deprecated model in the provider's
//!     cost-output ascending order. Returns None if already at the most
//!     expensive non-deprecated model.
//!   - `next_after_failure(current, depth, failure)` — legacy Claude-only
//!     ladder (kept for backward compatibility with `calibrate.rs`).
//!
//! Constructor Pattern: pure-fn cube, no I/O. Side effects (ledger write)
//! happen in callers.

use crate::pricing::Model;
use crate::registry::Registry;

pub const MAX_ESCALATION_DEPTH: u32 = 2;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EscalationDecision {
    /// Retry on the next-tier model.
    Retry { next: Model, depth: u32 },
    /// No more tiers above OR depth ceiling reached.
    Surrender,
}

// ──────────────────────────────────────────────────────────────────────────────
// Registry-backed escalation
// ──────────────────────────────────────────────────────────────────────────────

/// Result of a registry-backed escalation lookup.
/// Distinguishes "at top of ladder" from "model not found" (e.g. typo).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EscalationResult<'r> {
    /// Caller should retry on this model id.
    Next(&'r str),
    /// `current_model_id` is the most expensive non-deprecated model.
    AtTop,
    /// `current_model_id` is not present in this provider's model list.
    NotFound,
}

/// Given `current_model_id` within `provider_id`, return the next
/// more expensive non-deprecated model from the registry (sorted by
/// `cost_output_per_mtok_micro` ascending).
pub fn next_model<'r>(
    current_model_id: &str,
    provider_id: &str,
    registry: &'r Registry,
) -> EscalationResult<'r> {
    let sorted = registry.models_for_provider(provider_id);
    let mut found_current = false;
    for m in &sorted {
        if found_current {
            return EscalationResult::Next(&m.id);
        }
        if m.id == current_model_id {
            found_current = true;
        }
    }
    if found_current {
        EscalationResult::AtTop
    } else {
        EscalationResult::NotFound
    }
}

// ──────────────────────────────────────────────────────────────────────────────
// Legacy ladder (Claude-only)
// ──────────────────────────────────────────────────────────────────────────────

pub fn next_after_failure(
    current: Model,
    depth: u32,
    outcome_is_failure: bool,
) -> EscalationDecision {
    if !outcome_is_failure {
        return EscalationDecision::Surrender;
    }
    if depth >= MAX_ESCALATION_DEPTH {
        return EscalationDecision::Surrender;
    }
    match current.next_tier() {
        Some(next) => EscalationDecision::Retry { next, depth: depth + 1 },
        None => EscalationDecision::Surrender,
    }
}

// ──────────────────────────────────────────────────────────────────────────────
// Tests
// ──────────────────────────────────────────────────────────────────────────────

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

    fn reg() -> Registry {
        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .parent().unwrap()
            .parent().unwrap()
            .parent().unwrap()
            .join("_blocks/registries");
        Registry::load_from(&dir).expect("registry load failed")
    }

    // ── next_model() tests ────────────────────────────────────────────────

    #[test]
    fn haiku_escalates_to_sonnet_within_anthropic() {
        let r = reg();
        assert_eq!(next_model("claude-haiku-4-5-20251001", "anthropic", &r), EscalationResult::Next("claude-sonnet-4-6"));
    }

    #[test]
    fn sonnet_escalates_to_opus_within_anthropic() {
        let r = reg();
        assert_eq!(next_model("claude-sonnet-4-6", "anthropic", &r), EscalationResult::Next("claude-opus-4-7"));
    }

    /// Finding 5: at-top must be `AtTop`, not `NotFound`.
    #[test]
    fn opus_at_top_returns_at_top() {
        let r = reg();
        assert_eq!(next_model("claude-opus-4-7", "anthropic", &r), EscalationResult::AtTop);
    }

    /// Finding 5: typo / unknown model must be `NotFound`, not `AtTop`.
    #[test]
    fn unknown_model_returns_not_found() {
        let r = reg();
        assert_eq!(next_model("does-not-exist", "anthropic", &r), EscalationResult::NotFound);
    }

    /// Finding 5: `Next` variant carries the correct model id.
    #[test]
    fn next_variant_carries_model_id() {
        let r = reg();
        assert!(matches!(next_model("claude-haiku-4-5-20251001", "anthropic", &r), EscalationResult::Next("claude-sonnet-4-6")));
    }

    #[test]
    fn escalation_skips_deprecated_models() {
        // All current Anthropic models have deprecated_at = "" so this
        // verifies the escalation ladder works without deprecated entries.
        let r = reg();
        let ms = r.models_for_provider("anthropic");
        for m in &ms {
            assert!(!m.is_deprecated(), "{} is deprecated but should not be", m.id);
        }
    }

    // ── legacy next_after_failure() tests ────────────────────────────────

    #[test]
    fn haiku_failure_escalates_to_sonnet() {
        assert_eq!(
            next_after_failure(Model::Haiku45, 0, true),
            EscalationDecision::Retry { next: Model::Sonnet46, depth: 1 }
        );
    }

    #[test]
    fn sonnet_failure_escalates_to_opus() {
        assert_eq!(
            next_after_failure(Model::Sonnet46, 1, true),
            EscalationDecision::Retry { next: Model::Opus47, depth: 2 }
        );
    }

    #[test]
    fn opus_failure_surrenders() {
        assert_eq!(next_after_failure(Model::Opus47, 1, true), EscalationDecision::Surrender);
    }

    #[test]
    fn ceiling_reached_surrenders_even_below_top() {
        assert_eq!(
            next_after_failure(Model::Haiku45, MAX_ESCALATION_DEPTH, true),
            EscalationDecision::Surrender
        );
    }

    #[test]
    fn success_returns_surrender_defensively() {
        assert_eq!(
            next_after_failure(Model::Haiku45, 0, false),
            EscalationDecision::Surrender
        );
    }
}
