//! Thread-local LRU cache for fluid property queries.
//!
//! Avoids redundant backend calls without mutex contention by using
//! per-thread storage. Cache keys use quantized state values since f64
//! does not implement Hash.
//!
//! # Quantization Strategy
//!
//! State values (P, T, h, s, x) are quantized to 1e-9 relative precision
//! for cache key derivation. Solver iterations often repeat the same
//! (P,T) or (P,h) states; quantization should not lose cache hits for
//! typical thermodynamic ranges (P: 1e3–1e7 Pa, T: 200–600 K).

use crate::mixture::Mixture;
use crate::types::{FluidId, FluidState, Property};
use lru::LruCache;
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;

/// Default cache capacity (entries). LRU eviction when exceeded.
pub const DEFAULT_CACHE_CAPACITY: usize = 10_000;

const DEFAULT_CAP_NONZERO: NonZeroUsize = NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).unwrap();

/// Quantization factor: values rounded to 1e-9 relative.
/// (v * 1e9).round() as i64 for Hash-compatible key.
///
/// Overflow safety (Story 0.7, 2026-07-19 finite-poisoning incident): for
/// |v| > 9e9 the product `v * 1e9` exceeds `i64::MAX` and wraps to garbage,
/// making extreme solver trial states (blown iterates, h ~ 1e13) collide
/// with legitimate keys. Those states fall back to exact bit hashing — they
/// do not need near-state hits, they need to never collide.
#[inline]
fn quantize(v: f64) -> i64 {
    if v.is_nan() {
        #[cfg(debug_assertions)]
        eprintln!("[WARN] quantize: NaN value encountered, mapping to 0");
        0
    } else if v.is_infinite() {
        #[cfg(debug_assertions)]
        eprintln!("[WARN] quantize: Infinite value encountered, mapping to 0");
        0
    } else if v.abs() > 9.0e9 {
        // Exact, sign-masked bit pattern (no quantization) for huge values.
        (v.to_bits() & 0x7fff_ffff_ffff_ffff) as i64
    } else {
        (v * 1e9).round() as i64
    }
}

/// Cache key for fluid property lookups.
///
/// Uses quantized state values since f64 does not implement Hash.
/// Includes backend_id so multiple CachedBackend instances don't mix results.
/// For mixtures, includes a hash of the mixture composition.
#[derive(Clone, Debug)]
pub struct CacheKey {
    backend_id: usize,
    fluid: String,
    property: Property,
    variant: u8,
    p_quantized: i64,
    second_quantized: i64,
    mixture_hash: Option<u64>,
}

impl PartialEq for CacheKey {
    fn eq(&self, other: &Self) -> bool {
        self.backend_id == other.backend_id
            && self.fluid == other.fluid
            && self.property == other.property
            && self.variant == other.variant
            && self.p_quantized == other.p_quantized
            && self.second_quantized == other.second_quantized
            && self.mixture_hash == other.mixture_hash
    }
}

impl Eq for CacheKey {}

impl Hash for CacheKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.backend_id.hash(state);
        self.fluid.hash(state);
        self.property.hash(state);
        self.variant.hash(state);
        self.p_quantized.hash(state);
        self.second_quantized.hash(state);
        self.mixture_hash.hash(state);
    }
}

impl CacheKey {
    /// Build a cache key from fluid, property, state, and backend id.
    pub fn new(backend_id: usize, fluid: &FluidId, property: Property, state: &FluidState) -> Self {
        let (p, second, variant, mixture_hash) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin(), 0u8, None),
            FluidState::PressureEnthalpy(p, h) => (p.to_pascals(), h.to_joules_per_kg(), 1u8, None),
            FluidState::PressureEntropy(p, s) => {
                (p.to_pascals(), s.to_joules_per_kg_kelvin(), 2u8, None)
            }
            FluidState::PressureQuality(p, x) => (p.to_pascals(), x.value(), 3u8, None),
            FluidState::PressureTemperatureMixture(p, t, ref m) => {
                (p.to_pascals(), t.to_kelvin(), 4u8, Some(mix_hash(m)))
            }
            FluidState::PressureEnthalpyMixture(p, h, ref m) => {
                (p.to_pascals(), h.to_joules_per_kg(), 5u8, Some(mix_hash(m)))
            }
            FluidState::PressureQualityMixture(p, x, ref m) => {
                (p.to_pascals(), x.value(), 6u8, Some(mix_hash(m)))
            }
        };
        CacheKey {
            backend_id,
            fluid: fluid.0.clone(),
            property,
            variant,
            p_quantized: quantize(p),
            second_quantized: quantize(second),
            mixture_hash,
        }
    }
}

/// Compute a simple hash for a mixture for cache key purposes.
fn mix_hash(mixture: &Mixture) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    let mut hasher = DefaultHasher::new();
    mixture.hash(&mut hasher);
    hasher.finish()
}

thread_local! {
    static CACHE: RefCell<LruCache<CacheKey, f64>> = RefCell::new(
        LruCache::new(DEFAULT_CAP_NONZERO)
    );
}

/// Get a value from the thread-local cache (no allocation on key build for hot path).
pub fn cache_get(
    backend_id: usize,
    fluid: &FluidId,
    property: Property,
    state: &FluidState,
) -> Option<f64> {
    let key = CacheKey::new(backend_id, fluid, property, state);
    CACHE.with(|c| {
        c.try_borrow_mut()
            .ok()
            .and_then(|mut cache| cache.get(&key).copied())
    })
}

/// Insert a value into the thread-local cache.
pub fn cache_insert(
    backend_id: usize,
    fluid: &FluidId,
    property: Property,
    state: &FluidState,
    value: f64,
) {
    let key = CacheKey::new(backend_id, fluid, property, state);
    CACHE.with(|c| {
        if let Ok(mut cache) = c.try_borrow_mut() {
            cache.put(key, value);
        }
        // Silently ignore if borrow fails (cache miss is acceptable)
    });
}

/// Clear the thread-local cache (e.g. at solver iteration boundaries).
pub fn cache_clear() {
    CACHE.with(|c| {
        if let Ok(mut cache) = c.try_borrow_mut() {
            cache.clear();
        }
        // Silently ignore if borrow fails
    });
}

/// Resize the thread-local cache capacity.
pub fn cache_resize(capacity: NonZeroUsize) {
    CACHE.with(|c| {
        if let Ok(mut cache) = c.try_borrow_mut() {
            cache.resize(capacity);
        }
        // Silently ignore if borrow fails
    });
}

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

    #[test]
    fn test_cache_key_quantization() {
        let fluid = FluidId::new("R134a");
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
        let key2 = CacheKey::new(0, &fluid, Property::Density, &state);
        assert_eq!(key1, key2);
        // Equal keys must have same hash (for HashMap use)
        use std::collections::hash_map::DefaultHasher;
        let mut h1 = DefaultHasher::new();
        let mut h2 = DefaultHasher::new();
        key1.hash(&mut h1);
        key2.hash(&mut h2);
        assert_eq!(h1.finish(), h2.finish());
    }

    #[test]
    fn test_cache_key_different_states() {
        let fluid = FluidId::new("R134a");
        let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let state2 = FluidState::from_pt(Pressure::from_bar(2.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state1);
        let key2 = CacheKey::new(0, &fluid, Property::Density, &state2);
        assert_ne!(key1, key2);
    }

    #[test]
    fn test_lru_eviction() {
        use std::num::NonZeroUsize;

        cache_clear();
        cache_resize(NonZeroUsize::new(2).expect("2 is non-zero"));

        let fluid = FluidId::new("R134a");
        let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let state2 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let state3 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(30.0));

        cache_insert(0, &fluid, Property::Density, &state1, 1000.0);
        cache_insert(0, &fluid, Property::Density, &state2, 1100.0);
        cache_insert(0, &fluid, Property::Density, &state3, 1200.0);

        assert!(cache_get(0, &fluid, Property::Density, &state1).is_none());
        assert_eq!(
            cache_get(0, &fluid, Property::Density, &state2),
            Some(1100.0)
        );
        assert_eq!(
            cache_get(0, &fluid, Property::Density, &state3),
            Some(1200.0)
        );

        cache_resize(NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity is non-zero"));
    }

    #[test]
    fn test_quantize_extreme_values_do_not_collide_with_normal_keys() {
        // 2026-07-19 incident: `v * 1e9` overflows i64 for |v| > ~9e9, wrapping
        // blown-up trial states onto legitimate keys (finite poisoning).
        let normal_a = quantize(100_000.0);
        let normal_b = quantize(420_000.0);
        for extreme in [1.0e13, -1.0e13, 3.7e15, f64::MAX / 2.0] {
            let q = quantize(extreme);
            assert_ne!(q, normal_a, "extreme {extreme} collides with 1e5");
            assert_ne!(q, normal_b, "extreme {extreme} collides with 4.2e5");
        }
        // Distinct extremes get distinct keys (exact bit hashing).
        assert_ne!(quantize(1.0e13), quantize(1.0e13 * 1.000001));
        // Normal quantization unchanged.
        assert_eq!(quantize(1.5), quantize(1.5));
        assert_ne!(quantize(1.5), quantize(1.5 + 1e-8));
    }

    #[test]
    fn test_cache_key_different_backends() {
        let fluid = FluidId::new("R134a");
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
        let key2 = CacheKey::new(1, &fluid, Property::Density, &state);
        assert_ne!(key1, key2);
    }
}
