//! Boundary Condition Components — Source & Sink
//!
//! This module provides `FlowSource` and `FlowSink` for both incompressible
//! (water, glycol, brine) and compressible (refrigerant, CO₂) fluid systems.
//!
//! ## Design Philosophy (à la Modelica)
//!
//! - **`FlowSource`** imposes a fixed thermodynamic state (P, h) on its outlet
//!   edge. It is the entry point of a fluid circuit — it represents an infinite
//!   reservoir at constant conditions (city water supply, district heating header,
//!   refrigerant reservoir, etc.).
//!
//! - **`FlowSink`** absorbs flow at a fixed pressure (back-pressure). It is the
//!   termination point of a circuit. Optionally, a fixed outlet enthalpy can also
//!   be imposed (isothermal return, phase separator, etc.).
//!
//! ## Equations
//!
//! ### FlowSource — 2 equations
//!
//! ```text
//! r_P = P_edge − P_set = 0       (pressure boundary condition)
//! r_h = h_edge − h_set = 0       (enthalpy / temperature BC)
//! ```
//!
//! ### FlowSink — 1 or 2 equations
//!
//! ```text
//! r_P = P_edge − P_back = 0      (back-pressure boundary condition)
//! [optional] r_h = h_edge − h_back = 0
//! ```
//!
//! ## Incompressible vs Compressible
//!
//! Same physics, different construction-time validation. Use:
//! - `FlowSource::incompressible` / `FlowSink::incompressible` for water, glycol…
//! - `FlowSource::compressible`   / `FlowSink::compressible`   for refrigerant, CO₂…
//!
//! ## Example
//!
//! ```no_run
//! use entropyk_components::flow_boundary::{FlowSource, FlowSink};
//! use entropyk_components::port::{FluidId, Port};
//! use entropyk_core::{Pressure, Enthalpy};
//!
//! let make_port = |p: f64, h: f64| {
//!     let a = Port::new(FluidId::new("Water"), Pressure::from_pascals(p),
//!                       Enthalpy::from_joules_per_kg(h));
//!     let b = Port::new(FluidId::new("Water"), Pressure::from_pascals(p),
//!                       Enthalpy::from_joules_per_kg(h));
//!     a.connect(b).unwrap().0
//! };
//!
//! // City water supply: 3 bar, 15°C (h ≈ 63 kJ/kg)
//! let source = FlowSource::incompressible(
//!     "Water", 3.0e5, 63_000.0, make_port(3.0e5, 63_000.0),
//! ).unwrap();
//!
//! // Return header: 1.5 bar back-pressure
//! let sink = FlowSink::incompressible(
//!     "Water", 1.5e5, None, make_port(1.5e5, 63_000.0),
//! ).unwrap();
//! ```

use crate::{
    flow_junction::is_incompressible, flow_junction::FluidKind, Component, ComponentError,
    ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
};

// ─────────────────────────────────────────────────────────────────────────────
// FlowSource — Fixed P & h boundary condition
// ─────────────────────────────────────────────────────────────────────────────

/// A boundary source that imposes fixed pressure and enthalpy on its outlet edge.
///
/// Represents an ideal infinite reservoir (city water, refrigerant header, steam
/// drum, etc.) at constant thermodynamic conditions.
///
/// # Equations (always 2)
///
/// ```text
/// r₀ = P_edge − P_set = 0
/// r₁ = h_edge − h_set = 0
/// ```
#[derive(Debug, Clone)]
pub struct FlowSource {
    /// Fluid kind.
    kind: FluidKind,
    /// Fluid name.
    fluid_id: String,
    /// Set-point pressure [Pa].
    p_set_pa: f64,
    /// Set-point specific enthalpy [J/kg].
    h_set_jkg: f64,
    /// Connected outlet port (links to first edge in the System).
    outlet: ConnectedPort,
}

impl FlowSource {
    // ── Constructors ─────────────────────────────────────────────────────────

    /// Creates an **incompressible** source (water, glycol, brine…).
    ///
    /// # Arguments
    ///
    /// * `fluid`     — fluid identifier string (e.g. `"Water"`)
    /// * `p_set_pa`  — set-point pressure in Pascals
    /// * `h_set_jkg` — set-point specific enthalpy in J/kg
    /// * `outlet`    — connected port linked to the first system edge
    pub fn incompressible(
        fluid: impl Into<String>,
        p_set_pa: f64,
        h_set_jkg: f64,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();
        if !is_incompressible(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "FlowSource::incompressible: '{}' does not appear incompressible. \
                 Use FlowSource::compressible for refrigerants.",
                fluid
            )));
        }
        Self::new_inner(FluidKind::Incompressible, fluid, p_set_pa, h_set_jkg, outlet)
    }

    /// Creates a **compressible** source (R410A, CO₂, steam…).
    pub fn compressible(
        fluid: impl Into<String>,
        p_set_pa: f64,
        h_set_jkg: f64,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();
        Self::new_inner(FluidKind::Compressible, fluid, p_set_pa, h_set_jkg, outlet)
    }

    fn new_inner(
        kind: FluidKind,
        fluid: String,
        p_set_pa: f64,
        h_set_jkg: f64,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        if p_set_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "FlowSource: set-point pressure must be positive".into(),
            ));
        }
        Ok(Self { kind, fluid_id: fluid, p_set_pa, h_set_jkg, outlet })
    }

    // ── Accessors ────────────────────────────────────────────────────────────

    /// Fluid kind.
    pub fn fluid_kind(&self) -> FluidKind { self.kind }
    /// Fluid id.
    pub fn fluid_id(&self) -> &str { &self.fluid_id }
    /// Set-point pressure [Pa].
    pub fn p_set_pa(&self) -> f64 { self.p_set_pa }
    /// Set-point enthalpy [J/kg].
    pub fn h_set_jkg(&self) -> f64 { self.h_set_jkg }
    /// Reference to the outlet port.
    pub fn outlet(&self) -> &ConnectedPort { &self.outlet }

    /// Updates the set-point pressure (useful for parametric studies).
    pub fn set_pressure(&mut self, p_pa: f64) -> Result<(), ComponentError> {
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "FlowSource: pressure must be positive".into(),
            ));
        }
        self.p_set_pa = p_pa;
        Ok(())
    }

    /// Updates the set-point enthalpy.
    pub fn set_enthalpy(&mut self, h_jkg: f64) {
        self.h_set_jkg = h_jkg;
    }
}

impl Component for FlowSource {
    fn n_equations(&self) -> usize { 2 }

    fn compute_residuals(
        &self,
        _state: &SystemState,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        if residuals.len() < 2 {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: 2,
                actual: residuals.len(),
            });
        }
        // Pressure residual: P_edge − P_set = 0
        residuals[0] = self.outlet.pressure().to_pascals() - self.p_set_pa;
        // Enthalpy residual: h_edge − h_set = 0
        residuals[1] = self.outlet.enthalpy().to_joules_per_kg() - self.h_set_jkg;
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &SystemState,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        // Both residuals are linear in the edge state: ∂r/∂x = 1
        jacobian.add_entry(0, 0, 1.0);
        jacobian.add_entry(1, 1, 1.0);
        Ok(())
    }

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

// ─────────────────────────────────────────────────────────────────────────────
// FlowSink — Back-pressure boundary condition
// ─────────────────────────────────────────────────────────────────────────────

/// A boundary sink that imposes a fixed back-pressure (and optionally enthalpy)
/// on its inlet edge.
///
/// Represents an infinite low-pressure reservoir (drain, condenser header,
/// discharge line, atmospheric vent, etc.).
///
/// # Equations (1 or 2)
///
/// ```text
/// r₀ = P_edge − P_back = 0                                 [always]
/// r₁ = h_edge − h_back = 0         [only if h_back is set]
/// ```
#[derive(Debug, Clone)]
pub struct FlowSink {
    /// Fluid kind.
    kind: FluidKind,
    /// Fluid name.
    fluid_id: String,
    /// Back-pressure [Pa].
    p_back_pa: f64,
    /// Optional fixed outlet enthalpy [J/kg].
    h_back_jkg: Option<f64>,
    /// Connected inlet port.
    inlet: ConnectedPort,
}

impl FlowSink {
    // ── Constructors ─────────────────────────────────────────────────────────

    /// Creates an **incompressible** sink (water, glycol…).
    ///
    /// # Arguments
    ///
    /// * `fluid`      — fluid identifier string
    /// * `p_back_pa`  — back-pressure in Pascals
    /// * `h_back_jkg` — optional fixed return enthalpy; `None` = free (solver decides)
    /// * `inlet`      — connected port
    pub fn incompressible(
        fluid: impl Into<String>,
        p_back_pa: f64,
        h_back_jkg: Option<f64>,
        inlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();
        if !is_incompressible(&fluid) {
            return Err(ComponentError::InvalidState(format!(
                "FlowSink::incompressible: '{}' does not appear incompressible. \
                 Use FlowSink::compressible for refrigerants.",
                fluid
            )));
        }
        Self::new_inner(FluidKind::Incompressible, fluid, p_back_pa, h_back_jkg, inlet)
    }

    /// Creates a **compressible** sink (R410A, CO₂, steam…).
    pub fn compressible(
        fluid: impl Into<String>,
        p_back_pa: f64,
        h_back_jkg: Option<f64>,
        inlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        let fluid = fluid.into();
        Self::new_inner(FluidKind::Compressible, fluid, p_back_pa, h_back_jkg, inlet)
    }

    fn new_inner(
        kind: FluidKind,
        fluid: String,
        p_back_pa: f64,
        h_back_jkg: Option<f64>,
        inlet: ConnectedPort,
    ) -> Result<Self, ComponentError> {
        if p_back_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "FlowSink: back-pressure must be positive".into(),
            ));
        }
        Ok(Self { kind, fluid_id: fluid, p_back_pa, h_back_jkg, inlet })
    }

    // ── Accessors ────────────────────────────────────────────────────────────

    /// Fluid kind.
    pub fn fluid_kind(&self) -> FluidKind { self.kind }
    /// Fluid id.
    pub fn fluid_id(&self) -> &str { &self.fluid_id }
    /// Back-pressure [Pa].
    pub fn p_back_pa(&self) -> f64 { self.p_back_pa }
    /// Optional back-enthalpy [J/kg].
    pub fn h_back_jkg(&self) -> Option<f64> { self.h_back_jkg }
    /// Reference to the inlet port.
    pub fn inlet(&self) -> &ConnectedPort { &self.inlet }

    /// Updates the back-pressure.
    pub fn set_pressure(&mut self, p_pa: f64) -> Result<(), ComponentError> {
        if p_pa <= 0.0 {
            return Err(ComponentError::InvalidState(
                "FlowSink: back-pressure must be positive".into(),
            ));
        }
        self.p_back_pa = p_pa;
        Ok(())
    }

    /// Sets a fixed return enthalpy (activates the second equation).
    pub fn set_return_enthalpy(&mut self, h_jkg: f64) {
        self.h_back_jkg = Some(h_jkg);
    }

    /// Removes the fixed enthalpy constraint (solver determines enthalpy freely).
    pub fn clear_return_enthalpy(&mut self) {
        self.h_back_jkg = None;
    }
}

impl Component for FlowSink {
    fn n_equations(&self) -> usize {
        if self.h_back_jkg.is_some() { 2 } else { 1 }
    }

    fn compute_residuals(
        &self,
        _state: &SystemState,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        if residuals.len() < n {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n,
                actual: residuals.len(),
            });
        }
        // Back-pressure residual
        residuals[0] = self.inlet.pressure().to_pascals() - self.p_back_pa;
        // Optional enthalpy residual
        if let Some(h_back) = self.h_back_jkg {
            residuals[1] = self.inlet.enthalpy().to_joules_per_kg() - h_back;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        _state: &SystemState,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        for i in 0..n {
            jacobian.add_entry(i, i, 1.0);
        }
        Ok(())
    }

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

// ─────────────────────────────────────────────────────────────────────────────
// Convenience type aliases (à la Modelica)
// ─────────────────────────────────────────────────────────────────────────────

/// Source for incompressible fluids (water, glycol, brine…).
pub type IncompressibleSource = FlowSource;
/// Source for compressible fluids (refrigerant, CO₂, steam…).
pub type CompressibleSource = FlowSource;
/// Sink for incompressible fluids.
pub type IncompressibleSink = FlowSink;
/// Sink for compressible fluids.
pub type CompressibleSink = FlowSink;

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

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

    fn make_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
        let a = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa),
                          Enthalpy::from_joules_per_kg(h_jkg));
        let b = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa),
                          Enthalpy::from_joules_per_kg(h_jkg));
        a.connect(b).unwrap().0
    }

    // ── FlowSource ────────────────────────────────────────────────────────────

    #[test]
    fn test_source_incompressible_water() {
        // City water supply: 3 bar, 15°C (h ≈ 63 kJ/kg)
        let port = make_port("Water", 3.0e5, 63_000.0);
        let s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
        assert_eq!(s.n_equations(), 2);
        assert_eq!(s.fluid_kind(), FluidKind::Incompressible);
        assert_eq!(s.p_set_pa(), 3.0e5);
        assert_eq!(s.h_set_jkg(), 63_000.0);
    }

    #[test]
    fn test_source_compressible_refrigerant() {
        // R410A high-side: 24 bar, h = 465 kJ/kg (superheated vapour)
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let s = FlowSource::compressible("R410A", 24.0e5, 465_000.0, port).unwrap();
        assert_eq!(s.n_equations(), 2);
        assert_eq!(s.fluid_kind(), FluidKind::Compressible);
    }

    #[test]
    fn test_source_rejects_refrigerant_as_incompressible() {
        let port = make_port("R410A", 24.0e5, 465_000.0);
        let result = FlowSource::incompressible("R410A", 24.0e5, 465_000.0, port);
        assert!(result.is_err());
    }

    #[test]
    fn test_source_rejects_zero_pressure() {
        let port = make_port("Water", 3.0e5, 63_000.0);
        let result = FlowSource::incompressible("Water", 0.0, 63_000.0, port);
        assert!(result.is_err());
    }

    #[test]
    fn test_source_residuals_zero_at_set_point() {
        let p = 3.0e5_f64;
        let h = 63_000.0_f64;
        let port = make_port("Water", p, h);
        let s = FlowSource::incompressible("Water", p, h, port).unwrap();
        let state = vec![0.0; 4];
        let mut res = vec![0.0; 2];
        s.compute_residuals(&state, &mut res).unwrap();
        assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
        assert!(res[1].abs() < 1.0, "h residual = {}", res[1]);
    }

    #[test]
    fn test_source_residuals_nonzero_on_mismatch() {
        // Port at 2 bar but set-point 3 bar → residual = -1e5
        let port = make_port("Water", 2.0e5, 63_000.0);
        let s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
        let state = vec![0.0; 4];
        let mut res = vec![0.0; 2];
        s.compute_residuals(&state, &mut res).unwrap();
        assert!((res[0] - (-1.0e5)).abs() < 1.0, "expected -1e5, got {}", res[0]);
    }

    #[test]
    fn test_source_set_pressure() {
        let port = make_port("Water", 3.0e5, 63_000.0);
        let mut s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
        s.set_pressure(5.0e5).unwrap();
        assert_eq!(s.p_set_pa(), 5.0e5);
        assert!(s.set_pressure(0.0).is_err());
    }

    #[test]
    fn test_source_as_trait_object() {
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let src: Box<dyn Component> =
            Box::new(FlowSource::compressible("R410A", 8.5e5, 260_000.0, port).unwrap());
        assert_eq!(src.n_equations(), 2);
    }

    // ── FlowSink ──────────────────────────────────────────────────────────────

    #[test]
    fn test_sink_incompressible_back_pressure_only() {
        // Return header: 1.5 bar, free enthalpy
        let port = make_port("Water", 1.5e5, 63_000.0);
        let s = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
        assert_eq!(s.n_equations(), 1);
        assert_eq!(s.fluid_kind(), FluidKind::Incompressible);
    }

    #[test]
    fn test_sink_with_fixed_return_enthalpy() {
        // Fixed return temperature: 12°C, h ≈ 50.4 kJ/kg
        let port = make_port("Water", 1.5e5, 50_400.0);
        let s = FlowSink::incompressible("Water", 1.5e5, Some(50_400.0), port).unwrap();
        assert_eq!(s.n_equations(), 2);
    }

    #[test]
    fn test_sink_compressible_refrigerant() {
        // R410A low-side: 8.5 bar
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let s = FlowSink::compressible("R410A", 8.5e5, None, port).unwrap();
        assert_eq!(s.n_equations(), 1);
        assert_eq!(s.fluid_kind(), FluidKind::Compressible);
    }

    #[test]
    fn test_sink_rejects_refrigerant_as_incompressible() {
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let result = FlowSink::incompressible("R410A", 8.5e5, None, port);
        assert!(result.is_err());
    }

    #[test]
    fn test_sink_rejects_zero_back_pressure() {
        let port = make_port("Water", 1.5e5, 63_000.0);
        let result = FlowSink::incompressible("Water", 0.0, None, port);
        assert!(result.is_err());
    }

    #[test]
    fn test_sink_residual_zero_at_back_pressure() {
        let p = 1.5e5_f64;
        let port = make_port("Water", p, 63_000.0);
        let s = FlowSink::incompressible("Water", p, None, port).unwrap();
        let state = vec![0.0; 4];
        let mut res = vec![0.0; 1];
        s.compute_residuals(&state, &mut res).unwrap();
        assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
    }

    #[test]
    fn test_sink_residual_with_enthalpy() {
        let p = 1.5e5_f64;
        let h = 50_400.0_f64;
        let port = make_port("Water", p, h);
        let s = FlowSink::incompressible("Water", p, Some(h), port).unwrap();
        let state = vec![0.0; 4];
        let mut res = vec![0.0; 2];
        s.compute_residuals(&state, &mut res).unwrap();
        assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
        assert!(res[1].abs() < 1.0, "h residual = {}", res[1]);
    }

    #[test]
    fn test_sink_dynamic_enthalpy_toggle() {
        let port = make_port("Water", 1.5e5, 63_000.0);
        let mut s = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
        assert_eq!(s.n_equations(), 1);

        s.set_return_enthalpy(50_400.0);
        assert_eq!(s.n_equations(), 2);

        s.clear_return_enthalpy();
        assert_eq!(s.n_equations(), 1);
    }

    #[test]
    fn test_sink_as_trait_object() {
        let port = make_port("R410A", 8.5e5, 260_000.0);
        let sink: Box<dyn Component> =
            Box::new(FlowSink::compressible("R410A", 8.5e5, Some(260_000.0), port).unwrap());
        assert_eq!(sink.n_equations(), 2);
    }
}
