asurada 0.1.0

Asurada — a memory + cognition daemon that grows with the user. Local-first, BYOK, shared by Devist/Webchemist Core/etc.
// Asurada 데몬 진입점.
// localhost HTTP API 서버 + brain.db 소유.

mod ai;
mod api;
mod config;
mod core;
mod credentials;
mod db;
mod paths;
mod sync;

use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing_subscriber::{EnvFilter, fmt};

#[derive(Parser)]
#[command(name = "asurada", version, about = "Asurada — your AI partner that grows with you")]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Initialize ~/.asurada/brain.db (run once at install)
    Init,
    /// Start the HTTP server (foreground; daemon-mode handled by launchd)
    Serve {
        #[arg(long, default_value_t = 7878)]
        port: u16,
    },
    /// Print resolved paths
    Path,
    /// brain.db ↔ Supabase sync 작업
    #[command(subcommand)]
    Sync(SyncCmd),
    /// TTS — ElevenLabs 음성으로 어드바이스를 읽음
    #[command(subcommand)]
    Tts(TtsCmd),
}

#[derive(Subcommand)]
enum TtsCmd {
    /// TTS 활성화 (config.toml.tts.enabled = true)
    On,
    /// TTS 비활성화
    Off,
    /// TTS 상태 / 키 / voice 확인
    Status,
    /// 사용 가능한 voice 목록 (ElevenLabs 계정의 voice 들)
    Voices,
    /// 임의 텍스트 음성 합성 + 재생 (테스트용)
    Test {
        text: String,
    },
    /// 미확인 어드바이스를 음성으로 읽기
    Speak {
        #[arg(long)]
        project: Option<String>,
        #[arg(long, default_value_t = 1)]
        limit: usize,
    },
}

#[derive(Subcommand)]
enum SyncCmd {
    /// brain.db → Supabase 한 번 push (모든 테이블)
    Push,
    /// Supabase → brain.db 한 번 pull (모든 테이블)
    Pull,
    /// push + pull 한 번씩 실행
    Run,
}

#[tokio::main]
async fn main() -> Result<()> {
    fmt()
        .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
        .init();

    let cli = Cli::parse();
    match cli.cmd {
        Cmd::Init => cmd_init().await,
        Cmd::Serve { port } => cmd_serve(port).await,
        Cmd::Path => cmd_path(),
        Cmd::Sync(sub) => match sub {
            SyncCmd::Push => cmd_sync_push().await,
            SyncCmd::Pull => cmd_sync_pull().await,
            SyncCmd::Run => cmd_sync_run().await,
        },
        Cmd::Tts(sub) => match sub {
            TtsCmd::On => cmd_tts_set(true).await,
            TtsCmd::Off => cmd_tts_set(false).await,
            TtsCmd::Status => cmd_tts_status().await,
            TtsCmd::Voices => cmd_tts_voices().await,
            TtsCmd::Test { text } => cmd_tts_test(text).await,
            TtsCmd::Speak { project, limit } => cmd_tts_speak(project, limit).await,
        },
    }
}

// ── TTS 명령 구현 ─────────────────────────────────────────

fn elevenlabs_key() -> Result<String> {
    std::env::var("ASURADA_ELEVENLABS_API_KEY").map_err(|_| {
        anyhow::anyhow!(
            "ASURADA_ELEVENLABS_API_KEY 환경 변수 필요.\n\
             https://elevenlabs.io 에서 발급 후:\n\
             \texport ASURADA_ELEVENLABS_API_KEY=..."
        )
    })
}

fn load_config() -> Result<config::Config> {
    config::Config::load_or_default(&paths::config_file()?)
}

async fn cmd_tts_set(enabled: bool) -> Result<()> {
    let mut cfg = load_config()?;
    cfg.tts.enabled = enabled;
    cfg.save(&paths::config_file()?)?;
    println!("TTS {}", if enabled { "ON" } else { "OFF" });
    Ok(())
}

async fn cmd_tts_status() -> Result<()> {
    let cfg = load_config()?;
    println!("TTS:        {}", if cfg.tts.enabled { "ON" } else { "OFF" });
    println!(
        "API key:    {}",
        if std::env::var("ASURADA_ELEVENLABS_API_KEY").is_ok() {
            "set"
        } else {
            "not set"
        }
    );
    println!(
        "Voice ID:   {}",
        cfg.tts
            .voice_id
            .or_else(|| std::env::var("ASURADA_ELEVENLABS_VOICE_ID").ok())
            .unwrap_or_else(|| "(not configured)".into())
    );
    Ok(())
}

async fn cmd_tts_voices() -> Result<()> {
    let key = elevenlabs_key()?;
    let client = ai::elevenlabs::ElevenLabsClient::new(key)?;
    let voices = client.list_voices().await?;
    println!("ElevenLabs voices:");
    for v in &voices {
        println!("  {:32}  {}", v.name, v.voice_id);
    }
    println!();
    println!(
        "Set with: ASURADA_ELEVENLABS_VOICE_ID=<voice_id>  또는 config.toml [tts] voice_id"
    );
    Ok(())
}

async fn cmd_tts_test(text: String) -> Result<()> {
    let key = elevenlabs_key()?;
    let cfg = load_config()?;
    let voice_id = cfg
        .tts
        .voice_id
        .or_else(|| std::env::var("ASURADA_ELEVENLABS_VOICE_ID").ok())
        .ok_or_else(|| anyhow::anyhow!("voice_id 가 설정되지 않음. `asurada tts voices` 로 조회 후 설정."))?;
    let client = ai::elevenlabs::ElevenLabsClient::new(key)?;
    println!("[TTS] {}", text);
    client.speak(&text, &voice_id).await?;
    Ok(())
}

async fn cmd_tts_speak(project: Option<String>, limit: usize) -> Result<()> {
    let cfg = load_config()?;
    if !cfg.tts.enabled {
        println!("TTS OFF — `asurada tts on` 으로 활성화하세요.");
        return Ok(());
    }
    let key = elevenlabs_key()?;
    let voice_id = cfg
        .tts
        .voice_id
        .clone()
        .or_else(|| std::env::var("ASURADA_ELEVENLABS_VOICE_ID").ok())
        .ok_or_else(|| anyhow::anyhow!("voice_id 미설정. `asurada tts voices`"))?;

    // brain.db 에서 미확인 advice 조회.
    let user_id = std::env::var("ASURADA_USER_ID").unwrap_or_else(|_| "default".into());
    let brain_path = paths::brain_db()?;
    let conn = db::open(&brain_path)?;
    let pending = db::advice::list_pending(&conn, &user_id, project.as_deref(), limit)?;

    if pending.is_empty() {
        println!("읽을 미확인 어드바이스가 없습니다.");
        return Ok(());
    }

    let client = ai::elevenlabs::ElevenLabsClient::new(key)?;
    for adv in &pending {
        let speech = ai::speech::condense_for_speech(&adv.text, &adv.severity, &adv.project);
        println!("[{}] {}  {}", adv.severity, adv.project, speech);
        if let Err(e) = client.speak(&speech, &voice_id).await {
            tracing::warn!("speak failed: {}", e);
        }
    }
    Ok(())
}

async fn open_sync() -> Result<sync::Sync> {
    use std::sync::{Arc, Mutex};
    let creds = credentials::Credentials::require_env()?;
    let brain_path = paths::brain_db()?;
    let conn = db::open(&brain_path)?;
    let brain = Arc::new(Mutex::new(conn));
    sync::Sync::connect(brain, &creds).await
}

async fn cmd_sync_push() -> Result<()> {
    let s = open_sync().await?;
    let p = s.push_all().await?;
    println!(
        "push  events={} memories={} advice={} projects={}",
        p.events, p.memories, p.advice, p.projects
    );
    Ok(())
}

async fn cmd_sync_pull() -> Result<()> {
    let s = open_sync().await?;
    let p = s.pull_all().await?;
    println!(
        "pull  events={} memories={} advice={} projects={}",
        p.events, p.memories, p.advice, p.projects
    );
    Ok(())
}

async fn cmd_sync_run() -> Result<()> {
    let s = open_sync().await?;
    let pu = s.push_all().await?;
    let pl = s.pull_all().await?;
    println!(
        "push  events={} memories={} advice={} projects={}",
        pu.events, pu.memories, pu.advice, pu.projects
    );
    println!(
        "pull  events={} memories={} advice={} projects={}",
        pl.events, pl.memories, pl.advice, pl.projects
    );
    Ok(())
}

async fn cmd_init() -> Result<()> {
    let brain = paths::brain_db()?;
    if let Some(parent) = brain.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let _conn = db::open(&brain)?;
    tracing::info!("brain.db ready at {}", brain.display());
    println!("아스라다가 깨어났습니다 🌟");
    println!("  brain.db: {}", brain.display());
    Ok(())
}

async fn cmd_serve(port: u16) -> Result<()> {
    use std::sync::{Arc, Mutex};
    use std::time::Duration;

    let brain_path = paths::brain_db()?;
    if let Some(parent) = brain_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let conn = db::open(&brain_path)?;
    let brain = Arc::new(Mutex::new(conn));

    // 자격증명 있으면 백그라운드 sync 루프 시작.
    if let Some(creds) = credentials::Credentials::from_env() {
        match sync::Sync::connect(brain.clone(), &creds).await {
            Ok(s) => {
                tokio::spawn(s.run_loop(Duration::from_secs(30)));
            }
            Err(e) => {
                tracing::warn!("[sync] disabled — connect failed: {}", e);
            }
        }
    } else {
        tracing::warn!(
            "[sync] disabled — set ASURADA_DATABASE_URL and ASURADA_USER_ID to enable cloud sync"
        );
    }

    // HTTP API (axum) — brain 동일 Mutex 공유.
    let state = api::AppState { conn: brain };
    let app = api::router(state);

    let addr = format!("127.0.0.1:{}", port);
    tracing::info!("listening on http://{}", addr);
    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, app).await?;
    Ok(())
}

fn cmd_path() -> Result<()> {
    println!("config_dir: {}", paths::config_dir()?.display());
    println!("brain_db:   {}", paths::brain_db()?.display());
    Ok(())
}