<?php
namespace WalletUpPro\Core\Security;

if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly.
}
class DataEncryption
{
    private static ?self $instance = null;
    private $key;
    private $cipher = 'aes-256-gcm';
    private $tagLength = 16;

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function __construct()
    {
        $this->key = $this->getEncryptionKey();
    }

    /** @var string[] Fallback keys to try if primary decryption fails (populated lazily) */
    private $fallbackKeys = [];

    private function getEncryptionKey(): string
    {
        // Prefer a dedicated constant from wp-config.php
        if (defined('WALLETUP_ENCRYPTION_KEY') && !empty(WALLETUP_ENCRYPTION_KEY)) {
            $this->buildFallbackKeys('walletup_constant');
            return WALLETUP_ENCRYPTION_KEY;
        }

        // Derive from WordPress AUTH_KEY (lives in wp-config.php, not DB)
        if (defined('AUTH_KEY') && !empty(AUTH_KEY)) {
            $this->buildFallbackKeys('auth_key');
            return hash('sha256', AUTH_KEY . 'walletup_encryption_v1', true);
        }

        // Last resort: stored in DB (existing behavior for backwards compat)
        // NOTE: This is NOT PCI DSS compliant — define WALLETUP_ENCRYPTION_KEY or AUTH_KEY in wp-config.php
        $key = get_option('wallet_up_encryption_key');
        if (!$key) {
            $key = wp_generate_password(64, true, true);
            update_option('wallet_up_encryption_key', $key, false); // autoload=false
            // Log a warning so site admins know to set a proper key
            if (function_exists('error_log')) {
                error_log('[WalletUp Security] WARNING: Encryption key stored in database. Define WALLETUP_ENCRYPTION_KEY in wp-config.php for PCI DSS compliance.');
            }
        }
        return hash('sha256', $key . 'walletup_db_key_v1', true);
    }

    /**
     * Build fallback decryption keys from other sources.
     * If AUTH_KEY changed, the DB key might still decrypt old data.
     */
    private function buildFallbackKeys(string $primarySource): void
    {
        // If primary is AUTH_KEY or constant, add DB key as fallback
        if ($primarySource !== 'db_key') {
            $dbKey = get_option('wallet_up_encryption_key');
            if ($dbKey) {
                $this->fallbackKeys[] = hash('sha256', $dbKey . 'walletup_db_key_v1', true);
            }
        }
        // If primary is constant, add AUTH_KEY as fallback
        if ($primarySource === 'walletup_constant' && defined('AUTH_KEY') && !empty(AUTH_KEY)) {
            $this->fallbackKeys[] = hash('sha256', AUTH_KEY . 'walletup_encryption_v1', true);
        }
    }

    public function encrypt(string $data): string
    {
        $ivlen = openssl_cipher_iv_length($this->cipher);
        $iv = openssl_random_pseudo_bytes($ivlen);
        $tag = '';

        $encrypted = openssl_encrypt(
            $data,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            $this->tagLength
        );

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

    public function decrypt(string $data): string|false
    {
        $decoded = base64_decode($data, true);
        if ($decoded === false) {
            return false;
        }

        $ivlen = openssl_cipher_iv_length($this->cipher);
        if (strlen($decoded) < $ivlen + $this->tagLength) {
            return false;
        }

        $iv = substr($decoded, 0, $ivlen);
        $tag = substr($decoded, $ivlen, $this->tagLength);
        $ciphertext = substr($decoded, $ivlen + $this->tagLength);

        // Try primary key first
        $result = openssl_decrypt(
            $ciphertext,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($result !== false) {
            return $result;
        }

        // Primary key failed — try fallback keys (handles AUTH_KEY migration)
        foreach ($this->fallbackKeys as $fallbackKey) {
            $result = openssl_decrypt(
                $ciphertext,
                $this->cipher,
                $fallbackKey,
                OPENSSL_RAW_DATA,
                $iv,
                $tag
            );
            if ($result !== false) {
                // Re-encrypt with current primary key so future decryptions use primary
                $reEncrypted = $this->encrypt($result);
                // Note: Caller is responsible for saving the re-encrypted value if desired
                return $result;
            }
        }

        return false;
    }

    /**
     * Get an encrypted wp_option value, automatically decrypting it.
     * Handles backward compatibility with plaintext values.
     */
    public static function getEncryptedOption(string $optionKey, string $default = ''): string
    {
        $value = get_option($optionKey, $default);
        if (empty($value)) {
            return $default;
        }
        try {
            $instance = self::getInstance();
            $decrypted = $instance->decrypt($value);
            if ($decrypted !== false && $decrypted !== '') {
                // If decryption succeeded via a fallback key, re-encrypt with primary key
                $reEncrypted = $instance->encrypt($decrypted);
                if ($reEncrypted !== $value) {
                    update_option($optionKey, $reEncrypted);
                }
                return $decrypted;
            }
        } catch (\Exception $e) {
            // Value may be plaintext (pre-encryption); return as-is
        }
        return $value;
    }
}