//! ReversingValve Component (four-way / cycle-reversing valve)
//!
//! Models a four-way reversing valve used in reversible heat pumps to switch the
//! machine between **cooling** and **heating** operation. In a steady-state
//! (P, h)-per-edge cycle solver the valve is represented as a deterministic inline
//! 2-port element placed on a refrigerant line. It imposes two genuinely physical
//! effects (no artificial derating):
//!
//! 1. **Pressure drop** across the valve body — the dominant real penalty of a
//!    four-way valve (typically 0.1–0.3 bar at rated flow, which alone reduces
//!    capacity/COP by ~1–3 %). Modeled as `ΔP = ΔP_base + k·ṁ·|ṁ|` (a fixed
//!    baseline plus a quadratic flow term), exactly like the lumped two-phase
//!    pressure drop used elsewhere.
//! 2. **Internal leakage** — a four-way valve leaks a small amount of hot
//!    discharge gas across the slide to the suction port. When the valve is placed
//!    on the suction line this manifests as a measurable suction-superheat gain;
//!    it is modeled as a specific-enthalpy rise `Δh_leak` [J/kg] added to the
//!    stream. `Δh_leak = 0` reproduces an adiabatic valve exactly.
//!
//!    **Energy-conservation caveat:** a non-zero `Δh_leak` injects `ṁ·Δh_leak` of
//!    enthalpy on a *single* stream. Physically the leaked gas recirculates from
//!    the high side to the low side, so a rigorous representation needs an explicit
//!    bypass **mass** split (two streams). In a single-stream lumped model the added
//!    enthalpy has no matching source and would break machine-level First-Law
//!    closure. The CLI examples therefore keep `Δh_leak = 0` (isenthalpic, exact
//!    First Law); the pressure drop alone already reproduces the ~1–3 % penalty.
//!    The parameter is retained for a future bypass-mass topology.
//!
//! ## Residuals (2 thermodynamic equations, + 1 mass conservation when the inlet
//! and outlet edges do not share a mass-flow index):
//!
//! - `r0 = P_out − (P_in − ΔP)`                (pressure drop)
//! - `r1 = h_out − (h_in + Δh_leak)`           (leakage preheat / adiabatic)
//! - `r2 = ṁ_out − ṁ_in`                        (mass conservation, CM1.3/CM1.4)
//!
//! The valve fully determines its outlet edge from its inlet edge, so it is
//! degree-of-freedom neutral: inserting it adds one edge (2 unknowns) and two
//! equations. It therefore works unchanged in both the fixed-pressure and the
//! emergent-pressure cycle topologies.
//!
//! ## Mode selector
//!
//! [`ReversingMode`] records whether the valve is set to `Cooling` or `Heating`.
//! The physical penalty (ΔP + leakage) is identical in both modes; the mode is
//! reported for diagnostics and drives which coil the configuration wires as the
//! condenser/evaporator. Full automatic role-swapping of the two coils from a
//! single configuration is intentionally left to the topology layer.

use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};

/// Operating mode of a four-way reversing valve.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReversingMode {
    /// Cooling mode: compressor discharge routed to the outdoor coil (condenser).
    Cooling,
    /// Heating mode: compressor discharge routed to the indoor coil (condenser).
    Heating,
}

impl ReversingMode {
    /// Parse a mode from a case-insensitive string (`"cooling"` / `"heating"`).
    /// Also accepts `"cool"` / `"heat"`. Returns `None` for anything else.
    pub fn from_str(s: &str) -> Option<Self> {
        match s.trim().to_ascii_lowercase().as_str() {
            "cooling" | "cool" | "c" => Some(Self::Cooling),
            "heating" | "heat" | "h" => Some(Self::Heating),
            _ => None,
        }
    }

    /// Lower-case string representation (`"cooling"` / `"heating"`).
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Cooling => "cooling",
            Self::Heating => "heating",
        }
    }
}

/// Four-way reversing valve: a deterministic inline 2-port pressure-drop +
/// internal-leakage element with a cooling/heating mode selector.
#[derive(Debug, Clone)]
pub struct ReversingValve {
    /// Operating mode (cooling / heating).
    mode: ReversingMode,
    /// Fixed baseline pressure drop across the valve body [Pa]. Default 0.
    pressure_drop_pa: f64,
    /// Optional quadratic flow coefficient `k` [Pa·s²/kg²] so that
    /// `ΔP = pressure_drop_pa + k·ṁ·|ṁ|`. `None` uses only the fixed baseline.
    pressure_drop_coeff: Option<f64>,
    /// Internal-leakage specific-enthalpy rise added to the stream [J/kg].
    /// Default 0 (adiabatic valve).
    leak_enthalpy_j_kg: f64,

    // State indices (set by set_system_context).
    inlet_p_idx: Option<usize>,
    inlet_h_idx: Option<usize>,
    inlet_m_idx: Option<usize>,
    outlet_p_idx: Option<usize>,
    outlet_h_idx: Option<usize>,
    outlet_m_idx: Option<usize>,
    /// True when inlet and outlet share the same ṁ state index (CM1.4).
    same_branch_m: bool,
}

impl ReversingValve {
    /// Create a new reversing valve in the given mode with no pressure drop and
    /// no leakage (an ideal pass-through until parameters are configured).
    pub fn new(mode: ReversingMode) -> Self {
        Self {
            mode,
            pressure_drop_pa: 0.0,
            pressure_drop_coeff: None,
            leak_enthalpy_j_kg: 0.0,
            inlet_p_idx: None,
            inlet_h_idx: None,
            inlet_m_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            outlet_m_idx: None,
            same_branch_m: false,
        }
    }

    /// Set the fixed baseline pressure drop [Pa].
    pub fn with_pressure_drop_pa(mut self, dp_pa: f64) -> Self {
        self.pressure_drop_pa = dp_pa.max(0.0);
        self
    }

    /// Set the quadratic flow pressure-drop coefficient `k` [Pa·s²/kg²]
    /// (`ΔP = pressure_drop_pa + k·ṁ·|ṁ|`).
    pub fn with_pressure_drop_coeff(mut self, k: f64) -> Self {
        self.pressure_drop_coeff = Some(k.max(0.0));
        self
    }

    /// Set the internal-leakage specific-enthalpy rise [J/kg] (suction preheat).
    pub fn with_leak_enthalpy(mut self, dh_j_kg: f64) -> Self {
        self.leak_enthalpy_j_kg = dh_j_kg;
        self
    }

    /// Current operating mode.
    pub fn mode(&self) -> ReversingMode {
        self.mode
    }

    /// Set the operating mode.
    pub fn set_mode(&mut self, mode: ReversingMode) {
        self.mode = mode;
    }

    /// Effective pressure drop [Pa] at the given mass flow `ṁ` [kg/s].
    fn delta_p(&self, m_dot: f64) -> f64 {
        self.pressure_drop_pa + self.pressure_drop_coeff.unwrap_or(0.0) * m_dot * m_dot.abs()
    }

    /// Mass-flow value used for the quadratic ΔP term (outlet, else inlet).
    fn mass_flow(&self, state: &StateSlice) -> Option<f64> {
        self.outlet_m_idx.or(self.inlet_m_idx).map(|idx| state[idx])
    }
}

impl Component for ReversingValve {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // Layout: [0] = incoming edge, [1] = outgoing edge. Triple (m, p, h).
        if !external_edge_state_indices.is_empty() {
            self.inlet_m_idx = Some(external_edge_state_indices[0].0);
            self.inlet_p_idx = Some(external_edge_state_indices[0].1);
            self.inlet_h_idx = Some(external_edge_state_indices[0].2);
        }
        if external_edge_state_indices.len() >= 2 {
            self.outlet_m_idx = Some(external_edge_state_indices[1].0);
            self.outlet_p_idx = Some(external_edge_state_indices[1].1);
            self.outlet_h_idx = Some(external_edge_state_indices[1].2);
        }
        self.same_branch_m = matches!(
            (self.inlet_m_idx, self.outlet_m_idx),
            (Some(m_in), Some(m_out)) if m_in == m_out
        );
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if residuals.len() != self.n_equations() {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: self.n_equations(),
                actual: residuals.len(),
            });
        }

        if let (Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
            self.inlet_p_idx,
            self.inlet_h_idx,
            self.outlet_p_idx,
            self.outlet_h_idx,
        ) {
            let m_dot = self.mass_flow(state).unwrap_or(0.0);
            let dp = self.delta_p(m_dot);
            // r0: pressure drop  P_out = P_in − ΔP
            residuals[0] = state[out_p] - (state[in_p] - dp);
            // r1: leakage preheat  h_out = h_in + Δh_leak
            residuals[1] = state[out_h] - (state[in_h] + self.leak_enthalpy_j_kg);
            // r2: mass conservation (CM1.3), dropped when same_branch_m (CM1.4).
            if !self.same_branch_m {
                residuals[2] = match (self.inlet_m_idx, self.outlet_m_idx) {
                    (Some(m_in), Some(m_out)) => state[m_out] - state[m_in],
                    _ => 0.0,
                };
            }
            return Ok(());
        }

        Err(ComponentError::InvalidState(
            "ReversingValve requires live inlet/outlet edge state indices".to_string(),
        ))
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        if let (Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
            self.inlet_p_idx,
            self.inlet_h_idx,
            self.outlet_p_idx,
            self.outlet_h_idx,
        ) {
            // r0 = P_out − P_in + ΔP(ṁ)
            jacobian.add_entry(0, out_p, 1.0);
            jacobian.add_entry(0, in_p, -1.0);
            // ∂ΔP/∂ṁ = k · 2|ṁ|  (d/dm of m·|m| = 2|m|)
            if let (Some(k), Some(m_idx)) = (
                self.pressure_drop_coeff,
                self.outlet_m_idx.or(self.inlet_m_idx),
            ) {
                let m_dot = state[m_idx];
                jacobian.add_entry(0, m_idx, k * 2.0 * m_dot.abs());
            }
            // r1 = h_out − h_in − Δh_leak
            jacobian.add_entry(1, out_h, 1.0);
            jacobian.add_entry(1, in_h, -1.0);
            // r2 = ṁ_out − ṁ_in
            if !self.same_branch_m {
                if let (Some(m_in), Some(m_out)) = (self.inlet_m_idx, self.outlet_m_idx) {
                    jacobian.add_entry(2, m_out, 1.0);
                    jacobian.add_entry(2, m_in, -1.0);
                }
            }
        }
        Ok(())
    }

    fn n_equations(&self) -> usize {
        // 2 thermodynamic equations (ΔP + leakage); + 1 mass conservation unless
        // the inlet and outlet edges are collapsed onto a single ṁ index (CM1.4).
        if self.same_branch_m {
            2
        } else {
            3
        }
    }

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

    fn energy_transfers(
        &self,
        _state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        // The valve does no shaft work. Any leakage enthalpy is an internal
        // redistribution within the refrigerant stream, not an external heat
        // input, so it is not reported as Q here (that would double-count it in
        // the cycle energy balance).
        Some((
            entropyk_core::Power::from_watts(0.0),
            entropyk_core::Power::from_watts(0.0),
        ))
    }

    fn signature(&self) -> String {
        format!(
            "ReversingValve(mode={}, dp_pa={:.1}, dp_k={:?}, leak_j_kg={:.1})",
            self.mode.as_str(),
            self.pressure_drop_pa,
            self.pressure_drop_coeff,
            self.leak_enthalpy_j_kg
        )
    }
}

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

    fn wired(mode: ReversingMode) -> ReversingValve {
        let mut v = ReversingValve::new(mode);
        // Distinct branches: inlet (m0, p1, h2), outlet (m3, p4, h5).
        v.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
        v
    }

    #[test]
    fn test_mode_parsing_and_roundtrip() {
        assert_eq!(
            ReversingMode::from_str("Cooling"),
            Some(ReversingMode::Cooling)
        );
        assert_eq!(
            ReversingMode::from_str("HEAT"),
            Some(ReversingMode::Heating)
        );
        assert_eq!(ReversingMode::from_str("bogus"), None);
        assert_eq!(ReversingMode::Cooling.as_str(), "cooling");
        assert_eq!(ReversingMode::Heating.as_str(), "heating");
    }

    #[test]
    fn test_equation_count_branches() {
        // Distinct branches → 3 equations (thermo + mass).
        assert_eq!(wired(ReversingMode::Cooling).n_equations(), 3);
        // Same-branch (collapsed ṁ) → 2 equations.
        let mut same = ReversingValve::new(ReversingMode::Cooling);
        same.set_system_context(0, &[(0, 1, 2), (0, 3, 4)]);
        assert_eq!(same.n_equations(), 2);
    }

    #[test]
    fn test_ideal_passthrough_is_zero_residual_when_consistent() {
        let v = wired(ReversingMode::Cooling);
        // state: m0,p1,h2 (in) ; m3,p4,h5 (out). Consistent pass-through.
        let mut state = vec![0.0; 6];
        state[0] = 0.1; // m_in
        state[1] = 12.0e5; // p_in
        state[2] = 430_000.0; // h_in
        state[3] = 0.1; // m_out
        state[4] = 12.0e5; // p_out (no ΔP)
        state[5] = 430_000.0; // h_out (no leak)
        let mut r = vec![9.0; 3];
        v.compute_residuals(&state, &mut r).unwrap();
        assert!(r.iter().all(|x| x.abs() < 1e-9), "residuals: {r:?}");
    }

    #[test]
    fn test_residual_reacts_to_pressure_drop_and_leak() {
        let v = wired(ReversingMode::Heating)
            .with_pressure_drop_pa(20_000.0) // 0.2 bar
            .with_leak_enthalpy(1_500.0); // 1.5 kJ/kg suction preheat
        let mut state = vec![0.0; 6];
        state[0] = 0.1;
        state[1] = 3.5e5; // p_in
        state[2] = 400_000.0; // h_in
        state[3] = 0.1;
        state[4] = 3.5e5; // p_out (unchanged → violates ΔP)
        state[5] = 400_000.0; // h_out (unchanged → violates leak)
        let mut r = vec![0.0; 3];
        v.compute_residuals(&state, &mut r).unwrap();
        // r0 = p_out − (p_in − ΔP) = 0 − (−20000) = +20000
        assert!((r[0] - 20_000.0).abs() < 1e-6, "r0={}", r[0]);
        // r1 = h_out − (h_in + leak) = −1500
        assert!((r[1] + 1_500.0).abs() < 1e-6, "r1={}", r[1]);
    }

    #[test]
    fn test_jacobian_matches_finite_difference() {
        let v = wired(ReversingMode::Cooling)
            .with_pressure_drop_coeff(1.0e6) // quadratic flow term
            .with_pressure_drop_pa(5_000.0)
            .with_leak_enthalpy(800.0);
        let state = vec![0.15_f64, 3.4e5, 405_000.0, 0.15, 3.3e5, 404_000.0];

        // Analytic entries.
        let mut jb = JacobianBuilder::new();
        v.jacobian_entries(&state, &mut jb).unwrap();
        let neq = v.n_equations();
        let ncol = state.len();
        let mut jac = vec![vec![0.0; ncol]; neq];
        for &(row, col, val) in jb.entries() {
            jac[row][col] += val;
        }

        // Central finite difference of the residuals.
        let eval = |s: &[f64]| {
            let mut r = vec![0.0; neq];
            v.compute_residuals(s, &mut r).unwrap();
            r
        };
        for col in 0..ncol {
            let h = state[col].abs() * 1e-6 + 1e-3;
            let mut sp = state.clone();
            let mut sm = state.clone();
            sp[col] += h;
            sm[col] -= h;
            let rp = eval(&sp);
            let rm = eval(&sm);
            for row in 0..neq {
                let fd = (rp[row] - rm[row]) / (2.0 * h);
                assert!(
                    (fd - jac[row][col]).abs() < 1e-3 * (1.0 + fd.abs()),
                    "J[{row}][{col}] analytic={} fd={}",
                    jac[row][col],
                    fd
                );
            }
        }
    }
}
