//! Fan-coil unit (FCU): water–air ε-NTU with bypass factor (BPF) wet-coil model.
//!
//! Sensible / latent split via apparatus dew point (ADP) and BPF:
//! `T_leave = BPF · T_enter + (1 − BPF) · T_ADP`.

use super::eps_ntu::{EpsNtuModel, ExchangerType};

/// Psychrometric / coil inputs for FCU rating.
#[derive(Debug, Clone, Copy)]
pub struct FanCoilRatingInput {
    /// Entering air dry-bulb [K].
    pub t_air_in_k: f64,
    /// Entering air wet-bulb or humidity ratio proxy via dewpoint [K].
    pub t_dew_in_k: f64,
    /// Entering water temperature [K].
    pub t_water_in_k: f64,
    /// Air capacity rate ṁ·cp [W/K].
    pub c_air: f64,
    /// Water capacity rate ṁ·cp [W/K].
    pub c_water: f64,
    /// Bypass factor (0.05–0.40 typical).
    pub bypass_factor: f64,
    /// Apparatus dew-point temperature [K] (coil surface).
    pub t_adp_k: f64,
}

/// Result of an FCU rating.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FanCoilRating {
    /// Total cooling capacity [W] (sensible + latent proxy).
    pub q_total_w: f64,
    /// Sensible capacity [W].
    pub q_sensible_w: f64,
    /// Latent capacity [W] (q_total − q_sensible, ≥ 0).
    pub q_latent_w: f64,
    /// Leaving air dry-bulb [K].
    pub t_air_out_k: f64,
    /// Leaving water temperature [K].
    pub t_water_out_k: f64,
    /// Sensible heat ratio SHR = q_sensible / q_total.
    pub shr: f64,
    /// Effectiveness of the dry ε-NTU backbone [-].
    pub effectiveness: f64,
}

/// Fan-coil unit with fixed UA and BPF wet-coil model.
#[derive(Debug, Clone)]
pub struct FanCoilUnit {
    ua: f64,
    /// Default bypass factor when not overridden in the rating call.
    default_bpf: f64,
}

impl FanCoilUnit {
    /// Creates an FCU with overall UA [W/K].
    pub fn new(ua: f64) -> Self {
        Self {
            ua: ua.max(0.0),
            default_bpf: 0.15,
        }
    }

    /// Sets default bypass factor.
    pub fn with_bypass_factor(mut self, bpf: f64) -> Self {
        self.default_bpf = bpf.clamp(0.0, 0.5);
        self
    }

    /// Rates the coil. Uses `input.bypass_factor` if > 0, else default BPF.
    pub fn rate(&self, input: &FanCoilRatingInput) -> FanCoilRating {
        let bpf = if input.bypass_factor > 0.0 {
            input.bypass_factor.clamp(0.0, 0.5)
        } else {
            self.default_bpf
        };
        let c_min = input.c_air.min(input.c_water).max(1.0);
        let c_max = input.c_air.max(input.c_water).max(c_min);
        let cr = c_min / c_max;
        let model = EpsNtuModel::new(self.ua, ExchangerType::CrossFlowUnmixed);
        let ntu = self.ua / c_min;
        let eps = model.effectiveness(ntu, cr);

        // Dry ε-NTU sensible backbone on water–air.
        let q_max = c_min * (input.t_air_in_k - input.t_water_in_k).max(0.0);
        let q_dry = eps * q_max;

        // Wet-coil leaving air via BPF / ADP.
        let t_air_out = bpf * input.t_air_in_k + (1.0 - bpf) * input.t_adp_k;
        let q_sensible = input.c_air * (input.t_air_in_k - t_air_out).max(0.0);

        // Latent proxy: dewpoint depression when coil below dewpoint.
        let wet = input.t_adp_k < input.t_dew_in_k;
        let q_latent = if wet {
            // Contact factor × latent drive (simplified, ~2450 kJ/kg · Δω proxy via ΔT).
            let cf = 1.0 - bpf;
            cf * input.c_air * 0.5 * (input.t_dew_in_k - input.t_adp_k).max(0.0)
        } else {
            0.0
        };

        let (q_total, q_sensible_out, q_latent_out) = if wet {
            (q_sensible + q_latent, q_sensible, q_latent)
        } else {
            let q = q_dry.max(q_sensible);
            (q, q, 0.0)
        };
        let t_water_out = input.t_water_in_k + q_total / input.c_water.max(1.0);
        let shr = if wet && q_total > 1.0 {
            (q_sensible_out / q_total).clamp(0.0, 1.0)
        } else {
            1.0
        };

        FanCoilRating {
            q_total_w: q_total,
            q_sensible_w: q_sensible_out,
            q_latent_w: q_latent_out,
            t_air_out_k: t_air_out,
            t_water_out_k: t_water_out,
            shr,
            effectiveness: eps,
        }
    }

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

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

    fn cool_input() -> FanCoilRatingInput {
        FanCoilRatingInput {
            t_air_in_k: 300.15,   // 27°C
            t_dew_in_k: 289.15,   // 16°C dew
            t_water_in_k: 280.15, // 7°C
            c_air: 1200.0,
            c_water: 2500.0,
            bypass_factor: 0.15,
            t_adp_k: 283.15, // 10°C
        }
    }

    #[test]
    fn wet_coil_has_latent() {
        let fcu = FanCoilUnit::new(8000.0);
        let r = fcu.rate(&cool_input());
        assert!(r.q_latent_w > 0.0);
        assert!(r.shr < 1.0);
        assert!(r.t_air_out_k < cool_input().t_air_in_k);
    }

    #[test]
    fn dry_coil_zero_latent() {
        let fcu = FanCoilUnit::new(8000.0);
        let mut inp = cool_input();
        inp.t_adp_k = 295.0; // above dewpoint
        let r = fcu.rate(&inp);
        assert_eq!(r.q_latent_w, 0.0);
        assert!((r.shr - 1.0).abs() < 1e-9);
    }
}
