<?php

use App\Support\HostName;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Symfony\Component\Console\Output\ConsoleOutput;

/**
 * CluPilot vergibt die Hostnamen, nicht der Betreiber.
 *
 * Vorher führte diese Installation zwei Namen für dieselbe Maschine: den
 * getippten in `name` (den die Konsole zeigte) und den systematischen in
 * `dns_name` (den das DNS führte). Nur der zweite löste auf, und er stand in
 * keiner Ansicht.
 *
 * Der systematische gewinnt und wird DER Name. `pve-fns-1` heißt danach
 * `fsn-01` — die ZEILE wird umbenannt, die Maschine nicht: ein Proxmox-Node
 * lässt sich nachträglich nur mühsam umbenennen, und dass er anders heißt, ist
 * auf der Detailseite als eigenes Feld sichtbar.
 *
 * Der Zähler zieht von `app_settings` an die Rechenzentrums-Zeile um. Er darf
 * nicht aus den vorhandenen Hosts abgeleitet werden: `MAX(nummer) + 1` fällt
 * zurück, sobald ein Host entfernt wird, und die nächste Maschine bekäme den
 * Namen, der noch in Protokollen, Sicherungen und DNS-Zwischenspeichern steht.
 */
return new class extends Migration
{
    public function up(): void
    {
        // EINMAL geprüft, für den ganzen Lauf festgehalten. `dns_name` wird
        // weiter unten an drei Stellen angefasst — gelesen (Vorabprüfung),
        // geschrieben (Übertragung) und gelöscht (Spalten-Block) — und alle
        // drei müssen auf demselben Stand urteilen. Ein zweiter Anlauf nach
        // einem Fehlschlag, der die Spalte schon fallen ließ (der
        // Spalten-Block unten lief durch, aber `unique('name')` scheiterte
        // erst danach), darf nicht zwei der drei Stellen noch für vorhanden
        // halten, während die dritte sie schon vermisst.
        $hadDnsName = Schema::hasColumn('hosts', 'dns_name');

        // Vorabprüfung, bevor irgendetwas angefasst wird — auch vor der
        // Spalte weiter unten, die selbst schon DDL ist. `hosts.name` trug
        // noch nie einen eindeutigen Index (siehe create_hosts_table): ein
        // Host, der die DNS-Registrierung nie erreicht hat, behält seinen
        // getippten Namen, und ein zweiter Anlauf nach einem gescheiterten
        // Anlegen ist genau die Lage, in der derselbe Name zweimal getippt
        // wird. Bricht `unique('name')` weiter unten deswegen ab, ist es zu
        // spät: MariaDB committet DDL sofort und implizit, egal ob die
        // Migration insgesamt als gelaufen gilt. Ohne Eintrag in der
        // Migrationstabelle ruft niemand `down()` auf, und ein zweiter
        // `up()`-Versuch stirbt sofort an der Spalte, die im ersten
        // (fehlgeschlagenen) Anlauf schon angelegt wurde — Rettung dann nur
        // noch von Hand per SQL.
        //
        // Deshalb wird hier SIMULIERT, was die Übertragung gleich schreiben
        // würde (`dns_name`, wo gesetzt, sonst der unveränderte `name`),
        // statt real zu schreiben und erst danach nachzusehen. Das deckt
        // beide Quellen von Doppelten ab — zwei Hosts, die schon denselben
        // getippten Namen tragen, UND ein getippter Name, der zufällig einem
        // fremden `dns_name` gleicht — und lässt die Datenbank unangetastet,
        // wenn es einen Treffer gibt: eine reine SELECT-Prüfung vor der
        // ersten Schreiboperation, beliebig oft wiederholbar.
        //
        // GROUP BY/HAVING statt `pluck()->countBy()`: Letzteres vergleicht in
        // PHP byteweise, die Spalte aber liegt auf `utf8mb4_unicode_ci` —
        // unempfindlich gegen Groß-/Kleinschreibung und Akzente. `Pve-Fsn-1`
        // und `pve-fsn-1` kämen an der PHP-Prüfung vorbei und kollidierten
        // erst am `unique('name')`-Index weiter unten — ausgerechnet der Fall,
        // den diese Prüfung abfangen soll. GROUP BY lässt dieselbe Kollation
        // entscheiden, die später den Index baut, statt in PHP etwas
        // nachzubilden, das von der Spalte abdriften kann.
        //
        // Und auf `$hadDnsName` verzweigt: ist die Spalte schon gefallen (ein
        // zweiter Anlauf), gibt es den CASE-Ausdruck oben nicht mehr zu
        // bilden — `name` trägt dann bereits den vollständigen
        // Übertragungsstand, und die Prüfung läuft direkt auf ihr.
        $duplicatesQuery = DB::table('hosts');

        if ($hadDnsName) {
            $duplicatesQuery->selectRaw("(CASE WHEN dns_name IS NOT NULL AND dns_name <> '' THEN dns_name ELSE name END) as future_name");
        } else {
            $duplicatesQuery->select('name as future_name');
        }

        $duplicates = $duplicatesQuery
            ->groupBy('future_name')
            ->havingRaw('COUNT(*) > 1')
            ->pluck('future_name');

        if ($duplicates->isNotEmpty()) {
            throw new RuntimeException(
                'Mehrere Hosts würden nach dieser Migration denselben Namen tragen: '
                .$duplicates->implode(', ').'. Nichts wurde verändert — '
                .'die betroffenen Hosts von Hand umbenennen oder entfernen und die Migration erneut ausführen.'
            );
        }

        // Hinter `hasColumn`, damit ein zweiter Anlauf nach einem Fehlschlag
        // WEITER UNTEN (am Riegel, oder an einem `dns_name`-Index, den eine
        // Rücksicherung nicht mitbrachte) nicht sofort an „Duplicate column
        // name 'next_host_number'" stirbt. MariaDB hat diese Spalte im ersten
        // Anlauf längst committet, ganz ohne Eintrag in der Migrationstabelle
        // — ein zweiter Versuch soll bis zum tatsächlichen Fehler durchlaufen,
        // nicht schon davor. Billig, und es hält die Zusage von oben ein:
        // beliebig oft wiederholbar.
        if (! Schema::hasColumn('datacenters', 'next_host_number')) {
            Schema::table('datacenters', function (Blueprint $table) {
                $table->unsignedInteger('next_host_number')->default(1)->after('code');
            });
        }

        // Der systematische Name wird der Name. Hosts ohne DNS-Namen (noch im
        // Onboarding, nie so weit gekommen) behalten ihren — sie tragen keine
        // Nummer, die sich übertragen ließe, und das Weiterzählen in
        // HostName::free() geht an ihnen vorbei.
        //
        // Übersprungen, wenn `dns_name` laut `$hadDnsName` schon gefallen ist:
        // dann lief diese Zeile in einem früheren Anlauf schon durch, `name`
        // trägt den Übertragungsstand bereits, und ein erneuter Versuch
        // stürbe hier an „Unknown column 'dns_name'" — aus demselben Grund,
        // aus dem die Vorabprüfung oben verzweigt und der Spalten-Block
        // weiter unten bedingt läuft.
        if ($hadDnsName) {
            DB::table('hosts')
                ->whereNotNull('dns_name')
                ->where('dns_name', '<>', '')
                ->update(['name' => DB::raw('dns_name')]);
        }

        // Wer nach der Übertragung oben immer noch nicht wie `<rz>-<nn>`
        // aussieht, hatte keinen `dns_name` zum Übertragen — der getippte
        // Name blieb stehen. Auf dieser Installation ist das kein
        // theoretischer Fall: `pve-fsn-1` und `pve-hel-1` tragen genau diesen
        // Namen, unverändert, und die Host-Detailseite zeigt seitdem einen
        // Link auf `pve-fsn-1.node.<zone>` — einen Namen, den nie jemand ins
        // DNS geschrieben hat.
        //
        // Kein Abbruch: diese Hosts laufen und sollen weiterlaufen, das
        // Namensfeld ist nur beim Anlegen entfallen, nicht rückwirkend
        // Pflicht geworden. Aber still bleiben darf das nicht — eine
        // Migration läuft im Container, ihr Protokoll liest niemand von
        // selbst nach, deshalb zusätzlich auf die Konsole, wo ein Betreiber
        // `artisan migrate` tatsächlich ansieht.
        $unrepaired = DB::table('hosts')->get()->filter(function ($host) {
            $label = HostName::label($host->datacenter);

            return preg_match('/^'.preg_quote($label, '/').'-\d{2,}$/', (string) $host->name) !== 1;
        });

        if ($unrepaired->isNotEmpty()) {
            $message = sprintf(
                'clupilot_vergibt_die_hostnamen: %d Host(s) tragen nach dieser Migration weiterhin einen '
                .'getippten statt eines systematischen Namens (kein dns_name zum Übertragen vorhanden): %s. '
                .'Sie laufen unverändert weiter — aber jede Adresse, die aus dem Namen gebildet wird '
                .'(Detailseite, DNS, /etc/hosts), zeigt auf einen Namen, den niemand ins DNS geschrieben hat. '
                .'Von Hand umbenennen: Zeile auf ein systematisches <rz>-<nn> setzen, next_host_number '
                .'entsprechend hochziehen, DNS und Proxmox-Node nachziehen.',
                $unrepaired->count(),
                $unrepaired->pluck('name')->implode(', '),
            );

            Log::warning($message);
            (new ConsoleOutput)->writeln('<comment>'.$message.'</comment>');
        }

        foreach (DB::table('datacenters')->get() as $dc) {
            $label = HostName::label($dc->code);

            // Was schon vergeben ist — der Boden, unter den der Zähler nie darf.
            $inUse = DB::table('hosts')
                ->where('name', 'like', $label.'-%')
                ->pluck('name')
                ->map(fn (string $name) => (int) substr($name, strrpos($name, '-') + 1))
                ->max() ?? 0;

            // Und was der alte Zähler schon ausgegeben HATTE. Ohne diesen Wert
            // ginge die Zusage „niemals wiederverwendet" beim Umzug verloren:
            // ein Host, der angelegt und wieder entfernt wurde, steht in keiner
            // Zeile mehr, aber sein Name steht noch in den Protokollen. Die
            // Zeile steht hier noch, weil `app_settings` erst ganz am Ende
            // geleert wird (siehe dort) — an dieser Stelle ist sie also
            // garantiert noch da.
            $carried = (int) (json_decode(
                (string) DB::table('app_settings')->where('key', 'dns.sequence.'.$label)->value('value'),
                true,
            ) ?? 0);

            DB::table('datacenters')->where('id', $dc->id)
                ->update(['next_host_number' => max($inUse, $carried) + 1]);
        }

        // Getrennte Aufrufe: Index löschen, Spalte löschen und Index anlegen in
        // einem Blueprint bringt SQLite (Testlauf) durcheinander.
        //
        // Hinter demselben `$hadDnsName` wie Vorabprüfung und Übertragung
        // oben — nicht mehr hinter einer eigenen Schema::hasColumn-Abfrage,
        // damit alle drei Stellen garantiert dasselbe Urteil fällen. Eine
        // frühere Fassung dieses Kommentars behauptete, ein Fehlschlag am
        // Riegel weiter unten ließe `dns_name` hier noch stehen — das war
        // verkehrt herum: `dropColumn` läuft VOR dem Riegel, nicht danach.
        // Scheitert `unique('name')`, ist `dns_name` zu diesem Zeitpunkt
        // längst weg, und ein zweiter Anlauf, der das nicht wüsste, stürbe an
        // der Vorabprüfung oben statt hier. Mit `$hadDnsName` an einer
        // einzigen Stelle für den ganzen Lauf entschieden, ist diese
        // Reihenfolge egal geworden: Vorabprüfung, Übertragung und dieser
        // Block überspringen ihren `dns_name`-Anteil gemeinsam, sobald die
        // Spalte einmal gefallen ist — unabhängig davon, WESHALB der Anlauf,
        // der sie fallen ließ, danach noch gescheitert ist.
        if ($hadDnsName) {
            Schema::table('hosts', function (Blueprint $table) {
                $table->dropUnique(['dns_name']);
            });

            Schema::table('hosts', function (Blueprint $table) {
                $table->dropColumn('dns_name');
            });
        }

        // Der Riegel — dank der Vorabprüfung oben ohne Überraschung, aber
        // trotzdem der riskanteste Schritt dieser Migration: der einzige, der
        // an vorhandenen DATEN scheitern kann statt nur am Schema. Alles, was
        // danach noch aussteht (die Zeile unten), ist reine Aufräumarbeit an
        // etwas, das nichts mehr referenziert — sie steht deshalb ZULETZT.
        //
        // HIER, nicht mehr am `dns_name`-Block, endet die Wiederholbarkeit
        // von oben: Vorabprüfung, Übertragung und Spalten-Block sind jetzt
        // alle an `$hadDnsName` gebunden und damit beliebig oft wiederholbar.
        // Gelingt aber ausgerechnet dieser Aufruf, und scheitert erst die
        // reine DELETE-Zeile gleich danach (kein DDL mehr, aber ein
        // Verbindungsabbruch oder ein Sperr-Timeout bleiben möglich), findet
        // ein erneuter Anlauf den Index schon vor und stirbt an „Duplicate
        // key name 'hosts_name_unique'" statt einfach die DELETE-Zeile zu
        // wiederholen. Unbehandelt gelassen: den Riegel selbst gegen sich
        // selbst abzusichern ist nicht mehr die billige Ergänzung, die dieser
        // Fix war, sondern eine eigene Prüfrunde.
        Schema::table('hosts', function (Blueprint $table) {
            $table->unique('name');
        });

        // Der alte Zähler geht mit. Eine tote Einstellung, die noch wie eine
        // Quelle aussieht, ist genau das Problem, das diese Migration behebt.
        // Ganz am Ende, NACH dem Unique-Index: das ist der letzte wirklich
        // zerstörerische Schritt hier, und er gehört hinter jeden Schritt, der
        // noch scheitern könnte — nicht davor.
        DB::table('app_settings')->where('key', 'like', 'dns.sequence.%')->delete();
    }

    public function down(): void
    {
        Schema::table('hosts', function (Blueprint $table) {
            $table->dropUnique(['name']);
        });

        Schema::table('hosts', function (Blueprint $table) {
            $table->string('dns_name')->nullable()->unique()->after('name');
        });

        // Zurück in die zwei Namen: beide tragen ab hier denselben Wert. Die
        // getippten Namen von vorher sind fort — sie waren der Fehler.
        DB::table('hosts')->update(['dns_name' => DB::raw('name')]);

        foreach (DB::table('datacenters')->get() as $dc) {
            $key = 'dns.sequence.'.HostName::label($dc->code);

            DB::table('app_settings')->updateOrInsert(
                ['key' => $key],
                [
                    'value' => json_encode(max(0, (int) $dc->next_host_number - 1)),
                    'created_at' => now(),
                    'updated_at' => now(),
                ],
            );

            // App\Support\Settings hält jeden gelesenen Wert unbegrenzt im
            // Cache und leert ihn nur bei seinem eigenen set(). Ohne diese
            // Zeile schriebe die Zeile darüber die Datenbank richtig zurück,
            // während Settings::get('dns.sequence.'.$label) — läse den
            // jemand vor diesem Rückbau schon einmal — weiter den alten Wert
            // aus dem Cache liefert.
            Cache::forget('app_setting:'.$key);
        }

        Schema::table('datacenters', function (Blueprint $table) {
            $table->dropColumn('next_host_number');
        });
    }
};
