<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use InvalidArgumentException;

/**
 * Advanced Hash Service
 *
 * Provides advanced cryptographic hash functions beyond basic MD5/SHA-1.
 * Supports SHA-3, BLAKE2, and other modern hash algorithms.
 */
final readonly class AdvancedHash
{
    /**
     * Hash data using SHA-3
     */
    public function sha3(string $data, int $length = 256): HashResult
    {
        $supportedLengths = [224, 256, 384, 512];
        if (! in_array($length, $supportedLengths, true)) {
            throw new InvalidArgumentException('SHA-3 length must be 224, 256, 384, or 512 bits');
        }

        $algorithm = "sha3-{$length}";

        if (! in_array($algorithm, hash_algos(), true)) {
            throw new InvalidArgumentException('SHA-3 not available in this PHP installation');
        }

        $hash = hash($algorithm, $data, true);

        return new HashResult(
            hash: $hash,
            algorithm: $algorithm,
            inputLength: strlen($data)
        );
    }

    /**
     * Hash data using BLAKE2b
     */
    public function blake2b(string $data, int $length = 64, ?string $key = null): HashResult
    {
        if ($length < 1 || $length > 64) {
            throw new InvalidArgumentException('BLAKE2b length must be between 1 and 64 bytes');
        }

        if (! function_exists('sodium_crypto_generichash')) {
            throw new InvalidArgumentException('Sodium extension required for BLAKE2b');
        }

        if ($key !== null && (strlen($key) < 16 || strlen($key) > 64)) {
            throw new InvalidArgumentException('BLAKE2b key must be between 16 and 64 bytes');
        }

        $hash = sodium_crypto_generichash($data, $key, $length);

        return new HashResult(
            hash: $hash,
            algorithm: 'blake2b',
            inputLength: strlen($data),
            keyLength: $key ? strlen($key) : null
        );
    }

    /**
     * Hash data using BLAKE2s (faster variant for smaller outputs)
     */
    public function blake2s(string $data, int $length = 32, ?string $key = null): HashResult
    {
        if ($length < 1 || $length > 32) {
            throw new InvalidArgumentException('BLAKE2s length must be between 1 and 32 bytes');
        }

        // Fallback to BLAKE2b if BLAKE2s not available
        if (! function_exists('sodium_crypto_generichash')) {
            throw new InvalidArgumentException('Sodium extension required for BLAKE2s');
        }

        if ($key !== null && (strlen($key) < 16 || strlen($key) > 32)) {
            throw new InvalidArgumentException('BLAKE2s key must be between 16 and 32 bytes');
        }

        // Note: PHP's sodium uses BLAKE2b, but we can limit output length
        $hash = sodium_crypto_generichash($data, $key, $length);

        return new HashResult(
            hash: $hash,
            algorithm: 'blake2s',
            inputLength: strlen($data),
            keyLength: $key ? strlen($key) : null
        );
    }

    /**
     * Hash data using SHAKE128 (extendable-output function)
     */
    public function shake128(string $data, int $outputLength = 32): HashResult
    {
        if ($outputLength < 1 || $outputLength > 1024) {
            throw new InvalidArgumentException('SHAKE128 output length must be between 1 and 1024 bytes');
        }

        // Check if SHAKE128 is available
        if (! in_array('shake128', hash_algos(), true)) {
            throw new InvalidArgumentException('SHAKE128 not available in this PHP installation');
        }

        $hash = hash('shake128', $data, true, ['length' => $outputLength]);

        return new HashResult(
            hash: $hash,
            algorithm: 'shake128',
            inputLength: strlen($data),
            outputLength: $outputLength
        );
    }

    /**
     * Hash data using SHAKE256 (extendable-output function)
     */
    public function shake256(string $data, int $outputLength = 64): HashResult
    {
        if ($outputLength < 1 || $outputLength > 1024) {
            throw new InvalidArgumentException('SHAKE256 output length must be between 1 and 1024 bytes');
        }

        // Check if SHAKE256 is available
        if (! in_array('shake256', hash_algos(), true)) {
            throw new InvalidArgumentException('SHAKE256 not available in this PHP installation');
        }

        $hash = hash('shake256', $data, true, ['length' => $outputLength]);

        return new HashResult(
            hash: $hash,
            algorithm: 'shake256',
            inputLength: strlen($data),
            outputLength: $outputLength
        );
    }

    /**
     * Hash data using xxHash (fast non-cryptographic hash)
     */
    public function xxhash(string $data, int $seed = 0): HashResult
    {
        if (! function_exists('hash')) {
            throw new InvalidArgumentException('Hash extension required');
        }

        // xxHash may not be available in all PHP installations
        if (in_array('xxh64', hash_algos(), true)) {
            $hash = hash('xxh64', $data, true);
            $algorithm = 'xxh64';
        } elseif (in_array('xxh32', hash_algos(), true)) {
            $hash = hash('xxh32', $data, true);
            $algorithm = 'xxh32';
        } else {
            // Fallback to CRC32 for fast non-cryptographic hash
            $hash = pack('N', crc32($data));
            $algorithm = 'crc32';
        }

        return new HashResult(
            hash: $hash,
            algorithm: $algorithm,
            inputLength: strlen($data),
            seed: $seed !== 0 ? $seed : null
        );
    }

    /**
     * Compute HMAC with advanced hash functions
     */
    public function hmac(string $data, string $key, string $algorithm = 'sha3-256'): HashResult
    {
        if (empty($key)) {
            throw new InvalidArgumentException('HMAC key cannot be empty');
        }

        if (strlen($key) < 16) {
            throw new InvalidArgumentException('HMAC key should be at least 16 bytes');
        }

        // For BLAKE2b with key, use keyed hashing directly
        if ($algorithm === 'blake2b') {
            return $this->blake2b($data, 64, $key);
        }

        if ($algorithm === 'blake2s') {
            return $this->blake2s($data, 32, $key);
        }

        // Standard HMAC for other algorithms
        $supportedAlgorithms = ['sha3-256', 'sha3-512', 'sha256', 'sha512'];
        if (! in_array($algorithm, $supportedAlgorithms, true)) {
            throw new InvalidArgumentException('Unsupported HMAC algorithm');
        }

        if (! in_array($algorithm, hash_algos(), true)) {
            throw new InvalidArgumentException("Algorithm {$algorithm} not available");
        }

        $hash = hash_hmac($algorithm, $data, $key, true);

        return new HashResult(
            hash: $hash,
            algorithm: "hmac-{$algorithm}",
            inputLength: strlen($data),
            keyLength: strlen($key)
        );
    }

    /**
     * Verify hash against expected value (timing-safe comparison)
     */
    public function verify(string $data, HashResult $expectedHash): bool
    {
        try {
            $computedHash = match ($expectedHash->getAlgorithm()) {
                'sha3-224' => $this->sha3($data, 224),
                'sha3-256' => $this->sha3($data, 256),
                'sha3-384' => $this->sha3($data, 384),
                'sha3-512' => $this->sha3($data, 512),
                'blake2b' => $this->blake2b(
                    $data,
                    $expectedHash->getOutputLength() ?? 64,
                    null // Key not available for verification
                ),
                'blake2s' => $this->blake2s(
                    $data,
                    $expectedHash->getOutputLength() ?? 32,
                    null // Key not available for verification
                ),
                'shake128' => $this->shake128($data, $expectedHash->getOutputLength() ?? 32),
                'shake256' => $this->shake256($data, $expectedHash->getOutputLength() ?? 64),
                default => throw new InvalidArgumentException('Unsupported algorithm for verification')
            };

            return hash_equals($expectedHash->getHash(), $computedHash->getHash());

        } catch (\Exception) {
            return false;
        }
    }

    /**
     * Hash file using advanced algorithms
     */
    public function hashFile(string $filePath, string $algorithm = 'sha3-256'): HashResult
    {
        if (! file_exists($filePath)) {
            throw new InvalidArgumentException('File does not exist');
        }

        if (! is_readable($filePath)) {
            throw new InvalidArgumentException('File is not readable');
        }

        $fileSize = filesize($filePath);
        if ($fileSize === false) {
            throw new InvalidArgumentException('Cannot determine file size');
        }

        $data = file_get_contents($filePath);
        if ($data === false) {
            throw new InvalidArgumentException('Cannot read file contents');
        }

        return match ($algorithm) {
            'sha3-224' => $this->sha3($data, 224),
            'sha3-256' => $this->sha3($data, 256),
            'sha3-384' => $this->sha3($data, 384),
            'sha3-512' => $this->sha3($data, 512),
            'blake2b' => $this->blake2b($data),
            'blake2s' => $this->blake2s($data),
            'shake128' => $this->shake128($data),
            'shake256' => $this->shake256($data),
            default => throw new InvalidArgumentException("Unsupported algorithm: {$algorithm}")
        };
    }

    /**
     * Get available hash algorithms
     * @return array<int, string>
     */
    public function getAvailableAlgorithms(): array
    {
        $algorithms = [];

        // SHA-3 variants
        foreach ([224, 256, 384, 512] as $length) {
            $algo = "sha3-{$length}";
            if (in_array($algo, hash_algos(), true)) {
                $algorithms[] = $algo;
            }
        }

        // BLAKE2 variants
        if (function_exists('sodium_crypto_generichash')) {
            $algorithms[] = 'blake2b';
            $algorithms[] = 'blake2s';
        }

        // SHAKE variants
        if (in_array('shake128', hash_algos(), true)) {
            $algorithms[] = 'shake128';
        }
        if (in_array('shake256', hash_algos(), true)) {
            $algorithms[] = 'shake256';
        }

        // xxHash variants
        if (in_array('xxh64', hash_algos(), true)) {
            $algorithms[] = 'xxh64';
        }
        if (in_array('xxh32', hash_algos(), true)) {
            $algorithms[] = 'xxh32';
        }

        return $algorithms;
    }

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