use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, RwLock};
static ROOT: RwLock<Option<PathBuf>> = RwLock::new(None);
static WRITE: Mutex<()> = Mutex::new(());
type Usage = BTreeMap<String, BTreeMap<String, i64>>;
pub fn install(project_root: &Path) {
if let Ok(mut g) = ROOT.write() {
*g = Some(project_root.to_path_buf());
}
}
fn file(root: &Path) -> PathBuf {
root.join(".inkhaven").join("ai_usage.json")
}
fn load(root: &Path) -> Usage {
std::fs::read_to_string(file(root))
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
fn today() -> String {
chrono::Utc::now().format("%Y-%m-%d").to_string()
}
pub fn record(category: &str) {
let root = match ROOT.read() {
Ok(g) => g.clone(),
Err(_) => None,
};
let Some(root) = root else { return };
let _guard = WRITE.lock();
let mut usage = load(&root);
*usage
.entry(today())
.or_default()
.entry(category.to_string())
.or_insert(0) += 1;
while usage.len() > 30 {
let Some(oldest) = usage.keys().next().cloned() else { break };
usage.remove(&oldest);
}
let path = file(&root);
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(json) = serde_json::to_string_pretty(&usage) {
let _ = crate::io_atomic::write(&path, json.as_bytes());
}
}
pub fn usage_for(root: &Path, day: &str) -> Vec<(String, i64)> {
load(root)
.get(day)
.map(|m| m.iter().map(|(k, v)| (k.clone(), *v)).collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_tallies_by_category_and_reads_back() {
let dir = tempfile::tempdir().unwrap();
install(dir.path());
record("chat");
record("chat");
record("grammar");
let day = today();
let mut got = usage_for(dir.path(), &day);
got.sort();
assert_eq!(got, vec![("chat".to_string(), 2), ("grammar".to_string(), 1)]);
}
#[test]
fn usage_for_unknown_day_is_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(usage_for(dir.path(), "1999-01-01").is_empty());
}
}