//! Per-provider circuit breaker registry.
//!
//! Lazily creates one [`nexo_resilience::CircuitBreaker`] per
//! provider id. The fallback executor consults the registry before
//! invoking a provider — if its breaker rejects, the call is
//! skipped without trying the underlying endpoint. After every
//! outcome (success or failure), the executor reports back so the
//! breaker can tick its state machine.
//!
//! Rationale: a flaking provider (Edge WS closed, ElevenLabs 5xx
//! storm) should not poison every synth in the chain — we let the
//! breaker mark it Open for a backoff window and let the next
//! provider in line carry the traffic during that window.

use std::sync::Arc;

use dashmap::DashMap;
use nexo_resilience::{CircuitBreaker, CircuitBreakerConfig};
use serde::{Deserialize, Serialize};

/// Operator-tunable breaker config. Defaults match
/// `CircuitBreakerConfig::default()` (5 fails → open, 2 successes
/// in half-open → close, 10s initial backoff, 120s max).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct BreakerSettings {
    pub failure_threshold: u32,
    pub success_threshold: u32,
    pub initial_backoff_seconds: u64,
    pub max_backoff_seconds: u64,
}

impl Default for BreakerSettings {
    fn default() -> Self {
        Self {
            failure_threshold: 5,
            success_threshold: 2,
            initial_backoff_seconds: 10,
            max_backoff_seconds: 120,
        }
    }
}

impl BreakerSettings {
    fn to_config(&self) -> CircuitBreakerConfig {
        CircuitBreakerConfig {
            failure_threshold: self.failure_threshold,
            success_threshold: self.success_threshold,
            initial_backoff: std::time::Duration::from_secs(self.initial_backoff_seconds),
            max_backoff: std::time::Duration::from_secs(self.max_backoff_seconds),
        }
    }
}

/// Cloneable per-provider breaker registry. Breakers are created
/// on first reference so a provider that's never invoked carries
/// no state.
#[derive(Clone, Default)]
pub struct BreakerRegistry {
    inner: Arc<DashMap<String, Arc<CircuitBreaker>>>,
    settings: BreakerSettings,
}

impl std::fmt::Debug for BreakerRegistry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // CircuitBreaker doesn't impl Debug upstream; we surface
        // just the operator-relevant snapshot.
        f.debug_struct("BreakerRegistry")
            .field("breakers", &self.snapshot())
            .field("settings", &self.settings)
            .finish()
    }
}

impl BreakerRegistry {
    pub fn new(settings: BreakerSettings) -> Self {
        Self {
            inner: Arc::new(DashMap::new()),
            settings,
        }
    }

    /// Retrieve (or lazily create) the breaker for `provider_id`.
    pub fn get(&self, provider_id: &str) -> Arc<CircuitBreaker> {
        if let Some(b) = self.inner.get(provider_id) {
            return b.clone();
        }
        let b = Arc::new(CircuitBreaker::new(
            format!("voice.{provider_id}"),
            self.settings.to_config(),
        ));
        self.inner.insert(provider_id.to_string(), b.clone());
        b
    }

    /// `true` when the breaker would currently reject calls. Tests
    /// + admin stats consume this; the executor uses `allow()` on
    /// the breaker itself to atomically transition Open → HalfOpen.
    pub fn is_open(&self, provider_id: &str) -> bool {
        self.inner
            .get(provider_id)
            .map(|b| b.is_open())
            .unwrap_or(false)
    }

    /// Snapshot for admin RPC `stats`. Returns `(id, is_open)` for
    /// each registered breaker, sorted by id for stable output.
    pub fn snapshot(&self) -> Vec<(String, bool)> {
        let mut v: Vec<(String, bool)> = self
            .inner
            .iter()
            .map(|kv| (kv.key().clone(), kv.value().is_open()))
            .collect();
        v.sort_by(|a, b| a.0.cmp(&b.0));
        v
    }
}

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

    #[test]
    fn get_lazily_creates_breaker() {
        let r = BreakerRegistry::new(BreakerSettings::default());
        let b1 = r.get("edge");
        let b2 = r.get("edge");
        assert!(Arc::ptr_eq(&b1, &b2), "same id must hand back same Arc");
    }

    #[test]
    fn different_providers_get_distinct_breakers() {
        let r = BreakerRegistry::new(BreakerSettings::default());
        let edge = r.get("edge");
        let eleven = r.get("elevenlabs");
        assert!(!Arc::ptr_eq(&edge, &eleven));
    }

    #[test]
    fn snapshot_reports_known_breakers_alphabetically() {
        let r = BreakerRegistry::new(BreakerSettings::default());
        let _ = r.get("edge");
        let _ = r.get("elevenlabs");
        let _ = r.get("azure");
        let snap = r.snapshot();
        let names: Vec<&str> = snap.iter().map(|(k, _)| k.as_str()).collect();
        assert_eq!(names, vec!["azure", "edge", "elevenlabs"]);
        // None have failed yet.
        assert!(snap.iter().all(|(_, open)| !*open));
    }

    #[test]
    fn settings_round_trip_to_config() {
        let s = BreakerSettings {
            failure_threshold: 3,
            success_threshold: 1,
            initial_backoff_seconds: 5,
            max_backoff_seconds: 60,
        };
        let c = s.to_config();
        assert_eq!(c.failure_threshold, 3);
        assert_eq!(c.success_threshold, 1);
        assert_eq!(c.initial_backoff, std::time::Duration::from_secs(5));
        assert_eq!(c.max_backoff, std::time::Duration::from_secs(60));
    }
}
