gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! Configuration schema for GDELT CLI.
//!
//! Configuration file location: ~/.config/gdelt/config.toml

#![allow(dead_code)]

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

/// Root configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    /// General settings
    pub general: GeneralConfig,

    /// Network settings
    pub network: NetworkConfig,

    /// Cache settings
    pub cache: CacheConfig,

    /// Database settings
    pub database: DatabaseConfig,

    /// Default parameters for commands
    pub defaults: DefaultsConfig,

    /// Named profiles for different use cases
    #[serde(default)]
    pub profiles: HashMap<String, ProfileConfig>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            general: GeneralConfig::default(),
            network: NetworkConfig::default(),
            cache: CacheConfig::default(),
            database: DatabaseConfig::default(),
            defaults: DefaultsConfig::default(),
            profiles: HashMap::new(),
        }
    }
}

/// General settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
    /// Default output format
    pub default_format: String,

    /// Timezone for date display (e.g., "UTC", "America/New_York")
    pub timezone: String,

    /// Enable colored output
    pub color: bool,

    /// Suppress non-essential output by default
    pub quiet: bool,
}

impl Default for GeneralConfig {
    fn default() -> Self {
        Self {
            default_format: "auto".to_string(),
            timezone: "UTC".to_string(),
            color: true,
            quiet: false,
        }
    }
}

/// Network settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkConfig {
    /// Request timeout in seconds
    pub timeout_secs: u64,

    /// Number of retry attempts
    pub retries: u32,

    /// Rate limit: requests per second
    pub rate_limit_rps: f64,

    /// HTTP proxy URL
    pub proxy: Option<String>,

    /// User agent string
    pub user_agent: String,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            timeout_secs: 30,
            retries: 3,
            rate_limit_rps: 5.0,
            proxy: None,
            user_agent: format!("gdelt-cli/{}", env!("CARGO_PKG_VERSION")),
        }
    }
}

/// Cache settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CacheConfig {
    /// Enable caching
    pub enabled: bool,

    /// Cache directory path (defaults to system cache dir)
    pub path: Option<PathBuf>,

    /// Maximum cache size in megabytes
    pub max_size_mb: u64,

    /// TTL settings for different data types
    pub ttl: CacheTtlConfig,
}

impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            path: None,
            max_size_mb: 500,
            ttl: CacheTtlConfig::default(),
        }
    }
}

/// Cache TTL settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CacheTtlConfig {
    /// TTL for DOC API search results
    #[serde(with = "humantime_serde")]
    pub doc_search: Duration,

    /// TTL for DOC API timeline results
    #[serde(with = "humantime_serde")]
    pub doc_timeline: Duration,

    /// TTL for GEO API results
    #[serde(with = "humantime_serde")]
    pub geo: Duration,

    /// TTL for TV API results
    #[serde(with = "humantime_serde")]
    pub tv: Duration,

    /// TTL for raw API responses
    #[serde(with = "humantime_serde")]
    pub raw_response: Duration,

    /// Grace period for stale cache entries
    #[serde(with = "humantime_serde")]
    pub grace_period: Duration,
}

impl Default for CacheTtlConfig {
    fn default() -> Self {
        Self {
            doc_search: Duration::from_secs(3600),         // 1 hour
            doc_timeline: Duration::from_secs(1800),       // 30 minutes
            geo: Duration::from_secs(86400),               // 24 hours
            tv: Duration::from_secs(7200),                 // 2 hours
            raw_response: Duration::from_secs(900),        // 15 minutes
            grace_period: Duration::from_secs(300),        // 5 minutes
        }
    }
}

/// Database settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DatabaseConfig {
    /// Path to DuckDB database file
    pub path: Option<PathBuf>,

    /// Memory limit for DuckDB (e.g., "4GB")
    pub memory_limit: String,

    /// Number of threads for DuckDB
    pub threads: Option<u32>,

    /// Enable read-only mode
    pub read_only: bool,
}

impl Default for DatabaseConfig {
    fn default() -> Self {
        Self {
            path: None,
            memory_limit: "2GB".to_string(),
            threads: None,
            read_only: false,
        }
    }
}

/// Default parameter settings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DefaultsConfig {
    /// Default settings for DOC API
    pub doc: DocDefaults,

    /// Default settings for GEO API
    pub geo: GeoDefaults,

    /// Default settings for TV API
    pub tv: TvDefaults,

    /// Default settings for analytics
    pub analytics: AnalyticsDefaults,
}

impl Default for DefaultsConfig {
    fn default() -> Self {
        Self {
            doc: DocDefaults::default(),
            geo: GeoDefaults::default(),
            tv: TvDefaults::default(),
            analytics: AnalyticsDefaults::default(),
        }
    }
}

/// Default settings for DOC API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DocDefaults {
    /// Default timespan
    pub timespan: String,

    /// Default max records
    pub max_records: u32,

    /// Default sort order
    pub sort: String,
}

impl Default for DocDefaults {
    fn default() -> Self {
        Self {
            timespan: "24h".to_string(),
            max_records: 75,
            sort: "hybrid-rel".to_string(),
        }
    }
}

/// Default settings for GEO API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeoDefaults {
    /// Default timespan (max 7 days)
    pub timespan: String,

    /// Default max points
    pub max_points: u32,
}

impl Default for GeoDefaults {
    fn default() -> Self {
        Self {
            timespan: "24h".to_string(),
            max_points: 250,
        }
    }
}

/// Default settings for TV API
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TvDefaults {
    /// Default timespan
    pub timespan: String,

    /// Default max records
    pub max_records: u32,
}

impl Default for TvDefaults {
    fn default() -> Self {
        Self {
            timespan: "7d".to_string(),
            max_records: 250,
        }
    }
}

/// Default settings for analytics
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AnalyticsDefaults {
    /// Default timespan for trends
    pub timespan: String,

    /// Default granularity
    pub granularity: String,

    /// Default entity minimum count
    pub min_entity_count: u32,
}

impl Default for AnalyticsDefaults {
    fn default() -> Self {
        Self {
            timespan: "30d".to_string(),
            granularity: "day".to_string(),
            min_entity_count: 5,
        }
    }
}

/// Profile configuration (can override defaults)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProfileConfig {
    /// Override cache settings
    pub cache: Option<CacheConfig>,

    /// Override network settings
    pub network: Option<NetworkConfig>,

    /// Override defaults
    pub defaults: Option<DefaultsConfig>,
}

impl Config {
    /// Apply a named profile to the configuration
    pub fn with_profile(mut self, name: &str) -> Self {
        if let Some(profile) = self.profiles.get(name).cloned() {
            if let Some(cache) = profile.cache {
                self.cache = cache;
            }
            if let Some(network) = profile.network {
                self.network = network;
            }
            if let Some(defaults) = profile.defaults {
                self.defaults = defaults;
            }
        }
        self
    }

    /// Serialize to TOML string
    pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
        toml::to_string_pretty(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert!(config.cache.enabled);
        assert_eq!(config.network.timeout_secs, 30);
    }

    #[test]
    fn test_config_serialization() {
        let config = Config::default();
        let toml = config.to_toml().unwrap();
        assert!(toml.contains("[general]"));
        assert!(toml.contains("[network]"));
    }
}