//! Batch execution module.
//!
//! Handles parallel execution of multiple simulation scenarios.

use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::info;

use crate::error::{CliError, CliResult};
use crate::run::{run_simulation, SimulationResult, SimulationStatus};

/// Output format for batch results.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
    Json,
    Csv,
}

impl FromStr for OutputFormat {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "json" => Ok(OutputFormat::Json),
            "csv" => Ok(OutputFormat::Csv),
            _ => Err(format!(
                "Unknown output format: '{}'. Supported: json, csv",
                s
            )),
        }
    }
}

/// Aggregator for batch simulation results.
pub struct BatchAggregator {
    results: Vec<SimulationResult>,
}

impl BatchAggregator {
    pub fn new(results: Vec<SimulationResult>) -> Self {
        Self { results }
    }

    pub fn to_json(&self) -> CliResult<String> {
        let summary = self.summary();
        serde_json::to_string_pretty(&summary)
            .map_err(|e| CliError::Simulation(format!("Failed to serialize to JSON: {}", e)))
    }

    pub fn to_csv(&self) -> String {
        let mut lines = Vec::new();
        lines.push(
            "input,status,iterations,converged,residual,elapsed_ms,capacity_kw,power_kw,cop,error"
                .to_string(),
        );

        for r in &self.results {
            let status = format!("{:?}", r.status).to_lowercase();
            let iterations = r.iterations.map(|i| i.to_string()).unwrap_or_default();
            let converged = if r.status == SimulationStatus::Converged {
                "true"
            } else {
                "false"
            };
            let residual = r
                .convergence
                .as_ref()
                .map(|c| format!("{:.2e}", c.final_residual))
                .unwrap_or_default();
            let capacity = r
                .performance
                .as_ref()
                .and_then(|p| p.q_cooling_kw)
                .map(|q| format!("{:.2}", q))
                .unwrap_or_default();
            let power = r
                .performance
                .as_ref()
                .and_then(|p| p.compressor_power_kw)
                .map(|w| format!("{:.2}", w))
                .unwrap_or_default();
            let cop = r
                .performance
                .as_ref()
                .and_then(|p| p.cop)
                .map(|c| format!("{:.2}", c))
                .unwrap_or_default();
            let error = r
                .error
                .as_ref()
                .map(|e| format!("\"{}\"", e.replace('"', "\"\"")))
                .unwrap_or_default();

            lines.push(format!(
                "\"{}\",{},{},{},{},{},{},{},{},{}",
                r.input,
                status,
                iterations,
                converged,
                residual,
                r.elapsed_ms,
                capacity,
                power,
                cop,
                error
            ));
        }

        lines.join("\n")
    }

    pub fn summary(&self) -> BatchSummary {
        build_summary(self.results.clone(), 0)
    }
}

impl BatchSummary {
    /// Serialize summary to JSON string.
    pub fn to_json(&self) -> CliResult<String> {
        serde_json::to_string_pretty(self)
            .map_err(|e| CliError::Simulation(format!("Failed to serialize to JSON: {}", e)))
    }

    /// Serialize summary to CSV string.
    pub fn to_csv(&self) -> String {
        let aggregator = BatchAggregator::new(self.results.clone());
        aggregator.to_csv()
    }
}

/// Summary of batch execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchSummary {
    /// Total number of scenarios processed.
    pub total: usize,
    /// Number of successful simulations.
    pub succeeded: usize,
    /// Number of failed simulations.
    pub failed: usize,
    /// Number of non-converged simulations.
    pub non_converged: usize,
    /// Total execution time in milliseconds.
    pub total_elapsed_ms: u64,
    /// Average execution time per scenario in milliseconds.
    pub avg_elapsed_ms: f64,
    /// Individual results.
    pub results: Vec<SimulationResult>,
}

impl Default for BatchSummary {
    fn default() -> Self {
        Self {
            total: 0,
            succeeded: 0,
            failed: 0,
            non_converged: 0,
            total_elapsed_ms: 0,
            avg_elapsed_ms: 0.0,
            results: Vec::new(),
        }
    }
}

/// Run batch simulations from a directory of configuration files.
pub fn run_batch(
    directory: &Path,
    parallel: usize,
    output_dir: Option<&Path>,
    quiet: bool,
    verbose: bool,
) -> CliResult<BatchSummary> {
    if !directory.exists() {
        return Err(CliError::BatchDirNotFound(directory.to_path_buf()));
    }

    if !directory.is_dir() {
        return Err(CliError::Config(format!(
            "Path is not a directory: {}",
            directory.display()
        )));
    }

    let config_files = discover_config_files(directory)?;

    if config_files.is_empty() {
        return Err(CliError::NoConfigFiles(directory.to_path_buf()));
    }

    if verbose {
        info!("Found {} configuration files", config_files.len());
        info!("Running with {} parallel workers", parallel);
    }

    let start = std::time::Instant::now();
    let total = config_files.len();

    let pool = rayon::ThreadPoolBuilder::new()
        .num_threads(parallel)
        .build()
        .map_err(|e| CliError::Simulation(format!("Failed to create thread pool: {}", e)))?;

    let results: Vec<SimulationResult> = pool.install(|| {
        if quiet {
            config_files
                .par_iter()
                .map(|path| process_single_file(path, output_dir, verbose))
                .collect()
        } else {
            let progress = ProgressBar::new(total as u64);
            progress.set_style(
                ProgressStyle::default_bar()
                    .template(
                        "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
                    )
                    .unwrap()
                    .progress_chars("=>-"),
            );

            let completed = Arc::new(AtomicUsize::new(0));
            let errors = Arc::new(AtomicUsize::new(0));

            let results: Vec<SimulationResult> = config_files
                .par_iter()
                .progress_with(progress)
                .map(|path| {
                    let result = process_single_file(path, output_dir, verbose);

                    completed.fetch_add(1, Ordering::Relaxed);
                    if result.status == SimulationStatus::Error {
                        errors.fetch_add(1, Ordering::Relaxed);
                    }

                    result
                })
                .collect();

            let comp_count = completed.load(Ordering::Relaxed);
            let err_count = errors.load(Ordering::Relaxed);
            println!();
            println!(
                "Completed: {} | Errors: {} | Elapsed: {:.2}s",
                comp_count,
                err_count,
                start.elapsed().as_secs_f64()
            );

            results
        }
    });

    let summary = build_summary(results, start.elapsed().as_millis() as u64);

    if !quiet {
        print_batch_summary(&summary);
    }

    Ok(summary)
}

/// Discover all JSON configuration files in a directory.
pub fn discover_config_files(directory: &Path) -> CliResult<Vec<PathBuf>> {
    let mut files = Vec::new();

    for entry in std::fs::read_dir(directory)? {
        let entry = entry?;
        let path = entry.path();

        if path.extension().is_some_and(|ext| ext == "json") {
            files.push(path);
        }
    }

    files.sort();
    Ok(files)
}

/// Process a single configuration file.
fn process_single_file(
    config_path: &Path,
    output_dir: Option<&Path>,
    verbose: bool,
) -> SimulationResult {
    let output_path = output_dir.map(|dir| {
        let stem = config_path
            .file_stem()
            .unwrap_or_default()
            .to_string_lossy();
        dir.join(format!("{}_result.json", stem))
    });

    match run_simulation(config_path, output_path.as_deref(), verbose) {
        Ok(result) => result,
        Err(e) => {
            let status = match e.exit_code() {
                crate::error::ExitCode::ConfigError => SimulationStatus::Error,
                crate::error::ExitCode::SimulationError => SimulationStatus::NonConverged,
                _ => SimulationStatus::Error,
            };

            SimulationResult {
                input: config_path.display().to_string(),
                status,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some(e.to_string()),
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 0,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            }
        }
    }
}

/// Build a batch summary from individual results.
fn build_summary(results: Vec<SimulationResult>, total_elapsed_ms: u64) -> BatchSummary {
    let total = results.len();
    let succeeded = results
        .iter()
        .filter(|r| r.status == SimulationStatus::Converged)
        .count();
    let failed = results
        .iter()
        .filter(|r| r.status == SimulationStatus::Error)
        .count();
    let non_converged = results
        .iter()
        .filter(|r| {
            r.status == SimulationStatus::NonConverged || r.status == SimulationStatus::Timeout
        })
        .count();

    let total_time: u64 = results.iter().map(|r| r.elapsed_ms).sum();
    let avg_time = if total > 0 {
        total_time as f64 / total as f64
    } else {
        0.0
    };

    BatchSummary {
        total,
        succeeded,
        failed,
        non_converged,
        total_elapsed_ms,
        avg_elapsed_ms: avg_time,
        results,
    }
}

/// Print a formatted batch summary.
fn print_batch_summary(summary: &BatchSummary) {
    use colored::Colorize;

    println!();
    println!("{}", "═".repeat(60).cyan());
    println!("{}", "  BATCH EXECUTION SUMMARY".cyan().bold());
    println!("{}", "═".repeat(60).cyan());
    println!();
    println!("  Total scenarios:   {}", summary.total);
    println!(
        "  {} {:>15}",
        "Succeeded:".green(),
        summary.succeeded.to_string().green()
    );
    println!(
        "  {} {:>15}",
        "Failed:".red(),
        summary.failed.to_string().red()
    );
    println!(
        "  {} {:>13}",
        "Non-converged:".yellow(),
        summary.non_converged.to_string().yellow()
    );
    println!();
    println!(
        "  Total time:        {:.2} s",
        summary.total_elapsed_ms as f64 / 1000.0
    );
    println!("  Avg time/scenario: {:.2} ms", summary.avg_elapsed_ms);
    println!("{}", "═".repeat(60).cyan());
}

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

    #[test]
    fn test_discover_config_files() {
        let dir = tempdir().unwrap();

        std::fs::write(dir.path().join("config1.json"), "{}").unwrap();
        std::fs::write(dir.path().join("config2.json"), "{}").unwrap();
        std::fs::write(dir.path().join("readme.txt"), "").unwrap();

        let files = discover_config_files(dir.path()).unwrap();
        assert_eq!(files.len(), 2);
        assert!(files[0].ends_with("config1.json"));
        assert!(files[1].ends_with("config2.json"));
    }

    #[test]
    fn test_build_summary() {
        let results = vec![
            SimulationResult {
                input: "test1.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(10),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 50,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "test2.json".to_string(),
                status: SimulationStatus::Error,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some("Error".to_string()),
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 0,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
        ];

        let summary = build_summary(results, 100);
        assert_eq!(summary.total, 2);
        assert_eq!(summary.succeeded, 1);
        assert_eq!(summary.failed, 1);
        assert_eq!(summary.total_elapsed_ms, 100);
        assert_eq!(summary.avg_elapsed_ms, 25.0);
    }

    #[test]
    fn test_batch_summary_serialization() {
        let summary = BatchSummary {
            total: 10,
            succeeded: 8,
            failed: 1,
            non_converged: 1,
            total_elapsed_ms: 1000,
            avg_elapsed_ms: 100.0,
            results: vec![],
        };

        let json = serde_json::to_string_pretty(&summary).unwrap();
        assert!(json.contains("\"total\": 10"));
        assert!(json.contains("\"succeeded\": 8"));
    }

    #[test]
    fn test_batch_aggregator_to_json() {
        let results = vec![
            SimulationResult {
                input: "test1.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(10),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 50,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "test2.json".to_string(),
                status: SimulationStatus::Error,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some("Error".to_string()),
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 0,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
        ];

        let aggregator = BatchAggregator::new(results);
        let json = aggregator.to_json().unwrap();
        assert!(json.contains("\"total\": 2"));
        assert!(json.contains("\"succeeded\": 1"));
        assert!(json.contains("\"failed\": 1"));
    }

    #[test]
    fn test_batch_aggregator_to_csv() {
        let results = vec![
            SimulationResult {
                input: "test1.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: Some(crate::run::ConvergenceInfo {
                    final_residual: 1e-8,
                    tolerance: 1e-6,
                    iterations: Some(10),
                    strategy: None,
                    iteration_history: vec![],
                }),
                iterations: Some(10),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 50,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "test2.json".to_string(),
                status: SimulationStatus::Error,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some("Error".to_string()),
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 0,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
        ];

        let aggregator = BatchAggregator::new(results);
        let csv = aggregator.to_csv();
        let lines: Vec<&str> = csv.lines().collect();
        assert_eq!(lines.len(), 3); // header + 2 data lines
        assert!(lines[0].contains("input,status,iterations"));
        assert!(lines[1].contains("test1.json"));
        assert!(lines[2].contains("test2.json"));
    }

    #[test]
    fn test_batch_aggregator_summary() {
        let results = vec![
            SimulationResult {
                input: "test1.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(10),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 50,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "test2.json".to_string(),
                status: SimulationStatus::Timeout,
                convergence: None,
                iterations: Some(100),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 1000,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
        ];

        let aggregator = BatchAggregator::new(results);
        let summary = aggregator.summary();
        assert_eq!(summary.total, 2);
        assert_eq!(summary.succeeded, 1);
        assert_eq!(summary.non_converged, 1);
    }

    #[test]
    fn test_output_format_from_str() {
        assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
        assert_eq!("JSON".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
        assert_eq!("csv".parse::<OutputFormat>().unwrap(), OutputFormat::Csv);
        assert_eq!("CSV".parse::<OutputFormat>().unwrap(), OutputFormat::Csv);
        assert!("xml".parse::<OutputFormat>().is_err());
        assert!("".parse::<OutputFormat>().is_err());
    }

    #[test]
    fn test_batch_summary_to_json() {
        let summary = BatchSummary {
            total: 10,
            succeeded: 8,
            failed: 1,
            non_converged: 1,
            total_elapsed_ms: 1000,
            avg_elapsed_ms: 100.0,
            results: vec![],
        };

        let json = summary.to_json().unwrap();
        assert!(json.contains("\"total\": 10"));
    }

    #[test]
    fn test_batch_summary_to_csv() {
        let summary = BatchSummary {
            total: 1,
            succeeded: 1,
            failed: 0,
            non_converged: 0,
            total_elapsed_ms: 100,
            avg_elapsed_ms: 100.0,
            results: vec![SimulationResult {
                input: "test.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(25),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 100,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            }],
        };

        let csv = summary.to_csv();
        assert!(csv.contains("input,status"));
        assert!(csv.contains("test.json"));
    }

    #[test]
    fn test_build_summary_converged_count() {
        let results = vec![
            SimulationResult {
                input: "ok1.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(10),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 50,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "ok2.json".to_string(),
                status: SimulationStatus::Converged,
                convergence: None,
                iterations: Some(15),
                state: None,
                performance: None,
                error: None,
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 60,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
            SimulationResult {
                input: "fail.json".to_string(),
                status: SimulationStatus::Error,
                convergence: None,
                iterations: None,
                state: None,
                performance: None,
                error: Some("fail".to_string()),
                failure_diagnostics: None,
                initialization_diagnostics: None,
                dof: None,
                elapsed_ms: 0,
                raw_state_vector: None,
                solved_variables: Vec::new(),
            },
        ];

        let summary = build_summary(results, 200);
        assert_eq!(summary.total, 3);
        assert_eq!(summary.succeeded, 2);
        assert_eq!(summary.failed, 1);
        assert_eq!(summary.non_converged, 0);
    }
}
