//! Effectiveness-NTU (ε-NTU) Model
//!
//! Implements the ε-NTU method for heat exchanger calculations.
//!
//! ## Theory
//!
//! The heat transfer rate is calculated as:
//!
//! $$\dot{Q} = \varepsilon \cdot \dot{Q}_{max} = \varepsilon \cdot C_{min} \cdot (T_{hot,in} - T_{cold,in})$$
//!
//! Where:
//! - $\varepsilon$: Effectiveness (0 to 1)
//! - $C_{min} = \min(\dot{m}_{hot} \cdot c_{p,hot}, \dot{m}_{cold} \cdot c_{p,cold})$: Minimum heat capacity rate
//! - $NTU = UA / C_{min}$: Number of Transfer Units
//! - $C_r = C_{min} / C_{max}$: Heat capacity ratio
//!
//! ## Zero-flow regularization (Story 3.5)
//!
//! When $C_{min} < 10^{-10}$ (e.g. zero mass flow on one side), heat transfer is set to zero
//! and divisions by $C_{min}$ or $C_r$ are avoided to prevent NaN/Inf.
//!
//! Note: This module uses `1e-10` kW/K for capacity rate regularization, which is appropriate
//! for the kW/K scale. For mass flow regularization at the kg/s scale, see
//! [`MIN_MASS_FLOW_REGULARIZATION_KG_S`](entropyk_core::MIN_MASS_FLOW_REGULARIZATION_KG_S).
//!
//! For counter-flow:
//! $$\varepsilon = \frac{1 - \exp(-NTU \cdot (1 - C_r))}{1 - C_r \cdot \exp(-NTU \cdot (1 - C_r))}$$

use super::model::{FluidState, HeatTransferModel};
use crate::ResidualVector;
use entropyk_core::Power;

/// Heat exchanger type for ε-NTU calculations.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ExchangerType {
    /// Counter-flow (most efficient)
    #[default]
    CounterFlow,
    /// Parallel-flow (co-current)
    ParallelFlow,
    /// Cross-flow, both fluids unmixed
    CrossFlowUnmixed,
    /// Cross-flow, one fluid mixed (C_max mixed)
    CrossFlowMixedMax,
    /// Cross-flow, one fluid mixed (C_min mixed)
    CrossFlowMixedMin,
    /// Shell-and-tube with specified number of shell passes
    ShellAndTube {
        /// Number of shell passes
        passes: usize,
    },
}

/// ε-NTU (Effectiveness-NTU) heat transfer model.
///
/// Uses the effectiveness-NTU method for heat exchanger rating.
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::{EpsNtuModel, ExchangerType, HeatTransferModel};
///
/// let model = EpsNtuModel::new(5000.0, ExchangerType::CounterFlow);
/// assert_eq!(model.ua(), 5000.0);
/// ```
#[derive(Debug, Clone)]
pub struct EpsNtuModel {
    /// Overall heat transfer coefficient × Area (W/K), nominal
    ua: f64,
    /// UA calibration scale: UA_eff = ua_scale × ua (default 1.0)
    ua_scale: f64,
    /// Heat exchanger type
    exchanger_type: ExchangerType,
}

impl EpsNtuModel {
    /// Creates a new ε-NTU model.
    ///
    /// # Arguments
    ///
    /// * `ua` - Overall heat transfer coefficient × Area (W/K). Must be non-negative.
    /// * `exchanger_type` - Type of heat exchanger
    ///
    /// # Panics
    ///
    /// Panics if `ua` is negative or NaN.
    pub fn new(ua: f64, exchanger_type: ExchangerType) -> Self {
        assert!(
            ua.is_finite() && ua >= 0.0,
            "UA must be non-negative and finite, got {}",
            ua
        );
        Self {
            ua,
            ua_scale: 1.0,
            exchanger_type,
        }
    }

    /// Creates a counter-flow ε-NTU model.
    pub fn counter_flow(ua: f64) -> Self {
        Self::new(ua, ExchangerType::CounterFlow)
    }

    /// Creates a parallel-flow ε-NTU model.
    pub fn parallel_flow(ua: f64) -> Self {
        Self::new(ua, ExchangerType::ParallelFlow)
    }

    /// Creates a cross-flow (unmixed) ε-NTU model.
    pub fn cross_flow_unmixed(ua: f64) -> Self {
        Self::new(ua, ExchangerType::CrossFlowUnmixed)
    }

    /// Calculates the effectiveness ε.
    ///
    /// # Arguments
    ///
    /// * `ntu` - Number of Transfer Units (UA / C_min)
    /// * `c_r` - Heat capacity ratio (C_min / C_max)
    ///
    /// # Returns
    ///
    /// The effectiveness ε (0 to 1)
    pub fn effectiveness(&self, ntu: f64, c_r: f64) -> f64 {
        if ntu <= 0.0 {
            return 0.0;
        }

        match self.exchanger_type {
            ExchangerType::CounterFlow => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    let exp_term = (-ntu * (1.0 - c_r)).exp();
                    (1.0 - exp_term) / (1.0 - c_r * exp_term)
                }
            }
            ExchangerType::ParallelFlow => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    (1.0 - (-ntu * (1.0 + c_r)).exp()) / (1.0 + c_r)
                }
            }
            ExchangerType::CrossFlowUnmixed => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    1.0 - (-c_r * (1.0 - (-ntu / c_r).exp())).exp()
                }
            }
            ExchangerType::CrossFlowMixedMax => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    let ntu_c_r = ntu / c_r;
                    (1.0 - (-ntu_c_r).exp()) / c_r * (1.0 - (-c_r * ntu).exp())
                }
            }
            ExchangerType::CrossFlowMixedMin => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    (1.0 / c_r) * (1.0 - (-c_r * (1.0 - (-ntu).exp())).exp())
                }
            }
            ExchangerType::ShellAndTube { passes: _ } => {
                if c_r < 1e-10 {
                    1.0 - (-ntu).exp()
                } else {
                    (1.0 - (-ntu * (1.0 + c_r * c_r).sqrt()).exp()) / (1.0 + c_r)
                }
            }
        }
    }

    /// Calculates the maximum possible heat transfer rate.
    ///
    /// Q̇_max = C_min × (T_hot,in - T_cold,in)
    pub fn q_max(&self, c_min: f64, t_hot_in: f64, t_cold_in: f64) -> f64 {
        c_min * (t_hot_in - t_cold_in).max(0.0)
    }
}

impl HeatTransferModel for EpsNtuModel {
    fn compute_heat_transfer(
        &self,
        hot_inlet: &FluidState,
        _hot_outlet: &FluidState,
        cold_inlet: &FluidState,
        _cold_outlet: &FluidState,
        dynamic_ua_scale: Option<f64>,
    ) -> Power {
        let c_hot = hot_inlet.heat_capacity_rate();
        let c_cold = cold_inlet.heat_capacity_rate();

        let (c_min, c_max) = if c_hot < c_cold {
            (c_hot, c_cold)
        } else {
            (c_cold, c_hot)
        };

        if c_min < 1e-10 {
            return Power::from_watts(0.0);
        }

        let c_r = c_min / c_max;
        let ntu = self.effective_ua(dynamic_ua_scale) / c_min;

        let effectiveness = self.effectiveness(ntu, c_r);

        let q_max = self.q_max(c_min, hot_inlet.temperature, cold_inlet.temperature);

        Power::from_watts(effectiveness * q_max)
    }

    fn compute_residuals(
        &self,
        hot_inlet: &FluidState,
        hot_outlet: &FluidState,
        cold_inlet: &FluidState,
        cold_outlet: &FluidState,
        residuals: &mut ResidualVector,
        dynamic_ua_scale: Option<f64>,
    ) {
        let q = self
            .compute_heat_transfer(
                hot_inlet,
                hot_outlet,
                cold_inlet,
                cold_outlet,
                dynamic_ua_scale,
            )
            .to_watts();

        let q_hot = hot_inlet.mass_flow * (hot_inlet.enthalpy - hot_outlet.enthalpy);
        let q_cold = cold_inlet.mass_flow * (cold_outlet.enthalpy - cold_inlet.enthalpy);

        residuals[0] = q_hot - q;
        residuals[1] = q_cold - q;
    }

    fn n_equations(&self) -> usize {
        2
    }

    fn ua(&self) -> f64 {
        self.ua
    }

    fn ua_scale(&self) -> f64 {
        self.ua_scale
    }

    fn set_ua_scale(&mut self, s: f64) {
        self.ua_scale = s;
    }

    fn effective_ua(&self, dynamic_ua_scale: Option<f64>) -> f64 {
        self.ua * dynamic_ua_scale.unwrap_or(self.ua_scale)
    }
}

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

    #[test]
    fn test_eps_ntu_model_creation() {
        let model = EpsNtuModel::new(5000.0, ExchangerType::CounterFlow);
        assert_eq!(model.ua(), 5000.0);
    }

    #[test]
    fn test_effectiveness_counter_flow() {
        let model = EpsNtuModel::counter_flow(5000.0);

        let eps = model.effectiveness(5.0, 0.5);
        assert!(eps > 0.0 && eps < 1.0);

        let eps_cr_zero = model.effectiveness(5.0, 0.0);
        assert!((eps_cr_zero - (1.0 - (-5.0_f64).exp())).abs() < 1e-10);
    }

    #[test]
    fn test_effectiveness_parallel_flow() {
        let model = EpsNtuModel::parallel_flow(5000.0);

        let eps = model.effectiveness(5.0, 0.5);
        assert!(eps > 0.0 && eps < 1.0);
        assert!(eps < model.effectiveness(5.0, 0.5) + 0.1);
    }

    #[test]
    fn test_effectiveness_zero_ntu() {
        let model = EpsNtuModel::counter_flow(5000.0);
        let eps = model.effectiveness(0.0, 0.5);
        assert_eq!(eps, 0.0);
    }

    #[test]
    fn test_compute_heat_transfer() {
        let model = EpsNtuModel::counter_flow(5000.0);

        let hot_inlet = FluidState::new(80.0 + 273.15, 101_325.0, 400_000.0, 0.1, 1000.0);
        let hot_outlet = FluidState::new(60.0 + 273.15, 101_325.0, 380_000.0, 0.1, 1000.0);
        let cold_inlet = FluidState::new(20.0 + 273.15, 101_325.0, 80_000.0, 0.2, 4180.0);
        let cold_outlet = FluidState::new(30.0 + 273.15, 101_325.0, 120_000.0, 0.2, 4180.0);

        let q =
            model.compute_heat_transfer(&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None);

        assert!(q.to_watts() > 0.0);
    }

    #[test]
    fn test_n_equations() {
        let model = EpsNtuModel::counter_flow(1000.0);
        assert_eq!(model.n_equations(), 2);
    }

    #[test]
    fn test_q_max() {
        let model = EpsNtuModel::counter_flow(5000.0);

        let c_min = 1000.0;
        let t_hot_in = 350.0;
        let t_cold_in = 300.0;

        let q_max = model.q_max(c_min, t_hot_in, t_cold_in);
        assert_eq!(q_max, 50_000.0);
    }

    #[test]
    #[should_panic(expected = "UA must be non-negative")]
    fn test_negative_ua_panics() {
        let _model = EpsNtuModel::new(-1000.0, ExchangerType::CounterFlow);
    }

    #[test]
    fn test_effectiveness_cross_flow_unmixed_cr_zero() {
        let model = EpsNtuModel::cross_flow_unmixed(5000.0);

        let eps = model.effectiveness(5.0, 0.0);
        let expected = 1.0 - (-5.0_f64).exp();
        assert!((eps - expected).abs() < 1e-10);
    }
}
