main
Caleb Version 0.3.0: new copper reader, dome via filter, every layer read from the photos, net evidence, measured confidence;… e874d7d
//! AI Flow's own timeline window (assets/timeline.html, copied from adom/adom-aiflow tools/timeline)
//! fed by this flow: timeline.json (the step path, phases, statuses, visits, minutes and loop backs)
//! and presentation.json (the project card: current step, its findings, the latest board overlay).
//! Each finished step's line carries what it found, from findings.rs.
use crate::flow::{self, Flow};
use crate::items::read_json;
use serde_json::{json, Value};
use std::path::Path;

pub fn title(step: &str) -> &'static str {
    match step {
        "intake" => "Photos in", "scale" => "Scale", "parts" => "1 · Parts", "blocks" => "2 · Functional blocks",
        "copper" => "3 · Zones and routing", "mimic" => "4 · Copy what is visible", "snapshot" => "5a · Save the project",
        "connect" => "5b · Connections and layers", "review" => "6 · Review loop", "tests" => "7 · Tests for unknowns",
        "report" => "Report", _ => "Step",
    }
}

pub fn phase(step: &str) -> &'static str {
    match step {
        "intake" | "scale" => "Photos", "parts" | "blocks" | "copper" => "Identify", "mimic" | "snapshot" => "Copy",
        "connect" => "Infer", "review" | "tests" => "Verify", _ => "Deliver",
    }
}

/// The step a ledger event belongs to.
pub fn event_step(ev: &Value) -> Option<String> {
    let d = &ev["data"];
    let st = match ev["ev"].as_str()? {
        "init" | "prep" => "intake", "scale" => "scale", "snapshot" => "snapshot", "review" | "review-measure" | "audit-draw" | "audit-verdict" => "review", "report" => "report",
        "detect" | "parts-init" | "parts-snap" | "parts-reconcile" => "parts", "blocks-propose" => "blocks", "copper-prep" | "copper-fit" => "copper",
        "build" | "kicad" => "mimic", "connect-extract" | "connect-route" => "connect", "tests-generate" => "tests",
        "check" => match d["what"].as_str()? { "photos" | "prep" => "intake", "nets" | "layers" => "connect", w => return Some(w.to_string()) },
        "view" => return d["step"].as_str().map(str::to_string),
        _ => return None,
    };
    Some(st.into())
}

/// Seconds since the epoch for "YYYY-MM-DDTHH:MM:SSZ" (the ledger's own format).
pub fn epoch(t: &str) -> Option<i64> {
    let n = |a: usize, b: usize| t.get(a..b)?.parse::<i64>().ok();
    let (y, mo, d, h, mi, s) = (n(0, 4)?, n(5, 7)?, n(8, 10)?, n(11, 13)?, n(14, 16)?, n(17, 19)?);
    let (y2, m2) = if mo <= 2 { (y - 1, mo + 9) } else { (y, mo - 3) };
    let days = 365 * y2 + y2 / 4 - y2 / 100 + y2 / 400 + (153 * m2 + 2) / 5 + d - 1 - 719468;
    Some(days * 86400 + h * 3600 + mi * 60 + s)
}

fn first_sentence(s: &str) -> String {
    let s = s.split_once(": ").filter(|(a, _)| a.starts_with("STEP")).map(|(_, b)| b).unwrap_or(s);
    s.split(". ").next().unwrap_or(s).trim_end_matches('.').to_string()
}

pub fn timeline(flow_dir: &Path, fl: &Flow, findings: &Value) -> Value {
    let st = read_json(&flow_dir.join("state.json")).unwrap_or(json!({}));
    let ledger: Vec<Value> = std::fs::read_to_string(flow_dir.join("run.jsonl")).unwrap_or_default().lines().filter_map(|l| serde_json::from_str(l).ok()).collect();
    let cur = flow::current(fl, flow_dir).map(|s| s.name.clone());
    let rework = &st["rework"];
    let (rw_from, rw_to) = (rework["from"].as_str().unwrap_or(""), rework["to"].as_str().unwrap_or(""));
    let pos = |n: &str| fl.steps.iter().position(|s| s.name == n);
    // AI time per step: the gap before each ledger event goes to that event's step (idle over 20 minutes is cut)
    let mut minutes = std::collections::HashMap::<String, f64>::new();
    let mut visits = std::collections::HashMap::<String, u64>::new();
    let mut prev: Option<i64> = None;
    for ev in &ledger {
        let t = ev["t"].as_str().and_then(epoch);
        if let (Some(step), Some(t)) = (event_step(ev), t) {
            if let Some(p) = prev { *minutes.entry(step.clone()).or_default() += ((t - p).clamp(0, 1200)) as f64 / 60.0; }
            let passed = ev["ev"] != json!("check") || ev["data"]["ok"] == json!(true);
            if passed && ev["ev"] != json!("view") { *visits.entry(step).or_default() += 1; }
        }
        if t.is_some() { prev = t; }
    }
    let steps: Vec<Value> = fl.steps.iter().map(|s| {
        let done = s.done.iter().all(|d| flow::exists(flow_dir, d));
        let stale = s.done.iter().any(|d| !flow::stale(flow_dir, d).is_empty());
        let in_rework = match (pos(rw_to), pos(rw_from), pos(&s.name)) { (Some(a), Some(b), Some(i)) => i >= a && i <= b, _ => false };
        let status = if Some(&s.name) == cur.as_ref() { "current" } else if done { "done" } else if stale || in_rework { "redo" } else { "todo" };
        let found = findings[&s.name]["summary"].as_str().unwrap_or("");
        let does = if done && !found.is_empty() { found.to_string() } else { first_sentence(&s.todo) };
        json!({"name": s.name, "title": title(&s.name), "does": does, "phase": phase(&s.name), "status": status,
               "visits": visits.get(&s.name).copied().unwrap_or(0).max(if done { 1 } else { 0 }),
               "minutes": (minutes.get(&s.name).copied().unwrap_or(0.0) * 10.0).round() / 10.0})
    }).collect();
    let pcb = st["kicad"]["pcb"].as_str().and_then(|p| Path::new(p).file_name()).map(|f| f.to_string_lossy().to_string());
    let proj = flow_dir.parent().and_then(|p| p.file_name()).map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
    json!({
        "title": format!("Reverse engineering: {proj}"), "label": proj, "engine": "KiCad",
        "board": pcb, "aiThread": ledger.last().and_then(|e| e["aiThread"].as_str()).unwrap_or(""),
        "runFolder": proj, "runDir": flow_dir.display().to_string(),
        "finish": "KiCad copy, layer proposal, confidence and the test list",
        "updated": crate::ledger::now(), "current": cur, "steps": steps, "loops": st["loops"].clone(),
    })
}

/// The timeline's project card: the step in hand, what it found, and the latest board overlay or photo.
pub fn presentation(flow_dir: &Path, fl: &Flow, findings: &Value) -> Value {
    let st = read_json(&flow_dir.join("state.json")).unwrap_or(json!({}));
    let cur = flow::current(fl, flow_dir);
    let proj = flow_dir.parent().and_then(|p| p.file_name()).map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
    let (t, summary) = match cur {
        Some(s) => (format!("Now: {}", title(&s.name)), findings[&s.name]["summary"].as_str().map(str::to_string).unwrap_or_else(|| first_sentence(&s.todo))),
        None => ("Finished".to_string(), findings["review"]["summary"].as_str().unwrap_or("Every step is done.").to_string()),
    };
    let ok_img = |n: &str| { let l = n.to_lowercase(); l.ends_with(".jpg") || l.ends_with(".jpeg") || l.ends_with(".png") || l.ends_with(".webp") };
    let overlay = std::fs::read_dir(flow_dir.join("evidence")).ok().and_then(|rd| rd.flatten().map(|e| e.file_name().to_string_lossy().to_string()).filter(|n| n.starts_with("overlay-top") && ok_img(n)).max());
    let photo = st["photos"].as_array().and_then(|a| a.iter().find(|p| p["side"] == json!("top"))).and_then(|p| p["file"].as_str()).filter(|f| ok_img(f)).map(str::to_string);
    let (image, caption) = match (overlay, photo) {
        (Some(o), _) => (Some(format!("files/evidence/{o}")), format!("Copied board over the top photo ({o})")),
        (None, Some(p)) => (Some(format!("files/photos/{p}")), format!("{p} · top photo")),
        _ => (None, String::new()),
    };
    json!({"projectTitle": format!("Reverse engineering: {proj}"), "title": t, "summary": summary, "image": image, "caption": caption, "alt": caption})
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn epoch_and_events() {
        assert_eq!(epoch("1970-01-01T00:00:00Z"), Some(0));
        assert_eq!(epoch("2026-11-03T20:55:13Z").unwrap() - epoch("2026-11-03T20:54:13Z").unwrap(), 60);
        assert_eq!(event_step(&json!({"ev": "check", "data": {"what": "layers"}})).as_deref(), Some("connect"));
        assert_eq!(first_sentence("STEP 2: identify the functional blocks. Write blocks.json"), "identify the functional blocks");
    }
}