//! Damped backend wrapper for fluid property queries.
//!
//! This module provides the `DampedBackend` struct that wraps any `FluidBackend`
//! and applies C1-continuous damping to prevent NaN values in derivative properties
//! near the critical point.

use crate::backend::FluidBackend;
use crate::damping::{calculate_damping_state, damp_property, should_damp_property, DampingParams};
use crate::errors::FluidResult;
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};

/// Backend wrapper that applies critical point damping to property queries.
///
/// Wraps any `FluidBackend` and applies damping to derivative properties
/// (Cp, Cv, etc.) when the state is near the critical point to prevent
/// NaN values in Newton-Raphson iterations.
pub struct DampedBackend<B: FluidBackend> {
    inner: B,
    params: DampingParams,
}

impl<B: FluidBackend> DampedBackend<B> {
    /// Create a new damped backend wrapping the given backend.
    pub fn new(inner: B) -> Self {
        DampedBackend {
            inner,
            params: DampingParams::default(),
        }
    }

    /// Create a new damped backend with custom parameters.
    pub fn with_params(inner: B, params: DampingParams) -> Self {
        DampedBackend { inner, params }
    }

    /// Get a reference to the inner backend.
    pub fn inner(&self) -> &B {
        &self.inner
    }

    /// Get a mutable reference to the inner backend.
    pub fn inner_mut(&mut self) -> &mut B {
        &mut self.inner
    }

    /// Get the damping parameters.
    pub fn params(&self) -> &DampingParams {
        &self.params
    }

    /// Get critical point for a fluid.
    fn critical_point_internal(&self, fluid: &FluidId) -> Option<CriticalPoint> {
        self.inner.critical_point(fluid.clone()).ok()
    }

    /// Apply damping to a property value if needed.
    fn apply_damping(
        &self,
        fluid: &FluidId,
        property: Property,
        state: &FluidState,
        value: f64,
    ) -> FluidResult<f64> {
        // Only damp derivative properties
        if !should_damp_property(property) {
            return Ok(value);
        }

        // Check if value is NaN - if so, try to recover with damping
        if value.is_nan() {
            // Try to get critical point
            if let Some(cp) = self.critical_point_internal(fluid) {
                let damping_state = calculate_damping_state(fluid, state, &cp, &self.params);
                if damping_state.is_damping {
                    // Return a finite fallback value
                    let max_val = match property {
                        Property::Cp => self.params.cp_max,
                        Property::Cv => self.params.cv_max,
                        Property::Density => 1e5,
                        Property::SpeedOfSound => 1e4,
                        _ => self.params.derivative_max,
                    };
                    return Ok(max_val * damping_state.blend_factor);
                }
            }
            // No critical point info - return error
            return Ok(self.params.derivative_max);
        }

        // Get critical point for damping calculation
        let cp = match self.critical_point_internal(fluid) {
            Some(cp) => cp,
            None => return Ok(value),
        };

        let damping_state = calculate_damping_state(fluid, state, &cp, &self.params);

        if !damping_state.is_damping {
            return Ok(value);
        }

        // Apply damping based on property type
        let max_value = match property {
            Property::Cp => self.params.cp_max,
            Property::Cv => self.params.cv_max,
            Property::Density => 1e5,
            Property::SpeedOfSound => 1e4,
            _ => self.params.derivative_max,
        };

        let damped = damp_property(value, max_value, damping_state.blend_factor);
        Ok(damped)
    }
}

impl<B: FluidBackend> FluidBackend for DampedBackend<B> {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        let value = self
            .inner
            .property(fluid.clone(), property, state.clone())?;
        self.apply_damping(&fluid, property, &state, value)
    }

    fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
        self.inner.critical_point(fluid)
    }

    fn is_fluid_available(&self, fluid: &FluidId) -> bool {
        self.inner.is_fluid_available(fluid)
    }

    fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
        self.inner.phase(fluid, state)
    }

    fn list_fluids(&self) -> Vec<FluidId> {
        self.inner.list_fluids()
    }

    fn full_state(
        &self,
        fluid: FluidId,
        p: entropyk_core::Pressure,
        h: entropyk_core::Enthalpy,
    ) -> FluidResult<crate::types::ThermoState> {
        self.inner.full_state(fluid, p, h)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::backend::FluidBackend;
    use crate::errors::{FluidError, FluidResult};
    use crate::test_backend::TestBackend;
    use entropyk_core::{Pressure, Temperature};

    #[test]
    fn test_damped_backend_creation() {
        let inner = TestBackend::new();
        let damped = DampedBackend::new(inner);

        assert!(damped.is_fluid_available(&FluidId::new("R134a")));
    }

    #[test]
    fn test_damped_backend_delegates_non_derivative() {
        let inner = TestBackend::new();
        let damped = DampedBackend::new(inner);

        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));

        // Enthalpy should be delegated without damping
        let h = damped
            .property(FluidId::new("R134a"), Property::Enthalpy, state.clone())
            .unwrap();

        // TestBackend returns constant values, so check it's not zero
        assert!(h > 0.0);
    }

    #[test]
    fn test_damped_backend_with_custom_params() {
        let inner = TestBackend::new();
        let params = DampingParams {
            reduced_temp_threshold: 0.1,
            reduced_pressure_threshold: 0.1,
            smoothness: 0.02,
            cp_max: 5000.0,
            cv_max: 3000.0,
            derivative_max: 1e8,
        };
        let damped = DampedBackend::with_params(inner, params);

        assert_eq!(damped.params().cp_max, 5000.0);
    }

    #[test]
    fn test_damped_backend_returns_finite_values() {
        let inner = TestBackend::new();
        let damped = DampedBackend::new(inner);

        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));

        // Cp should return a finite value (not NaN)
        let cp = damped
            .property(FluidId::new("R134a"), Property::Cp, state.clone())
            .unwrap();

        assert!(!cp.is_nan(), "Cp should not be NaN");
        assert!(cp.is_finite(), "Cp should be finite");
    }

    #[test]
    fn test_damped_backend_handles_nan_input() {
        // Create a backend that returns NaN
        struct NaNBackend;

        impl FluidBackend for NaNBackend {
            fn property(
                &self,
                _fluid: FluidId,
                property: Property,
                _state: FluidState,
            ) -> FluidResult<f64> {
                if matches!(property, Property::Cp) {
                    Ok(f64::NAN)
                } else {
                    Ok(1000.0)
                }
            }
            fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
                Ok(CriticalPoint::new(
                    Temperature::from_kelvin(304.13),
                    Pressure::from_pascals(7.3773e6),
                    467.6,
                ))
            }
            fn is_fluid_available(&self, _fluid: &FluidId) -> bool {
                true
            }
            fn phase(&self, _fluid: FluidId, _state: FluidState) -> FluidResult<Phase> {
                Ok(Phase::Unknown)
            }
            fn list_fluids(&self) -> Vec<FluidId> {
                vec![FluidId::new("CO2")]
            }
            fn full_state(
                &self,
                _fluid: FluidId,
                _p: entropyk_core::Pressure,
                _h: entropyk_core::Enthalpy,
            ) -> FluidResult<crate::types::ThermoState> {
                Err(FluidError::CoolPropError(
                    "full_state not supported on NaNBackend".to_string(),
                ))
            }
        }

        let inner = NaNBackend;
        let damped = DampedBackend::new(inner);

        let state = FluidState::from_pt(
            Pressure::from_pascals(7.3773e6),
            Temperature::from_kelvin(304.13),
        );

        // Should return a finite value instead of NaN
        let cp = damped
            .property(FluidId::new("CO2"), Property::Cp, state)
            .unwrap();

        assert!(!cp.is_nan(), "Should return finite value instead of NaN");
    }

    #[test]
    #[cfg(feature = "coolprop")]
    fn test_co2_near_critical_no_nan() {
        use crate::coolprop::CoolPropBackend;

        let inner = CoolPropBackend::new();
        let damped = DampedBackend::new(inner);

        // CO2 at 0.99*Tc, 0.99*Pc - near critical
        let tc = 304.13;
        let pc = 7.3773e6;
        let state = FluidState::from_pt(
            Pressure::from_pascals(0.99 * pc),
            Temperature::from_kelvin(0.99 * tc),
        );

        // Should not return NaN
        let cp = damped
            .property(FluidId::new("CO2"), Property::Cp, state)
            .unwrap();

        assert!(!cp.is_nan(), "Cp should not be NaN near critical point");
        assert!(cp.is_finite(), "Cp should be finite");
    }

    #[test]
    #[cfg(feature = "coolprop")]
    fn test_co2_supercritical_no_nan() {
        use crate::coolprop::CoolPropBackend;

        let inner = CoolPropBackend::new();
        let damped = DampedBackend::new(inner);

        // CO2 at 1.01*Tc, 1.01*Pc - supercritical
        let tc = 304.13;
        let pc = 7.3773e6;
        let state = FluidState::from_pt(
            Pressure::from_pascals(1.01 * pc),
            Temperature::from_kelvin(1.01 * tc),
        );

        // Should not return NaN
        let cp = damped
            .property(FluidId::new("CO2"), Property::Cp, state)
            .unwrap();

        assert!(!cp.is_nan(), "Cp should not be NaN in supercritical region");
        assert!(cp.is_finite(), "Cp should be finite");
    }

    #[test]
    #[cfg(feature = "coolprop")]
    fn test_r134a_unchanged_far_from_critical() {
        use crate::coolprop::CoolPropBackend;

        let inner_no_damp = CoolPropBackend::new();
        let inner_damped = CoolPropBackend::new();
        let damped = DampedBackend::new(inner_damped);

        // R134a far from critical (room temp, 1 bar)
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));

        let cp_no_damp = inner_no_damp
            .property(FluidId::new("R134a"), Property::Cp, state.clone())
            .unwrap();
        let cp_damped = damped
            .property(FluidId::new("R134a"), Property::Cp, state)
            .unwrap();

        // Values should be essentially the same (damping shouldn't affect far-from-critical)
        assert!(
            (cp_no_damp - cp_damped).abs() < 1.0,
            "R134a far from critical should be unchanged"
        );
    }
}
