"""Fiók-aktiválás e-mailben küldött linkkel (2026-10-04, E szelet).

Részletes leírás Miklósnak és a jövőbeli fejlesztőknek: docs/AKTIVALAS.md

MI A LÉNYEG?
    Régen az admin az új felhasználónak KEZDŐJELSZÓT adott (és valahogy el kellett juttatnia hozzá).
    Most az admin csak a nevet és az e-mail címet adja meg. A rendszer:
      1. a fiókot "aktiválásra váró" állapotban hozza létre (`User.aktivalva = False`), olyan jelszóval,
         amit senki nem ismer (véletlen) - így a fiókba addig nem lehet belépni;
      2. aktiváló levelet küld egy egyszer használatos, 72 órás linkkel;
      3. a felhasználó a linken MAGA állítja be a jelszavát - ettől lesz a fiók aktív.
    A MEGLÉVŐ felhasználók aktiváltnak számítanak (az oszlop DEFAULT 1), nekik semmi nem változik.

BIZTONSÁGI DÖNTÉSEK
    - A token 256 bites véletlen (secrets.token_urlsafe), az `aktivalo_tokenek` táblában CSAK a SHA-256
      lenyomata van. (A kiküldött levél törzse a nyers linket tartalmazza, de azt az email_sor a
      kiküldés után törli - l. email_sor.TITKOS_TORZSU_KATEGORIAK.)
    - Egyszer használatos (ATOMI foglalás: két párhuzamos kérés közül csak az egyik nyerhet) és 72 óra
      után lejár. Új link kérésekor az előző tokenek érvényüket vesztik; az újraküldés 5 percenként
      kérhető (levélbombázás ellen).
    - A token a FELHASZNÁLÓHOZ kötött: ha egy váró fiók e-mail címét módosítják, a régi link érvényét
      veszti és az új címre új link megy (különben a régi címet birtokló személy aktiválhatná a fiókot).
    - Inaktivált fiókra / inaktív cég felhasználójára az aktiválás nem működik.
    - A link megnyitása (GET) NEM változtat semmit, csak az űrlapot mutatja - a levélszűrők / vírusirtók
      előzetes "megnyitása" így nem használja el a tokent; a jelszóbeállítás (POST) viszi végbe.
    - Érvénytelen, lejárt és már felhasznált tokenre ugyanaz a válasz (nem derül ki, melyik eset).
    - A jelszóbeállításkor a jelszóváltás időpontja is rögzül (JWT-visszavonás, mint a többi jelszócserénél).

MIT NE TEGYÉL?
    - Új felhasználót létrehozó végpontban NE `get_password_hash(jelszo)`-t használj, hanem
      `jelszo_nelkuli_hash()`-t és `meghivas()`-t (különben a fiók azonnal használható és jelszavas).
    - A nyers tokent ne naplózd és ne tárold (csak a levélbe és a linkbe kerülhet).
"""
import hashlib
import html
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional

import jelszo_hazirend
import models
import email_beallitasok
import email_sor
from security import get_password_hash

TOKEN_ERVENYESSEG_ORA = 72
UJRAKULDES_VARAKOZAS_PERC = 5    # ennyi időnek kell eltelnie két aktiváló levél között (újraküldésnél)
CEL_AKTIVALAS = "aktivalas"


def _utc_most() -> datetime:
    """Tz nélküli UTC pillanat (a DB-ben így tároljuk az időpontokat)."""
    return datetime.now(timezone.utc).replace(tzinfo=None)


def token_lenyomat(token: str) -> str:
    return hashlib.sha256((token or "").encode("utf-8")).hexdigest()


def jelszo_nelkuli_hash() -> str:
    """Egy senki által nem ismert (véletlen) jelszó bcrypt-lenyomata: az aktiválásra váró fiókba így
    semmilyen jelszóval nem lehet belépni."""
    return get_password_hash(secrets.token_urlsafe(48))


def email_ervenyes_e(email) -> bool:
    """Igaz, ha az e-mail cím olyan, amire a levélsor ki tud küldeni (ugyanaz a szabály, mint a küldésnél)."""
    return bool(email_sor.cimek_tisztitasa([email or ""]))


def aktivalva_e(user) -> bool:
    """Igaz, ha a felhasználó aktivált. NULL (régi sor) = aktivált; csak a kifejezett False vár aktiválásra."""
    return user.aktivalva is not False


# =============================================================================================
# 1. MEGHÍVÁS: token + levél
# =============================================================================================
def tokenek_torlese(db, user) -> None:
    """A felhasználó összes aktiváló tokenjének törlése (pl. inaktiváláskor). NEM commitol."""
    db.query(models.AktivaloToken).filter(models.AktivaloToken.user_id == user.id).delete(synchronize_session=False)


def _token_kiallitasa(db, user) -> str:
    """Új aktiváló token; az ehhez a felhasználóhoz tartozó korábbi (még fel nem használt) tokenek
    törlődnek. NEM commitol. Visszatérés: a NYERS token (csak a levélbe kerülhet!)."""
    A = models.AktivaloToken
    db.query(A).filter(A.user_id == user.id, A.cel == CEL_AKTIVALAS).delete(synchronize_session=False)
    token = secrets.token_urlsafe(32)
    most = _utc_most()
    db.add(A(user_id=user.id, token_hash=token_lenyomat(token), cel=CEL_AKTIVALAS,
             letrehozva_at=most, lejar_at=most + timedelta(hours=TOKEN_ERVENYESSEG_ORA)))
    return token


def _sorbeli_regi_levelek_torlese(db, cimek) -> None:
    """A kimenő sorban MÉG VÁRAKOZÓ (el nem küldött) aktiváló levelek törlése az adott címekről.
    Miért? Új link kiállításakor a régi link érvénytelen - a még el nem küldött régi levél csak a közös
    óránkénti/napi levélkorlátot fogyasztaná, és a címzettet is összezavarná (halott linkkel).
    NEM commitol."""
    tiszta = email_sor.cimek_tisztitasa([c for c in cimek if c])
    if not tiszta:
        return
    K = models.EmailKimeno
    db.query(K).filter(K.kategoria == "aktivalas", K.allapot == email_sor.ALLAPOT_VARAKOZIK,
                       K.cimzett.in_(tiszta)).delete(synchronize_session=False)


def _level_sorba(db, user, token: str, kuldo, regi_email=None) -> int:
    """Az aktiváló levél a kimenő sorba. NEM commitol. A (régi és az új) címre még váró korábbi aktiváló
    levelek törlődnek (lásd _sorbeli_regi_levelek_torlese)."""
    _sorbeli_regi_levelek_torlese(db, [user.email, regi_email])
    e = lambda x: html.escape("" if x is None else str(x), quote=True)  # noqa: E731
    beall = email_beallitasok.betolt(db)
    link = beall.alkalmazas_url_vagy_alap.rstrip("/") + "/aktivalas/" + token
    ceg = db.get(models.Company, user.company_id) if user.company_id else None
    ki = e(kuldo.username) if kuldo is not None else "a rendszergazda"
    torzs = (
        f"<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:auto\">"
        f"<h2>Üdvözlünk az EmeLOG rendszerben!</h2>"
        f"<p>Kedves {e(user.username)}!</p>"
        f"<p>{ki} fiókot hozott létre számodra"
        f"{(' a(z) <b>' + e(ceg.nev) + '</b> cégnél') if ceg is not None else ''}. "
        f"A fiók használatához állítsd be a jelszavadat az alábbi gombra kattintva:</p>"
        f"<p style=\"text-align:center;margin:28px 0\">"
        f"<a href=\"{e(link)}\" style=\"background-color:#0d6efd;color:#fff;padding:12px 24px;"
        f"text-decoration:none;border-radius:5px;font-weight:bold\">Fiók aktiválása</a></p>"
        f"<p>A link <b>{TOKEN_ERVENYESSEG_ORA} óráig</b> érvényes, és csak egyszer használható. "
        f"Ha a gomb nem működik, másold a böngésződbe ezt a címet:<br>"
        f"<span style=\"word-break:break-all\">{e(link)}</span></p>"
        f"<p style=\"color:#666;font-size:12px\">Ha nem számítottál erre a levélre, hagyd figyelmen kívül - "
        f"a fiók a link megnyitása nélkül nem használható.</p></div>"
    )
    # NORMÁL prioritás (nem sürgős): a sürgős levelek (kritikus gép-riasztás) kapják az elsőbbséget, és az
    # aktiváló levél ne tudja elhasználni a közös napi korlátot egy ismételt újraküldéssel.
    # kategoria="aktivalas": a törzs (benne a link) a kiküldés után törlődik (email_sor.TITKOS_TORZSU_KATEGORIAK).
    return email_sor.sorba_allit([user.email], "Aktiváld az EmeLOG fiókodat", torzs, kategoria="aktivalas",
                                 prioritas=email_sor.PRIORITAS_NORMAL, company_id=user.company_id, db=db)


def meghivas(db, user, kuldo) -> int:
    """Az (ÚJ, még db.add-olt) felhasználót aktiválásra váró állapotba teszi, tokent állít ki és
    kiküldi az aktiváló levelet. Hívás előtt a user.hashed_password-nek `jelszo_nelkuli_hash()`-nek
    kell lennie. NEM commitol (a hívó commitol az új felhasználóval együtt).
    Visszatérés: a sorba állított levelek száma (0, ha nincs érvényes e-mail cím)."""
    user.aktivalva = False
    db.flush()   # user.id kell a tokenhez
    token = _token_kiallitasa(db, user)
    return _level_sorba(db, user, token, kuldo)


def ujrakuldes(db, user, kuldo) -> bool:
    """Új aktiváló link küldése egy még aktiválásra váró felhasználónak (az előző link érvénytelenné
    válik). Igaz, ha ki lett küldve (sorba állítva). Hamis, ha a fiók már aktivált / inaktív, az e-mail cím
    érvénytelen (ilyenkor a régi link ÉRVÉNYBEN marad), vagy az utolsó levél óta még nem telt el
    UJRAKULDES_VARAKOZAS_PERC perc. COMMITOL."""
    if aktivalva_e(user) or user.aktiv is False or not email_ervenyes_e(user.email):
        return False
    A = models.AktivaloToken
    utolso = db.query(A).filter(A.user_id == user.id, A.cel == CEL_AKTIVALAS).order_by(A.letrehozva_at.desc()).first()
    if utolso is not None and utolso.letrehozva_at > _utc_most() - timedelta(minutes=UJRAKULDES_VARAKOZAS_PERC):
        return False
    token = _token_kiallitasa(db, user)
    _level_sorba(db, user, token, kuldo)
    db.add(models.AuditLog(
        company_id=user.company_id, user_id=kuldo.id if kuldo is not None else None,
        field_name="Aktiváló link újraküldve", old_value="-", new_value=f"{user.username} ({user.email})"))
    db.commit()
    return True


def email_valtozas_kezelese(db, user, kuldo, regi_email=None) -> None:
    """Egy váró fiók e-mail címének módosítása után hívandó: a régi címre küldött link érvényét veszti, az
    ÚJ címre új link megy. (Különben a régi - elgépelt vagy volt - címet birtokló személy aktiválhatná a
    fiókot.) Nem váró fiókra nem csinál semmit. A hívó commitol. NEM commitol."""
    if aktivalva_e(user):
        return
    tokenek_torlese(db, user)
    if user.aktiv is not False and email_ervenyes_e(user.email):
        _level_sorba(db, user, _token_kiallitasa(db, user), kuldo, regi_email=regi_email)
    else:
        _sorbeli_regi_levelek_torlese(db, [regi_email])


# =============================================================================================
# 2. A LINK MEGNYITÁSA ÉS A JELSZÓ BEÁLLÍTÁSA
# =============================================================================================
def token_ellenorzes(db, token: str):
    """A token alapján visszaadja a (token-sor, felhasználó) párt, ha a token érvényes (létezik, nem
    használták fel, nem járt le) ÉS a fiók még aktiválásra vár; különben None. Nem módosít semmit."""
    if not token or len(token) > 200:
        return None
    A = models.AktivaloToken
    sor = db.query(A).filter(A.token_hash == token_lenyomat(token), A.cel == CEL_AKTIVALAS).first()
    if sor is None or sor.felhasznalva_at is not None or sor.lejar_at <= _utc_most():
        return None
    user = db.get(models.User, sor.user_id)
    if user is None or aktivalva_e(user) or user.aktiv is False:
        return None
    ceg = db.get(models.Company, user.company_id) if user.company_id else None
    if ceg is not None and ceg.aktiv is False:   # inaktív cégnél minden szerkesztés tiltott
        return None
    return sor, user


def aktival(db, sor, user, jelszo: str) -> bool:
    """A jelszó beállítása és a fiók aktiválása a linken. A hívó előtte ellenőrzi a jelszó erősségét és a
    token érvényességét. A tokent ATOMIKUSAN foglalja le (`UPDATE ... WHERE felhasznalva_at IS NULL`):
    ha két kérés egyszerre érkezik ugyanazzal a tokennel, csak az egyik nyer (a másik hamisat kap) - az
    egyszeri felhasználhatóság így nem függ a MariaDB tranzakció-izolációjától. COMMITOL.
    Igaz, ha ez a hívás aktiválta a fiókot."""
    most = _utc_most()
    A = models.AktivaloToken
    foglalt = db.query(A).filter(A.id == sor.id, A.felhasznalva_at.is_(None)).update(
        {"felhasznalva_at": most}, synchronize_session=False)
    if foglalt != 1:
        db.rollback()
        return False
    user.hashed_password = get_password_hash(jelszo)
    user.jelszo_valtozott_at = most    # JWT-visszavonás: a korábbi (nem is létező) tokenek érvénytelenek
    user.aktivalva = True
    jelszo_hazirend.uj_jelszo_utan(db, user, None)     # v36: a jelszó lejárata az aktiválástól számít (a cég házirendje szerint)
    # a felhasználó többi tokenje is érvényét veszti
    db.query(A).filter(A.user_id == user.id, A.id != sor.id).delete(synchronize_session=False)
    db.add(models.AuditLog(
        company_id=user.company_id, user_id=user.id,
        field_name="Fiók aktiválva", old_value="Aktiválásra vár",
        new_value=f"{user.username} ({user.email}) - jelszó beállítva az aktiváló linken"))
    db.commit()
    return True


def kezi_aktivalas(db, user, rendszeradmin, jelszo: str) -> bool:
    """A rendszeradmin kézzel aktivál egy fiókot (pl. ha az SMTP nincs beállítva), és beállítja a jelszót.
    A jogosultságot (csak rendszeradmin) a HÍVÓ ellenőrzi. Igaz, ha aktivált. COMMITOL."""
    if aktivalva_e(user):
        return False
    most = _utc_most()
    user.hashed_password = get_password_hash(jelszo)
    user.jelszo_valtozott_at = most
    user.aktivalva = True
    jelszo_hazirend.uj_jelszo_utan(db, user, None)     # v36: a jelszó lejárata az aktiválástól számít (a cég házirendje szerint)
    db.query(models.AktivaloToken).filter(models.AktivaloToken.user_id == user.id).delete(synchronize_session=False)
    db.add(models.AuditLog(
        company_id=user.company_id, user_id=rendszeradmin.id,
        field_name="Fiók aktiválva kézzel", old_value="Aktiválásra vár",
        new_value=f"{user.username} ({user.email}) - aktiválta: {rendszeradmin.username}"))
    db.commit()
    return True


# =============================================================================================
# 3. TAKARÍTÁS (naponta, l. main.py)
# =============================================================================================
def takaritas() -> int:
    """A lejárt / felhasznált tokenek törlése (1 nap türelmi idővel). SOSEM dob kivételt."""
    from database import SessionLocal
    db = SessionLocal()
    try:
        hatar = _utc_most() - timedelta(days=1)
        A = models.AktivaloToken
        n = db.query(A).filter((A.lejar_at < hatar) | (A.felhasznalva_at.isnot(None) & (A.felhasznalva_at < hatar))
                               ).delete(synchronize_session=False)
        # A sikertelenül (hibásan) maradt aktiváló levelek törzsében is ott a link: a token lejárta (72 óra) után
        # már használhatatlan, de feleslegesen nem őrizzük tovább.
        K = models.EmailKimeno
        db.query(K).filter(K.kategoria.in_(list(email_sor.TITKOS_TORZSU_KATEGORIAK)),
                           K.letrehozva_at < _utc_most() - timedelta(hours=TOKEN_ERVENYESSEG_ORA + 24)
                           ).update({"torzs_html": "<p>(a levél tartalmát biztonsági okból töröltük)</p>"}, synchronize_session=False)
        db.commit()
        return n
    except Exception as e:
        db.rollback()
        print(f"❌ Hiba az aktiváló tokenek takarításakor: {type(e).__name__}: {e}")
        return 0
    finally:
        db.close()
