sara-tasks 2.7.5

Sara — folder-aware task manager
use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::domain::services::UrgencyWeights;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RecallConfig {
    pub semantic: bool,
    pub semantic_threshold: f32,
    pub semantic_top_k: usize,
}

impl Default for RecallConfig {
    fn default() -> Self {
        RecallConfig {
            semantic: false,
            semantic_threshold: 0.30,
            semantic_top_k: 5,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TelemetryConfig {
    pub enabled: bool,
    pub endpoint: Option<String>,
    pub token: Option<String>,
}

impl Default for TelemetryConfig {
    fn default() -> Self {
        TelemetryConfig {
            enabled: true,
            endpoint: None,
            token: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TuiConfig {
    pub theme: String,
}

impl Default for TuiConfig {
    fn default() -> Self {
        TuiConfig {
            theme: "classic".to_string(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    pub default_project: String,
    pub date_dialect: String,
    pub urgency: UrgencyWeights,
    pub vault_path: Option<PathBuf>,
    pub recall: RecallConfig,
    pub telemetry: TelemetryConfig,
    pub tui: TuiConfig,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            default_project: "inbox".to_string(),
            date_dialect: "uk".to_string(),
            urgency: UrgencyWeights::default(),
            vault_path: None,
            recall: RecallConfig::default(),
            telemetry: TelemetryConfig::default(),
            tui: TuiConfig::default(),
        }
    }
}

pub fn project_dirs() -> Result<ProjectDirs> {
    ProjectDirs::from("", "", "sara").context("Could not determine home directory")
}

fn legacy_tk_project_dirs() -> Option<ProjectDirs> {
    ProjectDirs::from("", "", "tk")
}

pub fn config_path() -> Result<PathBuf> {
    let dirs = project_dirs()?;
    Ok(dirs.config_dir().join("config.toml"))
}

pub fn db_path() -> Result<PathBuf> {
    let dirs = project_dirs()?;
    Ok(dirs.data_dir().join("tasks.db"))
}

pub fn vault_path(cfg: &Config) -> Result<PathBuf> {
    if let Some(ref p) = cfg.vault_path {
        return Ok(p.clone());
    }
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    Ok(cwd.join("Sara"))
}

pub fn set_vault_path(cfg: &mut Config, path: PathBuf) -> Result<()> {
    cfg.vault_path = Some(path);
    save(cfg)
}

fn legacy_tk_config_path() -> Option<PathBuf> {
    legacy_tk_project_dirs().map(|d| d.config_dir().join("config.toml"))
}

fn legacy_tk_db_path() -> Option<PathBuf> {
    legacy_tk_project_dirs().map(|d| d.data_dir().join("tasks.db"))
}

pub fn migrate_from_tk_if_needed() -> Result<bool> {
    let sara_cfg = config_path()?;
    let sara_db = db_path()?;
    let mut migrated = false;

    if let Some(tk_cfg) = legacy_tk_config_path()
        && tk_cfg.exists()
        && !sara_cfg.exists()
    {
        if let Some(parent) = sara_cfg.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::copy(&tk_cfg, &sara_cfg)
            .with_context(|| format!("Failed to copy config from {}", tk_cfg.display()))?;
        migrated = true;
    }

    if let Some(tk_db) = legacy_tk_db_path()
        && tk_db.exists()
        && !sara_db.exists()
    {
        if let Some(parent) = sara_db.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::copy(&tk_db, &sara_db)
            .with_context(|| format!("Failed to copy database from {}", tk_db.display()))?;
        migrated = true;
    }

    if migrated {
        eprintln!(
            "Imported existing tk config and tasks into sara.\n\
             You can remove the old tk binary with: cargo uninstall tk"
        );
    }

    Ok(migrated)
}

pub fn load() -> Result<Config> {
    migrate_from_tk_if_needed()?;
    let path = config_path()?;
    if !path.exists() {
        return Ok(Config::default());
    }
    let content = std::fs::read_to_string(&path)
        .with_context(|| format!("Failed to read config: {}", path.display()))?;
    toml::from_str(&content).with_context(|| format!("Failed to parse config: {}", path.display()))
}

pub fn save(cfg: &Config) -> Result<()> {
    let path = config_path()?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let content = toml::to_string_pretty(cfg).context("Failed to serialize config")?;
    std::fs::write(&path, content)
        .with_context(|| format!("Failed to write config: {}", path.display()))
}

#[cfg(test)]
#[path = "../../tests/unit/infrastructure/config.rs"]
mod tests;