//! Tabular interpolation backend for fluid properties.
//!
//! Provides 100x faster property lookups via pre-computed tables
//! with bilinear interpolation. Results deviate < 0.01% from NIST REFPROP.

use crate::backend::FluidBackend;
use crate::damped_backend::DampedBackend;
use crate::errors::{FluidError, FluidResult};
use crate::tabular::FluidTable;
#[allow(unused_imports)]
use crate::types::Entropy;
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
use std::collections::HashMap;
use std::path::Path;

/// Tabular backend using pre-computed property tables.
///
/// Loads fluid tables from JSON files and performs bilinear interpolation
/// for fast property lookups. No heap allocation in the property() hot path.
pub struct TabularBackend {
    /// Pre-loaded tables: fluid name -> table (no allocation during queries)
    tables: HashMap<String, FluidTable>,
    /// Ordered list of fluid IDs for list_fluids()
    fluid_ids: Vec<String>,
}

impl TabularBackend {
    /// Create an empty TabularBackend.
    pub fn new() -> Self {
        TabularBackend {
            tables: HashMap::new(),
            fluid_ids: Vec::new(),
        }
    }

    /// Creates a new TabularBackend with critical point damping enabled.
    ///
    /// This wraps the backend with a `DampedBackend` to apply C1-continuous
    /// damping to derivative properties (Cp, Cv, etc.) near the critical point.
    pub fn with_damping() -> DampedBackend<TabularBackend> {
        DampedBackend::new(Self::new())
    }

    /// Load a fluid table from a JSON file and register it.
    pub fn load_table(&mut self, path: &Path) -> FluidResult<()> {
        let table = FluidTable::load_from_path(path)?;
        let id = table.fluid_id.clone();
        if !self.fluid_ids.contains(&id) {
            self.fluid_ids.push(id.clone());
        }
        self.tables.insert(id, table);
        Ok(())
    }

    /// Load a fluid table from a JSON string (for embedded/test data).
    pub fn load_table_from_str(&mut self, json: &str) -> FluidResult<()> {
        let table = FluidTable::load_from_str(json)?;
        let id = table.fluid_id.clone();
        if !self.fluid_ids.contains(&id) {
            self.fluid_ids.push(id.clone());
        }
        self.tables.insert(id, table);
        Ok(())
    }

    /// Get a reference to a fluid table. Returns None if not loaded.
    #[inline]
    fn get_table(&self, fluid: &FluidId) -> Option<&FluidTable> {
        self.tables.get(&fluid.0)
    }

    /// Resolve FluidState to (p, t) in Pascals and Kelvin.
    /// For (P,x) uses saturation temperature at P.
    fn resolve_state(&self, fluid: &FluidId, state: FluidState) -> FluidResult<(f64, f64)> {
        match state {
            FluidState::PressureTemperature(p, t) => Ok((p.to_pascals(), t.to_kelvin())),
            FluidState::PressureEnthalpy(p, h) => {
                let table = self.get_table(fluid).ok_or(FluidError::UnknownFluid {
                    fluid: fluid.0.clone(),
                })?;
                self.find_t_from_ph(table, p.to_pascals(), h.to_joules_per_kg())
            }
            FluidState::PressureQuality(p, _x) => {
                let table = self.get_table(fluid).ok_or(FluidError::UnknownFluid {
                    fluid: fluid.0.clone(),
                })?;
                if let Some(ref sat) = table.saturation {
                    let (t_sat, _, _, _, _, _, _) =
                        sat.at_pressure(p.to_pascals())
                            .ok_or(FluidError::OutOfBounds {
                                fluid: fluid.0.clone(),
                                p: p.to_pascals(),
                                t: 0.0,
                            })?;
                    Ok((p.to_pascals(), t_sat))
                } else {
                    Err(FluidError::InvalidState {
                        reason: "Two-phase (P,x) requires saturation table".to_string(),
                    })
                }
            }
            FluidState::PressureEntropy(_, _) => Err(FluidError::InvalidState {
                reason: "TabularBackend does not yet support (P,s) state".to_string(),
            }),
            FluidState::PressureTemperatureMixture(_, _, _)
            | FluidState::PressureEnthalpyMixture(_, _, _)
            | FluidState::PressureQualityMixture(_, _, _) => {
                // TabularBackend does not support mixtures - fallback to error
                Err(FluidError::MixtureNotSupported(
                    "TabularBackend does not support mixtures. Use CoolPropBackend.".to_string(),
                ))
            }
        }
    }

    /// Find T given (P, h) using Newton iteration on the enthalpy table.
    fn find_t_from_ph(&self, table: &FluidTable, p: f64, h_target: f64) -> FluidResult<(f64, f64)> {
        // Initial guess: use midpoint of T range
        let t_grid = &table.single_phase.temperature;
        if t_grid.len() < 2 {
            return Err(FluidError::InvalidState {
                reason: "Table too small for (P,h) lookup".to_string(),
            });
        }

        let mut t = (t_grid[0] + t_grid[t_grid.len() - 1]) / 2.0;
        let dt_fd = 0.1; // K, for finite difference

        for _ in 0..20 {
            let h = table
                .single_phase
                .interpolate("enthalpy", p, t, &table.fluid_id)?;
            let err = h - h_target;

            if err.abs() < 1.0 {
                return Ok((p, t));
            }

            let h_plus = table
                .single_phase
                .interpolate("enthalpy", p, t + dt_fd, &table.fluid_id)
                .unwrap_or(h);
            let dh_dt = (h_plus - h) / dt_fd;

            if dh_dt.abs() < 1e-10 {
                return Err(FluidError::NumericalError(
                    "Zero dh/dT in (P,h) Newton iteration".to_string(),
                ));
            }

            t -= err / dh_dt;

            if t < t_grid[0] || t > t_grid[t_grid.len() - 1] {
                return Err(FluidError::OutOfBounds {
                    fluid: table.fluid_id.clone(),
                    p,
                    t,
                });
            }
        }

        Err(FluidError::NumericalError(
            "Newton iteration did not converge for (P,h)".to_string(),
        ))
    }

    /// Get property for two-phase (P, x) via linear blend.
    fn property_two_phase(
        &self,
        table: &FluidTable,
        p: f64,
        x: f64,
        property: Property,
    ) -> FluidResult<f64> {
        let sat = table.saturation.as_ref().ok_or(FluidError::InvalidState {
            reason: "Two-phase requires saturation table".to_string(),
        })?;

        let (t_sat, h_liq, h_vap, rho_liq, rho_vap, s_liq, s_vap) =
            sat.at_pressure(p).ok_or(FluidError::OutOfBounds {
                fluid: table.fluid_id.clone(),
                p,
                t: 0.0,
            })?;

        let val = match property {
            Property::Enthalpy => h_liq * (1.0 - x) + h_vap * x,
            Property::Density => {
                let v_liq = 1.0 / rho_liq;
                let v_vap = 1.0 / rho_vap;
                let v = v_liq * (1.0 - x) + v_vap * x;
                1.0 / v
            }
            Property::Entropy => s_liq * (1.0 - x) + s_vap * x,
            Property::Quality => x,
            Property::Temperature => t_sat,
            Property::Pressure => p,
            _ => {
                return Err(FluidError::UnsupportedProperty {
                    property: property.to_string(),
                })
            }
        };

        Ok(val)
    }

    /// Map Property enum to table property name.
    fn property_table_name(property: Property) -> Option<&'static str> {
        match property {
            Property::Density => Some("density"),
            Property::Enthalpy => Some("enthalpy"),
            Property::Entropy => Some("entropy"),
            Property::Cp => Some("cp"),
            Property::Cv => Some("cv"),
            Property::Temperature => Some("temperature"),
            Property::Pressure => Some("pressure"),
            _ => None,
        }
    }
}

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

impl FluidBackend for TabularBackend {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        let table = self.get_table(&fluid).ok_or(FluidError::UnknownFluid {
            fluid: fluid.0.clone(),
        })?;

        // Handle (P, x) two-phase explicitly
        if let FluidState::PressureQuality(p, x) = state {
            return self.property_two_phase(table, p.to_pascals(), x.value(), property);
        }

        let (p, t) = self.resolve_state(&fluid, state)?;

        // Temperature and Pressure are direct
        if property == Property::Temperature {
            return Ok(t);
        }
        if property == Property::Pressure {
            return Ok(p);
        }

        let name = Self::property_table_name(property).ok_or(FluidError::UnsupportedProperty {
            property: property.to_string(),
        })?;

        table.single_phase.interpolate(name, p, t, &fluid.0)
    }

    fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
        let table = self.get_table(&fluid).ok_or(FluidError::NoCriticalPoint {
            fluid: fluid.0.clone(),
        })?;

        let cp = &table.critical_point;
        Ok(CriticalPoint::new(cp.temperature, cp.pressure, cp.density))
    }

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

    fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
        let table = self.get_table(&fluid).ok_or(FluidError::UnknownFluid {
            fluid: fluid.0.clone(),
        })?;

        let (p, t) = self.resolve_state(&fluid, state.clone())?;
        let pc = table.critical_point.pressure.to_pascals();
        let tc = table.critical_point.temperature.to_kelvin();

        if p > pc && t > tc {
            return Ok(Phase::Supercritical);
        }

        if let Some(ref sat) = table.saturation {
            if let Some((_, h_liq, h_vap, _, _, _, _)) = sat.at_pressure(p) {
                if let FluidState::PressureEnthalpy(_, h) = state {
                    let hv = h.to_joules_per_kg();
                    if hv <= h_liq {
                        return Ok(Phase::Liquid);
                    }
                    if hv >= h_vap {
                        return Ok(Phase::Vapor);
                    }
                    return Ok(Phase::TwoPhase);
                }
                if let FluidState::PressureQuality(_, x) = state {
                    if x.value() <= 0.0 {
                        return Ok(Phase::Liquid);
                    }
                    if x.value() >= 1.0 {
                        return Ok(Phase::Vapor);
                    }
                    return Ok(Phase::TwoPhase);
                }
            }
        }

        Ok(Phase::Unknown)
    }

    fn list_fluids(&self) -> Vec<FluidId> {
        self.fluid_ids
            .iter()
            .map(|s| FluidId::new(s.clone()))
            .collect()
    }

    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 TabularBackend: Temperature is {:.2} K", t_k),
        })
    }
}

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

    fn make_test_backend() -> TabularBackend {
        let mut backend = TabularBackend::new();
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/r134a.json");
        backend.load_table(&path).unwrap();
        backend
    }

    #[test]
    fn test_tabular_load_r134a() {
        let backend = make_test_backend();
        assert!(backend.is_fluid_available(&FluidId::new("R134a")));
        assert!(!backend.is_fluid_available(&FluidId::new("R999")));
    }

    #[test]
    fn test_tabular_property_pt() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));

        let density = backend
            .property(FluidId::new("R134a"), Property::Density, state.clone())
            .unwrap();
        assert!(density > 1.0 && density < 100.0);

        let enthalpy = backend
            .property(FluidId::new("R134a"), Property::Enthalpy, state)
            .unwrap();
        assert!(enthalpy > 300_000.0 && enthalpy < 500_000.0);
    }

    /// Accuracy: at grid point (200 kPa, 290 K), density must match table exactly.
    /// Value is the CoolProp-generated grid entry in data/r134a.json.
    #[test]
    fn test_tabular_accuracy_at_grid_point() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(
            Pressure::from_pascals(200_000.0),
            Temperature::from_kelvin(290.0),
        );
        let density = backend
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert_relative_eq!(density, 8.870318857079596, epsilon = 1e-9);
    }

    /// Accuracy: interpolated value within 1% (table self-consistency check).
    #[test]
    fn test_tabular_accuracy_interpolated() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(
            Pressure::from_pascals(200_000.0),
            Temperature::from_kelvin(300.0),
        );
        let density = backend
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert_relative_eq!(density, 8.53, epsilon = 0.01);
    }

    #[test]
    fn test_tabular_critical_point() {
        let backend = make_test_backend();
        let cp = backend.critical_point(FluidId::new("R134a")).unwrap();
        assert!((cp.temperature_kelvin() - 374.21).abs() < 1.0);
        assert!((cp.pressure_pascals() - 4.059e6).abs() < 1e4);
    }

    #[test]
    fn test_tabular_list_fluids() {
        let backend = make_test_backend();
        let fluids = backend.list_fluids();
        assert_eq!(fluids.len(), 1);
        assert_eq!(fluids[0].0, "R134a");
    }

    #[test]
    fn test_tabular_unknown_fluid() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let result = backend.property(FluidId::new("R999"), Property::Density, state);
        assert!(result.is_err());
    }

    #[test]
    fn test_tabular_out_of_bounds() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(
            Pressure::from_pascals(50_000.0),
            Temperature::from_kelvin(200.0),
        );
        let result = backend.property(FluidId::new("R134a"), Property::Density, state);
        assert!(result.is_err());
    }

    #[test]
    fn test_tabular_ph_state() {
        let backend = make_test_backend();
        let state = FluidState::from_ph(
            Pressure::from_bar(1.0),
            entropyk_core::Enthalpy::from_kilojoules_per_kg(415.0),
        );
        let density = backend
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert!(density > 1.0);
    }

    #[test]
    fn test_tabular_px_state() {
        let backend = make_test_backend();
        let state = FluidState::from_px(
            Pressure::from_pascals(500_000.0),
            crate::types::Quality::new(0.5),
        );
        let enthalpy = backend
            .property(FluidId::new("R134a"), Property::Enthalpy, state)
            .unwrap();
        assert!(enthalpy > 300_000.0 && enthalpy < 450_000.0);
    }

    #[test]
    fn test_tabular_benchmark_10k_queries() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let start = std::time::Instant::now();
        for _ in 0..10_000 {
            let _ = backend.property(FluidId::new("R134a"), Property::Density, state.clone());
        }
        let elapsed = start.elapsed();
        assert!(
            elapsed.as_millis() < 100,
            "10k queries took {}ms, expected < 100ms (debug mode)",
            elapsed.as_millis()
        );
    }

    /// Release build: 10k queries must complete in < 10ms (AC #3).
    #[test]
    #[cfg_attr(debug_assertions, ignore = "run with cargo test --release")]
    fn test_tabular_benchmark_10k_queries_release() {
        let backend = make_test_backend();
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let start = std::time::Instant::now();
        for _ in 0..10_000 {
            let _ = backend.property(FluidId::new("R134a"), Property::Density, state.clone());
        }
        let elapsed = start.elapsed();
        assert!(
            elapsed.as_millis() < 10,
            "10k queries took {}ms, expected < 10ms in release",
            elapsed.as_millis()
        );
    }

    /// Compare TabularBackend vs CoolPropBackend. Embedded r134a.json may be from
    /// reference data; use epsilon 1% for compatibility. CoolProp-generated tables
    /// achieve < 0.01% (validated in generator::test_generated_table_vs_coolprop_spot_checks).
    #[test]
    #[cfg(feature = "coolprop")]
    fn test_tabular_vs_coolprop_accuracy() {
        use crate::coolprop::CoolPropBackend;
        use crate::types::Quality;

        let mut tabular = TabularBackend::new();
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/r134a.json");
        tabular.load_table(&path).unwrap();

        let coolprop = CoolPropBackend::new();
        let fluid = FluidId::new("R134a");

        // (P, T) at 1 bar, 25°C
        let state_pt =
            FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let rho_t = tabular
            .property(fluid.clone(), Property::Density, state_pt.clone())
            .unwrap();
        let rho_c = coolprop
            .property(fluid.clone(), Property::Density, state_pt.clone())
            .unwrap();
        assert_relative_eq!(rho_t, rho_c, epsilon = 0.01 * rho_c.max(1.0));

        let h_t = tabular
            .property(fluid.clone(), Property::Enthalpy, state_pt.clone())
            .unwrap();
        let h_c = coolprop
            .property(fluid.clone(), Property::Enthalpy, state_pt)
            .unwrap();
        assert_relative_eq!(h_t, h_c, epsilon = 0.01 * h_c.max(1.0));

        // (P, h) at 1 bar, h ≈ 415 kJ/kg
        let state_ph = FluidState::from_ph(
            Pressure::from_bar(1.0),
            entropyk_core::Enthalpy::from_kilojoules_per_kg(415.0),
        );
        let rho_t_ph = tabular
            .property(fluid.clone(), Property::Density, state_ph.clone())
            .unwrap();
        let rho_c_ph = coolprop
            .property(fluid.clone(), Property::Density, state_ph)
            .unwrap();
        assert_relative_eq!(rho_t_ph, rho_c_ph, epsilon = 0.01 * rho_c_ph.max(1.0));

        // (P, x) at 500 kPa, x = 0.5
        let state_px = FluidState::from_px(Pressure::from_pascals(500_000.0), Quality::new(0.5));
        let h_t_px = tabular
            .property(fluid.clone(), Property::Enthalpy, state_px.clone())
            .unwrap();
        let h_c_px = coolprop
            .property(fluid.clone(), Property::Enthalpy, state_px)
            .unwrap();
        assert_relative_eq!(h_t_px, h_c_px, epsilon = 0.01 * h_c_px.max(1.0));
    }
}
