//! API Key Validation with Cache and Rate Limiting
//!
//! Provides cached validation and rate limiting for API keys

use crate::core::db::postgres_db::ApiKeyRecord;
use crate::core::db::PostgresDb;
use anyhow::Result;
use chrono::{DateTime, Duration, Utc};
use moka::future::Cache;
use std::sync::Arc;
use std::time::Duration as StdDuration;

/// Cached API key record
#[derive(Clone)]
pub struct CachedApiKey {
    pub record: ApiKeyRecord,
    pub cached_at: DateTime<Utc>,
}

/// Rate limit result
#[derive(Debug, Clone)]
pub enum RateLimitResult {
    /// Request is allowed
    Allowed,
    /// Request is allowed but with warning
    AllowedWithWarning { remaining_attempts: u32 },
    /// Request is locked
    Locked {
        remaining_seconds: i64,
        attempts: u32,
    },
}

/// Attempt tracking info
#[derive(Clone)]
struct AttemptInfo {
    count: u32,
    first_attempt: DateTime<Utc>,
    last_attempt: DateTime<Utc>,
    locked_until: Option<DateTime<Utc>>,
}

/// API Key Validator with caching and rate limiting
pub struct ApiKeyValidator {
    db: Arc<PostgresDb>,
    cache: Cache<String, CachedApiKey>,
    rate_limiter: Cache<String, AttemptInfo>,
    max_attempts: u32,
    lockout_duration: Duration,
}

/// Configuration for ApiKeyValidator
pub struct ValidatorConfig {
    pub cache_ttl_secs: u64,
    pub cache_max_capacity: u64,
    pub max_attempts: u32,
    pub lockout_duration_secs: i64,
}

impl Default for ValidatorConfig {
    fn default() -> Self {
        Self {
            cache_ttl_secs: std::env::var("CACHE_TTL_SECONDS")
                .unwrap_or_else(|_| "300".to_string())
                .parse()
                .unwrap_or(300),
            cache_max_capacity: std::env::var("CACHE_MAX_CAPACITY")
                .unwrap_or_else(|_| "10000".to_string())
                .parse()
                .unwrap_or(10000),
            max_attempts: std::env::var("RATE_LIMIT_MAX_ATTEMPTS")
                .unwrap_or_else(|_| "5".to_string())
                .parse()
                .unwrap_or(5),
            lockout_duration_secs: std::env::var("RATE_LIMIT_WINDOW_SECONDS")
                .unwrap_or_else(|_| "900".to_string())
                .parse()
                .unwrap_or(900),
        }
    }
}

impl ApiKeyValidator {
    pub fn new(db: PostgresDb, config: ValidatorConfig) -> Self {
        Self {
            db: Arc::new(db),
            cache: Cache::builder()
                .time_to_live(StdDuration::from_secs(config.cache_ttl_secs))
                .time_to_idle(StdDuration::from_secs(config.cache_ttl_secs * 2))
                .max_capacity(config.cache_max_capacity)
                .build(),
            rate_limiter: Cache::builder()
                .time_to_live(StdDuration::from_secs(
                    config.lockout_duration_secs as u64 * 2,
                ))
                .max_capacity(10000)
                .build(),
            max_attempts: config.max_attempts,
            lockout_duration: Duration::seconds(config.lockout_duration_secs),
        }
    }

    pub fn with_default_config(db: PostgresDb) -> Self {
        Self::new(db, ValidatorConfig::default())
    }

    /// Check rate limit for an IP
    pub async fn check_rate_limit(&self, ip: &str) -> RateLimitResult {
        match self.rate_limiter.get(ip).await {
            None => RateLimitResult::Allowed,
            Some(info) => {
                if let Some(locked_until) = info.locked_until {
                    let remaining = locked_until - Utc::now();
                    if remaining.num_seconds() > 0 {
                        return RateLimitResult::Locked {
                            remaining_seconds: remaining.num_seconds(),
                            attempts: info.count,
                        };
                    }
                }
                if info.count >= self.max_attempts / 2 {
                    RateLimitResult::AllowedWithWarning {
                        remaining_attempts: self.max_attempts - info.count,
                    }
                } else {
                    RateLimitResult::Allowed
                }
            }
        }
    }

    /// Record a failed attempt
    pub async fn record_failure(&self, ip: &str) -> RateLimitResult {
        let mut info = self.rate_limiter.get(ip).await.unwrap_or(AttemptInfo {
            count: 0,
            first_attempt: Utc::now(),
            last_attempt: Utc::now(),
            locked_until: None,
        });

        info.count += 1;
        info.last_attempt = Utc::now();

        if info.count >= self.max_attempts {
            info.locked_until = Some(Utc::now() + self.lockout_duration);
            self.rate_limiter.insert(ip.to_string(), info.clone()).await;

            tracing::warn!("IP {} locked due to {} failed attempts", ip, info.count);

            RateLimitResult::Locked {
                remaining_seconds: self.lockout_duration.num_seconds(),
                attempts: info.count,
            }
        } else {
            let remaining = self.max_attempts - info.count;
            self.rate_limiter.insert(ip.to_string(), info).await;

            RateLimitResult::AllowedWithWarning {
                remaining_attempts: remaining,
            }
        }
    }

    /// Record a successful validation (clear rate limit)
    pub async fn record_success(&self, ip: &str) {
        self.rate_limiter.invalidate(ip).await;
    }

    /// Manually unlock an IP
    pub async fn unlock_ip(&self, ip: &str) {
        self.rate_limiter.invalidate(ip).await;
        tracing::info!("Manually unlocked IP: {}", ip);
    }

    /// Validate an API key with caching
    pub async fn validate(&self, key_hash: &str) -> Result<Option<ApiKeyRecord>> {
        // 1. Check cache
        if let Some(cached) = self.cache.get(key_hash).await {
            // Check if expired
            if let Some(expires_at) = cached.record.expires_at {
                if Utc::now() > expires_at {
                    self.cache.invalidate(key_hash).await;
                    return Ok(None);
                }
            }

            // Check if revoked
            if cached.record.status == "revoked" || cached.record.status == "suspended" {
                self.cache.invalidate(key_hash).await;
                return Ok(None);
            }

            return Ok(Some(cached.record));
        }

        // 2. Query database
        let record = self.db.get_api_key_by_hash(key_hash).await?;

        // 3. Cache if valid
        if let Some(ref r) = record {
            // Only cache active keys
            if r.status == "active" {
                self.cache
                    .insert(
                        key_hash.to_string(),
                        CachedApiKey {
                            record: r.clone(),
                            cached_at: Utc::now(),
                        },
                    )
                    .await;
            }
        }

        Ok(record)
    }

    /// Invalidate cache for a specific key
    pub async fn invalidate(&self, key_hash: &str) {
        self.cache.invalidate(key_hash).await;
    }

    /// Invalidate all cached keys
    pub fn invalidate_all(&self) {
        self.cache.invalidate_all();
    }

    /// Get cache statistics
    pub async fn cache_stats(&self) -> CacheStats {
        CacheStats {
            entry_count: self.cache.entry_count(),
            weighted_size: self.cache.weighted_size(),
        }
    }

    /// Get rate limiter statistics for an IP
    pub async fn rate_limit_stats(&self, ip: &str) -> Option<RateLimitStats> {
        self.rate_limiter.get(ip).await.map(|info| RateLimitStats {
            attempts: info.count,
            first_attempt: info.first_attempt,
            last_attempt: info.last_attempt,
            locked_until: info.locked_until,
        })
    }
}

/// Cache statistics
#[derive(Debug, Clone)]
pub struct CacheStats {
    pub entry_count: u64,
    pub weighted_size: u64,
}

/// Rate limit statistics for an IP
#[derive(Debug, Clone)]
pub struct RateLimitStats {
    pub attempts: u32,
    pub first_attempt: DateTime<Utc>,
    pub last_attempt: DateTime<Utc>,
    pub locked_until: Option<DateTime<Utc>>,
}

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

    #[test]
    fn test_validator_config_default() {
        let config = ValidatorConfig::default();
        assert!(config.cache_ttl_secs > 0);
        assert!(config.cache_max_capacity > 0);
        assert!(config.max_attempts > 0);
        assert!(config.lockout_duration_secs > 0);
    }

    #[test]
    fn test_rate_limit_result_variants() {
        let allowed = RateLimitResult::Allowed;
        let warning = RateLimitResult::AllowedWithWarning {
            remaining_attempts: 3,
        };
        let locked = RateLimitResult::Locked {
            remaining_seconds: 60,
            attempts: 5,
        };

        match allowed {
            RateLimitResult::Allowed => assert!(true),
            _ => assert!(false),
        }

        match warning {
            RateLimitResult::AllowedWithWarning { remaining_attempts } => {
                assert_eq!(remaining_attempts, 3)
            }
            _ => assert!(false),
        }

        match locked {
            RateLimitResult::Locked {
                remaining_seconds,
                attempts,
            } => {
                assert_eq!(remaining_seconds, 60);
                assert_eq!(attempts, 5);
            }
            _ => assert!(false),
        }
    }
}
