<?php
declare(strict_types=1);

namespace App\Services;

use RuntimeException;
use ZipArchive;

/** PLATFORM-023.3.4: routes catalogue contrôlées avec RouteAuthorization. */
final class SecurityComplianceService
{
    /** @return array{ok:bool,score:int,categories:array<string,array{score:int,status:string,issues:list<string>}>,critical:list<string>,warnings:list<string>,files_scanned:int} */
    public function auditZip(string $zipPath): array
    {
        if (!is_file($zipPath)) {
            throw new RuntimeException('Package introuvable pour l’audit de conformité.');
        }
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('Package illisible pour l’audit de conformité.');
        }
        $files = [];
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = str_replace('\\', '/', (string)$zip->getNameIndex($i));
            if (!str_starts_with($name, 'payload/') || str_ends_with($name, '/')) {
                continue;
            }
            $content = $zip->getFromIndex($i);
            if ($content === false) {
                $zip->close();
                throw new RuntimeException('Impossible de lire le fichier ' . $name . ' pendant l’audit.');
            }
            $files[substr($name, 8)] = $content;
        }
        $zip->close();
        return $this->auditContents($files);
    }

    /** @return array{ok:bool,score:int,categories:array<string,array{score:int,status:string,issues:list<string>}>,critical:list<string>,warnings:list<string>,files_scanned:int} */
    public function auditDirectory(string $directory): array
    {
        $root = realpath($directory);
        if ($root === false || !is_dir($root)) {
            throw new RuntimeException('Dossier absent pour l’audit de conformité.');
        }
        $files = [];
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS));
        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;
            $relative = str_replace('\\', '/', substr($file->getPathname(), strlen($root) + 1));
            $content = file_get_contents($file->getPathname());
            if ($content !== false) $files[$relative] = $content;
        }
        return $this->auditContents($files);
    }

    /** @param array<string,string> $files */
    private function auditContents(array $files): array
    {
        $categories = [
            'routes_permissions' => [],
            'isolation_donnees' => [],
            'sql_transactions' => [],
            'televersements' => [],
            'authentification_sessions' => [],
            'secrets_configuration' => [],
            'execution_dangereuse' => [],
            'integrite_package' => [],
            'audit_offensif' => [],
        ];
        $critical = [];
        $warnings = [];

        foreach ($files as $path => $source) {
            $normalized = strtolower($path);
            if (preg_match('~(^|/)(\.env|id_rsa|id_ed25519|.*\.pem|.*\.p12|.*\.key)$~i', $path)) {
                $this->critical($categories, $critical, 'secrets_configuration', "$path : secret ou clé privée interdit dans le payload.");
            }
            if (!in_array($path, ['app/Services/SecurityComplianceService.php', 'tools/build_signed_update.php', 'tools/audit_secrets.php', 'tools/generate_master_key.php'], true)
                && preg_match('/BEGIN (RSA |OPENSSH |EC )?PRIVATE KEY|UPDATE_SIGNING_SECRET|DB_PASSWORD\s*=|APP_KEY\s*=|AXIOM_MASTER_KEY_BASE64\s*=|SMTP_PASSWORD\s*=|CLIENT_SECRET\s*=/i', $source)) {
                $this->critical($categories, $critical, 'secrets_configuration', "$path : contenu ressemblant à un secret privé.");
            }
            if (preg_match('~(^|/)(secrets\.vault|\.env(?:\..*)?|.*\.(?:pfx|kdbx))$~i', $path)) {
                $this->critical($categories, $critical, 'secrets_configuration', "$path : coffre ou fichier de secrets interdit dans un package.");
            }
            $trustedCliTool = in_array($path, ['tools/build_signed_update.php', 'tools/audit_package_compliance.php'], true)
                && str_contains($source, "PHP_SAPI !== 'cli'");
            if (!$trustedCliTool && $path !== 'app/Services/SecurityComplianceService.php'
                && preg_match('/\b(eval|assert)\s*\(|\bshell_exec\s*\(|\bpassthru\s*\(|\bproc_open\s*\(|\bpopen\s*\(/i', $source)) {
                $this->critical($categories, $critical, 'execution_dangereuse', "$path : primitive d’exécution dynamique interdite.");
            }
            if (preg_match('/\bunserialize\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/i', $source)) {
                $this->critical($categories, $critical, 'execution_dangereuse', "$path : désérialisation de données HTTP interdite.");
            }
            if (preg_match('/\b(include|require)(?:_once)?\s*\(?\s*\$_(GET|POST|REQUEST|COOKIE)/i', $source)) {
                $this->critical($categories, $critical, 'audit_offensif', "$path : inclusion de fichier pilotée par une entrée HTTP.");
            }
            if (preg_match('/header\s*\(\s*["\']Location:\s*["\']\s*\.\s*\$_(GET|POST|REQUEST)/i', $source)) {
                $this->critical($categories, $critical, 'audit_offensif', "$path : redirection ouverte probable.");
            }
            if (preg_match('/\b(file_get_contents|curl_init|fopen)\s*\(\s*\$_(GET|POST|REQUEST)/i', $source)) {
                $this->critical($categories, $critical, 'audit_offensif', "$path : accès réseau ou fichier piloté directement par HTTP.");
            }
            if (preg_match('/echo\s+\$_(GET|POST|REQUEST|COOKIE)|<\?=\s*\$_(GET|POST|REQUEST|COOKIE)/i', $source)) {
                $this->critical($categories, $critical, 'audit_offensif', "$path : sortie HTTP non échappée probable.");
            }
            if (preg_match('/(SELECT|UPDATE|DELETE|INSERT)[^;\n]{0,220}\$_(GET|POST|REQUEST|COOKIE)/i', $source)
                || (preg_match('/->(?:query|exec)\s*\(\s*\$[a-z_][a-z0-9_]*\s*\)/i', $source)
                    && !str_contains($source, '@sql-safe-dynamic'))) {
                $this->critical($categories, $critical, 'sql_transactions', "$path : SQL dynamique alimenté par une variable à contrôler.");
            }
            if (preg_match('/\b(DROP\s+DATABASE|CREATE\s+USER|GRANT\s+|REVOKE\s+|SET\s+GLOBAL|LOAD\s+DATA|INTO\s+OUTFILE)\b/i', $source)) {
                $this->critical($categories, $critical, 'sql_transactions', "$path : instruction SQL administrative interdite.");
            }
            if (preg_match('/move_uploaded_file\s*\(|\$_FILES\s*\[/i', $source)
                && !str_contains($source, 'SecureFileService')) {
                $this->critical($categories, $critical, 'televersements', "$path : téléversement hors SecureFileService.");
            }
            if (preg_match('/\breadfile\s*\(|\bfile_get_contents\s*\(\s*\$_(GET|POST|REQUEST)/i', $source)
                && !str_contains($source, 'realpath')) {
                $this->warning($categories, $warnings, 'televersements', "$path : lecture de fichier sans preuve de confinement realpath.");
            }
            if (str_contains($normalized, 'controller') && preg_match('/public function (store|update|delete|archive|destroy|save|validate|approve|reject)\s*\(/i', $source)
                && !str_contains($source, 'Permission::require') && !str_contains($source, 'Permission::can')) {
                $this->warning($categories, $warnings, 'routes_permissions', "$path : action d’écriture sans contrôle métier visible dans le contrôleur.");
            }
            if ((str_contains($normalized, 'model') || str_contains($normalized, 'repository'))
                && preg_match('/\b(SELECT|UPDATE|DELETE)\b/i', $source)
                && str_contains($source, 'organisation_id') === false
                && str_contains($source, 'TenantContext') === false
                && str_contains($source, 'OrganizationGuard') === false) {
                $this->warning($categories, $warnings, 'isolation_donnees', "$path : accès aux données sans marqueur explicite d’isolation organisationnelle.");
            }
            if (preg_match('/session_start\s*\(|session_id\s*\(|setcookie\s*\(/i', $source)
                && $path !== 'public/index.php'
                && !str_contains($normalized, 'securitykernel') && !str_contains($normalized, 'auth')) {
                $this->warning($categories, $warnings, 'authentification_sessions', "$path : manipulation directe de session ou cookie hors noyau de sécurité.");
            }
        }

        if (isset($files['routes/web.php']) && !isset($files['app/Security/RouteAuthorization.php'])) {
            $this->critical($categories, $critical, 'routes_permissions', 'routes/web.php est modifié sans mise à jour explicite de RouteAuthorization.php.');
        }
        if (isset($files['app/Core/Router.php']) && !isset($files['app/Security/RouteAuthorization.php'])) {
            $this->warning($categories, $warnings, 'routes_permissions', 'Le routeur est modifié sans politique d’autorisation associée dans le package.');
        }
        if (isset($files['app/Services/UpdatePackageService.php']) && !isset($files['app/Services/SecurityComplianceService.php'])) {
            $this->critical($categories, $critical, 'integrite_package', 'Le moteur d’installation est modifié sans le moteur de conformité.');
        }

        $categoryResults = [];
        foreach ($categories as $name => $issues) {
            $criticalCount = count(array_filter($issues, static fn(array $i): bool => $i['critical']));
            $warningCount = count($issues) - $criticalCount;
            $score = max(0, 100 - ($criticalCount * 45) - ($warningCount * 10));
            $categoryResults[$name] = [
                'score' => $score,
                'status' => $criticalCount > 0 ? 'critical' : ($warningCount > 0 ? 'warning' : 'ok'),
                'issues' => array_map(static fn(array $i): string => $i['message'], $issues),
            ];
        }
        $score = (int)round(array_sum(array_column($categoryResults, 'score')) / max(1, count($categoryResults)));
        return [
            'ok' => $critical === [],
            'score' => $score,
            'categories' => $categoryResults,
            'critical' => $critical,
            'warnings' => $warnings,
            'files_scanned' => count($files),
        ];
    }

    private function critical(array &$categories, array &$critical, string $category, string $message): void
    {
        $categories[$category][] = ['critical' => true, 'message' => $message];
        $critical[] = $message;
    }

    private function warning(array &$categories, array &$warnings, string $category, string $message): void
    {
        $categories[$category][] = ['critical' => false, 'message' => $message];
        $warnings[] = $message;
    }
}
