"""Хэширование паролей (argon2) и выпуск/проверка JWT (access + refresh)."""

import asyncio
import uuid
from datetime import UTC, datetime, timedelta
from typing import Any

import jwt
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerifyMismatchError

from core.config import get_settings

JWT_ALGORITHM = "HS256"

# Параметры argon2id по рекомендации OWASP (Password Storage Cheat Sheet):
# t=2, m=19 МБ, p=1. Раньше использовались дефолты argon2-cffi
# (t=3, m=64 МБ, p=4) — это был не выбор, а «что было в коробке».
#
# Замер на боевом сервере (4 ядра): 95 мс против 42 мс на одну проверку.
# Отдельно важен `parallelism`: при p=4 ОДНА проверка пароля занимала все
# четыре ядра, конкурируя с LiveKit за то же железо ровно в момент, когда
# люди массово заходят в конференцию.
#
# Существующие хэши не ломаются: параметры хранятся внутри самой строки хэша
# и читаются при verify. Старые хэши перевыпускаются постепенно — см.
# `needs_rehash` и его использование при успешном входе.
_hasher = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)


def _hash_password_sync(password: str) -> str:
    return _hasher.hash(password)


def _verify_password_sync(password: str, password_hash: str) -> bool:
    try:
        return _hasher.verify(password_hash, password)
    except VerifyMismatchError:
        return False


async def hash_password(password: str) -> str:
    """Захэшировать пароль алгоритмом argon2 для хранения в БД.

    Считается в отдельном потоке — argon2 это CPU-bound работа на десятки
    миллисекунд, и в event loop ей не место (см. `verify_password`).
    """
    return await asyncio.to_thread(_hash_password_sync, password)


async def verify_password(password: str, password_hash: str) -> bool:
    """Сверить пароль с сохранённым argon2-хэшем; пароль/хэш никогда не логируются.

    Выполняется в пуле потоков, а не в event loop. Причина — нагрузочный тест
    31.07.2026: синхронный вызов останавливал весь процесс на 95–155 мс, и при
    массовом входе (около 70 человек разом) это давало p95 логина 7.28 секунды,
    33 соединения к БД в состоянии `idle in transaction` при одном активном
    запросе и отказы на совершенно посторонних ручках — включая вход в
    конференцию, где никакого пароля не проверялось.

    Потоки здесь работают по-настоящему параллельно: argon2-cffi — это
    C-расширение, освобождающее GIL на время вычисления.
    """
    return await asyncio.to_thread(_verify_password_sync, password, password_hash)


def needs_rehash(password_hash: str) -> bool:
    """Проверить, что хэш выпущен устаревшими параметрами argon2.

    Дешёвая операция: разбор строки хэша, без вычислений. Вызывается после
    успешной проверки пароля — только тогда у нас на руках открытый пароль,
    которым можно перевыпустить хэш.

    Невалидную строку считаем требующей перевыпуска: если в базе оказался
    мусор, лучше заменить его корректным хэшем, чем падать при каждом входе.
    """
    try:
        return _hasher.check_needs_rehash(password_hash)
    except InvalidHashError:
        return True


def create_access_token(user_id: uuid.UUID, role: str) -> str:
    """Выпустить access-токен: `sub`=user_id, `role`=роль, TTL из настроек."""
    settings = get_settings()
    now = datetime.now(UTC)
    payload = {
        "sub": str(user_id),
        "role": role,
        "type": "access",
        "iat": now,
        "exp": now + timedelta(minutes=settings.access_token_ttl_minutes),
    }
    return jwt.encode(payload, settings.jwt_secret, algorithm=JWT_ALGORITHM)


def create_refresh_token(user_id: uuid.UUID) -> tuple[str, str]:
    """Выпустить refresh-токен с уникальным `jti`.

    Возвращает пару (token, jti); сохранение jti в Redis — ответственность
    вызывающего кода (`services.auth.AuthService`).
    """
    settings = get_settings()
    jti = str(uuid.uuid4())
    now = datetime.now(UTC)
    payload = {
        "sub": str(user_id),
        "jti": jti,
        "type": "refresh",
        "iat": now,
        "exp": now + timedelta(days=settings.refresh_token_ttl_days),
    }
    token = jwt.encode(payload, settings.jwt_secret, algorithm=JWT_ALGORITHM)
    return token, jti


def decode_token(token: str) -> dict[str, Any]:
    """Декодировать и верифицировать JWT (сигнатура + срок действия).

    Бросает `jwt.PyJWTError` (или подкласс) при невалидном/просроченном токене.
    """
    settings = get_settings()
    return jwt.decode(token, settings.jwt_secret, algorithms=[JWT_ALGORITHM])
