<?php

namespace MintyPHP\Support;

// AES-256-GCM encryption/decryption for sensitive stored values (e.g. SSO secrets).
// Requires APP_CRYPTO_KEY — see EnvValidator for accepted key formats.
class Crypto
{
    private const CIPHER = 'aes-256-gcm';

    public static function isConfigured(): bool
    {
        return self::getKey() !== null;
    }

    public static function encryptString(string $plaintext): string
    {
        $key = self::getKey();
        if ($key === null) {
            throw new \RuntimeException('Missing APP_CRYPTO_KEY');
        }

        $iv = random_bytes(12);
        $tag = '';
        $ciphertext = openssl_encrypt(
            $plaintext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if (!is_string($ciphertext) || strlen($tag) !== 16) {
            throw new \RuntimeException('Encryption failed');
        }

        // Outer base64 wraps a JSON object with v (version), iv, auth tag, and ciphertext —
        // all inner binary values are base64url-encoded to stay JSON-safe.
        $payload = json_encode([
            'v' => 1,
            'iv' => self::base64UrlEncode($iv),
            'tag' => self::base64UrlEncode($tag),
            'ct' => self::base64UrlEncode($ciphertext),
        ]);

        if (!is_string($payload)) {
            throw new \RuntimeException('Encryption payload encoding failed');
        }

        return base64_encode($payload);
    }

    public static function decryptString(string $encoded): string
    {
        $key = self::getKey();
        if ($key === null) {
            throw new \RuntimeException('Missing APP_CRYPTO_KEY');
        }

        $json = base64_decode($encoded, true);
        if (!is_string($json) || $json === '') {
            throw new \RuntimeException('Encrypted payload is invalid');
        }

        $payload = json_decode($json, true);
        if (!is_array($payload)) {
            throw new \RuntimeException('Encrypted payload cannot be decoded');
        }

        $iv = self::base64UrlDecode((string) ($payload['iv'] ?? ''));
        $tag = self::base64UrlDecode((string) ($payload['tag'] ?? ''));
        $ciphertext = self::base64UrlDecode((string) ($payload['ct'] ?? ''));
        if ($iv === '' || strlen($iv) !== 12 || $tag === '' || strlen($tag) !== 16 || $ciphertext === '') {
            throw new \RuntimeException('Encrypted payload is malformed');
        }

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if (!is_string($plaintext)) {
            throw new \RuntimeException('Decryption failed');
        }

        return $plaintext;
    }

    private static function getKey(): ?string
    {
        $raw = defined('APP_CRYPTO_KEY') ? trim((string) APP_CRYPTO_KEY) : '';
        if ($raw === '') {
            return null;
        }

        // Accepts 64-char hex (32 bytes) or base64-encoded 32 bytes — both yield a 256-bit key.
        $hex = preg_match('/^[0-9a-f]{64}$/i', $raw) ? hex2bin($raw) : false;
        if (is_string($hex) && strlen($hex) === 32) {
            return $hex;
        }

        $base64 = base64_decode($raw, true);
        if (is_string($base64) && strlen($base64) === 32) {
            return $base64;
        }

        return null;
    }

    // URL-safe base64: replaces +/ with -_ and strips = padding.
    private static function base64UrlEncode(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }

    private static function base64UrlDecode(string $value): string
    {
        $value = strtr($value, '-_', '+/');
        $pad = strlen($value) % 4;
        if ($pad > 0) {
            $value .= str_repeat('=', 4 - $pad);
        }
        $decoded = base64_decode($value, true);
        return is_string($decoded) ? $decoded : '';
    }
}
