<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

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

/**
 * Key Derivation Function Service
 *
 * Provides secure key derivation functions for password hashing and key stretching.
 * Supports PBKDF2, Argon2ID, and scrypt algorithms.
 */
final readonly class KeyDerivationFunction
{
    public function __construct(
        private RandomGenerator $randomGenerator
    ) {
    }

    /**
     * Generate salt for key derivation
     */
    public function generateSalt(int $length = 32): string
    {
        if ($length < 16) {
            throw new InvalidArgumentException('Salt length must be at least 16 bytes');
        }

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

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

    /**
     * Derive key using PBKDF2
     */
    public function pbkdf2(
        string $password,
        string $salt,
        int $iterations = 100000,
        int $keyLength = 32,
        string $algorithm = 'sha256'
    ): DerivedKey {
        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }

        if (strlen($salt) < 16) {
            throw new InvalidArgumentException('Salt must be at least 16 bytes');
        }

        if ($iterations < 10000) {
            throw new InvalidArgumentException('Iterations must be at least 10,000 for security');
        }

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

        $supportedAlgorithms = ['sha256', 'sha512', 'sha384', 'sha224'];
        if (! in_array($algorithm, $supportedAlgorithms, true)) {
            throw new InvalidArgumentException('Unsupported hash algorithm');
        }

        $derivedKey = hash_pbkdf2($algorithm, $password, $salt, $iterations, $keyLength, true);

        return new DerivedKey(
            key: $derivedKey,
            salt: $salt,
            algorithm: "pbkdf2-{$algorithm}",
            iterations: $iterations,
            keyLength: $keyLength
        );
    }

    /**
     * Derive key using Argon2ID (recommended)
     */
    public function argon2id(
        string $password,
        string $salt,
        int $memoryCost = 65536, // 64 MB
        int $timeCost = 4,
        int $threads = 3,
        int $keyLength = 32
    ): DerivedKey {
        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }

        if (strlen($salt) < 16) {
            throw new InvalidArgumentException('Salt must be at least 16 bytes');
        }

        if ($memoryCost < 1024) {
            throw new InvalidArgumentException('Memory cost must be at least 1024 KB');
        }

        if ($timeCost < 1) {
            throw new InvalidArgumentException('Time cost must be at least 1');
        }

        if ($threads < 1 || $threads > 24) {
            throw new InvalidArgumentException('Threads must be between 1 and 24');
        }

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

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

        if (! defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID')) {
            throw new InvalidArgumentException('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID constant not available');
        }

        $derivedKey = sodium_crypto_pwhash(
            $keyLength,
            $password,
            $salt,
            $timeCost,
            $memoryCost,
            SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID
        );

        return new DerivedKey(
            key: $derivedKey,
            salt: $salt,
            algorithm: 'argon2id',
            iterations: $timeCost,
            keyLength: $keyLength,
            memoryCost: $memoryCost,
            threads: $threads
        );
    }

    /**
     * Derive key using scrypt
     */
    public function scrypt(
        string $password,
        string $salt,
        int $costParameter = 16384, // N
        int $blockSize = 8,         // r
        int $parallelization = 1,   // p
        int $keyLength = 32
    ): DerivedKey {
        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }

        if (strlen($salt) < 16) {
            throw new InvalidArgumentException('Salt must be at least 16 bytes');
        }

        if ($costParameter < 1024) {
            throw new InvalidArgumentException('Cost parameter must be at least 1024');
        }

        if ($blockSize < 1) {
            throw new InvalidArgumentException('Block size must be at least 1');
        }

        if ($parallelization < 1) {
            throw new InvalidArgumentException('Parallelization must be at least 1');
        }

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

        if (! function_exists('scrypt')) {
            throw new InvalidArgumentException('scrypt function not available');
        }

        $derivedKey = scrypt(
            $password,
            $salt,
            $costParameter,
            $blockSize,
            $parallelization,
            $keyLength
        );

        return new DerivedKey(
            key: $derivedKey,
            salt: $salt,
            algorithm: 'scrypt',
            iterations: $costParameter,
            keyLength: $keyLength,
            blockSize: $blockSize,
            parallelization: $parallelization
        );
    }

    /**
     * Verify password against derived key
     */
    public function verify(string $password, DerivedKey $derivedKey): bool
    {
        try {
            $verificationKey = match ($derivedKey->getAlgorithm()) {
                'pbkdf2-sha256' => $this->pbkdf2(
                    $password,
                    $derivedKey->getSalt(),
                    $derivedKey->getIterations(),
                    $derivedKey->getKeyLength(),
                    'sha256'
                ),
                'pbkdf2-sha512' => $this->pbkdf2(
                    $password,
                    $derivedKey->getSalt(),
                    $derivedKey->getIterations(),
                    $derivedKey->getKeyLength(),
                    'sha512'
                ),
                'argon2id' => $this->argon2id(
                    $password,
                    $derivedKey->getSalt(),
                    $derivedKey->getMemoryCost() ?? 65536,
                    $derivedKey->getIterations(),
                    $derivedKey->getThreads() ?? 3,
                    $derivedKey->getKeyLength()
                ),
                'scrypt' => $this->scrypt(
                    $password,
                    $derivedKey->getSalt(),
                    $derivedKey->getIterations(),
                    $derivedKey->getBlockSize() ?? 8,
                    $derivedKey->getParallelization() ?? 1,
                    $derivedKey->getKeyLength()
                ),
                default => throw new InvalidArgumentException('Unsupported algorithm: ' . $derivedKey->getAlgorithm())
            };

            return hash_equals($derivedKey->getKey(), $verificationKey->getKey());

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

    /**
     * Hash password with automatic salt generation (recommended for new passwords)
     */
    public function hashPassword(
        string $password,
        string $algorithm = 'argon2id',
        array $options = []
    ): DerivedKey {
        $salt = $this->generateSalt(32);

        return match ($algorithm) {
            'pbkdf2-sha256' => $this->pbkdf2(
                $password,
                $salt,
                $options['iterations'] ?? 100000,
                $options['key_length'] ?? 32,
                'sha256'
            ),
            'pbkdf2-sha512' => $this->pbkdf2(
                $password,
                $salt,
                $options['iterations'] ?? 100000,
                $options['key_length'] ?? 32,
                'sha512'
            ),
            'argon2id' => $this->argon2id(
                $password,
                $salt,
                $options['memory_cost'] ?? 65536,
                $options['time_cost'] ?? 4,
                $options['threads'] ?? 3,
                $options['key_length'] ?? 32
            ),
            'scrypt' => $this->scrypt(
                $password,
                $salt,
                $options['cost_parameter'] ?? 16384,
                $options['block_size'] ?? 8,
                $options['parallelization'] ?? 1,
                $options['key_length'] ?? 32
            ),
            default => throw new InvalidArgumentException("Unsupported algorithm: {$algorithm}")
        };
    }

    /**
     * Get recommended parameters for different security levels
     * @return array<string, mixed>
     */
    public function getRecommendedParameters(string $algorithm, string $securityLevel = 'standard'): array
    {
        return match ([$algorithm, $securityLevel]) {
            ['pbkdf2-sha256', 'low'] => ['iterations' => 50000, 'key_length' => 32],
            ['pbkdf2-sha256', 'standard'] => ['iterations' => 100000, 'key_length' => 32],
            ['pbkdf2-sha256', 'high'] => ['iterations' => 200000, 'key_length' => 32],

            ['argon2id', 'low'] => ['memory_cost' => 32768, 'time_cost' => 3, 'threads' => 2, 'key_length' => 32],
            ['argon2id', 'standard'] => ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, 'key_length' => 32],
            ['argon2id', 'high'] => ['memory_cost' => 131072, 'time_cost' => 6, 'threads' => 4, 'key_length' => 32],

            ['scrypt', 'low'] => ['cost_parameter' => 8192, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32],
            ['scrypt', 'standard'] => ['cost_parameter' => 16384, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32],
            ['scrypt', 'high'] => ['cost_parameter' => 32768, 'block_size' => 8, 'parallelization' => 2, 'key_length' => 32],

            default => throw new InvalidArgumentException("Unsupported algorithm or security level: {$algorithm}, {$securityLevel}")
        };
    }

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