//! PyO3 bindings for gdeltnews-rs.
//!
//! Exposes the Rust reconstruction engine to Python.

mod assemble;
mod fragment;
mod parse;
mod pipeline;

use pyo3::prelude::*;
use std::path::{Path, PathBuf};

/// A reconstructed article exposed to Python.
#[pyclass]
#[derive(Clone)]
struct Article {
    #[pyo3(get)]
    text: String,
    #[pyo3(get)]
    date: String,
    #[pyo3(get)]
    url: String,
    #[pyo3(get)]
    source: String,
    #[pyo3(get)]
    fragments_used: usize,
    #[pyo3(get)]
    fragments_total: usize,
}

#[pymethods]
impl Article {
    fn __repr__(&self) -> String {
        format!(
            "Article(url={:?}, len={}, frags={}/{})",
            self.url,
            self.text.len(),
            self.fragments_used,
            self.fragments_total,
        )
    }

    /// Convert to a Python dict.
    fn to_dict(&self) -> pyo3::PyResult<std::collections::HashMap<String, PyObject>> {
        Python::with_gil(|py| {
            let mut map = std::collections::HashMap::new();
            map.insert("text".into(), self.text.clone().into_pyobject(py)?.into_any().unbind());
            map.insert("date".into(), self.date.clone().into_pyobject(py)?.into_any().unbind());
            map.insert("url".into(), self.url.clone().into_pyobject(py)?.into_any().unbind());
            map.insert("source".into(), self.source.clone().into_pyobject(py)?.into_any().unbind());
            map.insert("fragments_used".into(), self.fragments_used.into_pyobject(py)?.into_any().unbind());
            map.insert("fragments_total".into(), self.fragments_total.into_pyobject(py)?.into_any().unbind());
            Ok(map)
        })
    }
}

impl From<pipeline::ReconstructedArticle> for Article {
    fn from(a: pipeline::ReconstructedArticle) -> Self {
        Article {
            text: a.text,
            date: a.date,
            url: a.url,
            source: a.source,
            fragments_used: a.fragments_used,
            fragments_total: a.fragments_total,
        }
    }
}

/// Process a single GDELT file and return reconstructed articles.
///
/// Args:
///     input_path: Path to a .json or .json.gz GDELT Web NGrams file.
///     language: Language code to filter by (e.g. "en"). None keeps all.
///     url_filters: List of URL substrings to filter by. None keeps all.
///
/// Returns:
///     List of Article objects.
#[pyfunction]
#[pyo3(signature = (input_path, language=None, url_filters=None))]
fn reconstruct_file(
    input_path: &str,
    language: Option<&str>,
    url_filters: Option<Vec<String>>,
) -> PyResult<Vec<Article>> {
    let path = Path::new(input_path);
    let filters_ref = url_filters.as_deref();

    let articles = pipeline::process_file_to_articles(path, language, filters_ref);

    Ok(articles.into_iter().map(Article::from).collect())
}

/// Process a single GDELT file and write results to a JSONL file.
///
/// Args:
///     input_path: Path to a .json or .json.gz GDELT Web NGrams file.
///     output_path: Path to output JSONL file.
///     language: Language code to filter by. None keeps all.
///     url_filters: List of URL substrings to filter by. None keeps all.
///
/// Returns:
///     Number of articles reconstructed.
#[pyfunction]
#[pyo3(signature = (input_path, output_path, language=None, url_filters=None))]
fn reconstruct_file_to_jsonl(
    input_path: &str,
    output_path: &str,
    language: Option<&str>,
    url_filters: Option<Vec<String>>,
) -> PyResult<usize> {
    let path = Path::new(input_path);
    let out_path = Path::new(output_path);
    let filters_ref = url_filters.as_deref();

    let articles = pipeline::process_file_to_articles(path, language, filters_ref);
    let count = articles.len();

    pipeline::write_jsonl(&articles, out_path)
        .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;

    Ok(count)
}

/// Process multiple GDELT files in parallel and write JSONL outputs.
///
/// Args:
///     input_paths: List of paths to .json or .json.gz files.
///     output_dir: Directory to write JSONL output files.
///     language: Language code to filter by. None keeps all.
///     url_filters: List of URL substrings to filter by. None keeps all.
///     num_threads: Number of threads for parallel processing. None uses all cores.
///
/// Returns:
///     Total number of articles reconstructed.
#[pyfunction]
#[pyo3(signature = (input_paths, output_dir, language=None, url_filters=None, num_threads=None))]
fn reconstruct_files(
    input_paths: Vec<String>,
    output_dir: &str,
    language: Option<&str>,
    url_filters: Option<Vec<String>>,
    num_threads: Option<usize>,
) -> PyResult<usize> {
    let paths: Vec<PathBuf> = input_paths.iter().map(PathBuf::from).collect();
    let out_dir = Path::new(output_dir);
    let filters_ref = url_filters.as_deref();

    let total = pipeline::process_files(&paths, out_dir, language, filters_ref, num_threads);
    Ok(total)
}

/// The native Rust module for gdeltnews-rs.
#[pymodule]
fn _gdeltnews_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<Article>()?;
    m.add_function(wrap_pyfunction!(reconstruct_file, m)?)?;
    m.add_function(wrap_pyfunction!(reconstruct_file_to_jsonl, m)?)?;
    m.add_function(wrap_pyfunction!(reconstruct_files, m)?)?;
    Ok(())
}
