//! Cached backend wrapper for fluid property queries.
//!
//! Wraps any `FluidBackend` with a thread-local LRU cache to avoid
//! redundant calculations. No mutex contention; zero allocation on cache hit.

use crate::backend::FluidBackend;
use crate::cache::{cache_clear, cache_get, cache_insert};
use crate::errors::FluidResult;
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
use std::sync::atomic::{AtomicUsize, Ordering};

static NEXT_BACKEND_ID: AtomicUsize = AtomicUsize::new(0);

/// Backend wrapper that caches property queries in a thread-local LRU cache.
///
/// Wraps any `FluidBackend` and caches successful property() results.
/// Other trait methods (critical_point, phase, etc.) delegate to the inner backend
/// without caching, as they are typically called less frequently.
///
/// # Example
///
/// ```
/// use entropyk_fluids::{CachedBackend, FluidBackend, FluidId, Property, FluidState, TestBackend};
/// use entropyk_core::{Pressure, Temperature};
///
/// let inner = TestBackend::new();
/// let cached = CachedBackend::new(inner);
///
/// let state = FluidState::from_pt(
///     Pressure::from_bar(1.0),
///     Temperature::from_celsius(25.0),
/// );
///
/// let v1 = cached.property(FluidId::new("R134a"), Property::Density, state.clone()).unwrap();
/// let v2 = cached.property(FluidId::new("R134a"), Property::Density, state).unwrap();
/// assert_eq!(v1, v2); // Second call served from cache
/// ```
pub struct CachedBackend<B: FluidBackend> {
    backend_id: usize,
    inner: B,
}

impl<B: FluidBackend> CachedBackend<B> {
    /// Create a new cached backend wrapping the given backend.
    pub fn new(inner: B) -> Self {
        let backend_id = NEXT_BACKEND_ID.fetch_add(1, Ordering::Relaxed);
        CachedBackend { backend_id, inner }
    }

    /// Clear the thread-local cache. Call at solver iteration boundaries if needed.
    ///
    /// **Note:** This clears the cache for *all* `CachedBackend` instances on the current
    /// thread, since they share one thread-local cache. If you need per-backend invalidation,
    /// use separate threads or a different caching strategy.
    pub fn clear_cache(&self) {
        cache_clear();
    }

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

impl<B: FluidBackend> FluidBackend for CachedBackend<B> {
    fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
        if let Some(v) = cache_get(self.backend_id, &fluid, property, &state) {
            return Ok(v);
        }
        let v = self
            .inner
            .property(fluid.clone(), property, state.clone())?;
        cache_insert(self.backend_id, &fluid, property, &state, v);
        Ok(v)
    }

    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::test_backend::TestBackend;
    use entropyk_core::{Pressure, Temperature};

    #[test]
    fn test_cache_hit_returns_same_value() {
        let inner = TestBackend::new();
        let cached = CachedBackend::new(inner);

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

        let v1 = cached
            .property(FluidId::new("R134a"), Property::Density, state.clone())
            .unwrap();
        let v2 = cached
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert_eq!(v1, v2);
    }

    #[test]
    fn test_cache_miss_delegates_to_backend() {
        let inner = TestBackend::new();
        let cached = CachedBackend::new(inner);

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

        let v = cached
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert!(v > 0.0);
    }

    #[test]
    fn test_cache_invalidation() {
        let inner = TestBackend::new();
        let cached = CachedBackend::new(inner);

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

        let _ = cached
            .property(FluidId::new("R134a"), Property::Density, state.clone())
            .unwrap();
        cached.clear_cache();
        // After clear, next query should still work (delegates to backend)
        let v = cached
            .property(FluidId::new("R134a"), Property::Density, state)
            .unwrap();
        assert!(v > 0.0);
    }

    #[test]
    fn test_cached_benchmark_10k_queries() {
        let inner = TestBackend::new();
        let cached = CachedBackend::new(inner);

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

        for _ in 0..10_000 {
            let _ = cached
                .property(FluidId::new("R134a"), Property::Density, state.clone())
                .unwrap();
        }
    }

    #[test]
    fn test_cached_backend_implements_fluid_backend() {
        let inner = TestBackend::new();
        let cached = CachedBackend::new(inner);

        assert!(cached.is_fluid_available(&FluidId::new("R134a")));
        let cp = cached.critical_point(FluidId::new("R134a")).unwrap();
        assert!(cp.temperature_kelvin() > 300.0);
        let fluids = cached.list_fluids();
        assert!(!fluids.is_empty());
    }
}
