//! Runtime-loaded shared library backend for fluid properties.
//!
//! This module provides a `DllBackend` that loads a CoolProp-compatible shared
//! library (`.dll`, `.so`, `.dylib`) at **runtime** via `libloading`.
//!
//! Unlike `CoolPropBackend` (which requires compile-time C++ linking), this
//! backend has **zero native build dependencies** — the user just needs to
//! place the pre-built shared library in a known location.
//!
//! # Supported Libraries
//!
//! Any shared library that exports the standard CoolProp C API:
//! - `PropsSI(Output, Name1, Value1, Name2, Value2, FluidName) -> f64`
//! - `Props1SI(FluidName, Output) -> f64`
//!
//! This includes:
//! - CoolProp shared library (`libCoolProp.so`, `CoolProp.dll`, `libCoolProp.dylib`)
//! - REFPROP via CoolProp wrapper DLL
//! - Any custom wrapper exposing the same C ABI
//!
//! # Example
//!
//! ```rust,no_run
//! use entropyk_fluids::DllBackend;
//! use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
//! use entropyk_core::{Pressure, Temperature};
//!
//! // Load from explicit path
//! let backend = DllBackend::load("/usr/local/lib/libCoolProp.so").unwrap();
//!
//! // Or search system paths
//! let backend = DllBackend::load_system_default().unwrap();
//!
//! let density = backend.property(
//!     FluidId::new("R134a"),
//!     Property::Density,
//!     FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0)),
//! ).unwrap();
//! ```

use std::ffi::CString;
use std::path::Path;

use libloading::{Library, Symbol};

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

/// Type alias for the CoolProp `PropsSI` C function signature.
///
/// ```c
/// double PropsSI(const char* Output, const char* Name1, double Value1,
///                const char* Name2, double Value2, const char* FluidName);
/// ```
type PropsSiFn = unsafe extern "C" fn(
    *const std::os::raw::c_char, // Output
    *const std::os::raw::c_char, // Name1
    f64,                         // Value1
    *const std::os::raw::c_char, // Name2
    f64,                         // Value2
    *const std::os::raw::c_char, // FluidName
) -> f64;

/// Type alias for the CoolProp `Props1SI` C function signature.
///
/// ```c
/// double Props1SI(const char* FluidName, const char* Output);
/// ```
type Props1SiFn = unsafe extern "C" fn(
    *const std::os::raw::c_char, // FluidName
    *const std::os::raw::c_char, // Output
) -> f64;

/// A fluid property backend that loads a CoolProp-compatible shared library at runtime.
///
/// This avoids compile-time C++ dependencies entirely. The user provides the
/// path to a pre-built `.dll`/`.so`/`.dylib` and this backend loads the
/// `PropsSI` and `Props1SI` symbols dynamically.
pub struct DllBackend {
    /// The loaded shared library handle. Kept alive for the lifetime of the backend.
    _lib: Library,
    /// Function pointer to `PropsSI`.
    props_si: PropsSiFn,
    /// Function pointer to `Props1SI`.
    props1_si: Props1SiFn,
}

// SAFETY: The loaded library functions are thread-safe (CoolProp is reentrant
// for property queries). The Library handle must remain alive.
unsafe impl Send for DllBackend {}
unsafe impl Sync for DllBackend {}

impl DllBackend {
    /// Load a CoolProp-compatible shared library from the given path.
    ///
    /// The library must export `PropsSI` and `Props1SI` with the standard
    /// CoolProp C ABI.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the shared library file
    ///
    /// # Errors
    ///
    /// Returns `FluidError::DllLoadError` if the library cannot be opened
    /// or the required symbols are not found.
    pub fn load<P: AsRef<Path>>(path: P) -> FluidResult<Self> {
        let path = path.as_ref();

        // SAFETY: Loading a shared library is inherently unsafe — the library
        // must be a valid CoolProp-compatible binary for the current platform.
        let lib = unsafe { Library::new(path) }.map_err(|e| {
            FluidError::CoolPropError(format!(
                "Failed to load shared library '{}': {}",
                path.display(),
                e
            ))
        })?;

        // Load PropsSI symbol
        let props_si: PropsSiFn = unsafe {
            let sym: Symbol<PropsSiFn> = lib.get(b"PropsSI\0").map_err(|e| {
                FluidError::CoolPropError(format!(
                    "Symbol 'PropsSI' not found in '{}': {}. \
                     Make sure this is a CoolProp shared library built with C exports.",
                    path.display(),
                    e
                ))
            })?;
            *sym
        };

        // Load Props1SI symbol
        let props1_si: Props1SiFn = unsafe {
            let sym: Symbol<Props1SiFn> = lib.get(b"Props1SI\0").map_err(|e| {
                FluidError::CoolPropError(format!(
                    "Symbol 'Props1SI' not found in '{}': {}",
                    path.display(),
                    e
                ))
            })?;
            *sym
        };

        Ok(Self {
            _lib: lib,
            props_si,
            props1_si,
        })
    }

    /// Search common system paths for a CoolProp shared library and load it.
    ///
    /// Search order:
    /// 1. `COOLPROP_LIB` environment variable (explicit override)
    /// 2. Current directory
    /// 3. System library paths (`/usr/local/lib`, etc.)
    ///
    /// # Errors
    ///
    /// Returns `FluidError::CoolPropError` if no CoolProp library is found.
    pub fn load_system_default() -> FluidResult<Self> {
        // 1. Check environment variable
        if let Ok(path) = std::env::var("COOLPROP_LIB") {
            if Path::new(&path).exists() {
                return Self::load(&path);
            }
        }

        // 2. Try common library names (OS-specific)
        let lib_names = if cfg!(target_os = "windows") {
            vec!["CoolProp.dll", "libCoolProp.dll"]
        } else if cfg!(target_os = "macos") {
            vec!["libCoolProp.dylib"]
        } else {
            vec!["libCoolProp.so"]
        };

        // Common search directories
        let search_dirs: Vec<&str> = if cfg!(target_os = "windows") {
            vec![".", "C:\\CoolProp", "C:\\Program Files\\CoolProp"]
        } else {
            vec![
                ".",
                "/usr/local/lib",
                "/usr/lib",
                "/opt/coolprop/lib",
                "/usr/local/lib/coolprop",
            ]
        };

        for dir in &search_dirs {
            for name in &lib_names {
                let path = Path::new(dir).join(name);
                if path.exists() {
                    return Self::load(&path);
                }
            }
        }

        Err(FluidError::CoolPropError(
            "CoolProp shared library not found. \
             Set COOLPROP_LIB environment variable to the library path, \
             or place it in a standard system library directory. \
             Download from: https://github.com/CoolProp/CoolProp/releases"
                .to_string(),
        ))
    }

    // ========================================================================
    // Internal helpers that call the loaded function pointers
    // ========================================================================

    /// Call PropsSI(Output, Name1, Value1, Name2, Value2, Fluid).
    fn call_props_si(
        &self,
        output: &str,
        name1: &str,
        value1: f64,
        name2: &str,
        value2: f64,
        fluid: &str,
    ) -> FluidResult<f64> {
        let c_output = CString::new(output).unwrap();
        let c_name1 = CString::new(name1).unwrap();
        let c_name2 = CString::new(name2).unwrap();
        let c_fluid = CString::new(fluid).unwrap();

        let result = unsafe {
            (self.props_si)(
                c_output.as_ptr(),
                c_name1.as_ptr(),
                value1,
                c_name2.as_ptr(),
                value2,
                c_fluid.as_ptr(),
            )
        };

        if result.is_nan() || result.is_infinite() {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "DllBackend: PropsSI returned invalid value for {}({}, {}={}, {}={}, {})",
                    output, fluid, name1, value1, name2, value2, fluid
                ),
            });
        }

        Ok(result)
    }

    /// Call Props1SI(Fluid, Output) for single-parameter queries (e.g., Tcrit).
    fn call_props1_si(&self, fluid: &str, output: &str) -> FluidResult<f64> {
        let c_fluid = CString::new(fluid).unwrap();
        let c_output = CString::new(output).unwrap();

        let result = unsafe { (self.props1_si)(c_fluid.as_ptr(), c_output.as_ptr()) };

        if result.is_nan() || result.is_infinite() {
            return Err(FluidError::InvalidState {
                reason: format!(
                    "DllBackend: Props1SI returned invalid value for {}({})",
                    output, fluid
                ),
            });
        }

        Ok(result)
    }

    /// Convert a `Property` enum to a CoolProp output code string.
    fn property_code(property: Property) -> &'static str {
        match property {
            Property::Density => "D",
            Property::Enthalpy => "H",
            Property::Entropy => "S",
            Property::InternalEnergy => "U",
            Property::Cp => "C",
            Property::Cv => "O",
            Property::SpeedOfSound => "A",
            Property::Viscosity => "V",
            Property::ThermalConductivity => "L",
            Property::SurfaceTension => "I",
            Property::Quality => "Q",
            Property::Temperature => "T",
            Property::Pressure => "P",
        }
    }
}

impl FluidBackend for DllBackend {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        let prop_code = Self::property_code(property);
        let fluid_name = &fluid.0;

        match state {
            FluidState::PressureTemperature(p, t) => self.call_props_si(
                prop_code,
                "P",
                p.to_pascals(),
                "T",
                t.to_kelvin(),
                fluid_name,
            ),
            FluidState::PressureEnthalpy(p, h) => self.call_props_si(
                prop_code,
                "P",
                p.to_pascals(),
                "H",
                h.to_joules_per_kg(),
                fluid_name,
            ),
            FluidState::PressureQuality(p, q) => {
                self.call_props_si(prop_code, "P", p.to_pascals(), "Q", q.value(), fluid_name)
            }
            FluidState::PressureEntropy(_p, _s) => Err(FluidError::UnsupportedProperty {
                property: "P-S state not directly supported".to_string(),
            }),
            // Mixture states: build CoolProp mixture string
            FluidState::PressureTemperatureMixture(p, t, ref mix) => {
                let cp_string = mix.to_coolprop_string();
                self.call_props_si(
                    prop_code,
                    "P",
                    p.to_pascals(),
                    "T",
                    t.to_kelvin(),
                    &cp_string,
                )
            }
            FluidState::PressureEnthalpyMixture(p, h, ref mix) => {
                let cp_string = mix.to_coolprop_string();
                self.call_props_si(
                    prop_code,
                    "P",
                    p.to_pascals(),
                    "H",
                    h.to_joules_per_kg(),
                    &cp_string,
                )
            }
            FluidState::PressureQualityMixture(p, q, ref mix) => {
                let cp_string = mix.to_coolprop_string();
                self.call_props_si(prop_code, "P", p.to_pascals(), "Q", q.value(), &cp_string)
            }
        }
    }

    fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
        let name = &fluid.0;

        let tc = self.call_props1_si(name, "Tcrit")?;
        let pc = self.call_props1_si(name, "pcrit")?;
        let dc = self.call_props1_si(name, "rhocrit")?;

        Ok(CriticalPoint::new(
            entropyk_core::Temperature::from_kelvin(tc),
            entropyk_core::Pressure::from_pascals(pc),
            dc,
        ))
    }

    fn is_fluid_available(&self, fluid: &FluidId) -> bool {
        self.call_props1_si(&fluid.0, "Tcrit").is_ok()
    }

    fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
        let quality = self.property(fluid, Property::Quality, state)?;

        if quality < 0.0 {
            Ok(Phase::Liquid)
        } else if quality > 1.0 {
            Ok(Phase::Vapor)
        } else if (quality - 0.0).abs() < 1e-6 {
            Ok(Phase::Liquid)
        } else if (quality - 1.0).abs() < 1e-6 {
            Ok(Phase::Vapor)
        } else {
            Ok(Phase::TwoPhase)
        }
    }

    fn list_fluids(&self) -> Vec<FluidId> {
        // Common refrigerants — we check availability dynamically
        let candidates = [
            "R134a",
            "R410A",
            "R32",
            "R1234yf",
            "R1234ze(E)",
            "R454B",
            "R513A",
            "R290",
            "R744",
            "R717",
            "Water",
            "Air",
            "CO2",
            "Ammonia",
            "Propane",
            "R404A",
            "R407C",
            "R22",
        ];

        candidates
            .iter()
            .copied()
            .filter(|name| self.is_fluid_available(&FluidId::new(*name)))
            .map(|name| FluidId::new(name))
            .collect()
    }

    fn full_state(
        &self,
        fluid: FluidId,
        p: entropyk_core::Pressure,
        h: entropyk_core::Enthalpy,
    ) -> FluidResult<ThermoState> {
        let name = &fluid.0;
        let p_pa = p.to_pascals();
        let h_j_kg = h.to_joules_per_kg();

        let t_k = self.call_props_si("T", "P", p_pa, "H", h_j_kg, name)?;
        let s = self.call_props_si("S", "P", p_pa, "H", h_j_kg, name)?;
        let d = self.call_props_si("D", "P", p_pa, "H", h_j_kg, name)?;
        let q = self
            .call_props_si("Q", "P", p_pa, "H", h_j_kg, name)
            .unwrap_or(f64::NAN);

        let phase = self.phase(fluid.clone(), FluidState::from_ph(p, h))?;

        let quality = if (0.0..=1.0).contains(&q) {
            Some(crate::types::Quality::new(q))
        } else {
            None
        };

        // Saturation temperatures (may fail for supercritical states)
        let t_bubble = self.call_props_si("T", "P", p_pa, "Q", 0.0, name).ok();
        let t_dew = self.call_props_si("T", "P", p_pa, "Q", 1.0, name).ok();

        let subcooling = t_bubble.and_then(|tb| {
            if t_k < tb {
                Some(crate::types::TemperatureDelta::new(tb - t_k))
            } else {
                None
            }
        });

        let superheat = t_dew.and_then(|td| {
            if t_k > td {
                Some(crate::types::TemperatureDelta::new(t_k - td))
            } else {
                None
            }
        });

        Ok(ThermoState {
            fluid,
            pressure: p,
            temperature: entropyk_core::Temperature::from_kelvin(t_k),
            enthalpy: h,
            entropy: crate::types::Entropy::from_joules_per_kg_kelvin(s),
            density: d,
            phase,
            quality,
            superheat,
            subcooling,
            t_bubble: t_bubble.map(entropyk_core::Temperature::from_kelvin),
            t_dew: t_dew.map(entropyk_core::Temperature::from_kelvin),
        })
    }
}

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

    #[test]
    fn test_load_nonexistent_library() {
        let result = DllBackend::load("/nonexistent/path/libCoolProp.so");
        assert!(result.is_err());
    }

    #[test]
    fn test_load_system_default_graceful_error() {
        // In CI/test environments, CoolProp DLL is typically not installed.
        // This should return a clean error, not panic.
        let result = DllBackend::load_system_default();
        // We don't assert is_err() because the user might have it installed;
        // we just verify it doesn't panic.
        let _ = result;
    }
}
