sara-tasks 2.5.1

Sara — folder-aware task manager
use std::collections::HashMap;

use anyhow::Result;
use rusqlite::Connection;

use crate::commands::shared::item_label;
use crate::infrastructure::db;
use crate::infrastructure::memory::graph::MemoryGraph;

use super::layout::{force_layout, graph_edges, spring_stiffness};
use super::types::{Assoc, Bond, DreamData, Neighbor, NodeKind, Star, WebData};

pub(super) fn load(conn: &Connection, handle: &str) -> Result<DreamData> {
    let item = db::get_item_by_handle(conn, handle)?;
    if item.kind != "memory" {
        anyhow::bail!(
            "`sara dream` peeks into memories — {handle} is a {}",
            item.kind
        );
    }
    let label = item_label(&item);
    let strength = db::item_strength(conn, &item);
    let files = db::get_item_files(conn, &item.uuid).unwrap_or_default();

    let mut neighbors = vec![];
    let uuid_str = item.uuid.to_string();
    for link in db::get_memory_links_from(conn, &uuid_str).unwrap_or_default() {
        if let Ok(other) = db::get_item_by_uuid(conn, &link.to_uuid) {
            neighbors.push(Neighbor {
                kind: NodeKind::Memory {
                    label: item_label(&other),
                    relation: link.relation,
                },
            });
        }
    }
    for link in db::get_memory_links_to(conn, &uuid_str).unwrap_or_default() {
        if let Ok(other) = db::get_item_by_uuid(conn, &link.from_uuid) {
            neighbors.push(Neighbor {
                kind: NodeKind::Memory {
                    label: item_label(&other),
                    relation: format!("⟵ {}", link.relation),
                },
            });
        }
    }
    for (task, source) in db::get_item_task_links(conn, &item.uuid).unwrap_or_default() {
        neighbors.push(Neighbor {
            kind: NodeKind::Task {
                id: task.id.map(|i| i.to_string()).unwrap_or_else(|| "?".into()),
                desc: task.description.clone(),
                source,
            },
        });
    }
    for f in &files {
        let name = f.rsplit('/').next().unwrap_or(f).to_string();
        neighbors.push(Neighbor {
            kind: NodeKind::File { name },
        });
    }

    let sparkline = db::memory_recall_daily_counts(conn, &item.uuid, 30);
    let recall_total_30d = sparkline.iter().sum();
    Ok(DreamData {
        provisional: item.status == "provisional",
        label,
        strength,
        files,
        neighbors,
        sparkline,
        recall_total_30d,
        item,
    })
}

pub(super) fn open(conn: &Connection, handle: &str) -> Result<DreamData> {
    let data = load(conn, handle)?;
    let _ = db::record_memory_recall(conn, &data.item.uuid);
    Ok(data)
}

pub(super) fn navigate_back(conn: &Connection, breadcrumb: &mut Vec<String>) -> Option<DreamData> {
    while let Some(prev) = breadcrumb.pop() {
        if let Ok(d) = load(conn, &prev) {
            return Some(d);
        }
    }
    None
}

pub(super) fn load_web(conn: &Connection) -> Result<WebData> {
    let memories = db::list_memories(conn)?;
    let strengths = db::item_strengths(conn, &memories);
    let recent_counts = db::memory_recall_daily_counts_all(conn, 7);
    let mut index = HashMap::new();
    let mut stars: Vec<Star> = memories
        .iter()
        .enumerate()
        .map(|(i, m)| {
            index.insert(m.uuid.to_string(), i);
            let recent: u64 = recent_counts.get(&m.uuid).map_or(0, |c| c.iter().sum());
            let label = item_label(m);
            let haystack = format!(
                "{} {} {} {}",
                label.to_lowercase(),
                m.tags.join(" ").to_lowercase(),
                m.title.to_lowercase(),
                m.body.to_lowercase()
            );
            Star {
                label,
                title: m.title.clone(),
                strength: strengths.get(&m.uuid).copied().unwrap_or(1.0),
                provisional: m.status == "provisional",
                tags: m.tags.clone(),
                haystack,
                x: 0.0,
                y: 0.0,
                recently_recalled: recent > 0,
            }
        })
        .collect();
    let bonds: Vec<Bond> = db::all_memory_links(conn)
        .unwrap_or_default()
        .into_iter()
        .filter_map(|l| {
            Some(Bond {
                a: *index.get(&l.from_uuid)?,
                b: *index.get(&l.to_uuid)?,
                relation: l.relation,
            })
        })
        .collect();
    let edges = MemoryGraph::build(conn)
        .map(|g| graph_edges(&g, &index))
        .unwrap_or_default();
    let affinity: Vec<(usize, usize, f64)> = edges
        .iter()
        .map(|&(a, b, w)| (a, b, spring_stiffness(w)))
        .collect();
    force_layout(&mut stars, &affinity, 250);
    let links: Vec<Assoc> = edges
        .into_iter()
        .map(|(a, b, weight)| Assoc { a, b, weight })
        .collect();
    Ok(WebData {
        stars,
        bonds,
        links,
    })
}