"""Kimenő levelek sora (outbox), sebességkorlát, újrapróbálás és az ismétlődő hibák szűrése.

TÖRTÉNET / MIÉRT KÉSZÜLT (2026-10-04, e-mail rendszer B szelet)
    Eddig a rendszer a leveleket azonnal, a kérés háttérszálán küldte (email_utils.py), így:
      - ha az SMTP-szerver éppen nem működött, a levél ELVESZETT;
      - semmi nem korlátozta, hány levél megy ki óránként - egy hiba-áradat a feladó-címet
        spam-listára vihette (ezért kell a dedikált, korlátozott "webapp@" feladó);
      - ugyanaz a hiba minden nap újra és újra levelet generált.
    Ez a modul ezt három dologgal oldja meg:
      1) SOR: minden levél előbb az `email_kimeno` táblába kerül (sorba_allit), egy percenként
         futó ütemezett feladat (kiurites) küldi ki őket.
      2) SEBESSÉGKORLÁT: óránként és naponta legfeljebb a rendszeradmin által beállított számú
         levél megy ki (alapérték: 10/óra, 100/nap). A többi sorban vár, nem vész el.
      3) ISMÉTLÉS-SZŰRÉS: ugyanarról a hibáról csak az első alkalommal megy levél, és csak
         `email_ismetles_nap` (alapból 7) nap után emlékeztetünk újra, ha még fennáll.

A KÜLDÉS MENETE (kiurites, percenként)
    1. Az `utemezo_utils.vedett` biztosítja, hogy a 3 gunicorn worker közül csak EGY futtassa.
    2. Ha nincs SMTP beállítva (szimulációs mód): a várakozó leveleket a konzolra írja és
       "szimulalt" állapotba teszi (nem számít bele a korlátba).
    3. Különben kiszámolja a még felhasználható keretet: min(óra-korlát - az elmúlt órában
       elküldöttek, nap-korlát - az elmúlt 24 órában elküldöttek). Egy futás legfeljebb
       TICK_MAX levelet küld (hogy a küldés ne "csomagban" menjen).
    4. A leveleket prioritás (0 sürgős, 1 normál, 2 alacsony), majd beérkezés szerint veszi.
    5. Sikeres küldés -> "elkuldve". Hiba esetén:
         - végleges hiba (pl. a szerver a címzettet elutasította, 5xx) -> "hibas";
         - átmeneti hiba -> újrapróbálás növekvő várakozással (5, 10, 20, 40, 80 perc), a
           MAX_PROBALKOZAS-adik hiba után "hibas";
         - kapcsolati/hitelesítési hiba (az SMTP-szerver nem elérhető) -> a futás abbamarad (nem
           próbálkozunk 5 levéllel, mindegyiknél 30 mp-es időtúllépéssel).
    A "hibas" leveleket a rendszeradmin a "Kimenő levelek" oldalon egy gombbal újra sorba állíthatja.

IDŐKEZELÉS
    Az adatbázisban minden időpont UTC, időzóna-jelölés nélkül ("naiv" UTC) - a megszokott
    mintával egyezően. A helyi (budapesti) dátumot csak az ismétlés-szűrés napi bontásához
    használjuk (a hívó adja át).

MIT NE TEGYÉL?
    - Levelet ne küldj közvetlenül (email_beallitasok.kuld) - állítsd sorba (sorba_allit), hogy a
      korlát és az újrapróbálás érvényesüljön. Egyetlen kivétel a rendszeradmin teszt-levél gombja.
    - A levél törzsébe kerülő, felhasználótól származó szöveget html.escape-pel védd (l. email_utils.py).

Részletes leírás: docs/EMAIL_RENDSZER.md
"""
import hashlib
import html
import re
import smtplib
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from typing import Dict, Iterable, List, Optional, Tuple

from sqlalchemy import or_

from sqlalchemy.exc import IntegrityError

import email_beallitasok
import models
import utemezo_utils
from database import SessionLocal

# --- A rendszer_beallitasok táblában használt kulcsok (a rendszeradmin állítja) ---
KULCS_LIMIT_ORAN = "email_limit_oraban"
KULCS_LIMIT_NAPON = "email_limit_naponta"
KULCS_ISMETLES_NAP = "email_ismetles_nap"

ALAP_LIMIT_ORAN = 10
ALAP_LIMIT_NAPON = 100
ALAP_ISMETLES_NAP = 7

# Egy percenkénti futásban legfeljebb ennyi levelet küldünk (a korláton belül is, hogy ne menjen "csomagban")
TICK_MAX = 5
# Ennyi sikertelen próbálkozás után lesz a levél végleg "hibas"
MAX_PROBALKOZAS = 6
# Az n-edik sikertelen próbálkozás utáni várakozás percekben
HIBA_VARAKOZAS_PERC = [5, 10, 20, 40, 80, 160]
# Ennyi percnél tovább "kuldes_alatt" állapotban ragadt levelet (összeomlott worker) visszaállítunk
KULDES_ALATT_LEJARAT_PERC = 10
# Megőrzési idők (nap) a takarításhoz
ELKULDOTT_MEGTARTAS_NAP = 30
HIBAS_MEGTARTAS_NAP = 90
ALLAPOT_MEGTARTAS_NAP = 90

ALLAPOT_VARAKOZIK = "varakozik"
ALLAPOT_KULDES_ALATT = "kuldes_alatt"
ALLAPOT_ELKULDVE = "elkuldve"
ALLAPOT_SZIMULALT = "szimulalt"
ALLAPOT_HIBAS = "hibas"
TITKOS_TORZSU_KATEGORIAK = {"aktivalas"}   # a levél törzse titkot (aktiváló link) tartalmaz: kiküldés után töröljük
ALLAPOT_VISSZATARTVA = "visszatartva"   # v35: a címzett SZABADSÁGON van - a levél nem megy ki, a szabadság végén egy összegzőben szerepel (szabadsag_utils.py)
ALLAPOT_OSSZEGEZVE = "osszegezve"       # v35: a visszatartott levél bekerült az összegző levélbe (a sor már csak naplózás)
# v35: ezek a kategóriák a címzett szabadsága alatt IS kimennek (a fiókot / a rendszert védő biztonsági levelek, az aktiváló link). Minden más
# kategória - a riasztások, a napi riport és az ÚJ, később bevezetett kategóriák is - alapból visszatartódik.
SZABADSAG_ALATT_IS_KULDOTT_KATEGORIAK = {"rendszer", "aktivalas"}
VISSZATARTOTT_MEGTARTAS_NAP = 400       # az elárvult (pl. a szabadság közben megváltozott e-mail cím miatt összegzetlen) visszatartott sorok törlése
ALLAPOT_LEJART = "lejart"   # túl régi lett a sorban (pl. tegnapi napi riport) - már nem küldjük ki

# A napi riport levelei ennyi óra után "lejártnak" számítanak, ha még mindig a sorban vannak (pl. hosszú
# SMTP-kiesés vagy kimerült korlát után): másnap már a következő riport készül, a tegnapit nem küldjük ki.
NAPI_RIPORT_LEJARAT_ORA = 20

PRIORITAS_SURGOS = 0
PRIORITAS_NORMAL = 1
PRIORITAS_ALACSONY = 2

_EMAIL_MINTA = re.compile(r"^[^@\s<>\"'\\,;]+@[^@\s<>\"'\\,;]+\.[^@\s<>\"'\\,;]+$")


def _utc_most() -> datetime:
    """A pillanatnyi idő UTC-ben, időzóna-jelölés nélkül (így tároljuk az adatbázisban)."""
    return datetime.now(timezone.utc).replace(tzinfo=None)


# =============================================================================================
# 1. BEÁLLÍTÁSOK: sebességkorlát és ismétlési idő (a rendszeradmin állítja a felületen)
# =============================================================================================
@dataclass
class Korlatok:
    """Az érvényes korlátok. Ha a rendszeradmin még nem állított semmit, az alapértékek élnek."""
    oraban: int = ALAP_LIMIT_ORAN
    naponta: int = ALAP_LIMIT_NAPON
    ismetles_nap: int = ALAP_ISMETLES_NAP


def _int_vagy(ertek, alap: int) -> int:
    try:
        v = int(str(ertek).strip())
        return v if v >= 1 else alap
    except (TypeError, ValueError):
        return alap


def korlatok_betolt(db=None) -> Korlatok:
    """Betölti az aktuális korlátokat; SOSEM dob kivételt (hiba esetén az alapértékeket adja)."""
    sajat = db is None
    if sajat:
        db = SessionLocal()
    try:
        sorok = db.query(models.RendszerBeallitas).filter(
            models.RendszerBeallitas.kulcs.in_([KULCS_LIMIT_ORAN, KULCS_LIMIT_NAPON, KULCS_ISMETLES_NAP])
        ).all()
        e = {s.kulcs: s.ertek for s in sorok}
    except Exception as ex:
        print(f"❌ Hiba az e-mail korlátok olvasásakor (alapértékek használatban): {type(ex).__name__}: {ex}")
        e = {}
    finally:
        if sajat:
            db.close()
    return Korlatok(
        oraban=_int_vagy(e.get(KULCS_LIMIT_ORAN), ALAP_LIMIT_ORAN),
        naponta=_int_vagy(e.get(KULCS_LIMIT_NAPON), ALAP_LIMIT_NAPON),
        ismetles_nap=_int_vagy(e.get(KULCS_ISMETLES_NAP), ALAP_ISMETLES_NAP),
    )


def korlatok_ellenoriz(nyers: Dict[str, str]) -> Tuple[Dict[str, str], List[str]]:
    """Ellenőrzi az űrlap értékeit. Visszatérés: (tiszta értékek, hibák). Hiba esetén nem szabad menteni."""
    hibak: List[str] = []
    tiszta: Dict[str, str] = {}
    szabalyok = (
        ("oraban", "Az óránkénti levélkorlát", 1, 1000),
        ("naponta", "A napi levélkorlát", 1, 20000),
        ("ismetles_nap", "Az ismétlődő hibák emlékeztetési ideje (nap)", 1, 90),
    )
    for kulcs, nev, also, felso in szabalyok:
        szoveg = (nyers.get(kulcs) or "").strip()
        try:
            ertek = int(szoveg)
            if not (also <= ertek <= felso):
                raise ValueError
        except ValueError:
            hibak.append(f"{nev} {also} és {felso} közötti egész szám kell legyen.")
            ertek = {"oraban": ALAP_LIMIT_ORAN, "naponta": ALAP_LIMIT_NAPON, "ismetles_nap": ALAP_ISMETLES_NAP}[kulcs]
        tiszta[kulcs] = str(ertek)
    return tiszta, hibak


def korlatok_ment(db, tiszta: Dict[str, str], user_id: Optional[int]) -> List[str]:
    """Elmenti a korlátokat (NEM commitol - a hívó az AuditLog-bejegyzéssel együtt commitol).
    Visszatérés: a ténylegesen módosult beállítások emberi nevei."""
    cimkek = (
        (KULCS_LIMIT_ORAN, "oraban", "óránkénti levélkorlát"),
        (KULCS_LIMIT_NAPON, "naponta", "napi levélkorlát"),
        (KULCS_ISMETLES_NAP, "ismetles_nap", "emlékeztetési idő (nap)"),
    )
    valtozott: List[str] = []
    for kulcs, mezo, cimke in cimkek:
        if email_beallitasok._upsert(db, kulcs, tiszta[mezo], False, user_id):
            valtozott.append(cimke)
    return valtozott


# --- Szimultán belépés (E-LIC-01) értesítő levele: opcionális kapcsoló (v33) ----------------------------------------
# A Jogosultsági naplóba a szimultán belépés MINDIG bekerül; a rendszeradminoknak szóló levél csak akkor megy, ha ez a kapcsoló be van
# kapcsolva (alapból KI: ne legyen levélzápor). Globális beállítás: a `rendszer_beallitasok` táblában ("1" = be, bármi más / nincs = ki).
KULCS_SZIMULTAN_ERTESITES = "szimultan_belepes_ertesites"


def szimultan_ertesites_be_van(db) -> bool:
    sor = db.query(models.RendszerBeallitas).filter(models.RendszerBeallitas.kulcs == KULCS_SZIMULTAN_ERTESITES).first()
    return bool(sor is not None and (sor.ertek or "") == "1")


def szimultan_ertesites_beallitasa(db, be: bool, user_id: Optional[int]) -> bool:
    """Be/ki kapcsolja a szimultán belépés értesítő levelét. NEM commitol. Igazzal tér vissza, ha az érték tényleg változott."""
    return email_beallitasok._upsert(db, KULCS_SZIMULTAN_ERTESITES, "1" if be else "0", False, user_id)


# =============================================================================================
# 2. SORBA ÁLLÍTÁS
# =============================================================================================
def cimek_tisztitasa(cimzettek: Iterable[str]) -> List[str]:
    """(A hívók az ismétlés-szűrés állapotának rögzítése ELŐTT is ezzel szűrik a címeket, hogy érvénytelen címzett esetén ne maradjon "elküldöttnek" jelölt hiba.)
    Érvényes, egyedi (kis-nagybetű-független) címek, sorrendtartó. A CR/LF-et tartalmazó vagy
    szintaktikailag hibás címeket eldobja (fejléc-injekció ellen is)."""
    latott = set()
    vissza: List[str] = []
    for c in cimzettek or []:
        c = (c or "").strip()
        if not c or len(c) > 255 or not _EMAIL_MINTA.match(c):
            continue
        kulcs = c.lower()
        if kulcs in latott:
            continue
        latott.add(kulcs)
        vissza.append(c)
    return vissza


def sorba_allit(cimzettek: Iterable[str], targy: str, torzs_html: str, kategoria: str = "riasztas",
                prioritas: int = PRIORITAS_NORMAL, company_id: Optional[int] = None, db=None,
                dedup_kulcs: Optional[str] = None) -> int:
    """A levelet (címzettenként egy sorral) a kimenő sorba teszi. Visszatérés: a sorba állított
    levelek száma (0, ha nem volt érvényes címzett vagy hiba történt - a hiba a konzolra kerül,
    de a hívót nem állítja meg).

    db: ha megadsz egy session-t, a sorok ABBA kerülnek és NEM commitolunk (a hívó commitol, így
    az ismétlés-szűrés állapotával egy tranzakcióban mentődik). Nélküle saját session-nel azonnal
    commitolunk.

    dedup_kulcs: az ismétlés-szűrés kulcsa (l. riasztas_kulcs), ha a levél egy "hiba-jelzés". Ha a levelet
    végleg nem sikerül kiküldeni ("hibas"), a kulcshoz tartozó állapot törlődik, hogy ugyanaz a hiba
    legközelebb ismét levelet generáljon - különben egy SMTP-kiesés 7 napra elnémíthatná a riasztást."""
    cimek = cimek_tisztitasa(cimzettek)
    if not cimek:
        return 0
    sajat = db is None
    if sajat:
        db = SessionLocal()
    try:
        targy_tiszta = str(targy).replace("\r", "").replace("\n", " ")[:500]
        # v35: a szabadságon lévő címzettnek NEM küldünk (a biztonsági kategóriák kivételével): a sor "visszatartva" állapotba kerül, a törzset
        # nem őrizzük meg (az összegző csak a tárgyat, az időt és a kategóriát használja). Hiba esetén a levél a megszokott módon megy ki.
        szabadsagon = set()
        if kategoria not in SZABADSAG_ALATT_IS_KULDOTT_KATEGORIAK:
            try:
                import szabadsag_utils
                szabadsagon = szabadsag_utils.szabadsagon_levo_cimek(db, cimek)
            except Exception as e:
                print(f"⚠️ A szabadság-ellenőrzés sikertelen (a levél a megszokott módon megy): {type(e).__name__}: {e}")
        for c in cimek:
            tartva = c.lower() in szabadsagon
            db.add(models.EmailKimeno(
                cimzett=c, targy=targy_tiszta, torzs_html="" if tartva else torzs_html, kategoria=kategoria,
                prioritas=prioritas, allapot=ALLAPOT_VISSZATARTVA if tartva else ALLAPOT_VARAKOZIK, probalkozasok=0, company_id=company_id,
                dedup_kulcs=dedup_kulcs,
            ))
        if sajat:
            db.commit()
        return len(cimek)
    except Exception as e:
        if sajat:
            db.rollback()
        print(f"❌ Hiba a levél sorba állításakor: {type(e).__name__}: {e}")
        return 0
    finally:
        if sajat:
            db.close()


# =============================================================================================
# 3. ISMÉTLŐDŐ HIBÁK SZŰRÉSE
# =============================================================================================
def riasztas_kulcs(gep_id, kritikus: bool, hiba_leiras: str) -> str:
    """A gépnapló-riasztások (hibajegyek) azonosítója: ugyanaz a gép + súlyosság + hibaszöveg
    mindig ugyanazt a kulcsot adja, egy eltérő szöveg (új hiba) újat."""
    ujsor_nelkul = " ".join(str(hiba_leiras or "").split())
    hash_ = hashlib.sha1(ujsor_nelkul.encode("utf-8")).hexdigest()[:16]
    return f"riasztas:{gep_id}:{'K' if kritikus else 'F'}:{hash_}"


def kikuldheto_e_es_rogzit(db, kulcs: str, company_id: Optional[int], ismetles_nap: int, ma: date) -> bool:
    """Eldönti, hogy egy hibáról küldhetünk-e levelet, és ha igen, rögzíti a mai napot.

    Igaz: a hiba új (még nincs bejegyzés), VAGY az utolsó kiküldés óta eltelt legalább
          `ismetles_nap` nap -> küldhető, a bejegyzés frissül.
    Hamis: nemrég már jeleztük -> NEM küldünk.
    A db session-ben csak flush történik; a commitot a hívó végzi (a sorba állítással együtt)."""
    sor = db.query(models.EmailErtesitesAllapot).filter(models.EmailErtesitesAllapot.kulcs == kulcs).first()
    if sor is None:
        try:
            # SAVEPOINT: ha egy másik folyamat egyszerre ugyanezt a kulcsot szúrta be, az
            # egyediségi hiba nem dönti el a külső tranzakciót - ilyenkor "már jelezte más".
            with db.begin_nested():
                db.add(models.EmailErtesitesAllapot(kulcs=kulcs, company_id=company_id, utolso_kikuldes_nap=ma))
            return True
        except IntegrityError:
            return False
    if (ma - sor.utolso_kikuldes_nap).days >= ismetles_nap:
        sor.utolso_kikuldes_nap = ma
        return True
    return False


def riasztas_allapot_torlese(db, gep_id) -> int:
    """Egy gép riasztás-bejegyzéseinek törlése - a hibajegy lezárásakor hívjuk, hogy ha a hiba később
    újra előfordul, az ismét "új hiba" legyen (és azonnal menjen levél). NEM commitol."""
    return db.query(models.EmailErtesitesAllapot).filter(
        models.EmailErtesitesAllapot.kulcs.like(f"riasztas:{int(gep_id)}:%")
    ).delete(synchronize_session=False)


def riport_tetelek_szurese(db, company_id: int, tetelek: List[Tuple[str, object]], ismetles_nap: int,
                           ma: date) -> Tuple[List[object], int]:
    """A napi riport tételeinek szűrése egy cégre.

    tetelek: (stabil_kulcs, tartalom) párok - a kulcs a hibát azonosítja (l. email_utils.py), a
    tartalom tetszőleges (a hívó dönti el, pl. a kész html és a csoport); a függvény csak visszaadja.
    Visszatérés: (a mostani levélbe kerülő tartalmak, az elnyomott = korábban már jelzett tételek száma).

    Mellékhatás (commit nélkül): az új/emlékeztetett tételek bejegyződnek, és az a bejegyzés,
    amelyhez ma már nincs tétel (a hiba megoldódott), törlődik - így ha később visszatér, újra "új".
    """
    elonyom = f"riport:{company_id}:"
    meglevo = {
        s.kulcs: s for s in db.query(models.EmailErtesitesAllapot).filter(
            models.EmailErtesitesAllapot.kulcs.like(elonyom + "%"),
            models.EmailErtesitesAllapot.company_id == company_id,
        ).all()
    }
    kuldendo: List[object] = []
    elnyomott = 0
    maiak = set()
    for kulcs_resz, tetel_html in tetelek:
        kulcs = elonyom + kulcs_resz
        if kulcs in maiak:
            continue  # ugyanaz a hiba kétszer szerepelne - az egyediségi kulcs miatt csak egyszer kezeljük
        maiak.add(kulcs)
        sor = meglevo.get(kulcs)
        if sor is None:
            db.add(models.EmailErtesitesAllapot(
                kulcs=kulcs, company_id=company_id, utolso_kikuldes_nap=ma, utoljara_latva_nap=ma))
            kuldendo.append(tetel_html)
        else:
            sor.utoljara_latva_nap = ma
            if (ma - sor.utolso_kikuldes_nap).days >= ismetles_nap:
                sor.utolso_kikuldes_nap = ma
                kuldendo.append(tetel_html)
            else:
                elnyomott += 1
    # Megoldódott (ma már nem szereplő) hibák bejegyzéseinek törlése
    for kulcs, sor in meglevo.items():
        if kulcs not in maiak:
            db.delete(sor)
    return kuldendo, elnyomott


# =============================================================================================
# 4. KIÜRÍTÉS (percenként) - a tényleges küldés
# =============================================================================================
def _titkos_torzs_torlese(sor) -> None:
    """Az aktiváló levél törzse (a nyers token a linkben) a kiküldés után törlődik - így az adatbázis /
    mentés kiszivárgása nem ad használható aktiváló linket (a tábla 30 napig őrzi az elküldött leveleket)."""
    if sor.kategoria in TITKOS_TORZSU_KATEGORIAK:
        sor.torzs_html = "<p>(Biztonsági okból a levél tartalmát - az aktiváló linket - a kiküldés után töröltük.)</p>"


def _szoveges_valtozat(torzs_html: str) -> str:
    """Egyszerű szöveges (text/plain) változat a HTML-ből - a multipart/alternative levelek kevésbé
    "spam-gyanúsak", ha van szöveges részük is."""
    s = re.sub(r"(?is)<(script|style).*?>.*?</\1>", "", torzs_html or "")
    s = re.sub(r"(?i)<br\s*/?>|</(p|div|li|h[1-6]|tr)>", "\n", s)
    s = re.sub(r"(?s)<[^>]+>", "", s)
    s = html.unescape(s)
    s = "\n".join(sor.strip() for sor in s.splitlines())
    return re.sub(r"\n{3,}", "\n\n", s).strip()


def _uzenet_epitese(sor, beall: email_beallitasok.EmailBeallitasok) -> MIMEMultipart:
    """A sorból kész MIME-levelet épít. Date és Message-ID fejlécet is ad (hiányuk rontja a
    kézbesíthetőséget), és "automatikus levél"-ként jelöli (az automatikus válaszokat elnyomja)."""
    msg = MIMEMultipart("alternative")
    msg["Subject"] = sor.targy
    msg["From"] = beall.felado_fejlec
    msg["To"] = sor.cimzett.replace("\r", "").replace("\n", "")
    msg["Date"] = formatdate(localtime=False)
    felado_cim = (beall.felado_cim or beall.felhasznalo or "").strip()
    domain = felado_cim.split("@", 1)[1] if "@" in felado_cim else None
    msg["Message-ID"] = make_msgid(domain=domain)
    msg["Auto-Submitted"] = "auto-generated"
    msg["X-Auto-Response-Suppress"] = "All"
    msg.attach(MIMEText(_szoveges_valtozat(sor.torzs_html), "plain", "utf-8"))
    msg.attach(MIMEText(sor.torzs_html, "html", "utf-8"))
    return msg


def _vegleges_hiba_e(e: Exception) -> bool:
    """Végleges hiba: újrapróbálással sem fog sikerülni (a szerver a címzettet vagy a levelet
    véglegesen elutasítja, vagy a levelet nem lehet összeállítani/elküldeni).

    FONTOS: a címzett-elutasítás (SMTPRecipientsRefused) csak akkor végleges, ha MINDEN elutasító
    kód 5xx. A 4xx (pl. greylisting, 452 "tele a postaláda", 421 túl sok kapcsolat) átmeneti - az
    újrapróbálásra áll."""
    if isinstance(e, smtplib.SMTPRecipientsRefused):
        kodok = [v[0] for v in (e.recipients or {}).values() if v]
        return bool(kodok) and all(int(k) >= 500 for k in kodok)
    if isinstance(e, smtplib.SMTPNotSupportedError):
        return True   # pl. nem ASCII címzett, de a szerver nem tud SMTPUTF8-at - újrapróbálás nem segít
    if isinstance(e, smtplib.SMTPDataError) and e.smtp_code >= 500:
        return True
    if isinstance(e, (ValueError, UnicodeError)):
        return True
    return False


def _szerver_hiba_e(e: Exception) -> bool:
    """A szerverrel/kapcsolattal van baj (nem a levéllel): időtúllépés, elutasított belépés,
    megszakadt kapcsolat... Ilyenkor a futást abbahagyjuk, mert a többi levél is elbukna."""
    return isinstance(e, OSError)  # az smtplib.SMTPException és a socket/ssl hibák is OSError-ok


def kiurites() -> Dict[str, int]:
    """A kimenő sor feldolgozása - percenként hívja az ütemező (l. main.py). Visszatérés: statisztika
    (tesztekhez és naplózáshoz). SOSEM dob kivételt."""
    stat = {"elkuldve": 0, "szimulalt": 0, "ujraprobalando": 0, "hibas": 0, "lejart": 0, "keret": 0}
    db = SessionLocal()
    try:
        most = _utc_most()
        beall = email_beallitasok.betolt(db)
        korlat = korlatok_betolt(db)

        # Ha a beállítások adatbázisból olvasása sikertelen volt, a betolt() a .env-re esik vissza - ha ott
        # nincs SMTP, a szimulációs ág a sor várakozó leveleit végleg "szimulált"-ra tenné (elvesztené).
        # Ezért átmeneti adatbázis-hibánál inkább kihagyjuk ezt a futást.
        if beall.betoltesi_hiba:
            print("❌ E-mail kiürítés kihagyva: az SMTP-beállítások nem olvashatók az adatbázisból "
                  f"({beall.betoltesi_hiba}). A levelek a sorban várnak.")
            return stat

        # Összeomlott worker miatt "kuldes_alatt" állapotban ragadt levelek visszaállítása
        lejart = most - timedelta(minutes=KULDES_ALATT_LEJARAT_PERC)
        db.query(models.EmailKimeno).filter(
            models.EmailKimeno.allapot == ALLAPOT_KULDES_ALATT,
            models.EmailKimeno.kovetkezo_proba_at < lejart,
        ).update({"allapot": ALLAPOT_VARAKOZIK, "elkuldve_at": None}, synchronize_session=False)
        db.commit()

        # Túl régi napi riportok: ne menjenek ki másnap/napokkal később (a következő riport már elkészül)
        stat["lejart"] = db.query(models.EmailKimeno).filter(
            models.EmailKimeno.allapot == ALLAPOT_VARAKOZIK,
            models.EmailKimeno.kategoria == "napi_riport",
            models.EmailKimeno.letrehozva_at < most - timedelta(hours=NAPI_RIPORT_LEJARAT_ORA),
        ).update({"allapot": ALLAPOT_LEJART,
                  "utolso_hiba": f"Lejárt: {NAPI_RIPORT_LEJARAT_ORA} óránál tovább várt a sorban, ezért nem küldtük ki"},
                 synchronize_session=False)
        db.commit()

        esedekes = (models.EmailKimeno.allapot == ALLAPOT_VARAKOZIK) & (
            models.EmailKimeno.kovetkezo_proba_at.is_(None) | (models.EmailKimeno.kovetkezo_proba_at <= most)
        )

        # --- Szimulációs mód: nincs SMTP -> konzolra írunk, nincs korlát ---
        if not beall.konfiguralt:
            for sor in db.query(models.EmailKimeno).filter(esedekes).order_by(
                    models.EmailKimeno.prioritas, models.EmailKimeno.id).limit(50).all():
                print("\n" + "=" * 50)
                print("📧 E-MAIL SZIMULÁLVA (Nincs SMTP beállítva)")
                print(f"Címzett: {sor.cimzett}")
                print(f"Tárgy: {sor.targy}")
                print("=" * 50 + "\n")
                sor.allapot = ALLAPOT_SZIMULALT
                sor.utolso_hiba = None
                _titkos_torzs_torlese(sor)
                stat["szimulalt"] += 1
            db.commit()
            return stat

        if not beall.jelszo_visszafejtheto:
            print("❌ E-mail kiürítés kihagyva: a mentett SMTP-jelszó nem fejthető vissza (megváltozott a titkosítási kulcs?). "
                  "A rendszeradmin írja be újra a jelszót az E-mail beállítások oldalon. A levelek a sorban várnak.")
            return stat

        # --- Fennmaradó keret: óra- és napi korlát, a legszigorúbb számít ---
        def _kuldott_db(mennyi: timedelta) -> int:
            return db.query(models.EmailKimeno).filter(
                models.EmailKimeno.allapot.in_([ALLAPOT_ELKULDVE, ALLAPOT_KULDES_ALATT]),
                models.EmailKimeno.elkuldve_at >= most - mennyi,
            ).count()

        kuldott_ora = _kuldott_db(timedelta(hours=1))
        kuldott_nap = _kuldott_db(timedelta(hours=24))
        keret_nap = korlat.naponta - kuldott_nap
        keret_ora = min(korlat.oraban - kuldott_ora, keret_nap)   # a normál levelekre a szigorúbb számít
        stat["keret"] = max(keret_ora, 0)

        # A SÜRGŐS (kritikus riasztás) levelekre az óránkénti korlát NEM vonatkozik, csak a napi - hogy
        # egy reggeli riport-áradat ne késleltessen egy gép-letiltásról szóló riasztást. A percenkénti
        # TICK_MAX továbbra is érvényes.
        azonositok: List[int] = []
        if keret_nap > 0:
            surgos = db.query(models.EmailKimeno).filter(
                esedekes, models.EmailKimeno.prioritas == PRIORITAS_SURGOS
            ).order_by(models.EmailKimeno.id).limit(min(keret_nap, TICK_MAX)).all()
            azonositok = [x.id for x in surgos]
        # A normál levelek: ami a percenkénti TICK_MAX-ból és az óra-keretből a sürgősek után megmaradt
        # (a most kiválasztott sürgősek is az óra-keretet fogyasztják, ezért levonjuk őket)
        normal_darab = max(min(TICK_MAX - len(azonositok), keret_ora - len(azonositok)), 0)
        if normal_darab > 0:
            tobbi = db.query(models.EmailKimeno).filter(
                esedekes, models.EmailKimeno.prioritas > PRIORITAS_SURGOS
            ).order_by(models.EmailKimeno.prioritas, models.EmailKimeno.id).limit(normal_darab).all()
            azonositok += [x.id for x in tobbi]
        if not azonositok:
            return stat

        for sor_id in azonositok:
            # Lefoglalás: csak az az egy folyamat küldi, amelyiknek az UPDATE sikerült. A lefoglalás
            # időbélyege a PILLANATNYI idő (nem a futás kezdete), hogy egy lassú küldés ne lépje túl
            # idő előtt a "beragadt" levelek visszaállítási határát.
            lefoglalas_ideje = _utc_most()
            lefoglalt = db.query(models.EmailKimeno).filter(
                models.EmailKimeno.id == sor_id, models.EmailKimeno.allapot == ALLAPOT_VARAKOZIK
            ).update({"allapot": ALLAPOT_KULDES_ALATT, "kovetkezo_proba_at": lefoglalas_ideje,
                      "elkuldve_at": lefoglalas_ideje},
                     synchronize_session=False)
            db.commit()
            if lefoglalt != 1:
                continue
            sor = db.get(models.EmailKimeno, sor_id)
            try:
                msg = _uzenet_epitese(sor, beall)
                email_beallitasok.kuld(msg, beall)
                sor.allapot = ALLAPOT_ELKULDVE
                sor.elkuldve_at = _utc_most()
                sor.kovetkezo_proba_at = None
                sor.utolso_hiba = None
                _titkos_torzs_torlese(sor)
                db.commit()
                stat["elkuldve"] += 1
            except Exception as e:
                db.rollback()
                sor = db.get(models.EmailKimeno, sor_id)
                sor.probalkozasok = (sor.probalkozasok or 0) + 1
                sor.utolso_hiba = f"{type(e).__name__}: {str(e)[:400]}"
                sor.elkuldve_at = None
                if _vegleges_hiba_e(e) or sor.probalkozasok >= MAX_PROBALKOZAS:
                    sor.allapot = ALLAPOT_HIBAS
                    sor.kovetkezo_proba_at = None
                    stat["hibas"] += 1
                    if sor.dedup_kulcs:
                        # A riasztás végleg nem ment ki: ne némítsa el az ismétlés-szűrés 7 napra -
                        # ha a hiba még fennáll, a következő jelzésnél újra levelet generál.
                        db.query(models.EmailErtesitesAllapot).filter(
                            models.EmailErtesitesAllapot.kulcs == sor.dedup_kulcs
                        ).delete(synchronize_session=False)
                else:
                    sor.allapot = ALLAPOT_VARAKOZIK
                    varakozas = HIBA_VARAKOZAS_PERC[min(sor.probalkozasok, len(HIBA_VARAKOZAS_PERC)) - 1]
                    sor.kovetkezo_proba_at = _utc_most() + timedelta(minutes=varakozas)
                    stat["ujraprobalando"] += 1
                db.commit()
                print(f"❌ Levélküldés sikertelen (#{sor_id}, {sor.probalkozasok}. próba): {sor.utolso_hiba}")
                if not _vegleges_hiba_e(e) and _szerver_hiba_e(e):
                    break  # a szerverrel van baj - a többi levél is elbukna, megvárjuk a következő futást
        return stat
    except Exception as e:
        db.rollback()
        print(f"❌ Hiba az e-mail sor feldolgozásakor: {type(e).__name__}: {e}")
        return stat
    finally:
        db.close()


# =============================================================================================
# 5. TAKARÍTÁS (naponta) és ÁTTEKINTÉS a rendszeradmin felülethez
# =============================================================================================
def takaritas() -> Dict[str, int]:
    """Régi sorok törlése: az elküldött/szimulált levelek 30 nap, a hibásak 90 nap után; a régi
    ismétlés-szűrési bejegyzések és ütemezett-futás sorok szintén. Naponta egyszer hívjuk."""
    stat = {"levelek": 0, "allapotok": 0, "futasok": 0}
    db = SessionLocal()
    try:
        most = _utc_most()
        K = models.EmailKimeno
        stat["levelek"] += db.query(K).filter(
            K.allapot.in_([ALLAPOT_ELKULDVE, ALLAPOT_SZIMULALT, ALLAPOT_LEJART]),
            K.letrehozva_at < most - timedelta(days=ELKULDOTT_MEGTARTAS_NAP)).delete(synchronize_session=False)
        stat["levelek"] += db.query(K).filter(
            K.allapot == ALLAPOT_HIBAS,
            K.letrehozva_at < most - timedelta(days=HIBAS_MEGTARTAS_NAP)).delete(synchronize_session=False)
        stat["levelek"] += db.query(K).filter(
            K.allapot.in_([ALLAPOT_OSSZEGEZVE]),
            K.letrehozva_at < most - timedelta(days=ELKULDOTT_MEGTARTAS_NAP)).delete(synchronize_session=False)
        stat["levelek"] += db.query(K).filter(
            K.allapot == ALLAPOT_VISSZATARTVA,
            K.letrehozva_at < most - timedelta(days=VISSZATARTOTT_MEGTARTAS_NAP)).delete(synchronize_session=False)
        ma = date.today()
        hatar = ma - timedelta(days=ALLAPOT_MEGTARTAS_NAP)
        A = models.EmailErtesitesAllapot
        for sor in db.query(A).all():
            utolso = sor.utoljara_latva_nap or sor.utolso_kikuldes_nap
            if utolso < hatar:
                db.delete(sor)
                stat["allapotok"] += 1
        db.commit()
    except Exception as e:
        db.rollback()
        print(f"❌ Hiba az e-mail sor takarításakor: {type(e).__name__}: {e}")
    finally:
        db.close()
    stat["futasok"] = utemezo_utils.takaritas()
    return stat


def statisztika(db) -> Dict[str, object]:
    """Az "Kimenő levelek" oldal összesítője."""
    most = _utc_most()
    K = models.EmailKimeno

    def _db(*felt):
        return db.query(K).filter(*felt).count()

    legregebbi = db.query(K).filter(K.allapot == ALLAPOT_VARAKOZIK).order_by(K.id).first()
    return {
        "varakozik": _db(K.allapot == ALLAPOT_VARAKOZIK),
        "kuldes_alatt": _db(K.allapot == ALLAPOT_KULDES_ALATT),
        "hibas": _db(K.allapot == ALLAPOT_HIBAS),
        "lejart": _db(K.allapot == ALLAPOT_LEJART),
        "visszatartva": _db(K.allapot == ALLAPOT_VISSZATARTVA),            # v35: szabadságon lévő címzettnek visszatartott levelek
        "szimulalt": _db(K.allapot == ALLAPOT_SZIMULALT),
        "elkuldve_ora": _db(K.allapot.in_([ALLAPOT_ELKULDVE, ALLAPOT_KULDES_ALATT]), K.elkuldve_at >= most - timedelta(hours=1)),
        "elkuldve_nap": _db(K.allapot.in_([ALLAPOT_ELKULDVE, ALLAPOT_KULDES_ALATT]), K.elkuldve_at >= most - timedelta(hours=24)),
        "legregebbi_varakozo": legregebbi.letrehozva_at if legregebbi else None,
    }


def legutobbi_levelek(db, limit: int = 50):
    return db.query(models.EmailKimeno).order_by(models.EmailKimeno.id.desc()).limit(limit).all()


def hibasak_ujrasorba_allitasa(db) -> int:
    """A "hibas" leveleket újra "varakozik" állapotba teszi (a rendszeradmin gombja). NEM commitol."""
    # Az aktiváló levelek kimaradnak: a linkjük a hiba óta már biztosan lecserélődött/lejárt vagy a törzsük törlődött
    # (a felhasználónak az aktiváló link ÚJRAKÜLDÉSE a kiút, nem a régi levél ismétlése).
    return db.query(models.EmailKimeno).filter(
        models.EmailKimeno.allapot == ALLAPOT_HIBAS,
        or_(models.EmailKimeno.kategoria.is_(None), models.EmailKimeno.kategoria != "aktivalas")).update(
        {"allapot": ALLAPOT_VARAKOZIK, "probalkozasok": 0, "kovetkezo_proba_at": None, "elkuldve_at": None},
        synchronize_session=False)
