"""Rechtemodell: Bereiche, Ressourcen, Aktionen.

Das Vorgaengermodell kannte fuenf Spalten (R/W/E/D/X), wobei ``E`` ("Aendern")
das Loeschen **mit abdeckte** und ``D``/``X`` je nach Zeile etwas voellig anderes
bedeuteten (Dashboard-Neustart bzw. Export). Das war kompakt, aber:

* Wer jemandem erlauben wollte, Geraete zu pflegen, musste ihm zwangslaeufig
  auch das Loeschen erlauben. Das ist genau die Trennung, die man in einer
  Verwaltungsanwendung am haeufigsten braucht.
* Dieselbe Spalte bedeutete in verschiedenen Zeilen Verschiedenes -- eine
  Rechtematrix, die man erklaeren muss, statt sie zu lesen.

Neues Modell: ein festes, ueberall gleich bedeutendes Aktionsvokabular. Welche
Aktionen eine Ressource ueberhaupt kennt, entscheidet die Ressource; die Matrix
rendert nur die Spalten, die im jeweiligen Bereich wirklich vorkommen.

Beibehalten wird der bewaehrte **Kill-Switch**: jeder Bereich hat ein
``<bereich>.view``-Recht. Ohne dieses greift kein einziges Recht innerhalb des
Bereichs -- unabhaengig davon, was einzeln angehakt ist. Das erlaubt es, einer
Gruppe einen ganzen Bereich mit einem Klick zu entziehen, ohne die fein
eingestellten Einzelrechte zu verlieren.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Iterable, Mapping, Sequence


class Action(str, Enum):
    """Aktionsvokabular. Bedeutung ist ueberall dieselbe -- keine Ausnahmen."""

    VIEW = "view"
    CREATE = "create"
    EDIT = "edit"
    DELETE = "delete"
    EXECUTE = "execute"
    EXPORT = "export"
    SECRETS = "secrets"


#: Kuerzel und Erklaerung je Aktion -- Kopfzeile der Rechtematrix.
ACTION_META: dict[Action, tuple[str, str, str]] = {
    Action.VIEW: ("R", "Ansehen", "Datensaetze und Seiten dieses Bereichs lesen."),
    Action.CREATE: ("C", "Anlegen", "Neue Datensaetze erstellen."),
    Action.EDIT: ("U", "Bearbeiten", "Bestehende Datensaetze aendern -- ohne Loeschen."),
    Action.DELETE: ("D", "Loeschen", "Datensaetze loeschen bzw. endgueltig entfernen."),
    Action.EXECUTE: ("X", "Ausfuehren", "Aktionen ausloesen (Neustart, Dienste, Jobs, Import)."),
    Action.EXPORT: ("E", "Exportieren", "Daten aus dem System heraustragen (Datei-Download)."),
    Action.SECRETS: (
        "S",
        "Geheimnisse",
        "Passwoerter und Schluessel im Klartext sehen oder mitnehmen.",
    ),
}

#: Aktionen, die zusaetzlich eine frische Passworteingabe verlangen (sudo-Modus).
SENSITIVE_ACTIONS: frozenset[Action] = frozenset({Action.SECRETS, Action.EXPORT})

#: ``view`` ist Voraussetzung fuer alles andere in derselben Zeile.
IMPLIED_BY: dict[Action, tuple[Action, ...]] = {
    action: (Action.VIEW,) for action in Action if action is not Action.VIEW
}


@dataclass(frozen=True, slots=True)
class Resource:
    """Eine Zeile der Rechtematrix."""

    key: str
    label: str
    actions: tuple[Action, ...]
    hint: str = ""
    #: Abweichende Erklaerung einzelner Aktionen (z. B. was "Ausfuehren" hier meint).
    action_hints: Mapping[Action, str] = field(default_factory=dict)

    def permission(self, action: Action) -> str:
        return f"{self.key}.{action.value}"

    def has(self, action: Action) -> bool:
        return action in self.actions

    def permissions(self) -> tuple[str, ...]:
        return tuple(self.permission(action) for action in self.actions)


@dataclass(frozen=True, slots=True)
class Area:
    """Ein Bereich (Gruppe von Ressourcen) mit eigenem Kill-Switch."""

    key: str
    label: str
    icon: str
    resources: tuple[Resource, ...]
    hint: str = ""

    @property
    def view_key(self) -> str:
        return f"{self.key}.view"

    def columns(self) -> tuple[Action, ...]:
        """Nur die Aktionen, die in diesem Bereich tatsaechlich vorkommen."""
        present = {action for resource in self.resources for action in resource.actions}
        return tuple(action for action in Action if action in present)

    def resource(self, key: str) -> Resource | None:
        for resource in self.resources:
            if resource.key == key:
                return resource
        return None


class PermissionTree:
    """Aus Bereichen abgeleitete Nachschlagestrukturen.

    Wird beim Import einmal gebaut und danach nur gelesen.
    """

    def __init__(self, areas: Sequence[Area]) -> None:
        self.areas: tuple[Area, ...] = tuple(areas)
        self._by_key: dict[str, Area] = {}
        self._parent: dict[str, str] = {}
        self._all: list[str] = []
        self._resources: dict[str, tuple[Area, Resource]] = {}

        seen_keys: set[str] = set()
        for area in self.areas:
            if area.key in seen_keys:
                raise ValueError(f"Doppelter Bereichsschluessel: {area.key}")
            seen_keys.add(area.key)
            self._by_key[area.key] = area
            self._all.append(area.view_key)
            for resource in area.resources:
                if resource.key in seen_keys:
                    raise ValueError(f"Doppelter Ressourcenschluessel: {resource.key}")
                seen_keys.add(resource.key)
                if Action.VIEW not in resource.actions:
                    raise ValueError(
                        f"Ressource {resource.key} muss die Aktion 'view' anbieten -- "
                        "sie ist Voraussetzung fuer jede andere Aktion."
                    )
                self._resources[resource.key] = (area, resource)
                for permission in resource.permissions():
                    self._all.append(permission)
                    self._parent[permission] = area.view_key

    # ------------------------------------------------------------------ #

    def all_keys(self) -> tuple[str, ...]:
        return tuple(self._all)

    def area(self, key: str) -> Area | None:
        return self._by_key.get(key)

    def parent_view_key(self, permission: str) -> str | None:
        return self._parent.get(permission)

    def resource(self, key: str) -> tuple[Area, Resource] | None:
        return self._resources.get(key)

    def label_for(self, permission: str) -> str:
        """Menschenlesbare Beschreibung eines Rechteschluessels -- fuer Audit-Logs."""
        if permission.endswith(".view") and permission[:-5] in self._by_key:
            return f"Bereich {self._by_key[permission[:-5]].label}: Zugang"
        resource_key, _, action_value = permission.rpartition(".")
        entry = self._resources.get(resource_key)
        if entry is None:
            return permission
        _, resource = entry
        try:
            action = Action(action_value)
        except ValueError:
            return permission
        return f"{resource.label}: {ACTION_META[action][1]}"

    def sanitize(self, permissions: Iterable[str]) -> list[str]:
        """Wirft unbekannte Rechte weg und ergaenzt implizierte.

        Regeln:
        * Ein Recht, das nicht im Baum steht, wird verworfen (kein stilles
          Weiterschleppen von Altrechten nach einem Refactoring).
        * Jede Aktion impliziert ``view`` derselben Zeile.
        * Existiert irgendein Recht eines Bereichs, wird der Kill-Switch des
          Bereichs mitgesetzt -- sonst waere die Auswahl wirkungslos und der
          Bearbeiter wuerde sich wundern.
        """
        known = set(self._all)
        result: set[str] = {p for p in permissions if p in known}
        for permission in list(result):
            resource_key, _, action_value = permission.rpartition(".")
            if resource_key in self._resources:
                try:
                    action = Action(action_value)
                except ValueError:
                    continue
                for implied in IMPLIED_BY.get(action, ()):
                    candidate = f"{resource_key}.{implied.value}"
                    if candidate in known:
                        result.add(candidate)
        for area in self.areas:
            if any(p in result for resource in area.resources for p in resource.permissions()):
                result.add(area.view_key)
        return sorted(result)

    def effective(self, permissions: Iterable[str]) -> set[str]:
        """Rechte, die nach Anwendung der Kill-Switches tatsaechlich greifen."""
        granted = set(permissions)
        active = {key for key in granted if key.endswith(".view") and key[:-5] in self._by_key}
        return {
            key
            for key in granted
            if (parent := self._parent.get(key)) is None or parent in active
        }

    def expand_preset(self, preset: "RolePreset") -> list[str]:
        return self.sanitize(preset.permissions(self))


@dataclass(frozen=True, slots=True)
class RolePreset:
    """Benannte Rechtevorlage -- wird beim ersten Start als Gruppe angelegt.

    Der Vorgaenger kannte nur "Admin" und eine Standardgruppe "Benutzer"; jede
    weitere Rolle musste jeder Kunde von Hand zusammenklicken. Vorlagen machen
    aus der haeufigsten Aufgabe einen Startpunkt.
    """

    key: str
    name: str
    description: str
    #: ``(ressource, aktionen)``-Paare, ``"*"`` fuer alle Aktionen der Ressource.
    grants: tuple[tuple[str, tuple[Action, ...] | str], ...]
    is_default: bool = False
    """Standardgruppe: wird jedem neuen Nicht-Admin automatisch zugeordnet."""

    def permissions(self, tree: PermissionTree) -> list[str]:
        result: list[str] = []
        for resource_key, actions in self.grants:
            entry = tree.resource(resource_key)
            if entry is None:
                continue
            _, resource = entry
            wanted = resource.actions if actions == "*" else tuple(a for a in actions if resource.has(a))  # type: ignore[arg-type]
            result.extend(resource.permission(action) for action in wanted)
        return result
