gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! Configuration command handlers.

use crate::cli::args::{ConfigCommand, GlobalArgs};
use crate::cli::output::OutputWriter;
use crate::config::{self, Config};
use crate::error::{ExitStatus, GdeltError, Result};
use serde_json::json;

/// Handle config commands
pub async fn handle_config(cmd: ConfigCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    match cmd {
        ConfigCommand::Show => show_config(global),
        ConfigCommand::Get(args) => get_config(&args.key, global),
        ConfigCommand::Set(args) => set_config(&args.key, &args.value, global),
        ConfigCommand::Reset => reset_config(global),
        ConfigCommand::Validate => validate_config(global),
    }
}

fn show_config(global: &GlobalArgs) -> Result<ExitStatus> {
    let config = config::load_config(global.config.clone())?;
    let output = OutputWriter::new(global);

    output.write_value(&json!({
        "config_path": config::default_config_path().map(|p| p.display().to_string()),
        "data_dir": config::data_dir().map(|p| p.display().to_string()),
        "cache_dir": config::cache_dir().map(|p| p.display().to_string()),
        "config": config,
    }))?;

    Ok(ExitStatus::Success)
}

fn get_config(key: &str, global: &GlobalArgs) -> Result<ExitStatus> {
    let config = config::load_config(global.config.clone())?;
    let output = OutputWriter::new(global);

    // Parse dot-separated keys
    let parts: Vec<&str> = key.split('.').collect();
    let value = get_nested_value(&config, &parts)?;

    output.write_value(&value)?;
    Ok(ExitStatus::Success)
}

fn get_nested_value(config: &Config, keys: &[&str]) -> Result<serde_json::Value> {
    let config_json = serde_json::to_value(config)?;

    let mut current = &config_json;
    for key in keys {
        current = current.get(key).ok_or_else(|| {
            GdeltError::Config(format!("Key not found: {}", keys.join(".")))
        })?;
    }

    Ok(current.clone())
}

fn set_config(key: &str, value: &str, global: &GlobalArgs) -> Result<ExitStatus> {
    let config_path = global.config.clone().or_else(config::default_config_path)
        .ok_or_else(|| GdeltError::Config("Could not determine config path".into()))?;

    // Load existing config or create new
    let mut config = if config_path.exists() {
        config::load_config(Some(config_path.clone()))?
    } else {
        Config::default()
    };

    // Parse and set the value
    let parts: Vec<&str> = key.split('.').collect();
    set_nested_value(&mut config, &parts, value)?;

    // Write config file
    let toml = config.to_toml().map_err(|e| GdeltError::Config(e.to_string()))?;

    if let Some(parent) = config_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&config_path, toml)?;

    let output = OutputWriter::new(global);
    output.write_value(&json!({
        "status": "success",
        "key": key,
        "value": value,
        "config_path": config_path.display().to_string(),
    }))?;

    Ok(ExitStatus::Success)
}

fn set_nested_value(config: &mut Config, keys: &[&str], value: &str) -> Result<()> {
    if keys.is_empty() {
        return Err(GdeltError::Config("Empty key".into()));
    }

    match keys[0] {
        "network" => match keys.get(1) {
            Some(&"timeout_secs") => config.network.timeout_secs = value.parse()
                .map_err(|_| GdeltError::Config("Invalid number".into()))?,
            Some(&"retries") => config.network.retries = value.parse()
                .map_err(|_| GdeltError::Config("Invalid number".into()))?,
            Some(&"rate_limit_rps") => config.network.rate_limit_rps = value.parse()
                .map_err(|_| GdeltError::Config("Invalid number".into()))?,
            Some(&"proxy") => config.network.proxy = if value.is_empty() { None } else { Some(value.to_string()) },
            Some(&"user_agent") => config.network.user_agent = value.to_string(),
            _ => return Err(GdeltError::Config(format!("Unknown network key: {}", keys.join(".")))),
        },
        "cache" => match keys.get(1) {
            Some(&"enabled") => config.cache.enabled = value.parse()
                .map_err(|_| GdeltError::Config("Invalid boolean".into()))?,
            Some(&"max_size_mb") => config.cache.max_size_mb = value.parse()
                .map_err(|_| GdeltError::Config("Invalid number".into()))?,
            _ => return Err(GdeltError::Config(format!("Unknown cache key: {}", keys.join(".")))),
        },
        "general" => match keys.get(1) {
            Some(&"default_format") => config.general.default_format = value.to_string(),
            Some(&"timezone") => config.general.timezone = value.to_string(),
            Some(&"color") => config.general.color = value.parse()
                .map_err(|_| GdeltError::Config("Invalid boolean".into()))?,
            Some(&"quiet") => config.general.quiet = value.parse()
                .map_err(|_| GdeltError::Config("Invalid boolean".into()))?,
            _ => return Err(GdeltError::Config(format!("Unknown general key: {}", keys.join(".")))),
        },
        "defaults" => match keys.get(1) {
            Some(&"doc") => match keys.get(2) {
                Some(&"timespan") => config.defaults.doc.timespan = value.to_string(),
                Some(&"max_records") => config.defaults.doc.max_records = value.parse()
                    .map_err(|_| GdeltError::Config("Invalid number".into()))?,
                Some(&"sort") => config.defaults.doc.sort = value.to_string(),
                _ => return Err(GdeltError::Config(format!("Unknown defaults.doc key: {}", keys.join(".")))),
            },
            _ => return Err(GdeltError::Config(format!("Unknown defaults key: {}", keys.join(".")))),
        },
        _ => return Err(GdeltError::Config(format!("Unknown key: {}", keys.join(".")))),
    }

    Ok(())
}

fn reset_config(global: &GlobalArgs) -> Result<ExitStatus> {
    let config_path = global.config.clone().or_else(config::default_config_path)
        .ok_or_else(|| GdeltError::Config("Could not determine config path".into()))?;

    let config = Config::default();
    let toml = config.to_toml().map_err(|e| GdeltError::Config(e.to_string()))?;

    if let Some(parent) = config_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&config_path, toml)?;

    let output = OutputWriter::new(global);
    output.write_value(&json!({
        "status": "success",
        "message": "Configuration reset to defaults",
        "config_path": config_path.display().to_string(),
    }))?;

    Ok(ExitStatus::Success)
}

fn validate_config(global: &GlobalArgs) -> Result<ExitStatus> {
    let output = OutputWriter::new(global);

    match config::load_config(global.config.clone()) {
        Ok(config) => {
            // Additional validation checks
            let mut warnings = Vec::new();

            if config.network.timeout_secs == 0 {
                warnings.push("network.timeout_secs should be greater than 0");
            }
            if config.network.retries > 10 {
                warnings.push("network.retries > 10 may cause excessive delays");
            }

            output.write_value(&json!({
                "valid": true,
                "warnings": warnings,
            }))?;

            Ok(ExitStatus::Success)
        }
        Err(e) => {
            output.write_value(&json!({
                "valid": false,
                "error": e.to_string(),
            }))?;

            Ok(ExitStatus::ConfigError)
        }
    }
}