"""Benutzerobjekt, Rechtepruefung und Zugriffsdekoratoren."""

from __future__ import annotations

import functools
from dataclasses import dataclass, field
from typing import Any, Callable, Iterable, Sequence

from flask import abort, flash, g, redirect, request, session, url_for
from werkzeug.local import LocalProxy

from ..rbac.model import Action, PermissionTree

AUTH_SOURCES = ("local", "ldap")


@dataclass(frozen=True, slots=True)
class Group:
    id: int
    name: str
    is_system: bool = False
    is_default: bool = False


@dataclass(slots=True)
class User:
    """Angemeldeter Benutzer. Unveraenderlich fuer die Dauer eines Requests."""

    id: int
    username: str
    is_admin: bool = False
    is_locked: bool = False
    auth_source: str = "local"
    first_name: str = ""
    last_name: str = ""
    email: str = ""
    avatar_filename: str = ""
    totp_enabled: bool = False
    must_change_password: bool = False
    groups: tuple[Group, ...] = ()
    granted: frozenset[str] = frozenset()
    _tree: PermissionTree | None = field(default=None, repr=False, compare=False)
    _effective: frozenset[str] | None = field(default=None, repr=False, compare=False)

    # -- Anzeige ------------------------------------------------------- #

    @property
    def is_authenticated(self) -> bool:
        return True

    @property
    def display_name(self) -> str:
        full = f"{self.first_name} {self.last_name}".strip()
        return full or self.username

    @property
    def initials(self) -> str:
        parts = [p for p in (self.first_name, self.last_name) if p]
        if parts:
            return "".join(p[0] for p in parts[:2]).upper()
        return (self.username[:2] or "?").upper()

    @property
    def role_label(self) -> str:
        if self.is_admin:
            return "Administrator"
        if self.groups:
            return ", ".join(group.name for group in self.groups)
        return "Ohne Gruppe"

    @property
    def is_directory_account(self) -> bool:
        return self.auth_source == "ldap"

    # -- Rechte -------------------------------------------------------- #

    @property
    def effective_permissions(self) -> frozenset[str]:
        """Rechte nach Anwendung der Bereichs-Kill-Switches."""
        if self._effective is None:
            tree = self._tree
            self._effective = (
                frozenset(tree.effective(self.granted)) if tree is not None else frozenset()
            )
        return self._effective

    def has(self, *permissions: str) -> bool:
        """True, wenn **alle** angegebenen Rechte greifen. Admins duerfen immer alles."""
        if self.is_admin:
            return True
        if not permissions:
            return True
        return all(permission in self.effective_permissions for permission in permissions)

    def has_any(self, permissions: Iterable[str]) -> bool:
        if self.is_admin:
            return True
        effective = self.effective_permissions
        return any(permission in effective for permission in permissions)

    def can(self, resource: str, action: Action | str) -> bool:
        value = action.value if isinstance(action, Action) else str(action)
        return self.has(f"{resource}.{value}")

    def can_any(self, resource: str, *actions: Action | str) -> bool:
        return self.has_any(
            f"{resource}.{a.value if isinstance(a, Action) else a}" for a in actions
        )

    def as_audit_actor(self) -> str:
        return self.username


class AnonymousUser:
    """Nicht angemeldet. Hat keine Rechte -- auch nicht ``view``."""

    id = 0
    username = ""
    is_admin = False
    is_locked = False
    auth_source = "anonymous"
    first_name = ""
    last_name = ""
    email = ""
    avatar_filename = ""
    totp_enabled = False
    must_change_password = False
    groups: tuple[Group, ...] = ()
    granted: frozenset[str] = frozenset()

    @property
    def is_authenticated(self) -> bool:
        return False

    @property
    def display_name(self) -> str:
        return "Gast"

    @property
    def initials(self) -> str:
        return "?"

    @property
    def role_label(self) -> str:
        return "Nicht angemeldet"

    @property
    def is_directory_account(self) -> bool:
        return False

    @property
    def effective_permissions(self) -> frozenset[str]:
        return frozenset()

    def has(self, *permissions: str) -> bool:
        return False

    def has_any(self, permissions: Iterable[str]) -> bool:
        return False

    def can(self, resource: str, action: Action | str) -> bool:
        return False

    def can_any(self, resource: str, *actions: Action | str) -> bool:
        return False

    def as_audit_actor(self) -> str:
        return "anonym"


ANONYMOUS = AnonymousUser()


def _lookup_user() -> User | AnonymousUser:
    return getattr(g, "user", ANONYMOUS)


#: Aktueller Benutzer -- in Templates und Views gleich benutzbar.
current_user: Any = LocalProxy(_lookup_user)


# --------------------------------------------------------------------------- #
# Dekoratoren
# --------------------------------------------------------------------------- #


def _deny(message: str) -> Any:
    """Nicht angemeldet -> Login; angemeldet aber ohne Recht -> 403."""
    if not current_user.is_authenticated:
        if request.method == "GET" and not request.headers.get("X-Requested-With"):
            session["_after_login"] = request.full_path if request.query_string else request.path
        if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
            abort(401, description="Anmeldung erforderlich.")
        flash("Bitte melden Sie sich an.", "info")
        return redirect(url_for("auth.login"))
    abort(403, description=message)


def login_required(view: Callable[..., Any]) -> Callable[..., Any]:
    @functools.wraps(view)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if not current_user.is_authenticated:
            return _deny("Anmeldung erforderlich.")
        return view(*args, **kwargs)

    return wrapper


def permission_required(*permissions: str, mode: str = "all") -> Callable[..., Any]:
    """Erzwingt Rechte serverseitig.

    ``mode="any"`` fuer Seiten, die mehrere unabhaengige Rechte akzeptieren --
    wichtig, weil im Vorgaenger eine Seite mit reinem Export-Recht nicht
    erreichbar war, obwohl das Recht vergeben war.
    """

    def decorator(view: Callable[..., Any]) -> Callable[..., Any]:
        @functools.wraps(view)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            ok = (
                current_user.has_any(permissions)
                if mode == "any"
                else current_user.has(*permissions)
            )
            if not ok:
                return _deny("Fuer diese Seite fehlt Ihnen die Berechtigung.")
            return view(*args, **kwargs)

        wrapper._required_permissions = permissions  # type: ignore[attr-defined]
        return wrapper

    return decorator


def admin_required(view: Callable[..., Any]) -> Callable[..., Any]:
    """Nur echte Administratoren -- nicht ueber Gruppenrechte delegierbar.

    Fuer Grenzen, die einen lokalen Admin *nicht* ueberleben muessen, aber auch
    nicht an eine Gruppe vergeben werden sollen (Rechtevergabe selbst,
    Navigationsreihenfolge, Schluesselrotation).
    """

    @functools.wraps(view)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if not current_user.is_authenticated or not current_user.is_admin:
            return _deny("Diese Aktion ist Administratoren vorbehalten.")
        return view(*args, **kwargs)

    return wrapper


def reauth_required(view: Callable[..., Any]) -> Callable[..., Any]:
    """Verlangt eine frische Passworteingabe ("sudo-Modus").

    Vorgeschaltet bei allem, was Geheimnisse offenlegt oder Daten aus dem System
    heraustraegt: Klartextpasswoerter, Export, Schluesselrotation, 2FA-Aenderung.
    Ein uebernommenes Sitzungscookie allein reicht dafuer nicht mehr aus.
    """

    @functools.wraps(view)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        from .service import reauth_is_fresh  # zyklischen Import vermeiden

        if not current_user.is_authenticated:
            return _deny("Anmeldung erforderlich.")
        if not reauth_is_fresh():
            session["_after_reauth"] = request.full_path if request.query_string else request.path
            flash("Bitte bestaetigen Sie zur Sicherheit Ihr Passwort.", "info")
            return redirect(url_for("auth.reauth"))
        return view(*args, **kwargs)

    return wrapper


def build_user(
    row: dict[str, Any],
    *,
    groups: Sequence[Group],
    granted: Iterable[str],
    tree: PermissionTree,
) -> User:
    return User(
        id=int(row["id"]),
        username=str(row["username"]),
        is_admin=bool(row.get("is_admin")),
        is_locked=bool(row.get("is_locked")),
        auth_source=str(row.get("auth_source") or "local"),
        first_name=str(row.get("first_name") or ""),
        last_name=str(row.get("last_name") or ""),
        email=str(row.get("email") or ""),
        avatar_filename=str(row.get("avatar_filename") or ""),
        totp_enabled=bool(row.get("totp_secret")),
        must_change_password=bool(row.get("must_change_password")),
        groups=tuple(groups),
        granted=frozenset(granted),
        _tree=tree,
    )
