"""TOTP (RFC 6238) und Wiederherstellungscodes -- ohne externe Abhaengigkeit.

Zwei-Faktor-Authentisierung gab es im Vorgaengerprojekt nicht. Sie ist hier
optional pro Benutzer aktivierbar und kann fuer Administratoren erzwungen
werden (``SecurityConfig.require_totp_for_admins``).

Implementiert mit ``hmac``/``hashlib`` aus der Standardbibliothek -- 60 Zeilen
gegenueber einer weiteren Abhaengigkeit im Deployment.
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import secrets
import struct
import time
from urllib.parse import quote

DIGITS = 6
PERIOD = 30
#: Toleranz in Zeitschritten (+/- 1 Schritt = +/- 30 s Uhrendrift).
WINDOW = 1
SECRET_BYTES = 20
RECOVERY_CODE_COUNT = 8


def generate_secret() -> str:
    """Base32-Geheimnis, wie es Authenticator-Apps erwarten (ohne Padding)."""
    return base64.b32encode(secrets.token_bytes(SECRET_BYTES)).decode("ascii").rstrip("=")


def _decode_secret(secret: str) -> bytes:
    cleaned = (secret or "").strip().replace(" ", "").upper()
    cleaned += "=" * (-len(cleaned) % 8)
    return base64.b32decode(cleaned, casefold=True)


def code_at(secret: str, counter: int) -> str:
    digest = hmac.new(_decode_secret(secret), struct.pack(">Q", counter), hashlib.sha1).digest()
    offset = digest[-1] & 0x0F
    truncated = struct.unpack(">I", digest[offset : offset + 4])[0] & 0x7FFFFFFF
    return str(truncated % (10**DIGITS)).zfill(DIGITS)


def current_code(secret: str, *, now: float | None = None) -> str:
    return code_at(secret, int((now if now is not None else time.time()) // PERIOD))


def verify_code(secret: str, code: str, *, now: float | None = None) -> bool:
    """Prueft einen Code mit Drift-Toleranz, in konstanter Zeit pro Kandidat."""
    cleaned = "".join(ch for ch in (code or "") if ch.isdigit())
    if len(cleaned) != DIGITS:
        return False
    counter = int((now if now is not None else time.time()) // PERIOD)
    hit = False
    for step in range(-WINDOW, WINDOW + 1):
        candidate = counter + step
        if candidate < 0:  # nur bei Testzeitpunkten nahe der Epoche moeglich
            continue
        # Nicht fruehzeitig abbrechen: alle Kandidaten pruefen, damit die Laufzeit
        # nicht verraet, welcher Zeitschritt gepasst hat.
        hit |= hmac.compare_digest(code_at(secret, candidate), cleaned)
    return hit


def provisioning_uri(secret: str, *, account: str, issuer: str) -> str:
    """``otpauth://``-URI fuer den QR-Code."""
    label = quote(f"{issuer}:{account}", safe="")
    return (
        f"otpauth://totp/{label}?secret={secret}&issuer={quote(issuer, safe='')}"
        f"&algorithm=SHA1&digits={DIGITS}&period={PERIOD}"
    )


def format_secret(secret: str) -> str:
    """Gruppiert das Geheimnis in Vierergruppen -- fuer die manuelle Eingabe."""
    return " ".join(secret[i : i + 4] for i in range(0, len(secret), 4))


def generate_recovery_codes(count: int = RECOVERY_CODE_COUNT) -> list[str]:
    """Einmal-Codes fuer den Fall eines verlorenen Authenticators."""
    return [f"{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}" for _ in range(count)]
