<?php

namespace App\Libraries;

/**
 * FieldEncryption
 *
 * AES-256-CBC field-level encryption for sensitive profile data.
 *
 * Encrypted format stored in DB:
 *   "ENC:" + base64( iv[16] . ciphertext )
 *
 * The "ENC:" prefix lets us safely detect already-encrypted values
 * so double-encryption never occurs and plaintext legacy data is
 * returned as-is until re-saved.
 *
 * Key is read from .env:  FIELD_ENCRYPTION_KEY (32 hex chars = 16 bytes key)
 */
class FieldEncryption
{
    private const PREFIX  = 'ENC:';
    private const CIPHER  = 'AES-256-CBC';
    private const IV_LEN  = 16;
    private const KEY_LEN = 32;   // 256-bit

    // ----------------------------------------------------------------
    // Public API
    // ----------------------------------------------------------------

    /**
     * Encrypt a plaintext value.
     * Returns the original value unchanged if:
     *   – it is null / empty string
     *   – it is already encrypted (starts with "ENC:")
     */
    public static function encrypt(?string $value): ?string
    {
        if ($value === null || $value === '') {
            return $value;
        }

        if (str_starts_with($value, self::PREFIX)) {
            return $value;   // already encrypted
        }

        $key  = self::key();
        $iv   = random_bytes(self::IV_LEN);
        $ct   = openssl_encrypt($value, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);

        if ($ct === false) {
            throw new \RuntimeException('FieldEncryption: openssl_encrypt failed.');
        }

        return self::PREFIX . base64_encode($iv . $ct);
    }

    /**
     * Decrypt an encrypted value.
     * Returns the original value unchanged if:
     *   – it is null / empty string
     *   – it does NOT start with "ENC:" (legacy plaintext, not yet encrypted)
     */
    public static function decrypt(?string $value): ?string
    {
        if ($value === null || $value === '') {
            return $value;
        }

        if (! str_starts_with($value, self::PREFIX)) {
            return $value;   // legacy plaintext — return as-is
        }

        $key     = self::key();
        $decoded = base64_decode(substr($value, strlen(self::PREFIX)), true);

        if ($decoded === false || strlen($decoded) <= self::IV_LEN) {
            return '';   // corrupted data — return empty rather than crash
        }

        $iv = substr($decoded, 0, self::IV_LEN);
        $ct = substr($decoded, self::IV_LEN);

        $plain = openssl_decrypt($ct, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);

        return ($plain === false) ? '' : $plain;
    }

    /**
     * Returns true if the stored value is encrypted.
     */
    public static function isEncrypted(?string $value): bool
    {
        return $value !== null && str_starts_with($value, self::PREFIX);
    }

    // ----------------------------------------------------------------
    // Private
    // ----------------------------------------------------------------

    /**
     * Derive the 32-byte key from the env variable.
     * The env value is treated as a hex string (32 hex chars = 16 bytes)
     * or a plain string; padded / trimmed to exactly 32 bytes.
     */
    private static function key(): string
    {
        // CI4's env() when running inside the framework; fall back to $_ENV / getenv() otherwise
        $raw = function_exists('env')
            ? env('FIELD_ENCRYPTION_KEY', '')
            : ($_ENV['FIELD_ENCRYPTION_KEY'] ?? getenv('FIELD_ENCRYPTION_KEY') ?? '');

        if (empty($raw)) {
            throw new \RuntimeException(
                'FieldEncryption: FIELD_ENCRYPTION_KEY is not set in .env'
            );
        }

        // If it looks like a hex string, decode it; otherwise use as-is
        if (ctype_xdigit($raw) && strlen($raw) % 2 === 0) {
            $raw = hex2bin($raw);
        }

        // Pad or trim to exactly 32 bytes
        if (strlen($raw) < self::KEY_LEN) {
            $raw = str_pad($raw, self::KEY_LEN, "\0");
        }

        return substr($raw, 0, self::KEY_LEN);
    }
}
