//! Physical mass-flow models for expansion devices.
//!
//! Complements the isenthalpic `ExpansionValve` residual structure with
//! catalogue / TXV flow equations used for sizing and EXV/TXV actuation.

/// Selectable orifice / TXV / EXV flow model.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ValveFlowModel {
    /// Simple orifice: `ṁ = β · opening · √(2 ρ ΔP)`.
    IsenthalpicOrifice {
        /// Effective flow area constant β [m²].
        beta_m2: f64,
    },
    /// Electronic expansion valve: `ṁ = C_d · A_max · opening · √(2 ρ ΔP)`.
    ExvCdA {
        /// Discharge coefficient [-].
        cd: f64,
        /// Maximum orifice area [m²].
        area_max_m2: f64,
    },
    /// Eames–Milazzo–Maidment (2014) TXV model (liquid-charged bulb).
    TxvEames {
        /// Flow-area constant β [m²].
        beta_m2: f64,
        /// Static superheat setting pressure equivalent α [Pa].
        static_superheat_pa: f64,
        /// Full-open bulb ΔP = δ [Pa].
        full_open_delta_pa: f64,
    },
}

impl Default for ValveFlowModel {
    fn default() -> Self {
        Self::IsenthalpicOrifice { beta_m2: 1.0e-6 }
    }
}

/// Inputs for valve mass-flow evaluation (SI).
#[derive(Debug, Clone, Copy)]
pub struct ValveFlowInput {
    /// Upstream density [kg/m³] (usually saturated / subcooled liquid).
    pub density_kg_m3: f64,
    /// Condenser / upstream pressure [Pa].
    pub p_upstream_pa: f64,
    /// Evaporator / downstream pressure [Pa].
    pub p_downstream_pa: f64,
    /// Normalized opening in [0, 1] (EXV / orifice).
    pub opening: f64,
    /// Bulb pressure [Pa] (TXV); ignored for orifice/EXV.
    pub p_bulb_pa: f64,
}

impl ValveFlowInput {
    fn validate(&self) -> Result<(), String> {
        if !self.density_kg_m3.is_finite() || self.density_kg_m3 <= 0.0 {
            return Err(format!("density must be positive, got {}", self.density_kg_m3));
        }
        if !self.p_upstream_pa.is_finite() || !self.p_downstream_pa.is_finite() {
            return Err("pressures must be finite".into());
        }
        if !(0.0..=1.0).contains(&self.opening) || !self.opening.is_finite() {
            return Err(format!("opening must be in [0,1], got {}", self.opening));
        }
        Ok(())
    }
}

/// Mass flow [kg/s] for the selected model. Always ≥ 0.
pub fn valve_mass_flow(model: &ValveFlowModel, input: &ValveFlowInput) -> Result<f64, String> {
    input.validate()?;
    let dp = (input.p_upstream_pa - input.p_downstream_pa).max(0.0);
    let sqrt_term = (2.0 * input.density_kg_m3 * dp).sqrt();
    let m = match model {
        ValveFlowModel::IsenthalpicOrifice { beta_m2 } => {
            beta_m2.max(0.0) * input.opening.clamp(0.0, 1.0) * sqrt_term
        }
        ValveFlowModel::ExvCdA { cd, area_max_m2 } => {
            cd.max(0.0) * area_max_m2.max(0.0) * input.opening.clamp(0.0, 1.0) * sqrt_term
        }
        ValveFlowModel::TxvEames {
            beta_m2,
            static_superheat_pa,
            full_open_delta_pa,
        } => {
            // Eames: ṁ = β √(2ρ(Pc−Pe)) · [(Pb−Pe) − α] / δ  for α < (Pb−Pe) ≤ δ
            // fully open when (Pb−Pe) ≥ δ → factor 1.
            let drive = (input.p_bulb_pa - input.p_downstream_pa) - static_superheat_pa;
            let delta = full_open_delta_pa.max(1.0);
            let opening_eff = if drive <= 0.0 {
                0.0
            } else if drive >= delta {
                1.0
            } else {
                drive / delta
            };
            beta_m2.max(0.0) * opening_eff * sqrt_term
        }
    };
    Ok(m.max(0.0))
}

/// Analytic ∂ṁ/∂P_upstream for orifice/EXV (TXV uses same √ΔP branch).
pub fn valve_mass_flow_dp_up(model: &ValveFlowModel, input: &ValveFlowInput) -> Result<f64, String> {
    let m = valve_mass_flow(model, input)?;
    let dp = (input.p_upstream_pa - input.p_downstream_pa).max(0.0);
    if dp <= 0.0 || m <= 0.0 {
        return Ok(0.0);
    }
    // ṁ ∝ √ΔP ⇒ ∂ṁ/∂P_up = ṁ / (2 ΔP)
    Ok(m / (2.0 * dp))
}

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

    fn base_input() -> ValveFlowInput {
        ValveFlowInput {
            density_kg_m3: 1200.0,
            p_upstream_pa: 1.5e6,
            p_downstream_pa: 0.4e6,
            opening: 0.5,
            p_bulb_pa: 0.5e6,
        }
    }

    #[test]
    fn orifice_scales_with_opening() {
        let model = ValveFlowModel::IsenthalpicOrifice { beta_m2: 2e-6 };
        let mut half = base_input();
        half.opening = 0.5;
        let mut full = base_input();
        full.opening = 1.0;
        let m_half = valve_mass_flow(&model, &half).unwrap();
        let m_full = valve_mass_flow(&model, &full).unwrap();
        assert!((m_full - 2.0 * m_half).abs() < 1e-9);
    }

    #[test]
    fn exv_uses_cd_a() {
        let model = ValveFlowModel::ExvCdA {
            cd: 0.7,
            area_max_m2: 1e-5,
        };
        let m = valve_mass_flow(&model, &base_input()).unwrap();
        assert!(m.is_finite() && m > 0.0);
    }

    #[test]
    fn txv_closed_below_sss() {
        let model = ValveFlowModel::TxvEames {
            beta_m2: 2e-6,
            static_superheat_pa: 50_000.0,
            full_open_delta_pa: 150_000.0,
        };
        let mut inp = base_input();
        // Pb - Pe = 0.5e6 - 0.4e6 = 0.1e6 < α=50k → wait, 100k > 50k so partially open
        inp.p_bulb_pa = inp.p_downstream_pa + 20_000.0; // drive = 20k - 50k < 0
        let m = valve_mass_flow(&model, &inp).unwrap();
        assert_eq!(m, 0.0);
    }

    #[test]
    fn txv_fully_open_above_delta() {
        let model = ValveFlowModel::TxvEames {
            beta_m2: 2e-6,
            static_superheat_pa: 50_000.0,
            full_open_delta_pa: 100_000.0,
        };
        let mut inp = base_input();
        inp.p_bulb_pa = inp.p_downstream_pa + 200_000.0; // drive = 150k > δ
        let orifice = ValveFlowModel::IsenthalpicOrifice { beta_m2: 2e-6 };
        let mut full = inp;
        full.opening = 1.0;
        let m_txv = valve_mass_flow(&model, &inp).unwrap();
        let m_orf = valve_mass_flow(&orifice, &full).unwrap();
        assert!((m_txv - m_orf).abs() < 1e-9);
    }

    #[test]
    fn jacobian_dp_matches_fd() {
        let model = ValveFlowModel::ExvCdA {
            cd: 0.65,
            area_max_m2: 5e-6,
        };
        let inp = base_input();
        let d_analytic = valve_mass_flow_dp_up(&model, &inp).unwrap();
        let eps = 1.0;
        let mut up = inp;
        up.p_upstream_pa += eps;
        let mut dn = inp;
        dn.p_upstream_pa -= eps;
        let d_fd = (valve_mass_flow(&model, &up).unwrap() - valve_mass_flow(&model, &dn).unwrap())
            / (2.0 * eps);
        assert!((d_analytic - d_fd).abs() / d_analytic.max(1e-12) < 1e-4);
    }
}
