<?php

/**
 * Symmetric encryption helper using AES-256-GCM (OpenSSL).
 *
 * Key is read from APP_KEY in app/.env (base64-encoded 32 bytes).
 * Ciphertext format stored in the DB: base64( iv [12 bytes] | tag [16 bytes] | ciphertext )
 */
class Crypto
{
    private const CIPHER  = 'aes-256-gcm';
    private const IV_LEN  = 12;
    private const TAG_LEN = 16;

    private static ?string $key = null;

    // ── Key loading ────────────────────────────────────────────────────────────

    /**
     * Returns the raw 32-byte key, loading it from .env on first call.
     */
    private static function key(): string
    {
        if (self::$key !== null) {
            return self::$key;
        }

        $envFile = defined('APP_ROOT') ? APP_ROOT . '/.env' : __DIR__ . '/../.env';

        if (!file_exists($envFile)) {
            throw new RuntimeException('APP_KEY not found: .env file missing at ' . $envFile);
        }

        foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
            if (str_starts_with(trim($line), 'APP_KEY=')) {
                $b64 = trim(substr($line, strlen('APP_KEY=')));
                $raw = base64_decode($b64, strict: true);
                if ($raw === false || strlen($raw) !== 32) {
                    throw new RuntimeException('APP_KEY must be a base64-encoded 32-byte value.');
                }
                self::$key = $raw;
                return self::$key;
            }
        }

        throw new RuntimeException('APP_KEY not found in .env');
    }

    // ── Public API ─────────────────────────────────────────────────────────────

    /**
     * Encrypt a plaintext string. Returns a base64-encoded blob safe to store in the DB.
     */
    public static function encrypt(string $plaintext): string
    {
        $iv  = random_bytes(self::IV_LEN);
        $tag = '';

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

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

        return base64_encode($iv . $tag . $ciphertext);
    }

    /**
     * Decrypt a blob produced by encrypt(). Returns the original plaintext.
     * Returns '' and logs a warning if the blob is invalid or authentication fails.
     */
    public static function decrypt(string $blob): string
    {
        if ($blob === '') {
            return '';
        }

        $raw = base64_decode($blob, strict: true);
        if ($raw === false || strlen($raw) < self::IV_LEN + self::TAG_LEN + 1) {
            // Likely a legacy plaintext value — return as-is so existing installs
            // don't hard-break before the migration has run.
            return $blob;
        }

        $iv         = substr($raw, 0, self::IV_LEN);
        $tag        = substr($raw, self::IV_LEN, self::TAG_LEN);
        $ciphertext = substr($raw, self::IV_LEN + self::TAG_LEN);

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

        if ($plaintext === false) {
            error_log('Crypto::decrypt — authentication tag mismatch; returning empty string.');
            return '';
        }

        return $plaintext;
    }

    /**
     * Returns true if a value looks like it was produced by encrypt()
     * (as opposed to a legacy plaintext password).
     */
    public static function isEncrypted(string $value): bool
    {
        if ($value === '') {
            return false;
        }
        $raw = base64_decode($value, strict: true);
        return $raw !== false && strlen($raw) >= self::IV_LEN + self::TAG_LEN + 1;
    }
}
