//! Microchannel Heat Exchanger (MCHX) Condenser Coil
//!
//! Models a multi-pass microchannel condenser coil used in air-cooled chillers,
//! as an alternative to round-tube plate-fin (RTPF) condensers.
//!
//! ## MCHX vs. Conventional Coil
//!
//! A MCHX (Microchannel Heat Exchanger) uses flat multi-port extruded aluminium
//! tubes with a louvered fin structure. Compared to round-tube/plate-fin (RTPF):
//!
//! | Property            | RTPF                  | MCHX                  |
//! |---------------------|-----------------------|-----------------------|
//! | Air-side UA         | Base                  | +30–60% per unit area |
//! | Refrigerant charge  | Base                  | −25–40%               |
//! | Air pressure drop   | Base                  | Similar               |
//! | Weight              | Base                  | −30%                  |
//! | Air mal-distribution| Less sensitive        | More sensitive        |
//!
//! ## Model
//!
//! The model extends `CondenserCoil` (LMTD, UA-based) with:
//!
//! 1. **UA correction for air velocity** (`UA_eff = UA_nominal × f_air(G_air)`)
//!    based on ASHRAE Handbook correlations for louvered fins.
//!
//! 2. **UA correction for ambient temperature** (density effect on fan curves)
//!
//! 3. **Multi-pass arrangement** tracking: each coil (of 4 total) can have an
//!    independent fan group and air-side conditions.
//!
//! ### UA correction function
//!
//! ```text
//! UA_eff = UA_nominal × (G_air / G_ref)^n_air
//!
//! where:
//!   G_air   = air mass velocity [kg/(m²·s)] = ṁ_air / A_frontal
//!   G_ref   = reference air mass velocity (at design point)
//!   n_air   = 0.4–0.6 (typical for louvered fins, ASHRAE HoF 4.21)
//! ```
//!
//! When a fan speed ratio is provided, air velocity scales as:
//! ```text
//! G_air = G_ref × fan_speed_ratio
//! UA_eff = UA_nominal × fan_speed_ratio^n_air
//! ```
//!
//! ## Usage
//!
//! ```rust,ignore
//! use entropyk_components::heat_exchanger::MchxCondenserCoil;
//!
//! // 4 coils, each with UA_nominal = 15 kW/K
//! for i in 0..4 {
//!     let coil = MchxCondenserCoil::new(
//!         15_000.0,   // UA nominal [W/K]
//!         0.6,        // air-side exponent n_air (louvered fin ASHRAE)
//!         i,          // coil index (0–3)
//!     );
//!     // Set air conditions from fan
//!     coil.set_air_conditions(35.0 + 273.15, 1.12, fan_speed_ratio);
//! }
//! ```

use super::condenser::Condenser;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};

/// Minimum fan speed ratio below which the coil is considered inactive.
const MIN_ACTIVE_SPEED_RATIO: f64 = 0.05;

/// MCHX Condenser Coil with variable UA based on fan speed and air conditions.
///
/// Builds on `Condenser` (LMTD) and adds:
/// - Air-side UA correction via louvered-fin correlation
/// - Fan speed ratio tracking per coil (for anti-override control)
/// - Ambient temperature input (air density correction)
/// - Coil index for identification in the 4-coil bank
#[derive(Debug)]
pub struct MchxCondenserCoil {
    /// Inner condenser with LMTD model
    inner: Condenser,
    /// Nominal UA at design-point air conditions [W/K]
    ua_nominal: f64,
    /// Air-side heat transfer exponent for UA correction (0.4–0.6 typical)
    n_air: f64,
    /// Current fan speed ratio (0.0 = stopped, 1.0 = full speed)
    fan_speed_ratio: f64,
    /// Ambient air temperature [K]
    t_air_k: f64,
    /// Air density at ambient conditions [kg/m³]
    rho_air: f64,
    /// Coil index (0–3 for a 4-coil bank)
    coil_index: usize,
    /// Flag: coil is validated for Air fluid on cold side
    air_validated: std::sync::atomic::AtomicBool,
}

impl MchxCondenserCoil {
    /// Creates a new MCHX condenser coil.
    ///
    /// # Arguments
    ///
    /// * `ua_nominal`       — Design-point UA [W/K] (refrigerant-to-air)
    /// * `n_air`            — Air-side exponent for UA vs. velocity: UA ∝ G^n_air
    ///                        Typical range 0.4–0.6 for louvered-fin MCHX.
    /// * `coil_index`       — Coil number in the bank (0-based, 0–3 for 4 coils)
    ///
    /// # Design Point Reference
    ///
    /// The design point is fan_speed_ratio = 1.0, T_air = 35°C (308.15 K).
    /// UA correction is relative to this point.
    pub fn new(ua_nominal: f64, n_air: f64, coil_index: usize) -> Self {
        assert!(ua_nominal >= 0.0, "UA must be non-negative");
        assert!((0.0..=1.5).contains(&n_air), "n_air must be in [0, 1.5]");

        Self {
            inner: Condenser::new(ua_nominal),
            ua_nominal,
            n_air,
            fan_speed_ratio: 1.0,
            t_air_k: 35.0 + 273.15, // default 35°C
            rho_air: 1.12,          // kg/m³ at 35°C, sea level
            coil_index,
            air_validated: std::sync::atomic::AtomicBool::new(false),
        }
    }

    /// Creates a coil with specific design parameters for a 35°C ambient system.
    ///
    /// Convenience constructor matching the chiller spec (35°C air, 4 coils).
    ///
    /// # Arguments
    ///
    /// * `ua_per_coil`  — UA per coil at design point [W/K]
    /// * `coil_index`   — Coil index (0–3)
    pub fn for_35c_ambient(ua_per_coil: f64, coil_index: usize) -> Self {
        // n_air = 0.5 is the typical ASHRAE recommendation for louvered-fin MCHX
        let mut coil = Self::new(ua_per_coil, 0.5, coil_index);
        coil.t_air_k = 35.0 + 273.15;
        coil.rho_air = air_density_kg_m3(35.0 + 273.15, 101_325.0);
        coil
    }

    // ─── Setters ─────────────────────────────────────────────────────────────

    /// Sets the fan speed ratio for this coil (0.0–1.0).
    ///
    /// This updates the effective UA through the UA-velocity correlation.
    /// Anti-override control adjusts this value to prevent condensing pressure
    /// from rising above safe limits in high-ambient conditions.
    pub fn set_fan_speed_ratio(&mut self, ratio: f64) {
        self.fan_speed_ratio = ratio.clamp(0.0, 1.0);
        self.update_ua_effective();
    }

    /// Sets ambient air temperature [°C] and updates UA correction.
    pub fn set_air_temperature_celsius(&mut self, t_celsius: f64) {
        self.t_air_k = t_celsius + 273.15;
        // Update air density (ideal gas approximation)
        self.rho_air = air_density_kg_m3(self.t_air_k, 101_325.0);
        self.update_ua_effective();
    }

    /// Sets air conditions explicitly (temperature, density, fan speed).
    ///
    /// # Arguments
    ///
    /// * `t_air_k`          — Air temperature [K]
    /// * `rho_air_kg_m3`    — Air density [kg/m³]
    /// * `fan_speed_ratio`  — Fan speed ratio (0.0–1.0)
    pub fn set_air_conditions(&mut self, t_air_k: f64, rho_air_kg_m3: f64, fan_speed_ratio: f64) {
        self.t_air_k = t_air_k;
        self.rho_air = rho_air_kg_m3.max(0.5);
        self.fan_speed_ratio = fan_speed_ratio.clamp(0.0, 1.0);
        self.update_ua_effective();
        // Also update the inner condenser's air inlet temperature for LMTD
        // (This sets the cold-side inlet temperature used by the LMTD model)
        // The inner Condenser doesn't expose this directly; we handle it via
        // the saturation temperature set-point instead.
    }

    // ─── Accessors ────────────────────────────────────────────────────────────

    /// Returns nominal UA [W/K].
    pub fn ua_nominal(&self) -> f64 {
        self.ua_nominal
    }

    /// Returns the current effective UA after fan-speed and air corrections [W/K].
    pub fn ua_effective(&self) -> f64 {
        self.inner.ua()
    }

    /// Returns the current fan speed ratio.
    pub fn fan_speed_ratio(&self) -> f64 {
        self.fan_speed_ratio
    }

    /// Returns the ambient air temperature [K].
    pub fn t_air_k(&self) -> f64 {
        self.t_air_k
    }

    /// Returns the coil index in the bank.
    pub fn coil_index(&self) -> usize {
        self.coil_index
    }

    /// Returns the air-side exponent.
    pub fn n_air(&self) -> f64 {
        self.n_air
    }

    /// Returns the inner `Condenser` (for state access).
    pub fn inner(&self) -> &Condenser {
        &self.inner
    }

    // ─── Internal ─────────────────────────────────────────────────────────────

    /// Recalculates effective UA from current fan speed and air density.
    ///
    /// ```text
    /// UA_eff = UA_nominal × (ρ_air / ρ_ref) × fan_speed_ratio^n_air
    /// ```
    ///
    /// The density correction accounts for reduced air mass flow at high ambient
    /// temperatures (same volumetric flow → lower mass flow → lower UA).
    ///
    /// Reference conditions: 35°C, ρ_ref = 1.12 kg/m³.
    fn update_ua_effective(&mut self) {
        const RHO_REF: f64 = 1.12; // kg/m³ at 35°C, 101 325 Pa

        // Density correction: lower air density → lower mass flow → lower UA
        let rho_correction = self.rho_air / RHO_REF;

        // Velocity correction: UA ∝ G^n ≈ (speed_ratio)^n at constant frontal area
        let velocity_correction = if self.fan_speed_ratio < MIN_ACTIVE_SPEED_RATIO {
            0.0 // fan stopped → coil inactive
        } else {
            self.fan_speed_ratio.powf(self.n_air)
        };

        // Combined scale = (ρ/ρ_ref) × speed^n_air
        let scale = rho_correction * velocity_correction;
        self.inner.set_ua(self.ua_nominal * scale.max(0.0));
    }
}

// ─── Air density helper ──────────────────────────────────────────────────────

/// Computes dry air density via ideal gas law.
///
/// ρ = P / (R_air × T)   where R_air = 287.058 J/(kg·K)
fn air_density_kg_m3(t_k: f64, p_pa: f64) -> f64 {
    const R_AIR: f64 = 287.058; // J/(kg·K)
    if t_k > 0.0 && p_pa > 0.0 {
        p_pa / (R_AIR * t_k)
    } else {
        1.2 // fallback standard air
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Component trait
// ─────────────────────────────────────────────────────────────────────────────

impl Component for MchxCondenserCoil {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // Validate Air on the cold side (lazy check like CondenserCoil)
        if !self
            .air_validated
            .load(std::sync::atomic::Ordering::Relaxed)
        {
            if let Some(fluid_id) = self.inner.cold_fluid_id() {
                if fluid_id.0.as_str() != "Air" {
                    return Err(ComponentError::InvalidState(format!(
                        "MchxCondenserCoil[{}]: requires Air on cold side, found {}",
                        self.coil_index,
                        fluid_id.0.as_str()
                    )));
                }
                self.air_validated
                    .store(true, std::sync::atomic::Ordering::Relaxed);
            }
        }
        self.inner.compute_residuals(state, residuals)
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        self.inner.jacobian_entries(state, jacobian)
    }

    fn n_equations(&self) -> usize {
        self.inner.n_equations()
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        self.inner.get_ports()
    }

    /// CM1.4: Forward system context to the inner `Condenser` so that
    /// `same_branch_m` is detected and the redundant mass-conservation residual
    /// is dropped when inlet and outlet share the same ṁ branch index.
    fn set_system_context(
        &mut self,
        state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.inner
            .set_system_context(state_offset, external_edge_state_indices);
    }

    fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
        self.inner.set_calib_indices(indices);
    }

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        self.inner.port_mass_flows(state)
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        self.inner.port_enthalpies(state)
    }

    fn energy_transfers(
        &self,
        state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        self.inner.energy_transfers(state)
    }

    fn signature(&self) -> String {
        format!(
            "MchxCondenserCoil[{}](UA_nom={:.0},UA_eff={:.0},fan={:.2},T_air={:.1}K)",
            self.coil_index,
            self.ua_nominal,
            self.ua_effective(),
            self.fan_speed_ratio,
            self.t_air_k
        )
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        self.inner.update_calib_factor(factor, value)
    }
}

impl StateManageable for MchxCondenserCoil {
    fn state(&self) -> OperationalState {
        self.inner.state()
    }

    fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
        self.inner.set_state(state)
    }

    fn can_transition_to(&self, target: OperationalState) -> bool {
        self.inner.can_transition_to(target)
    }

    fn circuit_id(&self) -> &CircuitId {
        self.inner.circuit_id()
    }

    fn set_circuit_id(&mut self, circuit_id: CircuitId) {
        self.inner.set_circuit_id(circuit_id);
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

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

    #[test]
    fn test_creation_defaults() {
        let coil = MchxCondenserCoil::new(15_000.0, 0.5, 0);
        assert_eq!(coil.ua_nominal(), 15_000.0);
        assert_eq!(coil.fan_speed_ratio(), 1.0);
        assert_eq!(coil.coil_index(), 0);
        assert_eq!(coil.n_air(), 0.5);
    }

    #[test]
    fn test_for_35c_ambient_constructor() {
        let coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 2);
        assert_eq!(coil.coil_index(), 2);
        assert!((coil.t_air_k() - 308.15).abs() < 0.01);
    }

    #[test]
    fn test_ua_effective_at_full_speed() {
        let coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        // At design point: fan=1.0, T_air=35°C → UA_eff ≈ UA_nominal
        // (small delta due to density rounding)
        assert_relative_eq!(coil.ua_effective(), 15_000.0, epsilon = 500.0);
    }

    #[test]
    fn test_ua_effective_at_half_speed() {
        let coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        let ua_full = coil.ua_effective();

        let mut coil2 = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        coil2.set_fan_speed_ratio(0.5);
        let ua_half = coil2.ua_effective();

        // UA_half = UA_full × 0.5^0.5 ≈ UA_full × 0.707
        let ratio = ua_half / ua_full;
        assert_relative_eq!(ratio, 0.5_f64.sqrt(), epsilon = 0.02);
    }

    #[test]
    fn test_ua_effective_fan_stopped() {
        let mut coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        coil.set_fan_speed_ratio(0.0);
        assert_eq!(coil.ua_effective(), 0.0);
    }

    #[test]
    fn test_ua_effective_fan_below_minimum() {
        let mut coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        coil.set_fan_speed_ratio(0.01); // below MIN_ACTIVE_SPEED_RATIO
        assert_eq!(coil.ua_effective(), 0.0);
    }

    #[test]
    fn test_ua_decreases_with_higher_temperature() {
        // Higher ambient → lower air density → lower UA
        let mut coil_35 = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        coil_35.set_air_conditions(35.0 + 273.15, air_density_kg_m3(308.15, 101_325.0), 1.0);
        let ua_35 = coil_35.ua_effective();

        let mut coil_45 = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
        coil_45.set_air_temperature_celsius(45.0);
        let ua_45 = coil_45.ua_effective();

        assert!(ua_45 < ua_35, "UA at 45°C should be less than at 35°C");
    }

    #[test]
    fn test_n_equations_is_two() {
        let coil = MchxCondenserCoil::new(15_000.0, 0.5, 0);
        // CM1.3: 2 thermo + 1 mass-flow = 3 (delegates to inner Condenser)
        assert_eq!(coil.n_equations(), 3);
    }

    #[test]
    fn test_compute_residuals_ok() {
        let coil = MchxCondenserCoil::new(15_000.0, 0.5, 0);
        let state = vec![0.0; 10];
        let mut residuals = vec![0.0; 3];
        let result = coil.compute_residuals(&state, &mut residuals);
        assert!(matches!(result, Err(ComponentError::InvalidState(_))));
    }

    #[test]
    fn test_signature_contains_index() {
        let coil = MchxCondenserCoil::new(15_000.0, 0.5, 2);
        let sig = coil.signature();
        assert!(sig.contains("MchxCondenserCoil[2]"));
    }

    #[test]
    fn test_state_manageable() {
        let coil = MchxCondenserCoil::new(15_000.0, 0.5, 0);
        assert_eq!(coil.state(), OperationalState::On);
    }

    #[test]
    fn test_air_density_helper() {
        // At 20°C and 101325 Pa: ρ ≈ 1.204 kg/m³
        let rho = air_density_kg_m3(293.15, 101_325.0);
        assert_relative_eq!(rho, 1.204, epsilon = 0.005);
    }
}
