<?php

declare(strict_types=1);

namespace ShieldX\Support;

defined('ABSPATH') || exit;

final class CryptoBox
{
    public const PREFIX_V2 = 'enc:v2:';

    private const CIPHER = 'aes-256-gcm';
    private const NONCE_BYTES = 12;
    private const TAG_BYTES = 16;

    public static function isAvailable(): bool
    {
        return function_exists('openssl_encrypt')
            && function_exists('openssl_decrypt')
            && in_array(self::CIPHER, array_map('strtolower', openssl_get_cipher_methods()), true);
    }

    public static function isV2(string $stored): bool
    {
        return strpos(trim($stored), self::PREFIX_V2) === 0;
    }

    public static function protect(string $plaintext, string $key): string
    {
        if ($plaintext === '' || !self::isAvailable()) {
            return '';
        }

        try {
            $nonce = random_bytes(self::NONCE_BYTES);
            $tag = '';
            $ciphertext = openssl_encrypt($plaintext, self::CIPHER, self::normalizeKey($key), OPENSSL_RAW_DATA, $nonce, $tag, '', self::TAG_BYTES);
        } catch (\Throwable $error) {
            return '';
        }

        if (!is_string($ciphertext) || $ciphertext === '' || strlen($tag) !== self::TAG_BYTES) {
            return '';
        }

        return self::PREFIX_V2 . base64_encode($nonce) . ':' . base64_encode($tag) . ':' . base64_encode($ciphertext);
    }

    public static function reveal(string $stored, string $key): string
    {
        $stored = trim($stored);
        if (!self::isV2($stored) || !self::isAvailable()) {
            return '';
        }

        $payload = substr($stored, strlen(self::PREFIX_V2));
        $parts = explode(':', $payload);
        if (count($parts) !== 3) {
            return '';
        }

        $nonce = base64_decode($parts[0], true);
        $tag = base64_decode($parts[1], true);
        $ciphertext = base64_decode($parts[2], true);
        if (
            !is_string($nonce)
            || strlen($nonce) !== self::NONCE_BYTES
            || !is_string($tag)
            || strlen($tag) !== self::TAG_BYTES
            || !is_string($ciphertext)
            || $ciphertext === ''
        ) {
            return '';
        }

        try {
            $plaintext = openssl_decrypt($ciphertext, self::CIPHER, self::normalizeKey($key), OPENSSL_RAW_DATA, $nonce, $tag);
        } catch (\Throwable $error) {
            return '';
        }

        return is_string($plaintext) ? $plaintext : '';
    }

    private static function normalizeKey(string $key): string
    {
        return hash('sha256', $key, true);
    }
}
