<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use PDO;
use ZipArchive;

final class BackupService
{
    public const VERSION = \App\Core\Version::NUMBER;
    public const BUILD = 'P3';

    public function createDatabaseBackup(): array
    {
        $pdo = Database::connection();
        $this->ensureBackupDirectoryExists();

        $databaseName = (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
        if ($databaseName === '') {
            throw new \RuntimeException('Impossible de déterminer le nom de la base de données.');
        }

        $createdAt = date('Y-m-d H:i:s');
        $createdBy = Auth::name() ?: 'Utilisateur';
        $fileName = sprintf(
            '%s_%s.sql',
            preg_replace('/[^a-zA-Z0-9_-]/', '_', $databaseName),
            date('Y-m-d_H-i-s')
        );
        $filePath = $this->backupDirectory() . DIRECTORY_SEPARATOR . $fileName;
        $handle = fopen($filePath, 'wb');

        if ($handle === false) {
            throw new \RuntimeException('Impossible de créer le fichier de sauvegarde.');
        }

        try {
            fwrite($handle, "-- Darlink ERP Phoenix\n");
            fwrite($handle, "-- Version : " . self::VERSION . "\n");
            fwrite($handle, "-- Utilisateur : {$createdBy}\n");
            fwrite($handle, "-- Sauvegarde de la base : {$databaseName}\n");
            fwrite($handle, "-- Date : {$createdAt}\n\n");
            fwrite($handle, "SET FOREIGN_KEY_CHECKS = 0;\n");
            fwrite($handle, "SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';\n");
            fwrite($handle, "SET NAMES utf8mb4;\n\n");

            $tables = $pdo
                ->query('SHOW FULL TABLES WHERE Table_type = "BASE TABLE"')
                ->fetchAll(PDO::FETCH_NUM);

            foreach ($tables as $tableRow) {
                $table = (string) $tableRow[0];
                $escapedTable = str_replace('`', '``', $table);
                $createRow = $pdo
                    ->query("SHOW CREATE TABLE `{$escapedTable}`")
                    ->fetch(PDO::FETCH_NUM);

                if (!$createRow || !isset($createRow[1])) {
                    throw new \RuntimeException('Impossible de lire la structure de la table ' . $table . '.');
                }

                fwrite($handle, "\n-- Structure de la table `{$escapedTable}`\n\n");
                fwrite($handle, "DROP TABLE IF EXISTS `{$escapedTable}`;\n");
                fwrite($handle, $createRow[1] . ";\n\n");

                $dataStatement = $pdo->query("SELECT * FROM `{$escapedTable}`");
                $columnCount = $dataStatement->columnCount();

                while ($row = $dataStatement->fetch(PDO::FETCH_NUM)) {
                    $values = [];
                    for ($index = 0; $index < $columnCount; $index++) {
                        $value = $row[$index];
                        $values[] = $value === null ? 'NULL' : $pdo->quote((string) $value);
                    }

                    fwrite(
                        $handle,
                        "INSERT INTO `{$escapedTable}` VALUES (" . implode(', ', $values) . ");\n"
                    );
                }
            }

            fwrite($handle, "\nSET FOREIGN_KEY_CHECKS = 1;\n");
        } catch (\Throwable $exception) {
            fclose($handle);
            if (is_file($filePath)) {
                @unlink($filePath);
            }
            throw $exception;
        }

        fclose($handle);
        $fileSize = filesize($filePath);

        return [
            'file_name' => $fileName,
            'file_path' => $filePath,
            'file_size' => $fileSize !== false ? $fileSize : 0,
            'created_at' => $createdAt,
            'type' => 'database',
            'version' => self::VERSION,
            'build' => self::BUILD,
            'created_by' => $createdBy,
            'file_count' => 1,
            'integrity' => 'valid',
        ];
    }

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

        $this->ensureBackupDirectoryExists();
        $rootPath = dirname(__DIR__, 2);
        $databaseBackup = $this->createDatabaseBackup();
        $createdAt = date('Y-m-d H:i:s');
        $createdBy = Auth::name() ?: 'Utilisateur';
        $archiveName = 'Darlink_Backup_' . date('Y-m-d_H-i-s') . '.zip';
        $archivePath = $this->backupDirectory() . DIRECTORY_SEPARATOR . $archiveName;
        $zip = new ZipArchive();

        if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            @unlink($databaseBackup['file_path']);
            throw new \RuntimeException('Impossible de créer l’archive ZIP.');
        }

        $fileCount = 0;

        try {
            if (!$zip->addFile($databaseBackup['file_path'], 'database.sql')) {
                throw new \RuntimeException('Impossible d’ajouter la base à l’archive.');
            }
            $fileCount++;

            foreach ([
                'public/assets/img',
                'public/uploads',
                'storage/documents',
                'storage/imports',
            ] as $relativePath) {
                $absolutePath = $rootPath . DIRECTORY_SEPARATOR
                    . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);

                if (is_dir($absolutePath)) {
                    $fileCount += $this->addDirectoryToZip($zip, $absolutePath, $relativePath);
                }
            }

            $pdo = Database::connection();
            $manifest = [
                'application' => 'Darlink ERP',
                'edition' => 'Phoenix',
                'version' => self::VERSION,
                'build' => self::BUILD,
                'type' => 'complete',
                'database' => (string) $pdo->query('SELECT DATABASE()')->fetchColumn(),
                'created_at' => $createdAt,
                'created_by' => $createdBy,
                'php_version' => PHP_VERSION,
                'mysql_version' => (string) $pdo->query('SELECT VERSION()')->fetchColumn(),
                'file_count' => $fileCount,
                'database_sha256' => hash_file('sha256', $databaseBackup['file_path']) ?: '',
                'included_paths' => [
                    'public/assets/img',
                    'public/uploads',
                    'storage/documents',
                    'storage/imports',
                ],
            ];

            $manifestJson = json_encode(
                $manifest,
                JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            );

            if ($manifestJson === false || !$zip->addFromString('manifest.json', $manifestJson)) {
                throw new \RuntimeException('Impossible de générer le manifeste.');
            }

            $versionText = "Darlink ERP Phoenix\n"
                . "Version : " . self::VERSION . "\n"
                . "Build : " . self::BUILD . "\n"
                . "Type : Sauvegarde complète\n"
                . "Date : {$createdAt}\n"
                . "Utilisateur : {$createdBy}\n";

            if (!$zip->addFromString('version.txt', $versionText)) {
                throw new \RuntimeException('Impossible d’ajouter le fichier de version.');
            }

            if (!$zip->close()) {
                throw new \RuntimeException('Impossible de finaliser l’archive ZIP.');
            }
        } catch (\Throwable $exception) {
            try {
                $zip->close();
            } catch (\Throwable) {
            }
            if (is_file($archivePath)) {
                @unlink($archivePath);
            }
            @unlink($databaseBackup['file_path']);
            throw $exception;
        }

        @unlink($databaseBackup['file_path']);
        $fileSize = filesize($archivePath);

        return [
            'file_name' => $archiveName,
            'file_path' => $archivePath,
            'file_size' => $fileSize !== false ? $fileSize : 0,
            'created_at' => $createdAt,
            'type' => 'complete',
            'version' => self::VERSION,
            'build' => self::BUILD,
            'created_by' => $createdBy,
            'file_count' => $fileCount,
            'integrity' => 'valid',
        ];
    }

    /** @return array<int,array<string,mixed>> */
    public function listBackups(): array
    {
        if (!is_dir($this->backupDirectory())) {
            return [];
        }

        $files = array_merge(
            glob($this->backupDirectory() . DIRECTORY_SEPARATOR . '*.sql') ?: [],
            glob($this->backupDirectory() . DIRECTORY_SEPARATOR . '*.zip') ?: []
        );
        $backups = [];

        foreach ($files as $filePath) {
            if (!is_file($filePath)) {
                continue;
            }

            $fileName = basename($filePath);
            $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
            $modifiedAt = filemtime($filePath);
            $inspection = $this->inspectBackup($fileName);

            $backups[] = [
                'file_name' => $fileName,
                'file_size' => filesize($filePath) ?: 0,
                'created_at' => $inspection['created_at']
                    ?? ($modifiedAt !== false ? date('Y-m-d H:i:s', $modifiedAt) : null),
                'type' => $extension === 'zip' ? 'complete' : 'database',
                'version' => $inspection['version'] ?? ($extension === 'zip' ? self::VERSION : 'Historique'),
                'build' => $inspection['build'] ?? '',
                'created_by' => $inspection['created_by'] ?? '',
                'file_count' => $inspection['file_count'] ?? ($extension === 'sql' ? 1 : null),
                'integrity' => $inspection['valid'] ? 'valid' : 'invalid',
                'integrity_message' => $inspection['message'],
                'version_state' => ($inspection['version'] ?? '') === self::VERSION
                    ? 'current'
                    : 'legacy',
            ];
        }

        usort($backups, static fn(array $a, array $b): int =>
            strcmp((string) $b['created_at'], (string) $a['created_at'])
        );

        return $backups;
    }

    /**
     * @return array<string,mixed>
     */
    public function backupMetrics(): array
    {
        $backups = $this->listBackups();

        $sqlCount = 0;
        $completeCount = 0;
        $validCount = 0;
        $invalidCount = 0;
        $totalSize = 0;

        foreach ($backups as $backup) {
            $totalSize += (int)($backup['file_size'] ?? 0);

            if (($backup['type'] ?? '') === 'complete') {
                $completeCount++;
            } else {
                $sqlCount++;
            }

            if (($backup['integrity'] ?? '') === 'valid') {
                $validCount++;
            } else {
                $invalidCount++;
            }
        }

        return [
            'total_count' => count($backups),
            'sql_count' => $sqlCount,
            'complete_count' => $completeCount,
            'valid_count' => $validCount,
            'invalid_count' => $invalidCount,
            'total_size' => $totalSize,
            'latest' => $backups[0] ?? null,
        ];
    }

    public function listDatabaseBackups(): array
    {
        return $this->listBackups();
    }

    public function backupStatus(): array
    {
        $backups = $this->listBackups();
        $validBackups = array_values(array_filter(
            $backups,
            static fn(array $backup): bool => ($backup['integrity'] ?? '') === 'valid'
        ));
        $latest = $validBackups[0] ?? null;

        if ($latest === null || empty($latest['created_at'])) {
            return [
                'status' => 'danger',
                'label' => 'Aucune sauvegarde valide',
                'message' => 'Créez une sauvegarde complète avant la mise en production.',
                'age_days' => null,
                'latest' => null,
            ];
        }

        $timestamp = strtotime((string) $latest['created_at']);
        $ageDays = $timestamp !== false ? (int) floor((time() - $timestamp) / 86400) : null;

        if ($ageDays !== null && $ageDays <= 7) {
            $status = 'success';
            $label = 'Sauvegardes conformes';
            $message = 'Une sauvegarde valide a été créée récemment.';
        } elseif ($ageDays !== null && $ageDays <= 14) {
            $status = 'warning';
            $label = 'Sauvegarde à renouveler';
            $message = 'La dernière sauvegarde valide date de plus de 7 jours.';
        } else {
            $status = 'danger';
            $label = 'Sauvegarde trop ancienne';
            $message = 'La dernière sauvegarde valide date de plus de 14 jours.';
        }

        return [
            'status' => $status,
            'label' => $label,
            'message' => $message,
            'age_days' => $ageDays,
            'latest' => $latest,
        ];
    }

    /** @return array<string,mixed> */
    public function inspectBackup(string $fileName): array
    {
        $filePath = $this->getDatabaseBackupPath($fileName);
        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));

        if ($extension === 'sql') {
            $sample = file_get_contents($filePath, false, null, 0, 65536);
            $valid = is_string($sample)
                && stripos($sample, 'Darlink ERP Phoenix') !== false
                && stripos($sample, 'SET FOREIGN_KEY_CHECKS') !== false;

            preg_match('/-- Version\s*:\s*(.+)/i', (string) $sample, $versionMatch);
            preg_match('/-- Utilisateur\s*:\s*(.+)/i', (string) $sample, $userMatch);
            preg_match('/-- Date\s*:\s*(.+)/i', (string) $sample, $dateMatch);

            return [
                'valid' => $valid,
                'message' => $valid ? 'Sauvegarde SQL valide.' : 'Fichier SQL non reconnu.',
                'version' => trim((string) ($versionMatch[1] ?? 'Historique')),
                'build' => '',
                'created_by' => trim((string) ($userMatch[1] ?? '')),
                'created_at' => trim((string) ($dateMatch[1] ?? '')) ?: null,
                'file_count' => 1,
            ];
        }

        if (!class_exists(ZipArchive::class)) {
            return ['valid' => false, 'message' => 'Extension ZIP indisponible.'];
        }

        $zip = new ZipArchive();
        if ($zip->open($filePath) !== true) {
            return ['valid' => false, 'message' => 'Archive ZIP illisible.'];
        }

        try {
            $manifestJson = $zip->getFromName('manifest.json');
            $databaseSql = $zip->getFromName('database.sql');
            if (!is_string($manifestJson) || !is_string($databaseSql)) {
                return ['valid' => false, 'message' => 'Manifest ou base SQL manquant.'];
            }

            $manifest = json_decode($manifestJson, true);
            if (!is_array($manifest) || ($manifest['application'] ?? '') !== 'Darlink ERP') {
                return ['valid' => false, 'message' => 'Manifest Darlink invalide.'];
            }

            $expectedHash = trim((string) ($manifest['database_sha256'] ?? ''));
            $hashValid = $expectedHash === '' || hash('sha256', $databaseSql) === $expectedHash;
            $sqlValid = stripos($databaseSql, 'SET FOREIGN_KEY_CHECKS') !== false
                && stripos($databaseSql, 'CREATE TABLE') !== false;
            $valid = $hashValid && $sqlValid;

            return [
                'valid' => $valid,
                'message' => $valid
                    ? 'Archive complète valide.'
                    : (!$hashValid ? 'Empreinte de la base incorrecte.' : 'Base SQL invalide.'),
                'version' => (string) ($manifest['version'] ?? ''),
                'build' => (string) ($manifest['build'] ?? ''),
                'created_by' => (string) ($manifest['created_by'] ?? ''),
                'created_at' => (string) ($manifest['created_at'] ?? ''),
                'file_count' => (int) ($manifest['file_count'] ?? 0),
                'manifest' => $manifest,
            ];
        } finally {
            $zip->close();
        }
    }

    public function deleteDatabaseBackup(string $fileName): void
    {
        $filePath = $this->getDatabaseBackupPath($fileName);
        if (!unlink($filePath)) {
            throw new \RuntimeException('Impossible de supprimer la sauvegarde.');
        }
    }

    public function getDatabaseBackupPath(string $fileName): string
    {
        $filePath = $this->resolveBackupPath($fileName);
        if (!is_file($filePath)) {
            throw new \RuntimeException('La sauvegarde demandée est introuvable.');
        }
        return $filePath;
    }

    public function backupDirectoryPath(): string
    {
        $this->ensureBackupDirectoryExists();
        return $this->backupDirectory();
    }

    private function addDirectoryToZip(ZipArchive $zip, string $sourceDirectory, string $archiveDirectory): int
    {
        $fileCount = 0;
        $sourceDirectory = rtrim($sourceDirectory, DIRECTORY_SEPARATOR);
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($sourceDirectory, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $absolutePath = $item->getPathname();
            $relativePath = str_replace(
                DIRECTORY_SEPARATOR,
                '/',
                substr($absolutePath, strlen($sourceDirectory) + 1)
            );
            $zipPath = trim(str_replace('\\', '/', $archiveDirectory), '/');
            if ($relativePath !== '') {
                $zipPath .= '/' . $relativePath;
            }

            if ($item->isDir()) {
                $zip->addEmptyDir($zipPath);
            } elseif ($item->isFile()) {
                if (!$zip->addFile($absolutePath, $zipPath)) {
                    throw new \RuntimeException('Impossible d’ajouter le fichier : ' . $relativePath);
                }
                $fileCount++;
            }
        }

        return $fileCount;
    }

    private function ensureBackupDirectoryExists(): void
    {
        $backupDirectory = $this->backupDirectory();
        if (!is_dir($backupDirectory)
            && !mkdir($backupDirectory, 0775, true)
            && !is_dir($backupDirectory)) {
            throw new \RuntimeException('Impossible de créer le dossier storage/backups.');
        }
        if (!is_writable($backupDirectory)) {
            throw new \RuntimeException('Le dossier storage/backups n’est pas accessible en écriture.');
        }
    }

    private function backupDirectory(): string
    {
        return dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'storage'
            . DIRECTORY_SEPARATOR . 'backups';
    }

    private function resolveBackupPath(string $fileName): string
    {
        $safeFileName = basename(trim($fileName));
        if ($safeFileName === ''
            || $safeFileName !== $fileName
            || !preg_match('/^[a-zA-Z0-9._-]+\.(sql|zip)$/i', $safeFileName)) {
            throw new \RuntimeException('Nom de sauvegarde invalide.');
        }
        return $this->backupDirectory() . DIRECTORY_SEPARATOR . $safeFileName;
    }
}
