use anyhow::Result;
use chrono::Utc;
use std::collections::{BTreeMap, HashMap};
use std::fs::{self, OpenOptions};
use std::io::{BufWriter, Write};
use crate::context::SpoolContext;
use crate::event::{Event, Operation};
use crate::state::{materialize, Task, TaskStatus};
use crate::writer::get_current_branch;
pub fn archive_tasks(ctx: &SpoolContext, days: u32, dry_run: bool) -> Result<Vec<String>> {
let state = materialize(ctx)?;
let cutoff = Utc::now() - chrono::Duration::days(days as i64);
let mut to_archive: Vec<&Task> = state
.tasks
.values()
.filter(|t| {
t.status == TaskStatus::Complete
&& t.completed.is_some_and(|c| c < cutoff)
&& t.archived.is_none()
})
.collect();
to_archive.sort_by_key(|t| t.completed);
if to_archive.is_empty() {
println!("No tasks to archive.");
return Ok(Vec::new());
}
let archived_ids: Vec<String> = to_archive.iter().map(|t| t.id.clone()).collect();
if dry_run {
println!("Would archive {} tasks:", to_archive.len());
for task in &to_archive {
println!(" {} - {}", task.id, task.title);
}
return Ok(archived_ids);
}
let mut by_month: BTreeMap<String, Vec<&Task>> = BTreeMap::new();
for task in &to_archive {
if let Some(completed) = task.completed {
let month = completed.format("%Y-%m").to_string();
by_month.entry(month).or_default().push(task);
}
}
fs::create_dir_all(&ctx.archive_dir)?;
let all_events = collect_all_events(ctx)?;
for (month, tasks) in &by_month {
let archive_file = ctx.archive_dir.join(format!("{}.jsonl", month));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&archive_file)?;
let mut writer = BufWriter::new(file);
for task in tasks {
if let Some(events) = all_events.get(&task.id) {
for event in events {
let json = serde_json::to_string(event)?;
writeln!(writer, "{}", json)?;
}
}
}
writer.flush()?;
}
let today = Utc::now().format("%Y-%m-%d").to_string();
let event_file = ctx.events_dir.join(format!("{}.jsonl", today));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&event_file)?;
let mut writer = BufWriter::new(file);
let branch = get_current_branch()?;
for task in &to_archive {
if let Some(completed) = task.completed {
let month = completed.format("%Y-%m").to_string();
let archive_event = Event {
v: 1,
op: Operation::Archive,
id: task.id.clone(),
ts: Utc::now(),
by: "@spool".to_string(),
branch: branch.clone(),
d: serde_json::json!({ "ref": month }),
};
let json = serde_json::to_string(&archive_event)?;
writeln!(writer, "{}", json)?;
}
}
writer.flush()?;
println!("Archived {} tasks.", to_archive.len());
for (month, tasks) in &by_month {
println!(" {} tasks to archive/{}.jsonl", tasks.len(), month);
}
Ok(archived_ids)
}
pub fn collect_all_events(ctx: &SpoolContext) -> Result<HashMap<String, Vec<Event>>> {
let mut events_by_task: HashMap<String, Vec<Event>> = HashMap::new();
for file in ctx.get_event_files()? {
let events = ctx.parse_events_from_file(&file)?;
for event in events {
events_by_task
.entry(event.id.clone())
.or_default()
.push(event);
}
}
Ok(events_by_task)
}