//! Incompressible fluid properties backend.
//!
//! Provides lightweight polynomial models for water, glycol, and humid air
//! without external library calls. Properties obtained from IAPWS-IF97
//! (water) and ASHRAE (glycol) reference data.

use crate::backend::FluidBackend;
use crate::errors::{FluidError, FluidResult};
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};

/// Incompressible fluid identifier.
///
/// Maps FluidId strings to internal fluid types. Supports:
/// - Water
/// - EthyleneGlycol with concentration 0.0–0.6 mass fraction
/// - PropyleneGlycol with concentration 0.0–0.6 mass fraction
/// - HumidAir
#[derive(Debug, Clone, PartialEq)]
pub enum IncompFluid {
    /// Pure water (liquid phase)
    Water,
    /// Ethylene glycol aqueous solution, concentration = mass fraction (0.0–0.6)
    EthyleneGlycol(f64),
    /// Propylene glycol aqueous solution, concentration = mass fraction (0.0–0.6)
    PropyleneGlycol(f64),
    /// Humid air (simplified psychrometric)
    HumidAir,
}

impl IncompFluid {
    /// Parses a FluidId into an IncompFluid if it represents an incompressible fluid.
    ///
    /// Recognized formats:
    /// - "Water"
    /// - "EthyleneGlycol" or "EthyleneGlycol30" (30% = 0.3)
    /// - "PropyleneGlycol" or "PropyleneGlycol50" (50% = 0.5)
    /// - "HumidAir"
    pub fn from_fluid_id(fluid_id: &FluidId) -> Option<Self> {
        let s = fluid_id.0.as_str();
        if s.eq_ignore_ascii_case("Water") {
            return Some(IncompFluid::Water);
        }
        if s.eq_ignore_ascii_case("HumidAir") {
            return Some(IncompFluid::HumidAir);
        }
        if s.to_lowercase().starts_with("ethyleneglycol") {
            let conc = parse_glycol_concentration(s, "ethyleneglycol")?;
            if (0.0..=0.6).contains(&conc) {
                return Some(IncompFluid::EthyleneGlycol(conc));
            }
        }
        if s.to_lowercase().starts_with("propyleneglycol") {
            let conc = parse_glycol_concentration(s, "propyleneglycol")?;
            if (0.0..=0.6).contains(&conc) {
                return Some(IncompFluid::PropyleneGlycol(conc));
            }
        }
        None
    }

    /// Valid temperature range (K) for this fluid.
    pub fn valid_temp_range(&self) -> (f64, f64) {
        match self {
            IncompFluid::Water => (273.15, 373.15),
            IncompFluid::EthyleneGlycol(_) | IncompFluid::PropyleneGlycol(_) => (243.15, 373.15),
            IncompFluid::HumidAir => (233.15, 353.15),
        }
    }
}

fn parse_glycol_concentration(s: &str, prefix: &str) -> Option<f64> {
    let rest = s.get(prefix.len()..)?.trim();
    if rest.is_empty() {
        return Some(0.0); // Pure water in glycol context = 0%
    }
    rest.parse::<f64>().ok().map(|x| x / 100.0)
}

/// Valid temperature range for incompressible fluids.
#[derive(Debug, Clone, Copy)]
pub struct ValidRange {
    /// Minimum temperature (K)
    pub min_temp_k: f64,
    /// Maximum temperature (K)
    pub max_temp_k: f64,
}

impl ValidRange {
    /// Checks if temperature is within valid range.
    pub fn contains(&self, t_k: f64) -> bool {
        t_k >= self.min_temp_k && t_k <= self.max_temp_k
    }
}

/// Water density from simplified polynomial (liquid region 273–373 K).
///
/// Fitted to IAPWS-IF97 reference: 20°C→998.2, 50°C→988.0, 80°C→971.8 kg/m³ (within 0.1%).
/// ρ(kg/m³) = 1001.7 - 0.107*T°C - 0.00333*(T°C)²
fn water_density_kelvin(t_k: f64) -> f64 {
    let t_c = t_k - 273.15;
    1001.7 - 0.107 * t_c - 0.00333 * t_c * t_c
}

fn water_cp_kelvin(_t_k: f64) -> f64 {
    // Cp ≈ 4182 J/(kg·K) at 20°C, varies slightly with T
    // Simplified: constant 4184 for liquid water 0–100°C
    4184.0
}

fn water_viscosity_kelvin(t_k: f64) -> f64 {
    let t_c = t_k - 273.15;
    // μ(Pa·s) for liquid water: 20°C→0.001, 40°C→0.00065
    // Rational form: μ = 0.001 / (1 + 0.02*(T-20)) for T in °C
    0.001 / (1.0 + 0.02 * (t_c - 20.0).max(0.0))
}

/// Incompressible fluid properties backend.
///
/// Implements FluidBackend for water, ethylene glycol, propylene glycol,
/// and humid air using lightweight polynomial models. No external library calls.
pub struct IncompressibleBackend;

impl IncompressibleBackend {
    /// Creates a new IncompressibleBackend.
    pub fn new() -> Self {
        IncompressibleBackend
    }

    fn property_water(&self, property: Property, t_k: f64) -> FluidResult<f64> {
        if !t_k.is_finite() {
            return Err(FluidError::InvalidState {
                reason: format!("Temperature {} K is not finite", t_k),
            });
        }
        let (min_t, max_t) = IncompFluid::Water.valid_temp_range();
        if t_k < min_t || t_k > max_t {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "Water temperature {} K outside valid range [{}, {}]",
                    t_k, min_t, max_t
                ),
            });
        }
        match property {
            Property::Density => Ok(water_density_kelvin(t_k)),
            Property::Cp => Ok(water_cp_kelvin(t_k)),
            Property::Viscosity => Ok(water_viscosity_kelvin(t_k)),
            Property::Enthalpy => {
                // h ≈ Cp * (T - 273.15) relative to 0°C liquid
                Ok(water_cp_kelvin(t_k) * (t_k - 273.15))
            }
            Property::Temperature => Ok(t_k),
            _ => Err(FluidError::UnsupportedProperty {
                property: format!("{} for Water", property),
            }),
        }
    }

    fn property_glycol(
        &self,
        property: Property,
        t_k: f64,
        concentration: f64,
        is_ethylene: bool,
    ) -> FluidResult<f64> {
        if !t_k.is_finite() {
            return Err(FluidError::InvalidState {
                reason: format!("Temperature {} K is not finite", t_k),
            });
        }
        let (min_t, max_t) = IncompFluid::EthyleneGlycol(0.0).valid_temp_range();
        if t_k < min_t || t_k > max_t {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "Glycol temperature {} K outside valid range [{}, {}]",
                    t_k, min_t, max_t
                ),
            });
        }
        if !(0.0..=0.6).contains(&concentration) {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "Glycol concentration {} outside valid range [0, 0.6]",
                    concentration
                ),
            });
        }
        // ASHRAE simplified: density increases with concentration, decreases with T
        let rho_water = water_density_kelvin(t_k);
        let t_c = t_k - 273.15;
        match (property, is_ethylene) {
            (Property::Density, true) => {
                // EG: ρ ≈ ρ_water*(1 - 0.4*X) + 1115*X for X=concentration (approx)
                Ok(rho_water * (1.0 - concentration) + 1115.0 * concentration)
            }
            (Property::Density, false) => {
                Ok(rho_water * (1.0 - concentration) + 1036.0 * concentration)
            }
            (Property::Cp, true) => {
                // EG 30%: ~3900, EG 50%: ~3400 J/(kg·K) at 20°C
                Ok(4184.0 * (1.0 - concentration) + 2400.0 * concentration)
            }
            (Property::Cp, false) => Ok(4184.0 * (1.0 - concentration) + 2500.0 * concentration),
            (Property::Viscosity, _) => {
                // Viscosity increases strongly with concentration and decreases with T
                let mu_water = water_viscosity_kelvin(t_k);
                let conc_factor = 1.0 + 10.0 * concentration;
                let temp_factor = (-0.02 * (t_c - 20.0)).exp();
                Ok(mu_water * conc_factor * temp_factor)
            }
            (Property::Enthalpy, _) => {
                let cp = if is_ethylene {
                    4184.0 * (1.0 - concentration) + 2400.0 * concentration
                } else {
                    4184.0 * (1.0 - concentration) + 2500.0 * concentration
                };
                Ok(cp * (t_k - 273.15))
            }
            (Property::Temperature, _) => Ok(t_k),
            _ => Err(FluidError::UnsupportedProperty {
                property: format!("{} for glycol", property),
            }),
        }
    }

    fn property_humid_air(&self, property: Property, t_k: f64) -> FluidResult<f64> {
        if !t_k.is_finite() {
            return Err(FluidError::InvalidState {
                reason: format!("Temperature {} K is not finite", t_k),
            });
        }
        let (min_t, max_t) = IncompFluid::HumidAir.valid_temp_range();
        if t_k < min_t || t_k > max_t {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "HumidAir temperature {} K outside valid range [{}, {}]",
                    t_k, min_t, max_t
                ),
            });
        }
        match property {
            Property::Cp => Ok(1005.0), // Dry air Cp
            Property::Temperature => Ok(t_k),
            Property::Density => Ok(1.2), // Approximate at 20°C, 1 atm
            _ => Err(FluidError::UnsupportedProperty {
                property: format!("{} for HumidAir", property),
            }),
        }
    }
}

impl Default for IncompressibleBackend {
    fn default() -> Self {
        Self::new()
    }
}

impl FluidBackend for IncompressibleBackend {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        let (t_k, _p) = match &state {
            FluidState::PressureTemperature(p, t) => (t.to_kelvin(), p.to_pascals()),
            _ => {
                return Err(FluidError::InvalidState {
                    reason: "IncompressibleBackend requires PressureTemperature state".to_string(),
                })
            }
        };

        if let Some(incomp) = IncompFluid::from_fluid_id(&fluid) {
            match incomp {
                IncompFluid::Water => self.property_water(property, t_k),
                IncompFluid::EthyleneGlycol(conc) => {
                    self.property_glycol(property, t_k, conc, true)
                }
                IncompFluid::PropyleneGlycol(conc) => {
                    self.property_glycol(property, t_k, conc, false)
                }
                IncompFluid::HumidAir => self.property_humid_air(property, t_k),
            }
        } else {
            Err(FluidError::UnknownFluid { fluid: fluid.0 })
        }
    }

    fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
        if IncompFluid::from_fluid_id(&fluid).is_none() {
            return Err(FluidError::UnknownFluid { fluid: fluid.0 });
        }
        Err(FluidError::NoCriticalPoint { fluid: fluid.0 })
    }

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

    fn phase(&self, fluid: FluidId, _state: FluidState) -> FluidResult<Phase> {
        match IncompFluid::from_fluid_id(&fluid) {
            Some(IncompFluid::HumidAir) => Ok(Phase::Vapor),
            Some(_) => Ok(Phase::Liquid),
            None => Err(FluidError::UnknownFluid { fluid: fluid.0 }),
        }
    }

    fn list_fluids(&self) -> Vec<FluidId> {
        vec![
            FluidId::new("Water"),
            FluidId::new("EthyleneGlycol"),
            FluidId::new("EthyleneGlycol30"),
            FluidId::new("EthyleneGlycol50"),
            FluidId::new("PropyleneGlycol"),
            FluidId::new("PropyleneGlycol30"),
            FluidId::new("PropyleneGlycol50"),
            FluidId::new("HumidAir"),
        ]
    }

    fn full_state(
        &self,
        fluid: FluidId,
        p: entropyk_core::Pressure,
        h: entropyk_core::Enthalpy,
    ) -> FluidResult<crate::types::ThermoState> {
        let t_k = self.property(
            fluid.clone(),
            Property::Temperature,
            FluidState::from_ph(p, h),
        )?;
        Err(FluidError::UnsupportedProperty {
            property: format!("full_state for IncompressibleBackend: Temperature is {:.2} K but full state not natively implemented yet", t_k),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use entropyk_core::{Pressure, Temperature};

    #[test]
    fn test_incomp_fluid_from_fluid_id() {
        assert!(matches!(
            IncompFluid::from_fluid_id(&FluidId::new("Water")),
            Some(IncompFluid::Water)
        ));
        assert!(matches!(
            IncompFluid::from_fluid_id(&FluidId::new("water")),
            Some(IncompFluid::Water)
        ));
        assert!(matches!(
            IncompFluid::from_fluid_id(&FluidId::new("EthyleneGlycol30")),
            Some(IncompFluid::EthyleneGlycol(c)) if (c - 0.3).abs() < 0.01
        ));
        assert!(matches!(
            IncompFluid::from_fluid_id(&FluidId::new("PropyleneGlycol50")),
            Some(IncompFluid::PropyleneGlycol(c)) if (c - 0.5).abs() < 0.01
        ));
        assert!(IncompFluid::from_fluid_id(&FluidId::new("R134a")).is_none());
    }

    #[test]
    fn test_water_density_at_temperatures() {
        let backend = IncompressibleBackend::new();
        let state_20 =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let state_50 =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(50.0));
        let state_80 =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(80.0));

        let rho_20 = backend
            .property(FluidId::new("Water"), Property::Density, state_20)
            .unwrap();
        let rho_50 = backend
            .property(FluidId::new("Water"), Property::Density, state_50)
            .unwrap();
        let rho_80 = backend
            .property(FluidId::new("Water"), Property::Density, state_80)
            .unwrap();

        // IAPWS-IF97 reference: 20°C→998.2, 50°C→988.0, 80°C→971.8 kg/m³ (AC #2: within 0.1%)
        assert!((rho_20 - 998.2).abs() / 998.2 < 0.001, "rho_20={}", rho_20);
        assert!((rho_50 - 988.0).abs() / 988.0 < 0.001, "rho_50={}", rho_50);
        assert!((rho_80 - 971.8).abs() / 971.8 < 0.001, "rho_80={}", rho_80);
    }

    #[test]
    fn test_water_cp_accuracy() {
        let backend = IncompressibleBackend::new();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let cp = backend
            .property(FluidId::new("Water"), Property::Cp, state)
            .unwrap();
        // IAPWS: Cp ≈ 4182 J/(kg·K) at 20°C (AC #2: within 0.1%)
        assert!((cp - 4182.0).abs() / 4182.0 < 0.001, "Cp={}", cp);
    }

    #[test]
    fn test_water_out_of_range() {
        let backend = IncompressibleBackend::new();
        let state_cold =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(-10.0));
        let state_hot =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(150.0));

        assert!(backend
            .property(FluidId::new("Water"), Property::Density, state_cold)
            .is_err());
        assert!(backend
            .property(FluidId::new("Water"), Property::Density, state_hot)
            .is_err());
    }

    #[test]
    fn test_critical_point_returns_error() {
        let backend = IncompressibleBackend::new();
        let result = backend.critical_point(FluidId::new("Water"));
        assert!(matches!(result, Err(FluidError::NoCriticalPoint { .. })));
    }

    #[test]
    fn test_critical_point_unknown_fluid() {
        let backend = IncompressibleBackend::new();
        let result = backend.critical_point(FluidId::new("R134a"));
        assert!(matches!(result, Err(FluidError::UnknownFluid { .. })));
    }

    #[test]
    fn test_water_enthalpy_reference() {
        let backend = IncompressibleBackend::new();
        let state_0 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(0.0));
        let state_20 =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let h_0 = backend
            .property(FluidId::new("Water"), Property::Enthalpy, state_0)
            .unwrap();
        let h_20 = backend
            .property(FluidId::new("Water"), Property::Enthalpy, state_20)
            .unwrap();
        // h = Cp * (T - 273.15) relative to 0°C: h_0 ≈ 0, h_20 ≈ 4184 * 20 = 83680 J/kg
        assert!(h_0.abs() < 1.0, "h at 0°C should be ~0");
        assert!(
            (h_20 - 83680.0).abs() / 83680.0 < 0.01,
            "h at 20°C={}",
            h_20
        );
    }

    #[test]
    fn test_glycol_concentration_effect() {
        let backend = IncompressibleBackend::new();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let rho_water = backend
            .property(FluidId::new("Water"), Property::Density, state.clone())
            .unwrap();
        let rho_eg30 = backend
            .property(
                FluidId::new("EthyleneGlycol30"),
                Property::Density,
                state.clone(),
            )
            .unwrap();
        let rho_eg50 = backend
            .property(
                FluidId::new("EthyleneGlycol50"),
                Property::Density,
                state.clone(),
            )
            .unwrap();
        let cp_eg30 = backend
            .property(
                FluidId::new("EthyleneGlycol30"),
                Property::Cp,
                state.clone(),
            )
            .unwrap();
        let cp_eg50 = backend
            .property(
                FluidId::new("EthyleneGlycol50"),
                Property::Cp,
                state.clone(),
            )
            .unwrap();
        // Higher concentration → higher density, lower Cp (ASHRAE)
        assert!(rho_eg30 > rho_water && rho_eg50 > rho_eg30);
        assert!(cp_eg50 < cp_eg30 && cp_eg30 < 4184.0);
    }

    #[test]
    fn test_glycol_out_of_range() {
        let backend = IncompressibleBackend::new();
        let state_cold =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(-40.0));
        let state_hot =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(150.0));
        assert!(backend
            .property(
                FluidId::new("EthyleneGlycol30"),
                Property::Density,
                state_cold
            )
            .is_err());
        assert!(backend
            .property(
                FluidId::new("EthyleneGlycol30"),
                Property::Density,
                state_hot
            )
            .is_err());
    }

    #[test]
    fn test_humid_air_psychrometrics() {
        let backend = IncompressibleBackend::new();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let cp = backend
            .property(FluidId::new("HumidAir"), Property::Cp, state.clone())
            .unwrap();
        let rho = backend
            .property(FluidId::new("HumidAir"), Property::Density, state)
            .unwrap();
        // Dry air Cp ≈ 1005 J/(kg·K), ρ ≈ 1.2 kg/m³ at 20°C, 1 atm
        assert!((cp - 1005.0).abs() < 1.0, "Cp={}", cp);
        assert!((rho - 1.2).abs() < 0.2, "ρ={}", rho);
    }

    #[test]
    fn test_phase_humid_air_is_vapor() {
        let backend = IncompressibleBackend::new();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let phase = backend.phase(FluidId::new("HumidAir"), state).unwrap();
        assert_eq!(phase, Phase::Vapor);
    }

    #[test]
    fn test_nan_temperature_rejected() {
        let backend = IncompressibleBackend::new();
        let state =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_kelvin(f64::NAN));
        assert!(backend
            .property(FluidId::new("Water"), Property::Density, state)
            .is_err());
    }

    #[test]
    fn test_glycol_properties() {
        let backend = IncompressibleBackend::new();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));

        let rho_eg30 = backend
            .property(
                FluidId::new("EthyleneGlycol30"),
                Property::Density,
                state.clone(),
            )
            .unwrap();
        let rho_water = backend
            .property(FluidId::new("Water"), Property::Density, state.clone())
            .unwrap();

        // EG 30% should be denser than water
        assert!(
            rho_eg30 > rho_water,
            "EG30 ρ={} should be > water ρ={}",
            rho_eg30,
            rho_water
        );
    }

    #[test]
    fn test_cached_backend_wrapper() {
        use crate::cached_backend::CachedBackend;

        let inner = IncompressibleBackend::new();
        let backend = CachedBackend::new(inner);

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

        let rho = backend
            .property(FluidId::new("Water"), Property::Density, state)
            .unwrap();
        assert!((rho - 997.0).abs() < 5.0);
    }
}
