<?php

namespace App\Services\Nextcloud;

use App\Models\Instance;
use App\Models\Seat;
use App\Services\Proxmox\ProxmoxClient;
use App\Support\NextcloudOcc;
use Illuminate\Support\Facades\Log;
use RuntimeException;
use Throwable;

/**
 * Der Griff, mit dem ein Sitz in der Nextcloud eines Kunden wirksam wird.
 *
 * Nur Griffe: anlegen, einladen, Gruppe setzen, sperren, freigeben. WER wann
 * welchen zieht, steht in SyncSeatToNextcloud — dieselbe Trennung wie bei
 * HostFirewall und BlockAddress.
 *
 * Keine Methode wirft. Ein nicht erreichbarer Gast gibt `false` zurück, und
 * der Auftrag schreibt das an den Sitz, wo der Inhaber es liest. Eine
 * Ausnahme würde stattdessen den Bereitstellungs-Arbeiter mitreissen, auf dem
 * die bezahlte Kundenbereitstellung läuft.
 *
 * Jeder Benutzername geht vor dem Einsetzen durch `isWellFormed()`. Das ist
 * kein doppelter Boden für einen ohnehin sauberen Aufrufer, sondern die
 * Bedingung dafür, dass dieser Dienst eine Shell im Gast füttern darf.
 */
class NextcloudUsers
{
    public function __construct(private ProxmoxClient $pve) {}

    /** Anlegen falls nötig, danach die Willkommensmail — in einem Zug. */
    public function invite(Instance $instance, Seat $seat): bool
    {
        $user = (string) $seat->nc_username;

        if (! $this->isWellFormed($user, $seat)) {
            return false;
        }

        return $this->run($instance, function ($pve, $node, $vmid) use ($user, $seat) {
            $vorhanden = (int) ($pve->guestExec(
                $node, $vmid, NextcloudOcc::command('user:info '.escapeshellarg($user))
            )['exitcode'] ?? 1) === 0;

            // Wiederholbar nach einem Absturz: ein zweiter Lauf legt keinen
            // zweiten Benutzer an, sondern schickt die Willkommensmail erneut.
            // Genau wie CreateCustomerAdmin es tut.
            return $vorhanden
                ? ['user:welcome --reset-password '.escapeshellarg($user)]
                : [
                    'user:add --generate-password'
                        .' --email='.escapeshellarg((string) $seat->email)
                        .' --display-name='.escapeshellarg((string) ($seat->name ?: $seat->email))
                        .' --group='.escapeshellarg(Seat::GROUPS[$seat->role] ?? 'mitarbeiter')
                        .' '.escapeshellarg($user),
                ];
        });
    }

    /** Gruppe setzen — und bei readonly der Speicherplatz. */
    public function applyRole(Instance $instance, Seat $seat): bool
    {
        $user = (string) $seat->nc_username;

        if (! $this->isWellFormed($user, $seat)) {
            return false;
        }

        $ziel = Seat::GROUPS[$seat->role] ?? 'mitarbeiter';

        return $this->run($instance, function ($pve, $node, $vmid) use ($user, $seat, $ziel) {
            $befehle = [];

            // Aus jeder anderen bekannten Gruppe heraus, in die eine hinein.
            //
            // `2>/dev/null || true` hinter dem Entfernen ist kein Wegsehen,
            // sondern die richtige Bedeutung — dieselbe Begründung wie bei
            // HostFirewall::releaseMany(): jemanden aus einer Gruppe zu
            // nehmen, in der er nicht ist, ist kein Fehler, sondern der
            // gewünschte Endzustand.
            //
            // Und ohne das Netz war es der HAEUFIGSTE Fall, nicht ein
            // Randfall: `mitarbeiter` und `nur-lesen` legt niemand an. Sie
            // entstehen erst, wenn `user:add --group=…` sie zum ersten Mal
            // braucht — `group:removeuser` legt nichts an und beendet mit
            // Fehlercode, wenn die Gruppe fehlt. Auf einer frischen Instanz
            // scheiterte damit die erste Rollenänderung dauerhaft, weil
            // run() alle Exitcodes verundet und Wiederholen wieder `role`
            // wählt.
            //
            // Nur fürs Entfernen. Ein gescheitertes `group:adduser` bleibt
            // ein Fehlschlag: wer in keiner Gruppe landet, sieht in seiner
            // neuen Cloud nichts.
            //
            // Genauer als es oben klingt: `&&` und `||` sind in der Shell
            // linksassoziativ, und NextcloudOcc::command() setzt VOR den
            // occ-Aufruf ein eigenes `cd … &&` (siehe dort). Das fertige
            // Kommando ist damit EIN einziger Ausdruck von diesem `cd` bis
            // zum `|| true` am Ende, nicht zwei getrennte. Das Netz fängt
            // damit auch ein gescheitertes `cd` auf, nicht nur die fehlende
            // Gruppe.
            //
            // Folgenlos: `group:adduser` und die Speicherplatzzeile laufen
            // in derselben Runde OHNE eigenes Netz und ziehen den Lauf beim
            // selben `cd`-Fehlschlag ohnehin auf Fehlschlag (run() verundet
            // alle Exitcodes). Dieses Netz rettet also nichts, was den Lauf
            // sonst überstünde — es beschreibt nur mehr, als sein Kommentar
            // eben behauptet.
            foreach (array_unique(array_values(Seat::GROUPS)) as $gruppe) {
                if ($gruppe !== $ziel) {
                    $befehle[] = 'group:removeuser '.escapeshellarg($gruppe).' '
                        .escapeshellarg($user).' 2>/dev/null || true';
                }
            }

            $befehle[] = 'group:adduser '.escapeshellarg($ziel).' '.escapeshellarg($user);

            // Der Speicherplatz. Siehe ApplyStorageQuota: ein Konto mit
            // EIGENEM Wert folgt der Vorgabe der Instanz nicht mehr. Für
            // readonly ist genau das gewollt; beim VERLASSEN der Rolle muss
            // der eigene Wert deshalb WEG, nicht überschrieben werden.
            $befehle[] = $seat->isReadonly()
                ? 'user:setting '.escapeshellarg($user).' files quota '.escapeshellarg('0 B')
                : 'user:setting '.escapeshellarg($user).' files quota --delete';

            return $befehle;
        });
    }

    public function disable(Instance $instance, Seat $seat): bool
    {
        $user = (string) $seat->nc_username;

        if (! $this->isWellFormed($user, $seat)) {
            return false;
        }

        return $this->run($instance, fn ($pve, $node, $vmid) => [
            'user:disable '.escapeshellarg($user),
            // user:disable allein lässt laufende Sitzungen bis zu fünf
            // Minuten weiterleben. Bei jemandem, der gerade gegangen ist,
            // sind fünf Minuten fünf zu viel.
            'user:auth-tokens:delete '.escapeshellarg($user),
        ]);
    }

    public function enable(Instance $instance, Seat $seat): bool
    {
        $user = (string) $seat->nc_username;

        if (! $this->isWellFormed($user, $seat)) {
            return false;
        }

        return $this->run($instance, fn ($pve, $node, $vmid) => ['user:enable '.escapeshellarg($user)]);
    }

    /**
     * Nextcloud lässt Buchstaben, Ziffern und `-_.@` in Kennungen zu. Alles
     * andere ist entweder ein Fehler weiter oben oder ein Versuch — beides
     * will man sehen, und keines darf in eine Shell.
     */
    private function isWellFormed(string $user, Seat $seat): bool
    {
        if ($user !== '' && preg_match('/^[A-Za-z0-9._@-]+$/', $user) === 1) {
            return true;
        }

        report(new RuntimeException(
            "NextcloudUsers: abgewiesene Kennung fuer Sitz [{$seat->uuid}] — nichts ausgefuehrt."
        ));

        return false;
    }

    /**
     * Der Verbindungsaufbau steht EINMAL hier, nicht in jeder Methode. Der
     * Rückruf bekommt den fertigen Client mit — er braucht ihn, weil `invite()`
     * erst nachsehen muss, ob es den Benutzer schon gibt, bevor es entscheidet,
     * welchen Befehl es baut.
     *
     * @param  callable(ProxmoxClient, string, int): array<int, string>  $bauen
     */
    private function run(Instance $instance, callable $bauen): bool
    {
        if ($instance->host === null || blank($instance->vmid)) {
            return false;
        }

        $node = $instance->host->node ?? 'pve';
        $vmid = (int) $instance->vmid;

        try {
            $pve = $this->pve->forHost($instance->host);
            $ok = true;

            foreach ($bauen($pve, $node, $vmid) as $argumente) {
                $ergebnis = $pve->guestExec($node, $vmid, NextcloudOcc::command($argumente));
                $ok = ((int) ($ergebnis['exitcode'] ?? 1) === 0) && $ok;
            }

            return $ok;
        } catch (Throwable $e) {
            // Ein abgeschalteter Gast wirft, statt einen Fehlercode zu liefern.
            // Nie mit Zugangsdaten, nie mit Stacktrace an den Kunden.
            Log::warning('nextcloud user command failed', [
                'instance' => $instance->uuid, 'error' => $e->getMessage(),
            ]);

            return false;
        }
    }
}
