//! Persistent quota tracker — counts searches + results returned per
//! calendar day (Central Time, where Mouser's daily quota resets). Fires
//! a one-shot Google Chat (Kel) webhook alert the moment we see the
//! first quota-exhausted response for the day.
//!
//! Stored at `/tmp/adom-mouser-quota.json` so it survives restarts but
//! not reboots (the counter resets at midnight CT anyway).

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;

const STATE_PATH: &str = "/tmp/adom-mouser-quota.json";

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QuotaState {
    /// "YYYY-MM-DD" in America/Chicago (Central). Resets at midnight CT.
    pub date_ct: String,
    pub searches: u64,
    pub results_returned: u64,
    /// Unix epoch seconds (UTC) of first quota-exhausted response today.
    pub first_quota_hit_ts: Option<i64>,
    /// Whether the Kel alert webhook has been fired for today's quota hit.
    pub alerted: bool,
}

static STATE: Mutex<Option<QuotaState>> = Mutex::new(None);

/// Current date in America/Chicago as "YYYY-MM-DD". Shells out to
/// `date` with `TZ=America/Chicago` so we inherit the system tzdb and
/// get DST handled for free. Falls back to UTC if `date` is unavailable.
fn today_ct() -> String {
    let out = std::process::Command::new("date")
        .env("TZ", "America/Chicago")
        .arg("+%Y-%m-%d")
        .output()
        .ok()
        .and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok() } else { None })
        .map(|s| s.trim().to_string())
        .filter(|s| s.len() == 10);
    out.unwrap_or_else(|| {
        // Fallback: UTC date (may be wrong by 1 day near midnight CT).
        let secs = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0) as i64;
        let days = secs / 86400;
        let z = days + 719468;
        let era = if z >= 0 { z } else { z - 146096 } / 146097;
        let doe = (z - era * 146097) as u64;
        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
        let y = yoe as i64 + era * 400;
        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
        let mp = (5 * doy + 2) / 153;
        let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
        let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
        let y = if m <= 2 { y + 1 } else { y };
        format!("{:04}-{:02}-{:02}", y, m, d)
    })
}

fn load() -> QuotaState {
    let raw = fs::read_to_string(STATE_PATH).unwrap_or_default();
    serde_json::from_str::<QuotaState>(&raw).unwrap_or_default()
}

fn save(s: &QuotaState) {
    if let Ok(p) = serde_json::to_string(s) {
        let _ = fs::write(Path::new(STATE_PATH), p);
    }
}

/// Initialize (load-from-disk / reset-if-new-day) and return a clone.
pub fn get() -> QuotaState {
    let mut guard = STATE.lock().unwrap();
    if guard.is_none() {
        *guard = Some(load());
    }
    let s = guard.as_mut().unwrap();
    let today = today_ct();
    if s.date_ct != today {
        // New day — reset.
        *s = QuotaState { date_ct: today, ..Default::default() };
        save(s);
    }
    s.clone()
}

/// Increment on every successful search response.
pub fn record_success(results_returned: u64) {
    let mut guard = STATE.lock().unwrap();
    if guard.is_none() {
        *guard = Some(load());
    }
    let s = guard.as_mut().unwrap();
    let today = today_ct();
    if s.date_ct != today {
        *s = QuotaState { date_ct: today, ..Default::default() };
    }
    s.searches += 1;
    s.results_returned += results_returned;
    save(s);
}

/// Record a quota-exhausted event. Returns `true` exactly once per day
/// (first-hit-of-the-day), so callers can use the return value to
/// trigger a single Kel alert per day.
pub fn record_quota_hit() -> bool {
    let mut guard = STATE.lock().unwrap();
    if guard.is_none() {
        *guard = Some(load());
    }
    let s = guard.as_mut().unwrap();
    let today = today_ct();
    if s.date_ct != today {
        *s = QuotaState { date_ct: today, ..Default::default() };
    }
    s.searches += 1;
    if s.first_quota_hit_ts.is_none() {
        s.first_quota_hit_ts = Some(
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_secs() as i64)
                .unwrap_or(0),
        );
    }
    let should_alert = !s.alerted;
    if should_alert {
        s.alerted = true;
    }
    save(s);
    should_alert
}

/// Post a one-line Google Chat message via an incoming webhook. No-op
/// if `KEL_ALERT_WEBHOOK` is unset. Uses a short timeout so we never
/// block the search-response path on a slow chat API.
pub fn post_kel_alert(text: &str) {
    let url = match std::env::var("KEL_ALERT_WEBHOOK") {
        Ok(u) if !u.is_empty() => u,
        _ => return,
    };
    let body = serde_json::json!({ "text": text });
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(3))
        .build();
    // Fire-and-forget: never panic on failure.
    let _ = agent
        .post(&url)
        .set("Content-Type", "application/json")
        .send_json(body);
}