//! Python wrappers for Entropyk thermodynamic components.
//!
//! Components are wrapped with simplified Pythonic constructors.
//! Real thermodynamic components use the python_components module
//! with actual CoolProp-based physics.

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;

use entropyk_components::{Component, ConnectedPort};

// =============================================================================
// Compressor - Real AHRI 540 Implementation
// =============================================================================

/// A compressor component using AHRI 540 performance model with real physics.
///
/// Uses CoolProp for thermodynamic property calculations.
///
/// Example::
///
///     comp = Compressor(
///         m1=0.85, m2=2.5,
///         m3=500.0, m4=1500.0, m5=-2.5, m6=1.8,
///         m7=600.0, m8=1600.0, m9=-3.0, m10=2.0,
///         speed_rpm=2900.0,
///         displacement=0.0001,
///         efficiency=0.85,
///         fluid="R134a",
///     )
#[pyclass(name = "Compressor", module = "entropyk")]
#[derive(Clone)]
pub struct PyCompressor {
    pub(crate) inner: entropyk_components::PyCompressorReal,
}

#[pymethods]
impl PyCompressor {
    /// Create a Compressor with AHRI 540 coefficients.
    #[new]
    #[pyo3(signature = (
        m1=0.85, m2=2.5,
        m3=500.0, m4=1500.0, m5=-2.5, m6=1.8,
        m7=600.0, m8=1600.0, m9=-3.0, m10=2.0,
        speed_rpm=2900.0,
        displacement=0.0001,
        efficiency=0.85,
        fluid="R134a"
    ))]
    #[allow(clippy::too_many_arguments)]
    fn new(
        m1: f64,
        m2: f64,
        m3: f64,
        m4: f64,
        m5: f64,
        m6: f64,
        m7: f64,
        m8: f64,
        m9: f64,
        m10: f64,
        speed_rpm: f64,
        displacement: f64,
        efficiency: f64,
        fluid: &str,
    ) -> PyResult<Self> {
        if speed_rpm <= 0.0 {
            return Err(PyValueError::new_err("speed_rpm must be positive"));
        }
        if displacement <= 0.0 {
            return Err(PyValueError::new_err("displacement must be positive"));
        }
        if !(0.0..=1.0).contains(&efficiency) {
            return Err(PyValueError::new_err(
                "efficiency must be between 0.0 and 1.0",
            ));
        }

        let inner =
            entropyk_components::PyCompressorReal::new(fluid, speed_rpm, displacement, efficiency)
                .with_coefficients(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);

        Ok(PyCompressor { inner })
    }

    /// Speed in RPM.
    #[getter]
    fn speed(&self) -> f64 {
        self.inner.speed_rpm
    }

    /// Isentropic efficiency (0–1).
    #[getter]
    fn efficiency_value(&self) -> f64 {
        self.inner.efficiency
    }

    /// Fluid name.
    #[getter]
    fn fluid_name(&self) -> String {
        self.inner.fluid.0.clone()
    }

    fn __repr__(&self) -> String {
        format!(
            "Compressor(speed={:.0} RPM, η={:.2}, fluid={})",
            self.inner.speed_rpm, self.inner.efficiency, self.inner.fluid.0
        )
    }
}

impl PyCompressor {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(self.inner.clone())
    }
}

// =============================================================================
// ScrewEconomizerCompressor - 3-port screw compressor with economizer
// =============================================================================

/// A screw compressor with an intermediate economizer injection port.
#[pyclass(name = "ScrewEconomizerCompressor", module = "entropyk")]
#[derive(Clone)]
pub struct PyScrewEconomizerCompressor {
    pub(crate) fluid: String,
    pub(crate) nominal_frequency_hz: f64,
    pub(crate) frequency_hz: f64,
    pub(crate) mechanical_efficiency: f64,
    pub(crate) economizer_fraction: f64,
}

#[pymethods]
impl PyScrewEconomizerCompressor {
    /// Create a screw compressor with a fixed economizer flow fraction.
    #[new]
    #[pyo3(signature = (
        fluid="R134a",
        nominal_frequency_hz=50.0,
        frequency_hz=50.0,
        mechanical_efficiency=0.92,
        economizer_fraction=0.12
    ))]
    fn new(
        fluid: &str,
        nominal_frequency_hz: f64,
        frequency_hz: f64,
        mechanical_efficiency: f64,
        economizer_fraction: f64,
    ) -> PyResult<Self> {
        if nominal_frequency_hz <= 0.0 {
            return Err(PyValueError::new_err(
                "nominal_frequency_hz must be positive",
            ));
        }
        if frequency_hz < 0.0 {
            return Err(PyValueError::new_err("frequency_hz cannot be negative"));
        }
        if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
            return Err(PyValueError::new_err(
                "mechanical_efficiency must be in (0.0, 1.0]",
            ));
        }
        if !(0.0..=0.4).contains(&economizer_fraction) {
            return Err(PyValueError::new_err(
                "economizer_fraction must be between 0.0 and 0.4",
            ));
        }
        Ok(Self {
            fluid: fluid.to_string(),
            nominal_frequency_hz,
            frequency_hz,
            mechanical_efficiency,
            economizer_fraction,
        })
    }

    #[getter]
    fn fluid(&self) -> String {
        self.fluid.clone()
    }

    #[getter]
    fn frequency_hz(&self) -> f64 {
        self.frequency_hz
    }

    #[setter]
    fn set_frequency_hz(&mut self, value: f64) -> PyResult<()> {
        if value < 0.0 {
            return Err(PyValueError::new_err("frequency_hz cannot be negative"));
        }
        self.frequency_hz = value;
        Ok(())
    }

    #[getter]
    fn mechanical_efficiency(&self) -> f64 {
        self.mechanical_efficiency
    }

    #[getter]
    fn economizer_fraction(&self) -> f64 {
        self.economizer_fraction
    }

    fn __repr__(&self) -> String {
        format!(
            "ScrewEconomizerCompressor(fluid={}, frequency={:.1} Hz, eco_fraction={:.3})",
            self.fluid, self.frequency_hz, self.economizer_fraction
        )
    }
}

impl PyScrewEconomizerCompressor {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        use entropyk_components::port::{FluidId, Port};
        use entropyk_components::{
            Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves,
        };
        use entropyk_core::{Enthalpy, Pressure};

        fn connected_port(fluid: &str, pressure_bar: f64, enthalpy_kj_kg: f64) -> ConnectedPort {
            let a = Port::new(
                FluidId::new(fluid),
                Pressure::from_bar(pressure_bar),
                Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
            );
            let b = Port::new(
                FluidId::new(fluid),
                Pressure::from_bar(pressure_bar),
                Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
            );
            a.connect(b)
                .expect("matching screw compressor ports should connect")
                .0
        }

        let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
            Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01),
            Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
            self.economizer_fraction,
        );
        let mut component = ScrewEconomizerCompressor::new(
            curves,
            &self.fluid,
            self.nominal_frequency_hz,
            self.mechanical_efficiency,
            connected_port(&self.fluid, 3.2, 400.0),
            connected_port(&self.fluid, 12.8, 440.0),
            connected_port(&self.fluid, 6.4, 260.0),
        )
        .expect("validated screw compressor parameters should build");
        component
            .set_frequency_hz(self.frequency_hz)
            .expect("validated screw compressor frequency should set");
        Box::new(component)
    }
}

// =============================================================================
// Condenser - Real Heat Exchanger with Water Side
// =============================================================================

/// A condenser with water-side heat transfer.
///
/// Uses ε-NTU method with CoolProp for refrigerant properties.
///
/// Example::
///
///     cond = Condenser(ua=5000.0, fluid="R134a", water_temp=30.0, water_flow=0.5)
#[pyclass(name = "Condenser", module = "entropyk")]
#[derive(Clone)]
pub struct PyCondenser {
    pub(crate) ua: f64,
    pub(crate) fluid: String,
    pub(crate) water_temp: f64,
    pub(crate) water_flow: f64,
}

#[pymethods]
impl PyCondenser {
    #[new]
    #[pyo3(signature = (ua=5000.0, fluid="R134a", water_temp=30.0, water_flow=0.5))]
    fn new(ua: f64, fluid: &str, water_temp: f64, water_flow: f64) -> PyResult<Self> {
        if ua <= 0.0 {
            return Err(PyValueError::new_err("ua must be positive"));
        }
        if water_flow <= 0.0 {
            return Err(PyValueError::new_err("water_flow must be positive"));
        }
        Ok(PyCondenser {
            ua,
            fluid: fluid.to_string(),
            water_temp,
            water_flow,
        })
    }

    /// Thermal conductance UA in W/K.
    #[getter]
    fn ua_value(&self) -> f64 {
        self.ua
    }

    fn __repr__(&self) -> String {
        format!(
            "Condenser(UA={:.1} W/K, fluid={}, water={:.1}°C)",
            self.ua, self.fluid, self.water_temp
        )
    }
}

impl PyCondenser {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyHeatExchangerReal::condenser(
            self.ua,
            &self.fluid,
            self.water_temp,
            self.water_flow,
        ))
    }
}

// =============================================================================
// Evaporator - Real Heat Exchanger with Water Side
// =============================================================================

/// An evaporator with water-side heat transfer.
///
/// Uses ε-NTU method with CoolProp for refrigerant properties.
///
/// Example::
///
///     evap = Evaporator(ua=3000.0, fluid="R134a", water_temp=12.0, water_flow=0.4)
#[pyclass(name = "Evaporator", module = "entropyk")]
#[derive(Clone)]
pub struct PyEvaporator {
    pub(crate) ua: f64,
    pub(crate) fluid: String,
    pub(crate) water_temp: f64,
    pub(crate) water_flow: f64,
}

#[pymethods]
impl PyEvaporator {
    #[new]
    #[pyo3(signature = (ua=3000.0, fluid="R134a", water_temp=12.0, water_flow=0.4))]
    fn new(ua: f64, fluid: &str, water_temp: f64, water_flow: f64) -> PyResult<Self> {
        if ua <= 0.0 {
            return Err(PyValueError::new_err("ua must be positive"));
        }
        if water_flow <= 0.0 {
            return Err(PyValueError::new_err("water_flow must be positive"));
        }
        Ok(PyEvaporator {
            ua,
            fluid: fluid.to_string(),
            water_temp,
            water_flow,
        })
    }

    /// Thermal conductance UA in W/K.
    #[getter]
    fn ua_value(&self) -> f64 {
        self.ua
    }

    fn __repr__(&self) -> String {
        format!(
            "Evaporator(UA={:.1} W/K, fluid={}, water={:.1}°C)",
            self.ua, self.fluid, self.water_temp
        )
    }
}

impl PyEvaporator {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyHeatExchangerReal::evaporator(
            self.ua,
            &self.fluid,
            self.water_temp,
            self.water_flow,
        ))
    }
}

// =============================================================================
// Economizer
// =============================================================================

/// An economizer (subcooler / internal heat exchanger) component.
#[pyclass(name = "Economizer", module = "entropyk")]
#[derive(Clone)]
pub struct PyEconomizer {
    pub(crate) ua: f64,
}

#[pymethods]
impl PyEconomizer {
    #[new]
    #[pyo3(signature = (ua=2000.0))]
    fn new(ua: f64) -> PyResult<Self> {
        if ua <= 0.0 {
            return Err(PyValueError::new_err("ua must be positive"));
        }
        Ok(PyEconomizer { ua })
    }

    fn __repr__(&self) -> String {
        format!("Economizer(UA={:.1} W/K)", self.ua)
    }
}

impl PyEconomizer {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::Economizer::new(self.ua))
    }
}

// =============================================================================
// Expansion Valve - Real Isenthalpic
// =============================================================================

/// An expansion valve with isenthalpic throttling.
///
/// Example::
///
///     valve = ExpansionValve(fluid="R134a", opening=0.5)
#[pyclass(name = "ExpansionValve", module = "entropyk")]
#[derive(Clone)]
pub struct PyExpansionValve {
    pub(crate) fluid: String,
    pub(crate) opening: f64,
}

#[pymethods]
impl PyExpansionValve {
    #[new]
    #[pyo3(signature = (fluid="R134a", opening=0.5))]
    fn new(fluid: &str, opening: f64) -> PyResult<Self> {
        if !(0.0..=1.0).contains(&opening) {
            return Err(PyValueError::new_err("opening must be between 0.0 and 1.0"));
        }
        Ok(PyExpansionValve {
            fluid: fluid.to_string(),
            opening,
        })
    }

    /// Fluid name.
    #[getter]
    fn fluid_name(&self) -> &str {
        &self.fluid
    }

    /// Valve opening (0–1).
    #[getter]
    fn opening_value(&self) -> f64 {
        self.opening
    }

    fn __repr__(&self) -> String {
        format!(
            "ExpansionValve(fluid={}, opening={:.2})",
            self.fluid, self.opening
        )
    }
}

impl PyExpansionValve {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyExpansionValveReal::new(
            &self.fluid,
            self.opening,
        ))
    }
}

// =============================================================================
// Pipe - Real Pressure Drop
// =============================================================================

/// A pipe component with Darcy-Weisbach pressure drop.
#[pyclass(name = "Pipe", module = "entropyk")]
#[derive(Clone)]
pub struct PyPipe {
    pub(crate) inner: entropyk_components::PyPipeReal,
}

#[pymethods]
impl PyPipe {
    #[new]
    #[pyo3(signature = (length=10.0, diameter=0.05, fluid="R134a"))]
    fn new(length: f64, diameter: f64, fluid: &str) -> PyResult<Self> {
        if length <= 0.0 {
            return Err(PyValueError::new_err("length must be positive"));
        }
        if diameter <= 0.0 {
            return Err(PyValueError::new_err("diameter must be positive"));
        }
        Ok(PyPipe {
            inner: entropyk_components::PyPipeReal::new(length, diameter, fluid),
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "Pipe(L={:.2}m, D={:.4}m, fluid={})",
            self.inner.length, self.inner.diameter, self.inner.fluid.0
        )
    }
}

impl PyPipe {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(self.inner.clone())
    }
}

// =============================================================================
// Pump
// =============================================================================

/// A pump component for liquid flow.
#[pyclass(name = "Pump", module = "entropyk")]
#[derive(Clone)]
pub struct PyPump {
    pub(crate) pressure_rise_pa: f64,
    pub(crate) efficiency: f64,
}

#[pymethods]
impl PyPump {
    #[new]
    #[pyo3(signature = (pressure_rise_pa=200000.0, efficiency=0.75))]
    fn new(pressure_rise_pa: f64, efficiency: f64) -> PyResult<Self> {
        if pressure_rise_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_rise_pa must be positive"));
        }
        if !(0.0..=1.0).contains(&efficiency) {
            return Err(PyValueError::new_err(
                "efficiency must be between 0.0 and 1.0",
            ));
        }
        Ok(PyPump {
            pressure_rise_pa,
            efficiency,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "Pump(ΔP={:.0} Pa, η={:.2})",
            self.pressure_rise_pa, self.efficiency
        )
    }
}

impl PyPump {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyPipeReal::new(1.0, 0.05, "Water"))
    }
}

// =============================================================================
// Fan
// =============================================================================

/// A fan component for air flow.
#[pyclass(name = "Fan", module = "entropyk")]
#[derive(Clone)]
pub struct PyFan {
    pub(crate) pressure_rise_pa: f64,
    pub(crate) efficiency: f64,
}

#[pymethods]
impl PyFan {
    #[new]
    #[pyo3(signature = (pressure_rise_pa=500.0, efficiency=0.65))]
    fn new(pressure_rise_pa: f64, efficiency: f64) -> PyResult<Self> {
        if pressure_rise_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_rise_pa must be positive"));
        }
        if !(0.0..=1.0).contains(&efficiency) {
            return Err(PyValueError::new_err(
                "efficiency must be between 0.0 and 1.0",
            ));
        }
        Ok(PyFan {
            pressure_rise_pa,
            efficiency,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "Fan(ΔP={:.0} Pa, η={:.2})",
            self.pressure_rise_pa, self.efficiency
        )
    }
}

impl PyFan {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyPipeReal::new(1.0, 0.1, "Air"))
    }
}

// =============================================================================
// FlowSplitter
// =============================================================================

/// A flow splitter that divides a stream into branches.
#[pyclass(name = "FlowSplitter", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowSplitter {
    pub(crate) n_outlets: usize,
}

#[pymethods]
impl PyFlowSplitter {
    #[new]
    #[pyo3(signature = (n_outlets=2))]
    fn new(n_outlets: usize) -> PyResult<Self> {
        if n_outlets < 2 {
            return Err(PyValueError::new_err("n_outlets must be >= 2"));
        }
        Ok(PyFlowSplitter { n_outlets })
    }

    fn __repr__(&self) -> String {
        format!("FlowSplitter(n_outlets={})", self.n_outlets)
    }
}

impl PyFlowSplitter {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyFlowSplitterReal::new(self.n_outlets))
    }
}

// =============================================================================
// FlowMerger
// =============================================================================

/// A flow merger that combines branches into one.
#[pyclass(name = "FlowMerger", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowMerger {
    pub(crate) n_inlets: usize,
}

#[pymethods]
impl PyFlowMerger {
    #[new]
    #[pyo3(signature = (n_inlets=2))]
    fn new(n_inlets: usize) -> PyResult<Self> {
        if n_inlets < 2 {
            return Err(PyValueError::new_err("n_inlets must be >= 2"));
        }
        Ok(PyFlowMerger { n_inlets })
    }

    fn __repr__(&self) -> String {
        format!("FlowMerger(n_inlets={})", self.n_inlets)
    }
}

impl PyFlowMerger {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyFlowMergerReal::new(self.n_inlets))
    }
}

// =============================================================================
// FlowSource - Real Boundary Condition
// =============================================================================

/// A boundary condition representing a mass flow source.
///
/// Example::
///
///     source = FlowSource(pressure_pa=101325.0, temperature_k=300.0, fluid="Water")
#[pyclass(name = "FlowSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowSource {
    pub(crate) pressure_pa: f64,
    pub(crate) temperature_k: f64,
    pub(crate) fluid: String,
}

#[pymethods]
impl PyFlowSource {
    #[new]
    #[pyo3(signature = (pressure_pa=101325.0, temperature_k=300.0, fluid="Water"))]
    fn new(py: Python<'_>, pressure_pa: f64, temperature_k: f64, fluid: &str) -> PyResult<Self> {
        let warnings = py.import_bound("warnings")?;
        warnings.call_method1(
            "warn",
            (
                "FlowSource is deprecated. Use RefrigerantSource, BrineSource, or AirSource instead.",
                py.get_type_bound::<pyo3::exceptions::PyDeprecationWarning>(),
                2,
            ),
        )?;

        if pressure_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_pa must be positive"));
        }
        if temperature_k <= 0.0 {
            return Err(PyValueError::new_err("temperature_k must be positive"));
        }
        Ok(PyFlowSource {
            pressure_pa,
            temperature_k,
            fluid: fluid.to_string(),
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "FlowSource(P={:.0} Pa, T={:.1} K, fluid={})",
            self.pressure_pa, self.temperature_k, self.fluid
        )
    }
}

impl PyFlowSource {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyFlowSourceReal::new(
            &self.fluid,
            self.pressure_pa,
            self.temperature_k,
        ))
    }
}

// =============================================================================
// FlowSink
// =============================================================================

/// A boundary condition representing a mass flow sink.
#[pyclass(name = "FlowSink", module = "entropyk")]
#[derive(Clone, Default)]

pub struct PyFlowSink;

#[pymethods]
impl PyFlowSink {
    #[new]
    fn new(py: Python<'_>) -> PyResult<Self> {
        let warnings = py.import_bound("warnings")?;
        warnings.call_method1(
            "warn",
            (
                "FlowSink is deprecated. Use RefrigerantSink, BrineSink, or AirSink instead.",
                py.get_type_bound::<pyo3::exceptions::PyDeprecationWarning>(),
                2,
            ),
        )?;
        Ok(PyFlowSink)
    }

    fn __repr__(&self) -> String {
        "FlowSink()".to_string()
    }
}

impl PyFlowSink {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyFlowSinkReal::default())
    }
}

// =============================================================================
// OperationalState
// =============================================================================

/// Operational state of a component: On, Off, or Bypass.
#[pyclass(name = "OperationalState", module = "entropyk")]
#[derive(Clone)]
pub struct PyOperationalState {
    pub(crate) inner: entropyk_components::OperationalState,
}

#[pymethods]
impl PyOperationalState {
    /// Create an OperationalState. Valid values: "on", "off", "bypass".
    #[new]
    fn new(state: &str) -> PyResult<Self> {
        let inner = match state.to_lowercase().as_str() {
            "on" => entropyk_components::OperationalState::On,
            "off" => entropyk_components::OperationalState::Off,
            "bypass" => entropyk_components::OperationalState::Bypass,
            _ => {
                return Err(PyValueError::new_err(
                    "state must be one of: 'on', 'off', 'bypass'",
                ))
            }
        };
        Ok(PyOperationalState { inner })
    }

    fn __repr__(&self) -> String {
        format!("OperationalState({:?})", self.inner)
    }

    fn __str__(&self) -> String {
        format!("{:?}", self.inner)
    }

    fn __eq__(&self, other: &PyOperationalState) -> bool {
        self.inner == other.inner
    }
}

// =============================================================================
// Refrigerant Boundary Conditions
// =============================================================================

/// A boundary condition representing a refrigerant mass flow source.
#[pyclass(name = "RefrigerantSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyRefrigerantSource {
    pub(crate) fluid: String,
    pub(crate) p_set_pa: f64,
    pub(crate) quality: f64,
}

#[pymethods]
impl PyRefrigerantSource {
    #[new]
    #[pyo3(signature = (fluid="R410A", pressure_pa=101325.0, quality=1.0))]
    fn new(fluid: &str, pressure_pa: f64, quality: f64) -> PyResult<Self> {
        if pressure_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_pa must be positive"));
        }
        if !(0.0..=1.0).contains(&quality) {
            return Err(PyValueError::new_err("quality must be between 0.0 and 1.0"));
        }
        Ok(PyRefrigerantSource {
            fluid: fluid.to_string(),
            p_set_pa: pressure_pa,
            quality,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "RefrigerantSource(fluid={}, P={:.0} Pa, q={:.2})",
            self.fluid, self.p_set_pa, self.quality
        )
    }
}

impl PyRefrigerantSource {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyRefrigerantSourceReal::new(
            &self.fluid,
            self.p_set_pa,
            self.quality,
        ))
    }
}

/// A boundary condition representing a refrigerant mass flow sink.
#[pyclass(name = "RefrigerantSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyRefrigerantSink {
    pub(crate) fluid: String,
    pub(crate) p_back_pa: f64,
    pub(crate) quality_opt: Option<f64>,
}

#[pymethods]
impl PyRefrigerantSink {
    #[new]
    #[pyo3(signature = (fluid="R410A", p_back_pa=101325.0, quality=None))]
    fn new(fluid: &str, p_back_pa: f64, quality: Option<f64>) -> PyResult<Self> {
        if p_back_pa <= 0.0 {
            return Err(PyValueError::new_err("p_back_pa must be positive"));
        }
        if let Some(q) = quality {
            if !(0.0..=1.0).contains(&q) {
                return Err(PyValueError::new_err("quality must be between 0.0 and 1.0"));
            }
        }
        Ok(PyRefrigerantSink {
            fluid: fluid.to_string(),
            p_back_pa,
            quality_opt: quality,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "RefrigerantSink(fluid={}, P_back={:.0} Pa, q={:?})",
            self.fluid, self.p_back_pa, self.quality_opt
        )
    }
}

impl PyRefrigerantSink {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyRefrigerantSinkReal::new(
            &self.fluid,
            self.p_back_pa,
            self.quality_opt,
        ))
    }
}

// =============================================================================
// Brine Boundary Conditions
// =============================================================================

/// A boundary condition representing a brine mass flow source.
#[pyclass(name = "BrineSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyBrineSource {
    pub(crate) fluid: String,
    pub(crate) concentration: f64,
    pub(crate) temperature_k: f64,
    pub(crate) pressure_pa: f64,
}

#[pymethods]
impl PyBrineSource {
    #[new]
    #[pyo3(signature = (fluid="Water", concentration=0.0, temperature_k=300.0, pressure_pa=101325.0))]
    fn new(
        fluid: &str,
        concentration: f64,
        temperature_k: f64,
        pressure_pa: f64,
    ) -> PyResult<Self> {
        if pressure_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_pa must be positive"));
        }
        if temperature_k <= 0.0 {
            return Err(PyValueError::new_err("temperature_k must be positive"));
        }
        if !(0.0..=1.0).contains(&concentration) {
            return Err(PyValueError::new_err(
                "concentration must be between 0.0 and 1.0",
            ));
        }
        Ok(PyBrineSource {
            fluid: fluid.to_string(),
            concentration,
            temperature_k,
            pressure_pa,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "BrineSource(fluid={}, c={:.2}, T={:.1} K, P={:.0} Pa)",
            self.fluid, self.concentration, self.temperature_k, self.pressure_pa
        )
    }
}

impl PyBrineSource {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyBrineSourceReal::new(
            &self.fluid,
            self.concentration,
            self.temperature_k,
            self.pressure_pa,
        ))
    }
}

/// A boundary condition representing a brine mass flow sink.
#[pyclass(name = "BrineSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyBrineSink {
    pub(crate) p_back_pa: f64,
}

#[pymethods]
impl PyBrineSink {
    #[new]
    #[pyo3(signature = (p_back_pa=101325.0))]
    fn new(p_back_pa: f64) -> PyResult<Self> {
        if p_back_pa <= 0.0 {
            return Err(PyValueError::new_err("p_back_pa must be positive"));
        }
        Ok(PyBrineSink { p_back_pa })
    }

    fn __repr__(&self) -> String {
        format!("BrineSink(P_back={:.0} Pa)", self.p_back_pa)
    }
}

impl PyBrineSink {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyBrineSinkReal::new(self.p_back_pa))
    }
}

// =============================================================================
// Air Boundary Conditions
// =============================================================================

/// A boundary condition representing an air mass flow source.
#[pyclass(name = "AirSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyAirSource {
    pub(crate) temperature_k: f64,
    pub(crate) relative_humidity: f64,
    pub(crate) pressure_pa: f64,
}

#[pymethods]
impl PyAirSource {
    #[new]
    #[pyo3(signature = (temperature_k=300.0, relative_humidity=0.5, pressure_pa=101325.0))]
    fn new(temperature_k: f64, relative_humidity: f64, pressure_pa: f64) -> PyResult<Self> {
        if pressure_pa <= 0.0 {
            return Err(PyValueError::new_err("pressure_pa must be positive"));
        }
        if temperature_k <= 0.0 {
            return Err(PyValueError::new_err("temperature_k must be positive"));
        }
        if !(0.0..=1.0).contains(&relative_humidity) {
            return Err(PyValueError::new_err(
                "relative_humidity must be between 0.0 and 1.0",
            ));
        }
        Ok(PyAirSource {
            temperature_k,
            relative_humidity,
            pressure_pa,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "AirSource(T={:.1} K, RH={:.2}, P={:.0} Pa)",
            self.temperature_k, self.relative_humidity, self.pressure_pa
        )
    }
}

impl PyAirSource {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyAirSourceReal::new(
            self.temperature_k,
            self.relative_humidity,
            self.pressure_pa,
        ))
    }
}

/// A boundary condition representing an air mass flow sink.
#[pyclass(name = "AirSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyAirSink {
    pub(crate) p_back_pa: f64,
}

#[pymethods]
impl PyAirSink {
    #[new]
    #[pyo3(signature = (p_back_pa=101325.0))]
    fn new(p_back_pa: f64) -> PyResult<Self> {
        if p_back_pa <= 0.0 {
            return Err(PyValueError::new_err("p_back_pa must be positive"));
        }
        Ok(PyAirSink { p_back_pa })
    }

    fn __repr__(&self) -> String {
        format!("AirSink(P_back={:.0} Pa)", self.p_back_pa)
    }
}

impl PyAirSink {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        Box::new(entropyk_components::PyAirSinkReal::new(self.p_back_pa))
    }
}

/// Capillary tube (1D segmented ΔP, isenthalpic).
#[pyclass(name = "CapillaryTube", module = "entropyk")]
#[derive(Clone)]
pub struct PyCapillaryTube {
    fluid: String,
    diameter_m: f64,
    length_m: f64,
    n_segments: usize,
}

#[pymethods]
impl PyCapillaryTube {
    #[new]
    #[pyo3(signature = (fluid="R134a", diameter_m=0.001, length_m=2.0, n_segments=20))]
    fn new(fluid: &str, diameter_m: f64, length_m: f64, n_segments: usize) -> PyResult<Self> {
        if diameter_m <= 0.0 || length_m <= 0.0 || n_segments < 2 {
            return Err(PyValueError::new_err("invalid capillary geometry"));
        }
        Ok(Self {
            fluid: fluid.to_string(),
            diameter_m,
            length_m,
            n_segments,
        })
    }

    fn __repr__(&self) -> String {
        format!(
            "CapillaryTube(D={:.3}mm, L={:.2}m)",
            self.diameter_m * 1000.0,
            self.length_m
        )
    }
}

impl PyCapillaryTube {
    pub(crate) fn build(&self) -> Box<dyn Component> {
        use entropyk_components::port::{FluidId, Port};
        use entropyk_components::{CapillaryGeometry, CapillaryTube};
        use entropyk_core::{Enthalpy, Pressure};
        let geom = CapillaryGeometry {
            diameter_m: self.diameter_m,
            length_m: self.length_m,
            n_segments: self.n_segments,
        };
        let p = Pressure::from_bar(10.0);
        let h = Enthalpy::from_joules_per_kg(250_000.0);
        let fid = FluidId::new(&self.fluid);
        let tube = CapillaryTube::new(
            geom,
            Port::new(fid.clone(), p, h),
            Port::new(fid.clone(), p, h),
        )
        .and_then(|c| c.connect(Port::new(fid.clone(), p, h), Port::new(fid, p, h)))
        .expect("CapillaryTube construction");
        Box::new(tube)
    }
}

/// Fan-coil unit rating (ε-NTU + BPF).
#[pyclass(name = "FanCoilUnit", module = "entropyk")]
#[derive(Clone)]
pub struct PyFanCoilUnit {
    ua: f64,
    bpf: f64,
}

#[pymethods]
impl PyFanCoilUnit {
    #[new]
    #[pyo3(signature = (ua=8000.0, bypass_factor=0.15))]
    fn new(ua: f64, bypass_factor: f64) -> Self {
        Self {
            ua,
            bpf: bypass_factor,
        }
    }

    /// Rate total / sensible / latent capacity [W] and leaving air temp [K].
    #[pyo3(signature = (
        t_air_in_k, t_dew_in_k, t_water_in_k, c_air, c_water, t_adp_k
    ))]
    fn rate(
        &self,
        t_air_in_k: f64,
        t_dew_in_k: f64,
        t_water_in_k: f64,
        c_air: f64,
        c_water: f64,
        t_adp_k: f64,
    ) -> (f64, f64, f64, f64, f64) {
        use entropyk_components::heat_exchanger::{FanCoilRatingInput, FanCoilUnit};
        let fcu = FanCoilUnit::new(self.ua).with_bypass_factor(self.bpf);
        let r = fcu.rate(&FanCoilRatingInput {
            t_air_in_k,
            t_dew_in_k,
            t_water_in_k,
            c_air,
            c_water,
            bypass_factor: self.bpf,
            t_adp_k,
        });
        (
            r.q_total_w,
            r.q_sensible_w,
            r.q_latent_w,
            r.t_air_out_k,
            r.shr,
        )
    }

    fn __repr__(&self) -> String {
        format!("FanCoilUnit(UA={:.0}, BPF={:.2})", self.ua, self.bpf)
    }
}

// =============================================================================
// Component enum for type-erasure
// =============================================================================

/// Internal enum to hold any Python component wrapper.
#[derive(Clone)]
pub(crate) enum AnyPyComponent {
    Compressor(PyCompressor),
    ScrewEconomizerCompressor(PyScrewEconomizerCompressor),
    Condenser(PyCondenser),
    Evaporator(PyEvaporator),
    Economizer(PyEconomizer),
    ExpansionValve(PyExpansionValve),
    Pipe(PyPipe),
    Pump(PyPump),
    Fan(PyFan),
    FlowSplitter(PyFlowSplitter),
    FlowMerger(PyFlowMerger),
    FlowSource(PyFlowSource),
    FlowSink(PyFlowSink),
    RefrigerantSource(PyRefrigerantSource),
    RefrigerantSink(PyRefrigerantSink),
    BrineSource(PyBrineSource),
    BrineSink(PyBrineSink),
    AirSource(PyAirSource),
    AirSink(PyAirSink),
    CapillaryTube(PyCapillaryTube),
}

impl AnyPyComponent {
    /// Build the Rust component to insert into a System.
    pub(crate) fn build(&self) -> Box<dyn Component> {
        match self {
            AnyPyComponent::Compressor(c) => c.build(),
            AnyPyComponent::ScrewEconomizerCompressor(c) => c.build(),
            AnyPyComponent::Condenser(c) => c.build(),
            AnyPyComponent::Evaporator(c) => c.build(),
            AnyPyComponent::Economizer(c) => c.build(),
            AnyPyComponent::ExpansionValve(c) => c.build(),
            AnyPyComponent::Pipe(c) => c.build(),
            AnyPyComponent::Pump(c) => c.build(),
            AnyPyComponent::Fan(c) => c.build(),
            AnyPyComponent::FlowSplitter(c) => c.build(),
            AnyPyComponent::FlowMerger(c) => c.build(),
            AnyPyComponent::FlowSource(c) => c.build(),
            AnyPyComponent::FlowSink(c) => c.build(),
            AnyPyComponent::RefrigerantSource(c) => c.build(),
            AnyPyComponent::RefrigerantSink(c) => c.build(),
            AnyPyComponent::BrineSource(c) => c.build(),
            AnyPyComponent::BrineSink(c) => c.build(),
            AnyPyComponent::AirSource(c) => c.build(),
            AnyPyComponent::AirSink(c) => c.build(),
            AnyPyComponent::CapillaryTube(c) => c.build(),
        }
    }
}
