//! Fluid property table structure and loading.
//!
//! Defines the JSON format for tabular fluid data and provides loading logic.

use crate::errors::{FluidError, FluidResult};
use entropyk_core::{Pressure, Temperature};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

use super::interpolate::bicubic_interpolate;

/// Critical point data stored in table metadata.
#[derive(Debug, Clone)]
pub struct TableCriticalPoint {
    /// Critical temperature (K)
    pub temperature: Temperature,
    /// Critical pressure (Pa)
    pub pressure: Pressure,
    /// Critical density (kg/m³)
    pub density: f64,
}

/// Single-phase property table (P, T) grid.
#[derive(Debug, Clone)]
pub struct SinglePhaseTable {
    /// Pressure grid (Pa), ascending
    pub pressure: Vec<f64>,
    /// Temperature grid (K), ascending
    pub temperature: Vec<f64>,
    /// Property grids: density, enthalpy, entropy, cp, cv, etc.
    /// Key: property name, Value: row-major 2D data [p_idx * n_t + t_idx]
    pub properties: HashMap<String, Vec<f64>>,
}

impl SinglePhaseTable {
    /// Interpolate a property at (p, t). Returns error if out of bounds.
    #[inline]
    pub fn interpolate(
        &self,
        property_name: &str,
        p: f64,
        t: f64,
        fluid_name: &str,
    ) -> FluidResult<f64> {
        let values = self
            .properties
            .get(property_name)
            .ok_or(FluidError::UnsupportedProperty {
                property: property_name.to_string(),
            })?;

        bicubic_interpolate(&self.pressure, &self.temperature, values, p, t).ok_or(
            FluidError::OutOfBounds {
                fluid: fluid_name.to_string(),
                p,
                t,
            },
        )
    }

    /// Check if (p, t) is within table bounds.
    #[inline]
    pub fn in_bounds(&self, p: f64, t: f64) -> bool {
        if self.pressure.is_empty() || self.temperature.is_empty() {
            return false;
        }
        let p_min = self.pressure[0];
        let p_max = self.pressure[self.pressure.len() - 1];
        let t_min = self.temperature[0];
        let t_max = self.temperature[self.temperature.len() - 1];
        p >= p_min && p <= p_max && t >= t_min && t <= t_max
    }
}

/// Saturation line data for two-phase (P, x) lookups.
#[derive(Debug, Clone)]
pub struct SaturationTable {
    /// Temperature (K) - independent variable
    pub temperature: Vec<f64>,
    /// Saturation pressure (Pa)
    pub pressure: Vec<f64>,
    /// Saturated liquid enthalpy (J/kg)
    pub h_liq: Vec<f64>,
    /// Saturated vapor enthalpy (J/kg)
    pub h_vap: Vec<f64>,
    /// Saturated liquid density (kg/m³)
    pub rho_liq: Vec<f64>,
    /// Saturated vapor density (kg/m³)
    pub rho_vap: Vec<f64>,
    /// Saturated liquid entropy (J/(kg·K))
    pub s_liq: Vec<f64>,
    /// Saturated vapor entropy (J/(kg·K))
    pub s_vap: Vec<f64>,
}

impl SaturationTable {
    /// Find saturation properties at pressure P via 1D interpolation on P_sat(T).
    /// Returns (T_sat, h_liq, h_vap, rho_liq, rho_vap, s_liq, s_vap).
    pub fn at_pressure(&self, p: f64) -> Option<(f64, f64, f64, f64, f64, f64, f64)> {
        if self.pressure.is_empty() || self.pressure.len() != self.temperature.len() {
            return None;
        }

        // Find T such that P_sat(T) = p (pressure is monotonic with T)
        let n = self.pressure.len();
        if p < self.pressure[0] || p > self.pressure[n - 1] {
            return None;
        }

        let idx = self.pressure.iter().position(|&x| x >= p).unwrap_or(n - 1);

        let i = if idx == 0 { 0 } else { idx - 1 };
        let j = (i + 1).min(n - 1);

        let p0 = self.pressure[i];
        let p1 = self.pressure[j];
        let frac = if (p1 - p0).abs() < 1e-15 {
            0.0
        } else {
            ((p - p0) / (p1 - p0)).clamp(0.0, 1.0)
        };

        let t_sat = self.temperature[i] * (1.0 - frac) + self.temperature[j] * frac;
        let h_liq = self.h_liq[i] * (1.0 - frac) + self.h_liq[j] * frac;
        let h_vap = self.h_vap[i] * (1.0 - frac) + self.h_vap[j] * frac;
        let rho_liq = self.rho_liq[i] * (1.0 - frac) + self.rho_liq[j] * frac;
        let rho_vap = self.rho_vap[i] * (1.0 - frac) + self.rho_vap[j] * frac;
        let s_liq = self.s_liq[i] * (1.0 - frac) + self.s_liq[j] * frac;
        let s_vap = self.s_vap[i] * (1.0 - frac) + self.s_vap[j] * frac;

        Some((t_sat, h_liq, h_vap, rho_liq, rho_vap, s_liq, s_vap))
    }
}

/// Complete fluid table with single-phase and saturation data.
#[derive(Debug, Clone)]
pub struct FluidTable {
    /// Fluid identifier
    pub fluid_id: String,
    /// Critical point
    pub critical_point: TableCriticalPoint,
    /// Single-phase (P, T) table
    pub single_phase: SinglePhaseTable,
    /// Saturation table (optional - for two-phase support)
    pub saturation: Option<SaturationTable>,
}

/// JSON deserialization structures (internal format).
#[derive(Debug, Deserialize)]
struct JsonCriticalPoint {
    tc: f64,
    pc: f64,
    rho_c: f64,
}

#[derive(Debug, Deserialize)]
struct JsonSinglePhase {
    pressure: Vec<f64>,
    temperature: Vec<f64>,
    density: Vec<f64>,
    enthalpy: Vec<f64>,
    entropy: Vec<f64>,
    cp: Vec<f64>,
    cv: Vec<f64>,
}

#[derive(Debug, Deserialize)]
struct JsonSaturation {
    temperature: Vec<f64>,
    pressure: Vec<f64>,
    h_liq: Vec<f64>,
    h_vap: Vec<f64>,
    rho_liq: Vec<f64>,
    rho_vap: Vec<f64>,
    s_liq: Vec<f64>,
    s_vap: Vec<f64>,
}

#[derive(Debug, Deserialize)]
struct JsonFluidTable {
    fluid: String,
    critical_point: JsonCriticalPoint,
    single_phase: JsonSinglePhase,
    saturation: Option<JsonSaturation>,
}

impl FluidTable {
    /// Load a fluid table from a JSON file.
    pub fn load_from_path(path: &Path) -> FluidResult<Self> {
        let contents = std::fs::read_to_string(path).map_err(|e| FluidError::TableNotFound {
            path: format!("{}: {}", path.display(), e),
        })?;

        let json: JsonFluidTable =
            serde_json::from_str(&contents).map_err(|e| FluidError::InvalidState {
                reason: format!("Invalid table JSON: {}", e),
            })?;

        Self::from_json(json)
    }

    /// Load from JSON string (for embedded tables in tests).
    pub fn load_from_str(s: &str) -> FluidResult<Self> {
        let json: JsonFluidTable =
            serde_json::from_str(s).map_err(|e| FluidError::InvalidState {
                reason: format!("Invalid table JSON: {}", e),
            })?;
        Self::from_json(json)
    }

    fn from_json(json: JsonFluidTable) -> FluidResult<Self> {
        let n_p = json.single_phase.pressure.len();
        let n_t = json.single_phase.temperature.len();
        let expected = n_p * n_t;

        if json.single_phase.density.len() != expected
            || json.single_phase.enthalpy.len() != expected
            || json.single_phase.entropy.len() != expected
            || json.single_phase.cp.len() != expected
            || json.single_phase.cv.len() != expected
        {
            return Err(FluidError::InvalidState {
                reason: "Table grid dimensions do not match property arrays".to_string(),
            });
        }

        let mut properties = HashMap::new();
        properties.insert("density".to_string(), json.single_phase.density);
        properties.insert("enthalpy".to_string(), json.single_phase.enthalpy);
        properties.insert("entropy".to_string(), json.single_phase.entropy);
        properties.insert("cp".to_string(), json.single_phase.cp);
        properties.insert("cv".to_string(), json.single_phase.cv);

        let single_phase = SinglePhaseTable {
            pressure: json.single_phase.pressure,
            temperature: json.single_phase.temperature,
            properties,
        };

        let saturation = json
            .saturation
            .map(|s| {
                let n = s.temperature.len();
                if s.pressure.len() != n
                    || s.h_liq.len() != n
                    || s.h_vap.len() != n
                    || s.rho_liq.len() != n
                    || s.rho_vap.len() != n
                    || s.s_liq.len() != n
                    || s.s_vap.len() != n
                {
                    return Err(FluidError::InvalidState {
                        reason: format!(
                            "Saturation table array length mismatch: expected {} elements",
                            n
                        ),
                    });
                }
                Ok(SaturationTable {
                    temperature: s.temperature,
                    pressure: s.pressure,
                    h_liq: s.h_liq,
                    h_vap: s.h_vap,
                    rho_liq: s.rho_liq,
                    rho_vap: s.rho_vap,
                    s_liq: s.s_liq,
                    s_vap: s.s_vap,
                })
            })
            .transpose()?;

        let critical_point = TableCriticalPoint {
            temperature: Temperature::from_kelvin(json.critical_point.tc),
            pressure: Pressure::from_pascals(json.critical_point.pc),
            density: json.critical_point.rho_c,
        };

        Ok(FluidTable {
            fluid_id: json.fluid,
            critical_point,
            single_phase,
            saturation,
        })
    }
}
