<?php
declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use App\Models\BackupJob;
use PDO;
use RuntimeException;
use Throwable;

final class RollbackTransactionManagerService
{
    /**
     * @return array<string,mixed>
     */
    public function dashboard(): array
    {
        return [
            'packages' => $this->eligiblePackages(),
            'transactions' => $this->transactions(),
            'generated_at' => date('Y-m-d H:i:s'),
            'current_version' => $this->installedVersion(),
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function eligiblePackages(): array
    {
        $statement = Database::connection()->query(
            "SELECT
                p.id,
                p.nom_package,
                p.version_package,
                p.statut,
                p.installe_at,
                p.backup_job_id,
                p.message,
                b.statut AS backup_status,
                b.version_erp AS backup_version,
                b.chemin AS backup_path,
                b.checksum_sha256 AS backup_checksum,
                b.created_at AS backup_created_at
             FROM cda_update_packages AS p
             LEFT JOIN cda_backup_jobs AS b
                ON b.id = p.backup_job_id
             WHERE p.statut = 'installe'
               AND p.backup_job_id IS NOT NULL
             ORDER BY p.installe_at DESC, p.id DESC
             LIMIT 60"
        );

        $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $statement->closeCursor();

        foreach ($rows as &$row) {
            $path = (string) ($row['backup_path'] ?? '');
            $checksum = (string) ($row['backup_checksum'] ?? '');

            $row['rollback_ready'] =
                (string) ($row['backup_status'] ?? '') === 'succes'
                && $path !== ''
                && is_file($path)
                && $checksum !== ''
                && hash_equals($checksum, (string) hash_file('sha256', $path));
        }
        unset($row);

        return $rows;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function transactions(): array
    {
        try {
            $statement = Database::connection()->query(
                "SELECT
                    t.id,
                    t.package_id,
                    t.target_backup_id,
                    t.safety_backup_id,
                    t.status,
                    t.source_version,
                    t.target_version,
                    t.journal_path,
                    t.message,
                    t.requested_by,
                    t.created_at,
                    t.started_at,
                    t.finished_at,
                    p.nom_package
                 FROM system_package_rollback_transactions AS t
                 LEFT JOIN cda_update_packages AS p
                    ON p.id = t.package_id
                 ORDER BY t.id DESC
                 LIMIT 40"
            );

            $rows = $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
            $statement->closeCursor();

            return $rows;
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function execute(
        int $packageId,
        string $confirmation,
        int $userId
    ): array {
        if ($packageId <= 0) {
            throw new RuntimeException('Package invalide.');
        }

        $expectedConfirmation = 'ROLLBACK #' . $packageId;
        if (!hash_equals($expectedConfirmation, trim($confirmation))) {
            throw new RuntimeException(
                'Confirmation incorrecte. Saisissez exactement « '
                . $expectedConfirmation
                . ' ».'
            );
        }

        $lock = new UpdateLockService();
        $lockToken = $lock->acquire($packageId, $userId);

        try {
            $package = $this->lockedPackage($packageId);

            if ((string) $package['statut'] !== 'installe') {
                throw new RuntimeException(
                    'Seul un package installé peut être annulé.'
                );
            }

            $targetBackupId = (int) ($package['backup_job_id'] ?? 0);
            if ($targetBackupId <= 0) {
                throw new RuntimeException(
                    'Aucun point de retour n’est associé à ce package.'
                );
            }

            $targetBackup = BackupJob::find($targetBackupId);
            $this->assertBackup($targetBackup);

            $journalDirectory = BASE_PATH
                . '/storage/updates/rollback-transactions';

            if (!is_dir($journalDirectory)
                && !mkdir($journalDirectory, 0770, true)
                && !is_dir($journalDirectory)) {
                throw new RuntimeException(
                    'Création du journal de rollback impossible.'
                );
            }

            $journalPath = $journalDirectory
                . '/rollback_package_'
                . $packageId
                . '_'
                . date('Ymd_His')
                . '.jsonl';

            $transactionId = $this->createTransaction(
                $package,
                $targetBackupId,
                $expectedConfirmation,
                $journalPath,
                $userId
            );

            $this->appendJournal($journalPath, [
                'event' => 'rollback.requested',
                'transaction_id' => $transactionId,
                'package_id' => $packageId,
                'target_backup_id' => $targetBackupId,
                'source_version' => $this->installedVersion(),
                'target_version' => (string) ($targetBackup['version_erp'] ?? ''),
                'requested_by' => $userId,
                'lock_token_sha256' => hash('sha256', $lockToken),
            ]);

            $this->updateTransaction($transactionId, [
                'status' => 'SAFETY_BACKUP',
                'message' => 'Création de la sauvegarde de secours.',
                'started' => true,
            ]);

            $safetyBackupId = (new BackupService())->create(
                $userId,
                'manuel'
            );

            $this->setSafetyBackup(
                $transactionId,
                $safetyBackupId
            );

            $this->appendJournal($journalPath, [
                'event' => 'rollback.safety_backup_created',
                'transaction_id' => $transactionId,
                'safety_backup_id' => $safetyBackupId,
            ]);

            $this->updateTransaction($transactionId, [
                'status' => 'RESTORING',
                'message' => 'Restauration du point de retour AXIOM.',
            ]);

            /*
             * BackupService::restore() vérifie :
             * - le statut de la sauvegarde ;
             * - le SHA-256 de l’archive ;
             * - le manifeste et les empreintes internes ;
             * - le confinement des chemins ;
             * - l’intégrité de l’export SQL et du projet.
             */
            (new BackupService())->restore($targetBackupId);

            /*
             * La restauration peut remettre une base antérieure ne contenant
             * pas la transaction courante. Le journal JSONL, placé sous
             * storage/updates, est volontairement exclu de la restauration.
             */
            $this->appendJournal($journalPath, [
                'event' => 'rollback.completed',
                'transaction_id' => $transactionId,
                'package_id' => $packageId,
                'restored_backup_id' => $targetBackupId,
                'safety_backup_id' => $safetyBackupId,
                'completed_at' => gmdate('c'),
            ]);

            return [
                'ok' => true,
                'transaction_id' => $transactionId,
                'package_id' => $packageId,
                'target_backup_id' => $targetBackupId,
                'safety_backup_id' => $safetyBackupId,
                'journal_path' => $journalPath,
                'message' => 'Rollback terminé. Rechargez AXIOM.',
            ];
        } catch (Throwable $error) {
            $this->writeEmergencyFailure(
                $packageId,
                $userId,
                $error
            );
            throw $error;
        } finally {
            $lock->release();
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function lockedPackage(int $packageId): array
    {
        $statement = Database::connection()->prepare(
            "SELECT
                id,
                nom_package,
                version_package,
                statut,
                backup_job_id,
                installe_at
             FROM cda_update_packages
             WHERE id = :id
             LIMIT 1
             FOR UPDATE"
        );
        $statement->execute(['id' => $packageId]);
        $package = $statement->fetch(PDO::FETCH_ASSOC);
        $statement->closeCursor();

        if (!is_array($package)) {
            throw new RuntimeException('Package introuvable.');
        }

        return $package;
    }

    /**
     * @param array<string,mixed>|null $backup
     */
    private function assertBackup(?array $backup): void
    {
        if ($backup === null
            || (string) ($backup['statut'] ?? '') !== 'succes') {
            throw new RuntimeException(
                'Le point de retour n’est pas disponible.'
            );
        }

        $path = (string) ($backup['chemin'] ?? '');
        $expected = (string) ($backup['checksum_sha256'] ?? '');

        if ($path === '' || !is_file($path)) {
            throw new RuntimeException(
                'L’archive du point de retour est introuvable.'
            );
        }

        if ($expected === ''
            || !hash_equals(
                $expected,
                (string) hash_file('sha256', $path)
            )) {
            throw new RuntimeException(
                'L’intégrité SHA-256 du point de retour est invalide.'
            );
        }
    }

    /**
     * @param array<string,mixed> $package
     */
    private function createTransaction(
        array $package,
        int $targetBackupId,
        string $confirmation,
        string $journalPath,
        int $userId
    ): int {
        $targetBackup = BackupJob::find($targetBackupId);

        $statement = Database::connection()->prepare(
            "INSERT INTO system_package_rollback_transactions
             (
                package_id,
                target_backup_id,
                status,
                source_version,
                target_version,
                confirmation_code,
                journal_path,
                message,
                requested_by,
                created_at
             )
             VALUES
             (
                :package_id,
                :target_backup_id,
                'PENDING',
                :source_version,
                :target_version,
                :confirmation_code,
                :journal_path,
                'Rollback demandé.',
                :requested_by,
                NOW()
             )"
        );

        $statement->execute([
            'package_id' => (int) $package['id'],
            'target_backup_id' => $targetBackupId,
            'source_version' => $this->installedVersion(),
            'target_version' => (string) ($targetBackup['version_erp'] ?? ''),
            'confirmation_code' => hash('sha256', $confirmation),
            'journal_path' => $journalPath,
            'requested_by' => $userId,
        ]);

        return (int) Database::connection()->lastInsertId();
    }

    /**
     * @param array<string,mixed> $values
     */
    private function updateTransaction(int $transactionId, array $values): void
    {
        $statement = Database::connection()->prepare(
            "UPDATE system_package_rollback_transactions
             SET
                status = :status,
                message = :message,
                started_at = CASE
                    WHEN :started = 1 THEN COALESCE(started_at, NOW())
                    ELSE started_at
                END
             WHERE id = :id"
        );

        $statement->execute([
            'status' => (string) $values['status'],
            'message' => (string) $values['message'],
            'started' => !empty($values['started']) ? 1 : 0,
            'id' => $transactionId,
        ]);
    }

    private function setSafetyBackup(
        int $transactionId,
        int $safetyBackupId
    ): void {
        $statement = Database::connection()->prepare(
            "UPDATE system_package_rollback_transactions
             SET safety_backup_id = :safety_backup_id
             WHERE id = :id"
        );

        $statement->execute([
            'safety_backup_id' => $safetyBackupId,
            'id' => $transactionId,
        ]);
    }

    /**
     * @param array<string,mixed> $entry
     */
    private function appendJournal(string $path, array $entry): void
    {
        $entry['at'] = gmdate('c');

        $encoded = json_encode(
            $entry,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );

        if (!is_string($encoded)
            || @file_put_contents(
                $path,
                $encoded . PHP_EOL,
                FILE_APPEND | LOCK_EX
            ) === false) {
            throw new RuntimeException(
                'Écriture du journal de rollback impossible.'
            );
        }
    }

    private function writeEmergencyFailure(
        int $packageId,
        int $userId,
        Throwable $error
    ): void {
        $directory = BASE_PATH
            . '/storage/updates/rollback-transactions';

        if (!is_dir($directory)) {
            @mkdir($directory, 0770, true);
        }

        $entry = json_encode([
            'at' => gmdate('c'),
            'event' => 'rollback.failed',
            'package_id' => $packageId,
            'requested_by' => $userId,
            'type' => get_class($error),
            'message' => $error->getMessage(),
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        if (is_string($entry)) {
            @file_put_contents(
                $directory . '/rollback_failures.jsonl',
                $entry . PHP_EOL,
                FILE_APPEND | LOCK_EX
            );
        }
    }

    private function installedVersion(): string
    {
        return is_file(BASE_PATH . '/VERSION')
            ? trim((string) file_get_contents(BASE_PATH . '/VERSION'))
            : 'inconnue';
    }
}
