"""Bezheslova autentifikacia cez TOTP (Google Authenticator a pod.).

Domenova logika nad `db/` -- tenke socket handlery v api/__init__.py ju volaju.
Hodnoty sa overuju cez pyotp; replay sa bloku pomocou Player.totp_last_step.
"""

import secrets
import time
from collections import defaultdict

import pyotp
from sqlalchemy import select

from db import crypto
from db.db import async_session
from db.models import Player

ISSUER = "Bridžik"
TOTP_PERIOD = 30  # sekund -- default pyotp

# Lockout na neuspesne prihlasovacie pokusy, per username, len v pamati
# procesu (rovnaky pattern ako games/sessions/accounts v api/__init__.py --
# proces je jediny, ziadny Redis).
_LOGIN_ATTEMPT_LIMIT = 5
_LOGIN_ATTEMPT_WINDOW = 300  # sekund
_failed_attempts: dict[str, list[float]] = defaultdict(list)


class AuthError(Exception):
    """Chyba prihlasenia/registracie (slovenska sprava pre klienta)."""


class RegistrationIncomplete(AuthError):
    """Ucet existuje, ale registracia nebola nikdy potvrdena kodom.

    Handler v api/__init__.py na nu reaguje novym QR kodom namiesto chyby.
    """


def _is_unconfirmed(player: Player) -> bool:
    """Ucet, ktory nikdy neoveril TOTP kod (confirm_account nastavuje oboje)."""
    return player.auth_token is None and player.totp_last_step == 0


def _new_token() -> str:
    return secrets.token_urlsafe(48)


def _check_lockout(username: str) -> None:
    cutoff = time.monotonic() - _LOGIN_ATTEMPT_WINDOW
    attempts = [t for t in _failed_attempts.get(username, []) if t > cutoff]
    if attempts:
        _failed_attempts[username] = attempts
    else:
        _failed_attempts.pop(username, None)
    if len(attempts) >= _LOGIN_ATTEMPT_LIMIT:
        raise AuthError("Príliš veľa neúspešných pokusov. Skúste to znova o pár minút.")


def _register_failure(username: str) -> None:
    _failed_attempts[username].append(time.monotonic())


def _clear_failures(username: str) -> None:
    _failed_attempts.pop(username, None)


def _current_step() -> int:
    return int(time.time()) // TOTP_PERIOD


def _verify_code(player: Player, code: str) -> None:
    """Overi TOTP kod a posunie totp_last_step. Pri neuspechu vyhodi AuthError.

    Akceptuje +-1 casovy krok (tolerancia hodin) a odmietne uz pouzity krok.
    """
    totp = pyotp.TOTP(crypto.decrypt(player.totp_secret))
    current = _current_step()
    for step in (current - 1, current, current + 1):
        if step <= player.totp_last_step:
            continue
        if totp.verify(code, for_time=step * TOTP_PERIOD):
            player.totp_last_step = step
            return
    raise AuthError("Nesprávny alebo už použitý kód.")


async def register_account(username: str) -> dict:
    """Zaregistruje meno a vygeneruje TOTP secret. Vrati otpauth URI pre QR.

    Nedokoncenu registraciu (meno existuje, ale kod nebol nikdy potvrdeny)
    prepise novym secretom -- povodny QR kod tym prestane platit.
    """
    username = (username or "").strip()
    if not username:
        raise AuthError("Zadajte meno.")
    secret = pyotp.random_base32()
    async with async_session() as session:
        existing = await session.scalar(
            select(Player).where(Player.username == username)
        )
        if existing is not None and not _is_unconfirmed(existing):
            raise AuthError("Toto meno je už obsadené.")
        if existing is not None:
            existing.totp_secret = crypto.encrypt(secret)
        else:
            session.add(Player(username=username, totp_secret=crypto.encrypt(secret)))
        await session.commit()
    otpauth_uri = pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=ISSUER)
    return {"username": username, "secret": secret, "otpauth_uri": otpauth_uri}


async def confirm_account(username: str, code: str) -> dict:
    """Potvrdi registraciu prvym kodom z aplikacie a vrati session token."""
    return await _verify_and_issue_token(username, code)


async def login(username: str, code: str) -> dict:
    """Prihlasi existujuci ucet a vrati session token.

    Pre nedokoncenu registraciu vyhodi RegistrationIncomplete namiesto
    overovania kodu -- pouzivatel bez naskenovaneho QR ziadny kod nema.
    """
    return await _verify_and_issue_token(username, code, unconfirmed_ok=False)


async def _verify_and_issue_token(
    username: str, code: str, *, unconfirmed_ok: bool = True
) -> dict:
    username = (username or "").strip()
    _check_lockout(username)
    async with async_session() as session:
        player = await session.scalar(select(Player).where(Player.username == username))
        if player is None:
            _register_failure(username)
            raise AuthError("Účet neexistuje.")
        if not unconfirmed_ok and _is_unconfirmed(player):
            raise RegistrationIncomplete(
                "Registrácia nie je dokončená — naskenuj QR kód a potvrď prvým kódom."
            )
        try:
            _verify_code(player, (code or "").strip())
        except AuthError:
            _register_failure(username)
            raise
        _clear_failures(username)
        token = _new_token()
        player.auth_token = crypto.hash_token(token)
        await session.commit()
        return {"player_id": player.id, "username": player.username, "token": token}


async def player_by_token(token: str) -> dict | None:
    """Overi session token (z Socket.IO `auth`). Vrati identitu alebo None."""
    if not token:
        return None
    async with async_session() as session:
        player = await session.scalar(
            select(Player).where(Player.auth_token == crypto.hash_token(token))
        )
        if player is None:
            return None
        return {"player_id": player.id, "username": player.username}
