sara-tasks 2.7.6

Sara — folder-aware task manager
use super::super::*;
use crate::domain::services::memory_strength::{
    RECALL_BOOST_WINDOW_DAYS, base_strength, derived_bonus, recall_boost,
};
use crate::domain::{Item, Relation, Status};
use anyhow::Result;
use chrono::Utc;
use rusqlite::Connection;
use uuid::Uuid;

pub fn item_strength(conn: &Connection, item: &Item) -> f64 {
    item_base_strength(conn, item)
        + recall_usage_boost(conn, &item.uuid)
        + canonical_derived_bonus(conn, &item.uuid)
}

pub fn item_strengths(conn: &Connection, items: &[Item]) -> std::collections::HashMap<Uuid, f64> {
    let bases = item_base_strengths(conn, items);
    let boosts = recall_usage_boosts(conn);
    let bonuses = canonical_derived_bonuses(conn);
    items
        .iter()
        .map(|item| {
            let s = bases.get(&item.uuid).copied().unwrap_or(1.0)
                + boosts.get(&item.uuid).copied().unwrap_or(0.0)
                + bonuses.get(&item.uuid).copied().unwrap_or(0.0);
            (item.uuid, s)
        })
        .collect()
}

fn canonical_derived_bonus(conn: &Connection, item_uuid: &Uuid) -> f64 {
    let count = get_memory_links_to(conn, &item_uuid.to_string())
        .unwrap_or_default()
        .into_iter()
        .filter(|l| l.relation == Relation::DerivedFrom)
        .count();
    derived_bonus(count)
}

fn item_base_strength(conn: &Connection, item: &Item) -> f64 {
    let source = item
        .source_task_uuid
        .and_then(|source| {
            get_task_by_uuid_prefix(conn, &source.to_string())
                .ok()
                .flatten()
        })
        .map(|task| task.status);
    let linked: Vec<Status> = if source.is_some() {
        Vec::new()
    } else {
        get_item_task_links(conn, &item.uuid)
            .unwrap_or_default()
            .into_iter()
            .map(|(t, _)| t.status)
            .collect()
    };
    base_strength(item.status, source.as_ref(), &linked)
}

pub fn item_base_strengths(
    conn: &Connection,
    items: &[Item],
) -> std::collections::HashMap<Uuid, f64> {
    let mut task_status: std::collections::HashMap<String, Status> =
        std::collections::HashMap::new();
    if let Ok(mut stmt) = conn.prepare("SELECT uuid, status FROM tasks")
        && let Ok(rows) =
            stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
    {
        for (u, s) in rows.flatten() {
            task_status.insert(u, status_from_str(&s));
        }
    }
    let mut link_status: std::collections::HashMap<Uuid, Vec<Status>> =
        std::collections::HashMap::new();
    if let Ok(mut stmt) = conn.prepare(
        "SELECT l.item_uuid, t.status FROM item_task_links l JOIN tasks t ON t.uuid = l.task_uuid",
    ) && let Ok(rows) =
        stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
    {
        for (item_uuid, status) in rows.flatten() {
            if let Ok(u) = Uuid::parse_str(&item_uuid) {
                link_status
                    .entry(u)
                    .or_default()
                    .push(status_from_str(&status));
            }
        }
    }

    let mut out = std::collections::HashMap::with_capacity(items.len());
    for item in items {
        let source = item
            .source_task_uuid
            .and_then(|s| task_status.get(&s.to_string()));
        let linked = link_status.get(&item.uuid).map_or(&[][..], |v| v);
        out.insert(item.uuid, base_strength(item.status, source, linked));
    }
    out
}

pub fn canonical_derived_bonuses(conn: &Connection) -> std::collections::HashMap<Uuid, f64> {
    let mut counts: std::collections::HashMap<Uuid, u32> = std::collections::HashMap::new();
    if let Ok(mut stmt) =
        conn.prepare("SELECT to_uuid FROM memory_links WHERE relation = 'derived_from'")
        && let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(0))
    {
        for uuid_str in rows.flatten() {
            if let Ok(u) = Uuid::parse_str(&uuid_str) {
                *counts.entry(u).or_insert(0) += 1;
            }
        }
    }
    counts
        .into_iter()
        .map(|(u, c)| (u, derived_bonus(c as usize)))
        .collect()
}

fn status_from_str(s: &str) -> Status {
    match s {
        "completed" => Status::Completed,
        "deleted" => Status::Deleted,
        _ => Status::Pending,
    }
}

pub fn all_item_files(conn: &Connection) -> std::collections::HashMap<Uuid, Vec<String>> {
    let mut map: std::collections::HashMap<Uuid, Vec<String>> = std::collections::HashMap::new();
    if let Ok(mut stmt) =
        conn.prepare("SELECT item_uuid, file_path FROM item_files ORDER BY item_uuid, file_path")
        && let Ok(rows) =
            stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
    {
        for (item_uuid, path) in rows.flatten() {
            if let Ok(u) = Uuid::parse_str(&item_uuid) {
                map.entry(u).or_default().push(path);
            }
        }
    }
    map
}

pub fn all_item_task_uuids(conn: &Connection) -> std::collections::HashMap<Uuid, Vec<Uuid>> {
    let mut map: std::collections::HashMap<Uuid, Vec<Uuid>> = std::collections::HashMap::new();
    if let Ok(mut stmt) = conn.prepare("SELECT item_uuid, task_uuid FROM item_task_links")
        && let Ok(rows) =
            stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
    {
        for (item_uuid, task_uuid) in rows.flatten() {
            if let (Ok(iu), Ok(tu)) = (Uuid::parse_str(&item_uuid), Uuid::parse_str(&task_uuid)) {
                map.entry(iu).or_default().push(tu);
            }
        }
    }
    map
}

pub fn record_memory_recall(conn: &Connection, item_uuid: &Uuid) -> Result<()> {
    super::uses::attribute_use(conn, item_uuid, crate::domain::MemoryUseKind::Recalled);
    record_event(
        conn,
        "memory_recalled",
        Some(item_uuid),
        Some("memory"),
        &[],
        None,
    )
}

pub fn record_memory_surfaced(conn: &Connection, item_uuid: &Uuid) -> Result<()> {
    super::uses::attribute_use(conn, item_uuid, crate::domain::MemoryUseKind::Surfaced);
    record_event(
        conn,
        "memory_surfaced",
        Some(item_uuid),
        Some("memory"),
        &[],
        None,
    )
}

fn recall_usage_boost(conn: &Connection, item_uuid: &Uuid) -> f64 {
    let cutoff = dt_to_str(&(Utc::now() - chrono::Duration::days(RECALL_BOOST_WINDOW_DAYS)));
    let count: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM events
             WHERE action='memory_recalled' AND ref_uuid=?1 AND at >= ?2",
            rusqlite::params![item_uuid.to_string(), cutoff],
            |r| r.get(0),
        )
        .unwrap_or(0);
    recall_boost(count)
}

pub fn recall_usage_boosts(conn: &Connection) -> std::collections::HashMap<Uuid, f64> {
    let cutoff = dt_to_str(&(Utc::now() - chrono::Duration::days(RECALL_BOOST_WINDOW_DAYS)));
    let mut map: std::collections::HashMap<Uuid, f64> = std::collections::HashMap::new();
    let Ok(mut stmt) = conn.prepare(
        "SELECT ref_uuid, COUNT(*) FROM events
         WHERE action='memory_recalled' AND ref_uuid IS NOT NULL AND at >= ?1
         GROUP BY ref_uuid",
    ) else {
        return map;
    };
    let rows = stmt.query_map([cutoff], |r| {
        Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?))
    });
    if let Ok(rows) = rows {
        for (uuid_str, count) in rows.flatten() {
            if let Ok(u) = Uuid::parse_str(&uuid_str) {
                map.insert(u, recall_boost(count));
            }
        }
    }
    map
}

pub fn item_strength_with_boost(conn: &Connection, item: &Item, recall_boost: f64) -> f64 {
    item_base_strength(conn, item) + recall_boost
}

pub fn memory_recall_daily_counts_all(
    conn: &Connection,
    days: i64,
) -> std::collections::HashMap<Uuid, Vec<u64>> {
    let mut out: std::collections::HashMap<Uuid, Vec<u64>> = std::collections::HashMap::new();
    let now = Utc::now();
    let cutoff = dt_to_str(&(now - chrono::Duration::days(days)));
    let Ok(mut stmt) = conn.prepare(
        "SELECT ref_uuid, at FROM events
         WHERE action='memory_recalled' AND ref_uuid IS NOT NULL AND at >= ?1",
    ) else {
        return out;
    };
    let rows = stmt.query_map([cutoff], |r| {
        Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
    });
    if let Ok(rows) = rows {
        for (uuid_str, at) in rows.flatten() {
            let Ok(u) = Uuid::parse_str(&uuid_str) else {
                continue;
            };
            if let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&at) {
                let age_days = (now - ts.with_timezone(&Utc)).num_days();
                if (0..days).contains(&age_days) {
                    let idx = (days - 1 - age_days) as usize;
                    out.entry(u)
                        .or_insert_with(|| vec![0u64; days.max(1) as usize])[idx] += 1;
                }
            }
        }
    }
    out
}

pub fn memory_recall_daily_counts(conn: &Connection, item_uuid: &Uuid, days: i64) -> Vec<u64> {
    let mut counts = vec![0u64; days.max(1) as usize];
    let now = Utc::now();
    let cutoff = dt_to_str(&(now - chrono::Duration::days(days)));
    let Ok(mut stmt) = conn.prepare(
        "SELECT at FROM events
         WHERE action='memory_recalled' AND ref_uuid=?1 AND at >= ?2",
    ) else {
        return counts;
    };
    let rows = stmt
        .query_map(rusqlite::params![item_uuid.to_string(), cutoff], |r| {
            r.get::<_, String>(0)
        })
        .map(|rows| rows.flatten().collect::<Vec<_>>())
        .unwrap_or_default();
    for at in rows {
        if let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&at) {
            let age_days = (now - ts.with_timezone(&Utc)).num_days();
            if (0..days).contains(&age_days) {
                let idx = (days - 1 - age_days) as usize;
                counts[idx] += 1;
            }
        }
    }
    counts
}