<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Serialization;

use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\LiveComponents\Exceptions\StateEncryptionException;
use App\Framework\Random\RandomGenerator;

/**
 * Encrypts and decrypts LiveComponent state data
 *
 * Uses authenticated encryption (AES-256-GCM) via libsodium to protect sensitive state data.
 * Each encryption operation generates a unique nonce for security.
 *
 * Framework Principles:
 * - Readonly class with immutable encryption key
 * - Uses framework's CryptographicUtilities and RandomGenerator
 * - Value Objects for inputs/outputs
 * - Explicit exceptions for all error conditions
 * - No inheritance - composition only
 *
 * @see CryptographicUtilities For cryptographic primitives
 * @see RandomGenerator For secure random number generation
 */
final readonly class StateEncryptor
{
    private const ENCRYPTION_VERSION = 1;
    private const NONCE_LENGTH = 24; // SODIUM_CRYPTO_SECRETBOX_NONCEBYTES

    public function __construct(
        private string $encryptionKey,
        private CryptographicUtilities $crypto,
        private RandomGenerator $random
    ) {
        if (! extension_loaded('sodium')) {
            throw new \RuntimeException(
                'Sodium extension required for StateEncryptor. Install via: apt-get install php8.5-sodium'
            );
        }

        if (strlen($this->encryptionKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
            throw new \InvalidArgumentException(
                'Encryption key must be exactly ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes (32 bytes). ' .
                'Generate via: bin/console generate:encryption-key'
            );
        }

        // Validate key entropy (4.0 bits/byte is realistic minimum for cryptographically secure random data)
        // Perfect random data has ~5 bits/byte Shannon entropy, not 8 (theoretical max for uniform distribution)
        if (! $this->crypto->validateEntropy($this->encryptionKey, 4.0)) {
            throw new \InvalidArgumentException(
                'Encryption key has insufficient entropy. Use a cryptographically secure key.'
            );
        }
    }

    /**
     * Encrypt state data
     *
     * @param string $plaintext The serialized state data to encrypt
     * @return string Base64-encoded encrypted data with nonce prepended
     * @throws StateEncryptionException if encryption fails
     */
    public function encrypt(string $plaintext): string
    {
        try {
            // Generate unique nonce using framework's CryptographicUtilities
            $nonce = $this->crypto->generateNonce(self::NONCE_LENGTH);

            // Authenticated encryption (encrypt + MAC) using libsodium
            $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->encryptionKey);

            if ($ciphertext === false) {
                throw StateEncryptionException::encryptionFailed('Encryption operation failed');
            }

            // Prepend version byte + nonce to ciphertext
            $versionByte = chr(self::ENCRYPTION_VERSION);
            $encrypted = $versionByte . $nonce . $ciphertext;

            // Clear sensitive data from memory using framework utility
            $plaintextCopy = $plaintext;
            $nonceCopy = $nonce;
            $this->crypto->secureWipe($plaintextCopy);
            $this->crypto->secureWipe($nonceCopy);

            // Base64 encode for safe storage/transmission
            return base64_encode($encrypted);
        } catch (StateEncryptionException $e) {
            throw $e; // Re-throw our exceptions
        } catch (\Throwable $e) {
            throw StateEncryptionException::encryptionFailed(
                'Failed to encrypt state data: ' . $e->getMessage(),
                $e
            );
        }
    }

    /**
     * Decrypt state data
     *
     * @param string $encrypted Base64-encoded encrypted data with nonce prepended
     * @return string The decrypted plaintext state data
     * @throws StateEncryptionException if decryption fails or data is corrupted
     */
    public function decrypt(string $encrypted): string
    {
        try {
            // Decode from base64
            $decoded = base64_decode($encrypted, strict: true);

            if ($decoded === false) {
                throw StateEncryptionException::decryptionFailed('Invalid base64 encoding');
            }

            // Validate minimum length: version (1) + nonce (24) + at least some ciphertext (1)
            $minLength = 1 + self::NONCE_LENGTH + 1;
            if (strlen($decoded) < $minLength) {
                throw StateEncryptionException::decryptionFailed(
                    'Encrypted data too short - possible corruption'
                );
            }

            // Extract version byte
            $version = ord($decoded[0]);

            if ($version !== self::ENCRYPTION_VERSION) {
                throw StateEncryptionException::decryptionFailed(
                    "Unsupported encryption version: {$version}. Expected: " . self::ENCRYPTION_VERSION
                );
            }

            // Extract nonce (24 bytes)
            $nonce = substr($decoded, 1, self::NONCE_LENGTH);

            // Extract ciphertext (remaining bytes)
            $ciphertext = substr($decoded, 1 + self::NONCE_LENGTH);

            // Authenticated decryption (verify MAC + decrypt) using libsodium
            $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->encryptionKey);

            if ($plaintext === false) {
                throw StateEncryptionException::decryptionFailed(
                    'Decryption failed - data may be corrupted or tampered with (MAC verification failed)'
                );
            }

            // Clear sensitive data from memory using framework utility
            $nonceCopy = $nonce;
            $ciphertextCopy = $ciphertext;
            $this->crypto->secureWipe($nonceCopy);
            $this->crypto->secureWipe($ciphertextCopy);

            return $plaintext;
        } catch (StateEncryptionException $e) {
            throw $e; // Re-throw our exceptions
        } catch (\Throwable $e) {
            throw StateEncryptionException::decryptionFailed(
                'Failed to decrypt state data: ' . $e->getMessage(),
                $e
            );
        }
    }

    /**
     * Verify if data is encrypted (has version byte + nonce + ciphertext)
     *
     * @param string $data The data to check
     * @return bool True if data appears to be encrypted
     */
    public function isEncrypted(string $data): bool
    {
        try {
            $decoded = base64_decode($data, strict: true);

            if ($decoded === false) {
                return false;
            }

            // Must have at least: version byte (1) + nonce (24) + some ciphertext (>0)
            $minLength = 1 + self::NONCE_LENGTH;

            if (strlen($decoded) <= $minLength) {
                return false;
            }

            // Check version byte is valid
            $version = ord($decoded[0]);

            return $version === self::ENCRYPTION_VERSION;
        } catch (\Throwable) {
            return false;
        }
    }
}
