<?php
namespace EasyChat\Library\Security;









final class PasswordCredentialService {
    public const MIN_LENGTH = 15;
    public const MAX_LENGTH = 128;
    private const MAX_BYTES = 1024;
    private const PBKDF2_ITERATIONS = 600000;
    private const PBKDF2_PREFIX = '$easychat$pbkdf2-sha256$';

     
    public function validate(string $password, string $username = '', string $display_name = ''): array {
        $length = $this->unicodeLength($password);
        if ($length < self::MIN_LENGTH) {
            return $this->invalid(
                'password_too_short',
                'Используйте не менее ' . self::MIN_LENGTH . ' символов. Лучше выбрать длинную парольную фразу.'
            );
        }
        if ($length > self::MAX_LENGTH || strlen($password) > self::MAX_BYTES) {
            return $this->invalid(
                'password_too_long',
                'Пароль слишком длинный. Максимум — ' . self::MAX_LENGTH . ' символов.'
            );
        }

        $normalized = $this->normalizeForComparison($password);
        if ($normalized === '' || in_array($normalized, $this->commonPasswords(), true)) {
            return $this->invalid(
                'password_too_common',
                'Этот пароль слишком распространён. Выберите уникальную парольную фразу.'
            );
        }

        foreach ([$username, $display_name, 'easychat'] as $context) {
            $context = $this->normalizeForComparison((string)$context);
            if ($context !== '' && $this->unicodeLength($context) >= 4 && hash_equals($context, $normalized)) {
                return $this->invalid(
                    'password_matches_profile',
                    'Пароль не должен совпадать с логином, именем или названием EasyChat.'
                );
            }
        }

        return ['valid' => true, 'code' => '', 'message' => ''];
    }

    public function hash(string $password): string {
        if ($this->argon2idAvailable()) {
            $hash = password_hash($password, PASSWORD_ARGON2ID, [
                'memory_cost' => 19456,
                'time_cost' => 2,
                'threads' => 1,
            ]);
            if (is_string($hash) && $hash !== '') {
                return $hash;
            }
        }

        $salt = random_bytes(32);
        $derived = hash_pbkdf2('sha256', $password, $salt, self::PBKDF2_ITERATIONS, 32, true);
        if (!is_string($derived) || strlen($derived) !== 32) {
            throw new \RuntimeException('password_hash_failed');
        }

        return self::PBKDF2_PREFIX
            . self::PBKDF2_ITERATIONS . '$'
            . $this->base64UrlEncode($salt) . '$'
            . $this->base64UrlEncode($derived);
    }

    public function verify(string $password, string $stored_hash): bool {
        $stored_hash = trim($stored_hash);
        if ($stored_hash === '') {
            return false;
        }
        if (strpos($stored_hash, self::PBKDF2_PREFIX) === 0) {
            return $this->verifyPbkdf2($password, $stored_hash);
        }

        return password_verify($password, $stored_hash);
    }

    public function needsRehash(string $stored_hash): bool {
        if (strpos($stored_hash, self::PBKDF2_PREFIX) === 0) {
            $parts = explode('$', $stored_hash);
            return (int)($parts[3] ?? 0) < self::PBKDF2_ITERATIONS;
        }
        if ($this->argon2idAvailable()) {
            return password_needs_rehash($stored_hash, PASSWORD_ARGON2ID, [
                'memory_cost' => 19456,
                'time_cost' => 2,
                'threads' => 1,
            ]);
        }
         
         
        return strpos($stored_hash, '$2') === 0;
    }

     
    public function publicPolicy(): array {
        return [
            'min_length' => self::MIN_LENGTH,
            'max_length' => self::MAX_LENGTH,
            'spaces' => true,
            'unicode' => true,
            'composition_rules' => false,
        ];
    }

    private function verifyPbkdf2(string $password, string $stored_hash): bool {
        $parts = explode('$', $stored_hash);
        if (count($parts) !== 6
            || $parts[1] !== 'easychat'
            || $parts[2] !== 'pbkdf2-sha256'
            || preg_match('/^\d{5,9}$/D', $parts[3]) !== 1
        ) {
            return false;
        }

        $iterations = (int)$parts[3];
        if ($iterations < 100000 || $iterations > 2000000) {
            return false;
        }
        $salt = $this->base64UrlDecode($parts[4]);
        $expected = $this->base64UrlDecode($parts[5]);
        if ($salt === null || strlen($salt) < 16 || $expected === null || strlen($expected) !== 32) {
            return false;
        }

        $actual = hash_pbkdf2('sha256', $password, $salt, $iterations, 32, true);
        return is_string($actual) && hash_equals($expected, $actual);
    }

    private function argon2idAvailable(): bool {
        if (!defined('PASSWORD_ARGON2ID')) {
            return false;
        }
        if (!function_exists('password_algos')) {
            return true;
        }
        return in_array('argon2id', password_algos(), true);
    }

    private function normalizeForComparison(string $value): string {
        $value = trim($value);
        $value = function_exists('mb_strtolower')
            ? mb_strtolower($value, 'UTF-8')
            : strtolower($value);
        return preg_replace('/\s+/u', ' ', $value) ?: '';
    }

    private function unicodeLength(string $value): int {
        if (function_exists('mb_strlen')) {
            return (int)mb_strlen($value, 'UTF-8');
        }
        $matched = preg_match_all('/./us', $value, $characters);
        return $matched === false ? strlen($value) : (int)$matched;
    }

     
    private function commonPasswords(): array {
        return [
            '123456789012345',
            '1234567890123456',
            'qwertyuiopasdfgh',
            'qwerty123456789',
            'passwordpassword',
            'password123456',
            'administrator',
            'administrator123',
            'easychateasychat',
            'easychat123456',
            'парольпарольпароль',
            'администратор123',
            'оператороператор',
        ];
    }

     
    private function invalid(string $code, string $message): array {
        return ['valid' => false, 'code' => $code, 'message' => $message];
    }

    private function base64UrlEncode(string $value): string {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }

    private function base64UrlDecode(string $value): ?string {
        if ($value === '' || preg_match('/^[A-Za-z0-9_-]+$/D', $value) !== 1) {
            return null;
        }
        $decoded = base64_decode(strtr($value, '-_', '+/'), true);
        return is_string($decoded) ? $decoded : null;
    }
}
