//! Pipeline orchestration: file-level and article-level parallelism.
//!
//! Processes multiple GDELT files in parallel, and within each file
//! reconstructs articles in parallel using rayon.

use crate::assemble::{assemble, clean_text};
use crate::fragment::create_fragments;
use crate::parse::{load_and_filter, RawEntry};
use rayon::prelude::*;
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};

/// A single reconstructed article.
#[derive(Debug, Clone)]
pub struct ReconstructedArticle {
    pub text: String,
    pub date: String,
    pub url: String,
    pub source: String,
    pub fragments_used: usize,
    pub fragments_total: usize,
}

/// Determine source label from URL and filters.
fn source_label(url: &str, url_filters: Option<&[String]>) -> String {
    let filters = match url_filters {
        Some(f) if !f.is_empty() => f,
        _ => return String::new(),
    };

    let url_lower = url.to_lowercase();
    let matches: Vec<&String> = filters
        .iter()
        .filter(|f| !f.is_empty() && url_lower.contains(&f.to_lowercase()))
        .collect();

    match matches.len() {
        1 => matches[0].clone(),
        n if n > 1 => "Multiple URL matched".to_string(),
        _ => String::new(),
    }
}

/// Reconstruct a single article from its raw entries.
fn reconstruct_article(
    url: &str,
    entries: &[RawEntry],
    url_filters: Option<&[String]>,
) -> ReconstructedArticle {
    let source = source_label(url, url_filters);

    if entries.is_empty() {
        return ReconstructedArticle {
            text: String::new(),
            date: String::new(),
            url: url.to_string(),
            source,
            fragments_used: 0,
            fragments_total: 0,
        };
    }

    let fragments = create_fragments(entries);
    let result = assemble(&fragments);

    let text = clean_text(&result.words);

    // Date from first entry
    let date = entries
        .first()
        .map(|e| {
            if e.date.len() >= 10 {
                e.date[..10].to_string()
            } else {
                String::new()
            }
        })
        .unwrap_or_default();

    ReconstructedArticle {
        text,
        date,
        url: url.to_string(),
        source,
        fragments_used: result.fragments_used,
        fragments_total: result.fragments_total,
    }
}

/// Process a single GDELT file: parse, reconstruct all articles, return results.
///
/// Articles within a file are processed in parallel using rayon.
pub fn process_file(
    path: &Path,
    language: Option<&str>,
    url_filters: Option<&[String]>,
) -> Vec<ReconstructedArticle> {
    let grouped = load_and_filter(path, language, url_filters);

    if grouped.groups.is_empty() {
        return Vec::new();
    }

    // Collect into a Vec of (url, entries) for parallel processing
    let mut groups = grouped.groups;
    let work: Vec<(String, Vec<RawEntry>)> = grouped
        .url_order
        .into_iter()
        .filter_map(|url| {
            groups.remove(&url).map(|entries| (url, entries))
        })
        .collect();

    // Parallel article reconstruction
    work.par_iter()
        .map(|(url, entries)| reconstruct_article(url, entries, url_filters))
        .collect()
}

/// Write reconstructed articles to a JSONL file.
pub fn write_jsonl(articles: &[ReconstructedArticle], output_path: &Path) -> std::io::Result<()> {
    let file = fs::File::create(output_path)?;
    let mut writer = BufWriter::new(file);

    for article in articles {
        // Manual JSON construction to avoid serde dependency on output structs
        let json = format!(
            r#"{{"text":{},"date":{},"url":{},"source":{},"fragments_used":{},"fragments_total":{}}}"#,
            serde_json::to_string(&article.text).unwrap_or_default(),
            serde_json::to_string(&article.date).unwrap_or_default(),
            serde_json::to_string(&article.url).unwrap_or_default(),
            serde_json::to_string(&article.source).unwrap_or_default(),
            article.fragments_used,
            article.fragments_total,
        );
        writeln!(writer, "{}", json)?;
    }

    writer.flush()?;
    Ok(())
}

/// Process multiple GDELT files in parallel (file-level parallelism).
///
/// Each file is processed independently. Within each file, articles are
/// reconstructed in parallel (article-level parallelism via rayon).
///
/// Returns the total number of articles reconstructed.
pub fn process_files(
    input_paths: &[PathBuf],
    output_dir: &Path,
    language: Option<&str>,
    url_filters: Option<&[String]>,
    num_threads: Option<usize>,
) -> usize {
    // Configure rayon thread pool
    if let Some(threads) = num_threads {
        rayon::ThreadPoolBuilder::new()
            .num_threads(threads)
            .build_global()
            .ok(); // Ignore if already set
    }

    fs::create_dir_all(output_dir).expect("Failed to create output directory");

    let results: Vec<(PathBuf, Vec<ReconstructedArticle>)> = input_paths
        .par_iter()
        .map(|path| {
            let articles = process_file(path, language, url_filters);
            let stem = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("output");
            // Strip .webngrams.json or .json suffix for clean output name
            let clean_stem = stem
                .strip_suffix(".webngrams")
                .unwrap_or(stem);
            let output_path = output_dir.join(format!("{}.jsonl", clean_stem));
            (output_path, articles)
        })
        .collect();

    let mut total = 0;
    for (output_path, articles) in &results {
        total += articles.len();
        if !articles.is_empty() {
            write_jsonl(articles, output_path)
                .unwrap_or_else(|e| eprintln!("Failed to write {}: {}", output_path.display(), e));
        }
    }

    total
}

/// Process a single file and return articles as a vector (for Python binding).
pub fn process_file_to_articles(
    path: &Path,
    language: Option<&str>,
    url_filters: Option<&[String]>,
) -> Vec<ReconstructedArticle> {
    process_file(path, language, url_filters)
}
