<?php

declare(strict_types=1);

/**
 * Files Gallery (Dateiablage /dateien) aktualisieren — App-Datei und self-hosted Assets.
 *
 * Warum ein Skript: die App lädt ihre Assets sonst vom jsDelivr-CDN (index.php:965), was
 * gegen Regel 1 verstößt. Self-Hosting ist offiziell vorgesehen, muss aber bei JEDEM
 * Versionswechsel wiederholt werden, weil die Asset-Pfade die Version enthalten
 * (files.photo.gallery@<version>/...). Dazu kommt der TSV-PATCH in index.php, den ein
 * Update sonst wegwirft. Beides von Hand ist genau der Schritt, der einmal vergessen wird.
 *
 * NUR CLI/Build — nie im Request-Pfad. Die einzige ausgehende Verbindung ist jsDelivr,
 * und die passiert hier, nicht im Browser des Besuchers.
 *
 * Aufruf:
 *   php bin/filesgallery-update.php --check     nur prüfen (Patch da? Assets zur Version da?)
 *   php bin/filesgallery-update.php --assets    Assets zur vendorten Version neu holen
 *   php bin/filesgallery-update.php             neue index.php holen, patchen, Assets holen
 *
 * Nach einem echten Versionssprung: Login, Upload und Vorschau einmal durchklicken und
 * die alten Asset-Verzeichnisse löschen (das Skript sagt, welche das sind).
 */
if (PHP_SAPI !== 'cli') {
    exit(1);
}

require dirname(__DIR__) . '/app/bootstrap.php';

const FG_APP = PUBLIC_PATH . '/dateien/index.php';
const FG_ASSETS_DIR = PUBLIC_PATH . '/assets/filesgallery';
const FG_APP_URL = 'https://cdn.jsdelivr.net/npm/files.photo.gallery/index.php';
const FG_LOG = STORAGE_PATH . '/logs/filesgallery.log';

// Der Patch, ohne den die App bei abweichendem Arbeitsverzeichnis ohne Login startet.
const FG_PATCH_NEEDLE = "public static \$localconfigpath = '_filesconfig.php';";
const FG_PATCH_MARKER = "public static \$localconfigpath = __DIR__ . '/_filesconfig.php';";
const FG_PATCH_REPLACEMENT = <<<'PHP'
  // TSV-PATCH (einzige Änderung an dieser Datei): absolut statt relativ.
  // Der Original-Pfad wird gegen das aktuelle Arbeitsverzeichnis aufgelöst. Weicht das ab
  // (z. B. unter `php -S` mit Router-Script), findet die App _filesconfig.php nicht und
  // startet mit ihrer Default-Config — also OHNE Login und mit falschen Pfaden.
  // Nebeneffekt: der Selbst-Ausschluss in Path::is_exclude() vergleicht diesen Wert mit
  // einem Basename und greift dadurch nicht mehr. Belanglos, weil unser `root`
  // (storage/dateien/uploads) außerhalb dieses Verzeichnisses liegt.
  // Nach jedem Update neu setzen: `php bin/filesgallery-update.php` erledigt und prüft das.
  public static $localconfigpath = __DIR__ . '/_filesconfig.php';
PHP;

$mode = $argv[1] ?? '';
if (!in_array($mode, ['', '--check', '--assets'], true)) {
    fwrite(STDERR, "Unbekannte Option: {$mode}\nAufruf: php bin/filesgallery-update.php [--check|--assets]\n");
    exit(1);
}

$errors = 0;

function fg_log(string $line): void
{
    @file_put_contents(FG_LOG, date('c') . ' ' . $line . "\n", FILE_APPEND);
}

function fg_fail(string $message): void
{
    global $errors;
    $errors++;
    fwrite(STDERR, "✗ {$message}\n");
    fg_log('FEHLER ' . $message);
}

/** Datei von jsDelivr holen; gibt den Rohinhalt zurück oder null. */
function fg_fetch(string $url): ?string
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 3,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 120, // die Assets sind ein paar MB
        CURLOPT_USERAGENT => 'tsv08kulmbach-filesgallery-update/1.0',
    ]);
    $body = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);

    if (!is_string($body) || $body === '' || $status !== 200) {
        fg_fail("Download fehlgeschlagen ({$status}" . ($error !== '' ? ", {$error}" : '') . "): {$url}");
        return null;
    }
    return $body;
}

/** Version aus dem Quelltext der App lesen. */
function fg_version(string $source): ?string
{
    return preg_match('/public static \$version = \'([0-9][^\']*)\'/', $source, $m) === 1 ? $m[1] : null;
}

/**
 * Den TSV-PATCH anwenden. Idempotent: ist er schon drin, bleibt die Datei unverändert.
 * Fehlt beides (Patch und Original-Zeile), bricht das Skript ab — dann hat die App ihre
 * Struktur geändert und der Patch muss von Hand nachgezogen werden.
 */
function fg_patch(string $source): ?string
{
    if (str_contains($source, FG_PATCH_MARKER)) {
        return $source;
    }
    if (!str_contains($source, FG_PATCH_NEEDLE)) {
        fg_fail('Patch-Stelle nicht gefunden ($localconfigpath) — index.php von Hand prüfen, NICHT ungepatcht ausliefern.');
        return null;
    }
    // Die Original-Zeile trägt einen Kommentar hinter dem Semikolon; den behalten wir.
    return (string) preg_replace(
        '/^\h*' . preg_quote(FG_PATCH_NEEDLE, '/') . '/m',
        FG_PATCH_REPLACEMENT,
        $source,
        1
    );
}

/** assets.zip zur Version holen und nach public/assets/filesgallery/ entpacken. */
function fg_assets(string $version): void
{
    $url = "https://cdn.jsdelivr.net/npm/files.photo.gallery@{$version}/_files/assets/assets.zip";
    $zipData = fg_fetch($url);
    if ($zipData === null) {
        return;
    }

    $tmp = tempnam(sys_get_temp_dir(), 'fg-assets-');
    if ($tmp === false || file_put_contents($tmp, $zipData) === false) {
        fg_fail('Konnte assets.zip nicht zwischenspeichern.');
        return;
    }

    $zip = new ZipArchive();
    if ($zip->open($tmp) !== true) {
        fg_fail('assets.zip ist kein lesbares ZIP-Archiv.');
        @unlink($tmp);
        return;
    }

    if (!is_dir(FG_ASSETS_DIR) && !mkdir(FG_ASSETS_DIR, 0775, true) && !is_dir(FG_ASSETS_DIR)) {
        fg_fail('Kann ' . FG_ASSETS_DIR . ' nicht anlegen.');
        $zip->close();
        @unlink($tmp);
        return;
    }

    $written = 0;
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = (string) $zip->getNameIndex($i);

        // Manche Archive tragen den _files/assets-Pfad noch mit sich; wir wollen den Inhalt flach.
        $rel = (string) preg_replace('#^(\./)?(_files/)?assets/#', '', $name);
        $rel = ltrim(str_replace('\\', '/', $rel), '/');

        // Zip-Slip: alles mit Parent-Traversal oder absolutem Pfad wird verworfen.
        if ($rel === '' || str_contains($rel, '../')) {
            continue;
        }
        $target = FG_ASSETS_DIR . '/' . $rel;

        if (str_ends_with($name, '/')) {
            if (!is_dir($target)) {
                @mkdir($target, 0775, true);
            }
            continue;
        }

        $dir = dirname($target);
        if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
            fg_fail("Kann Verzeichnis nicht anlegen: {$dir}");
            continue;
        }
        $stream = $zip->getStream($name);
        if ($stream === false) {
            fg_fail("Konnte Eintrag nicht lesen: {$name}");
            continue;
        }
        if (@file_put_contents($target, $stream) === false) {
            fg_fail("Konnte Datei nicht schreiben: {$target}");
        } else {
            $written++;
        }
        fclose($stream);
    }
    $zip->close();
    @unlink($tmp);

    echo "✓ Assets entpackt ({$written} Dateien) → " . str_replace(ROOT_PATH . '/', '', FG_ASSETS_DIR) . "\n";
    fg_log("Assets {$version} entpackt ({$written} Dateien)");
}

/** Prüfen, ob Patch und Assets zur vendorten Version passen. */
function fg_check(string $version, string $source): void
{
    if (str_contains($source, FG_PATCH_MARKER)) {
        echo "✓ TSV-PATCH in public/dateien/index.php vorhanden\n";
    } else {
        fg_fail('TSV-PATCH fehlt in public/dateien/index.php — die App würde ohne Login starten.');
    }

    $css = FG_ASSETS_DIR . "/files.photo.gallery@{$version}/css/files.css";
    if (is_file($css)) {
        echo "✓ Assets für Version {$version} vorhanden\n";
    } else {
        fg_fail("Assets für Version {$version} fehlen (erwartet: " . str_replace(ROOT_PATH . '/', '', $css) . ") — php bin/filesgallery-update.php --assets");
    }

    // Verirrte Assets anderer Versionen melden, damit sie nicht mitgeschleppt werden.
    foreach (glob(FG_ASSETS_DIR . '/files.photo.gallery@*') ?: [] as $dir) {
        if (basename($dir) !== "files.photo.gallery@{$version}") {
            echo "· Alte Assets, können weg: " . str_replace(ROOT_PATH . '/', '', $dir) . "\n";
        }
    }
}

// --- Ablauf ---------------------------------------------------------------------------

if (!is_file(FG_APP)) {
    fwrite(STDERR, '✗ ' . FG_APP . " fehlt.\n");
    exit(1);
}

$source = (string) file_get_contents(FG_APP);

if ($mode === '' ) {
    // Vollupdate: neue App-Datei holen, patchen, dann Assets zur neuen Version.
    $fresh = fg_fetch(FG_APP_URL);
    if ($fresh !== null) {
        $newVersion = fg_version($fresh);
        if ($newVersion === null) {
            fg_fail('Version im Download nicht gefunden — Datei nicht übernommen.');
        } else {
            $patched = fg_patch($fresh);
            if ($patched !== null) {
                $old = fg_version($source) ?? '?';
                if ($patched === $source) {
                    echo "· Version {$newVersion} ist schon aktuell und gepatcht\n";
                } elseif (file_put_contents(FG_APP, $patched) === false) {
                    fg_fail('Konnte ' . FG_APP . ' nicht schreiben.');
                } else {
                    echo "✓ index.php aktualisiert: {$old} → {$newVersion} (gepatcht)\n";
                    fg_log("index.php {$old} → {$newVersion}");
                    $source = $patched;
                }
            }
        }
    }
}

$version = fg_version($source);
if ($version === null) {
    fwrite(STDERR, "✗ Version in public/dateien/index.php nicht lesbar.\n");
    exit(1);
}

if ($mode !== '--check') {
    fg_assets($version);
}

fg_check($version, $source);

if ($errors > 0) {
    fwrite(STDERR, "\n{$errors} Problem(e) — nicht ausliefern, bis sie behoben sind.\n");
    exit(1);
}
echo "\nFiles Gallery {$version}: alles an seinem Platz.\n";
exit(0);
