<?php

declare(strict_types=1);

namespace App\Framework\Auth;

use App\Framework\Cryptography\DerivedKey;
use DateTimeImmutable;
use InvalidArgumentException;
use SensitiveParameter;

/**
 * HashedPassword Value Object
 *
 * Immutable representation of a hashed password with metadata about the
 * hashing algorithm and parameters used. Encapsulates the DerivedKey
 * for secure password storage and verification.
 */
final readonly class HashedPassword
{
    private DateTimeImmutable $hashedAt;

    /**
     * @param array<string, mixed> $parameters
     */
    public function __construct(
        #[SensitiveParameter]
        private DerivedKey $derivedKey,
        private string $algorithm,
        private array $parameters
    ) {
        if (empty($algorithm)) {
            throw new InvalidArgumentException('Algorithm cannot be empty');
        }

        $this->hashedAt = new DateTimeImmutable();
    }

    /**
     * Create from DerivedKey
     */
    public static function fromDerivedKey(DerivedKey $derivedKey): self
    {
        $algorithm = $derivedKey->getAlgorithm();

        $parameters = match ($algorithm) {
            'argon2id' => [
                'memory_cost' => $derivedKey->getMemoryCost(),
                'time_cost' => $derivedKey->getIterations(),
                'threads' => $derivedKey->getThreads(),
                'key_length' => $derivedKey->getKeyLength(),
            ],
            'pbkdf2-sha256', 'pbkdf2-sha512' => [
                'iterations' => $derivedKey->getIterations(),
                'key_length' => $derivedKey->getKeyLength(),
            ],
            'scrypt' => [
                'cost_parameter' => $derivedKey->getIterations(),
                'block_size' => $derivedKey->getBlockSize(),
                'parallelization' => $derivedKey->getParallelization(),
                'key_length' => $derivedKey->getKeyLength(),
            ],
            default => []
        };

        return new self($derivedKey, $algorithm, $parameters);
    }

    /**
     * Get the underlying DerivedKey
     */
    public function getDerivedKey(): DerivedKey
    {
        return $this->derivedKey;
    }

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

    /**
     * Get the parameters used for hashing
     * @return array<string, mixed>
     */
    public function getParameters(): array
    {
        return $this->parameters;
    }

    /**
     * Get when the password was hashed
     */
    public function getHashedAt(): DateTimeImmutable
    {
        return $this->hashedAt;
    }

    /**
     * Serialize for storage (database)
     */
    public function serialize(): string
    {
        $data = [
            'derived_key' => $this->derivedKey->toArray(),
            'algorithm' => $this->algorithm,
            'parameters' => $this->parameters,
            'hashed_at' => $this->hashedAt->format('c'),
        ];

        $json = json_encode($data);
        if ($json === false) {
            throw new \RuntimeException('Failed to encode password data');
        }

        return base64_encode($json);
    }

    /**
     * Deserialize from storage
     */
    public static function deserialize(#[SensitiveParameter] string $serialized): self
    {
        $decoded = base64_decode($serialized, true);

        if ($decoded === false) {
            throw new InvalidArgumentException('Invalid base64 encoding');
        }

        $data = json_decode($decoded, true);

        if (! is_array($data)) {
            throw new InvalidArgumentException('Invalid serialized data');
        }

        if (! isset($data['derived_key'], $data['algorithm'], $data['parameters'])) {
            throw new InvalidArgumentException('Missing required fields in serialized data');
        }

        $derivedKey = DerivedKey::fromArray($data['derived_key']);

        $hashedPassword = new self(
            $derivedKey,
            $data['algorithm'],
            $data['parameters']
        );

        // Note: hashedAt will be set to current time, not the original
        // If you need to preserve the original timestamp, extend the constructor

        return $hashedPassword;
    }

    /**
     * Check if rehashing is needed based on current standards
     * @param array<string, mixed> $currentParameters
     */
    public function needsRehash(string $currentAlgorithm, array $currentParameters): bool
    {
        // Different algorithm -> needs rehash
        if ($this->algorithm !== $currentAlgorithm) {
            return true;
        }

        // Check parameters based on algorithm
        return match ($this->algorithm) {
            'argon2id' => $this->needsArgon2Rehash($currentParameters),
            'pbkdf2-sha256', 'pbkdf2-sha512' => $this->needsPbkdf2Rehash($currentParameters),
            'scrypt' => $this->needsScryptRehash($currentParameters),
            default => true // Unknown algorithm -> rehash
        };
    }

    /**
     * @param array<string, mixed> $currentParams
     */
    private function needsArgon2Rehash(array $currentParams): bool
    {
        $memoryCost = $this->parameters['memory_cost'] ?? 0;
        $timeCost = $this->parameters['time_cost'] ?? 0;
        $threads = $this->parameters['threads'] ?? 0;

        return $memoryCost < ($currentParams['memory_cost'] ?? 65536) ||
               $timeCost < ($currentParams['time_cost'] ?? 4) ||
               $threads < ($currentParams['threads'] ?? 3);
    }

    /**
     * @param array<string, mixed> $currentParams
     */
    private function needsPbkdf2Rehash(array $currentParams): bool
    {
        $iterations = $this->parameters['iterations'] ?? 0;

        return $iterations < ($currentParams['iterations'] ?? 100000);
    }

    /**
     * @param array<string, mixed> $currentParams
     */
    private function needsScryptRehash(array $currentParams): bool
    {
        $costParameter = $this->parameters['cost_parameter'] ?? 0;
        $blockSize = $this->parameters['block_size'] ?? 0;

        return $costParameter < ($currentParams['cost_parameter'] ?? 16384) ||
               $blockSize < ($currentParams['block_size'] ?? 8);
    }

    /**
     * Get safe representation for logging (no sensitive data)
     * @return array<string, mixed>
     */
    public function getSafeSummary(): array
    {
        return [
            'algorithm' => $this->algorithm,
            'parameters' => $this->parameters,
            'hashed_at' => $this->hashedAt->format('c'),
            'salt_length' => strlen($this->derivedKey->getSalt()),
            'key_length' => $this->derivedKey->getKeyLength(),
        ];
    }

    /**
     * Check if password is using a legacy algorithm
     */
    public function isLegacy(): bool
    {
        $legacyAlgorithms = [
            'bcrypt',
            'md5',
            'sha1',
            'sha256', // Plain SHA256, not PBKDF2-SHA256
        ];

        foreach ($legacyAlgorithms as $legacy) {
            if (str_contains($this->algorithm, $legacy)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if password is using recommended algorithm
     */
    public function isUsingRecommendedAlgorithm(): bool
    {
        $recommendedAlgorithms = [
            'argon2id',
            'argon2i',
            'scrypt',
        ];

        return in_array($this->algorithm, $recommendedAlgorithms, true);
    }

    /**
     * Get strength assessment
     */
    public function getStrengthAssessment(): PasswordStrength
    {
        if ($this->isLegacy()) {
            return PasswordStrength::WEAK;
        }

        return match ($this->algorithm) {
            'argon2id' => $this->assessArgon2Strength(),
            'pbkdf2-sha256', 'pbkdf2-sha512' => $this->assessPbkdf2Strength(),
            'scrypt' => $this->assessScryptStrength(),
            default => PasswordStrength::UNKNOWN
        };
    }

    private function assessArgon2Strength(): PasswordStrength
    {
        $memoryCost = $this->parameters['memory_cost'] ?? 0;
        $timeCost = $this->parameters['time_cost'] ?? 0;

        if ($memoryCost >= 131072 && $timeCost >= 6) {
            return PasswordStrength::VERY_STRONG;
        }

        if ($memoryCost >= 65536 && $timeCost >= 4) {
            return PasswordStrength::STRONG;
        }

        if ($memoryCost >= 32768 && $timeCost >= 3) {
            return PasswordStrength::MODERATE;
        }

        return PasswordStrength::WEAK;
    }

    private function assessPbkdf2Strength(): PasswordStrength
    {
        $iterations = $this->parameters['iterations'] ?? 0;

        if ($iterations >= 200000) {
            return PasswordStrength::STRONG;
        }

        if ($iterations >= 100000) {
            return PasswordStrength::MODERATE;
        }

        return PasswordStrength::WEAK;
    }

    private function assessScryptStrength(): PasswordStrength
    {
        $costParameter = $this->parameters['cost_parameter'] ?? 0;

        if ($costParameter >= 32768) {
            return PasswordStrength::VERY_STRONG;
        }

        if ($costParameter >= 16384) {
            return PasswordStrength::STRONG;
        }

        if ($costParameter >= 8192) {
            return PasswordStrength::MODERATE;
        }

        return PasswordStrength::WEAK;
    }

    /**
     * Check if password hash is expired (for rotation policies)
     */
    public function isExpired(int $maxAgeInDays): bool
    {
        $now = new DateTimeImmutable();
        $diff = $now->diff($this->hashedAt);

        return $diff->days > $maxAgeInDays;
    }

    /**
     * Get age in days
     */
    public function getAgeInDays(): int
    {
        $now = new DateTimeImmutable();
        $diff = $now->diff($this->hashedAt);

        return (int) $diff->days;
    }
}
