//! Middleware Components
//!
//! Rate limiting, caching, and other middleware for the hosting gateway.

use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;
use std::time::{Duration, Instant};

/// Rate limiter using token bucket algorithm
pub struct RateLimiter {
    /// Requests per second limit
    rate_limit: u32,
    /// Bucket entries per IP
    buckets: RwLock<HashMap<String, TokenBucket>>,
    /// Cleanup interval
    cleanup_interval: Duration,
    /// Last cleanup time
    last_cleanup: RwLock<Instant>,
}

/// Token bucket for rate limiting
struct TokenBucket {
    /// Available tokens
    tokens: f64,
    /// Last refill time
    last_refill: Instant,
    /// Max tokens (burst)
    max_tokens: f64,
    /// Refill rate (tokens per second)
    refill_rate: f64,
}

impl TokenBucket {
    fn new(rate: u32) -> Self {
        Self {
            tokens: rate as f64,
            last_refill: Instant::now(),
            max_tokens: rate as f64 * 2.0, // Allow 2x burst
            refill_rate: rate as f64,
        }
    }

    fn try_consume(&mut self) -> bool {
        self.refill();

        if self.tokens >= 1.0 {
            self.tokens -= 1.0;
            true
        } else {
            false
        }
    }

    fn refill(&mut self) {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
        self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
        self.last_refill = now;
    }
}

impl RateLimiter {
    /// Create a new rate limiter
    pub fn new(rate_limit: u32) -> Self {
        Self {
            rate_limit,
            buckets: RwLock::new(HashMap::new()),
            cleanup_interval: Duration::from_secs(60),
            last_cleanup: RwLock::new(Instant::now()),
        }
    }

    /// Check if request is allowed
    pub fn check(&self, client_ip: &str) -> bool {
        self.maybe_cleanup();

        let mut buckets = self.buckets.write().unwrap();
        let bucket = buckets
            .entry(client_ip.to_string())
            .or_insert_with(|| TokenBucket::new(self.rate_limit));

        bucket.try_consume()
    }

    /// Cleanup old entries
    fn maybe_cleanup(&self) {
        let now = Instant::now();
        let should_cleanup = {
            let last = self.last_cleanup.read().unwrap();
            now.duration_since(*last) > self.cleanup_interval
        };

        if should_cleanup {
            let mut buckets = self.buckets.write().unwrap();
            let cutoff = now - Duration::from_secs(300); // 5 minutes

            buckets.retain(|_, bucket| bucket.last_refill > cutoff);

            *self.last_cleanup.write().unwrap() = now;
        }
    }

    /// Get current bucket count (for metrics)
    pub fn bucket_count(&self) -> usize {
        self.buckets.read().unwrap().len()
    }
}

/// Cache control settings
pub struct CacheControl {
    /// Default TTL for cacheable responses
    pub default_ttl: u64,
    /// TTL for immutable assets
    pub immutable_ttl: u64,
    /// TTL for HTML pages
    pub html_ttl: u64,
    /// TTL for API responses
    pub api_ttl: u64,
}

impl Default for CacheControl {
    fn default() -> Self {
        Self {
            default_ttl: 3600,         // 1 hour
            immutable_ttl: 31536000,   // 1 year
            html_ttl: 300,             // 5 minutes
            api_ttl: 60,               // 1 minute
        }
    }
}

impl CacheControl {
    /// Get cache control header value for a path
    pub fn get_header(&self, path: &str, is_fallback: bool) -> String {
        if is_fallback {
            // SPA fallback routes should not be cached
            "no-cache, no-store, must-revalidate".to_string()
        } else if self.is_immutable(path) {
            format!("public, max-age={}, immutable", self.immutable_ttl)
        } else if path.ends_with(".html") || path == "/" {
            format!("public, max-age={}", self.html_ttl)
        } else {
            format!("public, max-age={}", self.default_ttl)
        }
    }

    /// Check if path is an immutable asset
    fn is_immutable(&self, path: &str) -> bool {
        // Check for content hash in filename
        let parts: Vec<&str> = path.split('.').collect();
        if parts.len() >= 3 {
            let potential_hash = parts[parts.len() - 2];
            potential_hash.len() >= 8
                && potential_hash.chars().all(|c| c.is_ascii_alphanumeric())
        } else {
            false
        }
    }
}

/// Request metrics
pub struct Metrics {
    /// Total requests
    pub total_requests: AtomicU64,
    /// Successful requests (2xx)
    pub success_requests: AtomicU64,
    /// Not found requests (404)
    pub not_found_requests: AtomicU64,
    /// Error requests (5xx)
    pub error_requests: AtomicU64,
    /// Rate limited requests
    pub rate_limited_requests: AtomicU64,
    /// Bytes sent
    pub bytes_sent: AtomicU64,
}

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

impl Metrics {
    /// Create new metrics
    pub fn new() -> Self {
        Self {
            total_requests: AtomicU64::new(0),
            success_requests: AtomicU64::new(0),
            not_found_requests: AtomicU64::new(0),
            error_requests: AtomicU64::new(0),
            rate_limited_requests: AtomicU64::new(0),
            bytes_sent: AtomicU64::new(0),
        }
    }

    /// Record a request
    pub fn record_request(&self, status: u16, bytes: u64) {
        self.total_requests.fetch_add(1, Ordering::Relaxed);
        self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);

        match status {
            200..=299 => {
                self.success_requests.fetch_add(1, Ordering::Relaxed);
            }
            404 => {
                self.not_found_requests.fetch_add(1, Ordering::Relaxed);
            }
            429 => {
                self.rate_limited_requests.fetch_add(1, Ordering::Relaxed);
            }
            500..=599 => {
                self.error_requests.fetch_add(1, Ordering::Relaxed);
            }
            _ => {}
        }
    }

    /// Get metrics as JSON
    pub fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "total_requests": self.total_requests.load(Ordering::Relaxed),
            "success_requests": self.success_requests.load(Ordering::Relaxed),
            "not_found_requests": self.not_found_requests.load(Ordering::Relaxed),
            "error_requests": self.error_requests.load(Ordering::Relaxed),
            "rate_limited_requests": self.rate_limited_requests.load(Ordering::Relaxed),
            "bytes_sent": self.bytes_sent.load(Ordering::Relaxed),
        })
    }
}

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

    #[test]
    fn test_rate_limiter_allows_requests() {
        let limiter = RateLimiter::new(10);

        // First 10 requests should be allowed
        for _ in 0..10 {
            assert!(limiter.check("127.0.0.1"));
        }
    }

    #[test]
    fn test_rate_limiter_blocks_excess() {
        let limiter = RateLimiter::new(2);

        // First 2 (initial tokens = rate) should be allowed
        assert!(limiter.check("127.0.0.1"));
        assert!(limiter.check("127.0.0.1"));

        // 3rd should be blocked (no time to refill)
        assert!(!limiter.check("127.0.0.1"));
    }

    #[test]
    fn test_rate_limiter_separate_ips() {
        let limiter = RateLimiter::new(2);

        // Each IP has its own bucket
        assert!(limiter.check("192.168.1.1"));
        assert!(limiter.check("192.168.1.2"));
        assert!(limiter.check("192.168.1.1"));
        assert!(limiter.check("192.168.1.2"));
    }

    #[test]
    fn test_cache_control() {
        let cache = CacheControl::default();

        // SPA fallback
        let header = cache.get_header("/dashboard", true);
        assert!(header.contains("no-cache"));

        // Immutable asset
        let header = cache.get_header("/main.abc12345.js", false);
        assert!(header.contains("immutable"));

        // HTML page
        let header = cache.get_header("/index.html", false);
        assert!(header.contains("max-age=300"));

        // Regular asset
        let header = cache.get_header("/logo.png", false);
        assert!(header.contains("max-age=3600"));
    }

    #[test]
    fn test_metrics() {
        let metrics = Metrics::new();

        metrics.record_request(200, 1000);
        metrics.record_request(404, 100);
        metrics.record_request(500, 50);

        assert_eq!(metrics.total_requests.load(Ordering::Relaxed), 3);
        assert_eq!(metrics.success_requests.load(Ordering::Relaxed), 1);
        assert_eq!(metrics.not_found_requests.load(Ordering::Relaxed), 1);
        assert_eq!(metrics.error_requests.load(Ordering::Relaxed), 1);
        assert_eq!(metrics.bytes_sent.load(Ordering::Relaxed), 1150);
    }
}
