//! Centrifugal compressor with a normalized polytropic performance map.
//!
//! Independent variables: flow coefficient `φ = Q/(N D³)` and machine Mach
//! number `M_u = U/√(γ R T)`. Dependent: polytropic head coefficient `μ_p`
//! and polytropic efficiency `η_p`. VFD operation re-evaluates the map at the
//! new tip speed / Mach number.

use crate::port::{Connected, Disconnected, Port};
use crate::{
    CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
    ResidualVector, StateSlice,
};
use entropyk_core::Power;
use std::marker::PhantomData;

/// Single map point `(φ, M_u) → (μ_p, η_p)`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CentrifugalMapPoint {
    /// Flow coefficient φ [-].
    pub phi: f64,
    /// Machine Mach number M_u [-].
    pub mach: f64,
    /// Polytropic head coefficient μ_p [-].
    pub mu_p: f64,
    /// Polytropic efficiency η_p [-].
    pub eta_p: f64,
}

/// Bilinear performance map on a structured `(φ, M_u)` grid.
#[derive(Debug, Clone, PartialEq)]
pub struct CentrifugalMap {
    points: Vec<CentrifugalMapPoint>,
}

impl CentrifugalMap {
    /// Creates a map from unsorted points (must cover a rectangle in φ–Mach).
    pub fn new(points: Vec<CentrifugalMapPoint>) -> Result<Self, ComponentError> {
        if points.len() < 4 {
            return Err(ComponentError::InvalidState(
                "CentrifugalMap needs at least 4 points".into(),
            ));
        }
        if points
            .iter()
            .any(|p| !p.phi.is_finite() || !p.mach.is_finite() || p.eta_p <= 0.0)
        {
            return Err(ComponentError::InvalidState(
                "CentrifugalMap points must be finite with positive eta".into(),
            ));
        }
        Ok(Self { points })
    }

    /// Default demo map around φ∈[0.02,0.08], M_u∈[0.6,1.2].
    pub fn default_chiller_map() -> Self {
        Self::new(vec![
            CentrifugalMapPoint {
                phi: 0.02,
                mach: 0.6,
                mu_p: 0.55,
                eta_p: 0.78,
            },
            CentrifugalMapPoint {
                phi: 0.08,
                mach: 0.6,
                mu_p: 0.48,
                eta_p: 0.80,
            },
            CentrifugalMapPoint {
                phi: 0.02,
                mach: 1.2,
                mu_p: 0.62,
                eta_p: 0.76,
            },
            CentrifugalMapPoint {
                phi: 0.08,
                mach: 1.2,
                mu_p: 0.52,
                eta_p: 0.79,
            },
        ])
        .expect("default map")
    }

    /// Inverse-distance weighted interpolation of (μ_p, η_p).
    pub fn interpolate(&self, phi: f64, mach: f64) -> (f64, f64) {
        let mut w_sum = 0.0;
        let mut mu = 0.0;
        let mut eta = 0.0;
        for p in &self.points {
            let d2 = (p.phi - phi).powi(2) + (p.mach - mach).powi(2);
            let w = 1.0 / d2.max(1e-12);
            w_sum += w;
            mu += w * p.mu_p;
            eta += w * p.eta_p;
        }
        (mu / w_sum, (eta / w_sum).clamp(0.2, 0.95))
    }
}

/// Centrifugal compressor component (2-port).
#[derive(Debug, Clone)]
pub struct CentrifugalCompressor<State> {
    map: CentrifugalMap,
    /// Impeller tip diameter [m].
    diameter_m: f64,
    /// Rotational speed [rpm].
    speed_rpm: f64,
    /// Nominal speed [rpm] for VFD ratio.
    nominal_speed_rpm: f64,
    /// Specific gas constant R [J/(kg·K)].
    gas_constant: f64,
    /// Heat capacity ratio γ [-].
    gamma: f64,
    port_inlet: Port<State>,
    port_outlet: Port<State>,
    operational_state: OperationalState,
    circuit_id: CircuitId,
    _state: PhantomData<State>,
}

impl CentrifugalCompressor<Disconnected> {
    /// Creates a disconnected centrifugal compressor.
    pub fn new(
        map: CentrifugalMap,
        diameter_m: f64,
        speed_rpm: f64,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
    ) -> Result<Self, ComponentError> {
        if diameter_m <= 0.0 || speed_rpm <= 0.0 {
            return Err(ComponentError::InvalidState(
                "diameter and speed must be positive".into(),
            ));
        }
        Ok(Self {
            map,
            diameter_m,
            speed_rpm,
            nominal_speed_rpm: speed_rpm,
            gas_constant: 188.9, // R134a approx
            gamma: 1.12,
            port_inlet,
            port_outlet,
            operational_state: OperationalState::On,
            circuit_id: CircuitId::default(),
            _state: PhantomData,
        })
    }

    /// Sets gas properties for Mach / head evaluation.
    pub fn with_gas(mut self, r_j_kg_k: f64, gamma: f64) -> Self {
        self.gas_constant = r_j_kg_k.max(50.0);
        self.gamma = gamma.clamp(1.05, 1.4);
        self
    }

    /// Connects ports.
    pub fn connect(
        self,
        inlet: Port<Disconnected>,
        outlet: Port<Disconnected>,
    ) -> Result<CentrifugalCompressor<Connected>, ComponentError> {
        let (p_in, _) = self
            .port_inlet
            .connect(inlet)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;
        let (p_out, _) = self
            .port_outlet
            .connect(outlet)
            .map_err(|e| ComponentError::InvalidState(e.to_string()))?;
        Ok(CentrifugalCompressor {
            map: self.map,
            diameter_m: self.diameter_m,
            speed_rpm: self.speed_rpm,
            nominal_speed_rpm: self.nominal_speed_rpm,
            gas_constant: self.gas_constant,
            gamma: self.gamma,
            port_inlet: p_in,
            port_outlet: p_out,
            operational_state: self.operational_state,
            circuit_id: self.circuit_id,
            _state: PhantomData,
        })
    }
}

impl CentrifugalCompressor<Connected> {
    /// Tip speed U = π N D [m/s] (N in rev/s).
    pub fn tip_speed(&self) -> f64 {
        let n_rps = self.speed_rpm / 60.0;
        std::f64::consts::PI * n_rps * self.diameter_m
    }

    /// Sets VFD speed [rpm].
    pub fn set_speed_rpm(&mut self, rpm: f64) -> Result<(), ComponentError> {
        if rpm <= 0.0 {
            return Err(ComponentError::InvalidState(
                "speed must be positive".into(),
            ));
        }
        self.speed_rpm = rpm;
        Ok(())
    }

    /// Evaluates map at suction conditions; returns (head [J/kg], η_p, power [W]).
    pub fn rate(
        &self,
        t_suction_k: f64,
        rho_suction: f64,
        volume_flow_m3_s: f64,
    ) -> Result<(f64, f64, f64), ComponentError> {
        let u = self.tip_speed();
        let a = (self.gamma * self.gas_constant * t_suction_k.max(200.0)).sqrt();
        let mach = u / a.max(1.0);
        let n_rps = self.speed_rpm / 60.0;
        let phi = volume_flow_m3_s / (n_rps * self.diameter_m.powi(3)).max(1e-12);
        let (mu_p, eta_p) = self.map.interpolate(phi, mach);
        let head = mu_p * u * u; // J/kg
        let m_dot = volume_flow_m3_s * rho_suction.max(0.1);
        let power = m_dot * head / eta_p.max(0.2);
        Ok((head, eta_p, power))
    }
}

impl Component for CentrifugalCompressor<Connected> {
    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if state.len() < 2 {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 2,
                actual: state.len(),
            });
        }
        if residuals.len() < 2 {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: 2,
                actual: residuals.len(),
            });
        }
        // r0: mass continuity ṁ_out − ṁ_in = 0
        // r1: isentropic-like enthalpy rise placeholder using map head
        let m_in = state[0];
        let m_out = state[1];
        residuals[0] = m_out - m_in;
        let rho = 20.0; // fallback when edge density unavailable
        let vol = m_in.abs() / rho;
        let (_, _, power) = self.rate(280.0, rho, vol)?;
        let dh = if m_in.abs() > 1e-9 {
            power / m_in.abs()
        } else {
            0.0
        };
        // Enthalpy rise residual uses port enthalpies when available via state[2]/3]
        if state.len() >= 4 {
            residuals[1] = (state[3] - state[2]) - dh;
        } else {
            residuals[1] = 0.0;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        jacobian.add_entry(0, 0, -1.0);
        jacobian.add_entry(0, 1, 1.0);
        Ok(())
    }

    fn n_equations(&self) -> usize {
        2
    }

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

    fn signature(&self) -> String {
        format!(
            "CentrifugalCompressor(D={:.3}m, N={:.0}rpm)",
            self.diameter_m, self.speed_rpm
        )
    }

    fn energy_transfers(&self, state: &StateSlice) -> Option<(Power, Power)> {
        let m = state.first().copied().unwrap_or(0.0);
        let vol = m.abs() / 20.0;
        let power = self
            .rate(280.0, 20.0, vol)
            .map(|(_, _, p)| p)
            .unwrap_or(0.0);
        Some((Power::from_watts(0.0), Power::from_watts(-power)))
    }
}

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

    #[test]
    fn map_interpolates_interior() {
        let map = CentrifugalMap::default_chiller_map();
        let (mu, eta) = map.interpolate(0.05, 0.9);
        assert!(mu > 0.4 && mu < 0.7);
        assert!(eta > 0.7 && eta < 0.85);
    }

    #[test]
    fn rate_increases_with_speed() {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(3.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(430_000.0),
        );
        let c = CentrifugalCompressor::new(
            CentrifugalMap::default_chiller_map(),
            0.25,
            9000.0,
            inlet,
            outlet,
        )
        .unwrap();
        let connected = c
            .connect(
                Port::new(
                    FluidId::new("R134a"),
                    Pressure::from_bar(3.0),
                    Enthalpy::from_joules_per_kg(400_000.0),
                ),
                Port::new(
                    FluidId::new("R134a"),
                    Pressure::from_bar(10.0),
                    Enthalpy::from_joules_per_kg(430_000.0),
                ),
            )
            .unwrap();
        let (_, _, p_low) = connected.rate(280.0, 20.0, 0.05).unwrap();
        let mut fast = connected;
        fast.set_speed_rpm(12_000.0).unwrap();
        let (_, _, p_high) = fast.rate(280.0, 20.0, 0.05).unwrap();
        assert!(p_high > p_low);
    }
}
