<?php

namespace App\Services\Terminal;

use Illuminate\Support\Facades\Redis;

/**
 * Wo eine Eintrittskarte liegt, wie lange, und warum genau so.
 *
 * Herausgezogen, als ein zweiter Ausstellungsweg dazukam (das Terminal zum
 * CluPilot-Server selbst). Was hier steht, ist die Verabredung mit einem
 * Container, der dieses Repo nicht kennt — sie zweimal hinzuschreiben hieße,
 * darauf zu wetten, dass beide Abschriften gemeinsam altern. Wer die
 * Ausstellung ändert, ändert sie hier einmal für beide.
 *
 * ROHES REDIS, NICHT DIE Cache-FASSADE — zwei Gründe, beide gemessen:
 *
 * 1. `Cache::put()` läuft über `Illuminate\Cache\RedisStore`, und die
 *    serialisiert jeden Wert mit PHP `serialize()`, solange kein `serializer`
 *    in `config/database.php` gesetzt ist (hier: keiner). Aus dem JSON unten
 *    würde in Redis ein `s:412:"{...}";` — eine PHP-Hülle, die der
 *    Python-Container nicht kennt und nicht raten kann.
 * 2. `Cache::pull()` ist `get()` dann `forget()`, zwei getrennte Runden. Zwei
 *    gleichzeitige Einlösungen bekämen beide den Root-Schlüssel, bevor die
 *    zweite merkt, dass er weg ist. Redis' `GETDEL` ist eine einzige, atomare
 *    Runde: die zweite Einlösung sieht den fehlenden Schlüssel, statt ihn noch
 *    zu bekommen.
 *
 * DER SCHLÜSSEL, VOLLSTÄNDIG — für den Container, der dagegen schreibt:
 * `terminal:ticket:<64 Hex-Zeichen>`, auf der `cache`-Redis-Verbindung
 * (`config('database.redis.cache')`, standardmäßig Datenbank 1). phpredis legt
 * darüber transparent noch `REDIS_PREFIX`
 * (`config('database.redis.options.prefix')`, auf dieser Installation
 * `clupilot-database-`, aus `APP_NAME` abgeleitet) — unsichtbar für jeden
 * PHP-Aufruf über diese Verbindung, aber Teil des tatsächlichen Schlüssels für
 * jeden Client, der nicht über phpredis mit derselben Option spricht. Der
 * Container muss also denselben Wert voranstellen; er kann ihn aus dieser
 * Klasse allein nicht erraten, deshalb steht er hier.
 */
final class TicketStore
{
    /**
     * Dreißig Sekunden, weil ein Ticket nur den Weg vom Klick zum offenen
     * Fenster überbrücken muss. Wer die Seite offen liegen lässt und später neu
     * lädt, bekommt ein frisches.
     */
    public const TTL_SECONDS = 30;

    private const PREFIX = 'terminal:ticket:';

    /**
     * Legt eine Sitzung ab und gibt den Schlüssel zurück, den der Browser
     * bekommt: zweiunddreißig Byte Zufall, sonst nichts.
     *
     * Alles, was die Brücke wirklich braucht — Adresse, Benutzer, privater
     * Schlüssel, gepinnter Fingerabdruck — liegt serverseitig daneben und wird
     * vom Container gelesen, nie vom Browser mitgegeben. Ein Ticket, das die
     * Verbindungsdaten selbst trüge, stünde in der Adresszeile, im Verlauf und
     * in jedem Protokoll dazwischen.
     *
     * @param  array<string, mixed>  $payload
     */
    public static function issue(array $payload): string
    {
        $ticket = bin2hex(random_bytes(32));

        Redis::connection('cache')->setex(
            self::PREFIX.$ticket,
            self::TTL_SECONDS,
            json_encode($payload, JSON_THROW_ON_ERROR),
        );

        return $ticket;
    }

    /**
     * Liest das Ticket und löscht es im selben Zug — mit Redis' eigenem
     * `GETDEL`, nicht mit zwei Aufrufen. Erst das macht „genau eine Einlösung"
     * zu einer Zusage, die auch unter zwei gleichzeitigen Versuchen hält.
     *
     * @return array<string, mixed>|null
     */
    public static function redeem(string $ticket): ?array
    {
        $raw = Redis::connection('cache')->getdel(self::PREFIX.$ticket);

        // phpredis meldet ein fehlendes/abgelaufenes/schon geholtes Ticket als
        // `false`, nicht als `null` — die Cache-Fassade glättete das vorher.
        return $raw === false || $raw === null
            ? null
            : json_decode((string) $raw, true, flags: JSON_THROW_ON_ERROR);
    }
}
