//! Entropyk CLI - Batch thermodynamic simulation tool.
//!
//! A command-line interface for running thermodynamic simulations
//! in single or batch mode.
//!
//! # Usage
//!
//! ```text
//! entropyk-cli run config.json -o result.json
//! entropyk-cli batch ./scenarios/ --parallel 4
//! ```

use std::path::{Path, PathBuf};

use clap::{Parser, Subcommand};
use colored::Colorize;
use tracing::Level;
use tracing_subscriber::EnvFilter;

use entropyk_cli::error::{CliError, ExitCode};
use entropyk_cli::seasonal::SeasonalMode;

#[derive(Parser)]
#[command(name = "entropyk-cli")]
#[command(author)]
#[command(version)]
#[command(about = "Batch thermodynamic simulation CLI", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    /// Enable verbose output
    #[arg(short, long, global = true)]
    verbose: bool,

    /// Suppress all output except errors
    #[arg(short, long, global = true, conflicts_with = "verbose")]
    quiet: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Run a single simulation from a configuration file
    Run {
        /// Path to the JSON configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,

        /// Path to write the JSON output (default: stdout)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },

    /// Run multiple simulations from a directory
    Batch {
        /// Directory containing JSON configuration files
        #[arg(short, long, value_name = "DIR")]
        directory: PathBuf,

        /// Directory to write output files
        #[arg(short, long, value_name = "DIR")]
        output_dir: Option<PathBuf>,

        /// Number of parallel workers
        #[arg(short, long, default_value = "4")]
        parallel: usize,

        /// Output file for consolidated results
        #[arg(short = 'O', long, value_name = "FILE")]
        output: Option<PathBuf>,

        /// Output format: json or csv
        #[arg(short = 'f', long, default_value = "json", value_name = "FORMAT")]
        format: String,
    },

    /// Validate a configuration file without running
    Validate {
        /// Path to the JSON configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,
    },

    /// Qualify a single heat exchanger at a fixed refrigerant regime while
    /// sweeping the secondary (air/water/brine) inlet temperature and flow.
    Qualify {
        /// Path to the JSON qualification configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,

        /// Path to write the JSON report (default: table to stdout only)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },
    /// Rate a full cycle across standardized part-load points and integrate the
    /// seasonal efficiency metric (IPLV per AHRI 550/590 or ESEER per Eurovent).
    Rate {
        /// Path to the JSON rating configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,

        /// Path to write the JSON report (default: table to stdout only)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },
    /// Compute the seasonal heating performance (SCOP) by the EN 14825 bin method,
    /// re-solving the coupled cycle at each climate temperature bin.
    Scop {
        /// Path to the JSON seasonal configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,

        /// Path to write the JSON report (default: table to stdout only)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },
    /// Compute the seasonal cooling performance (SEER) by the EN 14825 bin method,
    /// re-solving the coupled cycle at each climate temperature bin.
    Seer {
        /// Path to the JSON seasonal configuration file
        #[arg(short, long, value_name = "FILE")]
        config: PathBuf,

        /// Path to write the JSON report (default: table to stdout only)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },
    /// Emit the canonical Model IR JSON Schema (single source of truth for the
    /// CLI, the web UI, and external tooling). Derived from the Rust config types.
    Schema {
        /// Path to write the JSON Schema (default: stdout)
        #[arg(short, long, value_name = "FILE")]
        output: Option<PathBuf>,
    },
    /// Compile an Entropyk JSON model into an FMI 2.0 Co-Simulation `.fmu`
    /// archive for embedding on a PLC / controller. The model and its IO map are
    /// baked into the FMU binary (no filesystem access at runtime).
    ExportFmu {
        /// Path to the Entropyk model JSON (a `ScenarioConfig`).
        #[arg(short, long, value_name = "FILE", alias = "model")]
        config: PathBuf,

        /// Path to the FMU IO-map JSON (declares PLC inputs/outputs).
        #[arg(short, long, value_name = "FILE", alias = "io")]
        io: PathBuf,

        /// Rust target triple (e.g. `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`,
        /// `x86_64-pc-windows-msvc`). Use `host` for the current platform.
        #[arg(short, long, value_name = "TRIPLE", default_value = "host")]
        target: String,

        /// Path to write the `.fmu` archive.
        #[arg(short, long, value_name = "FILE")]
        output: PathBuf,
    },
}

fn main() {
    let cli = Cli::parse();

    let log_level = if cli.verbose {
        Level::DEBUG
    } else if cli.quiet {
        Level::ERROR
    } else {
        Level::INFO
    };

    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::builder()
                .with_default_directive(log_level.into())
                .from_env_lossy(),
        )
        .with_target(false)
        .init();

    let result = match cli.command {
        Commands::Run { config, output } => run_single(config, output, cli.verbose, cli.quiet),
        Commands::Batch {
            directory,
            output_dir,
            parallel,
            output,
            format,
        } => run_batch(
            directory,
            output_dir,
            parallel,
            output,
            format,
            cli.quiet,
            cli.verbose,
        ),
        Commands::Validate { config } => validate_config(config),
        Commands::Qualify { config, output } => run_qualify(config, output, cli.quiet),
        Commands::Rate { config, output } => run_rate(config, output, cli.quiet),
        Commands::Scop { config, output } => {
            run_seasonal(config, output, cli.quiet, SeasonalMode::Scop)
        }
        Commands::Seer { config, output } => {
            run_seasonal(config, output, cli.quiet, SeasonalMode::Seer)
        }
        Commands::Schema { output } => emit_schema(output),
        Commands::ExportFmu {
            config,
            io,
            target,
            output,
        } => run_export_fmu(&config, &io, &target, &output, cli.verbose),
    };

    if entropyk_fluids::profiling::enabled() {
        eprintln!("{}", entropyk_fluids::profiling::report());
    }

    match result {
        Ok(()) => std::process::exit(ExitCode::Success as i32),
        Err(e) => {
            if !cli.quiet {
                eprintln!("{} {}", "Error:".red(), e);
            }
            std::process::exit(e.exit_code() as i32);
        }
    }
}

fn run_single(
    config: PathBuf,
    output: Option<PathBuf>,
    verbose: bool,
    quiet: bool,
) -> Result<(), CliError> {
    use entropyk_cli::run::run_simulation;

    if !quiet {
        println!("{}", "═".repeat(60).cyan());
        println!("{}", "  ENTROPYK CLI - Single Simulation".cyan().bold());
        println!("{}", "═".repeat(60).cyan());
        println!();
    }

    let result = run_simulation(&config, output.as_deref(), verbose)?;

    if !quiet {
        print_result(&result);
    } else if output.is_none() {
        let json = serde_json::to_string(&result)
            .map_err(|e| CliError::Simulation(format!("Failed to serialize result: {}", e)))?;
        println!("{}", json);
    }

    match result.status {
        entropyk_cli::run::SimulationStatus::Converged => Ok(()),
        entropyk_cli::run::SimulationStatus::Timeout
        | entropyk_cli::run::SimulationStatus::NonConverged => Err(CliError::Simulation(
            "Simulation did not converge".to_string(),
        )),
        entropyk_cli::run::SimulationStatus::Error => Err(CliError::Simulation(
            result.error.unwrap_or_else(|| "Unknown error".to_string()),
        )),
    }
}

fn print_result(result: &entropyk_cli::run::SimulationResult) {
    use colored::Colorize;

    println!("{}", "─".repeat(40).white());
    println!("  Input:      {}", result.input);

    let status_str = match result.status {
        entropyk_cli::run::SimulationStatus::Converged => "CONVERGED".green(),
        entropyk_cli::run::SimulationStatus::Timeout => "TIMEOUT".yellow(),
        entropyk_cli::run::SimulationStatus::NonConverged => "NON-CONVERGED".yellow(),
        entropyk_cli::run::SimulationStatus::Error => "ERROR".red(),
    };
    println!("  Status:     {}", status_str);

    if let Some(ref conv) = result.convergence {
        println!("  Residual:   {:.2e}", conv.final_residual);
    }

    if let Some(iters) = result.iterations {
        println!("  Iterations: {}", iters);
    }

    println!("  Time:       {} ms", result.elapsed_ms);

    if let Some(ref dof) = result.dof {
        let tag = if dof.n_equations == dof.n_unknowns {
            format!("{} = {}", dof.n_equations, dof.n_unknowns).green()
        } else {
            format!("{} ≠ {}", dof.n_equations, dof.n_unknowns).red()
        };
        println!(
            "  DoF:        {} eqs / {} unk  [{}]",
            dof.n_equations, dof.n_unknowns, tag
        );
        println!("              balance: {}", dof.balance);
    }

    if let Some(ref error) = result.error {
        println!();
        println!("  {} {}", "Error:".red(), error);
    }

    if let Some(ref state) = result.state {
        println!();
        println!("  {}", "Edge States:".cyan());
        for entry in state {
            println!(
                "    Edge {}: P = {:.3} bar, h = {:.2} kJ/kg",
                entry.edge, entry.pressure_bar, entry.enthalpy_kj_kg
            );
        }
    }

    if let Some(ref perf) = result.performance {
        println!();
        println!("  {}", "Performance:".cyan());
        if let Some(q) = perf.q_cooling_kw {
            println!("    Cooling capacity: {:.3} kW", q);
        }
        if let Some(q) = perf.q_heating_kw {
            println!("    Heating capacity: {:.3} kW", q);
        }
        if let Some(w) = perf.compressor_power_kw {
            println!("    Power input:      {:.3} kW", w);
        }
        if let Some(cop) = perf.cop {
            println!("    COP / EER:        {:.3}", cop);
        }
    }

    println!("{}", "─".repeat(40).white());
}

fn run_batch(
    directory: PathBuf,
    output_dir: Option<PathBuf>,
    parallel: usize,
    output: Option<PathBuf>,
    format: String,
    quiet: bool,
    verbose: bool,
) -> Result<(), CliError> {
    use entropyk_cli::batch::{run_batch, BatchAggregator, OutputFormat};

    if !quiet {
        println!("{}", "═".repeat(60).cyan());
        println!("{}", "  ENTROPYK CLI - Batch Execution".cyan().bold());
        println!("{}", "═".repeat(60).cyan());
        println!();
    }

    let summary = run_batch(&directory, parallel, output_dir.as_deref(), quiet, verbose)?;

    // Write consolidated output if requested
    if let Some(output_path) = output {
        let output_format: OutputFormat =
            format.parse().map_err(|e: String| CliError::Config(e))?;
        let aggregator = BatchAggregator::new(summary.results.clone());

        let content = match output_format {
            OutputFormat::Json => aggregator.to_json()?,
            OutputFormat::Csv => aggregator.to_csv(),
        };

        std::fs::write(&output_path, content)?;
        if !quiet {
            println!(
                "  Results written to: {} ({:?})",
                output_path.display(),
                output_format
            );
        }
    }

    if summary.failed > 0 || summary.non_converged > 0 {
        Err(CliError::Simulation(format!(
            "{} simulations failed, {} non-converged",
            summary.failed, summary.non_converged
        )))
    } else {
        Ok(())
    }
}

fn run_qualify(config: PathBuf, output: Option<PathBuf>, quiet: bool) -> Result<(), CliError> {
    use entropyk_cli::qualify::run_qualify as run_qualify_impl;

    if !quiet {
        println!("{}", "═".repeat(60).cyan());
        println!(
            "{}",
            "  ENTROPYK CLI - Heat Exchanger Qualification"
                .cyan()
                .bold()
        );
        println!("{}", "═".repeat(60).cyan());
        println!();
    }

    let report = run_qualify_impl(&config, output.as_deref())?;

    if quiet {
        if output.is_none() {
            let json = serde_json::to_string(&report)
                .map_err(|e| CliError::Simulation(format!("Failed to serialize report: {}", e)))?;
            println!("{}", json);
        }
        return Ok(());
    }

    let kind_label = match report.exchanger.as_str() {
        "evaporator" => "Evaporator (refrigerant absorbs heat)",
        "condenser" => "Condenser (refrigerant rejects heat)",
        other => other,
    };
    println!("  Exchanger:    {}", kind_label.bold());
    println!("  Refrigerant:  {}", report.refrigerant);
    println!("  UA:           {:.0} W/K", report.ua_w_per_k);
    println!(
        "  Regime:       {:.3} bar  (T_sat = {:.2} °C, held constant)",
        report.refrigerant_pressure_bar, report.refrigerant_sat_temp_c
    );
    println!();
    println!(
        "  {:>8}  {:>10}  {:>12}  {:>8}  {:>10}  {:>10}",
        "T_in[°C]", "C[W/K]", "Q[kW]", "ε[-]", "appr[K]", "T_out[°C]"
    );
    println!("  {}", "─".repeat(66).white());
    for row in &report.rows {
        println!(
            "  {:>8.2}  {:>10.0}  {:>12.3}  {:>8.3}  {:>10.2}  {:>10.2}",
            row.secondary_inlet_temp_c,
            row.secondary_capacity_rate_w_per_k,
            row.q_w / 1000.0,
            row.effectiveness,
            row.approach_k,
            row.secondary_outlet_temp_c,
        );
    }
    println!("  {}", "─".repeat(66).white());

    if let Some(out) = output {
        println!();
        println!("  Report written to: {}", out.display());
    }

    Ok(())
}

fn run_rate(config: PathBuf, output: Option<PathBuf>, quiet: bool) -> Result<(), CliError> {
    use entropyk_cli::rate::run_rate as run_rate_impl;

    if !quiet {
        println!("{}", "═".repeat(60).cyan());
        println!(
            "{}",
            "  ENTROPYK CLI - Seasonal Rating (IPLV / ESEER)"
                .cyan()
                .bold()
        );
        println!("{}", "═".repeat(60).cyan());
        println!();
    }

    let report = run_rate_impl(&config, output.as_deref())?;

    if quiet {
        if output.is_none() {
            let json = serde_json::to_string(&report)
                .map_err(|e| CliError::Simulation(format!("Failed to serialize report: {}", e)))?;
            println!("{}", json);
        }
        return Ok(());
    }

    println!("  Base config:  {}", report.base_config);
    println!("  Standard:     {}", report.metric.bold());
    if !report.standard_reference.is_empty() {
        println!("  Reference:    {}", report.standard_reference);
    }
    println!();
    println!(
        "  {:>8}  {:>12}  {:>10}  {:>10}  {:>10}",
        "Load[%]", "Status", "Q_cool[kW]", "Power[kW]", "EER[-]"
    );
    println!("  {}", "─".repeat(58).white());
    for row in &report.points {
        let eer = row
            .eer
            .map(|v| format!("{:.3}", v))
            .unwrap_or_else(|| "—".to_string());
        let q = row
            .q_cooling_kw
            .map(|v| format!("{:.3}", v))
            .unwrap_or_else(|| "—".to_string());
        let p = row
            .power_kw
            .map(|v| format!("{:.3}", v))
            .unwrap_or_else(|| "—".to_string());
        println!(
            "  {:>8.0}  {:>12}  {:>10}  {:>10}  {:>10}",
            row.load_fraction * 100.0,
            row.status,
            q,
            p,
            eer,
        );
    }
    println!("  {}", "─".repeat(58).white());
    println!();
    match report.integrated_value {
        Some(v) => println!(
            "  {} = {}",
            report.metric.bold(),
            format!("{:.3}", v).green().bold()
        ),
        None => println!(
            "  {}",
            "Integrated value unavailable (a part-load point did not converge)".yellow()
        ),
    }

    if let Some(out) = output {
        println!();
        println!("  Report written to: {}", out.display());
    }

    Ok(())
}

fn run_seasonal(
    config: PathBuf,
    output: Option<PathBuf>,
    quiet: bool,
    mode: SeasonalMode,
) -> Result<(), CliError> {
    use entropyk_cli::seasonal::run_seasonal as run_seasonal_impl;

    let title = match mode {
        SeasonalMode::Scop => "  ENTROPYK CLI - Seasonal Heating (SCOP, EN 14825)",
        SeasonalMode::Seer => "  ENTROPYK CLI - Seasonal Cooling (SEER, EN 14825)",
    };
    if !quiet {
        println!("{}", "═".repeat(60).cyan());
        println!("{}", title.cyan().bold());
        println!("{}", "═".repeat(60).cyan());
        println!();
    }

    let report = run_seasonal_impl(&config, output.as_deref(), mode)?;

    if quiet {
        if output.is_none() {
            let json = serde_json::to_string(&report)
                .map_err(|e| CliError::Simulation(format!("Failed to serialize report: {}", e)))?;
            println!("{}", json);
        }
        return Ok(());
    }

    println!("  Base config:  {}", report.base_config);
    println!("  Climate:      {}", report.climate.bold());
    if !report.climate_reference.is_empty() {
        println!("  Reference:    {}", report.climate_reference);
    }
    println!("  Total hours:  {:.0} h", report.total_hours);
    println!();
    println!(
        "  {:>8}  {:>7}  {:>11}  {:>11}  {:>8}  {:>7}  {:>8}",
        "T[°C]", "h", "Demand[kW]", "Cap[kW]", "COP_full", "backup", "COP_eff"
    );
    println!("  {}", "─".repeat(72).white());
    for b in &report.bins {
        let fmt = |v: Option<f64>, p: usize| {
            v.map(|x| format!("{:.*}", p, x))
                .unwrap_or_else(|| "—".to_string())
        };
        println!(
            "  {:>8.1}  {:>7.0}  {:>11.3}  {:>11}  {:>8}  {:>6.0}%  {:>8}",
            b.temperature_c,
            b.hours,
            b.demand_w / 1000.0,
            fmt(b.capacity_w.map(|w| w / 1000.0), 3),
            fmt(b.full_cop, 3),
            b.backup_fraction * 100.0,
            fmt(b.effective_cop, 3),
        );
    }
    println!("  {}", "─".repeat(72).white());
    println!();
    println!(
        "  Seasonal useful:   {:.1} kWh    Seasonal electric: {:.1} kWh",
        report.seasonal_useful_kwh, report.seasonal_electric_kwh
    );
    match report.integrated_value {
        Some(v) => println!(
            "  {} = {}",
            report.metric.bold(),
            format!("{:.3}", v).green().bold()
        ),
        None => println!(
            "  {}",
            "Seasonal value unavailable (a demanded bin did not converge)".yellow()
        ),
    }

    if let Some(out) = output {
        println!();
        println!("  Report written to: {}", out.display());
    }

    Ok(())
}

fn validate_config(config: PathBuf) -> Result<(), CliError> {
    use entropyk_cli::config::ScenarioConfig;
    use entropyk_cli::run::run_simulation;

    println!("{}", "═".repeat(60).cyan());
    println!(
        "{}",
        "  ENTROPYK CLI - Configuration Validation".cyan().bold()
    );
    println!("{}", "═".repeat(60).cyan());
    println!();

    let _cfg = ScenarioConfig::from_file(&config)?;
    println!("  {} Schema / file parse OK", "✓".green());
    println!("  File: {}", config.display());

    // Build + finalize the system (and attempt solve) to surface DoF imbalance.
    // Real-machine configs must be square: n_equations == n_unknowns.
    println!();
    println!("  {}", "DoF / topology check (build + solve):".cyan());
    let result = run_simulation(&config, None, false)?;
    if let Some(ref dof) = result.dof {
        let ok = dof.n_equations == dof.n_unknowns;
        if ok {
            println!(
                "  {} DoF balanced: {} equations = {} unknowns",
                "✓".green(),
                dof.n_equations,
                dof.n_unknowns
            );
        } else {
            println!(
                "  {} DoF imbalance: {} equations vs {} unknowns ({})",
                "✗".red(),
                dof.n_equations,
                dof.n_unknowns,
                dof.balance
            );
            println!();
            for line in dof.summary.lines() {
                println!("    {}", line);
            }
            return Err(CliError::Simulation(format!(
                "DoF imbalance: {} eqs vs {} unknowns",
                dof.n_equations, dof.n_unknowns
            )));
        }
        // Print compact component ledger
        for line in dof.summary.lines().skip(1).take(24) {
            println!("    {}", line);
        }
    }
    match result.status {
        entropyk_cli::run::SimulationStatus::Converged => {
            println!("  {} Solver converged (config is executable)", "✓".green());
        }
        entropyk_cli::run::SimulationStatus::Error => {
            if let Some(ref err) = result.error {
                if err.contains("DoF imbalance") {
                    return Err(CliError::Simulation(err.clone()));
                }
                println!(
                    "  {} Schema OK but solve failed: {}",
                    "!".yellow(),
                    err.lines().next().unwrap_or(err)
                );
            }
        }
        _ => {
            println!(
                "  {} Schema/DoF OK but solver did not converge",
                "!".yellow()
            );
        }
    }

    Ok(())
}

/// Emit the canonical Model IR JSON Schema, derived from the Rust config types.
fn emit_schema(output: Option<PathBuf>) -> Result<(), CliError> {
    use entropyk_cli::config::ScenarioConfig;

    let schema = ScenarioConfig::json_schema();

    match output {
        Some(path) => {
            std::fs::write(&path, &schema).map_err(CliError::Io)?;
            eprintln!("{} Schema written to {}", "✓".green(), path.display());
        }
        None => println!("{}", schema),
    }

    Ok(())
}

/// `export-fmu` subcommand: compile an Entropyk JSON model into an FMI 2.0
/// Co-Simulation `.fmu` archive.
fn run_export_fmu(
    config: &Path,
    io: &Path,
    target: &str,
    output: &Path,
    verbose: bool,
) -> Result<(), CliError> {
    use entropyk_cli::export_fmu::run_export_fmu as run;

    println!("{}", "═".repeat(60).cyan());
    println!(
        "{}",
        "  ENTROPYK CLI - Export FMU (FMI 2.0 Co-Simulation)"
            .cyan()
            .bold()
    );
    println!("{}", "═".repeat(60).cyan());
    println!();

    run(config, io, target, output, verbose)
}
