//! 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.
//!
//! # Regularization (Story 0.3 / FR19)
//!
//! Hard `ΔP.max(0)` and `opening.clamp(0,1)` zero Newton derivatives at domain
//! edges. This module uses [`entropyk_core::smoothing`]:
//! - `dp_eff = smooth_max(ΔP, 0, DP_SMOOTH_K_PA)` so ∂ṁ/∂P stays informative near ΔP≤0
//! - `opening_eff = smooth_clamp(opening, 0, 1, OPENING_WIDTH)` for C¹ opening bounds
//!
//! Physical-region ṁ (ΔP ≫ `DP_SMOOTH_K_PA`, opening interior) matches the legacy
//! hard formula to solver tolerance.

use entropyk_core::smoothing::{smooth_clamp, smooth_max, smooth_max_derivative};

/// Softness `k` [Pa] for `smooth_max(ΔP, 0, k)`. Aligns with historical EXV `DP_FLOOR`.
const DP_SMOOTH_K_PA: f64 = 1e3;

/// Transition width for `smooth_clamp` of opening on `[0, 1]`.
const OPENING_WIDTH: f64 = 1e-2;

/// 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 (EXV / orifice). Mapped through `smooth_clamp` to [0, 1].
    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 !self.opening.is_finite() {
            return Err(format!("opening must be finite, got {}", self.opening));
        }
        Ok(())
    }

    fn delta_p_raw(&self) -> f64 {
        self.p_upstream_pa - self.p_downstream_pa
    }

    fn dp_eff(&self) -> f64 {
        smooth_max(self.delta_p_raw(), 0.0, DP_SMOOTH_K_PA)
    }

    fn opening_eff(&self) -> f64 {
        smooth_clamp(self.opening, 0.0, 1.0, OPENING_WIDTH)
    }
}

/// Effective flow coefficient × opening factor (before √(2ρΔP)).
fn flow_prefactor(model: &ValveFlowModel, input: &ValveFlowInput) -> f64 {
    match model {
        ValveFlowModel::IsenthalpicOrifice { beta_m2 } => beta_m2.max(0.0) * input.opening_eff(),
        ValveFlowModel::ExvCdA { cd, area_max_m2 } => {
            cd.max(0.0) * area_max_m2.max(0.0) * input.opening_eff()
        }
        ValveFlowModel::TxvEames {
            beta_m2,
            static_superheat_pa,
            full_open_delta_pa,
        } => {
            // Eames drive uses a hard piecewise opening_eff (not a Newton unknown here).
            // The √ΔP branch is still regularized via `dp_eff`.
            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
        }
    }
}

/// 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.dp_eff();
    let sqrt_term = (2.0 * input.density_kg_m3 * dp).sqrt();
    let m = flow_prefactor(model, input) * sqrt_term;
    Ok(m.max(0.0))
}

/// Analytic ∂ṁ/∂P_upstream for orifice/EXV (TXV uses same √ΔP branch).
///
/// `ṁ ∝ √dp_eff` with `dp_eff = smooth_max(ΔP, 0, k)` ⇒
/// `∂ṁ/∂P_up = (ṁ / (2 · dp_eff)) · ∂dp_eff/∂P_up`.
pub fn valve_mass_flow_dp_up(
    model: &ValveFlowModel,
    input: &ValveFlowInput,
) -> Result<f64, String> {
    let m = valve_mass_flow(model, input)?;
    let dp = input.dp_eff();
    if dp <= 0.0 || m <= 0.0 {
        // Degenerate: no live flow coefficient / density; keep non-panicking zero.
        return Ok(0.0);
    }
    let d_dp = smooth_max_derivative(input.delta_p_raw(), 0.0, DP_SMOOTH_K_PA);
    Ok(m / (2.0 * dp) * d_dp)
}

/// Analytic ∂ṁ/∂P_downstream for the orifice/EXV √ΔP branch.
///
/// Equal to `−∂ṁ/∂P_upstream` through the shared `dp_eff` chain rule.
/// TXV bulb-drive opening is treated as independent of `P_downstream` here
/// (same simplification as the pre-regularization √ΔP-only Jacobian).
pub fn valve_mass_flow_dp_down(
    model: &ValveFlowModel,
    input: &ValveFlowInput,
) -> Result<f64, String> {
    Ok(-valve_mass_flow_dp_up(model, input)?)
}

#[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,
        }
    }

    /// Legacy hard formula for physical-region regression.
    fn hard_orifice_mass_flow(beta_m2: f64, input: &ValveFlowInput) -> f64 {
        let dp = (input.p_upstream_pa - input.p_downstream_pa).max(0.0);
        let opening = input.opening.clamp(0.0, 1.0);
        beta_m2.max(0.0) * opening * (2.0 * input.density_kg_m3 * dp).sqrt()
    }

    #[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();
        // At opening=1.0, smooth_clamp is at the upper join — allow small C¹ band error.
        assert!((m_full - 2.0 * m_half).abs() / m_full.max(1e-12) < 1e-3);
    }

    #[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();
        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() / m_orf.max(1e-12) < 1e-3);
    }

    #[test]
    fn physical_region_matches_hard_formula() {
        let beta = 2e-6;
        let model = ValveFlowModel::IsenthalpicOrifice { beta_m2: beta };
        let inp = base_input(); // ΔP = 1.1e6 ≫ k
        let m = valve_mass_flow(&model, &inp).unwrap();
        let m_hard = hard_orifice_mass_flow(beta, &inp);
        assert!(
            (m - m_hard).abs() / m_hard < 1e-6,
            "physical-region ṁ {m} vs hard {m_hard}"
        );
        let d_up = valve_mass_flow_dp_up(&model, &inp).unwrap();
        let d_hard = m_hard / (2.0 * (inp.p_upstream_pa - inp.p_downstream_pa));
        assert!(
            (d_up - d_hard).abs() / d_hard < 1e-6,
            "physical-region ∂ṁ/∂P_up {d_up} vs hard {d_hard}"
        );
    }

    #[test]
    fn nonpositive_dp_has_informative_phantom_gradient() {
        let model = ValveFlowModel::IsenthalpicOrifice { beta_m2: 1.0e-6 };
        let input = ValveFlowInput {
            density_kg_m3: 1200.0,
            p_upstream_pa: 4.0e5,
            p_downstream_pa: 4.005e5, // mild ΔP = −500 Pa
            opening: 0.5,
            p_bulb_pa: 0.0,
        };
        let m = valve_mass_flow(&model, &input).unwrap();
        let d_up = valve_mass_flow_dp_up(&model, &input).unwrap();
        let d_down = valve_mass_flow_dp_down(&model, &input).unwrap();
        assert!(m >= 0.0 && m.is_finite());
        assert!(
            d_up > 0.0,
            "phantom ∂ṁ/∂P_up must be informative at mild ΔP≤0, got {d_up}"
        );
        assert!(
            (d_down + d_up).abs() < 1e-15 * d_up.max(1.0),
            "dp_down must equal −dp_up"
        );
    }

    #[test]
    fn jacobian_dp_up_matches_fd() {
        let model = ValveFlowModel::ExvCdA {
            cd: 0.65,
            area_max_m2: 5e-6,
        };
        for (p_up, p_down) in [(1.5e6, 0.4e6), (4.0e5, 4.005e5), (5.0e5, 5.0e5)] {
            let inp = ValveFlowInput {
                density_kg_m3: 1200.0,
                p_upstream_pa: p_up,
                p_downstream_pa: p_down,
                opening: 0.5,
                p_bulb_pa: 0.0,
            };
            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);
            let scale = d_analytic.abs().max(d_fd.abs()).max(1e-12);
            assert!(
                (d_analytic - d_fd).abs() / scale < 1e-4,
                "ΔP={} analytic {d_analytic} vs FD {d_fd}",
                p_up - p_down
            );
        }
    }

    #[test]
    fn jacobian_dp_down_matches_fd() {
        let model = ValveFlowModel::IsenthalpicOrifice { beta_m2: 2e-6 };
        let inp = base_input();
        let d_analytic = valve_mass_flow_dp_down(&model, &inp).unwrap();
        let eps = 1.0;
        let mut up = inp;
        up.p_downstream_pa += eps;
        let mut dn = inp;
        dn.p_downstream_pa -= eps;
        let d_fd = (valve_mass_flow(&model, &up).unwrap() - valve_mass_flow(&model, &dn).unwrap())
            / (2.0 * eps);
        let scale = d_analytic.abs().max(d_fd.abs()).max(1e-12);
        assert!((d_analytic - d_fd).abs() / scale < 1e-4);
    }

    #[test]
    fn opening_outside_unit_interval_is_accepted() {
        let model = ValveFlowModel::IsenthalpicOrifice { beta_m2: 1e-6 };
        let mut inp = base_input();
        inp.opening = 1.2;
        let m = valve_mass_flow(&model, &inp).unwrap();
        assert!(m.is_finite() && m > 0.0);
        inp.opening = -0.1;
        let m_neg = valve_mass_flow(&model, &inp).unwrap();
        assert!(m_neg.is_finite() && m_neg >= 0.0);
        // Outside the upper band, flow saturates near opening=1.
        inp.opening = 1.0;
        let m1 = valve_mass_flow(&model, &inp).unwrap();
        inp.opening = 2.0;
        let m2 = valve_mass_flow(&model, &inp).unwrap();
        assert!((m1 - m2).abs() / m1.max(1e-12) < 1e-6);
    }
}
