<?php

namespace Velis\Crypt;

use InvalidArgumentException;
// todo: remove with ::legacyDecrypt()
use Laminas\Crypt\BlockCipher;
use RuntimeException;
// todo: remove with ::legacyDecrypt()
use Velis\Auth\FilterFactory;

class Crypt
{
    private const string CIPHER = 'aes-256-gcm';
    private const int KEY_LENGTH = 32; // 256-bit

    private string $key;

    public function __construct(string $key)
    {
        if (strlen($key) !== self::KEY_LENGTH) {
            throw new InvalidArgumentException('Key must be '  . self::KEY_LENGTH . ' bytes.');
        }

        $this->key = $key;
    }

    public function encrypt(string $plaintext): string
    {
        $ivLength = openssl_cipher_iv_length(self::CIPHER);
        $iv = random_bytes($ivLength);

        $tag = '';
        $ciphertext = openssl_encrypt(
            data: $plaintext,
            cipher_algo: self::CIPHER,
            passphrase: $this->key,
            options: OPENSSL_RAW_DATA,
            iv: $iv,
            tag: $tag  // tag length
        );

        if ($ciphertext === false) {
            throw new RuntimeException('Encryption failed');
        }

        return base64_encode(json_encode([
            'iv' => base64_encode($iv),
            'value' => base64_encode($ciphertext),
            'tag' => base64_encode($tag),
        ]));
    }

    public function decrypt(string $payload): string
    {
        $json = json_decode(base64_decode($payload), true);

        if (!is_array($json) || !isset($json['iv'], $json['value'], $json['tag'])) {
            /**
             * @deprecated Will be removed in the second half of August 2025
             * Legacy decryption method is used for backward compatibility until all data is re-encrypted
             */
            return self::legacyDecrypt($payload);

            // todo: Uncomment the line below to throw an exception after removing legacy decryption
            // throw new InvalidArgumentException('Invalid payload');
        }

        $iv = base64_decode($json['iv']);
        $ciphertext = base64_decode($json['value']);
        $tag = base64_decode($json['tag']);

        $plaintext = openssl_decrypt(
            data: $ciphertext,
            cipher_algo: self::CIPHER,
            passphrase: $this->key,
            options: OPENSSL_RAW_DATA,
            iv: $iv,
            tag: $tag
        );

        if ($plaintext === false) {
            throw new RuntimeException('Decryption failed');
        }

        return $plaintext;
    }

    /**
     * @deprecated Will be removed in the second half of August 2025, after reencrypting data
     */
    public static function legacyDecrypt(string $string): string
    {
        $privateKeyPath = ROOT_PATH . 'config/cert/auth.pem';
        // phpcs:ignore PHPCS_SecurityAudit.BadFunctions.FilesystemFunctions.WarnFilesystem
        $privateKey = file_get_contents($privateKeyPath);

        $blockCipher = BlockCipher::factory('openssl');
        $blockCipher->setKey($privateKey);
        $blockCipher->setBinaryOutput(true);

        $encrypted = base64_decode($string);

        $decrypted = $blockCipher->decrypt($encrypted);

        if (!empty($decrypted)) {
            return $decrypted;
        }

        // legacy decryption
        // @todo: remove after all data is re-encrypted
        $filterFactory = new FilterFactory();
        $filter = $filterFactory->createDecryptionFilter();

        return trim($filter->filter($string));
    }
}
