//! Fan Component Implementation
//!
//! This module provides a fan component for air handling systems using
//! polynomial performance curves and affinity laws for variable speed operation.
//!
//! ## Performance Curves
//!
//! **Static Pressure Curve:** P_s = a₀ + a₁Q + a₂Q² + a₃Q³
//!
//! **Efficiency Curve:** η = b₀ + b₁Q + b₂Q²
//!
//! **Fan Power:** P_fan = Q × P_s / η
//!
//! ## Affinity Laws (Variable Speed)
//!
//! When operating at reduced speed (VFD):
//! - Q₂/Q₁ = N₂/N₁
//! - P₂/P₁ = (N₂/N₁)²
//! - Pwr₂/Pwr₁ = (N₂/N₁)³

use crate::polynomials::{AffinityLaws, PerformanceCurves, Polynomial1D};
use crate::port::{Connected, Disconnected, FluidId, Port};
use crate::state_machine::StateManageable;
use crate::{
    CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
    ResidualVector, StateSlice,
};
use entropyk_core::{MassFlow, Power};
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;

/// Fan performance curve coefficients.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FanCurves {
    /// Performance curves (static pressure, efficiency)
    curves: PerformanceCurves,
}

impl FanCurves {
    /// Creates fan curves from performance curves.
    pub fn new(curves: PerformanceCurves) -> Result<Self, ComponentError> {
        curves.validate()?;
        Ok(Self { curves })
    }

    /// Creates fan curves from polynomial coefficients.
    ///
    /// # Arguments
    ///
    /// * `pressure_coeffs` - Static pressure curve [a0, a1, a2, ...] in Pa
    /// * `eff_coeffs` - Efficiency coefficients [b0, b1, b2, ...] as decimal
    ///
    /// # Units
    ///
    /// * Q (flow) in m³/s
    /// * P_s (static pressure) in Pascals
    /// * η (efficiency) as decimal (0.0 to 1.0)
    pub fn from_coefficients(
        pressure_coeffs: Vec<f64>,
        eff_coeffs: Vec<f64>,
    ) -> Result<Self, ComponentError> {
        let pressure_curve = Polynomial1D::new(pressure_coeffs);
        let eff_curve = Polynomial1D::new(eff_coeffs);
        let curves = PerformanceCurves::simple(pressure_curve, eff_curve);
        Self::new(curves)
    }

    /// Creates a quadratic fan curve.
    pub fn quadratic(
        p0: f64,
        p1: f64,
        p2: f64,
        e0: f64,
        e1: f64,
        e2: f64,
    ) -> Result<Self, ComponentError> {
        Self::from_coefficients(vec![p0, p1, p2], vec![e0, e1, e2])
    }

    /// Creates a cubic fan curve (common for fans).
    pub fn cubic(
        p0: f64,
        p1: f64,
        p2: f64,
        p3: f64,
        e0: f64,
        e1: f64,
        e2: f64,
    ) -> Result<Self, ComponentError> {
        Self::from_coefficients(vec![p0, p1, p2, p3], vec![e0, e1, e2])
    }

    /// Returns static pressure at given flow rate (full speed).
    pub fn static_pressure_at_flow(&self, flow_m3_per_s: f64) -> f64 {
        self.curves.head_curve.evaluate(flow_m3_per_s)
    }

    /// Returns efficiency at given flow rate (full speed).
    pub fn efficiency_at_flow(&self, flow_m3_per_s: f64) -> f64 {
        let eta = self.curves.efficiency_curve.evaluate(flow_m3_per_s);
        eta.clamp(0.0, 1.0)
    }

    /// Returns reference to performance curves.
    pub fn curves(&self) -> &PerformanceCurves {
        &self.curves
    }
}

impl Default for FanCurves {
    fn default() -> Self {
        Self::quadratic(500.0, 0.0, 0.0, 0.7, 0.0, 0.0).unwrap()
    }
}

/// Standard air properties at sea level (for reference).
pub mod standard_air {
    /// Standard air density at 20°C, 101325 Pa (kg/m³)
    pub const DENSITY: f64 = 1.204;
    /// Standard air specific heat at constant pressure (J/(kg·K))
    pub const CP: f64 = 1005.0;
}

/// A fan component with polynomial performance curves.
///
/// Fans differ from pumps in that:
/// - They work with compressible fluids (air)
/// - Static pressure is typically much lower
/// - Common to use cubic curves for pressure
///
/// # Example
///
/// ```ignore
/// use entropyk_components::fan::{Fan, FanCurves};
/// use entropyk_components::port::{FluidId, Port};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create fan curves: P_s = 500 - 50*Q - 10*Q² (Pa, m³/s)
/// let curves = FanCurves::quadratic(500.0, -50.0, -10.0, 0.5, 0.2, -0.1).unwrap();
///
/// let inlet = Port::new(
///     FluidId::new("Air"),
///     Pressure::from_bar(1.01325),
///     Enthalpy::from_joules_per_kg(300000.0),
/// );
/// let outlet = Port::new(
///     FluidId::new("Air"),
///     Pressure::from_bar(1.01325),
///     Enthalpy::from_joules_per_kg(300000.0),
/// );
///
/// let fan = Fan::new(curves, inlet, outlet, 1.2).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Fan<State> {
    /// Performance curves
    curves: FanCurves,
    /// Inlet port
    port_inlet: Port<State>,
    /// Outlet port
    port_outlet: Port<State>,
    /// Air density in kg/m³
    air_density_kg_per_m3: f64,
    /// Speed ratio (0.0 to 1.0)
    speed_ratio: f64,
    /// VFD part-load efficiency curve (quadratic in speed ratio), default ≈0.97.
    vfd_eff_coeffs: [f64; 3],
    /// Motor part-load efficiency curve (quadratic in speed ratio), default ≈0.92.
    motor_eff_coeffs: [f64; 3],
    /// When true, `fan_power` returns wire-to-air electrical power.
    use_drive_chain: bool,
    /// Circuit identifier
    circuit_id: CircuitId,
    /// Operational state
    operational_state: OperationalState,
    /// When true, the fan participates in the (P,h) graph solver as a 2-port
    /// element imposing a design-point static pressure rise.
    edge_coupled: bool,
    /// Design volumetric flow (m³/s) at which the curve pressure rise is read.
    design_flow_m3_s: f64,
    /// Captured (m,P,h) global state indices of the inlet edge (incoming).
    inlet_m_idx: Option<usize>,
    inlet_p_idx: Option<usize>,
    inlet_h_idx: Option<usize>,
    /// Captured (m,P,h) global state indices of the outlet edge (outgoing).
    outlet_m_idx: Option<usize>,
    outlet_p_idx: Option<usize>,
    outlet_h_idx: Option<usize>,
    /// Phantom data for type state
    _state: PhantomData<State>,
}

impl Fan<Disconnected> {
    /// Creates a new disconnected fan.
    ///
    /// # Arguments
    ///
    /// * `curves` - Fan performance curves
    /// * `port_inlet` - Inlet port (disconnected)
    /// * `port_outlet` - Outlet port (disconnected)
    /// * `air_density` - Air density in kg/m³ (use 1.2 for standard conditions)
    pub fn new(
        curves: FanCurves,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
        air_density: f64,
    ) -> Result<Self, ComponentError> {
        if port_inlet.fluid_id() != port_outlet.fluid_id() {
            return Err(ComponentError::InvalidState(
                "Inlet and outlet ports must have the same fluid type".to_string(),
            ));
        }

        if air_density <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Air density must be positive".to_string(),
            ));
        }

        Ok(Self {
            curves,
            port_inlet,
            port_outlet,
            air_density_kg_per_m3: air_density,
            speed_ratio: 1.0,
            vfd_eff_coeffs: [0.97, 0.0, 0.0],
            motor_eff_coeffs: [0.92, 0.0, 0.0],
            use_drive_chain: false,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            edge_coupled: false,
            design_flow_m3_s: 0.0,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_m_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            _state: PhantomData,
        })
    }

    /// Enables Bernier–Bourret wire-to-air drive chain (η_VFD × η_motor × η_fan).
    pub fn with_drive_chain(mut self, enabled: bool) -> Self {
        self.use_drive_chain = enabled;
        self
    }

    /// Sets quadratic VFD efficiency coefficients η = a0 + a1·N* + a2·N*².
    pub fn with_vfd_efficiency(mut self, a0: f64, a1: f64, a2: f64) -> Self {
        self.vfd_eff_coeffs = [a0, a1, a2];
        self
    }

    /// Sets quadratic motor efficiency coefficients η = a0 + a1·N* + a2·N*².
    pub fn with_motor_efficiency(mut self, a0: f64, a1: f64, a2: f64) -> Self {
        self.motor_eff_coeffs = [a0, a1, a2];
        self
    }

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

    /// Returns the air density.
    pub fn air_density(&self) -> f64 {
        self.air_density_kg_per_m3
    }

    /// Returns the speed ratio.
    pub fn speed_ratio(&self) -> f64 {
        self.speed_ratio
    }

    /// Sets the speed ratio (0.0 to 1.0).
    pub fn set_speed_ratio(&mut self, ratio: f64) -> Result<(), ComponentError> {
        if !(0.0..=1.0).contains(&ratio) {
            return Err(ComponentError::InvalidState(
                "Speed ratio must be between 0.0 and 1.0".to_string(),
            ));
        }
        self.speed_ratio = ratio;
        Ok(())
    }

    /// Connects the fan to inlet and outlet ports, transitioning the type-state
    /// from `Disconnected` to `Connected` at compile time.
    pub fn connect(
        self,
        inlet: Port<Disconnected>,
        outlet: Port<Disconnected>,
    ) -> Result<Fan<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()))?;

        let mut fan = Fan::<Connected>::from_connected_parts(
            self.curves,
            p_in,
            p_out,
            self.air_density_kg_per_m3,
        )?;
        fan.set_speed_ratio(self.speed_ratio)?;
        fan.vfd_eff_coeffs = self.vfd_eff_coeffs;
        fan.motor_eff_coeffs = self.motor_eff_coeffs;
        fan.use_drive_chain = self.use_drive_chain;
        Ok(fan)
    }
}

impl Fan<Connected> {
    /// Creates a new connected fan from pre-connected ports.
    pub(crate) fn from_connected_parts(
        curves: FanCurves,
        port_inlet: Port<Connected>,
        port_outlet: Port<Connected>,
        air_density: f64,
    ) -> Result<Self, ComponentError> {
        if air_density <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Air density must be positive".to_string(),
            ));
        }
        Ok(Self {
            curves,
            port_inlet,
            port_outlet,
            air_density_kg_per_m3: air_density,
            speed_ratio: 1.0,
            vfd_eff_coeffs: [0.97, 0.0, 0.0],
            motor_eff_coeffs: [0.92, 0.0, 0.0],
            use_drive_chain: false,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            edge_coupled: false,
            design_flow_m3_s: 0.0,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_m_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            _state: PhantomData,
        })
    }

    /// Enables the edge-coupled (P,h) solver model, imposing a design-point
    /// static pressure rise read from the fan curve at `design_flow_m3_s`
    /// (and the current speed ratio). Shaft power is added to the air stream as
    /// an enthalpy rise so the coupled model satisfies the First Law.
    pub fn with_edge_coupling(mut self, design_flow_m3_s: f64) -> Self {
        self.design_flow_m3_s = design_flow_m3_s.max(0.0);
        self.edge_coupled = true;
        if self.operational_state == OperationalState::Off {
            self.operational_state = OperationalState::On;
        }
        self
    }

    /// Returns the inlet port.
    pub fn port_inlet(&self) -> &Port<Connected> {
        &self.port_inlet
    }

    /// Returns the outlet port.
    pub fn port_outlet(&self) -> &Port<Connected> {
        &self.port_outlet
    }

    /// Calculates the static pressure rise across the fan.
    ///
    /// Applies affinity laws for variable speed operation.
    pub fn static_pressure_rise(&self, flow_m3_per_s: f64) -> f64 {
        // Handle zero speed - fan produces no pressure
        if self.speed_ratio <= 0.0 {
            return 0.0;
        }

        // Handle negative flow gracefully by using a linear extrapolation from Q=0
        // to prevent polynomial extrapolation issues with quadratic/cubic terms
        if flow_m3_per_s < 0.0 {
            let p0 = self.curves.static_pressure_at_flow(0.0);
            let p_eps = self.curves.static_pressure_at_flow(1e-6);
            let dp_dq = (p_eps - p0) / 1e-6;

            let pressure = p0 + dp_dq * flow_m3_per_s;
            return AffinityLaws::scale_head(pressure, self.speed_ratio);
        }

        // Handle exactly zero flow
        if flow_m3_per_s == 0.0 {
            let pressure = self.curves.static_pressure_at_flow(0.0);
            return AffinityLaws::scale_head(pressure, self.speed_ratio);
        }

        let equivalent_flow = AffinityLaws::unscale_flow(flow_m3_per_s, self.speed_ratio);
        let pressure = self.curves.static_pressure_at_flow(equivalent_flow);
        AffinityLaws::scale_head(pressure, self.speed_ratio)
    }

    /// Calculates total pressure (static + velocity pressure).
    ///
    /// Total pressure = Static pressure + ½ρv²
    ///
    /// # Arguments
    ///
    /// * `flow_m3_per_s` - Volumetric flow rate
    /// * `duct_area_m2` - Duct cross-sectional area
    pub fn total_pressure_rise(&self, flow_m3_per_s: f64, duct_area_m2: f64) -> f64 {
        let static_p = self.static_pressure_rise(flow_m3_per_s);

        if duct_area_m2 <= 0.0 {
            return static_p;
        }

        // Velocity pressure: P_v = ½ρv²
        let velocity = flow_m3_per_s / duct_area_m2;
        let velocity_pressure = 0.5 * self.air_density_kg_per_m3 * velocity * velocity;

        static_p + velocity_pressure
    }

    /// Calculates efficiency at the given flow rate.
    pub fn efficiency(&self, flow_m3_per_s: f64) -> f64 {
        // Handle zero speed - fan is not running
        if self.speed_ratio <= 0.0 {
            return 0.0;
        }

        // Handle zero flow
        if flow_m3_per_s <= 0.0 {
            return self.curves.efficiency_at_flow(0.0);
        }

        let equivalent_flow = AffinityLaws::unscale_flow(flow_m3_per_s, self.speed_ratio);
        self.curves.efficiency_at_flow(equivalent_flow)
    }

    /// Shaft aerodynamic power `Q × ΔP / η_fan` [W].
    pub fn shaft_power(&self, flow_m3_per_s: f64) -> Power {
        if flow_m3_per_s <= 0.0 || self.speed_ratio <= 0.0 {
            return Power::from_watts(0.0);
        }

        let pressure = self.static_pressure_rise(flow_m3_per_s);
        let eta = self.efficiency(flow_m3_per_s);

        if eta <= 0.0 {
            return Power::from_watts(0.0);
        }

        let power_w = flow_m3_per_s * pressure / eta;
        Power::from_watts(power_w)
    }

    /// Drive-chain efficiency η_VFD(N*) × η_motor(N*) at the current speed ratio.
    pub fn drive_chain_efficiency(&self) -> f64 {
        let n = self.speed_ratio.clamp(0.0, 1.0);
        let eta_vfd =
            (self.vfd_eff_coeffs[0] + self.vfd_eff_coeffs[1] * n + self.vfd_eff_coeffs[2] * n * n)
                .clamp(0.05, 1.0);
        let eta_motor = (self.motor_eff_coeffs[0]
            + self.motor_eff_coeffs[1] * n
            + self.motor_eff_coeffs[2] * n * n)
            .clamp(0.05, 1.0);
        eta_vfd * eta_motor
    }

    /// Fan power consumption.
    ///
    /// Shaft power by default; electrical wire-to-air power when drive chain is enabled:
    /// `P_elec = P_shaft / (η_VFD · η_motor)`.
    pub fn fan_power(&self, flow_m3_per_s: f64) -> Power {
        let shaft = self.shaft_power(flow_m3_per_s);
        if !self.use_drive_chain {
            return shaft;
        }
        let eta_chain = self.drive_chain_efficiency();
        if eta_chain <= 0.0 {
            return Power::from_watts(0.0);
        }
        Power::from_watts(shaft.to_watts() / eta_chain)
    }

    /// Enables or disables the wire-to-air drive chain.
    pub fn set_drive_chain(&mut self, enabled: bool) {
        self.use_drive_chain = enabled;
    }

    fn edge_coupled_flow_and_power(
        &self,
        state: &StateSlice,
        inlet_m_idx: usize,
    ) -> Result<(f64, f64, f64), ComponentError> {
        let mass_flow_kg_s = state.get(inlet_m_idx).copied().ok_or_else(|| {
            ComponentError::InvalidState(format!(
                "Fan edge-coupled inlet mass-flow index {inlet_m_idx} is outside the state vector"
            ))
        })?;
        let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
        let power_w = match self.operational_state {
            OperationalState::Off | OperationalState::Bypass => 0.0,
            OperationalState::On => self.fan_power(flow_m3_s).to_watts(),
        };
        let enthalpy_rise_j_kg = if mass_flow_kg_s.abs() > 1e-12 {
            power_w / mass_flow_kg_s
        } else {
            0.0
        };
        Ok((flow_m3_s, power_w, enthalpy_rise_j_kg))
    }

    /// Calculates mass flow from volumetric flow.
    pub fn mass_flow_from_volumetric(&self, flow_m3_per_s: f64) -> MassFlow {
        MassFlow::from_kg_per_s(flow_m3_per_s * self.air_density_kg_per_m3)
    }

    /// Calculates volumetric flow from mass flow.
    pub fn volumetric_from_mass_flow(&self, mass_flow: MassFlow) -> f64 {
        mass_flow.to_kg_per_s() / self.air_density_kg_per_m3
    }

    /// Returns the air density.
    pub fn air_density(&self) -> f64 {
        self.air_density_kg_per_m3
    }

    /// Returns the speed ratio.
    pub fn speed_ratio(&self) -> f64 {
        self.speed_ratio
    }

    /// Sets the speed ratio (0.0 to 1.0).
    pub fn set_speed_ratio(&mut self, ratio: f64) -> Result<(), ComponentError> {
        if !(0.0..=1.0).contains(&ratio) {
            return Err(ComponentError::InvalidState(
                "Speed ratio must be between 0.0 and 1.0".to_string(),
            ));
        }
        self.speed_ratio = ratio;
        Ok(())
    }

    /// Returns both ports as a slice for solver topology.
    pub fn get_ports_slice(&self) -> [&Port<Connected>; 2] {
        [&self.port_inlet, &self.port_outlet]
    }
}

impl Component for Fan<Connected> {
    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_idx, p_idx, h_idx)
        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);
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        // Edge-coupled (P,h) model: impose a design-point static pressure rise.
        if self.edge_coupled {
            if let (Some(in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
                self.inlet_m_idx,
                self.inlet_p_idx,
                self.inlet_h_idx,
                self.outlet_p_idx,
                self.outlet_h_idx,
            ) {
                if residuals.len() < 2 {
                    return Err(ComponentError::InvalidResidualDimensions {
                        expected: 2,
                        actual: residuals.len(),
                    });
                }
                let dp = match self.operational_state {
                    OperationalState::Off => 0.0,
                    _ => {
                        let (flow_m3_s, _, _) = self.edge_coupled_flow_and_power(state, in_m)?;
                        self.static_pressure_rise(flow_m3_s)
                    }
                };
                let (_, _, enthalpy_rise_j_kg) = self.edge_coupled_flow_and_power(state, in_m)?;
                // r0: imposed static pressure rise (fan adds pressure)
                residuals[0] = state[out_p] - (state[in_p] + dp);
                // r1: adiabatic fan casing, shaft power heats the air stream
                residuals[1] = state[out_h] - (state[in_h] + enthalpy_rise_j_kg);
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "Fan edge-coupled model requires inlet and outlet edge state indices".to_string(),
            ));
        }

        Err(ComponentError::InvalidState(
            "Fan physical simulation requires edge-coupled inlet/outlet state indices".to_string(),
        ))
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        // Edge-coupled (P,h) model.
        if self.edge_coupled {
            if let (Some(_in_m), Some(in_p), Some(in_h), Some(out_p), Some(out_h)) = (
                self.inlet_m_idx,
                self.inlet_p_idx,
                self.inlet_h_idx,
                self.outlet_p_idx,
                self.outlet_h_idx,
            ) {
                // r0 = P_out - (P_in + dp)
                jacobian.add_entry(0, out_p, 1.0);
                jacobian.add_entry(0, in_p, -1.0);
                // r1 = h_out - h_in
                jacobian.add_entry(1, out_h, 1.0);
                jacobian.add_entry(1, in_h, -1.0);
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "Fan edge-coupled model requires inlet and outlet edge state indices".to_string(),
            ));
        }

        Err(ComponentError::InvalidState(
            "Fan physical simulation requires edge-coupled inlet/outlet state indices".to_string(),
        ))
    }

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

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

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        if self.edge_coupled {
            let (Some(in_m), Some(out_m)) = (self.inlet_m_idx, self.outlet_m_idx) else {
                return Err(ComponentError::InvalidState(
                    "Fan edge-coupled model requires inlet and outlet mass-flow indices"
                        .to_string(),
                ));
            };
            let max_idx = in_m.max(out_m);
            if max_idx >= state.len() {
                return Err(ComponentError::InvalidStateDimensions {
                    expected: max_idx + 1,
                    actual: state.len(),
                });
            }
            return Ok(vec![
                entropyk_core::MassFlow::from_kg_per_s(state[in_m]),
                entropyk_core::MassFlow::from_kg_per_s(-state[out_m]),
            ]);
        }

        Err(ComponentError::InvalidState(
            "Fan mass-flow reporting requires edge-coupled inlet/outlet indices".to_string(),
        ))
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        if self.edge_coupled {
            let (Some(in_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) else {
                return Err(ComponentError::InvalidState(
                    "Fan edge-coupled model requires inlet and outlet enthalpy indices".to_string(),
                ));
            };
            let max_idx = in_h.max(out_h);
            if max_idx >= state.len() {
                return Err(ComponentError::InvalidStateDimensions {
                    expected: max_idx + 1,
                    actual: state.len(),
                });
            }
            return Ok(vec![
                entropyk_core::Enthalpy::from_joules_per_kg(state[in_h]),
                entropyk_core::Enthalpy::from_joules_per_kg(state[out_h]),
            ]);
        }

        Err(ComponentError::InvalidState(
            "Fan enthalpy reporting requires edge-coupled inlet/outlet indices".to_string(),
        ))
    }

    fn energy_transfers(
        &self,
        state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass => Some((
                entropyk_core::Power::from_watts(0.0),
                entropyk_core::Power::from_watts(0.0),
            )),
            OperationalState::On => {
                if self.edge_coupled {
                    let Some(in_m) = self.inlet_m_idx else {
                        return None;
                    };
                    let Ok((_, power_w, _)) = self.edge_coupled_flow_and_power(state, in_m) else {
                        return None;
                    };
                    return Some((
                        entropyk_core::Power::from_watts(0.0),
                        entropyk_core::Power::from_watts(-power_w),
                    ));
                }

                let in_m = self.inlet_m_idx?;
                let mass_flow_kg_s = *state.get(in_m)?;
                let flow_m3_s = mass_flow_kg_s / self.air_density_kg_per_m3;
                let power_calc = self.fan_power(flow_m3_s).to_watts();
                Some((
                    entropyk_core::Power::from_watts(0.0),
                    entropyk_core::Power::from_watts(-power_calc),
                ))
            }
        }
    }

    fn signature(&self) -> String {
        format!("Fan(circuit={})", self.circuit_id.0)
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("Fan")
            .with_param("circuitId", self.circuit_id.0)
            .with_param("airDensityKgPerM3", self.air_density_kg_per_m3)
            .with_param("speedRatio", self.speed_ratio)
    }
}

impl StateManageable for Fan<Connected> {
    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".to_string(),
            })
        }
    }

    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, circuit_id: CircuitId) {
        self.circuit_id = circuit_id;
    }
}

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

    fn create_test_curves() -> FanCurves {
        // Typical centrifugal fan:
        // P_s = 500 - 100*Q - 200*Q² (Pa, Q in m³/s)
        // η = 0.5 + 0.3*Q - 0.5*Q²
        FanCurves::quadratic(500.0, -100.0, -200.0, 0.5, 0.3, -0.5).unwrap()
    }

    fn create_test_fan_connected() -> Fan<Connected> {
        let curves = create_test_curves();
        let inlet = Port::new(
            FluidId::new("Air"),
            Pressure::from_bar(1.01325),
            Enthalpy::from_joules_per_kg(300000.0),
        );
        let outlet = Port::new(
            FluidId::new("Air"),
            Pressure::from_bar(1.01325),
            Enthalpy::from_joules_per_kg(300000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        Fan {
            curves,
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            air_density_kg_per_m3: 1.2,
            speed_ratio: 1.0,
            vfd_eff_coeffs: [0.97, 0.0, 0.0],
            motor_eff_coeffs: [0.92, 0.0, 0.0],
            use_drive_chain: false,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            edge_coupled: false,
            design_flow_m3_s: 0.0,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_m_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            _state: PhantomData,
        }
    }

    #[test]
    fn test_wire_to_air_drive_chain_increases_power() {
        let mut fan = create_test_fan_connected();
        let shaft = fan.shaft_power(0.5).to_watts();
        fan.set_drive_chain(true);
        let elec = fan.fan_power(0.5).to_watts();
        assert!(elec > shaft);
        assert!((elec / shaft - 1.0 / (0.97 * 0.92)).abs() < 1e-6);
    }

    #[test]
    fn test_fan_curves_creation() {
        let curves = create_test_curves();
        assert_eq!(curves.static_pressure_at_flow(0.0), 500.0);
        assert_relative_eq!(curves.efficiency_at_flow(0.0), 0.5);
    }

    #[test]
    fn test_fan_static_pressure() {
        let curves = create_test_curves();
        // P_s = 500 - 100*1 - 200*1 = 200 Pa
        let pressure = curves.static_pressure_at_flow(1.0);
        assert_relative_eq!(pressure, 200.0, epsilon = 1e-10);
    }

    #[test]
    fn test_fan_creation() {
        let fan = create_test_fan_connected();
        assert_relative_eq!(fan.air_density(), 1.2, epsilon = 1e-10);
        assert_eq!(fan.speed_ratio(), 1.0);
    }

    #[test]
    fn test_fan_pressure_rise_full_speed() {
        let fan = create_test_fan_connected();
        let pressure = fan.static_pressure_rise(0.0);
        assert_relative_eq!(pressure, 500.0, epsilon = 1e-10);
    }

    #[test]
    fn test_fan_pressure_rise_half_speed() {
        let mut fan = create_test_fan_connected();
        fan.set_speed_ratio(0.5).unwrap();

        // At 50% speed, shut-off pressure is 25% of full speed
        let pressure = fan.static_pressure_rise(0.0);
        assert_relative_eq!(pressure, 125.0, epsilon = 1e-10);
    }

    #[test]
    fn test_fan_fan_power() {
        let fan = create_test_fan_connected();

        // At Q=1 m³/s: P_s ≈ 200 Pa, η ≈ 0.3
        // P = 1 * 200 / 0.3 ≈ 667 W
        let power = fan.fan_power(1.0);
        assert!(power.to_watts() > 0.0);
        assert!(power.to_watts() < 2000.0);
    }

    #[test]
    fn test_fan_affinity_laws_power() {
        let fan_full = create_test_fan_connected();

        let mut fan_half = create_test_fan_connected();
        fan_half.set_speed_ratio(0.5).unwrap();

        let power_full = fan_full.fan_power(1.0);
        let power_half = fan_half.fan_power(0.5);

        // Ratio should be approximately 0.125 (cube law)
        let ratio = power_half.to_watts() / power_full.to_watts();
        assert_relative_eq!(ratio, 0.125, epsilon = 0.1);
    }

    #[test]
    fn test_fan_total_pressure() {
        let fan = create_test_fan_connected();

        // With a duct area of 0.5 m²
        let total_p = fan.total_pressure_rise(1.0, 0.5);
        let static_p = fan.static_pressure_rise(1.0);

        // Total > Static due to velocity pressure
        assert!(total_p > static_p);
    }

    #[test]
    fn test_fan_component_n_equations() {
        let fan = create_test_fan_connected();
        assert_eq!(fan.n_equations(), 2);
    }

    #[test]
    fn test_fan_state_manageable() {
        let fan = create_test_fan_connected();
        assert_eq!(fan.state(), OperationalState::On);
        assert!(fan.can_transition_to(OperationalState::Off));
    }

    #[test]
    fn test_standard_air_constants() {
        assert_relative_eq!(standard_air::DENSITY, 1.204, epsilon = 0.01);
        assert_relative_eq!(standard_air::CP, 1005.0);
    }
}
