//! Heat-exchanger qualification harness.
//!
//! Qualifies a single heat exchanger (evaporator or condenser) at a **fixed
//! refrigerant regime** (constant evaporating/condensing pressure) while sweeping
//! the secondary (air/water/brine) inlet temperature and flow. Every output is
//! solved from the genuine ε-NTU coupled model — nothing is imposed.
//!
//! This is the entry point that lets the user "calculer les performances des
//! machines pour différentes conditions d'air et eau" without running a full
//! cycle: the refrigerant side is held constant, the secondary side varies, and
//! the resulting duty / effectiveness / approach respond physically.
//!
//! # Example configuration
//!
//! ```json
//! {
//!   "exchanger": "evaporator",
//!   "refrigerant": "R134a",
//!   "fluid_backend": "Test",
//!   "ua_w_per_k": 8000.0,
//!   "refrigerant_pressure_bar": 3.5,
//!   "sweep": {
//!     "secondary_inlet_temp_c": [10.0, 12.0, 15.0],
//!     "secondary_mass_flow_kg_s": [1.0, 1.5, 2.0],
//!     "secondary_cp_j_per_kgk": 4180.0
//!   }
//! }
//! ```

use std::path::Path;
use std::sync::Arc;

use serde::{Deserialize, Serialize};

use entropyk_components::heat_exchanger::{Condenser, FloodedEvaporator};

use crate::error::{CliError, CliResult};

/// Which exchanger to qualify.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExchangerKind {
    Evaporator,
    Condenser,
}

impl ExchangerKind {
    fn parse(s: &str) -> CliResult<Self> {
        match s.trim().to_ascii_lowercase().as_str() {
            "evaporator" | "flooded_evaporator" | "evap" => Ok(ExchangerKind::Evaporator),
            "condenser" | "cond" => Ok(ExchangerKind::Condenser),
            other => Err(CliError::Config(format!(
                "Unknown exchanger '{}'. Supported: 'evaporator', 'condenser'",
                other
            ))),
        }
    }
}

/// Qualification configuration (JSON).
#[derive(Debug, Clone, Deserialize)]
pub struct QualifyConfig {
    /// Exchanger to qualify: "evaporator" or "condenser".
    pub exchanger: String,
    /// Refrigerant fluid id (e.g. "R134a", "R410A").
    pub refrigerant: String,
    /// Fluid backend: "Test" (default) or "CoolProp".
    #[serde(default)]
    pub fluid_backend: Option<String>,
    /// Overall conductance UA [W/K].
    pub ua_w_per_k: f64,
    /// Fixed refrigerant regime: evaporating/condensing pressure [bar].
    pub refrigerant_pressure_bar: f64,
    /// Secondary-stream sweep definition.
    pub sweep: SweepConfig,
}

/// Secondary-stream sweep: cartesian product of inlet temperatures × capacity rates.
#[derive(Debug, Clone, Deserialize)]
pub struct SweepConfig {
    /// Secondary inlet temperatures to sweep [°C].
    pub secondary_inlet_temp_c: Vec<f64>,
    /// Secondary heat-capacity rates ṁ·cp to sweep [W/K]. Mutually exclusive with
    /// `secondary_mass_flow_kg_s` + `secondary_cp_j_per_kgk`.
    #[serde(default)]
    pub secondary_capacity_rate_w_per_k: Vec<f64>,
    /// Secondary mass flows to sweep [kg/s] (combined with `secondary_cp_j_per_kgk`).
    #[serde(default)]
    pub secondary_mass_flow_kg_s: Vec<f64>,
    /// Secondary specific heat [J/(kg·K)] used with `secondary_mass_flow_kg_s`.
    #[serde(default)]
    pub secondary_cp_j_per_kgk: Option<f64>,
}

/// A single qualification result row.
#[derive(Debug, Clone, Serialize)]
pub struct QualifyRow {
    /// Secondary inlet temperature [°C].
    pub secondary_inlet_temp_c: f64,
    /// Secondary heat-capacity rate ṁ·cp [W/K].
    pub secondary_capacity_rate_w_per_k: f64,
    /// Heat duty [W] (>0 absorbed by evaporator, >0 rejected by condenser).
    pub q_w: f64,
    /// Effectiveness ε = 1 − exp(−UA/C_sec) [-].
    pub effectiveness: f64,
    /// Refrigerant saturation temperature [°C] (constant across the sweep).
    pub refrigerant_sat_temp_c: f64,
    /// Approach |T_sat − T_sec,in| [K].
    pub approach_k: f64,
    /// Secondary outlet temperature [°C].
    pub secondary_outlet_temp_c: f64,
}

/// Full qualification report.
#[derive(Debug, Clone, Serialize)]
pub struct QualifyReport {
    /// Exchanger kind ("evaporator" / "condenser").
    pub exchanger: String,
    /// Refrigerant fluid id.
    pub refrigerant: String,
    /// Conductance UA [W/K].
    pub ua_w_per_k: f64,
    /// Fixed refrigerant pressure [bar].
    pub refrigerant_pressure_bar: f64,
    /// Refrigerant saturation temperature [°C] at the fixed regime.
    pub refrigerant_sat_temp_c: f64,
    /// One row per swept secondary condition.
    pub rows: Vec<QualifyRow>,
}

fn build_backend(name: Option<&str>) -> CliResult<Arc<dyn entropyk_fluids::FluidBackend>> {
    match name {
        Some("CoolProp") => Ok(Arc::new(entropyk_fluids::CoolPropBackend::new())),
        Some("Test") | None => Ok(Arc::new(entropyk_fluids::TestBackend::new())),
        Some(other) => Err(CliError::Config(format!(
            "Unknown fluid backend: '{}'. Supported: 'CoolProp', 'Test'",
            other
        ))),
    }
}

/// Resolves the list of secondary capacity rates [W/K] from the sweep config.
fn capacity_rates(sweep: &SweepConfig) -> CliResult<Vec<f64>> {
    if !sweep.secondary_capacity_rate_w_per_k.is_empty() {
        return Ok(sweep.secondary_capacity_rate_w_per_k.clone());
    }
    if !sweep.secondary_mass_flow_kg_s.is_empty() {
        let cp = sweep.secondary_cp_j_per_kgk.ok_or_else(|| {
            CliError::Config(
                "sweep.secondary_cp_j_per_kgk is required when using secondary_mass_flow_kg_s"
                    .to_string(),
            )
        })?;
        return Ok(sweep
            .secondary_mass_flow_kg_s
            .iter()
            .map(|m| m * cp)
            .collect());
    }
    Err(CliError::Config(
        "sweep must define either 'secondary_capacity_rate_w_per_k' or 'secondary_mass_flow_kg_s'"
            .to_string(),
    ))
}

/// Runs the qualification sweep and returns a structured report.
pub fn qualify(config: &QualifyConfig) -> CliResult<QualifyReport> {
    let kind = ExchangerKind::parse(&config.exchanger)?;
    let backend = build_backend(config.fluid_backend.as_deref())?;
    let p_pa = config.refrigerant_pressure_bar * 1e5;

    if config.sweep.secondary_inlet_temp_c.is_empty() {
        return Err(CliError::Config(
            "sweep.secondary_inlet_temp_c must contain at least one temperature".to_string(),
        ));
    }
    let rates = capacity_rates(&config.sweep)?;

    let mut rows = Vec::new();
    let mut sat_temp_c = f64::NAN;

    for &t_c in &config.sweep.secondary_inlet_temp_c {
        let t_k = t_c + 273.15;
        for &c_sec in &rates {
            let row = match kind {
                ExchangerKind::Evaporator => {
                    let evap = FloodedEvaporator::new(config.ua_w_per_k)
                        .with_refrigerant(&config.refrigerant)
                        .with_fluid_backend(Arc::clone(&backend))
                        .with_secondary_stream(t_k, c_sec);
                    let r = evap.rate(p_pa).map_err(|e| {
                        CliError::Simulation(format!("Evaporator rating failed: {}", e))
                    })?;
                    sat_temp_c = r.t_evap_k - 273.15;
                    QualifyRow {
                        secondary_inlet_temp_c: t_c,
                        secondary_capacity_rate_w_per_k: c_sec,
                        q_w: r.q_w,
                        effectiveness: r.effectiveness,
                        refrigerant_sat_temp_c: r.t_evap_k - 273.15,
                        approach_k: r.approach_k.abs(),
                        secondary_outlet_temp_c: r.secondary_outlet_k - 273.15,
                    }
                }
                ExchangerKind::Condenser => {
                    let cond = Condenser::new(config.ua_w_per_k)
                        .with_refrigerant(&config.refrigerant)
                        .with_fluid_backend(Arc::clone(&backend))
                        .with_secondary_stream(t_k, c_sec);
                    let r = cond.rate(p_pa).map_err(|e| {
                        CliError::Simulation(format!("Condenser rating failed: {}", e))
                    })?;
                    sat_temp_c = r.t_cond_k - 273.15;
                    QualifyRow {
                        secondary_inlet_temp_c: t_c,
                        secondary_capacity_rate_w_per_k: c_sec,
                        q_w: r.q_w,
                        effectiveness: r.effectiveness,
                        refrigerant_sat_temp_c: r.t_cond_k - 273.15,
                        approach_k: r.approach_k.abs(),
                        secondary_outlet_temp_c: r.secondary_outlet_k - 273.15,
                    }
                }
            };
            rows.push(row);
        }
    }

    Ok(QualifyReport {
        exchanger: match kind {
            ExchangerKind::Evaporator => "evaporator".to_string(),
            ExchangerKind::Condenser => "condenser".to_string(),
        },
        refrigerant: config.refrigerant.clone(),
        ua_w_per_k: config.ua_w_per_k,
        refrigerant_pressure_bar: config.refrigerant_pressure_bar,
        refrigerant_sat_temp_c: sat_temp_c,
        rows,
    })
}

/// Loads a qualification config from a JSON file, runs the sweep, and optionally
/// writes the JSON report. Returns the report for further inspection.
pub fn run_qualify(config_path: &Path, output: Option<&Path>) -> CliResult<QualifyReport> {
    let raw = std::fs::read_to_string(config_path).map_err(|e| {
        CliError::Config(format!("Failed to read {}: {}", config_path.display(), e))
    })?;
    let config: QualifyConfig = serde_json::from_str(&raw)
        .map_err(|e| CliError::Config(format!("Invalid qualify config: {}", e)))?;

    let report = qualify(&config)?;

    if let Some(out) = output {
        let json = serde_json::to_string_pretty(&report)
            .map_err(|e| CliError::Simulation(format!("Failed to serialize report: {}", e)))?;
        std::fs::write(out, json)
            .map_err(|e| CliError::Config(format!("Failed to write {}: {}", out.display(), e)))?;
    }

    Ok(report)
}

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

    fn base_config() -> QualifyConfig {
        QualifyConfig {
            exchanger: "evaporator".to_string(),
            refrigerant: "R134a".to_string(),
            fluid_backend: Some("Test".to_string()),
            ua_w_per_k: 8000.0,
            refrigerant_pressure_bar: 3.5,
            sweep: SweepConfig {
                secondary_inlet_temp_c: vec![10.0, 15.0],
                secondary_capacity_rate_w_per_k: vec![],
                secondary_mass_flow_kg_s: vec![1.0, 2.0],
                secondary_cp_j_per_kgk: Some(4180.0),
            },
        }
    }

    #[test]
    fn test_evaporator_sweep_produces_cartesian_grid() {
        let cfg = base_config();
        let report = qualify(&cfg).unwrap();
        // 2 temperatures × 2 flows = 4 rows
        assert_eq!(report.rows.len(), 4);
        // Fixed refrigerant regime ⇒ constant saturation temperature.
        for row in &report.rows {
            assert!((row.refrigerant_sat_temp_c - report.refrigerant_sat_temp_c).abs() < 1e-9);
            assert!(row.q_w > 0.0, "evaporator must absorb heat from warm water");
            assert!(row.effectiveness > 0.0 && row.effectiveness < 1.0);
        }
    }

    #[test]
    fn test_warmer_water_increases_evaporator_duty() {
        let cfg = base_config();
        let report = qualify(&cfg).unwrap();
        // Rows are ordered temp-major; compare same flow, two temperatures.
        let cold = &report.rows[0]; // 10 °C, flow 1.0
        let warm = &report.rows[2]; // 15 °C, flow 1.0
        assert!(
            (cold.secondary_capacity_rate_w_per_k - warm.secondary_capacity_rate_w_per_k).abs()
                < 1e-9
        );
        assert!(
            warm.q_w > cold.q_w,
            "warmer secondary ⇒ more evaporator duty"
        );
    }

    #[test]
    fn test_condenser_sweep_rejects_heat() {
        let mut cfg = base_config();
        cfg.exchanger = "condenser".to_string();
        cfg.refrigerant_pressure_bar = 12.0; // ~46 °C condensing for R134a
        let report = qualify(&cfg).unwrap();
        assert_eq!(report.rows.len(), 4);
        for row in &report.rows {
            assert!(row.q_w > 0.0, "condenser must reject heat to cooler water");
            assert!(row.approach_k > 0.0);
        }
    }

    #[test]
    fn test_missing_secondary_definition_errors() {
        let mut cfg = base_config();
        cfg.sweep.secondary_mass_flow_kg_s = vec![];
        cfg.sweep.secondary_capacity_rate_w_per_k = vec![];
        assert!(qualify(&cfg).is_err());
    }
}
