gdelt 0.1.0

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

use crate::api::client::GdeltClient;
use crate::api::doc::{DocApi, DocSearchParams, DocTimelineParams, DocWordcloudParams};
use crate::cli::args::{DocCommand, GlobalArgs, SortOrder, TimelineMode};
use crate::cli::output::OutputWriter;
use crate::enrich::{ArticleInput, EnrichmentService};
use crate::error::{ExitStatus, Result};
use std::time::Duration;

/// Handle DOC commands
pub async fn handle_doc(cmd: DocCommand, global: &GlobalArgs) -> Result<ExitStatus> {
    let client = GdeltClient::with_timeout(Duration::from_secs(global.timeout))?;
    let api = DocApi::new(client.clone());

    match cmd {
        DocCommand::Search(args) => {
            let sort = match args.sort {
                SortOrder::DateDesc => "datedesc",
                SortOrder::DateAsc => "dateasc",
                SortOrder::ToneDesc => "tonedesc",
                SortOrder::ToneAsc => "toneasc",
                SortOrder::HybridRel => "hybridrel",
            };

            let params = DocSearchParams {
                query: args.query,
                timespan: Some(args.timespan),
                start_datetime: args.start,
                end_datetime: args.end,
                max_records: Some(args.max_records),
                sort: Some(sort.to_string()),
                source_lang: args.lang,
                source_country: args.country,
                domain: args.domain,
                theme: args.theme,
                tone_min: args.tone_min,
                tone_max: args.tone_max,
            };

            let response = api.search(params).await?;
            let output = OutputWriter::new(global);

            // If enrichment is requested, enrich the articles
            if args.enrich || args.fetch_text {
                if !global.quiet {
                    eprintln!("Enriching {} articles...", response.articles.len());
                }

                let service = if args.fetch_text {
                    EnrichmentService::with_fetcher(client)?
                } else {
                    EnrichmentService::new()?
                };

                // Convert articles to ArticleInput
                let inputs: Vec<ArticleInput> = response
                    .articles
                    .iter()
                    .map(|a| ArticleInput::with_metadata(
                        a.url.clone(),
                        Some(a.title.clone()),
                        Some(a.seendate.clone()),
                        Some(a.domain.clone()),
                        Some(a.language.clone()),
                    ))
                    .collect();

                let (enriched, stats) = service
                    .enrich_batch(inputs, args.fetch_text, args.fetch_concurrency)
                    .await;

                let result = serde_json::json!({
                    "articles": enriched,
                    "enrichment_stats": stats,
                });

                output.write_value(&result)?;

                if !global.quiet {
                    eprintln!();
                    eprintln!("Enrichment complete:");
                    eprintln!("  GKG found: {}/{}", stats.gkg_found, stats.total);
                    eprintln!("  GKG local: {}", stats.gkg_local);
                    if args.fetch_text {
                        eprintln!("  Text fetched: {}", stats.text_fetched);
                    }
                }
            } else {
                output.write_value(&response)?;
            }

            Ok(ExitStatus::Success)
        }

        DocCommand::Timeline(args) => {
            let mode = match args.mode {
                TimelineMode::Vol => "vol",
                TimelineMode::VolRaw => "volraw",
                TimelineMode::Tone => "tone",
                TimelineMode::Lang => "lang",
                TimelineMode::SourceCountry => "sourcecountry",
            };

            let params = DocTimelineParams {
                query: args.query,
                mode: Some(mode.to_string()),
                timespan: Some(args.timespan),
                smooth: args.smooth,
            };

            let response = api.timeline(params).await?;
            let output = OutputWriter::new(global);
            output.write_value(&response)?;

            Ok(ExitStatus::Success)
        }

        DocCommand::Wordcloud(args) => {
            let params = DocWordcloudParams {
                query: args.query,
                timespan: Some(args.timespan),
            };

            let response = api.wordcloud(params).await?;
            let output = OutputWriter::new(global);
            output.write_value(&response)?;

            Ok(ExitStatus::Success)
        }

        DocCommand::Themes(args) => {
            // Return a list of common GKG themes
            let themes = get_common_themes(args.filter.as_deref());
            let output = OutputWriter::new(global);
            output.write_value(&serde_json::json!({ "themes": themes }))?;

            Ok(ExitStatus::Success)
        }
    }
}

fn get_common_themes(filter: Option<&str>) -> Vec<&'static str> {
    let all_themes = vec![
        "AFFECT", "ARMEDCONFLICT", "ARREST", "BAN", "BANKRUPTCY",
        "CEASEFIRE", "CONSPIRACY", "CORRUPTION", "COUP", "CRIME",
        "CRISISLEX", "CYBER_ATTACK", "DEMOCRACY", "DIPLOMATIC_MEETING",
        "DISASTER", "DISEASE", "ECONOMY", "EDUCATION", "ELECTION",
        "EMBARGO", "EMIGRATION", "EMPLOYMENT", "ENVIRONMENT", "EPIDEMIC",
        "EVACUATION", "FAMINE", "FINANCIAL_CRISIS", "FOOD_SECURITY",
        "GENERAL_GOVERNMENT", "HEALTH", "HOSTAGE", "HUMANITARIAN",
        "HUMAN_RIGHTS", "IMMIGRATION", "INFRASTRUCTURE", "INTERNET",
        "KILL", "LABOR", "LEGISLATION", "MANMADE_DISASTER", "MEDIA",
        "MILITARY", "NATURAL_DISASTER", "NEGOTIATION", "NUCLEAR",
        "PEACEKEEPING", "POLITICAL_TURMOIL", "POLLUTION", "POVERTY",
        "PROTEST", "PUBLIC_HEALTH", "REBELLION", "REFUGEE", "RELIGION",
        "SANCTIONS", "SCIENCE", "SECURITY", "SOCIAL_MEDIA", "SOVEREIGNTY",
        "SPACE", "SPORTS", "SURVEILLANCE", "TAX", "TECHNOLOGY",
        "TERROR", "TORTURE", "TRADE", "UNEMPLOYMENT", "VIOLENCE",
        "WAR_CRIME", "WATER", "WEATHER", "WMD",
    ];

    match filter {
        Some(f) => {
            let f_upper = f.to_uppercase();
            all_themes
                .into_iter()
                .filter(|t| t.contains(&f_upper))
                .collect()
        }
        None => all_themes,
    }
}