<?php

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Support\Helpers;

/**
 * Cryptographic Helper Class
 *
 * Provides utilities for encryption, hashing, token generation,
 * and secure comparison operations.
 *
 * @package WPUltimateSecurity\UltimateSecurity\Support\Helpers
 * @since 1.0.0
 */
class CryptoHelper
{
    /**
     * Encryption cipher method.
     */
    private const CIPHER_METHOD = 'aes-256-gcm';

    /**
     * IV length for AES-256-GCM.
     */
    private const IV_LENGTH = 12;

    /**
     * Authentication tag length for GCM mode.
     */
    private const TAG_LENGTH = 16;

    /**
     * Encrypt data using WordPress authentication keys.
     *
     * Uses AES-256-GCM for authenticated encryption, providing both
     * confidentiality and integrity verification.
     *
     * @param string $data The plaintext data to encrypt
     * @param string|null $key Optional encryption key (defaults to AUTH_KEY)
     * @return string|false Base64-encoded encrypted data or false on failure
     */
    public static function encrypt(string $data, ?string $key = null): string|false
    {
        if (!self::isOpenSSLAvailable()) {
            return false;
        }

        // Derive encryption key from WordPress auth key
        $encryptionKey = self::deriveKey($key);

        // Generate a random IV
        $iv = random_bytes(self::IV_LENGTH);

        // Encrypt the data
        $tag = '';
        $encrypted = openssl_encrypt(
            $data,
            self::CIPHER_METHOD,
            $encryptionKey,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            self::TAG_LENGTH
        );

        if ($encrypted === false) {
            return false;
        }

        // Combine IV + Tag + Encrypted data and encode
        $combined = $iv . $tag . $encrypted;

        return base64_encode($combined);
    }

    /**
     * Decrypt data that was encrypted with the encrypt method.
     *
     * @param string $encrypted The Base64-encoded encrypted data
     * @param string|null $key Optional decryption key (defaults to AUTH_KEY)
     * @return string|false The decrypted plaintext or false on failure
     */
    public static function decrypt(string $encrypted, ?string $key = null): string|false
    {
        if (!self::isOpenSSLAvailable()) {
            return false;
        }

        // Decode the base64 data
        $combined = base64_decode($encrypted, true);
        if ($combined === false) {
            return false;
        }

        // Ensure we have enough data for IV + tag + at least 1 byte of data
        $minLength = self::IV_LENGTH + self::TAG_LENGTH + 1;
        if (strlen($combined) < $minLength) {
            return false;
        }

        // Extract components
        $iv = substr($combined, 0, self::IV_LENGTH);
        $tag = substr($combined, self::IV_LENGTH, self::TAG_LENGTH);
        $ciphertext = substr($combined, self::IV_LENGTH + self::TAG_LENGTH);

        // Derive decryption key
        $encryptionKey = self::deriveKey($key);

        // Decrypt the data
        $decrypted = openssl_decrypt(
            $ciphertext,
            self::CIPHER_METHOD,
            $encryptionKey,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        return $decrypted;
    }

    /**
     * Derive an encryption key from the WordPress auth key.
     *
     * @param string|null $key Optional base key (defaults to AUTH_KEY)
     * @return string A 32-byte encryption key
     */
    private static function deriveKey(?string $key = null): string
    {
        $baseKey = $key ?? (defined('AUTH_KEY') ? AUTH_KEY : wp_salt('auth'));

        // Use HKDF to derive a proper encryption key
        return hash_hkdf(
            'sha256',
            $baseKey,
            32,
            'ultimate-security-encryption',
            ''
        );
    }

    /**
     * Check if OpenSSL extension is available with required cipher.
     *
     * @return bool True if OpenSSL is available
     */
    private static function isOpenSSLAvailable(): bool
    {
        return extension_loaded('openssl')
            && in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true);
    }

    /**
     * Generate a cryptographically secure random token.
     *
     * @param int $bytes Number of random bytes (default: 32)
     * @param string $encoding Output encoding: 'hex', 'base64', 'base64url', 'raw'
     * @return string The generated token
     */
    public static function generateToken(int $bytes = 32, string $encoding = 'hex'): string
    {
        $token = random_bytes($bytes);

        return match ($encoding) {
            'hex' => bin2hex($token),
            'base64' => base64_encode($token),
            'base64url' => rtrim(strtr(base64_encode($token), '+/', '-_'), '='),
            'raw' => $token,
            default => bin2hex($token),
        };
    }

    /**
     * Hash a password using WordPress's password hashing.
     *
     * @param string $password The password to hash
     * @return string The hashed password
     */
    public static function hashPassword(string $password): string
    {
        return wp_hash_password($password);
    }

    /**
     * Verify a password against a hash.
     *
     * @param string $password The password to verify
     * @param string $hash The hash to verify against
     * @return bool True if the password matches the hash
     */
    public static function verifyPassword(string $password, string $hash): bool
    {
        return wp_check_password($password, $hash);
    }

    /**
     * Perform a timing-safe string comparison.
     *
     * Prevents timing attacks by ensuring the comparison takes
     * constant time regardless of where strings differ.
     *
     * @param string $a First string
     * @param string $b Second string
     * @return bool True if strings are equal
     */
    public static function compare(string $a, string $b): bool
    {
        return hash_equals($a, $b);
    }

    /**
     * Generate a HMAC signature for data.
     *
     * @param string $data The data to sign
     * @param string|null $key The secret key (defaults to AUTH_KEY)
     * @param string $algo The hash algorithm (default: sha256)
     * @return string The HMAC signature
     */
    public static function hmac(string $data, ?string $key = null, string $algo = 'sha256'): string
    {
        $secretKey = $key ?? (defined('AUTH_KEY') ? AUTH_KEY : wp_salt('auth'));

        return hash_hmac($algo, $data, $secretKey);
    }

    /**
     * Verify a HMAC signature.
     *
     * @param string $data The original data
     * @param string $signature The signature to verify
     * @param string|null $key The secret key
     * @param string $algo The hash algorithm
     * @return bool True if the signature is valid
     */
    public static function verifyHmac(
        string $data,
        string $signature,
        ?string $key = null,
        string $algo = 'sha256'
    ): bool {
        $expectedSignature = self::hmac($data, $key, $algo);

        return self::compare($expectedSignature, $signature);
    }

    /**
     * Create a signed token that can be verified later.
     *
     * The token contains the data and a signature, encoded as a single string.
     *
     * @param array<string, mixed> $data The data to include in the token
     * @param int|null $expiresAt Unix timestamp when token expires (null = no expiry)
     * @param string|null $key The signing key
     * @return string The signed token
     */
    public static function createSignedToken(array $data, ?int $expiresAt = null, ?string $key = null): string
    {
        $payload = [
            'data' => $data,
            'iat' => time(),
        ];

        if ($expiresAt !== null) {
            $payload['exp'] = $expiresAt;
        }

        $payloadJson = wp_json_encode($payload);
        $signature = self::hmac($payloadJson, $key);

        // Combine payload and signature
        $token = base64_encode($payloadJson) . '.' . $signature;

        return $token;
    }

    /**
     * Verify and decode a signed token.
     *
     * @param string $token The token to verify
     * @param string|null $key The signing key
     * @return array{valid: bool, data: array<string, mixed>|null, error: string|null}
     */
    public static function verifySignedToken(string $token, ?string $key = null): array
    {
        $parts = explode('.', $token);

        if (count($parts) !== 2) {
            return [
                'valid' => false,
                'data' => null,
                'error' => 'Invalid token format',
            ];
        }

        [$encodedPayload, $signature] = $parts;

        // Decode payload
        $payloadJson = base64_decode($encodedPayload, true);
        if ($payloadJson === false) {
            return [
                'valid' => false,
                'data' => null,
                'error' => 'Invalid payload encoding',
            ];
        }

        // Verify signature
        if (!self::verifyHmac($payloadJson, $signature, $key)) {
            return [
                'valid' => false,
                'data' => null,
                'error' => 'Invalid signature',
            ];
        }

        // Decode payload JSON
        $payload = json_decode($payloadJson, true);
        if (!is_array($payload)) {
            return [
                'valid' => false,
                'data' => null,
                'error' => 'Invalid payload JSON',
            ];
        }

        // Check expiration
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return [
                'valid' => false,
                'data' => $payload['data'] ?? null,
                'error' => 'Token expired',
            ];
        }

        return [
            'valid' => true,
            'data' => $payload['data'] ?? [],
            'error' => null,
        ];
    }

    /**
     * Generate a one-time password (OTP).
     *
     * @param int $length The OTP length (default: 6)
     * @param bool $alphanumeric Whether to use alphanumeric characters (default: false = numeric only)
     * @return string The generated OTP
     */
    public static function generateOtp(int $length = 6, bool $alphanumeric = false): string
    {
        if ($alphanumeric) {
            return StringHelper::random($length, 'alphanumeric');
        }

        return StringHelper::random($length, 'numeric');
    }

    /**
     * Hash data using a specified algorithm.
     *
     * @param string $data The data to hash
     * @param string $algo The hash algorithm (default: sha256)
     * @param bool $raw Whether to return raw binary (default: false)
     * @return string The hash
     */
    public static function hash(string $data, string $algo = 'sha256', bool $raw = false): string
    {
        return hash($algo, $data, $raw);
    }

    /**
     * Create a secure hash of a file.
     *
     * @param string $filePath The path to the file
     * @param string $algo The hash algorithm (default: sha256)
     * @return string|false The file hash or false on failure
     */
    public static function hashFile(string $filePath, string $algo = 'sha256'): string|false
    {
        if (!file_exists($filePath) || !is_readable($filePath)) {
            return false;
        }

        return hash_file($algo, $filePath);
    }

    /**
     * Generate a secure nonce with optional data binding.
     *
     * @param string $action The action name for the nonce
     * @param int|null $userId Optional user ID to bind the nonce to
     * @return string The nonce
     */
    public static function createNonce(string $action, ?int $userId = null): string
    {
        if ($userId !== null) {
            $action .= '|user:' . $userId;
        }

        return wp_create_nonce($action);
    }

    /**
     * Verify a nonce.
     *
     * @param string $nonce The nonce to verify
     * @param string $action The action name
     * @param int|null $userId Optional user ID the nonce was bound to
     * @return bool|int False if invalid, 1 if generated 0-12 hours ago, 2 if 12-24 hours ago
     */
    public static function verifyNonce(string $nonce, string $action, ?int $userId = null): bool|int
    {
        if ($userId !== null) {
            $action .= '|user:' . $userId;
        }

        return wp_verify_nonce($nonce, $action);
    }

    /**
     * Generate a cryptographically secure random integer.
     *
     * @param int $min Minimum value (inclusive)
     * @param int $max Maximum value (inclusive)
     * @return int A random integer between min and max
     */
    public static function randomInt(int $min, int $max): int
    {
        return random_int($min, $max);
    }

    /**
     * Securely wipe sensitive data from a string variable.
     *
     * Note: This provides best-effort wiping in PHP's managed memory environment.
     *
     * @param string &$string The string to wipe
     * @return void
     */
    public static function wipe(string &$string): void
    {
        if (function_exists('sodium_memzero')) {
            sodium_memzero($string);
        } else {
            $length = strlen($string);
            $string = str_repeat("\0", $length);
            $string = '';
        }
    }

    /**
     * Check if a string looks like it could be encrypted data.
     *
     * @param string $data The data to check
     * @return bool True if the data appears to be encrypted
     */
    public static function isEncrypted(string $data): bool
    {
        // Check if it's valid base64
        $decoded = base64_decode($data, true);
        if ($decoded === false) {
            return false;
        }

        // Check minimum length for IV + tag + data
        $minLength = self::IV_LENGTH + self::TAG_LENGTH + 1;
        if (strlen($decoded) < $minLength) {
            return false;
        }

        // Check if it starts with printable characters (likely not encrypted)
        $firstBytes = substr($decoded, 0, 4);
        if (ctype_print($firstBytes) && ctype_alnum($firstBytes)) {
            return false;
        }

        return true;
    }

    /**
     * Generate a key derivation using PBKDF2.
     *
     * @param string $password The password to derive from
     * @param string $salt The salt (should be random, at least 16 bytes)
     * @param int $iterations Number of iterations (default: 100000)
     * @param int $length Output key length in bytes (default: 32)
     * @param string $algo Hash algorithm (default: sha256)
     * @return string The derived key
     */
    public static function pbkdf2(
        string $password,
        string $salt,
        int $iterations = 100000,
        int $length = 32,
        string $algo = 'sha256'
    ): string {
        return hash_pbkdf2($algo, $password, $salt, $iterations, $length, true);
    }

    /**
     * Get a list of available hash algorithms.
     *
     * @return array<string> List of algorithm names
     */
    public static function getAvailableAlgorithms(): array
    {
        return hash_algos();
    }

    /**
     * Get a list of available cipher methods.
     *
     * @return array<string> List of cipher method names
     */
    public static function getAvailableCiphers(): array
    {
        if (!extension_loaded('openssl')) {
            return [];
        }

        return openssl_get_cipher_methods();
    }
}
