<?php

namespace App\Http\Controllers;

use App\Support\HostEnrolment;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\Process\Process;

/**
 * Liefert das Bootstrap-Skript als Archiv aus.
 *
 * Das ist der einzige Weg, auf dem das Skript auf eine nackte Maschine kommt:
 * ein Rettungssystem hat nichts als curl. Es ist eine STATISCHE Datei und kein
 * Endpunkt, der Auskunft gibt — Spec §5 bleibt damit unangetastet. Es steht
 * nichts Vertrauliches darin; alle Geheimnisse reist der Betreiber in der
 * Befehlszeile mit, nicht im Archiv.
 *
 * Als Archiv und nicht als eine Datei, weil `curl … | sh` keine `lib/` haben
 * kann und der Skript-Plan getrennte Dateien verlangt: `network.sh` und
 * `template.sh` sollen einzeln nachlesbar und wiederholbar bleiben, wenn auf
 * einer Maschine um drei Uhr morgens etwas klemmt.
 *
 * Gebaut wird beim ersten Abruf und danach nur, wenn sich unter `deploy/bootstrap`
 * etwas geändert hat. Ein Archiv, das im Repo läge, wäre die Fassung von dem
 * Tag, an dem jemand zuletzt daran gedacht hat, es neu zu bauen.
 */
class BootstrapArchiveController extends Controller
{
    public function __invoke(Request $request): BinaryFileResponse
    {
        // Der Ausweis ist der Einmal-Code, der ohnehin schon in der kopierten
        // Zeile steht. Kein zweites Geheimnis: ein eigener Download-Token liefe
        // nie ab, stünde für immer in Shell-Historien, und er reiste in
        // derselben Zeile wie der WireGuard-Schlüssel — er schützte also das
        // Unwichtigste mit derselben Aussetzung wie das Wichtigste.
        //
        // `resolve()` und nicht `claim()`: der Code wird hier NICHT verbraucht.
        // Er wird nach dem Herunterladen noch für jede Fortschrittsmeldung und
        // zuletzt für die Registrierung gebraucht.
        //
        // 404 und nicht 403, dieselbe Regel wie bei der Konsole: ein Fremder
        // soll nicht erfahren, dass es hier etwas gibt.
        $code = (string) ($request->query('code') ?? $request->header('X-CluPilot-Code', ''));

        if (HostEnrolment::resolve($code) === null) {
            abort(404);
        }

        $archive = storage_path('app/bootstrap.tar.gz');

        if ($this->isStale($archive)) {
            $this->build($archive);
        }

        return response()
            ->file($archive, [
                'Content-Type' => 'application/gzip',
                'Content-Disposition' => 'attachment; filename="bootstrap.tar.gz"',
                // Nicht in den Index. Die Adresse ist ohne Code wertlos, aber
                // eine Suchmaschine, die sie kennt, verrät immerhin, dass es
                // sie gibt.
                'X-Robots-Tag' => 'noindex, nofollow',
            ]);
    }

    /**
     * Neuer als alles, was drinsteckt?
     *
     * Über die Änderungszeit und nicht über einen Hash: das Verzeichnis ist
     * klein, der Vergleich läuft bei jedem Abruf, und ein Hash über alle Dateien
     * kostete bei jedem Abruf mehr als das Packen gelegentlich kostet.
     */
    private function isStale(string $archive): bool
    {
        if (! is_file($archive)) {
            return true;
        }

        $built = filemtime($archive);

        foreach ($this->sourceFiles() as $file) {
            if (filemtime($file) > $built) {
                return true;
            }
        }

        return false;
    }

    /** @return list<string> */
    private function sourceFiles(): array
    {
        $root = base_path('deploy/bootstrap');

        if (! is_dir($root)) {
            return [];
        }

        $files = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $files[] = $file->getPathname();
            }
        }

        return $files;
    }

    /**
     * Mit GNU tar und nicht mit PharData: `phar.readonly` steht in diesem
     * Container auf 1, und das Ausführbar-Bit soll erhalten bleiben.
     *
     * Der oberste Eintrag heißt `bootstrap/`, damit die kopierte Befehlszeile
     * mit `tar xz -C /opt/clupilot` genau `/opt/clupilot/bootstrap` ergibt —
     * der Ort, an dem das Skript sich selbst und seine Bibliothek erwartet.
     */
    private function build(string $archive): void
    {
        @mkdir(dirname($archive), 0755, true);

        $process = new Process(
            ['tar', 'czf', $archive, '-C', base_path('deploy'), 'bootstrap'],
            timeout: 60,
        );

        $process->mustRun();
    }
}
