asurada 0.2.1

Asurada — a memory + cognition daemon that grows with the user. Local-first, BYOK, shared by Devist/Webchemist Core/etc.
#![allow(dead_code)]

// `~/.asurada/config.toml` — Asurada 의 영구 설정.
//
// 비밀 가능성 있음 (database url 에 password) — chmod 600.
// API 키는 추후 macOS Keychain 으로 분리 예정.

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// 사용자 식별자 — 설치 시 random UUID 자동 발급, 영구 보존.
    /// Asurada/Devist 등 모든 소비자가 이 값을 user_id 로 사용.
    pub user: UserConfig,

    /// Postgres 연결 — sync + 클라우드 미러용. 필수.
    pub database: DatabaseConfig,

    #[serde(default)]
    pub server: ServerConfig,

    #[serde(default)]
    pub tts: TtsConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserConfig {
    pub id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
    /// `postgresql://user:pass@host:port/db`
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    pub port: u16,
    pub bind: String,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            port: 7878,
            bind: "127.0.0.1".into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TtsConfig {
    #[serde(default)]
    pub enabled: bool,
    #[serde(default)]
    pub voice_id: Option<String>,
    #[serde(default)]
    pub api_key: Option<String>,
    /// 매일 첫 가동 시 인사 발화 (default true).
    #[serde(default = "default_true")]
    pub daily_greeting: bool,
    /// 발화 침묵 시간대 — "21:00-09:00" 형식 (local time). None 이면 항상 발화.
    #[serde(default = "default_quiet_hours")]
    pub quiet_hours: Option<String>,
    /// 새 advice 자동 발화 (default true). false 면 사용자가 명시 호출 시만.
    #[serde(default = "default_true")]
    pub auto_speak_advice: bool,
}

fn default_true() -> bool {
    true
}

fn default_quiet_hours() -> Option<String> {
    Some("21:00-09:00".into())
}

impl Config {
    /// 디스크에서 로드 — 필수 필드 부족하면 에러.
    pub fn load(path: &Path) -> Result<Self> {
        if !path.exists() {
            return Err(anyhow!(
                "Asurada config not found at {}.\n먼저 `asurada init` 을 실행하세요.",
                path.display()
            ));
        }
        let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
        let cfg: Config =
            toml::from_str(&text).with_context(|| format!("parse {}", path.display()))?;
        if cfg.user.id.trim().is_empty() {
            return Err(anyhow!("config.user.id 비어있음 — `asurada init` 재실행"));
        }
        if cfg.database.url.trim().is_empty() {
            return Err(anyhow!(
                "config.database.url 미설정 — `asurada init` 으로 설정"
            ));
        }
        Ok(cfg)
    }

    /// 저장. unix 에서는 chmod 600.
    pub fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).ok();
        }
        let text = toml::to_string_pretty(self)?;
        fs::write(path, text).with_context(|| format!("write {}", path.display()))?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = fs::Permissions::from_mode(0o600);
            let _ = fs::set_permissions(path, perms);
        }
        Ok(())
    }

    /// 새 설정 — random UUID + 입력받은 db url.
    pub fn fresh(database_url: String) -> Self {
        Self {
            user: UserConfig {
                id: uuid::Uuid::new_v4().to_string(),
            },
            database: DatabaseConfig { url: database_url },
            server: ServerConfig::default(),
            tts: TtsConfig::default(),
        }
    }
}

// BYOK 모델: 사용자 본인의 Postgres URL 을 init 시점에 입력 받아 config 에 저장.
// 자격증명을 binary 에 박지 않는다 (보안 위험).