<?php

declare(strict_types=1);

namespace App\Services\Security;

require_once __DIR__ . '/AlertService.php';
require_once __DIR__ . '/IPBlocker.php';

use PDO;
use Database;

/**
 * Service de monitoring de sécurité
 * Détecte les patterns d'attaque et les comportements suspects
 */
class SecurityMonitor {
    
    private static ?PDO $db = null;
    
    // Patterns suspects dans les paramètres
    const SQL_INJECTION_PATTERNS = [
        '/\bunion\b.*\bselect\b/i',
        '/\bselect\b.*\bfrom\b.*\bwhere\b/i',
        '/\bdrop\b.*\btable\b/i',
        '/\binsert\b.*\binto\b/i',
        '/\bupdate\b.*\bset\b/i',
        '/\bdelete\b.*\bfrom\b/i',
        '/\bexec(\s|\()/i',
        '/\bscript\b.*\b\/script\b/i',
        '/\b(char|nchar|varchar|nvarchar)\s*\(/i',
        '/\bconvert\s*\(/i',
        '/\bcast\s*\(/i',
        '/\bwaitfor\s+delay\b/i',
        '/\bsleep\s*\(/i',
        '/\bbenchmark\s*\(/i',
        '/\;.*\-\-/i',
        '/\bor\b.*\=.*\bor\b/i',
        '/\b1\s*\=\s*1\b/i',
        '/\b\'\s*or\s*\'\s*\=\s*\'/i'
    ];
    
    // Patterns de scan/exploration
    const SCAN_PATTERNS = [
        'admin', 'administrator', 'wp-admin', 'phpmyadmin',
        '.git', '.env', 'config.php', 'wp-config.php',
        'backup', '.sql', '.bak', '.zip', '.tar',
        'shell.php', 'c99.php', 'r57.php', 'eval.php'
    ];
    
    /**
     * Vérifier les tentatives de brute force
     */
    public static function checkBruteForce(string $ip, string $username = null): bool {
        $db = self::getDb();
        
        // Compter les tentatives récentes depuis cette IP
        $stmt = $db->prepare('
            SELECT COUNT(*) as attempts,
                   COUNT(DISTINCT username) as unique_users,
                   MIN(attempt_time) as first_attempt,
                   MAX(attempt_time) as last_attempt
            FROM sec_failed_login_attempts 
            WHERE ip_address = :ip 
              AND attempt_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
        ');
        
        $stmt->execute(['ip' => $ip]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        $attempts = (int)$result['attempts'];
        $uniqueUsers = (int)$result['unique_users'];
        
        // Critères de détection
        $isBruteForce = false;
        $reason = '';

        if ($attempts >= 8) {
            $isBruteForce = true;
            $reason = "Plus de 8 tentatives en 5 minutes";
        }
        
        if ($isBruteForce) {
            // Déclencher l'alerte
            AlertService::trigger('BRUTE_FORCE', [
                'ip' => $ip,
                'username' => $username,
                'attempts' => $attempts,
                'unique_usernames' => $uniqueUsers,
                'timeframe' => '5 minutes',
                'reason' => $reason,
                'message' => "Attaque brute force détectée depuis $ip : $reason"
            ], 'SECURITY');
            
            return false; // Bloquer
        }
        
        return true; // Autoriser
    }
    
    /**
     * Enregistrer une tentative de login échouée
     */
    public static function recordFailedLogin(
        string $ip, 
        string $username = null, 
        string $errorType = 'invalid_credentials',
        string $userAgent = null
    ): void {
        $db = self::getDb();
        
        try {
            // Chercher si le username existe (pour stocker la version chiffrée)
            require_once __DIR__ . '/../ApiService.php';
            $encryptedUsername = null;
            if ($username) {
                // Chiffrer le username pour la recherche
                $searchUsername = \ApiService::encryptSearchableData($username);

                $userStmt = $db->prepare('
                    SELECT encrypted_user_name
                    FROM users
                    WHERE encrypted_user_name = :encrypted_username
                    LIMIT 1
                ');
                $userStmt->execute(['encrypted_username' => $searchUsername]);
                $user = $userStmt->fetch(PDO::FETCH_ASSOC);
                if ($user) {
                    $encryptedUsername = $user['encrypted_user_name'];
                }
            }
            
            // Enregistrer la tentative
            $stmt = $db->prepare('
                INSERT INTO sec_failed_login_attempts (
                    username, encrypted_username, ip_address, 
                    user_agent, error_type
                ) VALUES (
                    :username, :encrypted, :ip, :agent, :error
                )
            ');
            
            $stmt->execute([
                'username' => $username,
                'encrypted' => $encryptedUsername,
                'ip' => $ip,
                'agent' => $userAgent,
                'error' => $errorType
            ]);
            
            // Vérifier si on doit bloquer l'IP
            if (!self::checkBruteForce($ip, $username)) {
                // L'IP sera bloquée par AlertService via les actions automatiques
            }
            
        } catch (\Exception $e) {
            error_log("Failed to record login attempt: " . $e->getMessage());
        }
    }
    
    /**
     * Vérifier les patterns d'injection SQL
     */
    public static function checkSQLInjection(string $value): bool {
        // Nettoyer la valeur pour l'analyse
        $decoded = urldecode($value);
        
        foreach (self::SQL_INJECTION_PATTERNS as $pattern) {
            if (preg_match($pattern, $decoded)) {
                // Injection SQL détectée !
                $context = [
                    'pattern_matched' => $pattern,
                    'value' => substr($value, 0, 500), // Limiter la taille
                    'decoded_value' => substr($decoded, 0, 500),
                    'message' => 'Tentative d\'injection SQL détectée'
                ];
                
                // Enrichir avec le contexte de requête
                if (isset($_SERVER['REQUEST_URI'])) {
                    $context['endpoint'] = $_SERVER['REQUEST_URI'];
                    $context['method'] = $_SERVER['REQUEST_METHOD'] ?? 'unknown';
                    $context['ip'] = \AppConfig::getInstance()->getClientIp();
                }
                
                AlertService::trigger('SQL_INJECTION', $context, 'SECURITY');
                
                return false; // Bloquer la requête
            }
        }
        
        return true; // Pas d'injection détectée
    }
    
    /**
     * Vérifier tous les paramètres d'une requête
     */
    public static function checkRequestParameters(array $params): bool {
        $suspicious = [];
        
        foreach ($params as $key => $value) {
            if (is_string($value)) {
                if (!self::checkSQLInjection($value)) {
                    $suspicious[$key] = $value;
                }
            } elseif (is_array($value)) {
                // Vérifier récursivement
                if (!self::checkRequestParameters($value)) {
                    $suspicious[$key] = $value;
                }
            }
        }
        
        if (!empty($suspicious)) {
            // Paramètres suspects détectés
            return false;
        }
        
        return true;
    }
    
    /**
     * Détecter les patterns de scan/exploration
     */
    public static function checkScanPattern(string $uri): bool {
        $lowerUri = strtolower($uri);
        
        foreach (self::SCAN_PATTERNS as $pattern) {
            if (strpos($lowerUri, $pattern) !== false) {
                // Pattern de scan détecté
                AlertService::trigger('SUSPICIOUS_PATTERN', [
                    'pattern' => $pattern,
                    'uri' => $uri,
                    'message' => "Tentative d'accès à un fichier sensible : $pattern"
                ], 'WARNING');
                
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Vérifier les accès non autorisés
     */
    public static function checkUnauthorizedAccess(string $endpoint, bool $hasAuth): bool {
        // Endpoints publics qui n'ont pas besoin d'auth
        $publicEndpoints = [
            '/api/login',
            '/api/users/check-username',
            '/api/logs',
            '/api/health',
            '/api/status'
        ];
        
        // Vérifier si l'endpoint nécessite une auth
        $isPublic = false;
        foreach ($publicEndpoints as $public) {
            if (strpos($endpoint, $public) === 0) {
                $isPublic = true;
                break;
            }
        }
        
        if (!$isPublic && !$hasAuth) {
            // Accès non autorisé
            AlertService::trigger('UNAUTHORIZED_ACCESS', [
                'endpoint' => $endpoint,
                'message' => "Tentative d'accès sans authentification à : $endpoint"
            ], 'WARNING');
            
            return false;
        }
        
        return true;
    }
    
    /**
     * Analyser les patterns 404
     */
    public static function check404Pattern(string $ip): void {
        $db = self::getDb();
        
        // Compter les 404 récents de cette IP
        $stmt = $db->prepare('
            SELECT COUNT(*) as count,
                   COUNT(DISTINCT endpoint) as unique_endpoints
            FROM sec_performance_metrics 
            WHERE ip_address = :ip 
              AND http_status = 404
              AND created_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE)
        ');
        
        $stmt->execute(['ip' => $ip]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        $count404 = (int)$result['count'];
        $uniqueEndpoints = (int)$result['unique_endpoints'];
        
        // Si trop de 404, c'est suspect
        if ($count404 >= 10 || $uniqueEndpoints >= 5) {
            AlertService::trigger('HTTP_404_PATTERN', [
                'ip' => $ip,
                'count_404' => $count404,
                'unique_endpoints' => $uniqueEndpoints,
                'timeframe' => '10 minutes',
                'message' => "Pattern de scan détecté : $count404 erreurs 404 sur $uniqueEndpoints endpoints"
            ], 'WARNING');
        }
    }
    
    /**
     * Vérifier la vitesse des requêtes (rate limiting)
     */
    public static function checkRateLimit(string $ip): bool {
        $db = self::getDb();
        
        // Compter les requêtes de la dernière minute
        $stmt = $db->prepare('
            SELECT COUNT(*) as count 
            FROM sec_performance_metrics 
            WHERE ip_address = :ip 
              AND created_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
        ');
        
        $stmt->execute(['ip' => $ip]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        $requestsPerMinute = (int)$result['count'];
        
        // Limite : 60 requêtes par minute
        if ($requestsPerMinute > 60) {
            AlertService::trigger('RATE_LIMIT_EXCEEDED', [
                'ip' => $ip,
                'requests_per_minute' => $requestsPerMinute,
                'limit' => 60,
                'message' => "Limite de taux dépassée : $requestsPerMinute requêtes/minute"
            ], 'WARNING');
            
            return false;
        }
        
        return true;
    }
    
    /**
     * Analyser une erreur SQL pour détecter les problèmes
     */
    public static function analyzeSQLError(\PDOException $e, string $query = null): void {
        $message = $e->getMessage();
        $code = $e->getCode();
        
        // Classifier l'erreur
        $level = 'ERROR';
        $type = 'SQL_ERROR';
        
        if (strpos($message, 'Unknown column') !== false) {
            $type = 'SQL_ERROR';
            $level = 'ERROR';
        } elseif (strpos($message, 'Table') !== false && strpos($message, 'doesn\'t exist') !== false) {
            $type = 'SQL_ERROR';
            $level = 'CRITICAL';
        } elseif (strpos($message, 'Connection refused') !== false || strpos($message, 'Can\'t connect') !== false) {
            $type = 'DB_CONNECTION';
            $level = 'CRITICAL';
        } elseif (strpos($message, 'Too many connections') !== false) {
            $type = 'DB_CONNECTION';
            $level = 'CRITICAL';
        } elseif (strpos($message, 'Deadlock') !== false) {
            $type = 'DB_DEADLOCK';
            $level = 'ERROR';
        }
        
        // Créer l'alerte
        AlertService::trigger($type, [
            'sql_error' => $message,
            'sql_code' => $code,
            'query' => $query ? substr($query, 0, 500) : null,
            'message' => "Erreur SQL : $message"
        ], $level);
    }
    
    /**
     * Obtenir les statistiques de sécurité
     */
    public static function getSecurityStats(): array {
        $db = self::getDb();
        
        // Statistiques des dernières 24h
        $stats = [];
        
        // Tentatives de login
        $loginStmt = $db->prepare('
            SELECT 
                COUNT(*) as total_attempts,
                COUNT(DISTINCT ip_address) as unique_ips,
                COUNT(DISTINCT username) as unique_usernames
            FROM sec_failed_login_attempts
            WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
        ');
        $loginStmt->execute();
        $stats['failed_logins'] = $loginStmt->fetch(PDO::FETCH_ASSOC);
        
        // IPs bloquées
        $blockedStmt = $db->prepare('
            SELECT COUNT(*) as total,
                   SUM(CASE WHEN permanent = 1 THEN 1 ELSE 0 END) as permanent,
                   SUM(CASE WHEN blocked_until > NOW() THEN 1 ELSE 0 END) as active
            FROM sec_blocked_ips
        ');
        $blockedStmt->execute();
        $stats['blocked_ips'] = $blockedStmt->fetch(PDO::FETCH_ASSOC);
        
        // Alertes par type
        $alertsStmt = $db->prepare('
            SELECT 
                alert_type,
                alert_level,
                COUNT(*) as count,
                MAX(last_seen) as last_seen
            FROM sec_alerts
            WHERE last_seen >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
            GROUP BY alert_type, alert_level
            ORDER BY count DESC
        ');
        $alertsStmt->execute();
        $stats['alerts_by_type'] = $alertsStmt->fetchAll(PDO::FETCH_ASSOC);
        
        return $stats;
    }
    
    /**
     * Obtenir l'instance de base de données
     */
    private static function getDb(): PDO {
        if (self::$db === null) {
            self::$db = Database::getInstance();
        }
        return self::$db;
    }
}