//! Air-Cooled Condenser Component
//!
//! A condenseur à condensation par air où l'ingénieur fournit directement :
//! - `oat_k`      : Outdoor Air Temperature [K]  (OAT = température de l'air extérieur)
//! - `approach_k` : Approach temperature [K], différence de température entre la
//!                  condensation et l'air extérieur. Valeur typique : 10–15 K.
//!                  Défaut : 12 K (représentatif d'un chiller air-cooled industriel).
//!
//! La température de condensation est calculée automatiquement :
//! ```text
//! T_cond = OAT + approach_k
//! ```
//!
//! ## Équations (2)
//!
//! - r0 = P_out − P_sat(T_cond)      [drive outlet pressure to condensing saturation]
//! - r1 = H_out − H_sat_liq(T_cond)  [drive outlet enthalpy to saturated-liquid]
//!
//! ## Jacobian (analytique)
//!
//! - ∂r0/∂P_out = 1
//! - ∂r1/∂H_out = 1
//!
//! ## Exemple JSON
//!
//! ```json
//! {
//!   "type": "AirCooledCondenser",
//!   "name": "cond",
//!   "oat_k": 308.15,
//!   "approach_k": 12.0
//! }
//! ```
//!
//! Soit : OAT = 35°C → T_cond = 47°C → P_cond_sat(R290) ≈ 16.8 bar

use super::condenser::Condenser;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
    Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::Calib;
use entropyk_fluids::FluidBackend;
use std::sync::Arc;

/// Air-cooled condenser : T_cond = OAT + approach.
///
/// L'ingénieur saisit directement la température extérieure (OAT) et
/// l'approche de condensation (approach_k). Aucune conversion manuelle
/// en `t_sat_k` n'est nécessaire.
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::AirCooledCondenser;
/// use entropyk_components::Component;
///
/// // OAT = 35°C, approach = 12 K  →  T_cond = 47°C = 320.15 K
/// let cond = AirCooledCondenser::new(308.15, 12.0);
/// assert_eq!(cond.n_equations(), 3); // 2 thermo + 1 mass-flow (CM1.3)
/// assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
/// ```
#[derive(Debug)]
pub struct AirCooledCondenser {
    inner: Condenser,
    oat_k: f64,
    approach_k: f64,
}

impl AirCooledCondenser {
    /// Crée un condenseur à air.
    ///
    /// # Arguments
    ///
    /// * `oat_k`      — Outdoor Air Temperature \[K\]
    /// * `approach_k` — Approach temperature = T_cond − OAT \[K\]. Valeur typique : 10–15 K.
    pub fn new(oat_k: f64, approach_k: f64) -> Self {
        let t_cond_k = oat_k + approach_k;
        Self {
            inner: Condenser::with_saturation_temp(0.0, t_cond_k),
            oat_k,
            approach_k,
        }
    }

    /// Crée un condenseur à air avec UA connu.
    ///
    /// Le UA n'est pas utilisé dans les équations du solver (approach-based),
    /// mais il est disponible pour les calculs de performance post-traitement.
    pub fn with_ua(oat_k: f64, approach_k: f64, ua: f64) -> Self {
        let t_cond_k = oat_k + approach_k;
        Self {
            inner: Condenser::with_saturation_temp(ua, t_cond_k),
            oat_k,
            approach_k,
        }
    }

    /// Attache un identifiant de fluide réfrigérant.
    pub fn with_refrigerant(mut self, id: &str) -> Self {
        self.inner = self.inner.with_refrigerant(id);
        self
    }

    /// Attache un backend de propriétés thermodynamiques.
    pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
        self.inner = self.inner.with_fluid_backend(backend);
        self
    }

    /// Retourne la température de condensation calculée [K].
    pub fn t_cond_k(&self) -> f64 {
        self.oat_k + self.approach_k
    }

    /// Retourne la température extérieure (OAT) [K].
    pub fn oat_k(&self) -> f64 {
        self.oat_k
    }

    /// Retourne l'approche de condensation [K].
    pub fn approach_k(&self) -> f64 {
        self.approach_k
    }

    /// Retourne le UA [W/K].
    pub fn ua(&self) -> f64 {
        self.inner.ua()
    }

    /// Retourne les facteurs de calibration.
    pub fn calib(&self) -> &Calib {
        self.inner.calib()
    }

    /// Met à jour les facteurs de calibration.
    pub fn set_calib(&mut self, calib: Calib) {
        self.inner.set_calib(calib);
    }

    /// Met à jour OAT en runtime (ex. simulation paramétrique).
    pub fn set_oat_k(&mut self, oat_k: f64) {
        self.oat_k = oat_k;
        self.inner.set_saturation_temp(self.oat_k + self.approach_k);
    }

    /// Met à jour l'approche en runtime.
    pub fn set_approach_k(&mut self, approach_k: f64) {
        self.approach_k = approach_k;
        self.inner.set_saturation_temp(self.oat_k + self.approach_k);
    }
}

impl Component for AirCooledCondenser {
    fn set_system_context(
        &mut self,
        state_offset: usize,
        external_edge_state_indices: &[(usize, usize, usize)],
    ) {
        self.inner
            .set_system_context(state_offset, external_edge_state_indices);
    }

    fn compute_residuals(
        &self,
        state: &StateSlice,
        residuals: &mut ResidualVector,
    ) -> Result<(), ComponentError> {
        self.inner.compute_residuals(state, residuals)
    }

    fn jacobian_entries(
        &self,
        state: &StateSlice,
        jacobian: &mut JacobianBuilder,
    ) -> Result<(), ComponentError> {
        self.inner.jacobian_entries(state, jacobian)
    }

    fn n_equations(&self) -> usize {
        self.inner.n_equations()
    }

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

    fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
        self.inner.set_fluid_backend_from_builder(backend);
    }

    fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
        self.inner.set_calib_indices(indices);
    }

    fn port_mass_flows(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
        self.inner.port_mass_flows(state)
    }

    fn port_enthalpies(
        &self,
        state: &StateSlice,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        self.inner.port_enthalpies(state)
    }

    fn energy_transfers(
        &self,
        state: &StateSlice,
    ) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
        self.inner.energy_transfers(state)
    }

    fn signature(&self) -> String {
        format!(
            "AirCooledCondenser(oat={:.1}K, approach={:.1}K, t_cond={:.1}K)",
            self.oat_k,
            self.approach_k,
            self.t_cond_k()
        )
    }

    fn to_params(&self) -> crate::ComponentParams {
        self.inner.to_params()
    }

    fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
        self.inner.update_calib_factor(factor, value)
    }
}

impl StateManageable for AirCooledCondenser {
    fn state(&self) -> OperationalState {
        self.inner.state()
    }

    fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
        self.inner.set_state(state)
    }

    fn can_transition_to(&self, target: OperationalState) -> bool {
        self.inner.can_transition_to(target)
    }

    fn circuit_id(&self) -> &CircuitId {
        self.inner.circuit_id()
    }

    fn set_circuit_id(&mut self, circuit_id: CircuitId) {
        self.inner.set_circuit_id(circuit_id);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_t_cond_computed() {
        // OAT = 35°C = 308.15 K, approach = 12 K → T_cond = 47°C = 320.15 K
        let cond = AirCooledCondenser::new(308.15, 12.0);
        assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
        assert_eq!(cond.oat_k(), 308.15);
        assert_eq!(cond.approach_k(), 12.0);
        // CM1.3: 2 thermo + 1 mass-flow = 3 (delegates to inner Condenser)
        assert_eq!(cond.n_equations(), 3);
    }

    #[test]
    fn test_with_ua() {
        let cond = AirCooledCondenser::with_ua(308.15, 12.0, 6000.0);
        assert_eq!(cond.ua(), 6000.0);
        assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
    }

    #[test]
    fn test_set_oat_updates_t_cond() {
        let mut cond = AirCooledCondenser::new(308.15, 12.0);
        // Change OAT to 40°C = 313.15 K → T_cond = 52°C = 325.15 K
        cond.set_oat_k(313.15);
        assert!((cond.t_cond_k() - 325.15).abs() < 1e-9);
    }

    #[test]
    fn test_set_approach_updates_t_cond() {
        let mut cond = AirCooledCondenser::new(308.15, 12.0);
        cond.set_approach_k(10.0);
        assert!((cond.t_cond_k() - 318.15).abs() < 1e-9);
    }

    #[test]
    fn test_no_backend_does_not_fabricate_residuals() {
        let cond = AirCooledCondenser::new(308.15, 12.0);
        let state = vec![0.0_f64; 10];
        let mut residuals = vec![99.0_f64; 3];
        let result = cond.compute_residuals(&state, &mut residuals);
        assert!(matches!(result, Err(ComponentError::InvalidState(_))));
    }
}
