<?php

declare(strict_types=1);

namespace App\Services;

use App\Services\LogService;

require_once __DIR__ . '/LogService.php';

/**
 * Service de sécurité des mots de passe conforme à NIST SP 800-63B
 * Vérifie les mots de passe contre la base de données Have I Been Pwned
 * Utilise l'API k-anonymity pour préserver la confidentialité
 */
class PasswordSecurityService {
    
    private const HIBP_API_URL = 'https://api.pwnedpasswords.com/range/';
    private const MIN_PASSWORD_LENGTH = 8;
    private const MAX_PASSWORD_LENGTH = 64;
    private const REQUEST_TIMEOUT = 5; // secondes
    
    /**
     * Vérifie si un mot de passe a été compromis
     * Utilise l'API Have I Been Pwned avec k-anonymity
     * 
     * @param string $password Le mot de passe à vérifier
     * @return array ['compromised' => bool, 'occurrences' => int, 'error' => string|null]
     */
    public static function checkPasswordCompromised(string $password): array {
        try {
            // Calculer le hash SHA-1 du mot de passe
            $sha1 = strtoupper(sha1($password));
            
            // Extraire les 5 premiers caractères pour k-anonymity
            $prefix = substr($sha1, 0, 5);
            $suffix = substr($sha1, 5);
            
            // Appeler l'API HIBP
            $response = self::callHibpApi($prefix);
            
            if ($response === null) {
                // En cas d'erreur API, on laisse passer le mot de passe
                // pour ne pas bloquer l'utilisateur (fail open)
                return [
                    'compromised' => false,
                    'occurrences' => 0,
                    'error' => 'Impossible de vérifier le mot de passe contre la base de données'
                ];
            }
            
            // Rechercher le suffixe dans la réponse
            $lines = explode("\n", $response);
            foreach ($lines as $line) {
                $line = trim($line);
                if (empty($line)) continue;
                
                [$hashSuffix, $count] = explode(':', $line);
                if ($hashSuffix === $suffix) {
                    LogService::log('Mot de passe compromis détecté', [
                        'level' => 'warning',
                        'occurrences' => intval($count)
                    ]);
                    
                    return [
                        'compromised' => true,
                        'occurrences' => intval($count),
                        'error' => null
                    ];
                }
            }
            
            // Mot de passe non trouvé dans la base de données
            return [
                'compromised' => false,
                'occurrences' => 0,
                'error' => null
            ];
            
        } catch (\Exception $e) {
            LogService::log('Erreur lors de la vérification HIBP', [
                'level' => 'error',
                'error' => $e->getMessage()
            ]);
            
            // En cas d'erreur, on laisse passer (fail open)
            return [
                'compromised' => false,
                'occurrences' => 0,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Appelle l'API Have I Been Pwned
     * 
     * @param string $prefix Les 5 premiers caractères du hash SHA-1
     * @return string|null La réponse de l'API ou null en cas d'erreur
     */
    private static function callHibpApi(string $prefix): ?string {
        $url = self::HIBP_API_URL . $prefix;
        
        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'header' => [
                    'User-Agent: GeoSector-API',
                    'Accept: text/plain'
                ],
                'timeout' => self::REQUEST_TIMEOUT,
                'ignore_errors' => false
            ],
            'ssl' => [
                'verify_peer' => true,
                'verify_peer_name' => true
            ]
        ]);
        
        $response = @file_get_contents($url, false, $context);
        
        if ($response === false) {
            LogService::log('Échec de l\'appel à l\'API HIBP', [
                'level' => 'error',
                'url' => $url
            ]);
            return null;
        }
        
        return $response;
    }
    
    /**
     * Valide un mot de passe selon les critères NIST SP 800-63B
     * NIST recommande d'être très permissif : pas d'obligation de composition
     * 
     * @param string $password Le mot de passe à valider
     * @param bool $checkCompromised Vérifier si le mot de passe est compromis
     * @return array ['valid' => bool, 'errors' => array, 'warnings' => array]
     */
    public static function validatePassword(string $password, bool $checkCompromised = true): array {
        $errors = [];
        $warnings = [];
        
        // Calculer la longueur réelle en tenant compte de l'UTF-8
        $length = mb_strlen($password, 'UTF-8');
        
        // Vérification de la longueur minimale (NIST : minimum 8)
        if ($length < self::MIN_PASSWORD_LENGTH) {
            $errors[] = sprintf('Le mot de passe doit contenir au moins %d caractères', self::MIN_PASSWORD_LENGTH);
        }
        
        // Vérification de la longueur maximale (NIST : maximum 64 minimum)
        if ($length > self::MAX_PASSWORD_LENGTH) {
            $errors[] = sprintf('Le mot de passe ne doit pas dépasser %d caractères', self::MAX_PASSWORD_LENGTH);
        }
        
        // NIST : Les espaces sont acceptés (pas d'erreur, juste un avertissement informatif)
        if ($password !== trim($password)) {
            // C'est juste informatif, pas une erreur selon NIST
            $warnings[] = 'Note : Le mot de passe contient des espaces en début ou fin (c\'est autorisé)';
        }
        
        // Vérification contre les mots de passe compromis (NIST : obligatoire)
        if ($checkCompromised && empty($errors)) {
            $compromisedCheck = self::checkPasswordCompromised($password);
            
            if ($compromisedCheck['compromised']) {
                $errors[] = sprintf(
                    'Ce mot de passe a été trouvé %s fois dans des fuites de données. Veuillez en choisir un autre.',
                    number_format($compromisedCheck['occurrences'], 0, ',', ' ')
                );
            } elseif ($compromisedCheck['error']) {
                $warnings[] = 'Impossible de vérifier si le mot de passe a été compromis';
            }
        }
        
        // Avertissements optionnels (pas des erreurs selon NIST)
        // Ces vérifications sont juste informatives
        if (self::hasSimplePattern($password)) {
            $warnings[] = 'Suggestion : Évitez les motifs répétitifs pour plus de sécurité';
        }
        
        if (self::hasCommonSequence($password)) {
            $warnings[] = 'Suggestion : Évitez les séquences communes pour plus de sécurité';
        }
        
        // NIST : Pas d'obligation de majuscules, minuscules, chiffres ou caractères spéciaux
        // On peut ajouter des suggestions mais PAS d'erreurs
        $hasLower = preg_match('/[a-z]/u', $password);
        $hasUpper = preg_match('/[A-Z]/u', $password);
        $hasDigit = preg_match('/[0-9]/u', $password);
        $hasSpecial = preg_match('/[^a-zA-Z0-9]/u', $password);
        
        $complexity = ($hasLower ? 1 : 0) + ($hasUpper ? 1 : 0) + ($hasDigit ? 1 : 0) + ($hasSpecial ? 1 : 0);
        
        if ($complexity < 2 && $length < 12) {
            $warnings[] = 'Suggestion : Un mot de passe plus long ou plus varié serait plus sécurisé';
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'warnings' => $warnings
        ];
    }
    
    /**
     * Génère un mot de passe sécurisé non compromis
     * 
     * @param int $length Longueur du mot de passe (12-20 caractères)
     * @param int $maxAttempts Nombre maximum de tentatives
     * @return string|null Le mot de passe généré ou null si échec
     */
    public static function generateSecurePassword(int $length = 14, int $maxAttempts = 10): ?string {
        $length = max(12, min(20, $length));
        
        for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
            // Générer un mot de passe aléatoire
            $password = self::generateRandomPassword($length);
            
            // Vérifier s'il est compromis
            $check = self::checkPasswordCompromised($password);
            
            if (!$check['compromised']) {
                return $password;
            }
            
            LogService::log('Mot de passe généré était compromis, nouvelle tentative', [
                'level' => 'info',
                'attempt' => $attempt + 1,
                'occurrences' => $check['occurrences']
            ]);
        }
        
        LogService::log('Impossible de générer un mot de passe non compromis', [
            'level' => 'error',
            'attempts' => $maxAttempts
        ]);
        
        return null;
    }
    
    /**
     * Génère un mot de passe aléatoire
     * 
     * @param int $length Longueur du mot de passe
     * @return string Le mot de passe généré
     */
    private static function generateRandomPassword(int $length): string {
        // Caractères autorisés (sans ambiguïté visuelle)
        $lowercase = 'abcdefghijkmnopqrstuvwxyz'; // sans l
        $uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // sans I, O
        $numbers = '23456789'; // sans 0, 1
        $special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
        
        $password = '';
        
        // Garantir au moins un caractère de chaque type
        $password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
        $password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
        $password .= $numbers[random_int(0, strlen($numbers) - 1)];
        $password .= $special[random_int(0, strlen($special) - 1)];
        
        // Compléter avec des caractères aléatoires
        $allChars = $lowercase . $uppercase . $numbers . $special;
        for ($i = strlen($password); $i < $length; $i++) {
            $password .= $allChars[random_int(0, strlen($allChars) - 1)];
        }
        
        // Mélanger les caractères
        $passwordArray = str_split($password);
        shuffle($passwordArray);
        
        return implode('', $passwordArray);
    }
    
    /**
     * Vérifie si le mot de passe contient des motifs répétitifs simples
     * 
     * @param string $password Le mot de passe à vérifier
     * @return bool True si des motifs répétitifs sont détectés
     */
    private static function hasSimplePattern(string $password): bool {
        $lowPassword = strtolower($password);
        
        // Vérifier les caractères répétés (aaa, 111, etc.)
        if (preg_match('/(.)\1{2,}/', $lowPassword)) {
            return true;
        }
        
        // Vérifier les motifs répétés (ababab, 121212, etc.)
        if (preg_match('/(.{2,})\1{2,}/', $lowPassword)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Vérifie si le mot de passe contient des séquences communes
     * 
     * @param string $password Le mot de passe à vérifier
     * @return bool True si des séquences communes sont détectées
     */
    private static function hasCommonSequence(string $password): bool {
        $lowPassword = strtolower($password);
        
        $commonSequences = [
            '123', '234', '345', '456', '567', '678', '789',
            'abc', 'bcd', 'cde', 'def', 'efg', 'fgh',
            'qwerty', 'azerty', 'qwertz',
            'password', 'motdepasse', 'admin', 'user'
        ];
        
        foreach ($commonSequences as $sequence) {
            if (stripos($lowPassword, $sequence) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Estime la force d'un mot de passe selon l'approche NIST
     * NIST privilégie la longueur sur la complexité
     * 
     * @param string $password Le mot de passe à évaluer
     * @return array ['score' => int (0-100), 'strength' => string, 'feedback' => array]
     */
    public static function estimatePasswordStrength(string $password): array {
        $score = 0;
        $feedback = [];
        
        // Longueur (NIST : facteur le plus important)
        $length = mb_strlen($password, 'UTF-8');
        if ($length >= 8) $score += 20;   // Minimum requis
        if ($length >= 12) $score += 20;  // Bon
        if ($length >= 16) $score += 20;  // Très bon
        if ($length >= 20) $score += 15;  // Excellent
        if ($length >= 30) $score += 10;  // Exceptionnel
        
        // Diversité des caractères (bonus, pas obligatoire selon NIST)
        $hasLower = preg_match('/[a-z]/u', $password);
        $hasUpper = preg_match('/[A-Z]/u', $password);
        $hasDigit = preg_match('/[0-9]/u', $password);
        $hasSpecial = preg_match('/[^a-zA-Z0-9]/u', $password);
        
        $diversity = ($hasLower ? 1 : 0) + ($hasUpper ? 1 : 0) + ($hasDigit ? 1 : 0) + ($hasSpecial ? 1 : 0);
        
        // Bonus pour la diversité (mais pas de pénalité si absent)
        if ($diversity >= 4) {
            $score += 15;
        } elseif ($diversity >= 3) {
            $score += 10;
        } elseif ($diversity >= 2) {
            $score += 5;
        }
        
        // Suggestions constructives (pas de pénalités selon NIST)
        if ($length < 12) {
            $feedback[] = 'Suggestion : Un mot de passe plus long est plus sécurisé';
        }
        
        if ($diversity < 2 && $length < 16) {
            $feedback[] = 'Suggestion : Variez les types de caractères ou augmentez la longueur';
        }
        
        // Pénalités légères pour les mauvaises pratiques évidentes
        if (self::hasSimplePattern($password)) {
            $score = max(0, $score - 10);
            $feedback[] = 'Attention : Motifs répétitifs détectés';
        }
        
        if (self::hasCommonSequence($password)) {
            $score = max(0, $score - 10);
            $feedback[] = 'Attention : Séquences communes détectées';
        }
        
        // Vérification compromission (critique selon NIST)
        $compromisedCheck = self::checkPasswordCompromised($password);
        if ($compromisedCheck['compromised']) {
            $score = min($score, 10); // Score très bas si compromis
            $feedback[] = sprintf(
                'CRITIQUE : Mot de passe trouvé %s fois dans des fuites de données',
                number_format($compromisedCheck['occurrences'], 0, ',', ' ')
            );
        }
        
        // Déterminer la force basée principalement sur la longueur (approche NIST)
        $strength = 'Très faible';
        if ($compromisedCheck['compromised']) {
            $strength = 'Compromis';
        } elseif ($length >= 20) {
            $strength = 'Très fort';
        } elseif ($length >= 16) {
            $strength = 'Fort';
        } elseif ($length >= 12) {
            $strength = 'Bon';
        } elseif ($length >= 8) {
            $strength = 'Acceptable';
        } else {
            $strength = 'Trop court';
        }
        
        return [
            'score' => max(0, min(100, $score)),
            'strength' => $strength,
            'feedback' => $feedback,
            'length' => $length,
            'diversity' => $diversity
        ];
    }
}