//! Node - Passive Probe Component
//!
//! This module provides a passive probe component (0 equations) for extracting
//! thermodynamic measurements at any point in a circuit without affecting the
//! system of equations.
//!
//! ## Purpose
//!
//! The Node component allows you to:
//! - Extract superheat after the evaporator
//! - Measure subcooling after the condenser
//! - Obtain temperature at any point
//! - Serve as a junction point in the topology without adding constraints
//!
//! ## Zero-Equation Design
//!
//! Unlike other components, `Node` contributes **zero equations** to the solver.
//! It only reads values from its inlet port and computes derived quantities
//! (superheat, subcooling, quality, phase) using the attached `FluidBackend`.
//!
//! ## Example
//!
//! ```ignore
//! use entropyk_components::Node;
//! use entropyk_fluids::CoolPropBackend;
//! use std::sync::Arc;
//!
//! // Create a probe after the evaporator
//! let backend = Arc::new(CoolPropBackend::new());
//!
//! let probe = Node::new(
//!     "evaporator_outlet",
//!     evaporator.outlet_port(),
//!     compressor.inlet_port(),
//! )
//! .with_fluid_backend(backend);
//!
//! // After convergence
//! let t_sh = probe.superheat().expect("Should be superheated");
//! println!("Superheat: {:.1} K", t_sh);
//! ```

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::{Enthalpy, MassFlow, Power, Pressure};
use entropyk_fluids::FluidBackend;
use std::marker::PhantomData;
use std::sync::Arc;

/// Phase of the fluid at the node location.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NodePhase {
    /// Subcooled liquid (h < h_sat_liquid)
    SubcooledLiquid,
    /// Two-phase mixture (h_sat_liquid <= h <= h_sat_vapor)
    TwoPhase,
    /// Superheated vapor (h > h_sat_vapor)
    SuperheatedVapor,
    /// Supercritical fluid (P > P_critical)
    Supercritical,
    /// Unknown phase (no backend or computation failed)
    #[default]
    Unknown,
}

impl std::fmt::Display for NodePhase {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NodePhase::SubcooledLiquid => write!(f, "SubcooledLiquid"),
            NodePhase::TwoPhase => write!(f, "TwoPhase"),
            NodePhase::SuperheatedVapor => write!(f, "SuperheatedVapor"),
            NodePhase::Supercritical => write!(f, "Supercritical"),
            NodePhase::Unknown => write!(f, "Unknown"),
        }
    }
}

/// Measurements computed at the node location.
#[derive(Debug, Clone, Default)]
pub struct NodeMeasurements {
    /// Pressure in Pascals
    pub pressure_pa: f64,
    /// Temperature in Kelvin (requires backend)
    pub temperature_k: f64,
    /// Specific enthalpy in J/kg
    pub enthalpy_j_kg: f64,
    /// Specific entropy in J/(kg·K) (requires backend)
    pub entropy: Option<f64>,
    /// Vapor quality (0-1) if in two-phase region
    pub quality: Option<f64>,
    /// Superheat in Kelvin if superheated
    pub superheat_k: Option<f64>,
    /// Subcooling in Kelvin if subcooled
    pub subcooling_k: Option<f64>,
    /// Mass flow rate in kg/s
    pub mass_flow_kg_s: f64,
    /// Saturation temperature (bubble point) in Kelvin
    pub saturation_temp_k: Option<f64>,
    /// Phase at this location
    pub phase: NodePhase,
    /// Density in kg/m³ (requires backend)
    pub density: Option<f64>,
}

/// Node - Passive probe for extracting measurements.
///
/// A Node is a zero-equation component that passively reads values from its
/// inlet port and computes derived thermodynamic quantities. It does not
/// affect the solver's system of equations.
///
/// # Type Parameters
///
/// * `State` - Either `Disconnected` or `Connected`
#[derive(Clone)]
pub struct Node<State> {
    /// Node name for identification
    name: String,
    /// Inlet port
    port_inlet: Port<State>,
    /// Outlet port
    port_outlet: Port<State>,
    /// Fluid backend for computing advanced properties
    fluid_backend: Option<Arc<dyn FluidBackend>>,
    /// Cached measurements (updated in post_solve)
    measurements: NodeMeasurements,
    /// Circuit identifier
    circuit_id: CircuitId,
    /// Operational state
    operational_state: OperationalState,
    /// Phantom data for type state
    _state: PhantomData<State>,
}

impl<State> std::fmt::Debug for Node<State> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Node")
            .field("name", &self.name)
            .field("has_backend", &self.fluid_backend.is_some())
            .field("measurements", &self.measurements)
            .field("circuit_id", &self.circuit_id)
            .field("operational_state", &self.operational_state)
            .finish()
    }
}

impl Node<Disconnected> {
    /// Creates a new disconnected node.
    ///
    /// # Arguments
    ///
    /// * `name` - Node name for identification
    /// * `port_inlet` - Inlet port (disconnected)
    /// * `port_outlet` - Outlet port (disconnected)
    pub fn new(
        name: impl Into<String>,
        port_inlet: Port<Disconnected>,
        port_outlet: Port<Disconnected>,
    ) -> Self {
        Self {
            name: name.into(),
            port_inlet,
            port_outlet,
            fluid_backend: None,
            measurements: NodeMeasurements::default(),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            _state: PhantomData,
        }
    }

    /// Returns the node name.
    pub fn name(&self) -> &str {
        &self.name
    }

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

    /// Attaches a fluid backend for computing advanced properties.
    ///
    /// Without a backend, only basic measurements (P, h, mass flow) are available.
    /// With a backend, you also get temperature, phase, quality, superheat, subcooling.
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.fluid_backend = Some(backend);
        self
    }

    /// Connects the node to inlet and outlet ports.
    ///
    /// This consumes the disconnected node and returns a connected one.
    pub fn connect(
        self,
        inlet: Port<Disconnected>,
        outlet: Port<Disconnected>,
    ) -> Result<Node<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(Node {
            name: self.name,
            port_inlet: p_in,
            port_outlet: p_out,
            fluid_backend: self.fluid_backend,
            measurements: self.measurements,
            circuit_id: self.circuit_id,
            operational_state: self.operational_state,
            _state: PhantomData,
        })
    }
}

impl Node<Connected> {
    /// Returns the node name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// 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
    }

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

    /// Returns the current pressure in Pascals.
    pub fn pressure(&self) -> f64 {
        self.measurements.pressure_pa
    }

    /// Returns the current temperature in Kelvin.
    pub fn temperature(&self) -> f64 {
        self.measurements.temperature_k
    }

    /// Returns the current specific enthalpy in J/kg.
    pub fn enthalpy(&self) -> f64 {
        self.measurements.enthalpy_j_kg
    }

    /// Returns the mass flow rate in kg/s.
    pub fn mass_flow(&self) -> f64 {
        self.measurements.mass_flow_kg_s
    }

    /// Returns the vapor quality (0-1) if in two-phase region.
    pub fn quality(&self) -> Option<f64> {
        self.measurements.quality
    }

    /// Returns the superheat in Kelvin if superheated.
    pub fn superheat(&self) -> Option<f64> {
        self.measurements.superheat_k
    }

    /// Returns the subcooling in Kelvin if subcooled.
    pub fn subcooling(&self) -> Option<f64> {
        self.measurements.subcooling_k
    }

    /// Returns the saturation temperature in Kelvin.
    pub fn saturation_temp(&self) -> Option<f64> {
        self.measurements.saturation_temp_k
    }

    /// Returns the phase at this location.
    pub fn phase(&self) -> NodePhase {
        self.measurements.phase
    }

    /// Returns all measurements.
    pub fn measurements(&self) -> &NodeMeasurements {
        &self.measurements
    }

    /// Updates measurements from the current system state.
    ///
    /// This is called automatically by `post_solve` after the solver converges.
    pub fn update_measurements(&mut self, state: &StateSlice) -> Result<(), ComponentError> {
        self.measurements.pressure_pa = self.port_inlet.pressure().to_pascals();
        self.measurements.enthalpy_j_kg = self.port_inlet.enthalpy().to_joules_per_kg();

        self.measurements.mass_flow_kg_s = if !state.is_empty() { state[0] } else { 0.0 };

        if let Some(ref backend) = self.fluid_backend.clone() {
            self.compute_advanced_measurements(backend.as_ref())?;
        }

        Ok(())
    }

    fn compute_advanced_measurements(
        &mut self,
        backend: &dyn FluidBackend,
    ) -> Result<(), ComponentError> {
        let fluid = self.port_inlet.fluid_id().clone();
        let p = Pressure::from_pascals(self.measurements.pressure_pa);
        let h = Enthalpy::from_joules_per_kg(self.measurements.enthalpy_j_kg);

        match backend.full_state(fluid, p, h) {
            Ok(thermo_state) => {
                self.measurements.temperature_k = thermo_state.temperature.to_kelvin();
                self.measurements.entropy = Some(thermo_state.entropy.to_joules_per_kg_kelvin());
                self.measurements.density = Some(thermo_state.density);

                self.measurements.phase = match thermo_state.phase {
                    entropyk_fluids::Phase::Liquid => NodePhase::SubcooledLiquid,
                    entropyk_fluids::Phase::Vapor => NodePhase::SuperheatedVapor,
                    entropyk_fluids::Phase::TwoPhase => NodePhase::TwoPhase,
                    entropyk_fluids::Phase::Supercritical => NodePhase::Supercritical,
                    entropyk_fluids::Phase::Unknown => NodePhase::Unknown,
                };

                self.measurements.quality = thermo_state.quality.map(|q| q.value());

                self.measurements.superheat_k = thermo_state.superheat.map(|sh| sh.kelvin());

                self.measurements.subcooling_k = thermo_state.subcooling.map(|sc| sc.kelvin());

                self.measurements.saturation_temp_k = thermo_state
                    .t_dew
                    .or(thermo_state.t_bubble)
                    .map(|t| t.to_kelvin());
            }
            Err(_) => {
                self.measurements.phase = NodePhase::Unknown;
                self.measurements.quality = None;
                self.measurements.superheat_k = None;
                self.measurements.subcooling_k = None;
                self.measurements.saturation_temp_k = None;
                self.measurements.density = None;
            }
        }

        Ok(())
    }

    /// Attaches or replaces the fluid backend.
    pub fn set_fluid_backend(&mut self, backend: Arc<dyn FluidBackend>) {
        self.fluid_backend = Some(backend);
    }

    /// Returns true if a fluid backend is attached.
    pub fn has_fluid_backend(&self) -> bool {
        self.fluid_backend.is_some()
    }
}

impl Component for Node<Connected> {
    fn compute_residuals(
        &self,
        _state: &StateSlice,
        _residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        Ok(())
    }

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

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

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

    fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }

    fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
        let m = if state.is_empty() { 0.0 } else { state[0] };
        Ok(vec![
            MassFlow::from_kg_per_s(m),
            MassFlow::from_kg_per_s(-m),
        ])
    }

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

    fn set_fluid_backend_from_builder(
        &mut self,
        backend: std::sync::Arc<dyn entropyk_fluids::FluidBackend>,
    ) {
        if self.fluid_backend.is_none() {
            self.fluid_backend = Some(backend);
        }
    }

    fn signature(&self) -> String {
        format!("Node({}:{:?})", self.name, self.fluid_id().as_str())
    }

    fn to_params(&self) -> crate::ComponentParams {
        crate::ComponentParams::new("Node")
            .with_param("name", self.name.as_str())
            .with_param("fluid", self.fluid_id().as_str())
    }
}

impl StateManageable for Node<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) {
            self.operational_state = 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 entropyk_core::Pressure;

    fn create_test_node_connected() -> Node<Connected> {
        let inlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        );
        let outlet = Port::new(
            FluidId::new("R134a"),
            Pressure::from_bar(10.0),
            Enthalpy::from_joules_per_kg(400_000.0),
        );
        let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();

        Node {
            name: "test_node".to_string(),
            port_inlet: inlet_conn,
            port_outlet: outlet_conn,
            fluid_backend: None,
            measurements: NodeMeasurements::default(),
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
            _state: PhantomData,
        }
    }

    #[test]
    fn test_node_zero_equations() {
        let node = create_test_node_connected();
        assert_eq!(node.n_equations(), 0);
    }

    #[test]
    fn test_node_no_residuals() {
        let node = create_test_node_connected();
        let state = vec![1.0];
        let mut residuals = vec![];

        node.compute_residuals(&state, &mut residuals).unwrap();
        assert!(residuals.is_empty());
    }

    #[test]
    fn test_node_no_jacobian() {
        let node = create_test_node_connected();
        let state = vec![1.0];
        let mut jacobian = JacobianBuilder::new();

        node.jacobian_entries(&state, &mut jacobian).unwrap();
        assert!(jacobian.is_empty());
    }

    #[test]
    fn test_node_energy_transfers() {
        let node = create_test_node_connected();
        let state = vec![1.0];

        let (heat, work) = node.energy_transfers(&state).unwrap();
        assert_eq!(heat.to_watts(), 0.0);
        assert_eq!(work.to_watts(), 0.0);
    }

    #[test]
    fn test_node_extract_pressure() {
        let mut node = create_test_node_connected();
        let state = vec![0.1];

        node.update_measurements(&state).unwrap();

        assert!((node.pressure() - 1_000_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_node_extract_enthalpy() {
        let mut node = create_test_node_connected();
        let state = vec![0.1];

        node.update_measurements(&state).unwrap();

        assert!((node.enthalpy() - 400_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_node_extract_mass_flow() {
        let mut node = create_test_node_connected();
        let state = vec![0.5];

        node.update_measurements(&state).unwrap();

        assert!((node.mass_flow() - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_node_no_backend_graceful() {
        let mut node = create_test_node_connected();
        let state = vec![0.1];

        node.update_measurements(&state).unwrap();

        assert!(node.pressure() > 0.0);
        assert!(node.mass_flow() > 0.0);
        assert_eq!(node.phase(), NodePhase::Unknown);
        assert!(node.quality().is_none());
        assert!(node.superheat().is_none());
        assert!(node.subcooling().is_none());
    }

    #[test]
    fn test_node_port_mass_flows() {
        let node = create_test_node_connected();
        let state = vec![0.5];

        let flows = node.port_mass_flows(&state).unwrap();
        assert_eq!(flows.len(), 2);
        assert!((flows[0].to_kg_per_s() - 0.5).abs() < 1e-6);
        assert!((flows[1].to_kg_per_s() - (-0.5)).abs() < 1e-6);
    }

    #[test]
    fn test_node_port_enthalpies() {
        let node = create_test_node_connected();
        let state = vec![0.5];

        let enthalpies = node.port_enthalpies(&state).unwrap();
        assert_eq!(enthalpies.len(), 2);
        assert!((enthalpies[0].to_joules_per_kg() - 400_000.0).abs() < 1e-6);
    }

    #[test]
    fn test_node_state_manageable() {
        let node = create_test_node_connected();
        assert_eq!(node.state(), OperationalState::On);
        assert!(node.can_transition_to(OperationalState::Off));
    }

    #[test]
    fn test_node_signature() {
        let node = create_test_node_connected();
        let sig = node.signature();
        assert!(sig.contains("test_node"));
        assert!(sig.contains("R134a"));
    }

    #[test]
    fn test_node_phase_display() {
        assert_eq!(format!("{}", NodePhase::SubcooledLiquid), "SubcooledLiquid");
        assert_eq!(format!("{}", NodePhase::TwoPhase), "TwoPhase");
        assert_eq!(
            format!("{}", NodePhase::SuperheatedVapor),
            "SuperheatedVapor"
        );
        assert_eq!(format!("{}", NodePhase::Supercritical), "Supercritical");
        assert_eq!(format!("{}", NodePhase::Unknown), "Unknown");
    }

    #[test]
    fn test_node_measurements_default() {
        let m = NodeMeasurements::default();
        assert_eq!(m.pressure_pa, 0.0);
        assert_eq!(m.temperature_k, 0.0);
        assert_eq!(m.enthalpy_j_kg, 0.0);
        assert!(m.quality.is_none());
        assert!(m.superheat_k.is_none());
        assert!(m.subcooling_k.is_none());
        assert_eq!(m.phase, NodePhase::Unknown);
    }
}
