//! Screw Compressor with Economizer Port
//!
//! Models a twin-screw compressor with an intermediate economizer injection port,
//! as used in industrial air-cooled chillers and heat pumps.
//!
//! ## Physical Description
//!
//! A screw compressor with economizer operates in two internal compression stages
//! within the same machine casing:
//!
//! ```text
//! Stage 1:
//!   Suction (P_evap, h_suc) → compress to P_intermediate
//!
//! Intermediate Injection:
//!   Flash-gas from economizer at (P_eco, h_eco) injects into the rotor lobes
//!   at the intermediate pressure port. This cools the compressed gas and
//!   increases total mass flow delivered to stage 2.
//!
//! Stage 2:
//!   Mixed gas (P_intermediate, h_mix) → compress to P_discharge
//!
//! Net result:
//!   - Higher capacity vs. simple single-stage (~10-20%)
//!   - Better COP (~8-15%) for same condensing/evaporating temperatures
//!   - Extended operating range (higher compression ratios tolerated)
//! ```
//!
//! ## Ports
//!
//! - `port_suction`: Low-pressure refrigerant inlet (from evaporator/drum)
//! - `port_discharge`: High-pressure outlet (to condenser)
//! - `port_economizer`: Intermediate-pressure injection inlet (from economizer flash gas)
//!
//! ## Performance Model
//!
//! Uses the SST/SDT polynomial model (bi-quadratic), where the economizer effect
//! is captured via the intermediate pressure ratio and a user-supplied economizer
//! mass flow fraction curve:
//!
//! ```text
//! ṁ_suction  = f_m × Σ a_ij × SST^i × SDT^j      (polynomial, manufacturer data)
//! ṁ_eco      = x_eco × ṁ_suction                   (economizer fraction, 0.05–0.20)
//! ṁ_total    = ṁ_suction + ṁ_eco
//! W_comp     = f_pwr × Σ b_ij × SST^i × SDT^j      (shaft power, manufacturer data)
//!
//! Energy balance (adiabatic compressor):
//!   ṁ_suction × h_suc + ṁ_eco × h_eco + W_comp = ṁ_total × h_discharge
//! ```
//!
//! The intermediate pressure P_eco is typically the geometric mean of suction/discharge:
//!   P_eco = sqrt(P_suc × P_dis)
//!
//! ## Variable Speed Drive (VFD) Integration
//!
//! The `frequency_hz` field (25–60 Hz) scales:
//! - Mass flow  ∝ frequency (volumetric)
//! - Power      ∝ frequency (approximately)
//! - Economizer fraction: unchanged (geometry-driven)
//!
//! ## Usage
//!
//! ```rust,ignore
//! use entropyk_components::screw_economizer_compressor::{
//!     ScrewEconomizerCompressor, ScrewPerformanceCurves
//! };
//! use entropyk_components::polynomials::Polynomial2D;
//!
//! // Manufacturer curves for a ~200 kW screw (R134a, at 50 Hz):
//! let curves = ScrewPerformanceCurves {
//!     mass_flow_curve: Polynomial2D::bilinear(0.8, 0.005, -0.003, 0.00001),
//!     power_curve:     Polynomial2D::bilinear(50_000.0, 300.0, -200.0, 1.0),
//!     eco_flow_fraction_curve: Polynomial2D::bilinear(0.12, 0.001, -0.0005, 0.0),
//! };
//!
//! let compressor = ScrewEconomizerCompressor::new(curves, "R134a", 50.0, 0.92)?;
//! ```

use crate::polynomials::Polynomial2D;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Calib, CalibIndices, MassFlow, Power};
use serde::{Deserialize, Serialize};

// ─────────────────────────────────────────────────────────────────────────────
// Performance Curves
// ─────────────────────────────────────────────────────────────────────────────

/// Performance curves for a screw compressor with economizer port.
///
/// All curves are functions of (SST, SDT) evaluated in Kelvin.
/// Manufacturer data (e.g., Bitzer HSK/CSH series, Grasso) typically provides
/// these as polynomial fits to measured data tables.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScrewPerformanceCurves {
    /// Suction mass flow rate: ṁ_suc = f(SST, SDT)  [kg/s]
    ///
    /// SST = Saturated Suction Temperature  [K]
    /// SDT = Saturated Discharge Temperature [K]
    pub mass_flow_curve: Polynomial2D,

    /// Shaft power consumption: W = f(SST, SDT)  [W]
    pub power_curve: Polynomial2D,

    /// Economizer mass flow fraction: x_eco = f(SST, SDT)  [dimensionless, 0–1]
    ///
    /// x_eco = ṁ_eco / ṁ_suction
    /// Typically 0.05–0.20, increasing with pressure ratio.
    /// Set all coefficients to 0.0 and constant term to desired fraction
    /// for a fixed economizer fraction.
    pub eco_flow_fraction_curve: Polynomial2D,
}

impl ScrewPerformanceCurves {
    /// Creates curves with a constant economizer fraction (simple model).
    ///
    /// # Arguments
    /// * `mass_flow_curve`   — Polynomial2D for ṁ_suction = f(SST, SDT)
    /// * `power_curve`       — Polynomial2D for W = f(SST, SDT)
    /// * `eco_fraction`      — Fixed economizer mass flow fraction (0.05–0.20 typical)
    pub fn with_fixed_eco_fraction(
        mass_flow_curve: Polynomial2D,
        power_curve: Polynomial2D,
        eco_fraction: f64,
    ) -> Self {
        // Constant polynomial: f(x,y) = eco_fraction
        let eco_curve = Polynomial2D::bilinear(eco_fraction, 0.0, 0.0, 0.0);
        Self {
            mass_flow_curve,
            power_curve,
            eco_flow_fraction_curve: eco_curve,
        }
    }

    /// Validates that all curve coefficients are finite.
    pub fn validate(&self) -> Result<(), ComponentError> {
        self.mass_flow_curve.validate()?;
        self.power_curve.validate()?;
        self.eco_flow_fraction_curve.validate()?;
        Ok(())
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// ScrewEconomizerCompressor
// ─────────────────────────────────────────────────────────────────────────────

/// Screw compressor with intermediate economizer injection port.
///
/// **Ports (3 total):**
/// - `port_suction` (index 0): Low-pressure inlet
/// - `port_discharge` (index 1): High-pressure outlet  
/// - `port_economizer` (index 2): Intermediate-pressure injection inlet
///
/// **State variables (5 total):**
/// - `state[0]`: ṁ_suction   (kg/s)
/// - `state[1]`: ṁ_eco       (kg/s)
/// - `state[2]`: h_suction    (J/kg)
/// - `state[3]`: h_discharge  (J/kg)
/// - `state[4]`: W_shaft      (W)
///
/// **Equations (5 total):**
/// 1. Mass flow suction: ṁ_suc_calc − ṁ_suc_state = 0
/// 2. Economizer mass flow: ṁ_eco_calc − ṁ_eco_state = 0
/// 3. Energy balance: ṁ_suc×h_suc + ṁ_eco×h_eco + W = ṁ_total×h_dis
/// 4. Economizer pressure: P_eco − P_eco_nominal = 0  (geometric mean)
/// 5. Power: W_calc − W_state = 0
pub struct ScrewEconomizerCompressor {
    /// Performance curves
    curves: ScrewPerformanceCurves,
    /// Refrigerant fluid identifier (e.g., "R134a", "R410A", "R1234ze(E)")
    fluid_id: String,
    /// Nominal frequency [Hz] — typically 50 Hz at full speed
    nominal_frequency_hz: f64,
    /// Current operating frequency [Hz] (VFD controlled, 25–60 Hz)
    frequency_hz: f64,
    /// Mechanical efficiency (0.0–1.0)
    mechanical_efficiency: f64,
    /// Circuit identifier for multi-circuit topology
    circuit_id: CircuitId,
    /// Operational state: On / Off / Bypass
    operational_state: OperationalState,
    /// Calibration factors (f_m, f_power)
    calib: Calib,
    /// Calibration state vector indices (injected by solver)
    calib_indices: CalibIndices,
    /// Suction port — low-pressure inlet
    port_suction: ConnectedPort,
    /// Discharge port — high-pressure outlet
    port_discharge: ConnectedPort,
    /// Economizer injection port — intermediate pressure
    port_economizer: ConnectedPort,
    /// Offset of this component's internal state block in the global state vector.
    /// Set by `System::finalize()` via `set_system_context()`.
    /// The 5 internal variables at `state[offset..offset+5]` are:
    ///   [ṁ_suc, ṁ_eco, h_suc, h_dis, W_shaft]
    global_state_offset: usize,
}

impl std::fmt::Debug for ScrewEconomizerCompressor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ScrewEconomizerCompressor")
            .field("fluid_id", &self.fluid_id)
            .field("frequency_hz", &self.frequency_hz)
            .field("mechanical_efficiency", &self.mechanical_efficiency)
            .field("operational_state", &self.operational_state)
            .finish()
    }
}

impl ScrewEconomizerCompressor {
    /// Creates a new screw compressor with economizer port.
    ///
    /// # Arguments
    ///
    /// * `curves`                 — SST/SDT polynomial performance curves
    /// * `fluid_id`               — Refrigerant name (CoolProp compatible, e.g. `"R134a"`)
    /// * `nominal_frequency_hz`   — Full-speed frequency (usually 50 Hz in Europe)
    /// * `mechanical_efficiency`  — Shaft-to-fluid mechanical efficiency (0.0–1.0)
    /// * `port_suction`           — Connected suction port
    /// * `port_discharge`         — Connected discharge port
    /// * `port_economizer`        — Connected economizer injection port
    ///
    /// # Errors
    ///
    /// Returns [`ComponentError::InvalidState`] if:
    /// - Curves are invalid (NaN, infinite)
    /// - Frequency ≤ 0
    /// - Mechanical efficiency outside (0, 1]
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        curves: ScrewPerformanceCurves,
        fluid_id: impl Into<String>,
        nominal_frequency_hz: f64,
        mechanical_efficiency: f64,
        port_suction: ConnectedPort,
        port_discharge: ConnectedPort,
        port_economizer: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        curves.validate()?;

        if nominal_frequency_hz <= 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: nominal frequency must be positive".into(),
            ));
        }
        if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: mechanical_efficiency must be in (0, 1]".into(),
            ));
        }

        Ok(Self {
            curves,
            fluid_id: fluid_id.into(),
            nominal_frequency_hz,
            frequency_hz: nominal_frequency_hz,
            mechanical_efficiency,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::On,
            calib: Calib::default(),
            calib_indices: CalibIndices::default(),
            port_suction,
            port_discharge,
            port_economizer,
            global_state_offset: 0,
        })
    }

    // ─── Accessors ────────────────────────────────────────────────────────────

    /// Returns the refrigerant fluid identifier.
    pub fn fluid_id(&self) -> &str {
        &self.fluid_id
    }

    /// Returns the current operating frequency [Hz].
    pub fn frequency_hz(&self) -> f64 {
        self.frequency_hz
    }

    /// Sets the operating frequency [Hz] (VFD control).
    ///
    /// Allowed range: [0.0, nominal_frequency_hz × 1.1]
    /// Setting to 0.0 effectively stops the compressor (but use `set_state(Off)` instead).
    pub fn set_frequency_hz(&mut self, freq: f64) -> Result<(), ComponentError> {
        if freq < 0.0 {
            return Err(ComponentError::InvalidState(
                "ScrewEconomizerCompressor: frequency cannot be negative".into(),
            ));
        }
        let max_freq = self.nominal_frequency_hz * 1.1; // allow 10% overspeed
        if freq > max_freq {
            return Err(ComponentError::InvalidState(format!(
                "ScrewEconomizerCompressor: frequency {:.1} Hz exceeds maximum {:.1} Hz",
                freq, max_freq
            )));
        }
        self.frequency_hz = freq;
        Ok(())
    }

    /// Returns the frequency ratio (current / nominal).
    ///
    /// Used internally to scale volumetric performance.
    pub fn frequency_ratio(&self) -> f64 {
        if self.nominal_frequency_hz > 0.0 {
            self.frequency_hz / self.nominal_frequency_hz
        } else {
            1.0
        }
    }

    /// Returns the nominal frequency [Hz].
    pub fn nominal_frequency_hz(&self) -> f64 {
        self.nominal_frequency_hz
    }

    /// Returns mechanical efficiency.
    pub fn mechanical_efficiency(&self) -> f64 {
        self.mechanical_efficiency
    }

    /// Returns calibration factors.
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

    /// Sets calibration factors.
    pub fn set_calib(&mut self, calib: Calib) {
        self.calib = calib;
    }

    /// Returns reference to suction port.
    pub fn port_suction(&self) -> &ConnectedPort {
        &self.port_suction
    }

    /// Returns reference to discharge port.
    pub fn port_discharge(&self) -> &ConnectedPort {
        &self.port_discharge
    }

    /// Returns reference to economizer injection port.
    pub fn port_economizer(&self) -> &ConnectedPort {
        &self.port_economizer
    }

    // ─── Internal calculations ────────────────────────────────────────────────

    /// Extracts saturated temperatures from port pressures via Clausius-Clapeyron
    /// approximation (placeholder until CoolProp integration is wired).
    ///
    /// For the SST/SDT model these only need to be approximately correct.
    fn estimate_sst_sdt_k(&self) -> (f64, f64) {
        let p_suc_pa = self.port_suction.pressure().to_pascals();
        let p_dis_pa = self.port_discharge.pressure().to_pascals();

        // Simple Clausius-Clapeyron approximation for R134a family refrigerants:
        // T_sat [K] ≈ T_ref / (1 - (R*T_ref/h_vap) * ln(P/P_ref))
        // Using linearised form: T_sat ≈ A + B * ln(P)
        // These constants are approximate for R134a around typical chiller conditions.
        // They will be overridden by CoolProp when the full fluid backend is wired.
        const A: f64 = -47.0 + 273.15; // K
        const B: f64 = 22.0; // K / ln-unit

        let sst_k = (A + B * (p_suc_pa / 1e5_f64).ln()).max(230.0).min(320.0);
        let sdt_k = (A + B * (p_dis_pa / 1e5_f64).ln()).max(280.0).min(360.0);
        (sst_k, sdt_k)
    }

    /// Computes nominal suction mass flow [kg/s] at current SST/SDT, scaled by VFD.
    fn calc_mass_flow_suction(&self, sst_k: f64, sdt_k: f64, state: Option<&StateSlice>) -> f64 {
        let m_nominal = self.curves.mass_flow_curve.evaluate(sst_k, sdt_k);

        // VFD scaling: ṁ ∝ frequency (volumetric machine)
        let m_scaled = m_nominal * self.frequency_ratio();

        // Calibration factor f_m
        let f_m = if let Some(st) = state {
            self.calib_indices
                .f_m
                .map(|idx| st[idx])
                .unwrap_or(self.calib.f_m)
        } else {
            self.calib.f_m
        };

        m_scaled * f_m
    }

    /// Computes economizer mass flow fraction at current SST/SDT.
    fn calc_eco_fraction(&self, sst_k: f64, sdt_k: f64) -> f64 {
        self.curves
            .eco_flow_fraction_curve
            .evaluate(sst_k, sdt_k)
            .clamp(0.0, 0.4) // physical bound: eco fraction 0–40%
    }

    /// Computes nominal shaft power [W] scaled by VFD and calibration.
    fn calc_power(&self, sst_k: f64, sdt_k: f64, state: Option<&StateSlice>) -> f64 {
        let w_nominal = self.curves.power_curve.evaluate(sst_k, sdt_k);

        // VFD scaling: power ∝ frequency (approximately, for screw compressors)
        let w_scaled = w_nominal * self.frequency_ratio();

        // Calibration factor f_power
        let f_power = if let Some(st) = state {
            self.calib_indices
                .f_power
                .map(|idx| st[idx])
                .unwrap_or(self.calib.f_power)
        } else {
            self.calib.f_power
        };

        w_scaled * f_power
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Component trait implementation
// ─────────────────────────────────────────────────────────────────────────────

impl Component for ScrewEconomizerCompressor {
    /// Returns 5 equations:
    /// 1. Suction mass flow balance
    /// 2. Economizer mass flow balance
    /// 3. Energy balance (1st law)
    /// 4. Economizer pressure constraint
    /// 5. Shaft power balance
    fn n_equations(&self) -> usize {
        5
    }

    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(),
            });
        }

        // ── Operational state shortcuts ──────────────────────────────────────
        let off = self.global_state_offset;

        match self.operational_state {
            OperationalState::Off => {
                // Zero flow, zero power
                residuals[0] = state.get(off).copied().unwrap_or(0.0); // ṁ_suc = 0
                residuals[1] = state.get(off + 1).copied().unwrap_or(0.0); // ṁ_eco = 0
                residuals[2] = 0.0;
                residuals[3] = 0.0;
                residuals[4] = state.get(off + 2).copied().unwrap_or(0.0); // W = 0
                return Ok(());
            }
            OperationalState::Bypass => {
                // Adiabatic pass-through: P_dis = P_suc, h_dis = h_suc, no eco flow
                let p_suc = self.port_suction.pressure().to_pascals();
                let p_dis = self.port_discharge.pressure().to_pascals();
                let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
                let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
                residuals[0] = p_suc - p_dis;
                residuals[1] = h_suc - h_dis;
                residuals[2] = state.get(off + 1).copied().unwrap_or(0.0); // ṁ_eco = 0
                residuals[3] = 0.0;
                residuals[4] = state.get(off + 2).copied().unwrap_or(0.0); // W = 0
                return Ok(());
            }
            OperationalState::On => {}
        }

        // ── Validate state vector ────────────────────────────────────────────
        if state.len() < off + 3 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: off + 3,
                actual: state.len(),
            });
        }

        let m_suc_state = state[off]; // kg/s — solver variable
        let m_eco_state = state[off + 1]; // kg/s — solver variable
        let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
        let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
        let h_eco = self.port_economizer.enthalpy().to_joules_per_kg();
        let w_state = state[off + 2]; // W   — solver variable

        // ── Compute performance from curves ──────────────────────────────────
        let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
        let m_suc_calc = self.calc_mass_flow_suction(sst_k, sdt_k, Some(state));
        let x_eco = self.calc_eco_fraction(sst_k, sdt_k);
        let m_eco_calc = m_suc_calc * x_eco;
        let w_calc = self.calc_power(sst_k, sdt_k, Some(state));

        // ── Residual 0: Suction mass flow ────────────────────────────────────
        // r₀ = ṁ_suc_calc − ṁ_suc_state = 0
        residuals[0] = m_suc_calc - m_suc_state;

        // ── Residual 1: Economizer mass flow ─────────────────────────────────
        // r₁ = ṁ_eco_calc − ṁ_eco_state = 0
        residuals[1] = m_eco_calc - m_eco_state;

        // ── Residual 2: First-law energy balance ─────────────────────────────
        // ṁ_suc × h_suc + ṁ_eco × h_eco + W = ṁ_total × h_dis
        // r₂ = (ṁ_suc × h_suc + ṁ_eco × h_eco + W) − ṁ_total × h_dis = 0
        //
        // Note: W is the shaft power delivered TO the fluid (positive = power in).
        // Mechanical efficiency accounts for friction losses in bearings/seals.
        let energy_in =
            m_suc_state * h_suc + m_eco_state * h_eco + w_state / self.mechanical_efficiency;
        let energy_out = (m_suc_state + m_eco_state) * h_dis;
        residuals[2] = energy_in - energy_out;

        // ── Residual 3: Economizer pressure (geometric mean) ─────────────────
        // The economizer injection pressure is typically the geometric mean of
        // suction and discharge pressures for optimal performance.
        // P_eco_set = sqrt(P_suc × P_dis)
        // r₃ = P_eco_port − P_eco_set = 0
        let p_suc = self.port_suction.pressure().to_pascals();
        let p_dis = self.port_discharge.pressure().to_pascals();
        let p_eco_port = self.port_economizer.pressure().to_pascals();
        let p_eco_set = (p_suc * p_dis).sqrt();
        // Scale residual to Pa (same order of magnitude as pressures in system)
        residuals[3] = p_eco_port - p_eco_set;

        // ── Residual 4: Shaft power ──────────────────────────────────────────
        // r₄ = W_calc − W_state = 0
        residuals[4] = w_calc - w_state;

        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let off = self.global_state_offset;

        if state.len() < off + 3 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: off + 3,
                actual: state.len(),
            });
        }

        let m_suc_state = state[off];
        let m_eco_state = state[off + 1];
        let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
        let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
        let h_eco = self.port_economizer.enthalpy().to_joules_per_kg();

        // Row 0: ∂r₀/∂ṁ_suc = -1
        jacobian.add_entry(0, off, -1.0);

        // Row 1: ∂r₁/∂ṁ_eco = -1
        jacobian.add_entry(1, off + 1, -1.0);

        // Row 2: energy balance partial derivatives
        // ∂r₂/∂ṁ_suc = h_suc − h_dis
        jacobian.add_entry(2, off, h_suc - h_dis);
        // ∂r₂/∂ṁ_eco = h_eco − h_dis
        jacobian.add_entry(2, off + 1, h_eco - h_dis);
        // ∂r₂/∂W = 1 / η_mech
        jacobian.add_entry(2, off + 2, 1.0 / self.mechanical_efficiency);

        // Row 3: economizer pressure — no dependency on mass flows or power
        // (depends on port pressures which are graph state, not component state)
        jacobian.add_entry(3, off, 0.0);

        // Row 4: ∂r₄/∂W_state = -1
        jacobian.add_entry(4, off + 2, -1.0);

        // Calibration Jacobian entries
        if let Some(f_m_idx) = self.calib_indices.f_m {
            let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
            let m_nominal =
                self.curves.mass_flow_curve.evaluate(sst_k, sdt_k) * self.frequency_ratio();
            jacobian.add_entry(0, f_m_idx, m_nominal);
            // eco also depends on f_m via the fraction
            let x_eco = self.calc_eco_fraction(sst_k, sdt_k);
            jacobian.add_entry(1, f_m_idx, m_nominal * x_eco);
        }
        if let Some(f_power_idx) = self.calib_indices.f_power {
            let (sst_k, sdt_k) = self.estimate_sst_sdt_k();
            let w_nominal = self.curves.power_curve.evaluate(sst_k, sdt_k) * self.frequency_ratio();
            jacobian.add_entry(4, f_power_idx, w_nominal);
        }

        // Suppress unused variable warnings for state variables used only for
        // context (not in simplified Jacobian above)
        let _ = (m_suc_state, m_eco_state);

        Ok(())
    }

    fn get_ports(&self) -> &[ConnectedPort] {
        // Return empty slice — ports are accessed via dedicated methods.
        // Full port slice would require lifetime-coupled storage; use
        // port_suction(), port_discharge(), port_economizer() accessors instead.
        &[]
    }

    fn internal_state_len(&self) -> usize {
        // 3 internal variables: [ṁ_suc, ṁ_eco, W_shaft]
        3
    }

    fn set_system_context(
        &mut self,
        state_offset: usize,
        _external_edge_state_indices: &[(usize, usize)],
    ) {
        self.global_state_offset = state_offset;
    }

    fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        let off = self.global_state_offset;
        if state.len() < off + 2 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: off + 2,
                actual: state.len(),
            });
        }
        let m_suc = MassFlow::from_kg_per_s(state[off]);
        let m_eco = MassFlow::from_kg_per_s(state[off + 1]);
        let m_total = MassFlow::from_kg_per_s(state[off] + state[off + 1]);
        Ok(vec![
            m_suc,                                           // suction in (+)
            MassFlow::from_kg_per_s(-m_total.to_kg_per_s()), // discharge out (-)
            m_eco,                                           // economizer in (+)
        ])
    }

    fn energy_transfers(&self, state: &StateSlice) -> Option<(Power, Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass => {
                Some((Power::from_watts(0.0), Power::from_watts(0.0)))
            }
            OperationalState::On => {
                let off = self.global_state_offset;
                if state.len() < off + 3 {
                    return None;
                }
                let w = state[off + 2]; // shaft power W
                                  // Work done ON the compressor → negative sign convention
                Some((Power::from_watts(0.0), Power::from_watts(-w)))
            }
        }
    }

    fn set_calib_indices(&mut self, indices: CalibIndices) {
        self.calib_indices = indices;
    }

    fn signature(&self) -> String {
        format!(
            "ScrewEconomizerCompressor(fluid={},freq={:.0}Hz,eta_mech={:.2})",
            self.fluid_id, self.frequency_hz, self.mechanical_efficiency
        )
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// StateManageable
// ─────────────────────────────────────────────────────────────────────────────

impl StateManageable for ScrewEconomizerCompressor {
    fn state(&self) -> OperationalState {
        self.operational_state
    }

    fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
        if self.operational_state.can_transition_to(state) {
            let from = self.operational_state;
            self.operational_state = state;
            self.on_state_change(from, state);
            Ok(())
        } else {
            Err(ComponentError::InvalidStateTransition {
                from: self.operational_state,
                to: state,
                reason: "Transition not allowed for ScrewEconomizerCompressor".into(),
            })
        }
    }

    fn can_transition_to(&self, target: OperationalState) -> bool {
        self.operational_state.can_transition_to(target)
    }

    fn circuit_id(&self) -> &CircuitId {
        &self.circuit_id
    }

    fn set_circuit_id(&mut self, id: CircuitId) {
        self.circuit_id = id;
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::port::{FluidId, Port};
    use entropyk_core::{Enthalpy, Pressure};

    fn make_port(fluid: &str, p_bar: f64, h_kj_kg: f64) -> ConnectedPort {
        let a = Port::new(
            FluidId::new(fluid),
            Pressure::from_bar(p_bar),
            Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
        );
        let b = Port::new(
            FluidId::new(fluid),
            Pressure::from_bar(p_bar),
            Enthalpy::from_joules_per_kg(h_kj_kg * 1000.0),
        );
        a.connect(b).unwrap().0
    }

    fn default_curves() -> ScrewPerformanceCurves {
        // Simple bilinear model approximating a 200 kW R134a screw at 50 Hz
        // SST ≈ 276 K (+3°C), SDT ≈ 323 K (+50°C)
        ScrewPerformanceCurves::with_fixed_eco_fraction(
            Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01), // ṁ_suc kg/s
            Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5), // W shaft
            0.12,                                                 // 12% economizer fraction
        )
    }

    fn make_compressor() -> ScrewEconomizerCompressor {
        let suc = make_port("R134a", 3.2, 400.0); // ~276 K sat, superheated
        let dis = make_port("R134a", 12.8, 440.0); // ~323 K sat
        let eco = make_port("R134a", 6.4, 260.0); // ~300 K sat, flash gas

        ScrewEconomizerCompressor::new(default_curves(), "R134a", 50.0, 0.92, suc, dis, eco)
            .expect("valid compressor")
    }

    #[test]
    fn test_creation_succeeds() {
        let comp = make_compressor();
        assert_eq!(comp.fluid_id(), "R134a");
        assert_eq!(comp.nominal_frequency_hz(), 50.0);
        assert_eq!(comp.frequency_hz(), 50.0);
        assert!((comp.mechanical_efficiency() - 0.92).abs() < 1e-10);
    }

    #[test]
    fn test_n_equations() {
        let comp = make_compressor();
        assert_eq!(comp.n_equations(), 5);
    }

    #[test]
    fn test_frequency_ratio_at_nominal() {
        let comp = make_compressor();
        assert!((comp.frequency_ratio() - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_set_frequency_vfd() {
        let mut comp = make_compressor();
        comp.set_frequency_hz(40.0).unwrap();
        assert_eq!(comp.frequency_hz(), 40.0);
        assert!((comp.frequency_ratio() - 0.8).abs() < 1e-10);
    }

    #[test]
    fn test_frequency_out_of_range_rejected() {
        let mut comp = make_compressor();
        // nominal = 50 Hz → max = 50 × 1.1 = 55 Hz
        assert!(comp.set_frequency_hz(-1.0).is_err());
        assert!(comp.set_frequency_hz(55.0).is_ok()); // exactly at max: ok
        assert!(comp.set_frequency_hz(55.1).is_err()); // above max: rejected
        assert!(comp.set_frequency_hz(100.0).is_err());
    }

    #[test]
    fn test_compute_residuals_ok_with_reasonable_state() {
        let comp = make_compressor();
        // Provide a reasonable state vector: [ṁ_suc, ṁ_eco, W]
        // Values are approximate — residuals won't be zero but must be finite
        let state = vec![1.0, 0.12, 50_000.0];
        let mut residuals = vec![0.0; 5];
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(
            result.is_ok(),
            "compute_residuals failed: {:?}",
            result.err()
        );
        for (i, r) in residuals.iter().enumerate() {
            assert!(r.is_finite(), "residual[{}] = {} is not finite", i, r);
        }
    }

    #[test]
    fn test_compute_residuals_off_state() {
        let mut comp = make_compressor();
        comp.set_state(OperationalState::Off).unwrap();
        let state = vec![0.0; 5];
        let mut residuals = vec![0.0; 5];
        comp.compute_residuals(&state, &mut residuals).unwrap();
        // In Off state, all residuals should be zero
        for r in &residuals {
            assert!(r.abs() < 1e-10);
        }
    }

    #[test]
    fn test_compute_residuals_wrong_size() {
        let comp = make_compressor();
        let state = vec![1.0; 5];
        let mut residuals = vec![0.0; 3]; // wrong size
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(matches!(
            result,
            Err(ComponentError::InvalidResidualDimensions { .. })
        ));
    }

    #[test]
    fn test_port_mass_flows() {
        let comp = make_compressor();
        let state = vec![1.0, 0.12, 0.0, 0.0, 0.0];
        let flows = comp.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 3);
        // suction in, total discharge out, eco in
        assert!((flows[0].to_kg_per_s() - 1.0).abs() < 1e-10);
        assert!((flows[1].to_kg_per_s() + 1.12).abs() < 1e-10); // negative (out)
        assert!((flows[2].to_kg_per_s() - 0.12).abs() < 1e-10);
    }

    #[test]
    fn test_energy_transfers_on() {
        let comp = make_compressor();
        let state = vec![1.0, 0.12, 55_000.0];
        let (q, w) = comp.energy_transfers(&state).unwrap();
        assert_eq!(q.to_watts(), 0.0);
        assert!((w.to_watts() + 55_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_energy_transfers_off() {
        let mut comp = make_compressor();
        comp.set_state(OperationalState::Off).unwrap();
        let state = vec![0.0; 5];
        let (q, w) = comp.energy_transfers(&state).unwrap();
        assert_eq!(q.to_watts(), 0.0);
        assert_eq!(w.to_watts(), 0.0);
    }

    #[test]
    fn test_jacobian_entries_ok() {
        let comp = make_compressor();
        let state = vec![1.0, 0.12, 400_000.0, 440_000.0, 55_000.0];
        let mut jac = JacobianBuilder::new();
        let result = comp.jacobian_entries(&state, &mut jac);
        assert!(result.is_ok());
        assert!(!jac.is_empty());
    }

    #[test]
    fn test_signature() {
        let comp = make_compressor();
        let sig = comp.signature();
        assert!(sig.contains("ScrewEconomizerCompressor"));
        assert!(sig.contains("R134a"));
    }

    #[test]
    fn test_curves_with_fixed_eco_fraction() {
        let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
            Polynomial2D::bilinear(1.0, 0.0, 0.0, 0.0),
            Polynomial2D::bilinear(50_000.0, 0.0, 0.0, 0.0),
            0.15,
        );
        // eco fraction should be 0.15 regardless of SST/SDT
        let frac = curves.eco_flow_fraction_curve.evaluate(270.0, 320.0);
        assert!((frac - 0.15).abs() < 1e-10);
    }

    #[test]
    fn test_invalid_mechanical_efficiency() {
        let suc = make_port("R134a", 3.2, 400.0);
        let dis = make_port("R134a", 12.8, 440.0);
        let eco = make_port("R134a", 6.4, 260.0);

        let result = ScrewEconomizerCompressor::new(
            ScrewPerformanceCurves::with_fixed_eco_fraction(
                Polynomial2D::bilinear(1.0, 0.0, 0.0, 0.0),
                Polynomial2D::bilinear(50_000.0, 0.0, 0.0, 0.0),
                0.12,
            ),
            "R134a",
            50.0,
            1.5, // invalid
            suc,
            dis,
            eco,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_internal_state_len() {
        let comp = make_compressor();
        assert_eq!(comp.internal_state_len(), 3);
    }

    #[test]
    fn test_set_system_context_stores_offset() {
        let mut comp = make_compressor();
        assert_eq!(comp.global_state_offset, 0);
        comp.set_system_context(10, &[]);
        assert_eq!(comp.global_state_offset, 10);
    }

    #[test]
    fn test_compute_residuals_with_offset() {
        let mut comp = make_compressor();
        // Place internal state at offset 6 (simulating 3 edges × 2 vars = 6)
        comp.set_system_context(6, &[]);

        // Build state: 6 edge vars (zeros) + 3 internal vars
        let mut state = vec![0.0; 9];
        state[6] = 1.0;       // ṁ_suc at offset+0
        state[7] = 0.12;      // ṁ_eco at offset+1
        state[8] = 50_000.0; // W     at offset+2

        let mut residuals = vec![0.0; 5];
        let result = comp.compute_residuals(&state, &mut residuals);
        assert!(result.is_ok(), "compute_residuals failed: {:?}", result.err());
        for (i, r) in residuals.iter().enumerate() {
            assert!(r.is_finite(), "residual[{}] = {} is not finite", i, r);
        }
    }

    #[test]
    fn test_jacobian_entries_with_offset() {
        let mut comp = make_compressor();
        comp.set_system_context(6, &[]);

        let mut state = vec![0.0; 9];
        state[6] = 1.0;
        state[7] = 0.12;
        state[8] = 55_000.0;

        let mut jac = JacobianBuilder::new();
        let result = comp.jacobian_entries(&state, &mut jac);
        assert!(result.is_ok());

        // All column indices in the Jacobian should be >= 6 (the offset)
        for &(_, col, _) in jac.entries() {
            assert!(col >= 6, "Jacobian column {} should be >= offset 6", col);
        }
    }

    #[test]
    fn test_port_mass_flows_with_offset() {
        let mut comp = make_compressor();
        comp.set_system_context(4, &[]);

        let mut state = vec![0.0; 7];
        state[4] = 1.0;  // ṁ_suc at offset+0
        state[5] = 0.12; // ṁ_eco at offset+1
        state[6] = 0.0;  // W at offset+2

        let flows = comp.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 3);
        assert!((flows[0].to_kg_per_s() - 1.0).abs() < 1e-10);
        assert!((flows[1].to_kg_per_s() + 1.12).abs() < 1e-10);
        assert!((flows[2].to_kg_per_s() - 0.12).abs() < 1e-10);
    }

    #[test]
    fn test_energy_transfers_with_offset() {
        let mut comp = make_compressor();
        comp.set_system_context(4, &[]);
        let mut state = vec![0.0; 7];
        state[4] = 1.0;
        state[5] = 0.12;
        state[6] = 55_000.0;
        comp.set_state(OperationalState::On).unwrap();
        let transfers = comp.energy_transfers(&state).unwrap();
        let w = transfers.1;
        assert!((w.to_watts() + 55_000.0).abs() < 1e-6);
    }
}
