"""Mentőkódok a kétlépcsős hitelesítéshez (v34) - egyszer használható tartalék belépő kódok, ha a telefon nem érhető el.

MI EZ?
    A felhasználó a 2FA beállításakor (és bármikor később, a /mfa/mentokodok oldalon) 10 db mentőkódot kap, EGYSZER látja őket
    (kinyomtathatja / jelszókezelőbe teheti). Ha nincs nála a telefon, belépéskor a 6 jegyű kód helyett egy mentőkódot írhat be
    (/mfa/mentokod). Minden kód CSAK EGYSZER használható. Leírás Miklósnak: docs/KETLEPCSOS_HITELESITES.md, "Mentőkódok" fejezet.

FORMÁTUM
    10 karakter, 32 betűs ábécéből, amelyben nincs az összetéveszthető I, O, 0, 1 - kijelzéskor "ABCDE-FGHJK" (50 bit entrópia).
    Beíráskor a kis/nagybetű, a szóköz és a kötőjel nem számít.

TÁROLÁS (biztonság)
    Az adatbázisban SOHA nincs nyers kód: csak HMAC-SHA256 lenyomat (kulcs: SECRET_KEY, a kódhoz a felhasználó azonosítója is hozzá van kötve,
    így két felhasználó azonos kódja is más lenyomatot ad). Egy adatbázis-mentés kiszivárgása így önmagában nem ad használható kódot.
    (Ha a SECRET_KEY megváltozik, a meglévő mentőkódok érvénytelenek - ugyanaz, mint a TOTP-titkoknál; új készletet kell kérni.)
    A kód 50 bites és a próbálkozásokat ugyanaz a találgatás-védelem számolja, mint a 6 jegyű kódnál (login_vedelem), ezért a
    gyors hash elegendő (nem kell lassú jelszó-hash).

EGYSZER HASZNÁLHATÓ - ATOMIAN
    A felhasználás `UPDATE ... SET felhasznalva_at WHERE id=? AND felhasznalva_at IS NULL`: ha két kérés egyszerre ugyanazt a kódot küldi,
    csak az egyik talál (rowcount == 1), a másik elutasítva.

Egyik függvény sem commitol (a hívó commitol, a naplóbejegyzéssel egy tranzakcióban), kivéve az `ertesito_level`, ami a beállítás értesítőjével
azonos módon önállóan kezeli a saját hibáit.
"""
import hashlib
import hmac
import html
import re
import secrets
from datetime import datetime, timezone
from typing import List, Optional

from sqlalchemy import delete, func, select, update
from sqlalchemy.orm import Session

import models
from config import SECRET_KEY

DARAB = 10                                              # ennyi kód van egy készletben
HOSSZ = 10                                              # egy kód karakterszáma (kötőjel nélkül)
ABECE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"              # 32 karakter: nincs I, O, 0, 1
FIGYELMEZTETO_KUSZOB = 3                                # ennyi vagy kevesebb megmaradt kódnál az oldal/a levél új készletet javasol
_MINTA = re.compile(r"[" + ABECE + r"]{" + str(HOSSZ) + r"}")


def _most() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)      # a DB-ben tz nélküli UTC


def normalizal(kod) -> Optional[str]:
    """A beírt kód normalizált alakja (nagybetű, szóköz és kötőjel nélkül), vagy None, ha nem lehet mentőkód
    (rossz hossz / nem megengedett karakter) - ilyenkor adatbázis-lekérdezés sem kell."""
    if not isinstance(kod, str):
        return None
    tiszta = re.sub(r"[\s\-_]", "", kod).upper()
    return tiszta if _MINTA.fullmatch(tiszta) else None


def lenyomat(user_id: int, normalizalt: str) -> str:
    """A kód HMAC-SHA256 lenyomata (hex). A felhasználó azonosítója a kötés része."""
    return hmac.new(SECRET_KEY.encode("utf-8"), f"mentokod:u{user_id}:{normalizalt}".encode("utf-8"), hashlib.sha256).hexdigest()


def formaz(normalizalt: str) -> str:
    """Kijelzésre: "ABCDE-FGHJK"."""
    return normalizalt[:HOSSZ // 2] + "-" + normalizalt[HOSSZ // 2:]


def uj_keszlet(db: Session, user_id: int) -> List[str]:
    """ÚJ készletet készít (DARAB db), a felhasználó MINDEN korábbi kódját (használtat és használatlant) törli. Visszaadja a kódokat kijelzésre
    alkalmas alakban - EZ AZ EGYETLEN ALKALOM, amikor a nyers kódok léteznek. Nem commitol."""
    db.execute(delete(models.MfaMentokod).where(models.MfaMentokod.user_id == user_id))
    most = _most()
    kodok: List[str] = []
    while len(kodok) < DARAB:
        nyers = "".join(secrets.choice(ABECE) for _ in range(HOSSZ))
        if nyers in (normalizal(k) for k in kodok):
            continue                                              # (gyakorlatilag lehetetlen ütközés)
        kodok.append(formaz(nyers))
        db.add(models.MfaMentokod(user_id=user_id, kod_hash=lenyomat(user_id, nyers), letrehozva_at=most))
    return kodok


def hatralevo(db: Session, user_id: int) -> int:
    """Hány még fel nem használt mentőkódja van a felhasználónak."""
    return db.execute(
        select(func.count()).select_from(models.MfaMentokod)
        .where(models.MfaMentokod.user_id == user_id, models.MfaMentokod.felhasznalva_at.is_(None))
    ).scalar() or 0


def van_keszlet(db: Session, user_id: int) -> bool:
    """Készült-e valaha készlet (akkor is igaz, ha minden kódot felhasznált)."""
    return (db.execute(select(func.count()).select_from(models.MfaMentokod).where(models.MfaMentokod.user_id == user_id)).scalar() or 0) > 0


def felhasznalas(db: Session, user: models.User, kod) -> bool:
    """A beírt kód érvényes, még fel nem használt mentőkódja-e a felhasználónak; ha igen, ATOMIAN felhasználtnak jelöli. Nem commitol -
    a hívónak siker után azonnal commitolnia kell."""
    normalizalt = normalizal(kod)
    if normalizalt is None:
        return False
    sor_id = db.execute(
        select(models.MfaMentokod.id)
        .where(models.MfaMentokod.user_id == user.id,
               models.MfaMentokod.kod_hash == lenyomat(user.id, normalizalt),
               models.MfaMentokod.felhasznalva_at.is_(None))
    ).scalar()
    if sor_id is None:
        return False
    eredmeny = db.execute(
        update(models.MfaMentokod)
        .where(models.MfaMentokod.id == sor_id, models.MfaMentokod.felhasznalva_at.is_(None))
        .values(felhasznalva_at=_most())
        .execution_options(synchronize_session=False)
    )
    return eredmeny.rowcount == 1


def torles(db: Session, user_id: int) -> None:
    """A felhasználó összes mentőkódjának törlése (2FA-visszaállításkor / felhasználó törlésekor). Nem commitol."""
    db.execute(delete(models.MfaMentokod).where(models.MfaMentokod.user_id == user_id))


def ertesito_level(db: Session, user_id: int, client_ip: str, esemeny: str, maradt: int) -> None:
    """Biztonsági értesítő a felhasználónak: `esemeny` = "hasznalat" (mentőkóddal léptek be) vagy "generalas" (új készlet készült).
    Ha NEM ő volt, ebből észreveszi. Nem akadályozhatja a belépést/generálást: hibánál csak a konzolra ír. Kategória: "rendszer"
    (a levél-feliratkozásokkal nem kapcsolható ki). Commitol (a saját, önálló lépése)."""
    try:
        import email_sor
        import login_vedelem
        user = db.get(models.User, user_id)
        if user is None or not user.email:
            return
        e = lambda x: html.escape("" if x is None else str(x), quote=True)  # noqa: E731
        most = login_vedelem.helyi_ido_szoveg(login_vedelem._utc_most())
        if esemeny == "hasznalat":
            targy = "Mentőkóddal léptek be az EmeLOG fiókodba"
            fejlec = "Belépés mentőkóddal"
            szoveg = "Az EmeLOG fiókodba <b>mentőkóddal</b> (a hitelesítő alkalmazás kódja helyett) léptek be."
            figyelmeztetes = (f"<p>Még <b>{int(maradt)}</b> felhasználatlan mentőkódod van."
                              + (" Kevés maradt: generálj új készletet a fiókod menüjében (Mentőkódok)!" if maradt <= FIGYELMEZTETO_KUSZOB else "") + "</p>")
        else:
            targy = "Új mentőkódok készültek az EmeLOG fiókodhoz"
            fejlec = "Új mentőkódok"
            szoveg = "Az EmeLOG fiókodhoz <b>új mentőkód-készlet</b> készült; a korábbi kódok érvénytelenek."
            figyelmeztetes = ""
        torzs = (
            "<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:auto\">"
            f"<h2>{e(fejlec)}</h2>"
            f"<p>Kedves {e(user.username)}!</p>"
            f"<p>{szoveg}</p>"
            f"<p>Időpont: <b>{e(most)}</b><br>IP-cím: <b>{e(client_ip)}</b></p>"
            f"{figyelmeztetes}"
            "<p style=\"color:#b02a37\"><b>Ha nem te voltál</b>, azonnal értesítsd a rendszergazdát - valaki ismerheti a jelszavadat!</p></div>"
        )
        email_sor.sorba_allit([user.email], targy, torzs, kategoria="rendszer", prioritas=email_sor.PRIORITAS_NORMAL,
                              company_id=user.company_id, db=db)
        db.commit()
    except Exception as ex:                                       # pragma: no cover - védőháló
        db.rollback()
        print(f"⚠️ A mentőkód értesítő levelét nem sikerült sorba állítani: {type(ex).__name__}: {ex}")
