"""Kétlépcsős hitelesítés (2FA, TOTP) - alapfüggvények (2026-10-04, 2FA 1. fázis).

MI EZ?
    A TOTP (RFC 6238) az a 6 számjegyű kód, amit a felhasználó telefonján a hitelesítő alkalmazás
    (Google Authenticator, Microsoft Authenticator, Aegis, 2FAS ...) 30 másodpercenként újragenerál.
    A szerver és a telefon ugyanazt a TITKOT ismeri; az idő alapján mindkettő ugyanazt a kódot
    számolja ki, így a jelszó mellé egy második, "birtokolt" tényező is kell a belépéshez.

    Ez a modul a "hogyan számoljuk / ellenőrizzük / tároljuk" részt és a kötelező-e szabályt tartalmazza.
    A bejelentkezés (routers/auth.py + routers/mfa.py) és a security.get_current_user() a 2. fázistól
    használja, a QR-beállító oldal (routers/mfa.py) a 3. fázistól; a céges kapcsoló felülete (4.) és az
    elveszett eszköz helyreállítása (5.) még készül. A működés nem változik, amíg egy cégnél be nincs kapcsolva a
    kötelezés (a céges kapcsoló alapból KI van).

A TERV EGYIK FŐ ELVE
    - A céges kapcsolót (Company.ketlepcsos_hitelesites_kotelezo) KIZÁRÓLAG a rendszeradmin állíthatja.
    - Ha egy cégnél be van kapcsolva, a cég MINDEN felhasználójának kötelező a 2FA.
    - Ha ki van kapcsolva, a felhasználó korábbi beállítása megmarad az adatbázisban, de a belépéskor
      nem kérünk kódot (újrabekapcsoláskor ugyanaz a titok érvényes).
    - (2. fázis) Kötelező, ha BÁRMELYIK hozzá tartozó cégnél (saját vagy kiosztott) be van kapcsolva - lásd szukseges_e().
    - A rendszeradmin nem zárhatja ki magát: szerverről futtatható vészfeloldás az mfa_felold.py.

ADATOK (models.py, `users` tábla)
    totp_secret        a titok TITKOSÍTVA (Fernet, titkosito_utils) - nyersen sehol nem tárolódik.
                       Kötött formátum: titkosított("u<user_id>:<titok>") - az "u<id>:" előtag miatt
                       egy másik felhasználó titkosított titka nem másolható át (visszafejtéskor
                       a user_id-nek egyeznie kell).
    totp_megerositve   True: a felhasználó már egyszer sikeresen beírt egy kódot (a beállítás KÉSZ).
    totp_utolso_lepes  az utoljára elfogadott 30 mp-es időlépés sorszáma (visszajátszás elleni védelem).

BIZTONSÁGI DÖNTÉSEK
    1. Ugyanaz a kód KÉTSZER nem fogadható el: a `totp_utolso_lepes`-nél nem újabb időlépés kódját
       elutasítjuk. Egy lehallgatott/leshetett kód így 30 másodperc alatt sem használható újra.
       A rögzítés ATOMI (UPDATE ... WHERE lepes < új), mert a gunicorn 3 workerrel fut: két párhuzamos
       kérés ugyanazzal a kóddal nem mehet át mindkettő.
    2. Időtűrés: az aktuális lépés és a közvetlen szomszédai (±30 mp) elfogadottak (óraeltérés).
    3. A kódot a maga teljes hosszában, idő-állandó összehasonlítással (hmac.compare_digest) vetjük
       össze, és mindhárom lépést kiszámoljuk (nem lépünk ki az első találatnál), hogy a válaszidő
       ne árulkodjon.
    4. A titok sosem kerül naplóba, hibaüzenetbe, sablonba. A QR-kép az egyetlen hely, ahol a
       beállítás pillanatában megjelenik (3. fázis) - azt az oldalt `Cache-Control: no-store`-ral kell kiszolgálni.
    5. Ha a SECRET_KEY megváltozik (és nincs dedikált CONFIG_ENCRYPTION_KEY), a titok nem fejthető
       vissza (titok_olvasasa() -> None). Ha a beállítás korábban MEGERŐSÍTETT volt, az allapot()
       "helyreallitando"-t ad - ezt NEM szabad egyszerű jelszavas újraregisztrációval megoldani (különben
       egy kulcscsere csendben kikapcsolná a 2FA-t): rendszeradmin-reset kell (5. fázis). A visszafejtési
       hiba maga nem omlaszt: a titok_olvasasa() None-t ad. (Kivétel: ha a CONFIG_ENCRYPTION_KEY formailag
       érvénytelen, a titkosito_utils HANGOSAN, RuntimeError-ral leáll - ez szándékos, lásd ott.)
    6. A kód-próbálkozások korlátozása: routers/mfa.py - a sikertelen kód a login_vedelem számlálójába kerül (ugyanaz a tiltás).

HASZNÁLAT (a későbbi fázisokban)
    titok = uj_titok(); titok_mentese(user, titok)             # beállítás kezdete
    uri = provisioning_uri(user, titok); kep = qr_png_data_uri(uri)   # QR a képernyőre
    megerosites(db, user, kod)                                  # a felhasználó beírta az első kódot
    kod_ellenorzese_es_rogzitese(db, user, kod)                 # belépéskor
    torles(user)                                                # helyreállításkor (5. fázis)
Egyik függvény sem hív `db.commit()`-ot - a hívó végpont commitol (egy tranzakcióban az AuditLoggal).

Részletek: docs/KETLEPCSOS_HITELESITES.md
"""
import base64
import hmac
import html
import io
import re
import time
from datetime import datetime, timezone
from threading import Lock
from typing import Optional

import pyotp
import qrcode
from qrcode.constants import ERROR_CORRECT_M
from sqlalchemy import delete, or_, update
from sqlalchemy.orm import Session, object_session
from sqlalchemy.orm.attributes import flag_modified

import models
from titkosito_utils import titkosit, visszafejt

# Az alkalmazás neve, ahogy a hitelesítő alkalmazásban a fiók felett megjelenik.
KIBOCSATO = "EmeLOG"

# A kód paraméterei. Az alkalmazások (Google Authenticator stb.) az alapértelmezett 6 jegyet és
# 30 másodpercet várják - NE módosítsd, mert minden már beállított telefon használhatatlanná válna.
SZAMJEGY = 6
LEPES_MP = 30
# Hány szomszédos időlépést fogadunk el az aktuális mellett mindkét irányba (1 = ±30 mp).
IDOTURES_LEPES = 1

_KOD_MINTA = re.compile(r"[0-9]{6}")

# allapot() értékei
ALLAPOT_KI = "ki"                      # a cégnél nincs kötelezővé téve: nem kérünk kódot
ALLAPOT_BEALLITANDO = "beallitando"    # kötelező, de a felhasználónak még NINCS (megerősített) beállítása
ALLAPOT_HELYREALLITANDO = "helyreallitando"   # kötelező, a beállítás MEGERŐSÍTETT volt, de a titok nem fejthető
                                              # vissza (pl. megváltozott kulcs): csak rendszeradmin-reset után állítható újra
ALLAPOT_KESZ = "kesz"                  # kötelező, és a felhasználó beállítása kész: belépéskor kód kell


# =============================================================================================
# 1. TITOK: létrehozás, titkosított tárolás, visszaolvasás
# =============================================================================================
def uj_titok() -> str:
    """Új, véletlen TOTP-titok (32 karakteres base32 = 160 bit, kriptográfiai véletlenből)."""
    return pyotp.random_base32()


def _kotes(user_id: int, titok: str) -> str:
    return f"u{user_id}:{titok}"


def _kenyszeritett_iras(user: models.User) -> None:
    """Biztosítja, hogy a három 2FA-oszlop MINDENKÉPP kiíródik a commitkor. Miért kell? Ha a
    felhasználó-objektum "lejárt" (expired) állapotú volt (pl. egy korábbi commit után), az SQLAlchemy
    a None-ra állítást nem mindig érzékeli változásnak, és a régi `totp_utolso_lepes` az
    adatbázisban maradna - az új titok mellett megmaradó "utolsó lépés" pedig jogos kódot utasítana el.
    (Ezt a t_mfa.py M6 tesztje fedezte fel.)"""
    for oszlop in ("totp_secret", "totp_megerositve", "totp_utolso_lepes"):
        flag_modified(user, oszlop)


def titok_mentese(user: models.User, titok: str) -> None:
    """Beállítja a felhasználó (még NEM megerősített) titkát, titkosítva. Egy korábbi beállítást
    felülír, és törli a megerősítést + az utolsó lépést (új titokhoz új kezdet).

    A user-nek már mentettnek kell lennie (van `id`-ja). Nem commitol."""
    if user.id is None:
        raise ValueError("A felhasználót a titok mentése előtt el kell menteni (nincs id).")
    user.totp_secret = titkosit(_kotes(user.id, titok))
    user.totp_megerositve = False
    user.totp_utolso_lepes = None
    _kenyszeritett_iras(user)


def titok_inditasa(db: Session, user_id: int) -> Optional[str]:
    """ATOMI beállítás-indítás (külső átnézés találata, 4. fázis): új, megerősítetlen titkot ír a felhasználóhoz, de CSAK ha a
    beállítása még nincs megerősítve - egyetlen `UPDATE ... WHERE totp_megerositve nem igaz` utasításban. Így egy párhuzamos
    kérés (vagy egy elavult objektum) sosem írhat felül egy időközben megerősített beállítást. Commitol, és eldobja a
    session objektum-gyorsítótárát (a hívó frissen olvassa újra). Visszatér az új (nyers) titokkal, vagy None-nal, ha a
    beállítás már megerősített (vagy nincs ilyen felhasználó)."""
    titok = uj_titok()
    sorok = (
        db.query(models.User)
        .filter(models.User.id == user_id,
                or_(models.User.totp_megerositve.is_(False), models.User.totp_megerositve.is_(None)))
        .update({"totp_secret": titkosit(_kotes(user_id, titok)), "totp_megerositve": False, "totp_utolso_lepes": None},
                synchronize_session=False)
    )
    db.commit()
    db.expire_all()
    return titok if sorok == 1 else None


def titok_olvasasa(user: models.User) -> Optional[str]:
    """A felhasználó nyers titka, vagy None, ha nincs / nem fejthető vissza / nem az övé.

    A None NEM hiba: lásd a modul-leírás 5. pontját (megváltozott kulcs -> újra beállítandó)."""
    if not user.totp_secret or user.id is None:
        return None
    nyers = visszafejt(user.totp_secret)
    if not nyers:
        return None
    elvart_elotag = f"u{user.id}:"
    if not nyers.startswith(elvart_elotag):
        return None                    # átmásolt/átírt adat: nem fogadjuk el
    titok = nyers[len(elvart_elotag):]
    return titok or None


# =============================================================================================
# 2. QR-KÓD a beállításhoz
# =============================================================================================
def provisioning_uri(user: models.User, titok: str) -> str:
    """Az `otpauth://totp/...` cím, amit a hitelesítő alkalmazás QR-ből beolvas. A fiók neve az
    e-mail cím (ez az, amit a felhasználó az alkalmazásban látni fog)."""
    fiok_neve = user.email or user.username or f"felhasznalo-{user.id}"
    return pyotp.TOTP(titok, digits=SZAMJEGY, interval=LEPES_MP).provisioning_uri(
        name=fiok_neve, issuer_name=KIBOCSATO)


def qr_png_data_uri(uri: str) -> str:
    """A cím QR-kódja `data:image/png;base64,...` formában, közvetlenül `<img src="...">`-be
    írható (külön fájl nem készül a lemezen, és a sablonban nem kell `|safe`)."""
    qr = qrcode.QRCode(error_correction=ERROR_CORRECT_M, box_size=6, border=2)
    qr.add_data(uri)
    qr.make(fit=True)
    kep = qr.make_image(fill_color="black", back_color="white")
    puffer = io.BytesIO()
    kep.save(puffer, format="PNG")
    return "data:image/png;base64," + base64.b64encode(puffer.getvalue()).decode("ascii")


# =============================================================================================
# 3. KÓD ELLENŐRZÉSE
# =============================================================================================
def kod_tisztitas(kod) -> Optional[str]:
    """A beírt kódból eltávolítja a szóközt/kötőjelet (az alkalmazások "123 456"-ként mutatják), és
    csak pontosan 6 ASCII számjegyet enged át. Minden más -> None."""
    if not isinstance(kod, str):
        return None
    tiszta = kod.replace(" ", "").replace("-", "")
    return tiszta if _KOD_MINTA.fullmatch(tiszta) else None


def kod_ellenorzese(titok: str, kod, utolso_lepes: Optional[int] = None,
                    most: Optional[float] = None) -> Optional[int]:
    """TISZTA függvény (nem nyúl az adatbázishoz): ha a kód érvényes, visszaadja a hozzá tartozó
    időlépés sorszámát, különben None.

    - `utolso_lepes`: az utoljára elfogadott lépés; ennél nem újabb lépés kódját elutasítja
      (visszajátszás elleni védelem).
    - `most`: unix idő (tesztekhez); alapértelmezés a mostani idő."""
    tiszta = kod_tisztitas(kod)
    if tiszta is None or not titok:
        return None
    ma = time.time() if most is None else most
    aktualis = int(ma // LEPES_MP)
    totp = pyotp.TOTP(titok, digits=SZAMJEGY, interval=LEPES_MP)
    talalt = None
    for lepes in range(aktualis - IDOTURES_LEPES, aktualis + IDOTURES_LEPES + 1):
        # Mindhárom lépést kiszámoljuk és idő-állandóan hasonlítjuk (lásd a modul-leírás 3. pontja).
        if hmac.compare_digest(totp.generate_otp(lepes), tiszta):
            talalt = lepes
    if talalt is None:
        return None
    if utolso_lepes is not None and talalt <= utolso_lepes:
        return None
    return talalt


def kod_ellenorzese_es_rogzitese(db: Session, user: models.User, kod, most: Optional[float] = None) -> bool:
    """A felhasználó titka ellen ellenőrzi a kódot, és sikeres esetben ATOMIAN rögzíti az elfogadott
    lépést (így ugyanaz a kód többé nem megy át - még párhuzamos kérésnél sem).

    Nem commitol: a hívónak kell, siker után minél hamarabb (a sorzár a commitig tart)."""
    titok = titok_olvasasa(user)
    if titok is None:
        return False
    lepes = kod_ellenorzese(titok, kod, user.totp_utolso_lepes, most)
    if lepes is None:
        return False
    eredmeny = db.execute(
        update(models.User)
        .where(models.User.id == user.id,
               # A titkosított titok is egyezzen: ha közben egy másik kérés törölte/cserélte a titkot
               # (torles / titok_mentese), a régi titok kódja nem fogadható el és nem írja át az újat.
               models.User.totp_secret == user.totp_secret,
               or_(models.User.totp_utolso_lepes.is_(None), models.User.totp_utolso_lepes < lepes))
        .values(totp_utolso_lepes=lepes)
        .execution_options(synchronize_session=False)
    )
    if eredmeny.rowcount != 1:
        return False                   # közben egy másik kérés már felhasznált egy ugyanilyen/újabb kódot
    user.totp_utolso_lepes = lepes
    return True


def megerosites(db: Session, user: models.User, kod, most: Optional[float] = None) -> bool:
    """A beállítás lezárása: a felhasználó beírta az első érvényes kódot. Sikernél a beállítás
    "kész" (totp_megerositve=True). Nem commitol."""
    if not kod_ellenorzese_es_rogzitese(db, user, kod, most):
        return False
    user.totp_megerositve = True
    return True


def torles(user: models.User) -> None:
    """Törli a felhasználó 2FA-beállítását (elveszett eszköz - 5. fázis). A következő belépéskor,
    ha a cégnél kötelező, újra be kell állítania. Nem commitol."""
    user.totp_secret = None
    user.totp_megerositve = False
    user.totp_utolso_lepes = None
    _kenyszeritett_iras(user)
    # v34: a mentőkódok a (törölt) titokhoz tartoznak - nélküle értelmetlenek, sőt veszélyesek lennének (új beállítás után is élnének).
    # A felhasználó munkamenetén keresztül töröljük, így a hívó commitjával együtt történik (nem commitol).
    munkamenet = object_session(user)
    if munkamenet is not None and user.id is not None:
        munkamenet.execute(delete(models.MfaMentokod).where(models.MfaMentokod.user_id == user.id))


# =============================================================================================
# 4. ÁLLAPOT: kell-e kód, vagy be kell-e állítani
# =============================================================================================
# --- A KÖTELEZŐ-E SZABÁLY (2. fázis) ---------------------------------------------------------
# A megrendelő döntése: ha BÁRMELYIK olyan cégnél kötelezővé tették a 2FA-t, amelyhez a felhasználó
# tartozik (a saját cége VAGY a neki kiosztott, `allowed_companies` cégek valamelyike), neki is
# kötelező. Pl. egy külsős szakértő, aki 3 céghez fér hozzá: elég, ha az egyik cégnél kötelező.
# A rendszeradmin (Admin, company_id == 1) "mindenhez hozzáférése" NEM számít bele: rá csak a saját cége
# (az Alapértelmezett Cég) és az esetleg kifejezetten kiosztott cégek kapcsolója vonatkozik - különben
# bármelyik ügyfélcég kapcsolója a rendszeradmint is kényszerítené (és kizárhatná).
# Cég nélküli fiókra (company_id NULL) nem vonatkozik (a döntés szerint ez így maradhat).
#
# GYORSÍTÓTÁR: a get_current_user() MINDEN kérésnél ellenőrzi a szabályt, ezért a "mely cégeknél
# kötelező" halmazt workerenként GYORSITOTAR_MASODPERC-ig a memóriában tartjuk (a kapcsoló kikapcsolása
# vagy bekapcsolása legfeljebb ennyi idő alatt ér célba minden workeren). Nem kötelező cégeknél (a jelenlegi
# alapállapot) ez kérésenként 0 extra adatbázis-lekérdezés.
GYORSITOTAR_MASODPERC = 15
_gyorsitotar = {"ido": None, "idk": frozenset()}
_gyorsitotar_zar = Lock()


def gyorsitotar_torles() -> None:
    """Kiüríti a gyorsítótárat (tesztekhez, és a kapcsoló átállítása után a SAJÁT workerre azonnal)."""
    with _gyorsitotar_zar:
        _gyorsitotar["ido"] = None
        _gyorsitotar["idk"] = frozenset()


def kotelezo_ceg_idk(db: Session, gyorsitotar: bool = False) -> frozenset:
    """Azon cégek azonosítói, ahol a kétlépcsős hitelesítés kötelező. `gyorsitotar=True`: legfeljebb
    GYORSITOTAR_MASODPERC másodperces adat is jó (kérésenkénti ellenőrzéshez); a bejelentkezés és a
    kapcsoló-műveletek mindig friss adatot használnak."""
    most = time.monotonic()
    if gyorsitotar:
        with _gyorsitotar_zar:
            ido = _gyorsitotar["ido"]
            if ido is not None and most - ido < GYORSITOTAR_MASODPERC:
                return _gyorsitotar["idk"]
    idk = frozenset(
        r[0] for r in db.query(models.Company.id).filter(models.Company.ketlepcsos_hitelesites_kotelezo.is_(True)).all()
    )
    with _gyorsitotar_zar:
        _gyorsitotar["ido"] = most
        _gyorsitotar["idk"] = idk
    return idk


def szukseges_e(db: Session, user: models.User, gyorsitotar: bool = False) -> bool:
    """Kötelező-e ennek a felhasználónak a kétlépcsős hitelesítés (lásd fent: bármelyik hozzá tartozó cég)."""
    if user.company_id is None:
        return False                                   # cég nélküli fiók: megrendelői döntés szerint nem kötelezett
    kotelezo = kotelezo_ceg_idk(db, gyorsitotar)
    if not kotelezo:
        return False                                   # alapállapot: sehol nem kötelező -> nincs több lekérdezés
    if user.company_id in kotelezo:
        return True
    return any(c.id in kotelezo for c in user.allowed_companies)


def ceg_kotelezo_e(db: Session, user: models.User) -> bool:
    """Az 1. fázis neve - ugyanaz, mint a szukseges_e() (friss adattal)."""
    return szukseges_e(db, user)


def beallitva_e(user: models.User) -> bool:
    """A felhasználó beállítása KÉSZ és használható (megerősített, és a titok visszafejthető)."""
    return bool(user.totp_megerositve) and titok_olvasasa(user) is not None


def beallitas_van(user: models.User) -> bool:
    """Van-e a felhasználónak BÁRMILYEN 2FA-beállítása (megerősített, vagy megkezdett, meg nem erősített) - a titok értékét nem adja ki."""
    return bool(user.totp_secret or user.totp_megerositve)


def beallitasi_allapot(user: models.User) -> str:
    """A felhasználó SAJÁT beállítási állapota a céges kapcsolótól FÜGGETLENÜL: "kesz" (megerősített és olvasható) /
    "helyreallitando" (volt megerősített beállítás, de a titok nem olvasható, pl. megváltozott SECRET_KEY: csak a szerverről
    állítható vissza) / "nincs". A rendszeradmin felület (4. fázis) használja."""
    if beallitva_e(user):
        return "kesz"
    if user.totp_megerositve and user.totp_secret:
        return "helyreallitando"
    return "nincs"


def allapot(db: Session, user: models.User) -> str:
    """A felhasználó 2FA-állapota a cége szabálya szerint:
    ALLAPOT_KI / ALLAPOT_BEALLITANDO / ALLAPOT_KESZ / ALLAPOT_HELYREALLITANDO.

    MEGJEGYZÉS (a 2. fázis tervezéséhez): a `company_id` nélküli felhasználóra "ki" az eredmény - ilyen
    fiók a rendszerben nem várt; a bejelentkezésnek (2. fázis) ezt külön kell kezelnie, ha számít."""
    if not szukseges_e(db, user):
        return ALLAPOT_KI
    if beallitva_e(user):
        return ALLAPOT_KESZ
    if user.totp_megerositve and user.totp_secret:
        return ALLAPOT_HELYREALLITANDO     # volt megerősített beállítás, de nem olvasható: nem lehet csak jelszóval újra
    return ALLAPOT_BEALLITANDO


# =============================================================================================
# 5. KEZELŐ MŰVELETEK (a rendszeradmin felület - 4. fázis - és a vészfeloldó szkript használja)
# =============================================================================================
def _utc_most() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)      # a DB-ben tz nélküli UTC


def ceg_kapcsolo_beallitasa(db: Session, ceg: models.Company, ertek: bool) -> bool:
    """A céges "kétlépcsős hitelesítés kötelező" kapcsoló átállítása. Igaz, ha ténylegesen változott.
    Rögzíti a változás idejét, és üríti a (saját worker) gyorsítótárát. Nem commitol.

    FIGYELEM: ez NEM ellenőriz jogosultságot és NEM védi a rendszeradmint a kizárástól - a hívó köteles megtenni
    (a rendszeradmin felület, routers/rendszeradmin_mfa.py, a kizaras_vedelem()-mel meg is teszi: bekapcsolni csak akkor
    lehet, ha minden érintett rendszeradmin beállítása kész). A vészfeloldó szkript (mfa_felold.py) csak KIKAPCSOLNI használja."""
    if bool(ceg.ketlepcsos_hitelesites_kotelezo) == bool(ertek):
        return False
    ceg.ketlepcsos_hitelesites_kotelezo = bool(ertek)
    ceg.mfa_politika_valtozott_at = _utc_most()
    gyorsitotar_torles()
    return True


# --- A 4. fázis (rendszeradmin felület) segédei: kizárás elleni védelem, érintettek ---------------------------------
def rendszeradminok(db: Session) -> list:
    """Az AKTÍV rendszeradminisztrátorok (Admin szerepkör, company_id == 1 - ugyanaz, mint security.is_rendszeradminisztrator);
    az aktiválásra váró (még nem aktivált) fiók nem számít: ő be sem tud lépni, az aktiválás után pedig belépéskor beállítja a 2FA-t."""
    lista = (
        db.query(models.User)
        .filter(models.User.role == "Admin", models.User.company_id == 1, models.User.aktiv.is_(True))
        .order_by(models.User.id)
        .all()
    )
    return [u for u in lista if u.aktivalva is not False]       # az aktiválásra váró fiók még nem tud belépni: nem lehet kizárva


def erintett_felhasznalok(db: Session, ceg: models.Company) -> list:
    """Azok az AKTÍV felhasználók, akikre a cég kapcsolója vonatkozik (vagy vonatkozna): a cég saját felhasználói ÉS azok, akiknek
    ez a cég kiosztott cég (`allowed_companies`) - ugyanaz a szabály, mint a szukseges_e()-ben. Egy felhasználó egyszer szerepel."""
    egyedi = {}
    for u in db.query(models.User).filter(models.User.company_id == ceg.id, models.User.aktiv.is_(True)).all():
        egyedi[u.id] = u
    for u in ceg.allowed_users:
        if u.aktiv and u.company_id is not None:       # cég nélküli fiókra a szabály nem vonatkozik (l. szukseges_e)
            egyedi.setdefault(u.id, u)
    return sorted(egyedi.values(), key=lambda u: u.id)


def kizaras_vedelem(db: Session, ceg: models.Company) -> list:
    """KIZÁRÁS ELLENI VÉDELEM (megrendelői döntés: "a szuperadmin NEM zárhatja ki magát"): a cég kapcsolójának BEKAPCSOLÁSA előtt
    azon aktív rendszeradminok listája, akikre a kapcsoló vonatkozna, DE a 2FA-beállításuk még nem KÉSZ (nincs megerősítve, vagy a
    titok nem olvasható). Üres lista = bekapcsolható. Nem üres = TILOS bekapcsolni, amíg mindegyikük be nem állította (a rendszeradmin
    a saját beállítását a rendszeradmin felületen előre elvégezheti, akkor is, ha még nem kötelező).

    Kik az érintett rendszeradminok? Az 1-es cég kapcsolójánál mindegyik (a saját cégük); más cégnél csak azok, akiknek az a cég
    kiosztott cégük (`allowed_companies`). A többi rendszeradminra a kapcsoló nem hat, őket nem kell ellenőrizni.

    Miért kell ez, ha a 3. fázisban a kötelezett amúgy is beállíthatja magát belépéskor? Mert a beállítás belépéskor a jelszóra
    épül, és egy hibás kimenetel (pl. nem működő hitelesítő alkalmazás, rossz telefonidő) mellett a rendszeradmin már nem tud
    belépni, hogy a hibát kijavítsa - a védelem ezért a kapcsoló átbillentése ELŐTT megköveteli a bizonyítottan működő beállítást."""
    veszelyeztetettek = []
    for u in rendszeradminok(db):
        erintett = (ceg.id == u.company_id) or any(c.id == ceg.id for c in u.allowed_companies)
        if erintett and not beallitva_e(u):
            veszelyeztetettek.append(u)
    return veszelyeztetettek


# --- Az 5. fázis: elveszett eszköz - a rendszergazdai jelszó-visszaállítás a 2FA-t is törli ----------------------------
def visszaallitas_jelszocserevel(db: Session, cel: models.User, vegrehajto: models.User) -> bool:
    """ELVESZETT MÁSODIK FAKTOR (megrendelői döntés: "kötelező jelszócsere, majd új QR"): amikor egy rendszergazda új jelszót állít egy
    felhasználónak (routers/users.py, /admin-jelszo-csere), a felhasználó 2FA-beállítása is törlődik - EGY lépésben, egy commitban a
    jelszóval. A felhasználó a következő belépéskor az új jelszóval, majd az új QR-beállítással lép be (ha a cégénél kötelező a 2FA).
    Így a lopott jelszóval nem lehet "sima" 2FA-törléssel bejutni, és a nem olvasható titok (`helyreallitando`) is megoldódik.

    KIVÉTELEK (külső átnézés után; mindkettő azért, hogy egy ELLOPOTT admin-munkamenet ne tudja a 2FA-t kikapcsolni: új jelszó + törölt beállítás
    = tartós átvétel, és a 2FA addig pont ezt akadályozta):
      * a SAJÁT beállítását a végrehajtó ezzel nem törölheti;
      * egy RENDSZERADMIN beállítása sem törölhető így (senki által): az ő elveszett eszközét a szerverről lehet visszaállítani (mfa_felold.py,
        SSH-hozzáféréssel) - ez a "szuperadmin nem zárható ki" elv másik oldala is: egy átvett munkamenet nem tudja egy másik rendszeradmin
        második faktorát lekapcsolni. A jelszócsere ilyenkor megtörténik, de a 2FA marad (a belépéshez a telefon továbbra is kell).

    Nem commitol (a hívó a jelszóval együtt commitol). Igaz, ha törölt valamit (volt beállítás, vagy megkezdett beállítás); utána a hívó
    küldje el az értesítést (ertesito_visszaallitasrol)."""
    if cel.id == vegrehajto.id:
        return False
    if cel.role == "Admin" and cel.company_id == 1:        # rendszeradmin (security.is_rendszeradminisztrator; körkörös import miatt itt inline)
        return False
    if not (cel.totp_secret or cel.totp_megerositve):
        return False
    volt = "megerősített beállítás" if cel.totp_megerositve else "megkezdett (meg nem erősített) beállítás"
    torles(cel)
    db.add(models.AuditLog(
        company_id=cel.company_id,
        user_id=vegrehajto.id,
        field_name="Kétlépcsős hitelesítés",
        old_value=f"{volt} ({cel.email})",
        new_value="törölve jelszócserével (a felhasználónak új QR-beállítás kell)"
    ))
    return True


def ertesito_visszaallitasrol(db: Session, cel_id: int, vegrehajto_id: int, jelszo_is: bool = True, mfa_is: bool = True) -> None:
    """Biztonsági értesítők a 2FA-visszaállításról (külső átnézés után két címzett, mert a felhasználó címét egy rosszindulatú admin előbb
    átírhatta volna a sajátjára):
      1. a felhasználónak: a jelszavát és a 2FA-ját rendszergazda visszaállította (ha NEM kérte, ebből észreveszi);
      2. MÁSOLAT a többi aktív rendszeradminnak (a végrehajtót és az érintettet kivéve): ki kinek állította vissza - így egy átvett
         admin-munkamenet nem marad észrevétlen.
    Nem akadályozhat semmit: hibánál csak a konzolra ír. Kategória: "rendszer" (nem kapcsolható ki).

    v26 (rendszeradmin felhasználókezelés): `jelszo_is` / `mfa_is` jelzi, MIT változtatott a végrehajtó (alapértelmezés: mindkettőt, mint a
    /admin-jelszo-csere esetében) - a levél szövege ehhez igazodik (csak jelszó / csak 2FA-törlés / mindkettő)."""
    try:
        import email_sor                                # késői import: a mfa_utils-t sok modul tölti be, ne húzzon be levelezést
        import login_vedelem
        cel = db.get(models.User, cel_id)
        vegrehajto = db.get(models.User, vegrehajto_id)
        if cel is None or not cel.email:
            return
        vegrehajto_nev = vegrehajto.username if vegrehajto else "?"
        e = lambda x: html.escape("" if x is None else str(x), quote=True)  # noqa: E731
        most = login_vedelem.helyi_ido_szoveg(_utc_most())
        if jelszo_is and mfa_is:
            fejlec, masolat_tortent = "Kétlépcsős hitelesítés visszaállítva", "új jelszót állított, és törölte a kétlépcsős hitelesítését ennek a fióknak"
            fejlec_cel, torzs_tortent = "Jelszavad és kétlépcsős hitelesítésed visszaállítva", "megváltoztatta az EmeLOG fiókod jelszavát, és törölte a kétlépcsős hitelesítésed beállítását."
            torzs_teendo = "Következő belépéskor az új jelszavaddal lépj be, és (ha a cégednél kötelező) állítsd be újra a hitelesítő alkalmazásodat."
            targy_cel = "Jelszavad és kétlépcsős hitelesítésed visszaállítva az EmeLOG fiókodon"
        elif jelszo_is:
            fejlec, masolat_tortent = "Jelszócsere rendszeradmin által", "új jelszót állított ennek a fióknak"
            fejlec_cel, torzs_tortent = "Jelszavad megváltozott", "megváltoztatta az EmeLOG fiókod jelszavát."
            torzs_teendo = "Következő belépéskor az új jelszavaddal lépj be."
            targy_cel = "Jelszavad megváltozott az EmeLOG fiókodon"
        else:
            fejlec, masolat_tortent = "Kétlépcsős hitelesítés törölve", "törölte a kétlépcsős hitelesítését ennek a fióknak"
            fejlec_cel, torzs_tortent = "Kétlépcsős hitelesítésed visszaállítva", "törölte az EmeLOG fiókod kétlépcsős hitelesítésének beállítását."
            torzs_teendo = "Következő belépéskor (ha a cégednél kötelező) állítsd be újra a hitelesítő alkalmazásodat."
            targy_cel = "Kétlépcsős hitelesítésed visszaállítva az EmeLOG fiókodon"
        masolatot_kap = [u.email for u in rendszeradminok(db) if u.email and u.id not in (cel.id, vegrehajto_id)]
        if masolatot_kap:
            masolat = (
                "<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:auto\">"
                f"<h2>{e(fejlec)} (másolat a rendszeradminoknak)</h2>"
                f"<p><b>{e(vegrehajto_nev)}</b> {e(masolat_tortent)}: <b>{e(cel.username)}</b> ({e(cel.email)}).</p>"
                f"<p>Időpont: <b>{e(most)}</b></p>"
                "<p style=\"color:#b02a37\">Ha ez nem várt művelet, a végrehajtó fiókját átvehették - ellenőrizd!</p></div>"
            )
            email_sor.sorba_allit(masolatot_kap, fejlec + " az EmeLOG-ban (rendszeradmin másolat)", masolat,
                                  kategoria="rendszer", prioritas=email_sor.PRIORITAS_NORMAL, company_id=cel.company_id, db=db)
        torzs = (
            "<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:auto\">"
            f"<h2>{e(fejlec_cel)}</h2>"
            f"<p>Kedves {e(cel.username)}!</p>"
            f"<p>A rendszergazda (<b>{e(vegrehajto_nev)}</b>) {e(torzs_tortent)} {e(torzs_teendo)}</p>"
            f"<p>Időpont: <b>{e(most)}</b></p>"
            "<p style=\"color:#b02a37\"><b>Ha ezt nem kérted</b>, azonnal értesítsd a rendszergazdát!</p></div>"
        )
        email_sor.sorba_allit([cel.email], targy_cel, torzs,
                              kategoria="rendszer", prioritas=email_sor.PRIORITAS_NORMAL,
                              company_id=cel.company_id, db=db)
        db.commit()
    except Exception as ex:
        db.rollback()
        print(f"⚠️ A 2FA-visszaállítás értesítő levelét nem sikerült sorba állítani: {type(ex).__name__}: {ex}")


def felhasznalo_visszaallitasa(user: models.User) -> None:
    """Elveszett második faktor (vagy vészfeloldás): törli a 2FA-beállítást ÉS érvényteleníti a felhasználó
    minden meglévő munkamenetét (jelszo_valtozott_at = most - a get_current_user() ezt veti össze a token
    kiállításával). Nem commitol."""
    torles(user)
    user.jelszo_valtozott_at = _utc_most()
