// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Adaptive TTL metrics and recording functions.
//!
//! This module provides metrics recording for adaptive TTL adjustments
//! and access tracking for cache items.

use std::collections::HashMap;
use std::time::{Duration, Instant};

/// Record TTL adjustment.
///
/// # Arguments
///
/// * `key` - Cache key
/// * `base_ttl` - Base TTL in seconds
/// * `adjusted_ttl` - Adjusted TTL in seconds
#[inline(always)]
pub fn record_ttl_adjustment(_key: &str, base_ttl: u64, adjusted_ttl: u64) {
    use metrics::{counter, gauge};

    counter!("rustfs_cache_ttl_adjustments").increment(1);
    gauge!("rustfs_cache_ttl_base").set(base_ttl as f64);
    gauge!("rustfs_cache_ttl_adjusted").set(adjusted_ttl as f64);

    if adjusted_ttl > base_ttl {
        counter!("rustfs_cache_ttl_extensions").increment(1);
    } else if adjusted_ttl < base_ttl {
        counter!("rustfs_cache_ttl_reductions").increment(1);
    }
}

/// Record TTL expiration.
#[inline(always)]
pub fn record_ttl_expiration() {
    use metrics::counter;
    counter!("rustfs_cache_ttl_expirations").increment(1);
}

/// Record early eviction.
///
/// # Arguments
///
/// * `reason` - Reason for early eviction
#[inline(always)]
pub fn record_early_eviction(reason: &str) {
    use metrics::counter;
    counter!("rustfs_cache_evictions_early", "reason" => reason.to_string()).increment(1);
}

/// Record access pattern change.
///
/// # Arguments
///
/// * `from` - Previous pattern
/// * `to` - New pattern
#[inline(always)]
pub fn record_access_pattern_change(from: &str, to: &str) {
    use metrics::counter;
    counter!("rustfs_cache_access_pattern_changes", "from" => from.to_string(), "to" => to.to_string()).increment(1);
}

/// Adaptive TTL statistics.
#[derive(Debug, Clone, Default)]
pub struct AdaptiveTTLStats {
    /// Number of TTL adjustments.
    pub adjustments: u64,
    /// Number of TTL extensions.
    pub extensions: u64,
    /// Number of TTL reductions.
    pub reductions: u64,
    /// Number of TTL expirations.
    pub expirations: u64,
    /// Number of early evictions.
    pub early_evictions: u64,
}

impl AdaptiveTTLStats {
    /// Create new statistics.
    pub fn new() -> Self {
        Self::default()
    }

    /// Record an adjustment.
    pub fn record_adjustment(&mut self, base_ttl: u64, adjusted_ttl: u64) {
        self.adjustments += 1;
        if adjusted_ttl > base_ttl {
            self.extensions += 1;
        } else if adjusted_ttl < base_ttl {
            self.reductions += 1;
        }
    }

    /// Record an expiration.
    pub fn record_expiration(&mut self) {
        self.expirations += 1;
    }

    /// Record an early eviction.
    pub fn record_early_eviction(&mut self) {
        self.early_evictions += 1;
    }

    /// Get extension rate.
    pub fn extension_rate(&self) -> f64 {
        if self.adjustments == 0 {
            0.0
        } else {
            self.extensions as f64 / self.adjustments as f64
        }
    }

    /// Get reduction rate.
    pub fn reduction_rate(&self) -> f64 {
        if self.adjustments == 0 {
            0.0
        } else {
            self.reductions as f64 / self.adjustments as f64
        }
    }

    /// Reset statistics.
    pub fn reset(&mut self) {
        *self = Self::default();
    }
}

// ============================================================================
// Access Tracker
// ============================================================================

/// Access record for a cache item.
#[derive(Debug, Clone)]
pub struct AccessRecord {
    /// Number of accesses.
    pub count: u64,
    /// Last access time.
    pub last_access: Instant,
    /// First access time.
    pub first_access: Instant,
    /// Total size of accesses.
    pub total_size: u64,
}

impl AccessRecord {
    /// Create a new access record.
    pub fn new() -> Self {
        let now = Instant::now();
        Self {
            count: 1,
            last_access: now,
            first_access: now,
            total_size: 0,
        }
    }

    /// Record an access.
    pub fn record_access(&mut self, size: u64) {
        self.count += 1;
        self.last_access = Instant::now();
        self.total_size += size;
    }

    /// Get access frequency (accesses per second).
    pub fn frequency(&self) -> f64 {
        let elapsed = self.first_access.elapsed().as_secs_f64();
        if elapsed > 0.0 { self.count as f64 / elapsed } else { 0.0 }
    }

    /// Get time since last access.
    pub fn idle_time(&self) -> Duration {
        self.last_access.elapsed()
    }
}

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

/// Access tracker for cache items.
#[derive(Debug, Clone)]
pub struct AccessTracker {
    /// Access records by key.
    records: HashMap<String, AccessRecord>,
    /// Maximum number of tracked items.
    max_items: usize,
    /// Access window for frequency calculation.
    window: Duration,
}

impl AccessTracker {
    /// Create a new access tracker.
    pub fn new(max_items: usize, window: Duration) -> Self {
        Self {
            records: HashMap::with_capacity(max_items),
            max_items,
            window,
        }
    }

    /// Create with default settings.
    pub fn with_defaults() -> Self {
        Self::new(10_000, Duration::from_secs(60))
    }

    /// Record an access to a key.
    pub fn record_access(&mut self, key: &str, size: u64) {
        if let Some(record) = self.records.get_mut(key) {
            record.record_access(size);
        } else {
            if self.records.len() >= self.max_items {
                // Evict oldest entry
                self.evict_oldest();
            }
            let mut record = AccessRecord::new();
            record.total_size = size;
            self.records.insert(key.to_string(), record);
        }
    }

    /// Get access count for a key.
    pub fn get_access_count(&self, key: &str) -> u64 {
        self.records.get(key).map_or(0, |r| r.count)
    }

    /// Get access record for a key.
    pub fn get_record(&self, key: &str) -> Option<&AccessRecord> {
        self.records.get(key)
    }

    /// Check if a key is "hot" (high access frequency).
    pub fn is_hot(&self, key: &str, threshold: u64) -> bool {
        self.records.get(key).is_some_and(|r| r.count >= threshold)
    }

    /// Check if a key is "cold" (low access frequency).
    pub fn is_cold(&self, key: &str, threshold: u64) -> bool {
        self.records.get(key).is_none_or(|r| r.count <= threshold)
    }

    /// Get keys sorted by access count (descending).
    pub fn top_keys(&self, n: usize) -> Vec<(&String, &AccessRecord)> {
        let mut entries: Vec<_> = self.records.iter().collect();
        entries.sort_by_key(|entry| std::cmp::Reverse(entry.1.count));
        entries.into_iter().take(n).collect()
    }

    /// Remove old entries outside the window.
    pub fn prune(&mut self) {
        let now = Instant::now();
        self.records
            .retain(|_, record| now.duration_since(record.last_access) < self.window);
    }

    /// Evict the oldest entry.
    fn evict_oldest(&mut self) {
        let oldest = self.records.iter().min_by_key(|(_, r)| r.last_access).map(|(k, _)| k.clone());

        if let Some(key) = oldest {
            self.records.remove(&key);
        }
    }

    /// Get total number of tracked items.
    pub fn len(&self) -> usize {
        self.records.len()
    }

    /// Check if empty.
    pub fn is_empty(&self) -> bool {
        self.records.is_empty()
    }

    /// Clear all records.
    pub fn clear(&mut self) {
        self.records.clear();
    }

    /// Get total access count across all items.
    pub fn total_accesses(&self) -> u64 {
        self.records.values().map(|r| r.count).sum()
    }

    /// Get average access count.
    pub fn avg_access_count(&self) -> f64 {
        if self.records.is_empty() {
            0.0
        } else {
            self.total_accesses() as f64 / self.records.len() as f64
        }
    }
}

impl Default for AccessTracker {
    fn default() -> Self {
        Self::with_defaults()
    }
}

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

    /// Replaces the per-helper smoke tests that called the record_* helpers
    /// and asserted nothing: the calls (same literals) now run against a local
    /// DebuggingRecorder and every metric name the helpers own must actually
    /// be emitted (rustfs/backlog#1836 PR3).
    #[test]
    fn record_helpers_emit_their_metrics() {
        let recorder = metrics_util::debugging::DebuggingRecorder::new();
        let snapshotter = recorder.snapshotter();
        metrics::with_local_recorder(&recorder, || {
            record_ttl_adjustment("test-key", 100, 150);
            record_ttl_adjustment("test-key", 100, 50);
            record_ttl_expiration();
            record_early_eviction("cold");
            record_early_eviction("low_priority");
            record_access_pattern_change("sequential", "random");
            record_access_pattern_change("random", "sequential");
        });

        let emitted: std::collections::HashSet<String> = snapshotter
            .snapshot()
            .into_vec()
            .into_iter()
            .map(|(composite, _, _, _)| composite.key().name().to_string())
            .collect();
        for expected in [
            "rustfs_cache_ttl_adjustments",
            "rustfs_cache_ttl_base",
            "rustfs_cache_ttl_adjusted",
            "rustfs_cache_ttl_extensions",
            "rustfs_cache_ttl_reductions",
            "rustfs_cache_ttl_expirations",
            "rustfs_cache_evictions_early",
            "rustfs_cache_access_pattern_changes",
        ] {
            assert!(emitted.contains(expected), "{expected} must be emitted by its record helper");
        }
    }

    #[test]
    fn test_adaptive_ttl_stats() {
        let mut stats = AdaptiveTTLStats::new();

        stats.record_adjustment(100, 150); // Extension
        stats.record_adjustment(100, 50); // Reduction
        stats.record_adjustment(100, 100); // No change
        stats.record_expiration();
        stats.record_early_eviction();

        assert_eq!(stats.adjustments, 3);
        assert_eq!(stats.extensions, 1);
        assert_eq!(stats.reductions, 1);
        assert_eq!(stats.expirations, 1);
        assert_eq!(stats.early_evictions, 1);

        assert!((stats.extension_rate() - 0.3333333333333333).abs() < 0.01);
        assert!((stats.reduction_rate() - 0.3333333333333333).abs() < 0.01);
    }

    #[test]
    fn test_access_record() {
        let mut record = AccessRecord::new();
        assert_eq!(record.count, 1);

        record.record_access(1024);
        record.record_access(2048);

        assert_eq!(record.count, 3);
        assert_eq!(record.total_size, 3072);
    }

    #[test]
    fn test_access_tracker() {
        let mut tracker = AccessTracker::new(100, Duration::from_secs(60));

        tracker.record_access("key1", 1024);
        tracker.record_access("key1", 1024);
        tracker.record_access("key2", 2048);

        assert_eq!(tracker.len(), 2);
        assert_eq!(tracker.get_access_count("key1"), 2);
        assert_eq!(tracker.get_access_count("key2"), 1);
        assert_eq!(tracker.total_accesses(), 3);
    }

    #[test]
    fn test_access_tracker_hot_cold() {
        let mut tracker = AccessTracker::with_defaults();

        // Make key1 hot
        for _ in 0..10 {
            tracker.record_access("key1", 1024);
        }
        tracker.record_access("key2", 1024);

        assert!(tracker.is_hot("key1", 5));
        assert!(!tracker.is_hot("key2", 5));
        assert!(tracker.is_cold("key2", 1));
    }

    #[test]
    fn test_access_tracker_top_keys() {
        let mut tracker = AccessTracker::with_defaults();

        for _ in 0..10 {
            tracker.record_access("key1", 1024);
        }
        for _ in 0..5 {
            tracker.record_access("key2", 1024);
        }
        tracker.record_access("key3", 1024);

        let top = tracker.top_keys(2);
        assert_eq!(top.len(), 2);
        assert_eq!(top[0].0, "key1");
        assert_eq!(top[1].0, "key2");
    }

    #[test]
    fn test_access_tracker_clear() {
        let mut tracker = AccessTracker::with_defaults();

        tracker.record_access("key1", 1024);
        tracker.record_access("key2", 1024);

        assert_eq!(tracker.len(), 2);
        tracker.clear();
        assert!(tracker.is_empty());
    }
}
