<?php

namespace App\Support;

use App\Models\Datacenter;
use App\Models\Host;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Der Name eines Hosts — an dieser einen Stelle gebildet.
 *
 * Vorher gab es zwei: den getippten in `hosts.name`, den die Konsole zeigte,
 * und den aus Rechenzentrum und Nummer gebauten in `hosts.dns_name`, den das
 * DNS führte. Nur der zweite löste auf, und er stand in keiner einzigen
 * Ansicht. Der Betreiber rief den Namen auf, den er selbst vergeben hatte, und
 * stand vor einer Seite, die nicht lädt.
 *
 * Jetzt vergibt CluPilot ihn: `<rz>-<nn>`, fortlaufend je Rechenzentrum und
 * niemals wiederverwendet. Maschine, Proxmox-Node, DNS, /etc/hosts und Konsole
 * benutzen denselben.
 */
final class HostName
{
    /**
     * Der Name, den der nächste Host in diesem Rechenzentrum bekäme.
     *
     * LIEST den Zähler, verbraucht ihn nicht: die Anlegen-Seite zeigt den Namen,
     * bevor gespeichert wird. Zwei gleichzeitig geöffnete Formulare zeigen
     * deshalb beide dieselbe Nummer, und der zweite bekommt beim Speichern die
     * nächste. Eine Reservierung beim Öffnen wäre die schlechtere Antwort — ein
     * abgebrochenes Formular hinterließe eine Lücke im Zähler, die niemand
     * wieder auffüllt.
     */
    public static function preview(string $datacenterCode): string
    {
        $from = (int) (Datacenter::query()
            ->where('code', $datacenterCode)
            ->value('next_host_number') ?? 1);

        return self::free(self::label($datacenterCode), $from)[1];
    }

    /**
     * Vergibt den Namen und verbraucht die Nummer.
     *
     * Gehört in eine Transaktion: die Sperre auf die Rechenzentrums-Zeile hält
     * nur bis zu deren Ende, und ohne sie holten sich zwei gleichzeitige
     * Anlegen-Vorgänge dieselbe Nummer.
     */
    public static function claim(string $datacenterCode): string
    {
        if (DB::transactionLevel() === 0) {
            // Der Absatz oben VERLANGT eine Transaktion, erzwingt sie bisher
            // aber nicht: außerhalb einer Transaktion hält `lockForUpdate`
            // die Sperre nur bis zum Ende dieser einen Anfrage, nicht bis zum
            // `update()` weiter unten — zwei gleichzeitige Aufrufe holten
            // sich still dieselbe Nummer, ohne dass irgendwo ein Fehler
            // auftaucht. Lieber hier laut scheitern als dort leise falsch
            // vergeben.
            throw new RuntimeException('HostName::claim() muss innerhalb einer Transaktion aufgerufen werden.');
        }

        $dc = Datacenter::query()->where('code', $datacenterCode)->lockForUpdate()->first();

        if ($dc === null) {
            throw new RuntimeException("Kein Rechenzentrum mit dem Code {$datacenterCode}.");
        }

        [$number, $name] = self::free(self::label($datacenterCode), (int) $dc->next_host_number);

        $dc->update(['next_host_number' => $number + 1]);

        return $name;
    }

    /**
     * `fsn-03` → `fsn-03.node.clupilot.com`.
     *
     * Die PLATTFORM-Zone, nicht die Kundenzone: die zwei sind laut
     * OfficialDomains getrennt, und ein Host gehört auf die Seite der Plattform.
     */
    public static function fqdn(string $name): string
    {
        return $name.'.node.'.config('provisioning.dns.platform_zone');
    }

    /**
     * Rechenzentrums-Codes von vor der Verschärfung der Prüfregel können
     * Zeichen tragen, die eine DNS-Bezeichnung nicht darf. Ein Name, den der
     * Anbieter ablehnt, ließe den Host für immer namenlos.
     */
    public static function label(string $datacenterCode): string
    {
        $label = trim(preg_replace('/[^a-z0-9-]+/', '-', strtolower($datacenterCode)) ?? '', '-');

        return $label !== '' ? $label : 'node';
    }

    /**
     * Ab dieser Nummer aufwärts der erste freie Name.
     *
     * Der Zähler allein reicht nicht: zwei Alt-Codes (`eu_west` und `eu-west`)
     * werden zur selben Bezeichnung und führen doch getrennte Zähler. Ohne
     * dieses Weiterzählen bekäme der zweite `eu-west-01` und liefe in den
     * eindeutigen Index — ein Abbruch beim Anlegen statt eines Namens.
     *
     * Nach unten geht es dabei nie: eine entfernte `fsn-02` wird nicht wieder
     * vergeben, weil der Zähler längst darüber steht. Dieses Weiterzählen ist
     * der Riegel, nicht der Zähler — aber nur einfädig. `claim()` sperrt die
     * ZEILE des Rechenzentrums, und `eu_west` und `eu-west` sind zwei
     * verschiedene Zeilen, die zur selben Bezeichnung normalisieren. Laufen
     * beide gleichzeitig durchs Onboarding, sperrt jeder Aufruf nur seine
     * eigene Zeile, beide sehen hier denselben Kandidaten frei, und wer den
     * `INSERT` als Zweiter absetzt, bekommt am `unique('name')`-Index eine
     * unbehandelte `QueryException` statt der nächsten Nummer.
     *
     * `%02d` füllt zweistellig auf und wächst ab hundert von selbst weiter.
     *
     * @return array{0: int, 1: string}
     */
    private static function free(string $label, int $from): array
    {
        $number = max(1, $from);

        while (Host::query()->where('name', sprintf('%s-%02d', $label, $number))->exists()) {
            $number++;
        }

        return [$number, sprintf('%s-%02d', $label, $number)];
    }
}
