"""Benutzer- und Gruppenverwaltung inklusive Eskalationsschutz.

Die wichtigste inhaltliche Korrektur gegenueber dem Vorgaenger steckt in
``guard_no_amplification``:

Im Vorgaenger konnte jeder, der das Recht ``groups.edit`` besass, einer Gruppe
*beliebige* Rechte zuweisen -- also auch Rechte, die er selbst nicht hatte --
und sich anschliessend selbst in diese Gruppe setzen. Das Rechtesystem war
damit fuer jeden Gruppenverwalter effektiv offen. Hier gilt: wer selbst kein
Recht X hat, kann Recht X auch nicht vergeben. Entziehen ist immer erlaubt.
Echte Administratoren sind davon ausgenommen (sie haben ohnehin alles).

Weitere fest verdrahtete Grenzen, die kein Gruppenrecht aushebeln kann:

* Das Admin-Kennzeichen setzt oder entfernt nur ein echter Administrator.
* Ein bestehendes Administratorkonto bearbeitet, sperrt oder loescht nur ein
  echter Administrator.
* Es muss immer mindestens ein entsperrtes Administratorkonto uebrig bleiben.
* Niemand kann sich selbst das Admin-Kennzeichen entziehen oder sich selbst
  sperren (haeufigster Weg, sich komplett auszusperren).
"""

from __future__ import annotations

import sqlite3
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Iterable, Sequence

from ..db import Database
from ..security import passwords
from .model import Action, PermissionTree, RolePreset


class PermissionDenied(Exception):
    """Verstoss gegen eine feste Sicherheitsgrenze -- wird als Klartext angezeigt."""


class ValidationError(Exception):
    """Eingabefehler, der dem Benutzer erklaert wird."""


def _now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


# --------------------------------------------------------------------------- #
# Gruppen
# --------------------------------------------------------------------------- #


@dataclass(frozen=True, slots=True)
class GroupRecord:
    id: int
    name: str
    description: str
    is_system: bool
    is_default: bool
    preset_key: str
    permissions: tuple[str, ...]
    member_count: int
    deleted_at: str | None = None


def list_groups(
    conn: sqlite3.Connection, *, include_deleted: bool = False
) -> list[GroupRecord]:
    where = "" if include_deleted else "WHERE g.deleted_at IS NULL"
    rows = Database.all(
        conn,
        f"""
        SELECT g.*,
               (SELECT COUNT(*) FROM user_groups ug
                 JOIN users u ON u.id = ug.user_id AND u.deleted_at IS NULL
                WHERE ug.group_id = g.id) AS member_count
          FROM groups g {where}
         ORDER BY g.is_system DESC, g.name COLLATE NOCASE
        """,
    )
    perms = _permissions_by_group(conn)
    return [
        GroupRecord(
            id=int(row["id"]),
            name=str(row["name"]),
            description=str(row["description"] or ""),
            is_system=bool(row["is_system"]),
            is_default=bool(row["is_default"]),
            preset_key=str(row["preset_key"] or ""),
            permissions=tuple(perms.get(int(row["id"]), ())),
            member_count=int(row["member_count"] or 0),
            deleted_at=row["deleted_at"],
        )
        for row in rows
    ]


def _permissions_by_group(conn: sqlite3.Connection) -> dict[int, list[str]]:
    result: dict[int, list[str]] = {}
    for row in Database.all(conn, "SELECT group_id, permission FROM group_permissions"):
        result.setdefault(int(row["group_id"]), []).append(str(row["permission"]))
    for values in result.values():
        values.sort()
    return result


def get_group(conn: sqlite3.Connection, group_id: int) -> GroupRecord | None:
    for group in list_groups(conn, include_deleted=True):
        if group.id == group_id:
            return group
    return None


def group_permissions(conn: sqlite3.Connection, group_id: int) -> list[str]:
    return [
        str(row["permission"])
        for row in Database.all(
            conn,
            "SELECT permission FROM group_permissions WHERE group_id=? ORDER BY permission",
            (group_id,),
        )
    ]


def guard_no_amplification(
    actor: Any, tree: PermissionTree, added: Iterable[str], *, what: str = "Recht"
) -> None:
    """Verhindert, dass jemand Rechte vergibt, die er selbst nicht besitzt."""
    if getattr(actor, "is_admin", False):
        return
    missing = sorted(p for p in added if not actor.has(p))
    if missing:
        labels = ", ".join(tree.label_for(p) for p in missing[:6])
        more = "" if len(missing) <= 6 else f" (und {len(missing) - 6} weitere)"
        raise PermissionDenied(
            f"Sie koennen kein {what} vergeben, das Sie selbst nicht besitzen: {labels}{more}."
        )


def create_group(
    conn: sqlite3.Connection,
    *,
    name: str,
    description: str,
    permissions: Sequence[str],
    tree: PermissionTree,
    actor: Any,
    preset_key: str = "",
    is_system: bool = False,
    is_default: bool = False,
) -> int:
    clean_name = (name or "").strip()
    if not clean_name:
        raise ValidationError("Der Gruppenname darf nicht leer sein.")
    if len(clean_name) > 64:
        raise ValidationError("Der Gruppenname ist zu lang (maximal 64 Zeichen).")
    existing = Database.one(
        conn, "SELECT id, deleted_at FROM groups WHERE name=? COLLATE NOCASE", (clean_name,)
    )
    if existing is not None:
        if existing["deleted_at"]:
            raise ValidationError(
                f"Eine Gruppe namens {clean_name!r} liegt im Papierkorb. "
                "Stellen Sie sie wieder her oder loeschen Sie sie endgueltig."
            )
        raise ValidationError(f"Eine Gruppe namens {clean_name!r} existiert bereits.")

    wanted = tree.sanitize(permissions)
    guard_no_amplification(actor, tree, wanted)

    cursor = conn.execute(
        "INSERT INTO groups (name, description, is_system, is_default, preset_key, "
        "created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
        (clean_name, (description or "").strip(), int(is_system), int(is_default), preset_key, _now(), _now()),
    )
    group_id = int(cursor.lastrowid or 0)
    _write_permissions(conn, group_id, wanted)
    return group_id


def _write_permissions(conn: sqlite3.Connection, group_id: int, permissions: Sequence[str]) -> None:
    conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
    conn.executemany(
        "INSERT INTO group_permissions (group_id, permission) VALUES (?,?)",
        [(group_id, permission) for permission in permissions],
    )


def update_group(
    conn: sqlite3.Connection,
    group_id: int,
    *,
    name: str | None = None,
    description: str | None = None,
    permissions: Sequence[str] | None = None,
    tree: PermissionTree,
    actor: Any,
    unlock_system: bool = False,
) -> list[str]:
    """Aendert eine Gruppe. Rueckgabe: Liste der Aenderungen fuer das Audit-Log."""
    group = get_group(conn, group_id)
    if group is None or group.deleted_at:
        raise ValidationError("Diese Gruppe existiert nicht (mehr).")

    changes: list[str] = []

    if name is not None and name.strip() != group.name:
        if group.is_system:
            raise PermissionDenied("Systemgruppen koennen nicht umbenannt werden.")
        clean = name.strip()
        if not clean:
            raise ValidationError("Der Gruppenname darf nicht leer sein.")
        clash = Database.one(
            conn,
            "SELECT id FROM groups WHERE name=? COLLATE NOCASE AND id<>? AND deleted_at IS NULL",
            (clean, group_id),
        )
        if clash is not None:
            raise ValidationError(f"Eine Gruppe namens {clean!r} existiert bereits.")
        conn.execute("UPDATE groups SET name=?, updated_at=? WHERE id=?", (clean, _now(), group_id))
        changes.append(f"Name: {group.name} -> {clean}")

    if description is not None and description.strip() != group.description:
        conn.execute(
            "UPDATE groups SET description=?, updated_at=? WHERE id=?",
            (description.strip(), _now(), group_id),
        )
        changes.append("Beschreibung geaendert")

    if permissions is not None:
        if group.is_system and not unlock_system:
            raise PermissionDenied(
                "Die Rechte dieser Systemgruppe sind gesperrt. "
                "Sie muessen sie zuerst ausdruecklich freischalten."
            )
        wanted = tree.sanitize(permissions)
        added = sorted(set(wanted) - set(group.permissions))
        removed = sorted(set(group.permissions) - set(wanted))
        guard_no_amplification(actor, tree, added)
        if added or removed:
            _write_permissions(conn, group_id, wanted)
            conn.execute("UPDATE groups SET updated_at=? WHERE id=?", (_now(), group_id))
            if added:
                changes.append("Rechte hinzugefuegt: " + ", ".join(tree.label_for(p) for p in added))
            if removed:
                changes.append("Rechte entfernt: " + ", ".join(tree.label_for(p) for p in removed))

    return changes


def soft_delete_group(conn: sqlite3.Connection, group_id: int) -> None:
    group = get_group(conn, group_id)
    if group is None:
        raise ValidationError("Diese Gruppe existiert nicht.")
    if group.is_system:
        raise PermissionDenied("Systemgruppen koennen nicht geloescht werden.")
    conn.execute("UPDATE groups SET deleted_at=? WHERE id=?", (_now(), group_id))


def restore_group(conn: sqlite3.Connection, group_id: int) -> None:
    group = get_group(conn, group_id)
    if group is None:
        raise ValidationError("Diese Gruppe existiert nicht.")
    clash = Database.one(
        conn,
        "SELECT id FROM groups WHERE name=? COLLATE NOCASE AND id<>? AND deleted_at IS NULL",
        (group.name, group_id),
    )
    if clash is not None:
        raise ValidationError(
            f"Es existiert bereits eine aktive Gruppe namens {group.name!r}. "
            "Benennen Sie diese zuerst um."
        )
    conn.execute("UPDATE groups SET deleted_at=NULL WHERE id=?", (group_id,))


def purge_group(conn: sqlite3.Connection, group_id: int) -> None:
    # Mitgliedschaften und Rechte haengen per ON DELETE CASCADE dran.
    conn.execute("DELETE FROM groups WHERE id=?", (group_id,))


def ensure_presets(
    conn: sqlite3.Connection, tree: PermissionTree, presets: Sequence[RolePreset]
) -> list[str]:
    """Legt fehlende Vorlagen-Gruppen an.

    Wichtig -- Unterschied zum Vorgaenger: eine bereits existierende Gruppe wird
    **nicht** bei jedem Start auf den Vorlagen-Rechtesatz zurueckgesetzt. Der
    Vorgaenger tat das fuer die Standardgruppe "Benutzer", wodurch jede
    Anpassung durch den Kunden beim naechsten Neustart verschwand. Wer die
    Vorlage neu anwenden will, tut das ausdruecklich in der Oberflaeche.
    """
    created: list[str] = []
    for preset in presets:
        row = Database.one(
            conn, "SELECT id FROM groups WHERE name=? COLLATE NOCASE", (preset.name,)
        )
        if row is not None:
            continue
        permissions = tree.expand_preset(preset)
        cursor = conn.execute(
            "INSERT INTO groups (name, description, is_system, is_default, preset_key, "
            "created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
            (
                preset.name,
                preset.description,
                1 if preset.is_default else 0,
                1 if preset.is_default else 0,
                preset.key,
                _now(),
                _now(),
            ),
        )
        _write_permissions(conn, int(cursor.lastrowid or 0), permissions)
        created.append(preset.name)
    return created


def default_group_id(conn: sqlite3.Connection) -> int | None:
    value = Database.value(
        conn, "SELECT id FROM groups WHERE is_default=1 AND deleted_at IS NULL ORDER BY id LIMIT 1"
    )
    return int(value) if value is not None else None


# --------------------------------------------------------------------------- #
# Benutzer
# --------------------------------------------------------------------------- #


def granted_permissions(conn: sqlite3.Connection, user_id: int) -> list[str]:
    """Vereinigung der Rechte aller (nicht geloeschten) Gruppen des Benutzers."""
    return [
        str(row["permission"])
        for row in Database.all(
            conn,
            """
            SELECT DISTINCT gp.permission
              FROM user_groups ug
              JOIN groups g ON g.id = ug.group_id AND g.deleted_at IS NULL
              JOIN group_permissions gp ON gp.group_id = g.id
             WHERE ug.user_id = ?
            """,
            (user_id,),
        )
    ]


def user_groups(conn: sqlite3.Connection, user_id: int) -> list[dict[str, Any]]:
    return Database.all(
        conn,
        "SELECT g.id, g.name, g.is_system, g.is_default FROM user_groups ug "
        "JOIN groups g ON g.id = ug.group_id AND g.deleted_at IS NULL "
        "WHERE ug.user_id=? ORDER BY g.name COLLATE NOCASE",
        (user_id,),
    )


def list_users(
    conn: sqlite3.Connection, *, include_deleted: bool = False
) -> list[dict[str, Any]]:
    where = "" if include_deleted else "WHERE u.deleted_at IS NULL"
    users = Database.all(
        conn,
        f"""
        SELECT u.id, u.username, u.is_admin, u.is_locked, u.auth_source, u.first_name,
               u.last_name, u.email, u.avatar_filename, u.last_login_at, u.created_at,
               u.deleted_at, u.must_change_password,
               CASE WHEN u.totp_secret <> '' THEN 1 ELSE 0 END AS totp_enabled
          FROM users u {where}
         ORDER BY u.username COLLATE NOCASE
        """,
    )
    memberships: dict[int, list[str]] = {}
    for row in Database.all(
        conn,
        "SELECT ug.user_id, g.name FROM user_groups ug "
        "JOIN groups g ON g.id = ug.group_id AND g.deleted_at IS NULL",
    ):
        memberships.setdefault(int(row["user_id"]), []).append(str(row["name"]))
    for user in users:
        user["group_names"] = sorted(memberships.get(int(user["id"]), []))
    return users


def get_user(conn: sqlite3.Connection, user_id: int) -> dict[str, Any] | None:
    return Database.one(conn, "SELECT * FROM users WHERE id=?", (user_id,))


def find_user_by_username(
    conn: sqlite3.Connection, username: str, *, include_deleted: bool = False
) -> dict[str, Any] | None:
    suffix = "" if include_deleted else " AND deleted_at IS NULL"
    return Database.one(
        conn,
        f"SELECT * FROM users WHERE username=? COLLATE NOCASE{suffix}",
        (username,),
    )


def count_active_admins(conn: sqlite3.Connection, *, exclude_id: int | None = None) -> int:
    sql = "SELECT COUNT(*) FROM users WHERE is_admin=1 AND is_locked=0 AND deleted_at IS NULL"
    params: tuple[Any, ...] = ()
    if exclude_id is not None:
        sql += " AND id<>?"
        params = (exclude_id,)
    return int(Database.value(conn, sql, params, default=0) or 0)


def guard_target_account(actor: Any, target: dict[str, Any]) -> None:
    """Ein Administratorkonto darf nur ein echter Administrator anfassen."""
    if bool(target.get("is_admin")) and not getattr(actor, "is_admin", False):
        raise PermissionDenied(
            "Administratorkonten koennen nur von Administratoren bearbeitet werden."
        )


def guard_last_admin(conn: sqlite3.Connection, user_id: int, *, action: str) -> None:
    row = get_user(conn, user_id)
    if row is None or not row.get("is_admin"):
        return
    if count_active_admins(conn, exclude_id=user_id) == 0:
        raise PermissionDenied(
            f"{action} nicht moeglich: es muss mindestens ein entsperrtes "
            "Administratorkonto uebrig bleiben."
        )


def _assignable_groups(
    conn: sqlite3.Connection, actor: Any, tree: PermissionTree, group_ids: Sequence[int]
) -> list[int]:
    """Prueft, ob der Bearbeiter diese Gruppen zuweisen darf (kein Rechtezuwachs)."""
    if getattr(actor, "is_admin", False):
        return [int(gid) for gid in group_ids]
    result: list[int] = []
    for gid in group_ids:
        permissions = group_permissions(conn, int(gid))
        guard_no_amplification(actor, tree, permissions, what="Gruppenrecht")
        result.append(int(gid))
    return result


def set_user_groups(
    conn: sqlite3.Connection,
    user_id: int,
    group_ids: Sequence[int],
    *,
    actor: Any,
    tree: PermissionTree,
) -> None:
    allowed = _assignable_groups(conn, actor, tree, group_ids)
    conn.execute("DELETE FROM user_groups WHERE user_id=?", (user_id,))
    conn.executemany(
        "INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
        [(user_id, gid) for gid in allowed],
    )


def create_user(
    conn: sqlite3.Connection,
    *,
    username: str,
    password: str,
    is_admin: bool,
    first_name: str = "",
    last_name: str = "",
    email: str = "",
    group_ids: Sequence[int] = (),
    actor: Any,
    tree: PermissionTree,
    policy: passwords.PasswordPolicy,
    auth_source: str = "local",
    must_change_password: bool = False,
) -> int:
    clean = (username or "").strip()
    if not clean:
        raise ValidationError("Der Benutzername darf nicht leer sein.")
    if len(clean) > 64 or any(ch.isspace() for ch in clean):
        raise ValidationError("Der Benutzername darf keine Leerzeichen enthalten (max. 64 Zeichen).")

    existing = find_user_by_username(conn, clean, include_deleted=True)
    if existing is not None:
        if existing["deleted_at"]:
            raise ValidationError(
                f"Der Benutzername {clean!r} liegt im Papierkorb. Stellen Sie das Konto "
                "wieder her oder loeschen Sie es endgueltig."
            )
        raise ValidationError(f"Der Benutzername {clean!r} ist bereits vergeben.")

    if is_admin and not getattr(actor, "is_admin", False):
        raise PermissionDenied("Nur Administratoren koennen weitere Administratoren anlegen.")

    if auth_source == "local":
        problems = policy.check(password, username=clean)
        if problems:
            raise ValidationError(" ".join(problems))
        password_hash = passwords.hash_password(password)
    else:
        password_hash = ""  # Verzeichniskonto: Passwort wird nie hier geprueft.

    cursor = conn.execute(
        "INSERT INTO users (username, password_hash, is_admin, auth_source, first_name, "
        "last_name, email, must_change_password, password_changed_at, created_at, updated_at) "
        "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
        (
            clean,
            password_hash,
            int(is_admin),
            auth_source,
            first_name.strip(),
            last_name.strip(),
            email.strip(),
            int(must_change_password),
            _now() if auth_source == "local" else None,
            _now(),
            _now(),
        ),
    )
    user_id = int(cursor.lastrowid or 0)

    targets = list(group_ids)
    if not is_admin and not targets:
        fallback = default_group_id(conn)
        if fallback is not None:
            targets = [fallback]
    set_user_groups(conn, user_id, targets, actor=actor, tree=tree)
    return user_id


def update_user(
    conn: sqlite3.Connection,
    user_id: int,
    *,
    actor: Any,
    tree: PermissionTree,
    first_name: str | None = None,
    last_name: str | None = None,
    email: str | None = None,
    is_admin: bool | None = None,
    is_locked: bool | None = None,
    group_ids: Sequence[int] | None = None,
) -> list[str]:
    target = get_user(conn, user_id)
    if target is None or target["deleted_at"]:
        raise ValidationError("Dieses Konto existiert nicht (mehr).")
    guard_target_account(actor, target)

    changes: list[str] = []
    directory = str(target.get("auth_source")) == "ldap"

    if directory and any(value is not None for value in (first_name, last_name, email)):
        # Beim naechsten Login wuerden diese Felder ohnehin aus dem Verzeichnis
        # ueberschrieben -- die Bearbeitung waere eine Illusion.
        raise ValidationError(
            "Name und E-Mail eines Verzeichniskontos werden bei jeder Anmeldung "
            "aus dem Verzeichnis uebernommen und koennen hier nicht geaendert werden."
        )

    fields: dict[str, Any] = {}
    for column, value in (
        ("first_name", first_name),
        ("last_name", last_name),
        ("email", email),
    ):
        if value is not None and value.strip() != str(target.get(column) or ""):
            fields[column] = value.strip()
            changes.append(f"{column} geaendert")

    if is_admin is not None and bool(is_admin) != bool(target["is_admin"]):
        if not getattr(actor, "is_admin", False):
            raise PermissionDenied("Das Admin-Kennzeichen kann nur ein Administrator aendern.")
        if int(target["id"]) == int(getattr(actor, "id", 0)):
            raise PermissionDenied("Sie koennen Ihr eigenes Admin-Kennzeichen nicht aendern.")
        if not is_admin:
            guard_last_admin(conn, user_id, action="Herabstufen")
        fields["is_admin"] = int(bool(is_admin))
        changes.append("Administrator: " + ("ja" if is_admin else "nein"))

    if is_locked is not None and bool(is_locked) != bool(target["is_locked"]):
        if int(target["id"]) == int(getattr(actor, "id", 0)):
            raise PermissionDenied("Sie koennen Ihr eigenes Konto nicht sperren.")
        if is_locked:
            guard_last_admin(conn, user_id, action="Sperren")
        fields["is_locked"] = int(bool(is_locked))
        changes.append("Gesperrt: " + ("ja" if is_locked else "nein"))

    if fields:
        assignments = ", ".join(f"{column}=?" for column in fields)
        conn.execute(
            f"UPDATE users SET {assignments}, updated_at=? WHERE id=?",
            (*fields.values(), _now(), user_id),
        )

    if group_ids is not None:
        before = {int(row["id"]) for row in user_groups(conn, user_id)}
        after = {int(gid) for gid in group_ids}
        if before != after:
            set_user_groups(conn, user_id, sorted(after), actor=actor, tree=tree)
            changes.append("Gruppenzuordnung geaendert")

    return changes


def set_password(
    conn: sqlite3.Connection,
    user_id: int,
    new_password: str,
    *,
    policy: passwords.PasswordPolicy,
    must_change: bool = False,
) -> None:
    row = get_user(conn, user_id)
    if row is None:
        raise ValidationError("Dieses Konto existiert nicht.")
    if str(row.get("auth_source")) == "ldap":
        raise ValidationError(
            "Verzeichniskonten haben hier kein Passwort -- es wird im Verzeichnisdienst gepflegt."
        )
    problems = policy.check(new_password, username=str(row["username"]))
    if problems:
        raise ValidationError(" ".join(problems))
    conn.execute(
        "UPDATE users SET password_hash=?, must_change_password=?, password_changed_at=?, "
        "updated_at=? WHERE id=?",
        (passwords.hash_password(new_password), int(must_change), _now(), _now(), user_id),
    )


def soft_delete_user(conn: sqlite3.Connection, user_id: int, *, actor: Any) -> str:
    target = get_user(conn, user_id)
    if target is None or target["deleted_at"]:
        raise ValidationError("Dieses Konto existiert nicht (mehr).")
    guard_target_account(actor, target)
    if int(target["id"]) == int(getattr(actor, "id", 0)):
        raise PermissionDenied("Sie koennen Ihr eigenes Konto nicht loeschen.")
    guard_last_admin(conn, user_id, action="Loeschen")

    if str(target.get("auth_source")) == "ldap":
        # Ein Verzeichniskonto ist nur ein Schattenkonto: es legt sich bei der
        # naechsten gueltigen Anmeldung von selbst wieder an. Ein Papierkorb
        # waere hier bedeutungslos.
        conn.execute("DELETE FROM users WHERE id=?", (user_id,))
        return "endgueltig"

    conn.execute("UPDATE users SET deleted_at=?, updated_at=? WHERE id=?", (_now(), _now(), user_id))
    conn.execute(
        "UPDATE user_sessions SET revoked_at=?, revoked_reason='user-deleted' "
        "WHERE user_id=? AND revoked_at IS NULL",
        (_now(), user_id),
    )
    return "papierkorb"


def restore_user(conn: sqlite3.Connection, user_id: int) -> None:
    row = get_user(conn, user_id)
    if row is None:
        raise ValidationError("Dieses Konto existiert nicht.")
    clash = Database.one(
        conn,
        "SELECT id FROM users WHERE username=? COLLATE NOCASE AND id<>? AND deleted_at IS NULL",
        (row["username"], user_id),
    )
    if clash is not None:
        raise ValidationError(
            f"Es existiert bereits ein aktives Konto namens {row['username']!r}."
        )
    conn.execute("UPDATE users SET deleted_at=NULL, updated_at=? WHERE id=?", (_now(), user_id))


def purge_user(conn: sqlite3.Connection, user_id: int) -> None:
    conn.execute("DELETE FROM users WHERE id=?", (user_id,))


def matrix_state(
    tree: PermissionTree, permissions: Iterable[str]
) -> dict[str, dict[str, bool]]:
    """Hilfsstruktur fuer das Rendering der Rechtematrix."""
    granted = set(permissions)
    state: dict[str, dict[str, bool]] = {}
    for area in tree.areas:
        state[area.key] = {"view": area.view_key in granted}
        for resource in area.resources:
            state[resource.key] = {
                action.value: resource.permission(action) in granted for action in resource.actions
            }
    return state


def summarize_permissions(tree: PermissionTree, permissions: Iterable[str]) -> list[str]:
    """Kurzfassung fuer Listenansichten: ``Bereich: Ressource (RCUD)``."""
    granted = set(permissions)
    lines: list[str] = []
    for area in tree.areas:
        if area.view_key not in granted:
            continue
        parts: list[str] = []
        for resource in area.resources:
            letters = "".join(
                ACTION_LETTERS[action]
                for action in resource.actions
                if resource.permission(action) in granted
            )
            if letters:
                parts.append(f"{resource.label} ({letters})")
        if parts:
            lines.append(f"{area.label}: " + ", ".join(parts))
    return lines


ACTION_LETTERS: dict[Action, str] = {
    Action.VIEW: "R",
    Action.CREATE: "C",
    Action.EDIT: "U",
    Action.DELETE: "D",
    Action.EXECUTE: "X",
    Action.EXPORT: "E",
    Action.SECRETS: "S",
}
