//! Inter-bar feature result caching for streaming optimization
//!
//! GitHub Issue: https://github.com/terrylica/opendeviationbar-py/issues/96
//! GitHub Issue: https://github.com/terrylica/opendeviationbar-py/issues/59
//! Task #144 Phase 4: Result caching for deterministic sequences
//!
//! Caches computed inter-bar features (OFI, VWAP, Kyle Lambda, Hurst, etc.)
//! keyed by trade count and window hash to avoid redundant computation
//! in streaming scenarios where similar lookback windows repeat.
//!
//! ## Benefits
//!
//! - **Latency reduction**: 20-40% for repeated window patterns (Task #144 target)
//! - **Memory efficient**: LRU eviction, bounded capacity
//! - **Transparent**: Optional, backward compatible
//!
//! ## Architecture
//!
//! Cache key: `(trade_count, window_hash)` → `InterBarFeatures`
//!
//! Window hash captures:
//! - Price movement pattern (OHLC bounds)
//! - Volume distribution
//! - Temporal characteristics (duration, trade frequency)

use crate::interbar_types::{InterBarFeatures, TradeSnapshot};
use foldhash::fast::FixedState;
use std::hash::{BuildHasher, Hash, Hasher};

/// Maximum capacity for inter-bar feature cache
/// Trade-off: Larger → higher hit ratio; smaller → less memory
pub const INTERBAR_FEATURE_CACHE_CAPACITY: u64 = 256;

/// Compute a hash of trade window characteristics (optimized single-pass version)
/// Issue #96 Task #162: Eliminated redundant iteration in hash_trade_window
/// Issue #96 Task #171: Early-exit for insufficient lookback windows
/// Used as part of the cache key to identify similar trade sequences
fn hash_trade_window(lookback: &[&TradeSnapshot]) -> u64 {
    // Early-exit: windows with < 2 trades have no Tier 2/3 features anyway
    // (Kaufman ER requires 2+ trades; Hurst, PE, ApEn require 60+ or 500+)
    // Avoid hash computation overhead for ~10-30% of calls
    if lookback.len() < 2 {
        return lookback.len() as u64; // Return count as sentinel for cache miss
    }

    let mut hasher = FixedState::default().build_hasher();

    // Hash trade count (exact match)
    lookback.len().hash(&mut hasher);

    // Combine OHLC bounds, volume distribution, and buy/sell ratio into single pass
    // Previously: 3 separate iterations (OHLC, volume, buy/sell)
    // Now: Single pass through lookback trades
    let mut min_price = i64::MAX;
    let mut max_price = i64::MIN;
    // Issue #96 Task #186: Use u64 for volume (safe: typical trade volumes << u64::MAX)
    // Volume range: 0.01 - 1M BTC per trade; max sum in lookback (500 trades): ~500M BTC
    // u64 limit: ~18 EB BTC, so u64 is safe and eliminates i128 overhead
    let mut total_volume: u64 = 0;
    let mut buy_count = 0usize;

    for trade in lookback {
        min_price = min_price.min(trade.price.0);
        max_price = max_price.max(trade.price.0);
        total_volume = total_volume.wrapping_add(trade.volume.0 as u64);
        // Issue #96 Task #186: Branchless buy_count (eliminate conditional branch)
        // Converts !is_buyer_maker (bool) to 0 or 1
        buy_count += (!trade.is_buyer_maker) as usize;
    }

    // Compress to nearest 100 bps (0.01%) for fuzzy matching
    // The 100 divisor is conservative fuzzy matching: price movements < 1bps map to same hash
    // Rationale: sub-1bps price movements in lookback window typically don't affect feature computation
    let price_range = (max_price - min_price) / 100;
    price_range.hash(&mut hasher);

    // Hash average volume (use u64 division, avoid i128 overhead)
    let avg_volume = if !lookback.is_empty() {
        total_volume / lookback.len() as u64
    } else {
        0
    };
    avg_volume.hash(&mut hasher);

    // Hash buy/sell ratio
    ((buy_count * 100 / lookback.len()) as u8).hash(&mut hasher);

    hasher.finish()
}

/// Cache key for inter-bar feature results
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct InterBarCacheKey {
    /// Number of trades in lookback window
    pub trade_count: usize,
    /// Hash of window characteristics (price range, volume, buy/sell ratio)
    pub window_hash: u64,
}

impl InterBarCacheKey {
    /// Create cache key from lookback trades
    pub fn from_lookback(lookback: &[&TradeSnapshot]) -> Self {
        Self {
            trade_count: lookback.len(),
            window_hash: hash_trade_window(lookback),
        }
    }
}

/// LRU cache for inter-bar feature computation results
///
/// Task #144 Phase 4: Caches feature computation to reduce latency
/// in streaming scenarios with repeated window patterns.
///
/// Typical hit rate: 15-30% for streaming (depends on market conditions)
#[derive(Debug)]
pub struct InterBarFeatureCache {
    /// Underlying LRU cache (via quick_cache, S3-FIFO eviction)
    cache: quick_cache::sync::Cache<InterBarCacheKey, InterBarFeatures>,
}

impl InterBarFeatureCache {
    /// Create new inter-bar feature cache
    pub fn new() -> Self {
        Self::with_capacity(INTERBAR_FEATURE_CACHE_CAPACITY)
    }

    /// Create with custom capacity
    pub fn with_capacity(capacity: u64) -> Self {
        let cache = quick_cache::sync::Cache::new(capacity as usize);
        Self { cache }
    }

    /// Get cached features if key matches
    pub fn get(&self, key: &InterBarCacheKey) -> Option<InterBarFeatures> {
        self.cache.get(key)
    }

    /// Insert computed features into cache
    pub fn insert(&self, key: InterBarCacheKey, features: InterBarFeatures) {
        self.cache.insert(key, features);
    }

    /// Clear all cached entries
    pub fn clear(&self) {
        self.cache.clear();
    }

    /// Get cache statistics
    pub fn stats(&self) -> (u64, u64) {
        (self.cache.len() as u64, INTERBAR_FEATURE_CACHE_CAPACITY)
    }
}

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

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

    fn create_test_trade(price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: 1000000,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume) as i128,
        }
    }

    #[test]
    fn test_cache_key_from_lookback() {
        let trades = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.5, 1.5, true),
            create_test_trade(100.2, 1.2, false),
        ];
        let refs: Vec<_> = trades.iter().collect();

        let key = InterBarCacheKey::from_lookback(&refs);
        assert_eq!(key.trade_count, 3);
        assert!(key.window_hash > 0, "Window hash should be non-zero");
    }

    #[test]
    fn test_cache_insert_and_retrieve() {
        let cache = InterBarFeatureCache::new();
        let key = InterBarCacheKey {
            trade_count: 10,
            window_hash: 12345,
        };

        let features = InterBarFeatures::default();
        cache.insert(key, features.clone());

        let retrieved = cache.get(&key);
        assert!(retrieved.is_some());
    }

    #[test]
    fn test_cache_miss() {
        let cache = InterBarFeatureCache::new();
        let key = InterBarCacheKey {
            trade_count: 10,
            window_hash: 12345,
        };

        let result = cache.get(&key);
        assert!(result.is_none());
    }

    #[test]
    fn test_cache_clear() {
        let cache = InterBarFeatureCache::new();
        let key = InterBarCacheKey {
            trade_count: 10,
            window_hash: 12345,
        };

        cache.insert(key, InterBarFeatures::default());
        assert!(cache.get(&key).is_some());

        cache.clear();
        assert!(cache.get(&key).is_none());
    }

    #[test]
    fn test_identical_trades_same_hash() {
        let trade = create_test_trade(100.0, 1.0, false);
        let trades = vec![trade.clone(), trade.clone(), trade];
        let refs: Vec<_> = trades.iter().collect();

        let key1 = InterBarCacheKey::from_lookback(&refs);

        let trades2 = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.0, 1.0, false),
        ];
        let refs2: Vec<_> = trades2.iter().collect();
        let key2 = InterBarCacheKey::from_lookback(&refs2);

        // Identical trades should produce same cache key
        assert_eq!(key1, key2);
    }

    #[test]
    fn test_similar_trades_same_hash() {
        let trades1 = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.5, 1.5, true),
            create_test_trade(100.2, 1.2, false),
        ];
        let refs1: Vec<_> = trades1.iter().collect();
        let key1 = InterBarCacheKey::from_lookback(&refs1);

        // Slightly different prices (within fuzzy match tolerance)
        let trades2 = vec![
            create_test_trade(100.01, 1.0, false),
            create_test_trade(100.51, 1.5, true),
            create_test_trade(100.21, 1.2, false),
        ];
        let refs2: Vec<_> = trades2.iter().collect();
        let key2 = InterBarCacheKey::from_lookback(&refs2);

        // Should have same trade count; hash may or may not match depending on tolerance
        assert_eq!(key1.trade_count, key2.trade_count);
    }

    #[test]
    fn test_cache_eviction_beyond_capacity() {
        let capacity = 16u64;
        let cache = InterBarFeatureCache::with_capacity(capacity);

        // Insert 4x capacity entries with repeated access to early entries
        let total = (capacity * 4) as usize;
        for i in 0..total {
            let key = InterBarCacheKey {
                trade_count: i,
                window_hash: i as u64 * 7919, // distinct hashes
            };
            cache.insert(key, InterBarFeatures::default());
        }

        // quick_cache evicts synchronously (no pending tasks needed)
        let (count, _) = cache.stats();
        assert!(
            count <= capacity,
            "cache count ({count}) should not exceed capacity ({capacity})"
        );
        assert!(count > 0, "cache should not be empty after inserts");
    }

    // Issue #96 Task #89: Edge case tests for hash_trade_window

    #[test]
    fn test_hash_early_exit_empty_window() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let key = InterBarCacheKey::from_lookback(&refs);
        assert_eq!(key.trade_count, 0);
        // Empty window returns sentinel 0
        assert_eq!(key.window_hash, 0);
    }

    #[test]
    fn test_hash_early_exit_single_trade() {
        let trade = create_test_trade(100.0, 1.0, false);
        let refs: Vec<_> = vec![&trade];
        let key = InterBarCacheKey::from_lookback(&refs);
        assert_eq!(key.trade_count, 1);
        // Single trade returns sentinel 1
        assert_eq!(key.window_hash, 1);
    }

    #[test]
    fn test_hash_two_trades_not_sentinel() {
        let t1 = create_test_trade(100.0, 1.0, false);
        let t2 = create_test_trade(101.0, 2.0, true);
        let refs: Vec<_> = vec![&t1, &t2];
        let key = InterBarCacheKey::from_lookback(&refs);
        assert_eq!(key.trade_count, 2);
        // Two trades should compute a real hash, not sentinel
        assert!(
            key.window_hash > 1,
            "2-trade window should compute hash, not sentinel"
        );
    }

    #[test]
    fn test_hash_all_buyers_vs_all_sellers() {
        // All buyers (is_buyer_maker=false)
        let buyers = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(101.0, 1.0, false),
            create_test_trade(100.5, 1.0, false),
        ];
        let buyer_refs: Vec<_> = buyers.iter().collect();
        let key_buyers = InterBarCacheKey::from_lookback(&buyer_refs);

        // All sellers (is_buyer_maker=true)
        let sellers = vec![
            create_test_trade(100.0, 1.0, true),
            create_test_trade(101.0, 1.0, true),
            create_test_trade(100.5, 1.0, true),
        ];
        let seller_refs: Vec<_> = sellers.iter().collect();
        let key_sellers = InterBarCacheKey::from_lookback(&seller_refs);

        // Same count but different buy/sell ratios → different hashes
        assert_eq!(key_buyers.trade_count, key_sellers.trade_count);
        assert_ne!(
            key_buyers.window_hash, key_sellers.window_hash,
            "All-buyer and all-seller windows should produce different hashes"
        );
    }

    #[test]
    fn test_hash_different_price_ranges() {
        // Tight range: 100.0 → 100.5 (50 bps)
        let tight = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.5, 1.0, true),
        ];
        let tight_refs: Vec<_> = tight.iter().collect();
        let key_tight = InterBarCacheKey::from_lookback(&tight_refs);

        // Wide range: 100.0 → 110.0 (10000 bps)
        let wide = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(110.0, 1.0, true),
        ];
        let wide_refs: Vec<_> = wide.iter().collect();
        let key_wide = InterBarCacheKey::from_lookback(&wide_refs);

        assert_ne!(
            key_tight.window_hash, key_wide.window_hash,
            "Different price ranges should produce different hashes"
        );
    }

    #[test]
    fn test_feature_value_round_trip() {
        let cache = InterBarFeatureCache::new();
        let key = InterBarCacheKey {
            trade_count: 50,
            window_hash: 99999,
        };

        let mut features = InterBarFeatures::default();
        features.lookback_ofi = Some(0.75);
        features.lookback_trade_count = Some(50);
        features.lookback_intensity = Some(123.456);

        cache.insert(key, features);
        let retrieved = cache.get(&key).expect("should hit cache");

        assert_eq!(retrieved.lookback_ofi, Some(0.75));
        assert_eq!(retrieved.lookback_trade_count, Some(50));
        assert_eq!(retrieved.lookback_intensity, Some(123.456));
    }
}
