//! IP Blacklist Module
//!
//! Manages blocked IP addresses for API key validation

use chrono::{DateTime, Duration, Utc};
use moka::future::Cache;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration as StdDuration;
use tokio::sync::RwLock;

/// IP blacklist entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlacklistEntry {
    pub ip: String,
    pub reason: String,
    pub blocked_at: DateTime<Utc>,
    pub expires_at: Option<DateTime<Utc>>,
    pub blocked_by: Option<String>,
}

/// IP Blacklist manager
pub struct IpBlacklist {
    /// In-memory blacklist with TTL
    cache: Cache<String, BlacklistEntry>,
    /// Permanent blacklist (no TTL)
    permanent: Arc<RwLock<HashSet<String>>>,
}

/// Configuration for IP blacklist
pub struct BlacklistConfig {
    pub default_block_duration_secs: u64,
    pub max_entries: u64,
}

impl Default for BlacklistConfig {
    fn default() -> Self {
        Self {
            default_block_duration_secs: std::env::var("IP_BLACKLIST_DURATION")
                .unwrap_or_else(|_| "3600".to_string())
                .parse()
                .unwrap_or(3600),
            max_entries: std::env::var("IP_BLACKLIST_MAX_ENTRIES")
                .unwrap_or_else(|_| "10000".to_string())
                .parse()
                .unwrap_or(10000),
        }
    }
}

impl IpBlacklist {
    pub fn new(config: BlacklistConfig) -> Self {
        Self {
            cache: Cache::builder()
                .time_to_live(StdDuration::from_secs(config.default_block_duration_secs))
                .max_capacity(config.max_entries)
                .build(),
            permanent: Arc::new(RwLock::new(HashSet::new())),
        }
    }

    pub fn with_default_config() -> Self {
        Self::new(BlacklistConfig::default())
    }

    /// Check if an IP is blocked
    pub async fn is_blocked(&self, ip: &str) -> bool {
        // Check permanent blacklist first
        if self.permanent.read().await.contains(ip) {
            return true;
        }

        // Check temporary blacklist
        self.cache.get(ip).await.is_some()
    }

    /// Get blacklist entry for an IP
    pub async fn get_entry(&self, ip: &str) -> Option<BlacklistEntry> {
        self.cache.get(ip).await
    }

    /// Block an IP temporarily
    pub async fn block(&self, ip: &str, reason: &str, duration_secs: Option<u64>) {
        let entry = BlacklistEntry {
            ip: ip.to_string(),
            reason: reason.to_string(),
            blocked_at: Utc::now(),
            expires_at: duration_secs.map(|d| Utc::now() + Duration::seconds(d as i64)),
            blocked_by: Some("system".to_string()),
        };

        self.cache.insert(ip.to_string(), entry).await;
        tracing::info!("Blocked IP: {} - {}", ip, reason);
    }

    /// Block an IP permanently
    pub async fn block_permanent(&self, ip: &str, reason: &str) {
        self.permanent.write().await.insert(ip.to_string());
        tracing::info!("Permanently blocked IP: {} - {}", ip, reason);
    }

    /// Unblock an IP
    pub async fn unblock(&self, ip: &str) -> bool {
        let in_cache = self.cache.get(ip).await.is_some();
        if in_cache {
            self.cache.invalidate(ip).await;
        }
        let from_permanent = self.permanent.write().await.remove(ip);

        if in_cache || from_permanent {
            tracing::info!("Unblocked IP: {}", ip);
            true
        } else {
            false
        }
    }

    /// Get all blocked IPs
    pub async fn list_all(&self) -> Vec<String> {
        let mut ips: Vec<String> = self.cache.iter().map(|(k, _)| (*k).clone()).collect();
        ips.extend(self.permanent.read().await.iter().cloned());
        ips.sort();
        ips.dedup();
        ips
    }

    /// Get count of blocked IPs
    pub async fn count(&self) -> usize {
        self.cache.entry_count() as usize + self.permanent.read().await.len()
    }
}

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

    #[tokio::test]
    async fn test_block_and_check() {
        let blacklist = IpBlacklist::with_default_config();

        assert!(!blacklist.is_blocked("192.168.1.1").await);

        blacklist.block("192.168.1.1", "test", Some(60)).await;

        assert!(blacklist.is_blocked("192.168.1.1").await);
    }

    #[tokio::test]
    async fn test_unblock() {
        let blacklist = IpBlacklist::with_default_config();

        blacklist.block("192.168.1.1", "test", Some(60)).await;
        assert!(blacklist.is_blocked("192.168.1.1").await);

        assert!(blacklist.unblock("192.168.1.1").await);
        assert!(!blacklist.is_blocked("192.168.1.1").await);
    }

    #[tokio::test]
    async fn test_permanent_block() {
        let blacklist = IpBlacklist::with_default_config();

        blacklist.block_permanent("10.0.0.1", "permanent ban").await;

        assert!(blacklist.is_blocked("10.0.0.1").await);
        assert!(blacklist.unblock("10.0.0.1").await);
        assert!(!blacklist.is_blocked("10.0.0.1").await);
    }

    #[tokio::test]
    async fn test_list_all() {
        let blacklist = IpBlacklist::with_default_config();

        blacklist.block("192.168.1.1", "test", Some(60)).await;
        blacklist.block_permanent("10.0.0.1", "permanent").await;

        let ips = blacklist.list_all().await;
        assert_eq!(ips.len(), 2);
        assert!(ips.contains(&"192.168.1.1".to_string()));
        assert!(ips.contains(&"10.0.0.1".to_string()));
    }

    #[tokio::test]
    async fn test_count() {
        let blacklist = IpBlacklist::with_default_config();

        assert_eq!(blacklist.count().await, 0);

        blacklist.block("192.168.1.1", "test", Some(60)).await;
        blacklist.block("192.168.1.2", "test", Some(60)).await;
        blacklist.block_permanent("10.0.0.1", "permanent").await;

        // Count should be at least 1 (permanent) + 2 (cached) = 3
        // Note: cache entry_count might need time to update
        let count = blacklist.count().await;
        assert!(
            count >= 1,
            "Expected at least 1 entry (permanent), got {}",
            count
        );
    }
}
