<?php

namespace App\Provisioning\Jobs;

use App\Models\Instance;
use App\Models\Seat;
use App\Services\Nextcloud\NextcloudUsers;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;

/**
 * Spiegelt einen Sitz in die Nextcloud des Kunden.
 *
 * Läuft auf der `provisioning`-Warteschlange, weil nur dieser Arbeiter den
 * Tunnel und die Proxmox-Zugangsdaten hat — das Portal hat weder das eine noch
 * das andere und darf es auch gar nicht versuchen.
 *
 * Daraus folgt: der Knopf wirkt nicht sofort. Deshalb schreibt dieser Auftrag
 * zurück, was WIRKLICH passiert ist (`nc_state`, `nc_error`) — sonst drückt
 * der Inhaber wieder und wieder, weil nichts sichtbar geschieht. Genau die
 * Beschwerde, die dieses Projekt schon zweimal hatte.
 *
 * `tries = 1`: ein blinder Wiederholungslauf gegen einen nicht erreichbaren
 * Gast bringt nichts, und der Inhaber hat einen Knopf dafür.
 */
class SyncSeatToNextcloud implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 1;

    public function __construct(
        public string $seatUuid,
        /** invite | role | disable | enable | restore */
        public string $action,
    ) {
        $this->onConnection('provisioning');
        $this->onQueue('provisioning');
    }

    /** Auch was an handle() vorbeifliegt, muss am Sitz sichtbar werden. */
    public function failed(?Throwable $e): void
    {
        Seat::query()->where('uuid', $this->seatUuid)->update([
            'nc_state' => Seat::STATE_FAILED,
            'nc_error' => 'unexpected',
        ]);
    }

    public function handle(NextcloudUsers $users): void
    {
        $seat = Seat::query()->with('customer')->where('uuid', $this->seatUuid)->first();

        if ($seat === null) {
            return;
        }

        // Erneut geprüft, nicht der Seite geglaubt: eine geschlossene Instanz
        // kann ihre VMID auf demselben Host weiterverliehen haben — dieselbe
        // Falle, die in IssueInstanceAdminAccess schon beschrieben steht.
        $instance = $seat->customer?->instances()
            ->whereIn('status', ['active', 'cancellation_scheduled'])
            ->latest('id')->first();

        if ($instance === null || $instance->host === null || blank($instance->vmid)) {
            $this->record($seat, false, 'no_instance');

            return;
        }

        /*
         * Die Restnaht des K1-Fixes (queueSync()'s Ausnahme bei `pending`,
         * Users.php): ein nachgeschicktes `disable` — dispatcht, WÄHREND ein
         * `invite`-Auftrag für denselben Sitz noch in der Warteschlange
         * steht — geht davon aus, dass DIESER `invite`-Auftrag das Konto bis
         * dahin angelegt hat.
         *
         * Scheitert der `invite`-Auftrag stattdessen schon oben an
         * `no_instance` (kein seltener Fall — siehe den retry()-Kommentar in
         * Users.php: eine Instanz, die beim Absenden noch nicht aktiv war,
         * ist ein dokumentierter Weg dorthin), entsteht in der Nextcloud gar
         * nichts. Wird die Instanz genau im Fenster zwischen den beiden
         * Läufen erreichbar, kommt das nachgeschickte `disable` trotzdem bis
         * hierher und schickt `user:disable` gegen einen Benutzer, den
         * niemand angelegt hat.
         *
         * Bewusst nicht gehärtet: occ scheitert daran folgenlos (Exitcode
         * ungleich 0, in der Nextcloud ändert sich nichts), und
         * `nc_synced_at` bleibt `null` — genau daran hängt der
         * Wiederholen-Knopf im Portal (dieselbe Bedingung an zwei Stellen:
         * Users::retry() und resources/views/livewire/users.blade.php). Er
         * bleibt an dieser Zeile verborgen, weil eine entzogene oder
         * gesperrte Zeile ohne `nc_synced_at` dort nichts zu wiederholen
         * hat; der Weg zurück bleibt die Vordertür (dieselbe Adresse über
         * das Anlegen-Formular). Ein eigener „wurde je angelegt"-Zwischenstand
         * nur für dieses enge Zeitfenster wäre mehr Feld für einen Randfall,
         * der sich am Ende von selbst schliesst.
         */
        $ok = match ($this->action) {
            'invite' => $this->invite($users, $instance, $seat),
            'role' => $users->applyRole($instance, $seat),
            'disable' => $users->disable($instance, $seat),
            'enable' => $users->enable($instance, $seat),
            // Der Wiederholen-Knopf an einer OFFENEN Zeile. Er weiss nicht,
            // welcher der beiden Befehle beim letzten Mal scheiterte — das
            // stünde nirgends am Sitz, und ein Feld dafür wäre eine
            // weitere Behauptung über die Cloud des Kunden, die irgendwann
            // nicht mehr stimmt. Beide zu schicken ist billiger und ehrlicher
            // als es zu raten; `user:enable` auf ein offenes Konto ist
            // folgenlos, und das Setzen der Rolle ist es ohnehin. Siehe
            // Users::retry().
            'restore' => $users->enable($instance, $seat) && $users->applyRole($instance, $seat),
            default => false,
        };

        $this->record($seat, $ok, $ok ? null : 'guest_failed');
    }

    /**
     * Einladen setzt in einem Zug auch die Gruppe: ein Benutzer, der angelegt
     * ist und in keiner Rolle steckt, sieht in seiner neuen Cloud nichts und
     * meldet sich am ersten Tag beim Inhaber.
     *
     * Das Anlegen wird dabei SOFORT vermerkt, noch bevor die Gruppe gesetzt
     * wird. `nc_synced_at` beantwortet die Frage „gibt es dieses Konto dort
     * überhaupt?", und ab dem geglückten `user:add` lautet die Antwort ja —
     * auch wenn der nächste Befehl scheitert. Die Seite entscheidet genau
     * daran, ob ein späteres Sperren etwas zu sperren hätte; ohne diese Zeile
     * bliebe der gefährlichste Fall offen: Konto angelegt, Rolle gescheitert,
     * Zugang entzogen — und nie eine Sperre losgeschickt.
     *
     * `nc_state` bleibt davon unberührt: der Versuch ist erst gelungen, wenn
     * auch die Rolle sitzt.
     */
    private function invite(NextcloudUsers $users, Instance $instance, Seat $seat): bool
    {
        // VOR dem Vermerk gelesen: war dieser Sitz schon einmal in der
        // Nextcloud, ist dies eine Wiederaufnahme — und sein Konto dort steht
        // gesperrt, weil `revoke()` ein `disable` geschickt hat. `user:welcome`
        // macht eine Sperre nicht rückgängig; ohne diese Zeile bekäme der
        // Wiederaufgenommene eine Willkommensmail zu einer Tür, die zu bleibt.
        // Bei einem frisch angelegten Konto entfällt der Befehl ganz, statt
        // sich auf ein folgenloses `user:enable` zu verlassen.
        $wiederaufnahme = $seat->nc_synced_at !== null;

        if (! $users->invite($instance, $seat)) {
            return false;
        }

        $seat->forceFill(['nc_synced_at' => now()])->save();

        /*
         * Zwischen Absenden und Ausführen kann sich die Absicht geändert
         * haben — dieselbe Begründung wie beim Vermerk oben.
         *
         * Der Arbeiter teilt sich die Warteschlange mit der bezahlten
         * Bereitstellung; das Fenster ist MINUTEN lang, nicht Millisekunden.
         * In dieser Zeit merkt ein Inhaber seinen Fehlgriff und entzieht. Ohne
         * diese Zeilen führe der Auftrag danach blind aus, was beim Absenden
         * galt: die Zeile zeichnete „Entfernt" und in der Cloud stände ein
         * aktives, freigeschaltetes Konto.
         *
         * Frisch aus der Datenbank, nicht am mitgeschleppten Modell: dieses
         * Objekt stammt vom Anfang des Auftrags und trägt den Status von
         * damals.
         */
        $gesperrtGewollt = in_array(
            Seat::query()->where('uuid', $seat->uuid)->value('status'),
            ['revoked', 'suspended'],
            true,
        );

        // Eine Tür aufzumachen, um sie im selben Auftrag wieder zuzuziehen,
        // wäre im besten Fall folgenlos und im schlechteren ein offenes
        // Zeitfenster.
        if ($wiederaufnahme && ! $gesperrtGewollt && ! $users->enable($instance, $seat)) {
            return false;
        }

        $rolleOk = $users->applyRole($instance, $seat);

        if (! $gesperrtGewollt) {
            return $rolleOk;
        }

        // Die Sperre geht auch dann noch hinaus, wenn die Rolle scheiterte:
        // an ihr hängt ein beendeter Zugang, und eine misslungene Gruppe darf
        // ihn nicht offen lassen. Gemeldet wird trotzdem beides — die Zeile
        // stimmt nur, wenn sie den Fehlschlag zeigt.
        return $users->disable($instance, $seat) && $rolleOk;
    }

    /**
     * `nc_synced_at` bleibt beim Fehlschlag stehen, statt geleert zu werden:
     * wann dieser Sitz zuletzt WIRKLICH stimmte, ist genau die Angabe, die
     * einen Fehlschlag einordnet — heute Morgen oder vor drei Wochen.
     */
    private function record(Seat $seat, bool $ok, ?string $grund): void
    {
        $seat->forceFill([
            'nc_state' => $ok ? Seat::STATE_SYNCED : Seat::STATE_FAILED,
            'nc_error' => $grund,
            'nc_synced_at' => $ok ? now() : $seat->nc_synced_at,
        ])->save();
    }
}
