gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! Daemon process management.

#![allow(dead_code)]

use crate::error::{GdeltError, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;

use super::state::DaemonState;

/// Information about the daemon
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonInfo {
    pub running: bool,
    pub pid: Option<u32>,
    pub uptime_secs: Option<u64>,
    pub last_sync: Option<String>,
    pub next_sync: Option<String>,
    pub sync_status: super::state::SyncStatus,
    pub mcp_enabled: bool,
    pub mcp_port: u16,
}

/// Start the daemon process
#[cfg(unix)]
pub fn start_daemon(mcp_enabled: bool, sync_enabled: bool, port: u16) -> Result<DaemonInfo> {
    // Check if already running
    let state = DaemonState::load()?;
    if state.is_actually_running() {
        return Err(GdeltError::Daemon(format!(
            "Daemon already running with PID {}",
            state.pid.unwrap_or(0)
        )));
    }

    // Get the current executable
    let exe = std::env::current_exe()
        .map_err(|e| GdeltError::Daemon(format!("Could not get executable path: {}", e)))?;

    // Build arguments for the daemon run command
    let mut args = vec!["daemon".to_string(), "run".to_string()];
    if mcp_enabled {
        args.push("--mcp".to_string());
    }
    if sync_enabled {
        args.push("--sync".to_string());
    }
    args.push("--port".to_string());
    args.push(port.to_string());

    // Fork and exec
    let log_path = DaemonState::log_file_path()?;
    let log_file = std::fs::File::create(&log_path)
        .map_err(|e| GdeltError::Daemon(format!("Could not create log file: {}", e)))?;

    let child = Command::new(&exe)
        .args(&args)
        .stdout(log_file.try_clone()?)
        .stderr(log_file)
        .spawn()
        .map_err(|e| GdeltError::Daemon(format!("Could not spawn daemon: {}", e)))?;

    let pid = child.id();

    // Update state
    let mut state = DaemonState::default();
    state.mark_started(pid);
    state.mcp_enabled = mcp_enabled;
    state.mcp_port = port;
    state.save()?;

    // Write PID file
    let pid_path = DaemonState::pid_file_path()?;
    std::fs::write(&pid_path, pid.to_string())?;

    Ok(get_daemon_info(&state))
}

#[cfg(not(unix))]
pub fn start_daemon(_mcp_enabled: bool, _sync_enabled: bool, _port: u16) -> Result<DaemonInfo> {
    Err(GdeltError::Daemon(
        "Daemon mode is only supported on Unix systems".into(),
    ))
}

/// Stop the daemon process
pub fn stop_daemon() -> Result<DaemonInfo> {
    let mut state = DaemonState::load()?;

    if !state.is_actually_running() {
        state.mark_stopped();
        state.save()?;
        return Err(GdeltError::Daemon("Daemon is not running".into()));
    }

    let pid = state.pid.ok_or_else(|| GdeltError::Daemon("No PID found".into()))?;

    // Send SIGTERM
    #[cfg(unix)]
    {
        use std::process::Command;
        Command::new("kill")
            .args([&pid.to_string()])
            .output()
            .map_err(|e| GdeltError::Daemon(format!("Could not kill daemon: {}", e)))?;
    }

    // Update state
    state.mark_stopped();
    state.save()?;

    // Remove PID file
    let pid_path = DaemonState::pid_file_path()?;
    let _ = std::fs::remove_file(pid_path);

    Ok(get_daemon_info(&state))
}

/// Get daemon status
pub fn get_daemon_status() -> Result<DaemonInfo> {
    let mut state = DaemonState::load()?;

    // Verify actual running state
    if state.running && !state.is_actually_running() {
        state.mark_stopped();
        state.save()?;
    }

    Ok(get_daemon_info(&state))
}

/// Restart the daemon
pub fn restart_daemon() -> Result<DaemonInfo> {
    let state = DaemonState::load()?;
    let mcp_enabled = state.mcp_enabled;
    let port = state.mcp_port;

    // Stop if running
    if state.is_actually_running() {
        stop_daemon()?;
        // Brief pause to allow cleanup
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    // Start with same settings
    start_daemon(mcp_enabled, true, port)
}

fn get_daemon_info(state: &DaemonState) -> DaemonInfo {
    let uptime_secs = state.started_at.map(|started| {
        let now = chrono::Utc::now();
        (now - started).num_seconds() as u64
    });

    DaemonInfo {
        running: state.running && state.is_actually_running(),
        pid: state.pid,
        uptime_secs,
        last_sync: state.last_sync.map(|t| t.format("%Y-%m-%d %H:%M:%S UTC").to_string()),
        next_sync: state.next_sync.map(|t| t.format("%Y-%m-%d %H:%M:%S UTC").to_string()),
        sync_status: state.sync_status.clone(),
        mcp_enabled: state.mcp_enabled,
        mcp_port: state.mcp_port,
    }
}

/// Read daemon logs
pub fn read_daemon_logs(tail_lines: u32, follow: bool) -> Result<String> {
    let log_path = DaemonState::log_file_path()?;

    if !log_path.exists() {
        return Ok("No log file found".to_string());
    }

    if follow {
        // For follow mode, we'd need to spawn tail -f
        // For now, just return the last lines
        #[cfg(unix)]
        {
            let output = Command::new("tail")
                .args(["-n", &tail_lines.to_string(), &log_path.to_string_lossy()])
                .output()
                .map_err(|e| GdeltError::Daemon(format!("Could not read logs: {}", e)))?;
            return Ok(String::from_utf8_lossy(&output.stdout).to_string());
        }
    }

    // Read last N lines
    let content = std::fs::read_to_string(&log_path)?;
    let lines: Vec<&str> = content.lines().collect();
    let start = lines.len().saturating_sub(tail_lines as usize);
    Ok(lines[start..].join("\n"))
}

/// Generate systemd service file
pub fn generate_systemd_service() -> String {
    let exe_path = std::env::current_exe()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_else(|_| "gdelt".to_string());

    format!(
        r#"[Unit]
Description=GDELT CLI Daemon
After=network.target

[Service]
Type=simple
ExecStart={} daemon run --mcp --sync
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
"#,
        exe_path
    )
}

/// Generate launchd plist file
pub fn generate_launchd_plist() -> String {
    let exe_path = std::env::current_exe()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_else(|_| "gdelt".to_string());

    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.gdelt.daemon</string>
    <key>ProgramArguments</key>
    <array>
        <string>{}</string>
        <string>daemon</string>
        <string>run</string>
        <string>--mcp</string>
        <string>--sync</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
"#,
        exe_path
    )
}