//! Pipe Component Implementation
//!
//! This module provides a pipe component for fluid transport with
//! pressure drop calculation using the Darcy-Weisbach equation.
//!
//! **Pipe serves for both refrigerant and incompressible fluid circuits** (water,
//! seawater, glycol, etc.). Use [`Pipe::for_incompressible`] or [`Pipe::for_refrigerant`]
//! with **explicit ρ and μ** obtained from a fluid backend.
//!
//! ## Fluid Support
//!
//! - **Refrigerant** (compressible): ρ and μ vary with P,T. Use [`Pipe::for_refrigerant`]
//!   with design-point values from CoolProp or tabular backend.
//!
//! - **Incompressible** (water, seawater, glycol): ρ and μ from `IncompressibleBackend`
//!   (Story 2.7). **Do not hardcode**—obtain properties via fluid backend.
//!
//! ## Darcy-Weisbach Equation
//!
//! ```text
//! ΔP = f × (L/D) × (ρ × v² / 2)
//! ```
//!
//! Where:
//! - f = Darcy friction factor (dimensionless)
//! - L = Pipe length (m)
//! - D = Pipe inner diameter (m)
//! - ρ = Fluid density (kg/m³)
//! - v = Flow velocity (m/s)
//!
//! ## Haaland Friction Factor
//!
//! For turbulent flow, the Haaland approximation is used:
//!
//! ```text
//! 1/√f = -1.8 × log10[(ε/D/3.7)^1.11 + 6.9/Re]
//! ```
//!
//! Where:
//! - ε = Pipe roughness (m)
//! - Re = Reynolds number = ρ × v × D / μ

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::{Calib, MassFlow};
use std::marker::PhantomData;

/// Common pipe materials and their typical roughness values (in meters).
pub mod roughness {
    /// Smooth drawn tubing (copper, plastic) - 0.0015 mm
    pub const SMOOTH: f64 = 1.5e-6;
    /// Commercial steel pipe - 0.045 mm
    pub const STEEL_COMMERCIAL: f64 = 4.5e-5;
    /// Galvanized iron - 0.15 mm
    pub const GALVANIZED_IRON: f64 = 1.5e-4;
    /// Cast iron - 0.26 mm
    pub const CAST_IRON: f64 = 2.6e-4;
    /// Concrete - 1.0 mm
    pub const CONCRETE: f64 = 1.0e-3;
    /// PVC/HDPE plastic - 0.0015 mm
    pub const PLASTIC: f64 = 1.5e-6;
}

/// Pipe geometry specification.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PipeGeometry {
    /// Pipe length in meters
    pub length_m: f64,
    /// Inner diameter in meters
    pub diameter_m: f64,
    /// Pipe roughness in meters
    pub roughness_m: f64,
}

impl PipeGeometry {
    /// Creates a new pipe geometry specification.
    ///
    /// # Arguments
    ///
    /// * `length_m` - Pipe length in meters
    /// * `diameter_m` - Inner diameter in meters
    /// * `roughness_m` - Pipe roughness in meters (use values from `roughness` module)
    ///
    /// # Errors
    ///
    /// Returns an error if any dimension is non-positive.
    pub fn new(length_m: f64, diameter_m: f64, roughness_m: f64) -> Result<Self, ComponentError> {
        if length_m <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Pipe length must be positive".to_string(),
            ));
        }
        if diameter_m <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Pipe diameter must be positive".to_string(),
            ));
        }
        if roughness_m < 0.0 {
            return Err(ComponentError::InvalidState(
                "Pipe roughness cannot be negative".to_string(),
            ));
        }

        Ok(Self {
            length_m,
            diameter_m,
            roughness_m,
        })
    }

    /// Creates a smooth pipe geometry.
    pub fn smooth(length_m: f64, diameter_m: f64) -> Result<Self, ComponentError> {
        Self::new(length_m, diameter_m, roughness::SMOOTH)
    }

    /// Creates a commercial steel pipe geometry.
    pub fn steel(length_m: f64, diameter_m: f64) -> Result<Self, ComponentError> {
        Self::new(length_m, diameter_m, roughness::STEEL_COMMERCIAL)
    }

    /// Returns the cross-sectional area in m².
    pub fn area(&self) -> f64 {
        std::f64::consts::PI * self.diameter_m * self.diameter_m / 4.0
    }

    /// Returns the length-to-diameter ratio (L/D).
    pub fn ld_ratio(&self) -> f64 {
        self.length_m / self.diameter_m
    }

    /// Returns the relative roughness (ε/D).
    pub fn relative_roughness(&self) -> f64 {
        self.roughness_m / self.diameter_m
    }
}

/// Friction factor calculation methods.
pub mod friction_factor {
    use entropyk_core::smoothing::cubic_blend;

    /// Reynolds number below which the flow is treated as fully laminar.
    pub const RE_LAMINAR: f64 = 2300.0;

    /// Reynolds number above which the flow is treated as fully turbulent.
    pub const RE_TURBULENT: f64 = 4000.0;

    /// Pure turbulent Haaland friction factor (no transition handling).
    ///
    /// 1/√f = -1.8 × log10[(ε/D / 3.7)^1.11 + 6.9/Re]
    fn haaland_turbulent(relative_roughness: f64, reynolds: f64) -> f64 {
        let re_clamped = reynolds.max(1.0);
        let term1 = (relative_roughness / 3.7).powf(1.11);
        let term2 = 6.9 / re_clamped;
        let inv_sqrt_f = -1.8 * (term1 + term2).log10();
        1.0 / (inv_sqrt_f * inv_sqrt_f)
    }

    /// Calculates the Darcy friction factor with a C¹ laminar→turbulent blend.
    ///
    /// Below [`RE_LAMINAR`] the laminar law `f = 64/Re` is used; above
    /// [`RE_TURBULENT`] the explicit Haaland turbulent correlation is used. In
    /// the transition band `[RE_LAMINAR, RE_TURBULENT]` the two are blended with
    /// a C¹ [`cubic_blend`], so the friction factor — and therefore the analytic
    /// pressure-drop Jacobian — has **no slope discontinuity** at `Re = 2300`.
    /// The previous hard `if Re < 2300` switch introduced a kink that made the
    /// Newton Jacobian jump (the PR#563-style failure mode).
    ///
    /// # Arguments
    ///
    /// * `relative_roughness` - ε/D (dimensionless)
    /// * `reynolds` - Reynolds number (dimensionless)
    ///
    /// # Returns
    ///
    /// Darcy friction factor f
    pub fn haaland(relative_roughness: f64, reynolds: f64) -> f64 {
        if reynolds <= 0.0 {
            return 0.02; // Default for invalid input
        }

        // Laminar law. Reynolds is not clamped here so the pressure drop stays
        // linear near zero flow (Story 3.5 zero-flow regularization).
        let f_laminar = 64.0 / reynolds;
        if reynolds <= RE_LAMINAR {
            return f_laminar;
        }

        let f_turbulent = haaland_turbulent(relative_roughness, reynolds);
        if reynolds >= RE_TURBULENT {
            return f_turbulent;
        }

        // Transition band: C¹ blend. At both edges the blend weight and its
        // derivative are zero, so the slope matches the pure laminar / turbulent
        // branches there — the join is C¹.
        cubic_blend(f_laminar, f_turbulent, reynolds, RE_LAMINAR, RE_TURBULENT)
    }
}

/// A pipe component with pressure drop calculation.
///
/// Uses the Darcy-Weisbach equation with the Haaland friction factor
/// for accurate pressure drop estimation.
///
/// **Dual refrigerant/incompressible usage:** Use [`Pipe::for_incompressible`] for water,
/// seawater, glycol (ρ, μ from backend); use [`Pipe::for_refrigerant`] for refrigerant
/// circuits with design-point ρ and μ from a fluid backend.
///
/// # Example
///
/// ```ignore
/// use entropyk_components::pipe::{Pipe, PipeGeometry, roughness};
/// use entropyk_components::port::{FluidId, Port};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create a 10m long, 50mm diameter steel pipe
/// let geometry = PipeGeometry::steel(10.0, 0.05).unwrap();
///
/// let inlet = Port::new(
///     FluidId::new("Water"),
///     Pressure::from_bar(2.0),
///     Enthalpy::from_joules_per_kg(100000.0),
/// );
/// let outlet = Port::new(
///     FluidId::new("Water"),
///     Pressure::from_bar(2.0),
///     Enthalpy::from_joules_per_kg(100000.0),
/// );
///
/// let pipe = Pipe::new(geometry, inlet, outlet, 1000.0, 0.001).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Pipe<State> {
    /// Pipe geometry
    geometry: PipeGeometry,
    /// Inlet port
    port_inlet: Port<State>,
    /// Outlet port
    port_outlet: Port<State>,
    /// Fluid density in kg/m³
    fluid_density_kg_per_m3: f64,
    /// Fluid dynamic viscosity in Pa·s
    fluid_viscosity_pa_s: f64,
    /// Calibration: ΔP_eff = f_dp × ΔP_nominal
    calib: Calib,
    /// Circuit identifier
    circuit_id: CircuitId,
    /// Operational state
    operational_state: OperationalState,
    /// Design-point pressure drop (Pa) for the edge-coupled (P,h) solver model.
    /// When `> 0`, ΔP is imposed as this constant. When `0`, ΔP is computed from
    /// Darcy–Weisbach using geometry + live mass flow (`inlet_m_idx`).
    design_dp_pa: f64,
    /// When true, the component uses the edge-coupled (P,h) solver model
    /// (`n_equations() == 2`) instead of the legacy mass-flow model.
    edge_coupled: bool,
    /// Inlet edge mass-flow index (edge-coupled Darcy mode).
    inlet_m_idx: Option<usize>,
    /// Inlet edge pressure index in the global state vector (edge-coupled mode).
    inlet_p_idx: Option<usize>,
    /// Inlet edge enthalpy index in the global state vector (edge-coupled mode).
    inlet_h_idx: Option<usize>,
    /// Outlet edge pressure index in the global state vector (edge-coupled mode).
    outlet_p_idx: Option<usize>,
    /// Outlet edge enthalpy index in the global state vector (edge-coupled mode).
    outlet_h_idx: Option<usize>,
    /// Phantom data for type state
    _state: PhantomData<State>,
}

impl Pipe<Disconnected> {
    /// Creates a new disconnected pipe.
    ///
    /// # Arguments
    ///
    /// * `geometry` - Pipe geometry specification
    /// * `port_inlet` - Inlet port (disconnected)
    /// * `port_outlet` - Outlet port (disconnected)
    /// * `fluid_density` - Fluid density in kg/m³
    /// * `fluid_viscosity` - Fluid dynamic viscosity in Pa·s
    pub fn new(
        geometry: PipeGeometry,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
        fluid_density: f64,
        fluid_viscosity: 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 fluid_density <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Fluid density must be positive".to_string(),
            ));
        }

        if fluid_viscosity <= 0.0 {
            return Err(ComponentError::InvalidState(
                "Fluid viscosity must be positive".to_string(),
            ));
        }

        Ok(Self {
            geometry,
            port_inlet,
            port_outlet,
            fluid_density_kg_per_m3: fluid_density,
            fluid_viscosity_pa_s: fluid_viscosity,
            calib: Calib::default(),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            design_dp_pa: 0.0,
            edge_coupled: false,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            _state: PhantomData,
        })
    }

    /// Creates a pipe for incompressible fluid circuits (water, seawater, glycol).
    ///
    /// **Obtain ρ and μ from a fluid backend** (e.g. `IncompressibleBackend` from Story 2.7).
    /// Do not hardcode—water, seawater, and glycol have different properties.
    ///
    /// # Arguments
    ///
    /// * `geometry` - Pipe geometry specification
    /// * `port_inlet` - Inlet port (disconnected)
    /// * `port_outlet` - Outlet port (disconnected)
    /// * `density` - Fluid density at design point (kg/m³)
    /// * `viscosity` - Fluid dynamic viscosity at design point (Pa·s)
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::pipe::{Pipe, PipeGeometry};
    /// use entropyk_components::port::{FluidId, Port};
    /// use entropyk_core::{Pressure, Enthalpy};
    ///
    /// let geometry = PipeGeometry::smooth(5.0, 0.025).unwrap();
    /// let inlet = Port::new(
    ///     FluidId::new("Water"),
    ///     Pressure::from_bar(2.0),
    ///     Enthalpy::from_joules_per_kg(100000.0),
    /// );
    /// let outlet = Port::new(
    ///     FluidId::new("Water"),
    ///     Pressure::from_bar(2.0),
    ///     Enthalpy::from_joules_per_kg(100000.0),
    /// );
    /// // Get ρ, μ from IncompressibleBackend.property() at design temperature
    /// let pipe = Pipe::for_incompressible(geometry, inlet, outlet, 998.0, 0.001).unwrap();
    /// assert!((pipe.fluid_density() - 998.0).abs() < 1e-6);
    /// ```
    pub fn for_incompressible(
        geometry: PipeGeometry,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
        density: f64,
        viscosity: f64,
    ) -> Result<Self, ComponentError> {
        Self::new(geometry, port_inlet, port_outlet, density, viscosity)
    }

    /// Creates a pipe for refrigerant circuits with explicit design-point properties.
    ///
    /// Refrigerant ρ and μ vary with pressure and temperature. These values are
    /// **design-point typical values** at the operating condition. For accurate
    /// simulation, obtain ρ and μ from the fluid properties backend (CoolProp,
    /// tabular interpolation, etc.).
    ///
    /// Typical design-point values (liquid phase): R134a 40°C ~1140 kg/m³, ~0.0002 Pa·s;
    /// R410A 40°C ~1050 kg/m³, ~0.00015 Pa·s.
    ///
    /// # Arguments
    ///
    /// * `geometry` - Pipe geometry specification
    /// * `port_inlet` - Inlet port (disconnected)
    /// * `port_outlet` - Outlet port (disconnected)
    /// * `density` - Fluid density at design point (kg/m³)
    /// * `viscosity` - Fluid dynamic viscosity at design point (Pa·s)
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::pipe::{Pipe, PipeGeometry};
    /// use entropyk_components::port::{FluidId, Port};
    /// use entropyk_core::{Pressure, Enthalpy};
    ///
    /// let geometry = PipeGeometry::smooth(3.0, 0.012).unwrap();
    /// let inlet = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_bar(10.0),
    ///     Enthalpy::from_joules_per_kg(250000.0),
    /// );
    /// let outlet = Port::new(
    ///     FluidId::new("R134a"),
    ///     Pressure::from_bar(10.0),
    ///     Enthalpy::from_joules_per_kg(250000.0),
    /// );
    /// // R134a liquid at ~40°C: ρ ≈ 1140, μ ≈ 0.0002
    /// let pipe = Pipe::for_refrigerant(geometry, inlet, outlet, 1140.0, 0.0002).unwrap();
    /// assert_eq!(pipe.fluid_id().as_str(), "R134a");
    /// ```
    pub fn for_refrigerant(
        geometry: PipeGeometry,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
        density: f64,
        viscosity: f64,
    ) -> Result<Self, ComponentError> {
        Self::new(geometry, port_inlet, port_outlet, density, viscosity)
    }

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

    /// Returns the pipe geometry.
    pub fn geometry(&self) -> &PipeGeometry {
        &self.geometry
    }

    /// Returns the fluid density.
    pub fn fluid_density(&self) -> f64 {
        self.fluid_density_kg_per_m3
    }

    /// Returns the fluid viscosity.
    pub fn fluid_viscosity(&self) -> f64 {
        self.fluid_viscosity_pa_s
    }

    /// Returns calibration factors (f_dp for pressure drop scaling).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

    /// Sets calibration factors.
    pub fn set_calib(&mut self, calib: Calib) {
        self.calib = calib;
    }
    /// Connects the pipe to inlet and outlet ports.
    ///
    /// This consumes the disconnected pipe and returns a connected one,
    /// transitioning the state at compile time.
    pub fn connect(
        self,
        inlet: Port<Disconnected>,
        outlet: Port<Disconnected>,
    ) -> Result<Pipe<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(Pipe {
            geometry: self.geometry,
            port_inlet: p_in,
            port_outlet: p_out,
            fluid_density_kg_per_m3: self.fluid_density_kg_per_m3,
            fluid_viscosity_pa_s: self.fluid_viscosity_pa_s,
            calib: self.calib,
            circuit_id: self.circuit_id,
            operational_state: self.operational_state,
            design_dp_pa: self.design_dp_pa,
            edge_coupled: self.edge_coupled,
            inlet_m_idx: self.inlet_m_idx,
            inlet_p_idx: self.inlet_p_idx,
            inlet_h_idx: self.inlet_h_idx,
            outlet_p_idx: self.outlet_p_idx,
            outlet_h_idx: self.outlet_h_idx,
            _state: PhantomData,
        })
    }
}

impl Pipe<Connected> {
    /// Enables the edge-coupled (P,h) solver model so the pipe participates in
    /// the refrigeration/hydronic graph solver.
    ///
    /// In this mode the pipe owns 2 equations on its outlet edge:
    /// - `r0 = P_out - (P_in - ΔP)` — if `design_dp_pa > 0`, ΔP is that constant;
    ///   if `design_dp_pa == 0`, ΔP is Darcy–Weisbach from geometry + live ṁ
    /// - `r1 = h_out - h_in` — adiabatic pass-through
    pub fn with_design_pressure_drop_pa(mut self, design_dp_pa: f64) -> Self {
        self.design_dp_pa = design_dp_pa.max(0.0);
        self.edge_coupled = true;
        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 flow velocity.
    ///
    /// # Arguments
    ///
    /// * `flow_m3_per_s` - Volumetric flow rate in m³/s
    ///
    /// # Returns
    ///
    /// Velocity in m/s
    pub fn velocity(&self, flow_m3_per_s: f64) -> f64 {
        let area = self.geometry.area();
        if area > 0.0 {
            flow_m3_per_s / area
        } else {
            0.0
        }
    }

    /// Calculates the Reynolds number.
    ///
    /// Re = ρ × v × D / μ
    pub fn reynolds_number(&self, flow_m3_per_s: f64) -> f64 {
        let velocity = self.velocity(flow_m3_per_s);
        velocity * self.geometry.diameter_m * self.fluid_density_kg_per_m3
            / self.fluid_viscosity_pa_s
    }

    /// Calculates the Darcy friction factor using Haaland equation.
    pub fn friction_factor(&self, flow_m3_per_s: f64) -> f64 {
        let rel_roughness = self.geometry.relative_roughness();
        let re = self.reynolds_number(flow_m3_per_s);
        friction_factor::haaland(rel_roughness, re)
    }

    /// Calculates the pressure drop using Darcy-Weisbach equation.
    ///
    /// ΔP = f × (L/D) × (ρ × v² / 2)
    ///
    /// # Arguments
    ///
    /// * `flow_m3_per_s` - Volumetric flow rate in m³/s
    ///
    /// # Returns
    ///
    /// Pressure drop in Pascals (positive value)
    pub fn pressure_drop(&self, flow_m3_per_s: f64) -> f64 {
        let abs_flow = flow_m3_per_s.abs();
        if abs_flow <= std::f64::EPSILON {
            return 0.0;
        }

        let velocity = self.velocity(abs_flow);
        let f = self.friction_factor(abs_flow);
        let ld = self.geometry.ld_ratio();

        // Darcy-Weisbach nominal: ΔP_nominal = f × (L/D) × (ρ × v² / 2); ΔP_eff = f_dp × ΔP_nominal
        let dp_nominal = f * ld * self.fluid_density_kg_per_m3 * velocity * velocity / 2.0;
        let dp = dp_nominal * self.calib.z_dp;

        if flow_m3_per_s < 0.0 {
            -dp
        } else {
            dp
        }
    }

    /// 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.fluid_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.fluid_density_kg_per_m3
    }

    /// Returns the pipe geometry.
    pub fn geometry(&self) -> &PipeGeometry {
        &self.geometry
    }

    /// Returns the fluid density.
    pub fn fluid_density(&self) -> f64 {
        self.fluid_density_kg_per_m3
    }

    /// Returns the fluid viscosity.
    pub fn fluid_viscosity(&self) -> f64 {
        self.fluid_viscosity_pa_s
    }

    /// Returns calibration factors (f_dp for pressure drop scaling).
    pub fn calib(&self) -> &Calib {
        &self.calib
    }

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

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

impl Component for Pipe<Connected> {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // Layout: [0] = incoming edge (upstream→pipe), [1] = outgoing edge (pipe→downstream)
        // 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_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: constrain the outlet edge.
        if self.edge_coupled {
            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,
            ) {
                if residuals.len() < 2 {
                    return Err(ComponentError::InvalidResidualDimensions {
                        expected: 2,
                        actual: residuals.len(),
                    });
                }
                let dp = match self.operational_state {
                    OperationalState::Bypass => 0.0,
                    _ if self.design_dp_pa > 0.0 => self.calib.z_dp * self.design_dp_pa,
                    _ => {
                        // Darcy–Weisbach from geometry + live ṁ (design_dp unset/0).
                        let m = self.inlet_m_idx.map(|i| state[i]).unwrap_or(0.0);
                        let flow_m3 = m / self.fluid_density_kg_per_m3;
                        self.pressure_drop(flow_m3).abs()
                    }
                };
                // r0: pressure drop across the pipe
                residuals[0] = state[out_p] - (state[in_p] - dp);
                // r1: adiabatic pass-through (enthalpy conserved)
                residuals[1] = state[out_h] - state[in_h];
                return Ok(());
            }
            return Err(ComponentError::InvalidState(
                "Pipe edge-coupled model requires live inlet/outlet edge state indices".to_string(),
            ));
        }

        if residuals.len() != self.n_equations() {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: self.n_equations(),
                actual: residuals.len(),
            });
        }

        match self.operational_state {
            OperationalState::Off => {
                // Blocked pipe: no flow
                if state.is_empty() {
                    return Err(ComponentError::InvalidStateDimensions {
                        expected: 1,
                        actual: 0,
                    });
                }
                residuals[0] = state[0];
                return Ok(());
            }
            OperationalState::Bypass => {
                // No pressure drop (perfect pipe)
                let p_in = self.port_inlet.pressure().to_pascals();
                let p_out = self.port_outlet.pressure().to_pascals();
                residuals[0] = p_in - p_out;
                return Ok(());
            }
            OperationalState::On => {}
        }

        if state.is_empty() {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 1,
                actual: 0,
            });
        }

        let mass_flow_kg_s = state[0];
        let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;

        // Calculate pressure drop
        let dp_calc = self.pressure_drop(flow_m3_s);

        // Get actual pressure difference
        let p_in = self.port_inlet.pressure().to_pascals();
        let p_out = self.port_outlet.pressure().to_pascals();
        let dp_actual = p_in - p_out;

        // Residual: calculated drop - actual drop = 0
        residuals[0] = dp_calc - dp_actual;

        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        // Edge-coupled (P,h) model.
        if self.edge_coupled {
            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 - dp)  →  ∂r0/∂P_out = 1, ∂r0/∂P_in = -1
                jacobian.add_entry(0, out_p, 1.0);
                jacobian.add_entry(0, in_p, -1.0);
                // Darcy: dp depends on ṁ → ∂r0/∂ṁ = +∂(dp)/∂ṁ
                if self.design_dp_pa <= 0.0 {
                    if let Some(m_idx) = self.inlet_m_idx {
                        let m = state[m_idx];
                        let h = 1e-6_f64.max(m.abs() * 1e-5);
                        let rho = self.fluid_density_kg_per_m3;
                        let dp_plus = self.pressure_drop((m + h) / rho).abs();
                        let dp_minus = self.pressure_drop((m - h) / rho).abs();
                        let ddp_dm = (dp_plus - dp_minus) / (2.0 * h);
                        // r0 = P_out - P_in + dp(m)  →  ∂r0/∂m = ∂dp/∂m
                        jacobian.add_entry(0, m_idx, ddp_dm);
                    }
                }
                // r1 = h_out - h_in  →  ∂r1/∂h_out = 1, ∂r1/∂h_in = -1
                jacobian.add_entry(1, out_h, 1.0);
                jacobian.add_entry(1, in_h, -1.0);
            }
            return Ok(());
        }

        match self.operational_state {
            OperationalState::Off => {
                jacobian.add_entry(0, 0, 1.0);
                return Ok(());
            }
            OperationalState::Bypass => {
                jacobian.add_entry(0, 0, 0.0);
                return Ok(());
            }
            OperationalState::On => {}
        }

        if state.is_empty() {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 1,
                actual: 0,
            });
        }

        let mass_flow_kg_s = state[0];
        let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;

        // Numerical derivative of pressure drop with respect to mass flow
        let h = 1e-6_f64.max(mass_flow_kg_s.abs() * 1e-5);
        let dp_plus = self.pressure_drop(flow_m3_s + h / self.fluid_density_kg_per_m3);
        let dp_minus = self.pressure_drop(flow_m3_s - h / self.fluid_density_kg_per_m3);
        let dp_dm = (dp_plus - dp_minus) / (2.0 * h);

        jacobian.add_entry(0, 0, dp_dm);

        Ok(())
    }

    fn n_equations(&self) -> usize {
        if self.edge_coupled {
            2
        } else {
            1
        }
    }

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        if state.is_empty() {
            return Err(ComponentError::InvalidStateDimensions {
                expected: 1,
                actual: 0,
            });
        }
        let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
        Ok(vec![
            m,
            entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
        ])
    }

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

    fn port_enthalpies(
        &self,
        _state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        Ok(vec![
            self.port_inlet.enthalpy(),
            self.port_outlet.enthalpy(),
        ])
    }

    fn energy_transfers(
        &self,
        _state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        match self.operational_state {
            OperationalState::Off | OperationalState::Bypass | OperationalState::On => {
                // Pipes are adiabatic
                Some((
                    entropyk_core::Power::from_watts(0.0),
                    entropyk_core::Power::from_watts(0.0),
                ))
            }
        }
    }

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

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("Pipe")
            .with_param("circuitId", self.circuit_id.0)
            .with_param("lengthM", self.geometry.length_m)
            .with_param("diameterM", self.geometry.diameter_m)
            .with_param("roughnessM", self.geometry.roughness_m)
            .with_param("fluidDensityKgPerM3", self.fluid_density_kg_per_m3)
            .with_param("fluidViscosityPaS", self.fluid_viscosity_pa_s)
            .with_param(
                "calib",
                serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null),
            )
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        let mut c = self.calib().clone();
        if c.set_factor(factor, value) {
            self.set_calib(c);
            true
        } else {
            false
        }
    }
}

impl StateManageable for Pipe<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_geometry() -> PipeGeometry {
        PipeGeometry::steel(10.0, 0.05).unwrap()
    }

    fn create_test_pipe_connected() -> Pipe<Connected> {
        let geometry = create_test_geometry();
        let inlet = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );
        let outlet = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        // Water at 20°C: ρ ≈ 998 kg/m³, μ ≈ 0.001 Pa·s
        Pipe {
            geometry,
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            fluid_density_kg_per_m3: 998.0,
            fluid_viscosity_pa_s: 0.001,
            calib: Calib::default(),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            design_dp_pa: 0.0,
            edge_coupled: false,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            _state: PhantomData,
        }
    }

    #[test]
    fn test_pipe_geometry_creation() {
        let geo = create_test_geometry();
        assert_eq!(geo.length_m, 10.0);
        assert_eq!(geo.diameter_m, 0.05);
        assert_eq!(geo.ld_ratio(), 200.0);
    }

    #[test]
    fn test_pipe_geometry_area() {
        let geo = create_test_geometry();
        let area = geo.area();
        let expected = std::f64::consts::PI * 0.05 * 0.05 / 4.0;
        assert_relative_eq!(area, expected, epsilon = 1e-10);
    }

    #[test]
    fn test_pipe_geometry_invalid() {
        assert!(PipeGeometry::new(-1.0, 0.05, 0.001).is_err());
        assert!(PipeGeometry::new(10.0, -0.05, 0.001).is_err());
        assert!(PipeGeometry::new(10.0, 0.05, -0.001).is_err());
    }

    #[test]
    fn test_friction_factor_laminar() {
        // For Re = 1000 (laminar), f = 64/1000 = 0.064
        let f = friction_factor::haaland(0.001, 1000.0);
        assert_relative_eq!(f, 0.064, epsilon = 1e-10);
    }

    #[test]
    fn test_friction_factor_turbulent() {
        // For smooth pipe at Re = 100000
        let f = friction_factor::haaland(0.0, 100000.0);
        // Should be around 0.018 for this Reynolds number
        assert!(f > 0.01);
        assert!(f < 0.03);
    }

    #[test]
    fn test_friction_factor_transition() {
        // Near transition region (Re ≈ 2300)
        let f_lam = friction_factor::haaland(0.001, 2000.0);
        let f_turb = friction_factor::haaland(0.001, 3000.0);

        // Both should be positive and reasonable
        assert!(f_lam > 0.0);
        assert!(f_turb > 0.0);
    }

    #[test]
    fn test_friction_factor_transition_is_c1() {
        // The laminar→turbulent blend must be continuous in value AND slope
        // across the whole transition band [2300, 4000] — no PR#563-style kink.
        let rel = 0.001;
        let f = |re: f64| friction_factor::haaland(rel, re);
        let dfd = |re: f64| (f(re + 0.5) - f(re - 0.5)) / 1.0;

        // Value continuity across the band, including the former jump at 2300.
        let mut prev_v = f(2299.0);
        let mut prev_s = dfd(2299.0);
        for re_i in (2300..=4000).step_by(20) {
            let re = re_i as f64;
            let v = f(re);
            let s = dfd(re);
            assert!(v > 0.0 && v.is_finite());
            assert!(
                (v - prev_v).abs() < 1e-3,
                "friction value jump near Re={}: {} vs {}",
                re,
                v,
                prev_v
            );
            assert!(
                (s - prev_s).abs() < 1e-4,
                "friction slope jump near Re={}: {} vs {}",
                re,
                s,
                prev_s
            );
            prev_v = v;
            prev_s = s;
        }

        // Endpoints match the pure branches.
        assert_relative_eq!(f(2300.0), 64.0 / 2300.0, epsilon = 1e-9);
    }

    #[test]
    fn test_friction_factor_zero_flow_regularization() {
        // Re = 0 or very small must not cause division by zero (Story 3.5)
        let f0_haaland = friction_factor::haaland(0.001, 0.0);
        assert!(f0_haaland.is_finite());
        assert_relative_eq!(f0_haaland, 0.02, epsilon = 1e-10);

        let f_small_haaland = friction_factor::haaland(0.001, 0.5);
        assert!(f_small_haaland.is_finite());
    }

    #[test]
    fn test_pipe_pressure_drop_zero_and_small_flow() {
        let pipe = create_test_pipe_connected();
        let dp_zero = pipe.pressure_drop(0.0);
        assert_eq!(dp_zero, 0.0);
        let dp_tiny = pipe.pressure_drop(1e-15);
        assert!(dp_tiny.is_finite());
        assert!(dp_tiny >= 0.0);
    }

    #[test]
    fn test_pipe_small_nonzero_flow_continuity() {
        use entropyk_core::MIN_MASS_FLOW_REGULARIZATION_KG_S;
        let pipe = create_test_pipe_connected();

        let dp_zero = pipe.pressure_drop(0.0);
        let dp_epsilon =
            pipe.pressure_drop(MIN_MASS_FLOW_REGULARIZATION_KG_S / pipe.fluid_density());
        let dp_normal = pipe.pressure_drop(0.01);

        assert!(dp_zero.is_finite());
        assert!(dp_epsilon.is_finite());
        assert!(dp_normal.is_finite());

        assert!(
            dp_epsilon < dp_normal,
            "tiny flow should have smaller dp than normal"
        );
        assert!(
            dp_epsilon >= 0.0,
            "dp should be non-negative at epsilon flow"
        );
    }

    #[test]
    fn test_pipe_velocity() {
        let pipe = create_test_pipe_connected();
        let flow = 0.01; // 10 L/s
        let velocity = pipe.velocity(flow);

        // v = Q / A = 0.01 / (π × 0.05² / 4) ≈ 5.09 m/s
        let expected = 0.01 / pipe.geometry().area();
        assert_relative_eq!(velocity, expected, epsilon = 0.01);
    }

    #[test]
    fn test_pipe_reynolds() {
        let pipe = create_test_pipe_connected();
        let flow = 0.01;
        let re = pipe.reynolds_number(flow);

        // Re = ρ × v × D / μ
        // With water at typical flow, should be turbulent (> 4000)
        assert!(re > 4000.0);
        assert!(re < 1_000_000.0);
    }

    #[test]
    fn test_pipe_pressure_drop() {
        let pipe = create_test_pipe_connected();
        let flow = 0.005; // 5 L/s

        let dp = pipe.pressure_drop(flow);

        // Should be positive and reasonable (typically < 100 kPa for this pipe)
        assert!(dp > 0.0);
        assert!(dp < 200_000.0);
    }

    #[test]
    fn test_pipe_pressure_drop_scales() {
        let pipe = create_test_pipe_connected();

        let dp1 = pipe.pressure_drop(0.005);
        let dp2 = pipe.pressure_drop(0.010); // Double the flow

        // Pressure drop should increase (roughly quadruple for turbulent)
        assert!(dp2 > dp1);
    }

    #[test]
    fn test_edge_coupled_zero_design_dp_uses_darcy() {
        use crate::Component;
        let mut pipe = create_test_pipe_connected().with_design_pressure_drop_pa(0.0);
        // state layout: m, P_in, h_in, m_out, P_out, h_out
        pipe.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
        let m = 5.0_f64; // kg/s
        let flow_m3 = m / pipe.fluid_density();
        let dp_expected = pipe.pressure_drop(flow_m3).abs();
        assert!(
            dp_expected > 100.0,
            "test pipe should have meaningful Darcy ΔP"
        );

        let p_in = 300_000.0;
        let h = 100_000.0;
        let state = vec![m, p_in, h, m, p_in - dp_expected, h];
        let mut residuals = vec![0.0; 2];
        pipe.compute_residuals(&state, &mut residuals).unwrap();
        assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-6);
        assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-12);

        // Wrong P_out → residual equals ΔP error
        let state_bad = vec![m, p_in, h, m, p_in, h]; // isobaric — should fail
        pipe.compute_residuals(&state_bad, &mut residuals).unwrap();
        assert_relative_eq!(residuals[0], dp_expected, epsilon = 1.0);
    }

    #[test]
    fn test_f_dp_scales_pressure_drop() {
        let mut pipe = create_test_pipe_connected();
        let flow = 0.005;
        let dp_default = pipe.pressure_drop(flow);
        pipe.set_calib(Calib {
            z_dp: 1.1,
            ..Calib::default()
        });
        let dp_calib = pipe.pressure_drop(flow);
        assert_relative_eq!(dp_calib / dp_default, 1.1, epsilon = 1e-10);
    }

    #[test]
    fn test_pipe_component_n_equations() {
        let pipe = create_test_pipe_connected();
        assert_eq!(pipe.n_equations(), 1);
    }

    #[test]
    fn test_pipe_component_compute_residuals() {
        let pipe = create_test_pipe_connected();
        let state = vec![5.0]; // 5 kg/s
        let mut residuals = vec![0.0; 1];

        let result = pipe.compute_residuals(&state, &mut residuals);
        assert!(result.is_ok());
    }

    #[test]
    fn test_pipe_state_manageable() {
        let pipe = create_test_pipe_connected();
        assert_eq!(pipe.state(), OperationalState::On);
        assert!(pipe.can_transition_to(OperationalState::Off));
        assert!(pipe.can_transition_to(OperationalState::Bypass));
    }

    #[test]
    fn test_roughness_constants() {
        assert!(roughness::SMOOTH < roughness::STEEL_COMMERCIAL);
        assert!(roughness::STEEL_COMMERCIAL < roughness::CAST_IRON);
        assert!(roughness::PLASTIC < roughness::CONCRETE);
    }

    // Removed swamee_jain test as function was removed

    #[test]
    fn test_pipe_for_incompressible_creation() {
        let geometry = PipeGeometry::smooth(5.0, 0.025).unwrap();
        let inlet = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );
        let outlet = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );

        // ρ, μ from IncompressibleBackend at design point (e.g. water 20°C)
        let pipe = Pipe::for_incompressible(geometry, inlet, outlet, 998.0, 0.001).unwrap();

        assert_relative_eq!(pipe.fluid_density(), 998.0, epsilon = 1e-6);
        assert_relative_eq!(pipe.fluid_viscosity(), 0.001, epsilon = 1e-9);
        assert_eq!(pipe.fluid_id().as_str(), "Water");
    }

    #[test]
    fn test_pipe_for_incompressible_glycol() {
        // Glycol has different ρ, μ than water - user provides from backend
        let geometry = PipeGeometry::smooth(5.0, 0.025).unwrap();
        let inlet = Port::new(
            FluidId::new("EthyleneGlycol30"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );
        let outlet = Port::new(
            FluidId::new("EthyleneGlycol30"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );

        let pipe = Pipe::for_incompressible(geometry, inlet, outlet, 1055.0, 0.0022).unwrap();
        assert_relative_eq!(pipe.fluid_density(), 1055.0, epsilon = 1e-6);
        assert_eq!(pipe.fluid_id().as_str(), "EthyleneGlycol30");
    }

    #[test]
    fn test_pipe_for_refrigerant_creation() {
        let geometry = PipeGeometry::smooth(3.0, 0.012).unwrap();
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(250000.0),
        );

        let pipe = Pipe::for_refrigerant(geometry, inlet, outlet, 1140.0, 0.0002).unwrap();

        assert_eq!(pipe.fluid_id().as_str(), "R134a");
        assert_relative_eq!(pipe.fluid_density(), 1140.0, epsilon = 1e-6);
        assert_relative_eq!(pipe.fluid_viscosity(), 0.0002, epsilon = 1e-9);
    }

    #[test]
    fn test_pipe_inlet_outlet_same_fluid() {
        // Pipe::new and helpers require inlet/outlet same FluidId
        let geometry = PipeGeometry::smooth(5.0, 0.025).unwrap();
        let water_inlet = Port::new(
            FluidId::new("Water"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );
        let r134a_outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(2.0),
            Enthalpy::from_joules_per_kg(100000.0),
        );

        let result = Pipe::for_incompressible(geometry, water_inlet, r134a_outlet, 998.0, 0.001);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("same fluid"));
    }
}
