<?php

namespace App\Support;

use App\Models\Host;

/**
 * Die eine Zeile, die der Betreiber im Rettungssystem einfügt.
 *
 * An einer Stelle gebaut, weil sie an zwei Stellen gezeigt wird (beim Anlegen
 * und beim Neuausstellen eines Codes) und weil jeder ihrer sieben Werte aus
 * einer anderen Ecke kommt. Zwei Fassungen davon würden auseinanderlaufen, und
 * der Unterschied fiele erst auf einem Server auf, der schon bestellt ist.
 *
 * Sie trägt ALLES, was das Skript vor dem Tunnel braucht (Spec §5): es holt
 * nichts ab, es gibt nichts abzurufen. Deshalb steht der private
 * WireGuard-Schlüssel darin — mit einer Lebensdauer von Minuten, denn Task 9
 * des Skripts tauscht ihn gegen einen auf der Maschine erzeugten.
 */
final class HostTakeoverCommand
{
    /** Wohin das Archiv ausgepackt wird. Das Skript sucht sich dort selbst. */
    public const INSTALL_DIR = '/opt/clupilot';

    /**
     * @param  array{code: string, private_key: string, public_key: string, wg_ip: string}  $enrolment
     */
    public static function for(Host $host, array $enrolment): string
    {
        return implode(' ', [
            'mkdir -p '.self::INSTALL_DIR,
            // Der Code steht ohnehin gleich noch einmal als `--code` in
            // derselben Zeile. Ein zweites Geheimnis für den Download wäre eines
            // mehr, das nie abläuft und für immer in Shell-Historien steht.
            "&& curl -fsSL '".self::archiveUrlWithCode($enrolment['code'])."'",
            '| tar xz -C '.self::INSTALL_DIR,
            '&& sh '.self::INSTALL_DIR.'/bootstrap/clupilot-bootstrap.sh',
            '--code '.$enrolment['code'],
            // Einfache Anführungszeichen: WireGuard-Schlüssel sind base64 und
            // enthalten +, / und =. Ohne sie zerlegt die Shell die Zeile an
            // einer Stelle, die der Betreiber nicht sieht.
            "--wg-private '".$enrolment['private_key']."'",
            '--wg-ip '.$enrolment['wg_ip'].'/'.self::subnetPrefix(),
            "--hub-pubkey '".config('provisioning.wireguard.hub_public_key')."'",
            '--hub-endpoint '.config('provisioning.wireguard.endpoint'),
            '--api http://'.config('provisioning.wireguard.hub_address'),
            '--fqdn '.self::fqdnFor($host),
        ]);
    }

    /**
     * Immer über den ÖFFENTLICHEN Hostnamen.
     *
     * Die Konsole läuft unter `admin.…`, aber diese Zeile wird auf einer
     * Maschine ausgeführt, die den Adminbereich nicht erreichen darf und soll —
     * er ist genau dafür abgeriegelt. `route()` von der Konsole aus lieferte den
     * Konsolen-Hostnamen, und die Zeile liefe in eine 404 auf einem Server, an
     * den man dann nur noch über die Anbieterkonsole kommt.
     */
    /** Die Adresse mit Ausweis — das, was wirklich in der Zeile steht. */
    public static function archiveUrlWithCode(string $code): string
    {
        return self::archiveUrl().'?code='.rawurlencode($code);
    }

    public static function archiveUrl(): string
    {
        // Der eigene Datei-Hostname, wenn es ihn gibt. Er trägt zweierlei mit
        // denselben Ansprüchen: die Rechtsdokumente, deren Adressen in
        // Verträgen stehen und Jahre halten müssen, und dieses Archiv, das ein
        // Server im Rettungssystem holt, bevor er sonst irgendetwas kann.
        $host = (string) config('admin_access.files_host');

        if ($host === '') {
            $host = (string) config('admin_access.app_host');
        }

        // APP_HOST ist auf den meisten Installationen NICHT gesetzt — leer heißt
        // dort „das Portal antwortet auf jedem Hostnamen", und das ist die
        // Vorgabe. Der Rückfall auf `url()` war deshalb falsch: aufgerufen wird
        // diese Methode aus der Konsole, also hätte `url()` den KONSOLEN-Namen
        // geliefert, und die Zeile liefe auf einer Maschine, die den
        // Adminbereich nicht erreichen darf, in eine 404.
        //
        // Also aus APP_URL, denn das ist die Adresse der Plattform und auf jeder
        // Installation gesetzt.
        if ($host === '') {
            $host = (string) (parse_url((string) config('app.url'), PHP_URL_HOST) ?: '');
        }

        return $host === ''
            ? url('/bootstrap.tar.gz')
            : 'https://'.$host.'/bootstrap.tar.gz';
    }

    /**
     * Der Name, den CluPilot für diesen Host führt: `<rz>-<nn>.node.<zone>`.
     *
     * Mitgegeben statt vom Skript geraten. Die erste Fassung leitete ihn auf der
     * Maschine aus der Rückwärtsauflösung ab und fiel notfalls auf einen fest
     * verdrahteten Namen zurück — der Installer verlangt einen FQDN, und ein
     * geratener steht danach in `/etc/hosts`, im Zertifikatsantrag und in jeder
     * Protokollzeile des Hosts.
     *
     * Die PLATTFORM-Zone, nicht die Kundenzone: `clupilot.com`, nicht
     * `clupilot.cloud`. Die zwei sind laut OfficialDomains getrennt, und ein
     * Host gehört auf die Seite der Plattform.
     */
    public static function fqdnFor(Host $host): string
    {
        return HostName::fqdn($host->name);
    }

    /** Die Präfixlänge des Tunnel-Subnetzes, damit `--wg-ip` vollständig ist. */
    private static function subnetPrefix(): string
    {
        $subnet = (string) config('provisioning.wireguard.subnet', '10.66.0.0/24');

        return str_contains($subnet, '/') ? explode('/', $subnet)[1] : '24';
    }

    /**
     * Was in der Zeile fehlt, damit sie überhaupt funktionieren kann.
     *
     * Ein leerer Hub-Schlüssel oder ein leerer Endpunkt ergibt eine Zeile, die
     * sauber aussieht, kopiert wird, läuft — und in einem Tunnel endet, der nie
     * einen Handshake hat. Das fiele erst auf der Maschine auf, nach der
     * Installation. Lieber hier sagen, was fehlt.
     *
     * @return list<string>
     */
    public static function missingSettings(): array
    {
        $missing = [];

        if (blank(config('provisioning.wireguard.hub_public_key'))) {
            $missing[] = 'CLUPILOT_WG_HUB_PUBKEY';
        }

        if (blank(config('provisioning.wireguard.endpoint'))) {
            $missing[] = 'CLUPILOT_WG_ENDPOINT';
        }

        if (blank(config('provisioning.wireguard.hub_address'))) {
            $missing[] = 'CLUPILOT_WG_HUB_ADDRESS';
        }

        return $missing;
    }
}
