<?php

namespace App\Livewire\Admin;

use App\Models\Operator;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;

/**
 * Wer was darf — als Matrix, nicht als Migration.
 *
 * Bis hierher standen die Rechte einer Rolle in Migrationen und die Liste der
 * zulässigen Rollennamen an DREI Stellen im Code. Das ist kein Schönheitsfehler
 * gewesen: `Developer` existierte als Rolle und wurde vom Einladen-Formular nie
 * angeboten, weil eine der drei Listen nicht mitgezogen worden war.
 *
 * Deshalb kommt hier alles aus der Datenbank. Eine Rolle, die es gibt, steht
 * auf diesem Bildschirm — und was auf diesem Bildschirm steht, gilt.
 *
 * ZWEI Grenzen, beide mit Grund:
 *
 * - **Owner ist unantastbar.** Ein Klick, der dem Owner Rechte nimmt, sperrt
 *   den Betreiber aus seiner eigenen Installation aus, und es gibt niemanden
 *   mehr, der es zurücknehmen könnte.
 * - **Alles läuft auf dem `operator`-Guard.** Genau diese Verwechslung hat
 *   `dpa.manage` zusätzlich auf `web` hinterlassen, wo es kein Betreiber je
 *   sieht — Operator löst gegen `operator` auf (siehe dessen `$guard_name`).
 */
#[Layout('layouts.admin')]
class Roles extends Component
{
    public const GUARD = 'operator';

    /** @var array<int, array<int, string>> Rollen-ID => angehakte Berechtigungen */
    public array $granted = [];

    public string $newRole = '';

    public function mount(): void
    {
        abort_unless(Gate::allows('staff.manage'), 403);

        $this->loadGrants();
    }

    public function save(int $roleId): void
    {
        $this->authorize('staff.manage');

        $role = $this->role($roleId);

        if ($role->name === 'Owner') {
            $this->addError('granted.'.$roleId, __('roles.owner_locked'));

            return;
        }

        // Nur bekannte Namen: die Liste kommt aus dem Browser.
        $wanted = array_values(array_intersect(
            $this->granted[$roleId] ?? [],
            $this->allPermissions(),
        ));

        $role->syncPermissions($wanted);

        // Spatie hält die Rechte in einem Zwischenspeicher. Ohne das hier
        // gölte die Änderung erst, wenn der Zwischenspeicher von selbst
        // abläuft — und die Seite zeigte währenddessen das Neue an, während
        // das Alte wirkt.
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $this->loadGrants();
        $this->dispatch('notify', message: __('roles.saved', ['role' => $role->name]));
    }

    public function create(): void
    {
        $this->authorize('staff.manage');

        $this->validate(['newRole' => ['required', 'string', 'max:64', 'regex:/^[\pL][\pL\pN \-]*$/u']]);

        Role::findOrCreate(trim($this->newRole), self::GUARD);
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $this->newRole = '';
        $this->loadGrants();
        $this->dispatch('notify', message: __('roles.created'));
    }

    public function delete(int $roleId): void
    {
        $this->authorize('staff.manage');

        $role = $this->role($roleId);

        if ($role->name === 'Owner') {
            $this->addError('newRole', __('roles.owner_locked'));

            return;
        }

        // Eine Rolle zu löschen, die jemand hält, nähme diesem Menschen
        // wortlos jeden Zugang — er stünde beim nächsten Anmelden ohne Rolle
        // da und käme in die Konsole nicht mehr hinein.
        if ($role->users()->exists()) {
            $this->addError('newRole', __('roles.in_use', ['role' => $role->name]));

            return;
        }

        $role->delete();
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $this->loadGrants();
        $this->dispatch('notify', message: __('roles.deleted'));
    }

    private function role(int $roleId): Role
    {
        return Role::query()
            ->where('guard_name', self::GUARD)
            ->findOr($roleId, fn () => abort(404));
    }

    /** @return array<int, string> */
    private function allPermissions(): array
    {
        return Permission::query()
            ->where('guard_name', self::GUARD)
            ->orderBy('name')
            ->pluck('name')
            ->all();
    }

    private function loadGrants(): void
    {
        $this->granted = Role::query()
            ->where('guard_name', self::GUARD)
            ->with('permissions')
            ->get()
            ->mapWithKeys(fn (Role $role) => [$role->id => $role->permissions->pluck('name')->all()])
            ->all();
    }

    public function render()
    {
        return view('livewire.admin.roles', [
            'roles' => Role::query()
                ->where('guard_name', self::GUARD)
                ->withCount('users')
                ->orderBy('name')
                ->get(),
            // Nach dem Präfix gruppiert — die Namen tragen ihre Ordnung längst
            // in sich (`billing.`, `customers.`, `provisioning.`), und
            // einundzwanzig Kästchen flach untereinander sind eine Wand, in
            // der niemand das eine findet, das er sucht.
            'groups' => collect($this->allPermissions())
                ->groupBy(fn (string $name) => str_contains($name, '.') ? strtok($name, '.') : 'sonstiges')
                ->sortKeys(),
            'permissions' => $this->allPermissions(),
            // Der ganze `can`-Block als Array, nicht Eintrag für Eintrag über
            // `__()`. Ein Berechtigungsname trägt selbst einen Punkt
            // (`billing.manage`), und `__()` liest Punkte als Verschachtelung —
            // der Aufruf suchte `roles → can → billing → manage`, während in
            // der Sprachdatei ein FLACHER Schlüssel `'billing.manage'` steht.
            // Die Seite zeigte deshalb den Schlüssel statt der Beschreibung.
            'permissionLabels' => (array) trans('roles.can'),
            // Wie viele Menschen an dieser Rolle hängen — damit niemand blind
            // etwas wegnimmt, das gerade sechs Leute benutzen.
            'protectedRole' => 'Owner',
            'ownRoles' => Operator::find(auth('operator')->id())?->roles->pluck('name')->all() ?? [],
        ]);
    }
}
