<?php

declare(strict_types=1);

namespace App\Services\Security;

require_once __DIR__ . '/AlertService.php';

use PDO;
use Database;

/**
 * Service de gestion des blocages d'IP
 * Bloque temporairement ou définitivement des adresses IP suspectes
 */
class IPBlocker {

    private static ?PDO $db = null;
    private static array $cache = [];
    private static ?int $lastCacheClean = null;
    private static ?array $dynamicWhitelist = null;
    private static ?int $whitelistLastCheck = null;

    // IPs en whitelist statique (jamais bloquées)
    const WHITELIST = [
        '127.0.0.1',
        '::1',
        'localhost'
    ];

    // Configuration pour récupérer l'IP depuis IN3
    const IN3_CONFIG = [
        'host' => '195.154.80.116',
        'user' => 'root',
        'ip_file' => '/var/bat/IP',
        'cache_duration' => 3600, // 1 heure de cache
        'local_cache_file' => __DIR__ . '/../../../config/whitelist_ip_cache.txt'
    ];

    /**
     * Récupérer la whitelist dynamique depuis IN3
     */
    private static function getDynamicWhitelist(): array {
        $now = time();

        // Vérifier le cache en mémoire (valide 1 heure)
        if (self::$dynamicWhitelist !== null &&
            self::$whitelistLastCheck !== null &&
            ($now - self::$whitelistLastCheck) < self::IN3_CONFIG['cache_duration']) {
            return self::$dynamicWhitelist;
        }

        $dynamicIps = [];

        // D'abord, essayer de lire le cache local
        $cacheFile = self::IN3_CONFIG['local_cache_file'];
        if (file_exists($cacheFile)) {
            $cacheData = json_decode(file_get_contents($cacheFile), true);
            if ($cacheData && isset($cacheData['ip']) && isset($cacheData['timestamp'])) {
                // Si le cache a moins d'1 heure, l'utiliser
                if (($now - $cacheData['timestamp']) < self::IN3_CONFIG['cache_duration']) {
                    $dynamicIps[] = $cacheData['ip'];
                    self::$dynamicWhitelist = $dynamicIps;
                    self::$whitelistLastCheck = $now;
                    return $dynamicIps;
                }
            }
        }

        // Sinon, récupérer depuis IN3 via SSH
        try {
            $command = sprintf(
                'ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no %s@%s "cat %s 2>/dev/null"',
                escapeshellarg(self::IN3_CONFIG['user']),
                escapeshellarg(self::IN3_CONFIG['host']),
                escapeshellarg(self::IN3_CONFIG['ip_file'])
            );

            $output = shell_exec($command);

            if ($output) {
                $ip = trim($output);

                // Valider que c'est une IP valide
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    $dynamicIps[] = $ip;

                    // Sauvegarder dans le cache local
                    $cacheDir = dirname($cacheFile);
                    if (!is_dir($cacheDir)) {
                        mkdir($cacheDir, 0755, true);
                    }

                    file_put_contents($cacheFile, json_encode([
                        'ip' => $ip,
                        'timestamp' => $now,
                        'retrieved_at' => date('Y-m-d H:i:s')
                    ]));

                    error_log("Whitelist IP mise à jour depuis IN3: $ip");
                }
            }
        } catch (\Exception $e) {
            error_log("Erreur lors de la récupération de l'IP depuis IN3: " . $e->getMessage());

            // En cas d'erreur, utiliser le cache local même s'il est expiré
            if (file_exists($cacheFile)) {
                $cacheData = json_decode(file_get_contents($cacheFile), true);
                if ($cacheData && isset($cacheData['ip'])) {
                    $dynamicIps[] = $cacheData['ip'];
                    error_log("Utilisation du cache local expiré: " . $cacheData['ip']);
                }
            }
        }

        self::$dynamicWhitelist = $dynamicIps;
        self::$whitelistLastCheck = $now;

        return $dynamicIps;
    }

    /**
     * Vérifier si une IP est bloquée
     */
    public static function isBlocked(string $ip): bool {
        // Vérifier la whitelist statique
        if (in_array($ip, self::WHITELIST)) {
            return false;
        }

        // Vérifier la whitelist dynamique depuis IN3
        $dynamicWhitelist = self::getDynamicWhitelist();
        if (in_array($ip, $dynamicWhitelist)) {
            return false;
        }
        
        // Vérifier le cache en mémoire
        if (isset(self::$cache[$ip])) {
            $cached = self::$cache[$ip];
            if ($cached['blocked_until'] > time()) {
                return true;
            } else {
                // Expirée, retirer du cache
                unset(self::$cache[$ip]);
            }
        }
        
        // Vérifier en base de données
        $db = self::getDb();
        
        $stmt = $db->prepare('
            SELECT ip_address, blocked_until, permanent, reason
            FROM sec_blocked_ips 
            WHERE ip_address = :ip 
              AND (permanent = 1 OR blocked_until > NOW())
              AND unblocked_at IS NULL
            LIMIT 1
        ');
        
        $stmt->execute(['ip' => $ip]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($result) {
            // Mettre en cache
            self::$cache[$ip] = [
                'blocked_until' => strtotime($result['blocked_until']),
                'permanent' => (bool)$result['permanent'],
                'reason' => $result['reason']
            ];
            
            // Nettoyer le cache périodiquement
            self::cleanCacheIfNeeded();
            
            return true;
        }
        
        return false;
    }
    
    /**
     * Bloquer une IP temporairement
     */
    public static function block(
        string $ip, 
        int $durationSeconds = 3600, 
        string $reason = 'Suspicious activity',
        string $blockedBy = 'system'
    ): bool {
        // Ne pas bloquer les IPs en whitelist
        if (in_array($ip, self::WHITELIST)) {
            error_log("Attempted to block whitelisted IP: $ip");
            return false;
        }
        
        $db = self::getDb();
        
        try {
            // Vérifier si l'IP est déjà bloquée
            $checkStmt = $db->prepare('
                SELECT ip_address, block_count 
                FROM sec_blocked_ips 
                WHERE ip_address = :ip
                LIMIT 1
            ');
            $checkStmt->execute(['ip' => $ip]);
            $existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
            
            $blockedUntil = date('Y-m-d H:i:s', time() + $durationSeconds);
            
            if ($existing) {
                // Mettre à jour le blocage existant
                $updateStmt = $db->prepare('
                    UPDATE sec_blocked_ips 
                    SET blocked_until = :until,
                        reason = :reason,
                        blocked_at = NOW(),
                        blocked_by = :by,
                        block_count = block_count + 1,
                        unblocked_at = NULL,
                        permanent = 0
                    WHERE ip_address = :ip
                ');
                
                $updateStmt->execute([
                    'ip' => $ip,
                    'until' => $blockedUntil,
                    'reason' => $reason,
                    'by' => $blockedBy
                ]);
                
                $blockCount = $existing['block_count'] + 1;
                
                // Si bloquée plus de 3 fois, envisager un blocage permanent
                if ($blockCount >= 3) {
                    AlertService::trigger('REPEAT_OFFENDER', [
                        'ip' => $ip,
                        'block_count' => $blockCount,
                        'reason' => $reason,
                        'message' => "IP bloquée pour la {$blockCount}e fois, considérer blocage permanent"
                    ], 'WARNING');
                }
                
            } else {
                // Créer un nouveau blocage
                $insertStmt = $db->prepare('
                    INSERT INTO sec_blocked_ips (
                        ip_address, reason, blocked_until, blocked_by, 
                        permanent, details
                    ) VALUES (
                        :ip, :reason, :until, :by, 0, :details
                    )
                ');
                
                $details = json_encode([
                    'timestamp' => date('c'),
                    'duration_seconds' => $durationSeconds,
                    'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
                    'endpoint' => $_SERVER['REQUEST_URI'] ?? null
                ]);
                
                $insertStmt->execute([
                    'ip' => $ip,
                    'reason' => $reason,
                    'until' => $blockedUntil,
                    'by' => $blockedBy,
                    'details' => $details
                ]);
            }
            
            // Mettre à jour le cache
            self::$cache[$ip] = [
                'blocked_until' => time() + $durationSeconds,
                'permanent' => false,
                'reason' => $reason
            ];
            
            // Logger l'action
            error_log("IP blocked: $ip for {$durationSeconds}s - Reason: $reason");
            
            return true;
            
        } catch (\Exception $e) {
            error_log("Failed to block IP: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Bloquer une IP définitivement
     */
    public static function blockPermanent(
        string $ip, 
        string $reason = 'Security threat',
        string $blockedBy = 'system'
    ): bool {
        // Ne pas bloquer les IPs en whitelist
        if (in_array($ip, self::WHITELIST)) {
            error_log("Attempted to permanently block whitelisted IP: $ip");
            return false;
        }
        
        $db = self::getDb();
        
        try {
            // Vérifier si l'IP existe déjà
            $checkStmt = $db->prepare('
                SELECT ip_address FROM sec_blocked_ips 
                WHERE ip_address = :ip
                LIMIT 1
            ');
            $checkStmt->execute(['ip' => $ip]);
            $existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
            
            if ($existing) {
                // Mettre à jour en permanent
                $updateStmt = $db->prepare('
                    UPDATE sec_blocked_ips 
                    SET permanent = 1,
                        blocked_until = "2099-12-31 23:59:59",
                        reason = :reason,
                        blocked_at = NOW(),
                        blocked_by = :by,
                        unblocked_at = NULL
                    WHERE ip_address = :ip
                ');
                
                $updateStmt->execute([
                    'ip' => $ip,
                    'reason' => $reason,
                    'by' => $blockedBy
                ]);
            } else {
                // Créer un blocage permanent
                $insertStmt = $db->prepare('
                    INSERT INTO sec_blocked_ips (
                        ip_address, reason, blocked_until, blocked_by, 
                        permanent, details
                    ) VALUES (
                        :ip, :reason, "2099-12-31 23:59:59", :by, 1, :details
                    )
                ');
                
                $details = json_encode([
                    'timestamp' => date('c'),
                    'permanent' => true,
                    'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
                    'endpoint' => $_SERVER['REQUEST_URI'] ?? null
                ]);
                
                $insertStmt->execute([
                    'ip' => $ip,
                    'reason' => $reason,
                    'by' => $blockedBy,
                    'details' => $details
                ]);
            }
            
            // Mettre à jour le cache
            self::$cache[$ip] = [
                'blocked_until' => PHP_INT_MAX,
                'permanent' => true,
                'reason' => $reason
            ];
            
            // Alerter pour blocage permanent
            AlertService::trigger('IP_BLOCKED_PERMANENT', [
                'ip' => $ip,
                'reason' => $reason,
                'blocked_by' => $blockedBy,
                'message' => "IP bloquée définitivement : $ip"
            ], 'SECURITY');
            
            return true;
            
        } catch (\Exception $e) {
            error_log("Failed to permanently block IP: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Débloquer une IP
     */
    public static function unblock(string $ip, int $unblockedBy = null): bool {
        $db = self::getDb();
        
        try {
            $stmt = $db->prepare('
                UPDATE sec_blocked_ips 
                SET unblocked_at = NOW(),
                    unblocked_by = :by
                WHERE ip_address = :ip 
                  AND unblocked_at IS NULL
            ');
            
            $stmt->execute([
                'ip' => $ip,
                'by' => $unblockedBy
            ]);
            
            // Retirer du cache
            unset(self::$cache[$ip]);
            
            $affected = $stmt->rowCount();
            
            if ($affected > 0) {
                error_log("IP unblocked: $ip by user $unblockedBy");
                
                // Logger l'action
                AlertService::trigger('IP_UNBLOCKED', [
                    'ip' => $ip,
                    'unblocked_by' => $unblockedBy,
                    'message' => "IP débloquée manuellement : $ip"
                ], 'INFO');
            }
            
            return $affected > 0;
            
        } catch (\Exception $e) {
            error_log("Failed to unblock IP: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Vérifier si une IP est dans la whitelist (méthode publique)
     */
    public static function isWhitelisted(string $ip): bool {
        // Vérifier la whitelist statique
        if (in_array($ip, self::WHITELIST)) {
            return true;
        }

        // Vérifier la whitelist dynamique
        $dynamicWhitelist = self::getDynamicWhitelist();
        return in_array($ip, $dynamicWhitelist);
    }

    /**
     * Forcer la mise à jour de la whitelist dynamique
     */
    public static function refreshDynamicWhitelist(): array {
        // Forcer l'expiration du cache
        self::$whitelistLastCheck = 0;
        self::$dynamicWhitelist = null;

        // Récupérer la nouvelle whitelist
        return self::getDynamicWhitelist();
    }

    /**
     * Obtenir la liste des IPs bloquées
     */
    public static function getBlockedIPs(bool $activeOnly = true): array {
        $db = self::getDb();
        
        $whereClause = $activeOnly 
            ? 'WHERE unblocked_at IS NULL AND (permanent = 1 OR blocked_until > NOW())'
            : '';
        
        $stmt = $db->prepare("
            SELECT 
                ip_address,
                reason,
                blocked_at,
                blocked_until,
                blocked_by,
                permanent,
                unblocked_at,
                unblocked_by,
                block_count,
                details
            FROM sec_blocked_ips
            $whereClause
            ORDER BY blocked_at DESC
        ");
        
        $stmt->execute();
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    /**
     * Vérifier si une IP est dans un range CIDR
     */
    public static function isInRange(string $ip, string $cidr): bool {
        list($subnet, $bits) = explode('/', $cidr);
        
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && 
            filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            // IPv4
            $ip = ip2long($ip);
            $subnet = ip2long($subnet);
            $mask = -1 << (32 - $bits);
            $subnet &= $mask;
            
            return ($ip & $mask) == $subnet;
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && 
                  filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            // IPv6
            $ipBin = inet_pton($ip);
            $subnetBin = inet_pton($subnet);
            
            $byteCount = (int)($bits / 8);
            $bitCount = $bits % 8;
            
            for ($i = 0; $i < $byteCount; $i++) {
                if ($ipBin[$i] !== $subnetBin[$i]) {
                    return false;
                }
            }
            
            if ($bitCount > 0) {
                $mask = 0xFF << (8 - $bitCount);
                return (ord($ipBin[$byteCount]) & $mask) === (ord($subnetBin[$byteCount]) & $mask);
            }
            
            return true;
        }
        
        return false;
    }
    
    /**
     * Nettoyer les blocages expirés
     */
    public static function cleanupExpired(): int {
        $db = self::getDb();
        
        try {
            $stmt = $db->prepare('
                UPDATE sec_blocked_ips 
                SET unblocked_at = NOW()
                WHERE blocked_until <= NOW() 
                  AND permanent = 0
                  AND unblocked_at IS NULL
            ');
            
            $stmt->execute();
            
            $cleaned = $stmt->rowCount();
            
            if ($cleaned > 0) {
                error_log("Cleaned up $cleaned expired IP blocks");
            }
            
            return $cleaned;
            
        } catch (\Exception $e) {
            error_log("Failed to cleanup expired blocks: " . $e->getMessage());
            return 0;
        }
    }
    
    /**
     * Obtenir les statistiques de blocage
     */
    public static function getStats(): array {
        $db = self::getDb();
        
        // Total des IPs bloquées
        $totalStmt = $db->prepare('
            SELECT 
                COUNT(*) as total,
                SUM(CASE WHEN permanent = 1 THEN 1 ELSE 0 END) as permanent,
                SUM(CASE WHEN permanent = 0 AND blocked_until > NOW() THEN 1 ELSE 0 END) as temporary,
                SUM(CASE WHEN unblocked_at IS NOT NULL THEN 1 ELSE 0 END) as unblocked
            FROM sec_blocked_ips
        ');
        $totalStmt->execute();
        $totals = $totalStmt->fetch(PDO::FETCH_ASSOC);
        
        // Blocages récents (24h)
        $recentStmt = $db->prepare('
            SELECT COUNT(*) as count
            FROM sec_blocked_ips
            WHERE blocked_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
        ');
        $recentStmt->execute();
        $recent = $recentStmt->fetch(PDO::FETCH_ASSOC);
        
        // Top raisons de blocage
        $reasonsStmt = $db->prepare('
            SELECT 
                reason,
                COUNT(*) as count
            FROM sec_blocked_ips
            WHERE unblocked_at IS NULL
            GROUP BY reason
            ORDER BY count DESC
            LIMIT 10
        ');
        $reasonsStmt->execute();
        $reasons = $reasonsStmt->fetchAll(PDO::FETCH_ASSOC);
        
        return [
            'totals' => $totals,
            'recent_24h' => $recent['count'],
            'top_reasons' => $reasons,
            'cache_size' => count(self::$cache),
            'whitelist' => self::WHITELIST
        ];
    }
    
    /**
     * Nettoyer le cache si nécessaire
     */
    private static function cleanCacheIfNeeded(): void {
        $now = time();
        
        // Nettoyer toutes les 5 minutes
        if (self::$lastCacheClean === null || $now - self::$lastCacheClean > 300) {
            self::$cache = array_filter(self::$cache, function($item) use ($now) {
                return $item['permanent'] || $item['blocked_until'] > $now;
            });
            
            self::$lastCacheClean = $now;
        }
        
        // Limiter la taille du cache
        if (count(self::$cache) > 1000) {
            self::$cache = array_slice(self::$cache, -500, null, true);
        }
    }
    
    /**
     * Obtenir l'instance de base de données
     */
    private static function getDb(): PDO {
        if (self::$db === null) {
            self::$db = Database::getInstance();
        }
        return self::$db;
    }
}