<?php

declare(strict_types=1);

namespace App\Framework\Cryptography;

use InvalidArgumentException;

/**
 * Derived Key Value Object
 *
 * Represents a key derived from a password using key derivation functions.
 * Contains the derived key material and all parameters needed for verification.
 */
final readonly class DerivedKey
{
    public function __construct(
        private string $key,
        private string $salt,
        private string $algorithm,
        private int $iterations,
        private int $keyLength,
        private ?int $memoryCost = null,
        private ?int $threads = null,
        private ?int $blockSize = null,
        private ?int $parallelization = null
    ) {
        if (empty($key)) {
            throw new InvalidArgumentException('Key cannot be empty');
        }

        if (empty($salt)) {
            throw new InvalidArgumentException('Salt cannot be empty');
        }

        if (empty($algorithm)) {
            throw new InvalidArgumentException('Algorithm cannot be empty');
        }

        if ($iterations < 1) {
            throw new InvalidArgumentException('Iterations must be positive');
        }

        if ($keyLength < 1) {
            throw new InvalidArgumentException('Key length must be positive');
        }

        if (strlen($key) !== $keyLength) {
            throw new InvalidArgumentException('Key length does not match specified length');
        }
    }

    /**
     * Get the derived key bytes
     */
    public function getKey(): string
    {
        return $this->key;
    }

    /**
     * Get the salt used for derivation
     */
    public function getSalt(): string
    {
        return $this->salt;
    }

    /**
     * Get the algorithm used
     */
    public function getAlgorithm(): string
    {
        return $this->algorithm;
    }

    /**
     * Get the iteration count
     */
    public function getIterations(): int
    {
        return $this->iterations;
    }

    /**
     * Get the key length in bytes
     */
    public function getKeyLength(): int
    {
        return $this->keyLength;
    }

    /**
     * Get memory cost (Argon2 only)
     */
    public function getMemoryCost(): ?int
    {
        return $this->memoryCost;
    }

    /**
     * Get thread count (Argon2 only)
     */
    public function getThreads(): ?int
    {
        return $this->threads;
    }

    /**
     * Get block size (scrypt only)
     */
    public function getBlockSize(): ?int
    {
        return $this->blockSize;
    }

    /**
     * Get parallelization factor (scrypt only)
     */
    public function getParallelization(): ?int
    {
        return $this->parallelization;
    }

    /**
     * Get the key as hexadecimal string
     */
    public function getKeyHex(): string
    {
        return bin2hex($this->key);
    }

    /**
     * Get the salt as hexadecimal string
     */
    public function getSaltHex(): string
    {
        return bin2hex($this->salt);
    }

    /**
     * Get key as Base64 string
     */
    public function getKeyBase64(): string
    {
        return base64_encode($this->key);
    }

    /**
     * Get salt as Base64 string
     */
    public function getSaltBase64(): string
    {
        return base64_encode($this->salt);
    }

    /**
     * Check equality with another DerivedKey
     */
    public function equals(self $other): bool
    {
        return hash_equals($this->key, $other->key) &&
               hash_equals($this->salt, $other->salt) &&
               $this->algorithm === $other->algorithm &&
               $this->iterations === $other->iterations &&
               $this->keyLength === $other->keyLength &&
               $this->memoryCost === $other->memoryCost &&
               $this->threads === $other->threads &&
               $this->blockSize === $other->blockSize &&
               $this->parallelization === $other->parallelization;
    }

    /**
     * Export to array (for serialization)
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $data = [
            'key' => $this->getKeyBase64(),
            'salt' => $this->getSaltBase64(),
            'algorithm' => $this->algorithm,
            'iterations' => $this->iterations,
            'key_length' => $this->keyLength,
        ];

        if ($this->memoryCost !== null) {
            $data['memory_cost'] = $this->memoryCost;
        }

        if ($this->threads !== null) {
            $data['threads'] = $this->threads;
        }

        if ($this->blockSize !== null) {
            $data['block_size'] = $this->blockSize;
        }

        if ($this->parallelization !== null) {
            $data['parallelization'] = $this->parallelization;
        }

        return $data;
    }

    /**
     * Create from array (for deserialization)
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        $requiredFields = ['key', 'salt', 'algorithm', 'iterations', 'key_length'];

        foreach ($requiredFields as $field) {
            if (! isset($data[$field])) {
                throw new InvalidArgumentException("Missing required field: {$field}");
            }
        }

        try {
            $key = base64_decode($data['key'], true);
            $salt = base64_decode($data['salt'], true);

            if ($key === false || $salt === false) {
                throw new InvalidArgumentException('Invalid Base64 encoding');
            }
        } catch (\Exception) {
            throw new InvalidArgumentException('Failed to decode Base64 data');
        }

        return new self(
            key: $key,
            salt: $salt,
            algorithm: $data['algorithm'],
            iterations: (int)$data['iterations'],
            keyLength: (int)$data['key_length'],
            memoryCost: isset($data['memory_cost']) ? (int)$data['memory_cost'] : null,
            threads: isset($data['threads']) ? (int)$data['threads'] : null,
            blockSize: isset($data['block_size']) ? (int)$data['block_size'] : null,
            parallelization: isset($data['parallelization']) ? (int)$data['parallelization'] : null
        );
    }

    /**
     * Create from hexadecimal strings
     */
    public static function fromHex(
        string $keyHex,
        string $saltHex,
        string $algorithm,
        int $iterations,
        int $keyLength,
        ?int $memoryCost = null,
        ?int $threads = null,
        ?int $blockSize = null,
        ?int $parallelization = null
    ): self {
        $key = hex2bin($keyHex);
        $salt = hex2bin($saltHex);

        if ($key === false || $salt === false) {
            throw new InvalidArgumentException('Invalid hexadecimal encoding');
        }

        return new self(
            key: $key,
            salt: $salt,
            algorithm: $algorithm,
            iterations: $iterations,
            keyLength: $keyLength,
            memoryCost: $memoryCost,
            threads: $threads,
            blockSize: $blockSize,
            parallelization: $parallelization
        );
    }

    /**
     * Get summary information (safe for logging)
     * @return array<string, mixed>
     */
    public function getSummary(): array
    {
        return [
            'algorithm' => $this->algorithm,
            'iterations' => $this->iterations,
            'key_length' => $this->keyLength,
            'salt_length' => strlen($this->salt),
            'memory_cost' => $this->memoryCost,
            'threads' => $this->threads,
            'block_size' => $this->blockSize,
            'parallelization' => $this->parallelization,
        ];
    }

    /**
     * Check if this is an Argon2 derived key
     */
    public function isArgon2(): bool
    {
        return $this->algorithm === 'argon2id';
    }

    /**
     * Check if this is a PBKDF2 derived key
     */
    public function isPbkdf2(): bool
    {
        return str_starts_with($this->algorithm, 'pbkdf2-');
    }

    /**
     * Check if this is a scrypt derived key
     */
    public function isScrypt(): bool
    {
        return $this->algorithm === 'scrypt';
    }
}
