<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;

/**
 * Cryptographic Utilities Service
 *
 * Provides various cryptographic utility functions including timing-safe
 * comparisons, secure random validation, and constant-time operations.
 */
final readonly class CryptographicUtilities
{
    public function __construct(
        private RandomGenerator $randomGenerator
    ) {
    }

    /**
     * Timing-safe string comparison
     */
    public function timingSafeEquals(string $known, string $user): bool
    {
        return hash_equals($known, $user);
    }

    /**
     * Timing-safe comparison of arrays
     * @param array<mixed> $known
     * @param array<mixed> $user
     */
    public function timingSafeArrayEquals(array $known, array $user): bool
    {
        if (count($known) !== count($user)) {
            return false;
        }

        // Sort both arrays to ensure consistent comparison
        ksort($known);
        ksort($user);

        $knownSerialized = serialize($known);
        $userSerialized = serialize($user);

        return hash_equals($knownSerialized, $userSerialized);
    }

    /**
     * Generate cryptographically secure nonce
     */
    public function generateNonce(int $length = 32): string
    {
        if ($length < 8) {
            throw new InvalidArgumentException('Nonce length must be at least 8 bytes');
        }

        if ($length > 256) {
            throw new InvalidArgumentException('Nonce length cannot exceed 256 bytes');
        }

        return $this->randomGenerator->bytes($length);
    }

    /**
     * Generate initialization vector (IV)
     */
    public function generateIv(int $length = 16): string
    {
        if ($length < 8) {
            throw new InvalidArgumentException('IV length must be at least 8 bytes');
        }

        if ($length > 32) {
            throw new InvalidArgumentException('IV length cannot exceed 32 bytes');
        }

        return $this->randomGenerator->bytes($length);
    }

    /**
     * Validate entropy of random data
     */
    public function validateEntropy(string $data, float $minimumEntropy = 7.0): bool
    {
        if (empty($data)) {
            return false;
        }

        $entropy = $this->calculateShannonEntropy($data);

        return $entropy >= $minimumEntropy;
    }

    /**
     * Calculate Shannon entropy of data
     */
    public function calculateShannonEntropy(string $data): float
    {
        if (empty($data)) {
            return 0.0;
        }

        $length = strlen($data);
        $frequencies = [];

        // Count byte frequencies
        for ($i = 0; $i < $length; $i++) {
            $byte = ord($data[$i]);
            $frequencies[$byte] = ($frequencies[$byte] ?? 0) + 1;
        }

        $entropy = 0.0;

        foreach ($frequencies as $frequency) {
            $probability = $frequency / $length;
            if ($probability > 0) {
                $entropy -= $probability * log($probability, 2);
            }
        }

        return $entropy;
    }

    /**
     * Constant-time modular exponentiation
     */
    public function constantTimeModPow(string $base, string $exponent, string $modulus): string
    {
        if (! extension_loaded('gmp')) {
            throw new InvalidArgumentException('GMP extension required for modular exponentiation');
        }

        $baseGmp = gmp_init($base, 10);
        $exponentGmp = gmp_init($exponent, 10);
        $modulusGmp = gmp_init($modulus, 10);

        if ($baseGmp === false || $exponentGmp === false || $modulusGmp === false) {
            throw new InvalidArgumentException('Invalid numeric input');
        }

        $result = gmp_powm($baseGmp, $exponentGmp, $modulusGmp);

        return gmp_strval($result);
    }

    /**
     * Secure memory wipe (attempt to clear sensitive data)
     */
    public function secureWipe(string &$data): void
    {
        if (function_exists('sodium_memzero')) {
            sodium_memzero($data);
        } else {
            // Fallback: overwrite with random data multiple times
            $length = strlen($data);

            for ($pass = 0; $pass < 3; $pass++) {
                $data = $this->randomGenerator->bytes($length);
            }

            $data = str_repeat("\0", $length);
        }

        $data = '';
    }

    /**
     * Generate cryptographically secure UUID v4
     */
    public function generateUuid4(): string
    {
        $data = $this->randomGenerator->bytes(16);

        // Set version to 4
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);

        // Set variant
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

        return sprintf(
            '%s-%s-%s-%s-%s',
            bin2hex(substr($data, 0, 4)),
            bin2hex(substr($data, 4, 2)),
            bin2hex(substr($data, 6, 2)),
            bin2hex(substr($data, 8, 2)),
            bin2hex(substr($data, 10, 6))
        );
    }

    /**
     * Key stretching using iteration
     */
    public function stretchKey(
        string $key,
        string $salt,
        int $iterations = 10000,
        int $outputLength = 32
    ): string {
        if ($iterations < 1000) {
            throw new InvalidArgumentException('Iterations must be at least 1000');
        }

        if ($outputLength < 16 || $outputLength > 256) {
            throw new InvalidArgumentException('Output length must be between 16 and 256 bytes');
        }

        return hash_pbkdf2('sha256', $key, $salt, $iterations, $outputLength, true);
    }

    /**
     * XOR two strings (for XOR cipher operations)
     */
    public function xorStrings(string $str1, string $str2): string
    {
        $length = min(strlen($str1), strlen($str2));
        $result = '';

        for ($i = 0; $i < $length; $i++) {
            $result .= chr(ord($str1[$i]) ^ ord($str2[$i]));
        }

        return $result;
    }

    /**
     * Generate cryptographically secure random padding
     */
    public function generatePadding(int $blockSize, int $dataLength): string
    {
        if ($blockSize < 1 || $blockSize > 256) {
            throw new InvalidArgumentException('Block size must be between 1 and 256');
        }

        $paddingLength = $blockSize - ($dataLength % $blockSize);

        // PKCS#7 padding
        return str_repeat(chr($paddingLength), $paddingLength);
    }

    /**
     * Remove PKCS#7 padding
     */
    public function removePadding(string $data): string
    {
        if (empty($data)) {
            return $data;
        }

        $paddingLength = ord($data[strlen($data) - 1]);

        if ($paddingLength < 1 || $paddingLength > 16) {
            throw new InvalidArgumentException('Invalid padding');
        }

        // Verify padding
        $padding = substr($data, -$paddingLength);
        if ($padding !== str_repeat(chr($paddingLength), $paddingLength)) {
            throw new InvalidArgumentException('Invalid padding format');
        }

        return substr($data, 0, -$paddingLength);
    }

    /**
     * Generate cryptographically secure bit string
     */
    public function generateBitString(int $bits): string
    {
        if ($bits < 8) {
            throw new InvalidArgumentException('Bit length must be at least 8');
        }

        if ($bits % 8 !== 0) {
            throw new InvalidArgumentException('Bit length must be divisible by 8');
        }

        $bytes = intval($bits / 8);
        $randomBytes = $this->randomGenerator->bytes($bytes);

        $bitString = '';
        for ($i = 0; $i < $bytes; $i++) {
            $bitString .= sprintf('%08b', ord($randomBytes[$i]));
        }

        return $bitString;
    }

    /**
     * Convert bit string to bytes
     */
    public function bitStringToBytes(string $bitString): string
    {
        if (strlen($bitString) % 8 !== 0) {
            throw new InvalidArgumentException('Bit string length must be divisible by 8');
        }

        if (! preg_match('/^[01]+$/', $bitString)) {
            throw new InvalidArgumentException('Bit string contains invalid characters');
        }

        $bytes = '';
        $chunks = str_split($bitString, 8);

        foreach ($chunks as $chunk) {
            $bytes .= chr(bindec($chunk));
        }

        return $bytes;
    }

    /**
     * Validate cryptographic key strength
     */
    public function validateKeyStrength(string $key, int $minimumBits = 128): bool
    {
        $keyLengthBits = strlen($key) * 8;

        if ($keyLengthBits < $minimumBits) {
            return false;
        }

        // Check entropy
        return $this->validateEntropy($key, 6.0);
    }

    /**
     * Generate deterministic UUID from data (UUID v5)
     */
    public function generateUuid5(string $namespace, string $name): string
    {
        if (strlen($namespace) !== 16) {
            throw new InvalidArgumentException('Namespace must be 16 bytes');
        }

        $hash = hash('sha1', $namespace . $name, true);

        // Set version to 5
        $hash[6] = chr(ord($hash[6]) & 0x0f | 0x50);

        // Set variant
        $hash[8] = chr(ord($hash[8]) & 0x3f | 0x80);

        return sprintf(
            '%s-%s-%s-%s-%s',
            bin2hex(substr($hash, 0, 4)),
            bin2hex(substr($hash, 4, 2)),
            bin2hex(substr($hash, 6, 2)),
            bin2hex(substr($hash, 8, 2)),
            bin2hex(substr($hash, 10, 6))
        );
    }

    /**
     * Constant-time array search
     */
    public function constantTimeArraySearch(array $haystack, mixed $needle): bool
    {
        $found = false;

        foreach ($haystack as $value) {
            if (is_string($value) && is_string($needle)) {
                $found = $found || hash_equals($value, $needle);
            } else {
                $found = $found || ($value === $needle);
            }
        }

        return $found;
    }

    /**
     * Factory method
     */
    public static function create(RandomGenerator $randomGenerator): self
    {
        return new self($randomGenerator);
    }
}
