//! Anchor — inline spec node (BOLT `BoundaryNode.Refrigerant.Node` equivalent).
//!
//! An `Anchor` is a transparent 2-port node placed on any refrigerant line.
//! It always enforces pass-through continuity (2 equations):
//!
//! ```text
//! r0:  P_out − P_in = 0
//! r1:  h_out − h_in = 0
//! ```
//!
//! and can additionally **impose one thermodynamic spec** at its location
//! (+1 equation), exactly like BOLT's `x_fixed` / `dTsh_fixed` node options:
//!
//! | Constraint | Residual | BOLT equivalent |
//! |---|---|---|
//! | `Superheat(dTsh)` | `T(P,h) − T_sat(P) − dTsh` (dTsh < 0 ⇒ subcooling vs bubble point) | `dTsh_fixed=true, dTsh_set=…` |
//! | `Quality(x)` | `h − h(P, x)` | `x_fixed=true, x_set=…` |
//! | `Temperature(T)` | `T(P,h) − T_set` | `T_fixed=true` |
//! | `Pressure(P)` | `P − P_set` | `p_fixed=true` |
//!
//! ## Degrees of freedom
//!
//! A constraint consumes one DoF: pair it with a freed quantity elsewhere
//! (an emergent pressure, a free actuator, a boundary left free). An anchor
//! with **no constraint is DoF-neutral** (its 2 equations close the 2 unknowns
//! of the extra edge it introduces) and acts as a pure inline probe.
//!
//! ## Jacobian
//!
//! Continuity and pressure rows are exact units. Property-based rows
//! (T, x, dTsh) use central finite differences of the backend property calls
//! over the inlet (P, h) columns — the same technique as the solver's
//! measurement rows.

use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, EquationRole, JacobianBuilder, MeasuredOutput,
    ResidualVector, StateSlice,
};
use entropyk_core::{Enthalpy, Pressure};
use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property, Quality};
use std::sync::Arc;

/// Optional thermodynamic spec imposed by an [`Anchor`] (+1 equation).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnchorConstraint {
    /// Signed superheat [K]: `T − T_dew = dTsh` when `dTsh ≥ 0`;
    /// `T − T_bubble = dTsh` when `dTsh < 0` (i.e. subcooling of `−dTsh`).
    Superheat(f64),
    /// Vapor quality `x ∈ [0, 1]`: `h = h(P, x)`.
    Quality(f64),
    /// Absolute temperature [K]: `T(P, h) = T_set`.
    Temperature(f64),
    /// Absolute pressure [Pa]: `P = P_set`.
    Pressure(f64),
}

/// Inline spec node with pass-through continuity and one optional imposed
/// thermodynamic constraint (see module docs).
#[derive(Clone)]
pub struct Anchor {
    name: String,
    refrigerant_id: String,
    backend: Option<Arc<dyn FluidBackend>>,
    constraint: Option<AnchorConstraint>,
    inlet_m_idx: Option<usize>,
    inlet_p_idx: Option<usize>,
    inlet_h_idx: Option<usize>,
    outlet_p_idx: Option<usize>,
    outlet_h_idx: Option<usize>,
    circuit_id: CircuitId,
    operational_state: OperationalState,
}

impl std::fmt::Debug for Anchor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Anchor")
            .field("name", &self.name)
            .field("refrigerant_id", &self.refrigerant_id)
            .field("constraint", &self.constraint)
            .field("has_backend", &self.backend.is_some())
            .finish()
    }
}

impl Anchor {
    /// Creates a probe-mode anchor (continuity only, DoF-neutral).
    pub fn new(name: impl Into<String>, refrigerant: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            refrigerant_id: refrigerant.into(),
            backend: None,
            constraint: None,
            inlet_m_idx: None,
            inlet_p_idx: None,
            inlet_h_idx: None,
            outlet_p_idx: None,
            outlet_h_idx: None,
            circuit_id: CircuitId::default(),
            operational_state: OperationalState::default(),
        }
    }

    /// Imposes a thermodynamic spec at this anchor (+1 equation; consumes one
    /// system DoF — pair with a freed quantity elsewhere).
    pub fn with_constraint(mut self, constraint: AnchorConstraint) -> Self {
        self.constraint = Some(constraint);
        self
    }

    /// Attaches the fluid backend (required for T / x / dTsh constraints).
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.backend = Some(backend);
        self
    }

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

    /// Returns the active constraint, if any.
    pub fn constraint(&self) -> Option<AnchorConstraint> {
        self.constraint
    }

    fn fluid(&self) -> FluidId {
        FluidId::new(&self.refrigerant_id)
    }

    /// Temperature at (P, h) [K].
    fn temperature(&self, p: f64, h: f64) -> Result<f64, ComponentError> {
        let backend = self.backend.as_ref().ok_or_else(|| {
            ComponentError::InvalidState("Anchor: constraint requires a fluid backend".into())
        })?;
        backend
            .property(
                self.fluid(),
                Property::Temperature,
                FluidState::PressureEnthalpy(
                    Pressure::from_pascals(p),
                    Enthalpy::from_joules_per_kg(h),
                ),
            )
            .map_err(|e| ComponentError::CalculationFailed(format!("Anchor T(P,h): {e}")))
    }

    /// Saturation temperature at P for the given quality reference [K].
    fn saturation_temperature(&self, p: f64, x: f64) -> Result<f64, ComponentError> {
        let backend = self.backend.as_ref().ok_or_else(|| {
            ComponentError::InvalidState("Anchor: constraint requires a fluid backend".into())
        })?;
        backend
            .property(
                self.fluid(),
                Property::Temperature,
                FluidState::PressureQuality(Pressure::from_pascals(p), Quality(x)),
            )
            .map_err(|e| ComponentError::CalculationFailed(format!("Anchor T_sat(P): {e}")))
    }

    /// Saturation enthalpy at (P, x) [J/kg].
    fn saturation_enthalpy(&self, p: f64, x: f64) -> Result<f64, ComponentError> {
        let backend = self.backend.as_ref().ok_or_else(|| {
            ComponentError::InvalidState("Anchor: constraint requires a fluid backend".into())
        })?;
        backend
            .property(
                self.fluid(),
                Property::Enthalpy,
                FluidState::PressureQuality(Pressure::from_pascals(p), Quality(x)),
            )
            .map_err(|e| ComponentError::CalculationFailed(format!("Anchor h_sat(P,x): {e}")))
    }

    /// Constraint residual value at (P_in, h_in).
    fn constraint_residual(&self, p: f64, h: f64) -> Result<f64, ComponentError> {
        match self.constraint {
            None => Ok(0.0),
            Some(AnchorConstraint::Pressure(p_set)) => Ok(p - p_set),
            Some(AnchorConstraint::Temperature(t_set)) => Ok(self.temperature(p, h)? - t_set),
            Some(AnchorConstraint::Quality(x_set)) => Ok(h - self.saturation_enthalpy(p, x_set)?),
            Some(AnchorConstraint::Superheat(dtsh)) => {
                // dTsh ≥ 0 → measure against the dew point (superheat);
                // dTsh < 0 → against the bubble point (subcooling = −dTsh).
                let x_ref = if dtsh >= 0.0 { 1.0 } else { 0.0 };
                let t = self.temperature(p, h)?;
                let t_sat = self.saturation_temperature(p, x_ref)?;
                Ok(t - t_sat - dtsh)
            }
        }
    }

    fn wired(&self) -> Option<(usize, usize, usize, usize)> {
        Some((
            self.inlet_p_idx?,
            self.inlet_h_idx?,
            self.outlet_p_idx?,
            self.outlet_h_idx?,
        ))
    }
}

impl Component for Anchor {
    fn set_system_context(
        &mut self,
        _state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        // [0] = incoming edge (inlet), [1] = outgoing edge (outlet).
        if !external_edge_state_indices.is_empty() {
            let (m, p, h) = external_edge_state_indices[0];
            self.inlet_m_idx = Some(m);
            self.inlet_p_idx = Some(p);
            self.inlet_h_idx = Some(h);
        }
        if external_edge_state_indices.len() >= 2 {
            let (_, p, h) = external_edge_state_indices[1];
            self.outlet_p_idx = Some(p);
            self.outlet_h_idx = Some(h);
        }
    }

    fn n_equations(&self) -> usize {
        2 + usize::from(self.constraint.is_some())
    }

    fn equation_roles(&self) -> Vec<EquationRole> {
        let mut roles = vec![
            EquationRole::Continuity { quantity: "P" },
            EquationRole::Continuity { quantity: "h" },
        ];
        if let Some(c) = self.constraint {
            let kind = match c {
                AnchorConstraint::Superheat(_) => "superheat",
                AnchorConstraint::Quality(_) => "quality",
                AnchorConstraint::Temperature(_) => "temperature",
                AnchorConstraint::Pressure(_) => "pressure",
            };
            // Spec residual consumes one DoF — pair with a free unknown elsewhere.
            roles.push(EquationRole::OutletClosure { kind });
        }
        roles
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        let n = self.n_equations();
        if residuals.len() < n {
            return Err(ComponentError::InvalidResidualDimensions {
                expected: n,
                actual: residuals.len(),
            });
        }
        let Some((in_p, in_h, out_p, out_h)) = self.wired() else {
            return Err(ComponentError::InvalidState(
                "Anchor requires live inlet and outlet edge state indices".to_string(),
            ));
        };
        let p_in = state[in_p];
        let h_in = state[in_h];
        residuals[0] = state[out_p] - p_in;
        residuals[1] = state[out_h] - h_in;
        if self.constraint.is_some() {
            residuals[2] = self.constraint_residual(p_in, h_in)?;
        }
        Ok(())
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        let Some((in_p, in_h, out_p, out_h)) = self.wired() else {
            return Err(ComponentError::InvalidState(
                "Anchor Jacobian requires live inlet and outlet edge state indices".to_string(),
            ));
        };
        jacobian.add_entry(0, out_p, 1.0);
        jacobian.add_entry(0, in_p, -1.0);
        jacobian.add_entry(1, out_h, 1.0);
        jacobian.add_entry(1, in_h, -1.0);

        match self.constraint {
            None => {}
            Some(AnchorConstraint::Pressure(_)) => {
                jacobian.add_entry(2, in_p, 1.0);
            }
            Some(_) => {
                // Property-based rows: central finite differences over (P, h),
                // same technique as the solver's measurement Jacobian rows.
                let p = state[in_p];
                let h = state[in_h];
                let eps_p = p.abs() * 1e-6 + 1e-2;
                let eps_h = h.abs() * 1e-6 + 1e-2;

                let rp_plus = self.constraint_residual(p + eps_p, h)?;
                let rp_minus = self.constraint_residual(p - eps_p, h)?;
                let dp = (rp_plus - rp_minus) / (2.0 * eps_p);
                if dp.abs() > 1e-14 {
                    jacobian.add_entry(2, in_p, dp);
                }

                let rh_plus = self.constraint_residual(p, h + eps_h)?;
                let rh_minus = self.constraint_residual(p, h - eps_h)?;
                let dh = (rh_plus - rh_minus) / (2.0 * eps_h);
                if dh.abs() > 1e-14 {
                    jacobian.add_entry(2, in_h, dh);
                }
            }
        }
        Ok(())
    }

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

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        let Some(m_idx) = self.inlet_m_idx.filter(|&i| i < state.len()) else {
            return Err(ComponentError::InvalidState(
                "Anchor mass-flow reporting requires a live inlet mass-flow index".to_string(),
            ));
        };
        let m = state[m_idx];
        Ok(vec![
            entropyk_core::MassFlow::from_kg_per_s(m),
            entropyk_core::MassFlow::from_kg_per_s(-m),
        ])
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        let (Some(in_h), Some(out_h)) = (self.inlet_h_idx, self.outlet_h_idx) else {
            return Err(ComponentError::CalculationFailed(
                "Anchor: not wired to system context".to_string(),
            ));
        };
        Ok(vec![
            Enthalpy::from_joules_per_kg(state[in_h]),
            Enthalpy::from_joules_per_kg(state[out_h]),
        ])
    }

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

    fn measure_output(&self, kind: MeasuredOutput, state: &StateSlice) -> Option<f64> {
        let (in_p, in_h, _, _) = self.wired()?;
        if in_p >= state.len() || in_h >= state.len() {
            return None;
        }
        let p = state[in_p];
        let h = state[in_h];
        match kind {
            MeasuredOutput::Pressure => Some(p),
            MeasuredOutput::Temperature => self.temperature(p, h).ok().filter(|t| t.is_finite()),
            MeasuredOutput::SaturationTemperature => self
                .saturation_temperature(p, 1.0)
                .ok()
                .filter(|t| t.is_finite()),
            MeasuredOutput::Superheat => {
                let t = self.temperature(p, h).ok()?;
                let t_dew = self.saturation_temperature(p, 1.0).ok()?;
                let sh = t - t_dew;
                sh.is_finite().then_some(sh)
            }
            MeasuredOutput::Subcooling => {
                let t = self.temperature(p, h).ok()?;
                let t_bub = self.saturation_temperature(p, 0.0).ok()?;
                let sc = t_bub - t;
                sc.is_finite().then_some(sc)
            }
            MeasuredOutput::MassFlowRate => {
                let m_idx = self.inlet_m_idx?;
                (m_idx < state.len()).then(|| state[m_idx])
            }
            _ => None,
        }
    }

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

    fn signature(&self) -> String {
        format!(
            "Anchor(name={}, fluid={}, constraint={:?})",
            self.name, self.refrigerant_id, self.constraint
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        let mut params = crate::ComponentParams::new("Anchor")
            .with_param("name", self.name.as_str())
            .with_param("fluid", self.refrigerant_id.as_str());
        match self.constraint {
            Some(AnchorConstraint::Superheat(v)) => params = params.with_param("superheatK", v),
            Some(AnchorConstraint::Quality(v)) => params = params.with_param("quality", v),
            Some(AnchorConstraint::Temperature(v)) => params = params.with_param("tK", v),
            Some(AnchorConstraint::Pressure(v)) => params = params.with_param("pPa", v),
            None => {}
        }
        params
    }
}

impl StateManageable for Anchor {
    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 entropyk_fluids::TestBackend;

    /// Wired anchor over inlet edge (m=0, p=1, h=2) and outlet edge (p=3, h=4).
    fn wired(constraint: Option<AnchorConstraint>) -> Anchor {
        let mut a = Anchor::new("a", "R134a").with_fluid_backend(Arc::new(TestBackend::new()));
        if let Some(c) = constraint {
            a = a.with_constraint(c);
        }
        a.set_system_context(0, &[(0, 1, 2), (0, 3, 4)]);
        a
    }

    #[test]
    fn test_probe_mode_is_dof_neutral_continuity() {
        let a = wired(None);
        assert_eq!(a.n_equations(), 2);
        // Continuity satisfied ⇒ zero residuals.
        let state = vec![0.1, 1.0e6, 400_000.0, 1.0e6, 400_000.0];
        let mut r = vec![0.0; 2];
        a.compute_residuals(&state, &mut r).unwrap();
        assert!(r[0].abs() < 1e-12 && r[1].abs() < 1e-12);
        // Discontinuity detected.
        let state2 = vec![0.1, 1.0e6, 400_000.0, 1.1e6, 410_000.0];
        a.compute_residuals(&state2, &mut r).unwrap();
        assert!((r[0] - 1.0e5).abs() < 1e-9 && (r[1] - 10_000.0).abs() < 1e-9);
    }

    #[test]
    fn test_pressure_constraint_exact() {
        let a = wired(Some(AnchorConstraint::Pressure(9.0e5)));
        assert_eq!(a.n_equations(), 3);
        let state = vec![0.1, 9.0e5, 400_000.0, 9.0e5, 400_000.0];
        let mut r = vec![0.0; 3];
        a.compute_residuals(&state, &mut r).unwrap();
        assert!(r[2].abs() < 1e-12);
        let state2 = vec![0.1, 9.5e5, 400_000.0, 9.5e5, 400_000.0];
        a.compute_residuals(&state2, &mut r).unwrap();
        assert!((r[2] - 5.0e4).abs() < 1e-9);
    }

    #[test]
    fn test_quality_constraint_zero_at_saturation() {
        let backend = TestBackend::new();
        let p = 1.0e6;
        let h_sat = backend
            .property(
                FluidId::new("R134a"),
                Property::Enthalpy,
                FluidState::PressureQuality(Pressure::from_pascals(p), Quality(1.0)),
            )
            .unwrap();
        let a = wired(Some(AnchorConstraint::Quality(1.0)));
        let state = vec![0.1, p, h_sat, p, h_sat];
        let mut r = vec![0.0; 3];
        a.compute_residuals(&state, &mut r).unwrap();
        assert!(r[2].abs() < 1e-9, "r2 = {}", r[2]);
        // 10 kJ/kg above saturation → residual = +10 kJ/kg.
        let state2 = vec![0.1, p, h_sat + 10_000.0, p, h_sat + 10_000.0];
        a.compute_residuals(&state2, &mut r).unwrap();
        assert!((r[2] - 10_000.0).abs() < 1e-9);
    }

    #[test]
    fn test_superheat_constraint_semantics() {
        let backend = TestBackend::new();
        let p = 1.0e6;
        let fluid = FluidId::new("R134a");
        let t_dew = backend
            .property(
                fluid.clone(),
                Property::Temperature,
                FluidState::PressureQuality(Pressure::from_pascals(p), Quality(1.0)),
            )
            .unwrap();
        // Find h giving T = t_dew + 5 via the backend itself.
        let h_sh5 = {
            // TestBackend is monotone in h; bisect for T(P,h) = t_dew + 5.
            let t_target = t_dew + 5.0;
            let (mut lo, mut hi) = (200_000.0, 600_000.0);
            for _ in 0..80 {
                let mid = 0.5 * (lo + hi);
                let t = backend
                    .property(
                        fluid.clone(),
                        Property::Temperature,
                        FluidState::PressureEnthalpy(
                            Pressure::from_pascals(p),
                            Enthalpy::from_joules_per_kg(mid),
                        ),
                    )
                    .unwrap();
                if t < t_target {
                    lo = mid;
                } else {
                    hi = mid;
                }
            }
            0.5 * (lo + hi)
        };
        let a = wired(Some(AnchorConstraint::Superheat(5.0)));
        let state = vec![0.1, p, h_sh5, p, h_sh5];
        let mut r = vec![0.0; 3];
        a.compute_residuals(&state, &mut r).unwrap();
        assert!(r[2].abs() < 1e-3, "SH=5 spec must close: r2 = {}", r[2]);
    }

    #[test]
    fn test_constraint_jacobian_matches_fd_of_residual() {
        let a = wired(Some(AnchorConstraint::Quality(1.0)));
        let state = vec![0.1, 1.0e6, 410_000.0, 1.0e6, 410_000.0];
        let mut jac = JacobianBuilder::new();
        a.jacobian_entries(&state, &mut jac).unwrap();

        for col in [1usize, 2] {
            let eps = state[col].abs() * 1e-5 + 1e-3;
            let mut sp = state.clone();
            let mut sm = state.clone();
            sp[col] += eps;
            sm[col] -= eps;
            let (mut rp, mut rm) = (vec![0.0; 3], vec![0.0; 3]);
            a.compute_residuals(&sp, &mut rp).unwrap();
            a.compute_residuals(&sm, &mut rm).unwrap();
            let fd = (rp[2] - rm[2]) / (2.0 * eps);
            let analytic: f64 = jac
                .entries()
                .iter()
                .filter(|(r, c, _)| *r == 2 && *c == col)
                .map(|(_, _, v)| *v)
                .sum();
            assert!(
                (fd - analytic).abs() < 1e-4 * (1.0 + fd.abs()),
                "col {col}: fd {fd} vs analytic {analytic}"
            );
        }
    }

    #[test]
    fn test_constraint_without_backend_errors() {
        let mut a = Anchor::new("a", "R134a").with_constraint(AnchorConstraint::Temperature(300.0));
        a.set_system_context(0, &[(0, 1, 2), (0, 3, 4)]);
        let state = vec![0.1, 1.0e6, 400_000.0, 1.0e6, 400_000.0];
        let mut r = vec![0.0; 3];
        assert!(a.compute_residuals(&state, &mut r).is_err());
    }
}
