<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use ZipArchive;

final class RestoreService
{
    public function __construct(
        private readonly BackupService $backups = new BackupService()
    ) {
    }

    public function restore(string $fileName): array
    {
        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));

        return match ($extension) {
            'sql' => $this->restoreSqlBackup($fileName),
            'zip' => $this->restoreCompleteBackup($fileName),
            default => throw new \RuntimeException('Format de sauvegarde non pris en charge.'),
        };
    }

    private function restoreSqlBackup(string $fileName): array
    {
        $filePath = $this->backups->getDatabaseBackupPath($fileName);
        $sql = $this->readAndValidateSqlFile($filePath);
        $safetyBackup = $this->backups->createDatabaseBackup();
        $executedStatements = $this->executeSql($sql, $safetyBackup['file_name']);

        return [
            'restored_file' => $fileName,
            'restored_type' => 'database',
            'executed_statements' => $executedStatements,
            'restored_files' => 0,
            'safety_backup' => $safetyBackup['file_name'],
            'restored_at' => date('Y-m-d H:i:s'),
        ];
    }

    private function restoreCompleteBackup(string $fileName): array
    {
        if (!class_exists(ZipArchive::class)) {
            throw new \RuntimeException('L’extension PHP ZIP n’est pas disponible.');
        }

        $inspection = $this->backups->inspectBackup($fileName);
        if (!($inspection['valid'] ?? false)) {
            throw new \RuntimeException(
                'Restauration refusée : ' . (string) ($inspection['message'] ?? 'archive invalide')
            );
        }

        $archivePath = $this->backups->getDatabaseBackupPath($fileName);
        $temporaryDirectory = $this->temporaryRestoreDirectory();
        $this->removeDirectory($temporaryDirectory);

        if (!mkdir($temporaryDirectory, 0775, true) && !is_dir($temporaryDirectory)) {
            throw new \RuntimeException('Impossible de créer le dossier temporaire de restauration.');
        }

        $zip = new ZipArchive();
        if ($zip->open($archivePath) !== true) {
            $this->removeDirectory($temporaryDirectory);
            throw new \RuntimeException('Impossible d’ouvrir l’archive de sauvegarde.');
        }

        try {
            $this->assertSafeArchiveEntries($zip);
            if (!$zip->extractTo($temporaryDirectory)) {
                throw new \RuntimeException('Impossible d’extraire l’archive de sauvegarde.');
            }
        } finally {
            $zip->close();
        }

        $sqlPath = $temporaryDirectory . DIRECTORY_SEPARATOR . 'database.sql';
        $sql = $this->readAndValidateSqlFile($sqlPath);
        $safetyBackup = $this->backups->createDatabaseBackup();

        try {
            $executedStatements = $this->executeSql($sql, $safetyBackup['file_name']);
            $restoredFiles = $this->restoreWhitelistedFiles($temporaryDirectory);
        } catch (\Throwable $exception) {
            $this->removeDirectory($temporaryDirectory);
            throw $exception;
        }

        $this->removeDirectory($temporaryDirectory);

        return [
            'restored_file' => $fileName,
            'restored_type' => 'complete',
            'executed_statements' => $executedStatements,
            'restored_files' => $restoredFiles,
            'safety_backup' => $safetyBackup['file_name'],
            'restored_at' => date('Y-m-d H:i:s'),
        ];
    }

    private function readAndValidateSqlFile(string $filePath): string
    {
        if (!is_readable($filePath)) {
            throw new \RuntimeException('Le fichier SQL n’est pas accessible en lecture.');
        }

        $sql = file_get_contents($filePath);
        if (!is_string($sql) || trim($sql) === '') {
            throw new \RuntimeException('Le fichier SQL est vide ou illisible.');
        }

        if (stripos($sql, 'CREATE TABLE') === false
            || stripos($sql, 'SET FOREIGN_KEY_CHECKS') === false) {
            throw new \RuntimeException('Le fichier ne semble pas être une sauvegarde Darlink ERP valide.');
        }

        return $sql;
    }

    private function executeSql(string $sql, string $safetyBackupName): int
    {
        $pdo = Database::connection();
        $statements = $this->splitSqlStatements($sql);
        if ($statements === []) {
            throw new \RuntimeException('Aucune instruction SQL exploitable n’a été trouvée.');
        }

        $executed = 0;
        try {
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
            foreach ($statements as $statement) {
                $statement = trim($statement);
                if ($statement === '') {
                    continue;
                }
                $pdo->exec($statement);
                $executed++;
            }
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        } catch (\Throwable $exception) {
            try {
                $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
            } catch (\Throwable) {
            }

            throw new \RuntimeException(
                'La restauration a échoué après ' . $executed
                . ' instruction(s). Sauvegarde de sécurité : ' . $safetyBackupName
                . '. Erreur : ' . $exception->getMessage()
            );
        }

        return $executed;
    }

    private function restoreWhitelistedFiles(string $temporaryDirectory): int
    {
        $rootPath = dirname(__DIR__, 2);
        $restoredFiles = 0;

        foreach ([
            'public/assets/img',
            'public/uploads',
            'storage/documents',
            'storage/imports',
        ] as $relativePath) {
            $source = $temporaryDirectory . DIRECTORY_SEPARATOR
                . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
            if (!is_dir($source)) {
                continue;
            }

            $destination = $rootPath . DIRECTORY_SEPARATOR
                . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
            $restoredFiles += $this->copyDirectory($source, $destination);
        }

        return $restoredFiles;
    }

    private function assertSafeArchiveEntries(ZipArchive $zip): void
    {
        for ($index = 0; $index < $zip->numFiles; $index++) {
            $name = (string) $zip->getNameIndex($index);
            $normalized = str_replace('\\', '/', $name);
            if ($normalized === ''
                || str_starts_with($normalized, '/')
                || preg_match('#(^|/)\.\.(/|$)#', $normalized)
                || preg_match('/^[A-Za-z]:\//', $normalized)) {
                throw new \RuntimeException('Archive refusée : chemin non sécurisé détecté.');
            }
        }
    }

    private function copyDirectory(string $source, string $destination): int
    {
        if (!is_dir($destination)
            && !mkdir($destination, 0775, true)
            && !is_dir($destination)) {
            throw new \RuntimeException('Impossible de créer le dossier : ' . $destination);
        }

        $count = 0;
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relative = substr($item->getPathname(), strlen($source) + 1);
            $target = $destination . DIRECTORY_SEPARATOR . $relative;
            if ($item->isDir()) {
                if (!is_dir($target) && !mkdir($target, 0775, true) && !is_dir($target)) {
                    throw new \RuntimeException('Impossible de créer le dossier restauré : ' . $target);
                }
            } elseif ($item->isFile()) {
                $parent = dirname($target);
                if (!is_dir($parent) && !mkdir($parent, 0775, true) && !is_dir($parent)) {
                    throw new \RuntimeException('Impossible de créer le dossier restauré : ' . $parent);
                }
                if (!copy($item->getPathname(), $target)) {
                    throw new \RuntimeException('Impossible de restaurer le fichier : ' . $relative);
                }
                $count++;
            }
        }

        return $count;
    }

    /** @return array<int,string> */
    private function splitSqlStatements(string $sql): array
    {
        $statements = [];
        $buffer = '';
        $length = strlen($sql);
        $inSingle = false;
        $inDouble = false;
        $inBacktick = false;
        $escaped = false;
        $lineComment = false;
        $blockComment = false;

        for ($index = 0; $index < $length; $index++) {
            $character = $sql[$index];
            $next = $index + 1 < $length ? $sql[$index + 1] : '';

            if ($lineComment) {
                if ($character === "\n") {
                    $lineComment = false;
                    $buffer .= $character;
                }
                continue;
            }
            if ($blockComment) {
                if ($character === '*' && $next === '/') {
                    $blockComment = false;
                    $index++;
                }
                continue;
            }
            if (!$inSingle && !$inDouble && !$inBacktick && $character === '-' && $next === '-') {
                $lineComment = true;
                $index++;
                continue;
            }
            if (!$inSingle && !$inDouble && !$inBacktick && $character === '#') {
                $lineComment = true;
                continue;
            }
            if (!$inSingle && !$inDouble && !$inBacktick && $character === '/' && $next === '*') {
                $blockComment = true;
                $index++;
                continue;
            }

            $buffer .= $character;
            if ($escaped) {
                $escaped = false;
                continue;
            }
            if (($inSingle || $inDouble) && $character === '\\') {
                $escaped = true;
                continue;
            }
            if (!$inDouble && !$inBacktick && $character === "'") {
                $inSingle = !$inSingle;
                continue;
            }
            if (!$inSingle && !$inBacktick && $character === '"') {
                $inDouble = !$inDouble;
                continue;
            }
            if (!$inSingle && !$inDouble && $character === '`') {
                $inBacktick = !$inBacktick;
                continue;
            }
            if ($character === ';' && !$inSingle && !$inDouble && !$inBacktick) {
                $statement = trim(substr($buffer, 0, -1));
                if ($statement !== '') {
                    $statements[] = $statement;
                }
                $buffer = '';
            }
        }

        $remaining = trim($buffer);
        if ($remaining !== '') {
            $statements[] = $remaining;
        }

        return $statements;
    }

    private function temporaryRestoreDirectory(): string
    {
        return $this->backups->backupDirectoryPath()
            . DIRECTORY_SEPARATOR . '.restore_' . bin2hex(random_bytes(6));
    }

    private function removeDirectory(string $directory): void
    {
        if (!is_dir($directory)) {
            return;
        }
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($iterator as $item) {
            $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
        }
        @rmdir($directory);
    }
}
