<?php

declare(strict_types=1);

namespace App\Framework\Auth;

use App\Framework\Cryptography\KeyDerivationFunction;
use InvalidArgumentException;
use SensitiveParameter;

/**
 * Password Hasher Service
 *
 * Provides secure password hashing and verification using the framework's
 * cryptography module. Supports automatic rehashing when security standards
 * are updated.
 */
final readonly class PasswordHasher
{
    public const int MIN_PASSWORD_LENGTH = 8;
    public const int MAX_PASSWORD_LENGTH = 4096;

    // Default security levels
    public const string LEVEL_LOW = 'low';
    public const string LEVEL_STANDARD = 'standard';
    public const string LEVEL_HIGH = 'high';

    public function __construct(
        private KeyDerivationFunction $kdf,
        private string $defaultAlgorithm = 'argon2id',
        private string $defaultSecurityLevel = self::LEVEL_STANDARD
    ) {
        $this->validateConfiguration();
    }

    /**
     * Hash a plain text password
     */
    public function hash(
        #[SensitiveParameter]
        string $plainPassword,
        ?string $algorithm = null,
        ?string $securityLevel = null
    ): HashedPassword {
        $this->validatePassword($plainPassword);

        $algorithm = $algorithm ?? $this->defaultAlgorithm;
        $securityLevel = $securityLevel ?? $this->defaultSecurityLevel;

        $derivedKey = $this->kdf->hashPassword(
            $plainPassword,
            $algorithm,
            $this->getParametersForLevel($algorithm, $securityLevel)
        );

        return HashedPassword::fromDerivedKey($derivedKey);
    }

    /**
     * Verify a password against a hash
     */
    public function verify(
        #[SensitiveParameter]
        string $plainPassword,
        HashedPassword $hashedPassword
    ): bool {
        if (empty($plainPassword)) {
            return false;
        }

        try {
            return $this->kdf->verify($plainPassword, $hashedPassword->getDerivedKey());
        } catch (\Exception) {
            // Log exception for debugging but don't expose details
            return false;
        }
    }

    /**
     * Check if a password hash needs to be rehashed
     */
    public function needsRehash(
        HashedPassword $hashedPassword,
        ?string $algorithm = null,
        ?string $securityLevel = null
    ): bool {
        $algorithm = $algorithm ?? $this->defaultAlgorithm;
        $securityLevel = $securityLevel ?? $this->defaultSecurityLevel;

        $currentParameters = $this->getParametersForLevel($algorithm, $securityLevel);

        return $hashedPassword->needsRehash($algorithm, $currentParameters);
    }

    /**
     * Rehash a password if needed (requires plain password)
     */
    public function rehashIfNeeded(
        #[SensitiveParameter]
        string $plainPassword,
        HashedPassword $currentHash,
        ?string $algorithm = null,
        ?string $securityLevel = null
    ): ?HashedPassword {
        if (! $this->needsRehash($currentHash, $algorithm, $securityLevel)) {
            return null;
        }

        return $this->hash($plainPassword, $algorithm, $securityLevel);
    }

    /**
     * Validate password strength
     */
    public function validatePasswordStrength(
        #[SensitiveParameter]
        string $plainPassword
    ): PasswordValidationResult {
        $errors = [];
        $warnings = [];
        $score = 100;

        $length = mb_strlen($plainPassword);

        // Length validation
        if ($length < self::MIN_PASSWORD_LENGTH) {
            $errors[] = sprintf('Password must be at least %d characters long', self::MIN_PASSWORD_LENGTH);
            $score -= 50;
        } elseif ($length < 12) {
            $warnings[] = 'Consider using a longer password (12+ characters recommended)';
            $score -= 10;
        }

        // Complexity checks
        $hasUppercase = preg_match('/[A-Z]/', $plainPassword);
        $hasLowercase = preg_match('/[a-z]/', $plainPassword);
        $hasNumbers = preg_match('/[0-9]/', $plainPassword);
        $hasSpecialChars = preg_match('/[^A-Za-z0-9]/', $plainPassword);

        $complexityCount = (int)$hasUppercase + (int)$hasLowercase + (int)$hasNumbers + (int)$hasSpecialChars;

        if ($complexityCount < 2) {
            $errors[] = 'Password must contain at least 2 different character types';
            $score -= 30;
        } elseif ($complexityCount < 3) {
            $warnings[] = 'Consider using more character types for better security';
            $score -= 10;
        }

        // Common patterns
        if ($this->containsCommonPatterns($plainPassword)) {
            $warnings[] = 'Password contains common patterns';
            $score -= 20;
        }

        // Sequential characters
        if ($this->hasSequentialCharacters($plainPassword)) {
            $warnings[] = 'Avoid sequential characters (e.g., "123", "abc")';
            $score -= 15;
        }

        // Repeated characters
        if ($this->hasExcessiveRepeatedCharacters($plainPassword)) {
            $warnings[] = 'Avoid excessive character repetition';
            $score -= 10;
        }

        $score = max(0, $score);

        return new PasswordValidationResult(
            isValid: empty($errors),
            errors: $errors,
            warnings: $warnings,
            strengthScore: $score,
            strength: $this->calculateStrengthFromScore($score)
        );
    }

    /**
     * Generate a secure random password
     */
    public function generateSecurePassword(
        int $length = 16,
        bool $includeUppercase = true,
        bool $includeLowercase = true,
        bool $includeNumbers = true,
        bool $includeSpecialChars = true,
        string $excludeChars = ''
    ): string {
        if ($length < self::MIN_PASSWORD_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('Password length must be at least %d', self::MIN_PASSWORD_LENGTH)
            );
        }

        if ($length > self::MAX_PASSWORD_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('Password length cannot exceed %d', self::MAX_PASSWORD_LENGTH)
            );
        }

        $charset = '';

        if ($includeUppercase) {
            $charset .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        }

        if ($includeLowercase) {
            $charset .= 'abcdefghijklmnopqrstuvwxyz';
        }

        if ($includeNumbers) {
            $charset .= '0123456789';
        }

        if ($includeSpecialChars) {
            $charset .= '!@#$%^&*()-_=+[]{}|;:,.<>?/~`';
        }

        if (empty($charset)) {
            throw new InvalidArgumentException('At least one character type must be included');
        }

        // Remove excluded characters
        if (! empty($excludeChars)) {
            $charset = str_replace(str_split($excludeChars), '', $charset);
        }

        $password = '';
        $charsetLength = strlen($charset);

        for ($i = 0; $i < $length; $i++) {
            $randomIndex = random_int(0, $charsetLength - 1);
            $password .= $charset[$randomIndex];
        }

        return $password;
    }

    /**
     * Get parameters for security level
     * @return array<string, mixed>
     */
    private function getParametersForLevel(string $algorithm, string $level): array
    {
        try {
            $params = $this->kdf->getRecommendedParameters($algorithm, $level);

            return match ($algorithm) {
                'argon2id' => [
                    'memory_cost' => $params['memory_cost'] ?? 65536,
                    'time_cost' => $params['time_cost'] ?? 4,
                    'threads' => $params['threads'] ?? 3,
                    'key_length' => $params['key_length'] ?? 32,
                ],
                'pbkdf2-sha256', 'pbkdf2-sha512' => [
                    'iterations' => $params['iterations'] ?? 100000,
                    'key_length' => $params['key_length'] ?? 32,
                ],
                'scrypt' => [
                    'cost_parameter' => $params['cost_parameter'] ?? 16384,
                    'block_size' => $params['block_size'] ?? 8,
                    'parallelization' => $params['parallelization'] ?? 1,
                    'key_length' => $params['key_length'] ?? 32,
                ],
                default => []
            };
        } catch (\Exception) {
            // Fallback to standard parameters
            return $this->getDefaultParameters($algorithm);
        }
    }

    /**
     * Get default parameters for algorithm
     * @return array<string, mixed>
     */
    private function getDefaultParameters(string $algorithm): array
    {
        return match ($algorithm) {
            'argon2id' => [
                'memory_cost' => 65536,
                'time_cost' => 4,
                'threads' => 3,
                'key_length' => 32,
            ],
            'pbkdf2-sha256', 'pbkdf2-sha512' => [
                'iterations' => 100000,
                'key_length' => 32,
            ],
            'scrypt' => [
                'cost_parameter' => 16384,
                'block_size' => 8,
                'parallelization' => 1,
                'key_length' => 32,
            ],
            default => []
        };
    }

    /**
     * Validate password
     */
    private function validatePassword(#[SensitiveParameter] string $password): void
    {
        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }

        $length = mb_strlen($password);

        if ($length < self::MIN_PASSWORD_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('Password must be at least %d characters long', self::MIN_PASSWORD_LENGTH)
            );
        }

        if ($length > self::MAX_PASSWORD_LENGTH) {
            throw new InvalidArgumentException(
                sprintf('Password cannot exceed %d characters', self::MAX_PASSWORD_LENGTH)
            );
        }
    }

    /**
     * Validate configuration
     */
    private function validateConfiguration(): void
    {
        $supportedAlgorithms = ['argon2id', 'pbkdf2-sha256', 'pbkdf2-sha512', 'scrypt'];

        if (! in_array($this->defaultAlgorithm, $supportedAlgorithms, true)) {
            throw new InvalidArgumentException(
                sprintf('Unsupported algorithm: %s', $this->defaultAlgorithm)
            );
        }

        $supportedLevels = [self::LEVEL_LOW, self::LEVEL_STANDARD, self::LEVEL_HIGH];

        if (! in_array($this->defaultSecurityLevel, $supportedLevels, true)) {
            throw new InvalidArgumentException(
                sprintf('Unsupported security level: %s', $this->defaultSecurityLevel)
            );
        }
    }

    /**
     * Check for common patterns
     */
    private function containsCommonPatterns(#[SensitiveParameter] string $password): bool
    {
        $commonPatterns = [
            'password', '123456', 'qwerty', 'admin', 'letmein',
            'welcome', 'monkey', 'dragon', 'master', 'abc123',
        ];

        $lowerPassword = strtolower($password);

        foreach ($commonPatterns as $pattern) {
            if (str_contains($lowerPassword, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check for sequential characters
     */
    private function hasSequentialCharacters(#[SensitiveParameter] string $password): bool
    {
        $sequences = [
            '012', '123', '234', '345', '456', '567', '678', '789',
            'abc', 'bcd', 'cde', 'def', 'efg', 'fgh', 'ghi', 'hij',
            'ijk', 'jkl', 'klm', 'lmn', 'mno', 'nop', 'opq', 'pqr',
            'qrs', 'rst', 'stu', 'tuv', 'uvw', 'vwx', 'wxy', 'xyz',
        ];

        $lowerPassword = strtolower($password);

        foreach ($sequences as $sequence) {
            if (str_contains($lowerPassword, $sequence)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check for excessive repeated characters
     */
    private function hasExcessiveRepeatedCharacters(#[SensitiveParameter] string $password): bool
    {
        // Check for 3+ repeated characters
        return preg_match('/(.)\1{2,}/', $password) === 1;
    }

    /**
     * Calculate strength from score
     */
    private function calculateStrengthFromScore(int $score): PasswordStrength
    {
        return match (true) {
            $score >= 90 => PasswordStrength::VERY_STRONG,
            $score >= 70 => PasswordStrength::STRONG,
            $score >= 50 => PasswordStrength::MODERATE,
            $score >= 30 => PasswordStrength::WEAK,
            default => PasswordStrength::WEAK
        };
    }

    /**
     * Create hasher with custom configuration
     */
    public static function create(
        KeyDerivationFunction $kdf,
        string $algorithm = 'argon2id',
        string $securityLevel = self::LEVEL_STANDARD
    ): self {
        return new self($kdf, $algorithm, $securityLevel);
    }
}
