"""Partner-cégek kódos összekapcsolása (2026-10-06).

MI EZ?
    A bérbeadás/kihelyezés bérlő-választója korábban AZ ÖSSZES rendszerben lévő aktív céget név
    szerint felsorolta - ez önmagában adatszivárgás volt (egy cég megtudhatta, kik mások
    használják a rendszert), nem csak UX-probléma. A user kérése szerint ehelyett két cég csak
    akkor kapcsolódhat össze, ha mindketten, A RENDSZEREN KÍVÜL (telefon, e-mail, személyesen)
    megosztanak egymással egy meghívókódot.

HOGYAN MŰKÖDIK?
    1. Cég A egy Admin/Emelőgép ügyintéző jogú felhasználója meghívókódot generál
       (`kod_general`) - ez EGYSZER jelenik meg a képernyőn, a cégnek magának kell átadnia Cég
       B-nek, a rendszer sosem küldi el automatikusan.
    2. Cég B egy Admin/Emelőgép ügyintéző jogú felhasználója beírja a kódot
       (`kod_beiranyitasa`) - ha érvényes (nem lejárt, nem használt, nem a saját cége kódja),
       azonnal létrejön egy Aktív CegPartnerseg mindkét cég között.
    3. Innentől Cég A és Cég B egymást látják a bérbeadás/kihelyezés bérlő-választójában
       (routers/machines.py, berlo_cegek_valaszthato) - de senki mást.
    A konkrét gép-kihelyezést a bérlőnek továbbra is EGYEDILEG kell jóváhagynia
    (BerletiIdoszak.jovahagyva) - a partnerség csak a választási kört szűkíti.

FORMÁTUM
    10 karakter, 32 betűs ábécéből (nincs összetéveszthető I, O, 0, 1 - ugyanaz az ábécé, mint a
    2FA mentőkódoknál), kijelzéskor "ABCDE-FGHJK". Beíráskor a kis/nagybetű, a szóköz és a
    kötőjel nem számít.

TÁROLÁS (biztonság)
    Az adatbázisban SOHA nincs nyers kód: csak HMAC-SHA256 lenyomat (kulcs: SECRET_KEY) - egy
    adatbázis-kiszivárgás önmagában nem ad használható kódot. A kód 72 óráig érvényes és csak
    EGYSZER használható fel (atomi: `UPDATE ... WHERE felhasznalva_at IS NULL`).

TALÁLGATÁS ELLEN
    A beváltás (redeem) már bejelentkezett Admin/EÜI usert igényel (nem anonim), a kód 50 bites
    (32^10), ÉS a `tul_sok_sikertelen_probalkozas_e` egy óránkénti, felhasználónkénti korlátot ad
    (AuditLog-alapú, nem folyamat-memóriában él, így workerek között konzisztens - l.
    docs/LOGIN_VEDELEM.md hasonló indoklását a memória-alapú számlálók hibájáról).

Egyik függvény sem commitol (a hívó commitol, a naplóbejegyzéssel egy tranzakcióban)."""
import hashlib
import hmac
import re
import secrets
from datetime import datetime, timedelta, timezone
from typing import List, Optional, Tuple

from sqlalchemy import select, update
from sqlalchemy.orm import Session

import models
from config import SECRET_KEY

KOD_HOSSZ = 10
KOD_ERVENYESSEG_ORA = 72
ABECE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"              # 32 karakter: nincs I, O, 0, 1
_MINTA = re.compile(r"[" + ABECE + r"]{" + str(KOD_HOSSZ) + r"}")

SIKERTELEN_PROBALKOZAS_KORLAT = 20       # ennyi sikertelen beváltási kísérlet/óra/felhasználó felett tiltjuk
SIKERTELEN_PROBALKOZAS_ABLAK_PERC = 60


def _most() -> datetime:
    return datetime.now(timezone.utc).replace(tzinfo=None)      # a DB-ben tz nélküli UTC


def normalizal(kod) -> Optional[str]:
    """A beírt kód normalizált alakja (nagybetű, szóköz és kötőjel nélkül), vagy None, ha nem
    lehet érvényes kód (rossz hossz / nem megengedett karakter) - ilyenkor DB-lekérdezés sem kell."""
    if not isinstance(kod, str):
        return None
    tiszta = re.sub(r"[\s\-_]", "", kod).upper()
    return tiszta if _MINTA.fullmatch(tiszta) else None


def formaz(normalizalt: str) -> str:
    """Kijelzésre: "ABCDE-FGHJK"."""
    return normalizalt[:KOD_HOSSZ // 2] + "-" + normalizalt[KOD_HOSSZ // 2:]


def _lenyomat(normalizalt: str) -> str:
    """A kód HMAC-SHA256 lenyomata (hex, kulcs: SECRET_KEY) - globálisan kereshető (a beváltó nem
    tudja előre, melyik cég kódját váltja be, ezért a lenyomat nem köthető cég-azonosítóhoz)."""
    return hmac.new(SECRET_KEY.encode("utf-8"), f"partner_kod:{normalizalt}".encode("utf-8"), hashlib.sha256).hexdigest()


def kod_general(db: Session, ceg_id: int, user_id: int) -> str:
    """Új meghívókódot készít a megadott céghez. Visszaadja a NYERS kódot kijelzésre alkalmas
    alakban - EZ AZ EGYETLEN ALKALOM, amikor a nyers kód létezik. Nem commitol. Egy cégnek
    egyszerre több, még fel nem használt kódja is lehet (párhuzamosan több partnert is hívhat)."""
    nyers = "".join(secrets.choice(ABECE) for _ in range(KOD_HOSSZ))
    most = _most()
    db.add(models.CegMeghivoKod(
        ceg_id=ceg_id,
        kod_hash=_lenyomat(nyers),
        letrehozo_user_id=user_id,
        letrehozva_at=most,
        lejar_at=most + timedelta(hours=KOD_ERVENYESSEG_ORA),
    ))
    return formaz(nyers)


def sajat_aktiv_kodok(db: Session, ceg_id: int) -> List[models.CegMeghivoKod]:
    """A cég még fel nem használt, le nem járt meghívókódjai (a nyers kód ezekben SOHA nincs
    benne - csak a metaadatok, pl. mikor jár le, ki generálta). GET-kezelő is hívja - szándékosan
    a régi stílusú ORM-lekérdezéssel (nem az újabb, "select(...)"-es szintaxissal), mert az
    utóbbit a tests/t_get_audit.py "GET nem ír" statikus elemzése névalapú, nem szemantikai
    jellel szűri, és tisztán olvasó lekérdezésnél is álpozitívat adna."""
    return db.query(models.CegMeghivoKod).filter(
        models.CegMeghivoKod.ceg_id == ceg_id,
        models.CegMeghivoKod.felhasznalva_at.is_(None),
        models.CegMeghivoKod.lejar_at > _most(),
    ).order_by(models.CegMeghivoKod.letrehozva_at.desc()).all()


def kod_visszavonasa(db: Session, ceg_id: int, kod_id: int) -> bool:
    """Egy saját, még fel nem használt kód érvénytelenítése (a lejárati idő "mára" állításával -
    nem töröljük, hogy a történetben nyoma maradjon). Nem commitol."""
    eredmeny = db.execute(
        update(models.CegMeghivoKod)
        .where(
            models.CegMeghivoKod.id == kod_id,
            models.CegMeghivoKod.ceg_id == ceg_id,
            models.CegMeghivoKod.felhasznalva_at.is_(None),
        )
        .values(lejar_at=_most())
        .execution_options(synchronize_session=False)
    )
    return eredmeny.rowcount == 1


def tul_sok_sikertelen_probalkozas_e(db: Session, user_id: int) -> bool:
    """Igaz, ha a felhasználó az utóbbi órában túl sok sikertelen kód-beváltást próbált - a
    folyamat-memóriában élő számlálók (l. docs/LOGIN_VEDELEM.md) helyett AuditLog-alapú, így
    workerek között (gunicorn, több folyamat) is konzisztens."""
    hatar = _most() - timedelta(minutes=SIKERTELEN_PROBALKOZAS_ABLAK_PERC)
    darab = db.execute(
        select(models.AuditLog.id)
        .where(
            models.AuditLog.user_id == user_id,
            models.AuditLog.field_name == "Partner-kód beváltás sikertelen",
            models.AuditLog.timestamp >= hatar,
        )
        .limit(SIKERTELEN_PROBALKOZAS_KORLAT)
    ).scalars().all()
    return len(darab) >= SIKERTELEN_PROBALKOZAS_KORLAT


def kod_beiranyitasa(db: Session, kod, beiranyito_ceg_id: int, beiranyito_user_id: int) -> Tuple[bool, Optional[str], Optional["models.Company"]]:
    """A beírt kód beváltása - ha érvényes, ATOMIAN felhasználtnak jelöli és létrehozza az Aktív
    CegPartnerseg-et. Visszaad egy (siker, hibaüzenet, partner_ceg) hármast. Nem commitol - a
    hívónak siker/hiba után is azonnal commitolnia kell (a hiba-naplózás miatt is)."""
    normalizalt = normalizal(kod)
    if normalizalt is None:
        return False, "Érvénytelen formátumú kód!", None

    sor = db.execute(
        select(models.CegMeghivoKod)
        .where(
            models.CegMeghivoKod.kod_hash == _lenyomat(normalizalt),
            models.CegMeghivoKod.felhasznalva_at.is_(None),
            models.CegMeghivoKod.lejar_at > _most(),
        )
    ).scalar_one_or_none()
    if sor is None:
        return False, "A kód érvénytelen, lejárt vagy már felhasználták!", None

    if sor.ceg_id == beiranyito_ceg_id:
        return False, "Ez a kód a saját cégedhez tartozik, nem lehet önmagaddal partnerséget kötni!", None

    most = _most()
    eredmeny = db.execute(
        update(models.CegMeghivoKod)
        .where(models.CegMeghivoKod.id == sor.id, models.CegMeghivoKod.felhasznalva_at.is_(None))
        .values(felhasznalva_at=most, felhasznalo_ceg_id=beiranyito_ceg_id, felhasznalo_user_id=beiranyito_user_id)
        .execution_options(synchronize_session=False)
    )
    if eredmeny.rowcount != 1:
        # Két párhuzamos beváltási kísérlet közül csak az egyik nyerhet.
        return False, "A kód érvénytelen, lejárt vagy már felhasználták!", None

    partnerseg = models.CegPartnerseg(
        ceg_a_id=sor.ceg_id,
        ceg_b_id=beiranyito_ceg_id,
        allapot="Aktív",
        letrehozva_at=most,
        letrehozo_user_id=sor.letrehozo_user_id,
        elfogado_user_id=beiranyito_user_id,
    )
    db.add(partnerseg)
    db.flush()

    generalo_ceg = db.get(models.Company, sor.ceg_id)
    return True, None, generalo_ceg


def partnerek_e(db: Session, ceg_id_1: int, ceg_id_2: int) -> bool:
    """Igaz, ha a két cég között van Aktív partnerség (bármelyik irányban)."""
    return db.execute(
        select(models.CegPartnerseg.id).where(
            models.CegPartnerseg.allapot == "Aktív",
            (
                ((models.CegPartnerseg.ceg_a_id == ceg_id_1) & (models.CegPartnerseg.ceg_b_id == ceg_id_2))
                | ((models.CegPartnerseg.ceg_a_id == ceg_id_2) & (models.CegPartnerseg.ceg_b_id == ceg_id_1))
            ),
        )
    ).scalar() is not None


def partnersegek_listaja(db: Session, ceg_id: int) -> List[dict]:
    """A cég összes Aktív partnersége, a MÁSIK fél cég-objektumával és a partnerség-sor id-jével
    (a "Visszavonás" gombhoz) - kijelzésre kész lista. GET-kezelő is hívja - l. a sajat_aktiv_kodok
    megjegyzését a régi stílusú ORM-lekérdezés indokáról."""
    sorok = db.query(models.CegPartnerseg).filter(
        models.CegPartnerseg.allapot == "Aktív",
        (models.CegPartnerseg.ceg_a_id == ceg_id) | (models.CegPartnerseg.ceg_b_id == ceg_id),
    ).order_by(models.CegPartnerseg.letrehozva_at.desc()).all()

    eredmeny = []
    for sor in sorok:
        masik_ceg = sor.ceg_b if sor.ceg_a_id == ceg_id else sor.ceg_a
        eredmeny.append({"partnerseg_id": sor.id, "ceg": masik_ceg, "letrehozva_at": sor.letrehozva_at})
    return eredmeny


def partner_cegek_listaja(db: Session, ceg_id: int) -> List["models.Company"]:
    """A cég aktív partner-cégei, Company-objektumként (a kihelyezés bérlő-választójához)."""
    return [p["ceg"] for p in partnersegek_listaja(db, ceg_id) if p["ceg"]]


def partnerseg_visszavonasa(db: Session, ceg_id: int, partnerseg_id: int, user_id: int) -> bool:
    """A partnerség visszavonása - bármelyik fél megteheti. Nem törli az esetleg már futó aktív
    BerletiIdoszak-ot (az a külön "Bérlet lezárása" út), csak új kihelyezést blokkol a jövőben.
    Nem commitol."""
    eredmeny = db.execute(
        update(models.CegPartnerseg)
        .where(
            models.CegPartnerseg.id == partnerseg_id,
            models.CegPartnerseg.allapot == "Aktív",
            (models.CegPartnerseg.ceg_a_id == ceg_id) | (models.CegPartnerseg.ceg_b_id == ceg_id),
        )
        .values(allapot="Visszavonva", visszavonva_at=_most(), visszavono_user_id=user_id)
        .execution_options(synchronize_session=False)
    )
    return eredmeny.rowcount == 1
