"""
Auth-lag for TeeCup.

Ekte autentisering: magic-link ELLER e-post+passord (ADR-021, begge fører til
samme sesjons-cookie), pluss valgfri/påkrevd 2FA (TOTP eller e-post-
engangskode). `get_current_user` dekoder og verifiserer den innsendte
sesjonscookien, og bekrefter at brukeren fortsatt finnes.

`get_authorized_org` er fortsatt sikkerhetskritisk uendret: den verifiserer at
den innloggede brukeren faktisk er medlem av organisasjonen før org-konteksten
settes.

Hvorfor det er kritisk: RLS stoler blindt på `app.current_org`. Setter appen den
til en organisasjon brukeren ikke tilhører, gir RLS lydig tilgang til den
organisasjonens data. Isolasjonen står og faller altså på at denne sjekken skjer
FØR org_connection kalles — aldri sett konteksten fra en uverifisert kilde.

Sesjons-STADIER (ADR-021 Beslutning E): en sesjonscookie er ikke nødvendigvis
en FULL sesjon. Når 2FA kreves, utstedes et kortlevd JWT med
`stage: "pending_2fa"` eller `"must_enroll_2fa"` i stedet for en full
30-dagers sesjon -- `get_current_user` (brukt av ALLE vanlige endepunkter)
avviser eksplisitt alt annet enn `stage: "full"` (eller en ELDRE token uten
noe stage-felt i det hele tatt, utstedt før denne runden -- behandles som
"full" for bakoverkompatibilitet, ingen eksisterende bruker logges brått ut).
De egne 2FA-endepunktene bruker i stedet `get_pending_user`, som KUN godtar
en mellomtilstand.
"""

import time
from dataclasses import dataclass
from typing import Literal

import jwt
import pyotp
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, InvalidHash
from fastapi import Depends, Request, WebSocket

from .config import settings
from .db import plain_connection
from .errors import app_error

SESSION_COOKIE_NAME = "teecup_session"
_JWT_ALGORITHM = "HS256"  # Eksplisitt både ved signering og dekoding -- blokkerer
                          # alg-forvirring/"alg:none"-angrep.

Stage = Literal["full", "pending_2fa", "must_enroll_2fa"]
_PENDING_STAGE_MAX_AGE_SECONDS = 5 * 60  # kort levetid -- kun for å fullføre 2FA


@dataclass(frozen=True)
class CurrentUser:
    user_id: str


@dataclass(frozen=True)
class PendingUser:
    """En bruker som har bevist primær-identitet (magic-link/passord), men
    IKKE fullført 2FA ennå -- se modul-docstring."""

    user_id: str
    stage: Stage


def create_session_token(user_id: str, stage: Stage = "full") -> str:
    now = int(time.time())
    max_age = settings.SESSION_MAX_AGE_SECONDS if stage == "full" else _PENDING_STAGE_MAX_AGE_SECONDS
    payload: dict = {"sub": user_id, "iat": now, "exp": now + max_age}
    if stage != "full":
        payload["stage"] = stage
    return jwt.encode(payload, settings.SESSION_SECRET, algorithm=_JWT_ALGORITHM)


# ---------------------------------------------------------------------------
# Passord (ADR-021 Beslutning B) -- Argon2id, ikke bcrypt (unngår 72-byte-
# trunkering, viktig siden spesialtegn/mellomrom skal fungere korrekt).
# ---------------------------------------------------------------------------

_password_hasher = PasswordHasher()


def hash_password(raw_password: str) -> str:
    return _password_hasher.hash(raw_password)


def verify_password(raw_password: str, password_hash: str) -> bool:
    try:
        _password_hasher.verify(password_hash, raw_password)
        return True
    except (VerifyMismatchError, InvalidHash):
        return False


# ---------------------------------------------------------------------------
# TOTP (ADR-021 Beslutning C) -- RFC 6238, virker med enhver standard
# autentisator-app (Google Authenticator/Authy/1Password osv.).
# ---------------------------------------------------------------------------


def generate_totp_secret() -> str:
    return pyotp.random_base32()


def totp_provisioning_uri(secret: str, email: str) -> str:
    return pyotp.totp.TOTP(secret).provisioning_uri(name=email, issuer_name="TeeCup")


def verify_totp_code(secret: str, code: str) -> bool:
    # valid_window=1 tillater ett 30-sekunders steg klokkedrift hver vei --
    # standard toleranse, unngår at en litt ute-av-synk klokke på
    # brukerens enhet gir falske avvisninger.
    return pyotp.TOTP(secret).verify(code, valid_window=1)


def should_use_secure_cookies(request: Request) -> bool:
    """Secure-flagget skal kun være sant over https.

    Stoler på X-Forwarded-Proto -- dette er KUN trygt fordi appen ikke skal
    være nåbar unntatt gjennom Caddy, som terminerer TLS og proxyer videre
    over vanlig http internt.
    """
    if request.url.scheme == "https":
        return True
    return request.headers.get("x-forwarded-proto", "").strip().lower() == "https"


async def get_current_user(request: Request) -> CurrentUser:
    """Dekoder sesjonscookien og bekrefter at brukeren fortsatt finnes.

    Det siste steget (et ekte DB-oppslag, ikke bare å stole på JWT-en) gir
    faktisk tilbakekalling: en slettet/deaktivert bruker kan ikke ri ut
    resten av sesjonens levetid på en ellers gyldig token.

    Avviser eksplisitt alt annet enn `stage: "full"` -- en `pending_2fa`-
    eller `must_enroll_2fa`-token gir ALDRI tilgang her, kun via
    `get_pending_user` på de egne 2FA-endepunktene (se modul-docstring).
    """
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        raise app_error(401, "NOT_AUTHENTICATED", "Ikke innlogget.")
    try:
        claims = jwt.decode(token, settings.SESSION_SECRET, algorithms=[_JWT_ALGORITHM])
    except jwt.PyJWTError:
        raise app_error(401, "NOT_AUTHENTICATED", "Ugyldig eller utløpt sesjon.")

    user_id = claims.get("sub")
    if not user_id:
        raise app_error(401, "NOT_AUTHENTICATED", "Ugyldig sesjon.")
    if claims.get("stage", "full") != "full":
        raise app_error(401, "NOT_AUTHENTICATED", "Innloggingen er ikke fullført (2FA gjenstår).")

    async with plain_connection() as conn:
        exists = await conn.fetchval("SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1)", user_id)
    if not exists:
        raise app_error(401, "NOT_AUTHENTICATED", "Brukeren finnes ikke lenger.")

    return CurrentUser(user_id=user_id)


async def get_pending_user(request: Request) -> PendingUser:
    """Som get_current_user, men KUN for en mellomtilstand (2FA gjenstår).

    Brukt utelukkende av 2FA-verifiserings-/oppsett-endepunktene
    (app/routers/auth.py) -- avviser en FULL sesjon like strengt som en
    manglende en, siden dette endepunktet ikke gir mening for en allerede
    ferdig innlogget bruker (de bruker de vanlige kontoinnstillings-
    endepunktene for frivillig 2FA-oppsett i stedet, se
    get_current_or_enrolling_user)."""
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        raise app_error(401, "NOT_AUTHENTICATED", "Ikke innlogget.")
    try:
        claims = jwt.decode(token, settings.SESSION_SECRET, algorithms=[_JWT_ALGORITHM])
    except jwt.PyJWTError:
        raise app_error(401, "NOT_AUTHENTICATED", "Ugyldig eller utløpt sesjon.")

    user_id = claims.get("sub")
    stage = claims.get("stage", "full")
    if not user_id or stage == "full":
        raise app_error(401, "NOT_AUTHENTICATED", "Ingen 2FA-verifisering pågår.")

    async with plain_connection() as conn:
        exists = await conn.fetchval("SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1)", user_id)
    if not exists:
        raise app_error(401, "NOT_AUTHENTICATED", "Brukeren finnes ikke lenger.")

    return PendingUser(user_id=user_id, stage=stage)


async def get_current_or_enrolling_user(request: Request) -> CurrentUser:
    """Godtar BÅDE en full sesjon (frivillig 2FA-oppsett fra
    kontoinnstillinger) OG `must_enroll_2fa` (tvungen oppsett rett etter
    innlogging, ADR-021 Beslutning D) -- samme oppsett-endepunkter dekker
    begge veiene inn, siden selve oppsett-logikken er identisk."""
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        raise app_error(401, "NOT_AUTHENTICATED", "Ikke innlogget.")
    try:
        claims = jwt.decode(token, settings.SESSION_SECRET, algorithms=[_JWT_ALGORITHM])
    except jwt.PyJWTError:
        raise app_error(401, "NOT_AUTHENTICATED", "Ugyldig eller utløpt sesjon.")

    user_id = claims.get("sub")
    stage = claims.get("stage", "full")
    if not user_id or stage not in ("full", "must_enroll_2fa"):
        raise app_error(401, "NOT_AUTHENTICATED", "Ugyldig sesjon.")

    async with plain_connection() as conn:
        exists = await conn.fetchval("SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1)", user_id)
    if not exists:
        raise app_error(401, "NOT_AUTHENTICATED", "Brukeren finnes ikke lenger.")

    return CurrentUser(user_id=user_id)


async def user_requires_2fa_enrollment(conn, user_id: str) -> bool:
    """ADR-021 Beslutning D: org-eier/admin UTEN 2FA konfigurert må sette
    det opp FØR de får en full sesjon. organization_membership har ingen
    RLS (se 001), så dette fungerer på en plain_connection()."""
    return await conn.fetchval(
        """
        SELECT (two_factor_method IS NULL) AND EXISTS (
            SELECT 1 FROM organization_membership
            WHERE user_id = $1 AND role IN ('owner', 'admin')
        )
        FROM app_user WHERE id = $1
        """,
        user_id,
    )


async def get_superadmin_user(user: CurrentUser = Depends(get_current_user)) -> CurrentUser:
    """ADR-022 Beslutning D: is_super_admin er KUN manuelt DB-tildelt, aldri
    settbart via noe API-endepunkt. Denne avhengigheten sjekker bare
    flagget -- den gir ikke selv noen vei til å endre det."""
    async with plain_connection() as conn:
        is_super_admin = await conn.fetchval(
            "SELECT is_super_admin FROM app_user WHERE id = $1", user.user_id
        )
    if not is_super_admin:
        raise app_error(403, "NOT_SUPER_ADMIN", "Krever superadmin-rettigheter.")
    return user


async def get_current_user_optional(request: Request) -> CurrentUser | None:
    """Som get_current_user, men returnerer None i stedet for å kaste 401.

    Brukt av offentlige landingsside-endepunkter (ADR-018) som skal fungere
    for en helt anonym leser også -- bare med redusert tilgang (kun
    visibility='public'-turneringer), ikke en hard 401.
    """
    token = request.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        return None
    try:
        claims = jwt.decode(token, settings.SESSION_SECRET, algorithms=[_JWT_ALGORITHM])
    except jwt.PyJWTError:
        return None

    user_id = claims.get("sub")
    if not user_id or claims.get("stage", "full") != "full":
        return None

    async with plain_connection() as conn:
        exists = await conn.fetchval("SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1)", user_id)
    if not exists:
        return None

    return CurrentUser(user_id=user_id)


async def get_current_user_from_websocket(websocket: WebSocket) -> CurrentUser | None:
    """Som get_current_user_optional, men leser sesjonscookien fra en
    WebSocket-tilkobling (ADR-025) i stedet for en HTTP Request -- FastAPI
    sitt Depends()-system kan ikke gjenbruke en Request-typet avhengighet
    direkte i en WS-rute (ingen ekte Request finnes i en WS-scope), derav
    denne separate, bevisst minimale kopien av samme dekode-/oppslagslogikk.
    Returnerer None i stedet for å kaste -- ruten selv avgjør om anonym
    tilgang er greit (offentlig feed) eller ikke (lag-chat, som lukker
    tilkoblingen selv ved None)."""
    token = websocket.cookies.get(SESSION_COOKIE_NAME)
    if not token:
        return None
    try:
        claims = jwt.decode(token, settings.SESSION_SECRET, algorithms=[_JWT_ALGORITHM])
    except jwt.PyJWTError:
        return None

    user_id = claims.get("sub")
    if not user_id or claims.get("stage", "full") != "full":
        return None

    async with plain_connection() as conn:
        exists = await conn.fetchval("SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1)", user_id)
    if not exists:
        return None

    return CurrentUser(user_id=user_id)


async def get_authorized_org(
    organization_id: str,
    user: CurrentUser = Depends(get_current_user),
) -> str:
    """Returnerer organization_id KUN hvis brukeren er medlem — ellers 403.

    Medlemskapstabellen er ikke organisasjonsavgrenset, så oppslaget gjøres på en
    tilkobling uten org-kontekst.
    """
    async with plain_connection() as conn:
        is_member = await conn.fetchval(
            """
            SELECT EXISTS (
                SELECT 1 FROM organization_membership
                WHERE user_id = $1 AND organization_id = $2
            )
            """,
            user.user_id,
            organization_id,
        )
    if not is_member:
        raise app_error(403, "NOT_ORG_MEMBER", "Brukeren er ikke medlem av denne organisasjonen.")
    return organization_id
