use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderHealth {
pub last_success: Option<u64>,
pub last_failure: Option<u64>,
pub last_error: Option<String>,
pub consecutive_failures: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HealthState {
pub providers: HashMap<String, ProviderHealth>,
}
static HEALTH: Mutex<Option<HealthState>> = Mutex::new(None);
fn health_path() -> PathBuf {
super::opencrabs_home().join("provider_health.json")
}
fn ensure_loaded() -> HealthState {
let mut guard = HEALTH.lock().unwrap_or_else(|e| e.into_inner());
if let Some(ref state) = *guard {
return state.clone();
}
let state: HealthState = std::fs::read_to_string(health_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
*guard = Some(state.clone());
state
}
fn flush(state: &HealthState) {
let mut guard = HEALTH.lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(state.clone());
if let Ok(json) = serde_json::to_string_pretty(state) {
let _ = std::fs::write(health_path(), json);
}
}
pub fn record_success(provider_name: &str) {
let mut state = ensure_loaded();
let entry = state
.providers
.entry(provider_name.to_string())
.or_insert(ProviderHealth {
last_success: None,
last_failure: None,
last_error: None,
consecutive_failures: 0,
});
entry.last_success = Some(now_epoch());
entry.consecutive_failures = 0;
flush(&state);
}
pub fn record_failure(provider_name: &str, error: &str) {
let mut state = ensure_loaded();
let entry = state
.providers
.entry(provider_name.to_string())
.or_insert(ProviderHealth {
last_success: None,
last_failure: None,
last_error: None,
consecutive_failures: 0,
});
entry.last_failure = Some(now_epoch());
entry.last_error = Some(error.chars().take(200).collect());
entry.consecutive_failures += 1;
flush(&state);
}
pub fn last_working_provider() -> Option<String> {
let state = ensure_loaded();
state
.providers
.iter()
.filter_map(|(name, health)| health.last_success.map(|ts| (name.clone(), ts)))
.max_by_key(|(_, ts)| *ts)
.map(|(name, _)| name)
}
pub fn get_health(provider_name: &str) -> Option<ProviderHealth> {
let state = ensure_loaded();
state.providers.get(provider_name).cloned()
}
fn now_epoch() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}