gdelt 0.1.0

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

#![allow(dead_code)]

use crate::error::{GdeltError, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Daemon state persisted to disk
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonState {
    /// Process ID (if running)
    pub pid: Option<u32>,
    /// Whether the daemon is running
    pub running: bool,
    /// Last sync timestamp
    pub last_sync: Option<DateTime<Utc>>,
    /// Next scheduled sync
    pub next_sync: Option<DateTime<Utc>>,
    /// Sync interval in seconds
    pub sync_interval_secs: u64,
    /// MCP server enabled
    pub mcp_enabled: bool,
    /// MCP server port
    pub mcp_port: u16,
    /// Sync status
    pub sync_status: SyncStatus,
    /// Error message if any
    pub last_error: Option<String>,
    /// Start time
    pub started_at: Option<DateTime<Utc>>,
}

/// Status of the sync process
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SyncStatus {
    /// Files synced in last run
    pub files_synced: u64,
    /// Events imported in last run
    pub events_imported: u64,
    /// GKG records imported in last run
    pub gkg_imported: u64,
    /// Total syncs completed
    pub total_syncs: u64,
    /// Total sync failures
    pub sync_failures: u64,
}

impl Default for DaemonState {
    fn default() -> Self {
        Self {
            pid: None,
            running: false,
            last_sync: None,
            next_sync: None,
            sync_interval_secs: 15 * 60, // 15 minutes (GDELT update frequency)
            mcp_enabled: true,
            mcp_port: 8080,
            sync_status: SyncStatus::default(),
            last_error: None,
            started_at: None,
        }
    }
}

impl DaemonState {
    /// Get the state file path
    pub fn state_file_path() -> Result<PathBuf> {
        let data_dir = crate::config::data_dir()
            .ok_or_else(|| GdeltError::Config("Could not determine data directory".into()))?;
        Ok(data_dir.join("daemon.json"))
    }

    /// Get the PID file path
    pub fn pid_file_path() -> Result<PathBuf> {
        let data_dir = crate::config::data_dir()
            .ok_or_else(|| GdeltError::Config("Could not determine data directory".into()))?;
        Ok(data_dir.join("daemon.pid"))
    }

    /// Get the log file path
    pub fn log_file_path() -> Result<PathBuf> {
        let data_dir = crate::config::data_dir()
            .ok_or_else(|| GdeltError::Config("Could not determine data directory".into()))?;
        Ok(data_dir.join("daemon.log"))
    }

    /// Load state from disk
    pub fn load() -> Result<Self> {
        let path = Self::state_file_path()?;
        if path.exists() {
            let content = std::fs::read_to_string(&path)?;
            let state: DaemonState = serde_json::from_str(&content)?;
            Ok(state)
        } else {
            Ok(Self::default())
        }
    }

    /// Save state to disk
    pub fn save(&self) -> Result<()> {
        let path = Self::state_file_path()?;
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let content = serde_json::to_string_pretty(self)?;
        std::fs::write(path, content)?;
        Ok(())
    }

    /// Check if daemon is actually running (verify PID)
    pub fn is_actually_running(&self) -> bool {
        if let Some(pid) = self.pid {
            // Check if process is running
            #[cfg(unix)]
            {
                use std::process::Command;
                Command::new("kill")
                    .args(["-0", &pid.to_string()])
                    .output()
                    .map(|o| o.status.success())
                    .unwrap_or(false)
            }
            #[cfg(not(unix))]
            {
                false
            }
        } else {
            false
        }
    }

    /// Mark as started
    pub fn mark_started(&mut self, pid: u32) {
        self.pid = Some(pid);
        self.running = true;
        self.started_at = Some(Utc::now());
        self.last_error = None;
    }

    /// Mark as stopped
    pub fn mark_stopped(&mut self) {
        self.pid = None;
        self.running = false;
    }

    /// Record a successful sync
    pub fn record_sync_success(&mut self, files: u64, events: u64, gkg: u64) {
        self.last_sync = Some(Utc::now());
        self.next_sync = Some(Utc::now() + chrono::Duration::seconds(self.sync_interval_secs as i64));
        self.sync_status.files_synced = files;
        self.sync_status.events_imported = events;
        self.sync_status.gkg_imported = gkg;
        self.sync_status.total_syncs += 1;
        self.last_error = None;
    }

    /// Record a sync failure
    pub fn record_sync_failure(&mut self, error: &str) {
        self.sync_status.sync_failures += 1;
        self.last_error = Some(error.to_string());
    }
}