use std::collections::HashMap;
use anyhow::{anyhow, Result};
use easy_error::Error as BundError;
use rust_dynamic::value::Value;
use rust_multistackvm::multistackvm::VM;
use uuid::Uuid;
use super::helpers::{active_config, active_store, push};
use crate::progress::aggregates::{BookProgress, ProgressSnapshot};
use crate::progress::store::ProgressStore;
use crate::progress::{count_words, LiveTotals};
use crate::project::ProjectLayout;
use crate::store::hierarchy::Hierarchy;
use crate::store::{NodeKind, Store};
const ALL_HISTORY_DAYS: i64 = 365 * 200;
pub fn register(vm: &mut VM) -> Result<()> {
let words: &[(&str, fn(&mut VM) -> std::result::Result<&mut VM, BundError>)] =
&[("ink.goals.streak", w_streak), ("ink.goals.snapshot", w_snapshot)];
for (name, f) in words {
vm.register_inline(name.to_string(), *f).map_err(|e| anyhow!("register {name}: {e}"))?;
}
for (name, _) in words {
if let Some(short) = name.strip_prefix("ink.") {
let _ = vm.register_alias(short.to_string(), name.to_string());
}
}
Ok(())
}
fn to_bund_err(e: anyhow::Error) -> BundError {
easy_error::err_msg(e.to_string())
}
fn w_streak(vm: &mut VM) -> std::result::Result<&mut VM, BundError> {
do_streak(vm).map_err(to_bund_err)
}
fn do_streak(vm: &mut VM) -> Result<&mut VM> {
let tag = "ink.goals.streak";
let store = active_store(tag)?;
let cfg = active_config(tag)?;
crate::dayclock::set_boundary(cfg.goals.day_boundary);
let progress = ProgressStore::open(&store.project_root().join("progress.db"))
.map_err(|e| anyhow!("{tag}: open progress.db: {e}"))?;
let days = progress
.writing_days_recent(ALL_HISTORY_DAYS)
.map_err(|e| anyhow!("{tag}: {e}"))?;
let grace = cfg.goals.streak_grace_per_week;
let mut streak = crate::progress::aggregates::compute_streak(
&days,
crate::dayclock::today_days(),
grace,
);
streak.best = crate::progress::aggregates::longest_streak(&days, grace);
let mut m: HashMap<String, Value> = HashMap::new();
m.insert("days".into(), Value::from_int(streak.days));
m.insert("best".into(), Value::from_int(streak.best));
m.insert("grace_used".into(), Value::from_int(streak.grace_used));
m.insert("grace_per_week".into(), Value::from_int(streak.grace_per_week));
push(vm, Value::from_dict(m));
Ok(vm)
}
fn live_totals(layout: &ProjectLayout, h: &Hierarchy) -> LiveTotals {
let mut per_book: HashMap<Uuid, i64> = HashMap::new();
let mut book_titles: HashMap<Uuid, String> = HashMap::new();
let mut book_slugs: HashMap<Uuid, String> = HashMap::new();
for (node, _) in h.flatten() {
if node.kind != NodeKind::Paragraph {
continue;
}
let book = h
.ancestors(node)
.into_iter()
.find(|a| a.kind == NodeKind::Book)
.filter(|b| b.system_tag.is_none());
let Some(book) = book else { continue };
let Some(rel) = node.file.as_ref() else { continue };
let body = std::fs::read_to_string(layout.root.join(rel)).unwrap_or_default();
*per_book.entry(book.id).or_insert(0) += count_words(&body);
book_titles.entry(book.id).or_insert_with(|| book.title.clone());
book_slugs.entry(book.id).or_insert_with(|| book.slug.clone());
}
let project_total = per_book.values().sum();
LiveTotals { per_book, project_total, book_titles, book_slugs }
}
fn opt_int(m: &mut HashMap<String, Value>, key: &str, v: Option<i64>) {
if let Some(v) = v {
m.insert(key.into(), Value::from_int(v));
}
}
fn book_progress_dict(b: &BookProgress) -> Value {
let mut m: HashMap<String, Value> = HashMap::new();
m.insert("label".into(), Value::from_string(&b.label));
m.insert("today_words".into(), Value::from_int(b.today_words));
m.insert("total_words".into(), Value::from_int(b.total_words));
opt_int(&mut m, "daily_goal", b.daily_goal);
opt_int(&mut m, "target_words", b.target_words);
opt_int(&mut m, "required_pace", b.required_pace);
opt_int(&mut m, "days_to_deadline", b.days_to_deadline);
Value::from_dict(m)
}
fn ladder(rows: &[(String, i64)]) -> Value {
Value::from_list(
rows.iter()
.map(|(name, count)| {
let mut m: HashMap<String, Value> = HashMap::new();
m.insert("name".into(), Value::from_string(name));
m.insert("count".into(), Value::from_int(*count));
Value::from_dict(m)
})
.collect(),
)
}
fn w_snapshot(vm: &mut VM) -> std::result::Result<&mut VM, BundError> {
do_snapshot(vm).map_err(to_bund_err)
}
fn do_snapshot(vm: &mut VM) -> Result<&mut VM> {
let tag = "ink.goals.snapshot";
let store: &Store = active_store(tag)?;
let cfg = active_config(tag)?;
crate::dayclock::set_boundary(cfg.goals.day_boundary);
let layout = ProjectLayout::new(store.project_root());
let h = Hierarchy::load(store).map_err(|e| anyhow!("{tag}: {e}"))?;
let live = live_totals(&layout, &h);
let progress = ProgressStore::open(&store.project_root().join("progress.db"))
.map_err(|e| anyhow!("{tag}: open progress.db: {e}"))?;
let snap: ProgressSnapshot = crate::progress::aggregates::build_snapshot(&progress, &cfg.goals, &live)
.map_err(|e| anyhow!("{tag}: {e}"))?;
let mut streak: HashMap<String, Value> = HashMap::new();
streak.insert("days".into(), Value::from_int(snap.streak.days));
streak.insert("best".into(), Value::from_int(snap.streak.best));
streak.insert("grace_used".into(), Value::from_int(snap.streak.grace_used));
streak.insert("grace_per_week".into(), Value::from_int(snap.streak.grace_per_week));
let mut status: HashMap<String, Value> = HashMap::new();
status.insert("recent".into(), ladder(&snap.status.recent));
status.insert("goals".into(), ladder(&snap.status.goals));
let mut m: HashMap<String, Value> = HashMap::new();
m.insert("project".into(), book_progress_dict(&snap.project));
m.insert(
"books".into(),
Value::from_list(snap.books.iter().map(book_progress_dict).collect()),
);
m.insert("status".into(), Value::from_dict(status));
m.insert("streak".into(), Value::from_dict(streak));
m.insert(
"sparkline".into(),
Value::from_list(snap.sparkline.iter().map(|v| Value::from_int(*v)).collect()),
);
m.insert("active_seconds_today".into(), Value::from_int(snap.active_seconds_today));
m.insert("active_seconds_week".into(), Value::from_int(snap.active_seconds_week));
push(vm, Value::from_dict(m));
Ok(vm)
}