"""RENDSZERNAPLÓ (v37) - a rendszeradmin összes-naplója: minden cég minden eseménye egy helyen, cégre / felhasználóra / kategóriára / dátumra / szövegre szűrhetően.
Leírás: docs/RENDSZERNAPLO.md

MIT NÉZ?
    Az `audit_logs` tábla (models.AuditLog) minden sorát: a jogosultsági / fiók / belépési / biztonsági eseményeket ÉS a gépek adatváltozásait (gépnapló).
    A gép-eseményeknek (machine_id kitöltve) nincs saját `company_id`-jük: a céget a gépből kapjuk (COALESCE(audit.company_id, machine.company_id)).
    A "felhasználó" szűrő az esemény VÉGREHAJTÓJA (audit_logs.user_id); NULL = rendszer / ismeretlen (pl. ütemezett feladat, szerver-szkript).

KATEGÓRIÁK (a megjelenítéshez és a szűréshez; az esemény típusa (field_name) alapján, a gép-események külön "gep" kategóriába):
    belepes    - Bejelentkezés, Kijelentkezés, Egyidejű / Szimultán belépés
    biztonsag  - Kétlépcsős hitelesítés (és mentőkódok), 2FA vészfeloldás, Jelszó..., Bejelentkezési tiltás (és feloldása)
    fiok       - Új felhasználó, Felhasználó állapota / adata / törölve, Szerepkör, Modulok, Hozzáférések, Fiók aktiválva, Aktiváló link, Szabadságon vagyok
    levelezes  - E-mail beállítás / korlát, Hibás e-mailek újra sorba állítva, Levél-feliratkozás (saját és szabály)
    ceg        - Cég állapota, Bérbeadás/kihelyezés joga, Új szuperadmin, VIVÜ adatmigráció
    gep        - minden, ami géphez kötött (machine_id kitöltve): új gép, mezőmódosítás, dokumentum, üzemóra, státusz, ...
    egyeb      - minden más (új eseménytípus, amit még nem soroltunk be)
    Új eseménytípus felvételekor a megfelelő PREFIXEK listát kell bővíteni (a kategória-szűrő és a megjelenítés ugyanabból dolgozik).

SZABÁLYOK
    * CSAK OLVAS (a GET nem írhat: docs/GET_ATTEKINTES.md).
    * Rendezés: id szerint csökkenő (az id az időrendet követi, és indexelt: nagy táblán is gyors; a timestamp-re nincs index).
    * A megjelenített szövegek (régi / új érték) az eseményt kiváltó (akár támadó által beírt) szöveget tartalmazhatnak: a sablon automatikus escape-elése védi,
      a CSV-exportban a képletnek látszó cellák (=, +, -, @ kezdetűek) aposztróffal védettek.
"""
import csv
import io
from datetime import date, datetime, time, timedelta, timezone
from typing import Optional

from sqlalchemy import and_, func, not_, or_, select
from sqlalchemy.orm import joinedload

import models
from templates_env import HELYI_TZ

LAP_MERET = 50
KARTYA_SOROK = 15
EXPORT_MAX = 5000
KERESES_MAX_HOSSZ = 100
KARTYA_ERTEK_HOSSZ = 90
RENDSZERADMIN_CEG_ID = 1     # a rendszeradmin (Alapértelmezett) cég azonosítója - a security.is_rendszeradminisztrator is erre épül

KATEGORIAK = [
    ("belepes", "Belépés", "fa-right-to-bracket", "info"),
    ("biztonsag", "Biztonság (jelszó, 2FA, tiltás)", "fa-shield-halved", "danger"),
    ("fiok", "Fiók / felhasználó", "fa-user", "primary"),
    ("levelezes", "Levelezés", "fa-envelope", "warning"),
    ("ceg", "Cég / rendszer", "fa-building", "success"),
    ("gep", "Gép / dokumentum", "fa-crane", "secondary"),
    ("egyeb", "Egyéb", "fa-circle-question", "dark"),
]
KATEGORIA_KODOK = [k[0] for k in KATEGORIAK]
_KAT_NEV = {k[0]: k[1] for k in KATEGORIAK}
_KAT_SZIN = {k[0]: k[3] for k in KATEGORIAK}
_KAT_IKON = {k[0]: k[2] for k in KATEGORIAK}

# Az eseménytípus (field_name) KEZDETE alapján. A sorrend számít a python-oldali besorolásnál (az első találat nyer); az SQL-oldali szűrő ugyanezeket
# kizárólagosan használja (lásd _kategoria_feltetel), ezért a listák nem fedhetik egymást.
_PREFIXEK = {
    "biztonsag": ("Kétlépcsős", "2FA", "Jelszó", "Bejelentkezési tiltás"),
    "belepes": ("Bejelentkezés", "Kijelentkezés", "Egyidejű belépés", "Szimultán belépés"),
    "fiok": ("Új felhasználó", "Felhasználó állapota", "Felhasználó adata", "Felhasználó törölve", "Szerepkör módosítása", "Fiók aktiválva", "Aktiváló link",
             "Szabadságon vagyok", "Hozzáférés-hozzárendelések", "Modulok igazítása", "Extra cégek módosítása"),
    "levelezes": ("E-mail", "Hibás e-mail", "Levél-feliratkozás"),
    # J-20 (v77): "Cég " = létrehozva / adatai / licenc / törölve / modul-előfizetés / AI / súgó / logó / arculat ...; "Beállítás" = a Beállítások oldal írásai;
    # "Gép törölve" = a rendszeradmin végleges géptörlése (machine_id nélkül, mert a gép naplósorai is törlődtek)
    "ceg": ("Cég ", "Bérbeadás", "Új szuperadmin", "VIVÜ adatmigráció", "Beállítás", "Gép törölve"),
}
_SORREND = ("biztonsag", "belepes", "fiok", "levelezes", "ceg")


def kategoria_kod(sor) -> str:
    """Egy napló-sor (models.AuditLog) kategóriája."""
    if sor.machine_id is not None:
        return "gep"
    nev = sor.field_name or ""
    for kod in _SORREND:
        if nev.startswith(_PREFIXEK[kod]):
            return kod
    return "egyeb"


def kategoria_nev(kod: str) -> str:
    return _KAT_NEV.get(kod, "Egyéb")


def kategoria_szin(kod: str) -> str:
    return _KAT_SZIN.get(kod, "dark")


def kategoria_ikon(kod: str) -> str:
    return _KAT_IKON.get(kod, "fa-circle-question")


def _prefix_feltetel(kodok):
    A = models.AuditLog
    # substr(...) == prefix: KIS/NAGYBETŰ-ÉRZÉKENY egyezés (a LIKE az SQLite-ban az ASCII betűkre érzéketlen lenne, a python startswith viszont érzékeny: egyezniük kell)
    return or_(*[func.substr(A.field_name, 1, len(p)) == p for kod in kodok for p in _PREFIXEK[kod]])


def _kategoria_feltetel(kod: str):
    """SQL-feltétel egy kategóriára; a kategóriák kizárják egymást (a python-oldali kategoria_kod-dal egyezően)."""
    A = models.AuditLog
    if kod == "gep":
        return A.machine_id.isnot(None)
    gepnelkuli = A.machine_id.is_(None)
    if kod == "egyeb":
        return and_(gepnelkuli, or_(A.field_name.is_(None), not_(_prefix_feltetel(_SORREND))))
    # a "belepes" prefixei között a "Bejelentkezés..." a "Bejelentkezési tiltás"-t is lefedné: azt a biztonság veszi el (a sorrend itt is számít)
    if kod == "belepes":
        return and_(gepnelkuli, _prefix_feltetel(["belepes"]), not_(_prefix_feltetel(["biztonsag"])))
    return and_(gepnelkuli, _prefix_feltetel([kod]))


# =============================================================================================
# 1. SZŰRÉS
# =============================================================================================
def datum(szoveg: Optional[str]):
    """(date|None, hiba|None). Üres -> (None, None). Érvénytelen -> (None, hibaüzenet)."""
    szoveg = (szoveg or "").strip()
    if not szoveg:
        return None, None
    try:
        return date.fromisoformat(szoveg), None
    except ValueError:
        return None, f"Érvénytelen dátum: {szoveg[:20]}"


def _helyi_napkezdet_utc(nap: date) -> datetime:
    """Egy helyi (budapesti) nap 00:00-ja UTC-ben, tz nélkül (az adatbázis így tárolja az időt)."""
    return datetime.combine(nap, time.min).replace(tzinfo=HELYI_TZ).astimezone(timezone.utc).replace(tzinfo=None)


def szurt_lekerdezes(db, ceg=None, felhasznalo=None, kategoria=None, tol: Optional[date] = None, ig: Optional[date] = None, kereses: str = "", csak_rendszeradmin: bool = False):
    """A szűrt napló-lekérdezés (id szerint csökkenő).
    ceg: int = az a cég; "nincs" = cég nélküli esemény. felhasznalo: int = a végrehajtó; "nincs" = nincs végrehajtó (rendszer).
    tol / ig: helyi napok, mindkettő bezárólag. kereses: az eseménytípusban, a régi és az új értékben keres (kis/nagybetű-független).
    csak_rendszeradmin: a "rendszeradminra vonatkozó" események (a dashboard-kártya): amit rendszeradmin-fiók hajtott végre (Admin, company_id == 1; bármelyik cégnél),
    VAGY aminek a tényleges cége a rendszeradmin cég (az 1-es cég és a gépei). A naplóoldal ezt nem használja: ott minden esemény látszik."""
    A, M = models.AuditLog, models.Machine
    q = (db.query(A).outerjoin(M, M.id == A.machine_id)
         .options(joinedload(A.user), joinedload(A.ceg), joinedload(A.machine).joinedload(M.ceg)))
    ceg_oszlop = func.coalesce(A.company_id, M.company_id)
    if csak_rendszeradmin:
        ra_felhasznalok = select(models.User.id).where(models.User.role == "Admin", models.User.company_id == RENDSZERADMIN_CEG_ID)
        q = q.filter(or_(A.user_id.in_(ra_felhasznalok), ceg_oszlop == RENDSZERADMIN_CEG_ID))
    if ceg == "nincs":
        q = q.filter(ceg_oszlop.is_(None))
    elif ceg is not None:
        q = q.filter(ceg_oszlop == ceg)
    if felhasznalo == "nincs":
        q = q.filter(A.user_id.is_(None))
    elif felhasznalo is not None:
        q = q.filter(A.user_id == felhasznalo)
    if kategoria in KATEGORIA_KODOK:
        q = q.filter(_kategoria_feltetel(kategoria))
    if tol is not None:
        q = q.filter(A.timestamp >= _helyi_napkezdet_utc(tol))
    if ig is not None:
        q = q.filter(A.timestamp < _helyi_napkezdet_utc(ig + timedelta(days=1)))
    kereses = (kereses or "").strip()[:KERESES_MAX_HOSSZ]
    if kereses:
        minta = "%" + kereses.lower().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
        q = q.filter(or_(func.lower(A.field_name).like(minta, escape="\\"), func.lower(A.old_value).like(minta, escape="\\"),
                         func.lower(A.new_value).like(minta, escape="\\")))
    return q.order_by(A.id.desc())


# =============================================================================================
# 2. MEGJELENÍTÉS
# =============================================================================================
def _helyi_szoveg(utc_naiv) -> str:
    if utc_naiv is None:
        return "?"
    if utc_naiv.tzinfo is None:
        utc_naiv = utc_naiv.replace(tzinfo=timezone.utc)
    return utc_naiv.astimezone(HELYI_TZ).strftime("%Y-%m-%d %H:%M:%S")


def sor_adat(a, rovid: bool = False) -> dict:
    """Egy napló-sor megjelenítésre kész szótára. rovid: a hosszú értékeket levágja (dashboard-kártya)."""
    ceg_nev = a.ceg.nev if a.ceg is not None else (a.machine.ceg.nev if (a.machine is not None and a.machine.ceg is not None) else None)
    ceg_id = a.company_id if a.company_id is not None else (a.machine.company_id if a.machine is not None else None)
    kod = kategoria_kod(a)

    def vag(x):
        x = "" if x is None else str(x)
        return (x[:KARTYA_ERTEK_HOSSZ] + "…") if (rovid and len(x) > KARTYA_ERTEK_HOSSZ) else x

    gep = None
    if a.machine is not None:
        gep = " ".join(x for x in [a.machine.megnevezes, f"({a.machine.gyari_szam})" if a.machine.gyari_szam else None] if x) or f"#{a.machine.id}"
    elif a.machine_id is not None:
        gep = f"#{a.machine_id} (törölt gép)"
    return {
        "id": a.id, "ido": _helyi_szoveg(a.timestamp), "ceg_id": ceg_id, "ceg": ceg_nev,
        "user_id": a.user_id, "felhasznalo": a.user.username if a.user is not None else None, "email": a.user.email if a.user is not None else None,
        "kategoria": kod, "kategoria_nev": kategoria_nev(kod), "szin": kategoria_szin(kod), "ikon": kategoria_ikon(kod),
        "mezo": a.field_name or "", "regi": vag(a.old_value), "uj": vag(a.new_value), "gep": gep,
    }


def legutobbi(db, darab: int = KARTYA_SOROK, csak_rendszeradmin: bool = True) -> list:
    """A dashboard-kártya: az utolsó `darab` rendszeradminra vonatkozó esemény (v37 óta alapból SZŰRT: csak a rendszeradmin-fiókok által végrehajtott és a
    rendszeradmin céget érintő események; a teljes, minden céget mutató napló az /rendszeradmin/naplo oldalon van). Csak olvas."""
    return [sor_adat(a, rovid=True) for a in szurt_lekerdezes(db, csak_rendszeradmin=csak_rendszeradmin).limit(darab).all()]


def cegek_listaja(db) -> list:
    return [{"id": i, "nev": n} for i, n in db.query(models.Company.id, models.Company.nev).order_by(models.Company.nev).all()]


def felhasznalok_listaja(db) -> list:
    """Minden felhasználó a szűrő-listához (cég szerint csoportosítva a sablonban)."""
    sorok = (db.query(models.User.id, models.User.username, models.User.email, models.User.company_id, models.Company.nev)
             .outerjoin(models.Company, models.Company.id == models.User.company_id)
             .order_by(models.Company.nev, models.User.username).all())
    return [{"id": i, "nev": n, "email": e, "ceg_id": c, "ceg": cn or "(cég nélkül)"} for i, n, e, c, cn in sorok]


# =============================================================================================
# 3. CSV-EXPORT
# =============================================================================================
def _csv_cella(ertek) -> str:
    """CSV-injekció ellen: a képletnek látszó (=, +, -, @, tab, CR kezdetű) cella elé aposztróf kerül."""
    s = "" if ertek is None else str(ertek)
    return "'" + s if s[:1] in ("=", "+", "-", "@", "\t", "\r") else s


def csv_szoveg(sorok) -> str:
    """A napló-sorok (sor_adat szótárak) CSV-je, pontosvesszővel (magyar Excel) és UTF-8 BOM-mal."""
    ki = io.StringIO()
    iro = csv.writer(ki, delimiter=";", quoting=csv.QUOTE_MINIMAL, lineterminator="\r\n")
    iro.writerow(["Időpont", "Cég", "Felhasználó", "E-mail", "Kategória", "Esemény", "Régi érték", "Új érték", "Gép"])
    for s in sorok:
        iro.writerow([_csv_cella(x) for x in (s["ido"], s["ceg"] or "", s["felhasznalo"] or "Rendszer", s["email"] or "", s["kategoria_nev"], s["mezo"], s["regi"], s["uj"], s["gep"] or "")])
    return "﻿" + ki.getvalue()
