<?php

declare(strict_types=1);

final class CryptoService
{
    private const PREFIX = 'v2:';
    private const CIPHER = 'aes-256-gcm';
    private const NONCE_BYTES = 12;
    private const TAG_BYTES = 16;

    public static function encrypt(string $plainText): string
    {
        if ($plainText === '') {
            throw new InvalidArgumentException('No se puede cifrar un valor vacío.');
        }

        $key = self::primaryKey();
        $nonce = random_bytes(self::NONCE_BYTES);
        $tag = '';
        $cipher = openssl_encrypt(
            $plainText,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag,
            '',
            self::TAG_BYTES
        );

        if ($cipher === false || strlen($tag) !== self::TAG_BYTES) {
            throw new RuntimeException('No fue posible cifrar la información.');
        }

        return self::PREFIX . base64_encode($nonce . $tag . $cipher);
    }

    public static function decrypt(string $storedValue): ?string
    {
        if ($storedValue === '') {
            return '';
        }

        if (str_starts_with($storedValue, self::PREFIX)) {
            return self::decryptV2(substr($storedValue, strlen(self::PREFIX)));
        }

        return self::decryptLegacyStaticIv($storedValue)
            ?? self::decryptLegacyPrefixedIv($storedValue);
    }

    public static function decryptOrFail(string $storedValue): string
    {
        $plain = self::decrypt($storedValue);
        if ($plain === null || $plain === '') {
            throw new RuntimeException('La credencial cifrada no pudo ser recuperada.');
        }
        return $plain;
    }

    public static function needsMigration(string $storedValue): bool
    {
        return $storedValue !== '' && !str_starts_with($storedValue, self::PREFIX);
    }

    private static function decryptV2(string $payload): ?string
    {
        $raw = base64_decode($payload, true);
        if ($raw === false || strlen($raw) <= self::NONCE_BYTES + self::TAG_BYTES) {
            return null;
        }

        $nonce = substr($raw, 0, self::NONCE_BYTES);
        $tag = substr($raw, self::NONCE_BYTES, self::TAG_BYTES);
        $cipher = substr($raw, self::NONCE_BYTES + self::TAG_BYTES);

        $plain = openssl_decrypt(
            $cipher,
            self::CIPHER,
            self::primaryKey(),
            OPENSSL_RAW_DATA,
            $nonce,
            $tag
        );

        return self::plausible($plain) ? $plain : null;
    }

    private static function decryptLegacyStaticIv(string $storedValue): ?string
    {
        $raw = base64_decode($storedValue, true);
        $key = self::legacyKey();
        $iv = self::legacyIv();
        if ($raw === false || $key === null || $iv === null) {
            return null;
        }

        $plain = openssl_decrypt($raw, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
        return self::plausible($plain) ? $plain : null;
    }

    private static function decryptLegacyPrefixedIv(string $storedValue): ?string
    {
        $raw = base64_decode($storedValue, true);
        $key = self::legacyKey();
        if ($raw === false || $key === null || strlen($raw) <= 16) {
            return null;
        }

        $iv = substr($raw, 0, 16);
        $cipher = substr($raw, 16);
        $plain = openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
        return self::plausible($plain) ? $plain : null;
    }

    private static function primaryKey(): string
    {
        $value = defined('APP_KEY') ? (string) APP_KEY : '';
        if ($value === '') {
            throw new RuntimeException('APP_KEY no está configurada.');
        }
        return self::normalizeKey($value);
    }

    private static function legacyKey(): ?string
    {
        $value = defined('LEGACY_ENCRYPTION_KEY') ? (string) LEGACY_ENCRYPTION_KEY : '';
        if ($value === '' && defined('ENCRYPTION_KEY')) {
            $value = (string) ENCRYPTION_KEY;
        }
        if ($value === '') {
            return null;
        }
        return self::normalizeKey($value);
    }

    private static function legacyIv(): ?string
    {
        $value = defined('LEGACY_ENCRYPTION_IV') ? (string) LEGACY_ENCRYPTION_IV : '';
        if ($value === '' && defined('ENCRYPTION_SECRET_IV')) {
            $value = (string) ENCRYPTION_SECRET_IV;
        }
        if ($value === '') {
            return null;
        }
        return substr(hash('sha256', $value), 0, 16);
    }

    private static function normalizeKey(string $value): string
    {
        if (str_starts_with($value, 'base64:')) {
            $decoded = base64_decode(substr($value, 7), true);
            if ($decoded !== false && strlen($decoded) >= 32) {
                return substr($decoded, 0, 32);
            }
        }

        if (preg_match('/^[a-f0-9]{64,}$/i', $value)) {
            $decoded = hex2bin(substr($value, 0, 64));
            if ($decoded !== false) {
                return $decoded;
            }
        }

        return hash('sha256', $value, true);
    }

    private static function plausible(string|false $plain): bool
    {
        if ($plain === false || $plain === '' || str_contains($plain, "\0")) {
            return false;
        }
        return preg_match('//u', $plain) === 1;
    }
}
