<?php
declare(strict_types=1);

namespace App\Security;

use RuntimeException;

final class BackupArchiveCrypto
{
    private const MAGIC = "AXIOM-BACKUP-1\n";
    private const CHUNK_SIZE = 1048576;

    public static function isEncrypted(string $path): bool
    {
        $handle = @fopen($path, 'rb');
        if ($handle === false) return false;
        $magic = fread($handle, strlen(self::MAGIC));
        fclose($handle);
        return hash_equals(self::MAGIC, (string)$magic);
    }

    public static function encrypt(string $plainPath): string
    {
        if (!is_file($plainPath)) throw new RuntimeException('Archive de sauvegarde introuvable.');
        if (!function_exists('sodium_crypto_secretstream_xchacha20poly1305_init_push')) {
            throw new RuntimeException('Sodium SecretStream est obligatoire pour chiffrer les sauvegardes.');
        }

        $key = (new CryptoKeyring())->current('backup-archive');
        [$state, $header] = sodium_crypto_secretstream_xchacha20poly1305_init_push($key);
        $target = preg_replace('/\.(zip|tar\.gz|tgz)$/i', '', $plainPath) . '.axiombak';
        $tmp = $target . '.tmp.' . bin2hex(random_bytes(4));

        $in = fopen($plainPath, 'rb');
        $out = fopen($tmp, 'xb');
        if ($in === false || $out === false) {
            if (is_resource($in)) fclose($in);
            if (is_resource($out)) fclose($out);
            @unlink($tmp);
            throw new RuntimeException('Ouverture du flux de chiffrement impossible.');
        }

        try {
            fwrite($out, self::MAGIC);
            fwrite($out, $header);
            while (!feof($in)) {
                $plain = fread($in, self::CHUNK_SIZE);
                if ($plain === false) throw new RuntimeException('Lecture de sauvegarde impossible.');
                if ($plain === '' && feof($in)) break;
                $tag = feof($in)
                    ? SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL
                    : SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
                $cipher = sodium_crypto_secretstream_xchacha20poly1305_push($state, $plain, '', $tag);
                fwrite($out, pack('N', strlen($cipher)));
                fwrite($out, $cipher);
            }
        } finally {
            fclose($in);
            fclose($out);
        }

        @chmod($tmp, 0600);
        if (!rename($tmp, $target)) {
            @unlink($tmp);
            throw new RuntimeException('Activation de la sauvegarde chiffrée impossible.');
        }
        @chmod($target, 0600);
        return $target;
    }

    public static function decrypt(string $encryptedPath, string $targetZip): void
    {
        if (!self::isEncrypted($encryptedPath)) throw new RuntimeException('Format de sauvegarde chiffrée invalide.');
        $keyring = new CryptoKeyring();
        $in = fopen($encryptedPath, 'rb');
        if ($in === false) throw new RuntimeException('Lecture de la sauvegarde chiffrée impossible.');
        fread($in, strlen(self::MAGIC));
        $header = fread($in, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);
        if (strlen((string)$header) !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES) {
            fclose($in);
            throw new RuntimeException('En-tête de sauvegarde chiffrée incomplet.');
        }

        $lastError = null;
        foreach ($keyring->candidates('backup-archive') as $candidate) {
            rewind($in);
            fread($in, strlen(self::MAGIC));
            $header = fread($in, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);
            try {
                self::decryptWithKey($in, (string)$header, $candidate['key'], $targetZip);
                fclose($in);
                return;
            } catch (\Throwable $error) {
                $lastError = $error;
                @unlink($targetZip);
            }
        }
        fclose($in);
        throw new RuntimeException('Déchiffrement impossible avec les clés autorisées.', 0, $lastError);
    }

    private static function decryptWithKey($in, string $header, string $key, string $target): void
    {
        $state = sodium_crypto_secretstream_xchacha20poly1305_init_pull($header, $key);
        $tmp = $target . '.tmp.' . bin2hex(random_bytes(4));
        $out = fopen($tmp, 'xb');
        if ($out === false) throw new RuntimeException('Création du fichier temporaire impossible.');
        $finalSeen = false;
        try {
            while (!feof($in)) {
                $lengthRaw = fread($in, 4);
                if ($lengthRaw === '' && feof($in)) break;
                if (strlen((string)$lengthRaw) !== 4) throw new RuntimeException('Bloc chiffré tronqué.');
                $length = unpack('Nlength', $lengthRaw)['length'];
                if ($length < SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES
                    || $length > self::CHUNK_SIZE + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES) {
                    throw new RuntimeException('Taille de bloc chiffré invalide.');
                }
                $cipher = '';
                while (strlen($cipher) < $length && !feof($in)) {
                    $part = fread($in, $length - strlen($cipher));
                    if ($part === false) throw new RuntimeException('Lecture du bloc chiffré impossible.');
                    $cipher .= $part;
                }
                if (strlen($cipher) !== $length) throw new RuntimeException('Bloc chiffré incomplet.');
                $result = sodium_crypto_secretstream_xchacha20poly1305_pull($state, $cipher);
                if ($result === false) throw new RuntimeException('Authentification cryptographique de la sauvegarde échouée.');
                [$plain, $tag] = $result;
                fwrite($out, $plain);
                if ($tag === SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL) {
                    $finalSeen = true;
                    if (!feof($in) && fread($in, 1) !== '') throw new RuntimeException('Données ajoutées après le bloc final.');
                    break;
                }
            }
        } finally {
            fclose($out);
        }
        if (!$finalSeen) {
            @unlink($tmp);
            throw new RuntimeException('Bloc final de sauvegarde absent.');
        }
        @chmod($tmp, 0600);
        if (!rename($tmp, $target)) {
            @unlink($tmp);
            throw new RuntimeException('Activation du ZIP déchiffré impossible.');
        }
    }
}
