
// =============================================================================
// Python Boundary Types (Refrigerant, Brine, Air)
// =============================================================================

// ---------------------------------------------------------------------------
// RefrigerantSourceReal
// ---------------------------------------------------------------------------

/// Python-friendly refrigerant source: imposes fixed pressure + vapor quality.
///
/// This struct is instantiated by the Python binding `RefrigerantSource` and
/// implements the `Component` trait so it can be injected into the solver graph.
///
/// # Equations (always 2)
///
/// ```text
/// r0 = P_edge - P_set = 0
/// r1 = h_edge - h(P_set, quality) = 0
/// ```
///
/// where `h(P, x)` is computed via linear interpolation in the two-phase region.
#[derive(Debug, Clone)]
pub struct PyRefrigerantSourceReal {
    pub fluid: FluidId,
    pub p_set_pa: f64,
    pub quality: f64,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyRefrigerantSourceReal {
    /// Create a new refrigerant source.
    ///
    /// * `fluid`    – CoolProp fluid identifier, e.g. `"R410A"`.
    /// * `p_set_pa` – Imposed outlet pressure [Pa].
    /// * `quality`  – Vapor quality at outlet (0 = saturated liquid, 1 = saturated vapour).
    pub fn new(fluid: &str, p_set_pa: f64, quality: f64) -> Self {
        Self {
            fluid: FluidId::new(fluid),
            p_set_pa,
            quality,
            edge_indices: Vec::new(),
        }
    }

    /// Compute enthalpy from pressure + vapor quality using the fluid backend.
    fn enthalpy_from_quality(&self, backend: &dyn FluidBackend) -> Result<f64, ComponentError> {
        use entropyk_fluids::{FluidState as FState, Quality};
        let p = Pressure::from_pascals(self.p_set_pa);
        let state = FState::from_px(p, Quality::new(self.quality));
        backend
            .property(self.fluid.clone(), Property::Enthalpy, state)
            .map_err(|e| {
                ComponentError::CalculationFailed(format!(
                    "RefrigerantSource: enthalpy from quality: {}",
                    e
                ))
            })
    }
}

impl Component for PyRefrigerantSourceReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else {
            2
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, h_idx) = self.edge_indices[0];
        let p_edge = state[p_idx];
        let h_edge = state[h_idx];

        // Use tabular backend (no fluid backend stored — use CoolProp via fluids)
        let backend = entropyk_fluids::CoolPropBackend::new();
        let h_set = self.enthalpy_from_quality(&backend)?;

        residuals[0] = p_edge - self.p_set_pa;
        residuals[1] = h_edge - h_set;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}

// ---------------------------------------------------------------------------
// RefrigerantSinkReal
// ---------------------------------------------------------------------------

/// Python-friendly refrigerant sink: imposes back-pressure (and optional quality).
#[derive(Debug, Clone)]
pub struct PyRefrigerantSinkReal {
    pub fluid: FluidId,
    pub p_back_pa: f64,
    pub quality_opt: Option<f64>,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyRefrigerantSinkReal {
    /// Create a new refrigerant sink.
    ///
    /// * `fluid`       – CoolProp fluid identifier.
    /// * `p_back_pa`   – Back-pressure imposed on the inlet edge [Pa].
    /// * `quality_opt` – Optional vapor quality to fix enthalpy; `None` means free enthalpy.
    pub fn new(fluid: &str, p_back_pa: f64, quality_opt: Option<f64>) -> Self {
        Self {
            fluid: FluidId::new(fluid),
            p_back_pa,
            quality_opt,
            edge_indices: Vec::new(),
        }
    }
}

impl Component for PyRefrigerantSinkReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else if self.quality_opt.is_some() {
            2
        } else {
            1
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, h_idx) = self.edge_indices[0];
        let p_edge = state[p_idx];
        residuals[0] = p_edge - self.p_back_pa;

        if let Some(quality) = self.quality_opt {
            use entropyk_fluids::{FluidState as FState, Quality};
            let p = Pressure::from_pascals(self.p_back_pa);
            let backend = entropyk_fluids::CoolPropBackend::new();
            let fstate = FState::from_px(p, Quality::new(quality));
            let h_set = backend
                .property(self.fluid.clone(), Property::Enthalpy, fstate)
                .map_err(|e| {
                    ComponentError::CalculationFailed(format!(
                        "RefrigerantSink: enthalpy from quality: {}",
                        e
                    ))
                })?;
            residuals[1] = state[h_idx] - h_set;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}

// ---------------------------------------------------------------------------
// BrineSourceReal
// ---------------------------------------------------------------------------

/// Python-friendly brine source: imposes pressure + temperature + concentration.
#[derive(Debug, Clone)]
pub struct PyBrineSourceReal {
    pub fluid: FluidId,
    pub concentration: f64,
    pub temperature_k: f64,
    pub pressure_pa: f64,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyBrineSourceReal {
    /// Create a new brine source.
    ///
    /// * `fluid`         – Base fluid, e.g. `"MEG"`, `"EthyleneGlycol"`, `"Water"`.
    /// * `concentration` – Glycol mass fraction \[0, 1\].
    /// * `temperature_k` – Outlet temperature [K].
    /// * `pressure_pa`   – Outlet pressure [Pa].
    pub fn new(fluid: &str, concentration: f64, temperature_k: f64, pressure_pa: f64) -> Self {
        Self {
            fluid: FluidId::new(fluid),
            concentration,
            temperature_k,
            pressure_pa,
            edge_indices: Vec::new(),
        }
    }

    /// Build the CoolProp incompressible mixture name if concentration > 0.
    fn fluid_name(&self) -> String {
        if self.concentration < 1e-10 {
            self.fluid.as_str().to_string()
        } else {
            format!(
                "INCOMP::{}-{:.0}",
                self.fluid.as_str(),
                self.concentration * 100.0
            )
        }
    }

    fn enthalpy(&self, backend: &dyn FluidBackend) -> Result<f64, ComponentError> {
        use entropyk_fluids::FluidState as FState;
        let t = Temperature::from_kelvin(self.temperature_k);
        let p = Pressure::from_pascals(self.pressure_pa);
        let fid = FluidId::new(&self.fluid_name());
        let fstate = FState::from_pt(p, t);
        backend
            .property(fid, Property::Enthalpy, fstate)
            .map_err(|e| {
                ComponentError::CalculationFailed(format!("BrineSource: enthalpy: {}", e))
            })
    }
}

impl Component for PyBrineSourceReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else {
            2
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, h_idx) = self.edge_indices[0];
        let backend = entropyk_fluids::CoolPropBackend::new();
        let h_set = self.enthalpy(&backend)?;
        residuals[0] = state[p_idx] - self.pressure_pa;
        residuals[1] = state[h_idx] - h_set;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}

// ---------------------------------------------------------------------------
// BrineSinkReal
// ---------------------------------------------------------------------------

/// Python-friendly brine sink: imposes back-pressure on the inlet edge.
#[derive(Debug, Clone)]
pub struct PyBrineSinkReal {
    pub p_back_pa: f64,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyBrineSinkReal {
    /// Create a new brine sink.
    ///
    /// * `p_back_pa` – Back-pressure imposed on the inlet edge [Pa].
    pub fn new(p_back_pa: f64) -> Self {
        Self {
            p_back_pa,
            edge_indices: Vec::new(),
        }
    }
}

impl Component for PyBrineSinkReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else {
            1
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, _h_idx) = self.edge_indices[0];
        residuals[0] = state[p_idx] - self.p_back_pa;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}

// ---------------------------------------------------------------------------
// AirSourceReal
// ---------------------------------------------------------------------------

/// Python-friendly air source: imposes temperature + relative humidity + pressure.
///
/// Psychrometric formulas used:
///
/// ```text
/// P_sat = 610.78 * exp(17.27 * T_c / (T_c + 237.3))   [Pa]
/// W     = 0.622 * P_v / (P_atm - P_v)                 [kg/kg]
/// h     = 1006 * T_c + W * (2_501_000 + 1860 * T_c)   [J/kg]
/// ```
#[derive(Debug, Clone)]
pub struct PyAirSourceReal {
    pub temperature_k: f64,
    pub relative_humidity: f64,
    pub pressure_pa: f64,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyAirSourceReal {
    /// Create a new air source.
    ///
    /// * `temperature_k`      – Dry-bulb temperature [K].
    /// * `relative_humidity`  – Relative humidity \[0, 1\].
    /// * `pressure_pa`        – Total atmospheric pressure [Pa].
    pub fn new(temperature_k: f64, relative_humidity: f64, pressure_pa: f64) -> Self {
        Self {
            temperature_k,
            relative_humidity,
            pressure_pa,
            edge_indices: Vec::new(),
        }
    }

    /// Specific enthalpy of moist air [J/kg dry air].
    pub fn moist_air_enthalpy(&self) -> f64 {
        let t_c = self.temperature_k - 273.15;
        let p_sat = 610.78 * (17.27 * t_c / (t_c + 237.3)).exp();
        let p_v = self.relative_humidity * p_sat;
        let w = 0.622 * p_v / (self.pressure_pa - p_v).max(1.0);
        1006.0 * t_c + w * (2_501_000.0 + 1860.0 * t_c)
    }

    /// Humidity ratio W [kg water/kg dry air].
    pub fn humidity_ratio(&self) -> f64 {
        let t_c = self.temperature_k - 273.15;
        let p_sat = 610.78 * (17.27 * t_c / (t_c + 237.3)).exp();
        let p_v = self.relative_humidity * p_sat;
        0.622 * p_v / (self.pressure_pa - p_v).max(1.0)
    }
}

impl Component for PyAirSourceReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else {
            2
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, h_idx) = self.edge_indices[0];
        residuals[0] = state[p_idx] - self.pressure_pa;
        residuals[1] = state[h_idx] - self.moist_air_enthalpy();
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}

// ---------------------------------------------------------------------------
// AirSinkReal
// ---------------------------------------------------------------------------

/// Python-friendly air sink: imposes back-pressure on the inlet edge.
#[derive(Debug, Clone)]
pub struct PyAirSinkReal {
    pub p_back_pa: f64,
    pub edge_indices: Vec<(usize, usize)>,
}

impl PyAirSinkReal {
    /// Create a new air sink.
    ///
    /// * `p_back_pa` – Back-pressure imposed on the inlet edge [Pa].
    pub fn new(p_back_pa: f64) -> Self {
        Self {
            p_back_pa,
            edge_indices: Vec::new(),
        }
    }
}

impl Component for PyAirSinkReal {
    fn n_equations(&self) -> usize {
        if self.edge_indices.is_empty() {
            0
        } else {
            1
        }
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if self.edge_indices.is_empty() {
            return Ok(());
        }
        let (p_idx, _h_idx) = self.edge_indices[0];
        residuals[0] = state[p_idx] - self.p_back_pa;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &StateSlice,
        _jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.edge_indices = external_edge_state_indices.iter().map(|&(_, p, h)| (p, h)).collect();
    }
}
