<?php
declare(strict_types=1);
require __DIR__ . '/lib/Auth.php';
require __DIR__ . '/lib/Layout.php';
require __DIR__ . '/lib/Channels.php';

use PSLauncher\Admin\Auth;
use PSLauncher\Admin\Layout;
use PSLauncher\Admin\Channels;

Auth::requireLogin();
$config = require __DIR__ . '/../api/config.php';

$root         = dirname(__DIR__);
$manifestDir  = "$root/manifest";
$buildsDir    = "$root/builds";
$notesDir     = "$root/releasenotes";
$manifestPath = "$manifestDir/versions.json";

$message = null; $messageType = 'success';

// Liste des channels déclarés dans channels.json (incluant "default" implicite)
$channelRegistry = Channels::load($manifestDir);
$channelNames    = array_map(fn($c) => (string)($c['name'] ?? ''), $channelRegistry['channels']);

/**
 * Sanitize la liste de channels postée. On garde uniquement ceux qui existent
 * dans le registre — un attaquant ne peut pas tagger une version sur un channel
 * non déclaré. Si la liste finit vide, on retombe sur ['default'] (= public).
 */
function sanitizeChannels(array $posted, array $allowedNames): array
{
    $out = [];
    foreach ($posted as $c) {
        $c = (string)$c;
        if (in_array($c, $allowedNames, true) && !in_array($c, $out, true)) {
            $out[] = $c;
        }
    }
    if (empty($out)) $out = ['default'];
    return $out;
}

/**
 * ID stable interne pour identifier une entrée du manifest. Permet d'avoir
 * plusieurs entrées avec le même numéro de version (ex. v1.5.3 pour police +
 * v1.5.3 pour pompier) — la "version" devient un libellé human-readable, pas
 * un identifiant unique. Le tag de channels distingue qui voit quoi.
 *
 * Format : 'v' + 8 hex chars random. Immutable une fois généré.
 */
function generate_entry_id(): string
{
    try { return 'v' . bin2hex(random_bytes(4)); }
    catch (\Throwable) { return 'v' . dechex(mt_rand(0, 0xffffffff)); }
}

function loadManifest(string $path): array
{
    if (!is_file($path)) return ['schemaVersion' => 1, 'product' => 'PROSERVE_UE', 'latest' => null, 'versions' => []];
    $manifest = json_decode(file_get_contents($path), true) ?? [];

    // Migration douce : on ajoute un `id` aux entrées qui n'en ont pas (legacy
    // pre-v0.27.1). Auto-save à la fin pour que l'id soit persisté et ne change
    // plus aux reloads suivants. Sans ça, les forms pointeraient sur des ids
    // jetables et les actions tomberaient à plat au refresh.
    $dirty = false;
    foreach ($manifest['versions'] ?? [] as &$v) {
        if (empty($v['id'])) { $v['id'] = generate_entry_id(); $dirty = true; }
    }
    unset($v);
    if ($dirty) saveManifest($path, $manifest);

    return $manifest;
}

function saveManifest(string $path, array $manifest): void
{
    // Re-tri par SemVer descendant. Pour des entrées de même version,
    // tiebreak sur l'id (stable mais arbitraire) — l'admin peut compter
    // sur un ordre déterministe.
    usort($manifest['versions'], function($a, $b) {
        $cmp = version_compare($b['version'] ?? '0.0.0', $a['version'] ?? '0.0.0');
        return $cmp !== 0 ? $cmp : strcmp($a['id'] ?? '', $b['id'] ?? '');
    });
    file_put_contents(
        $path,
        json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
    );
}

function find_entry_index_by_id(array $manifest, string $id): ?int
{
    foreach ($manifest['versions'] ?? [] as $i => $v) {
        if (($v['id'] ?? '') === $id) return $i;
    }
    return null;
}

/**
 * Normalise un nom de fichier ZIP saisi par l'admin :
 *   - strip tout chemin (path traversal défense, basename)
 *   - whitelist [a-zA-Z0-9_.-] (laisse les majuscules, points, tirets, underscores)
 *   - assure le suffixe .zip
 *   - retombe sur la valeur par défaut si l'input est vide ou inexploitable
 */
function ps_normalize_zip_filename(string $raw, string $version): string
{
    $default = "proserve-{$version}.zip";
    $name = trim($raw);
    if ($name === '') return $default;
    // Strip tout chemin → garde juste le filename (sécurité)
    $name = basename($name);
    // Whitelist : on garde un set de chars filename-safe sur Windows + Linux
    $name = preg_replace('/[^a-zA-Z0-9_.\-]/', '', $name);
    if ($name === '' || $name === '.zip' || $name === '.') return $default;
    if (!str_ends_with(strtolower($name), '.zip')) $name .= '.zip';
    return $name;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    Auth::checkCsrf();
    $action = $_POST['action'] ?? '';
    $manifest = loadManifest($manifestPath);

    try {
        if ($action === 'add') {
            $version    = trim($_POST['version'] ?? '');
            $minLicDate = trim($_POST['min_license_date'] ?? '');
            $releasedAt = trim($_POST['released_at'] ?? '');
            $notes      = $_POST['notes'] ?? '';
            $available  = isset($_POST['available']);
            $isBeta     = isset($_POST['is_beta']);
            $betaNotes  = trim($_POST['beta_notes'] ?? '') ?: null;

            if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
                throw new Exception('Numéro de version invalide (format X.Y.Z attendu).');
            }
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) {
                throw new Exception('Date min_license_date invalide.');
            }

            // Le numéro de version peut être DUPLIQUÉ entre channels — c'est
            // exactement le cas d'usage (v1.5.3 pour police + v1.5.3 pour
            // pompier coexistent). On rejette uniquement la collision sur le
            // nom de ZIP, parce que là on aurait une vraie ambiguïté physique
            // : deux entries pointant sur le même fichier sur disque.
            $zipFilenameProposed = ps_normalize_zip_filename((string)($_POST['zip_filename'] ?? ''), $version);
            foreach ($manifest['versions'] as $v) {
                $existingZip = basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '');
                if ($existingZip === $zipFilenameProposed) {
                    throw new Exception(
                        "Le ZIP « {$zipFilenameProposed} » est déjà utilisé par v"
                        . htmlspecialchars($v['version'] ?? '?')
                        . ". Choisis un autre nom de ZIP (ex. proserve-{$version}-police.zip)."
                    );
                }
            }

            $releasedAtIso = $releasedAt !== ''
                ? (new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z')
                : (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');

            $base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
            // Le ZIP a déjà été normalisé pour le check anti-collision plus haut.
            $zipFilename = $zipFilenameProposed;
            $channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
            $entryId = generate_entry_id();
            $exeName = trim((string)($_POST['executable'] ?? 'PROSERVE_UE_5_7.exe'));
            // Validation stricte : pas de path traversal possible dans le nom du fichier.
            if (!preg_match('/^[A-Za-z0-9_.\-]+\.exe$/', $exeName)) {
                $exeName = 'PROSERVE_UE_5_7.exe';
            }
            $entry = [
                'id'                    => $entryId,
                'version'               => $version,
                'releasedAt'            => $releasedAtIso,
                'executable'            => $exeName,
                'installFolderTemplate' => 'PROSERVE v{version}',
                'channels'              => $channels,
                'download'              => [
                    'url'       => "{$base}/builds/{$zipFilename}",
                    'sizeBytes' => 0,
                    'sha256'    => 'REPLACE_AFTER_BUILD',
                ],
                // Release notes adressables par id stable — supporte plusieurs entries
                // de même version sans collision (chaque release a son propre .md).
                'releaseNotesUrl'       => "{$base}/api/releasenotes/{$entryId}",
                'minLicenseDate'        => $minLicDate,
                'availableForDownload'  => $available,
            ];
            if ($isBeta) {
                $entry['isBeta'] = true;
                if ($betaNotes !== null) $entry['betaNotes'] = $betaNotes;
            }
            $manifest['versions'][] = $entry;

            saveManifest($manifestPath, $manifest);

            if ($notes !== '') {
                if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
                file_put_contents("$notesDir/{$entryId}.md", $notes);
            }
            $message = "v{$version} ajoutée (id <code>{$entryId}</code>). Upload le ZIP <code>{$zipFilename}</code> en SFTP dans <code>builds/</code>, puis clique « Sync (sign-manifest) ».";
        }
        elseif ($action === 'edit_notes') {
            $entryId = (string)($_POST['id'] ?? '');
            $notes   = $_POST['notes'] ?? '';
            $idx     = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
            file_put_contents("$notesDir/{$entryId}.md", $notes);
            $message = "Release notes de v" . ($manifest['versions'][$idx]['version'] ?? '?') . " mises à jour.";
        }
        elseif ($action === 'edit_meta') {
            $entryId    = (string)($_POST['id'] ?? '');
            $minLicDate = trim($_POST['min_license_date'] ?? '');
            $releasedAt = trim($_POST['released_at'] ?? '');
            $newZipName = trim((string)($_POST['zip_filename'] ?? ''));
            if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) {
                throw new Exception('min_license_date invalide.');
            }
            $idx = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $version = $manifest['versions'][$idx]['version'] ?? '';

            $manifest['versions'][$idx]['minLicenseDate'] = $minLicDate;
            if ($releasedAt !== '') {
                $manifest['versions'][$idx]['releasedAt'] =
                    (new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
            }
            if ($newZipName !== '') {
                $normalized = ps_normalize_zip_filename($newZipName, $version);
                // Anti-collision : interdit de renommer vers un ZIP déjà utilisé par une autre entry
                foreach ($manifest['versions'] as $other) {
                    if (($other['id'] ?? '') === $entryId) continue;
                    $otherZip = basename(parse_url($other['download']['url'] ?? '', PHP_URL_PATH) ?: '');
                    if ($otherZip === $normalized) {
                        throw new Exception("Le ZIP « {$normalized} » est déjà utilisé par une autre entry. Choisis un autre nom.");
                    }
                }
                $base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
                $manifest['versions'][$idx]['download']['url']       = "{$base}/builds/{$normalized}";
                $manifest['versions'][$idx]['download']['sha256']    = 'REPLACE_AFTER_BUILD';
                $manifest['versions'][$idx]['download']['sizeBytes'] = 0;
            }
            saveManifest($manifestPath, $manifest);
            $message = "Méta de v{$version} mises à jour" . ($newZipName !== '' ? ". ⚠️ Le ZIP a été renommé : upload le nouveau fichier puis re-Sync." : '.');
        }
        elseif ($action === 'set_channels') {
            $entryId  = (string)($_POST['id'] ?? '');
            $idx      = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
            $manifest['versions'][$idx]['channels'] = $channels;
            saveManifest($manifestPath, $manifest);
            $version = $manifest['versions'][$idx]['version'] ?? '?';
            $message = "Channels de v{$version} : " . implode(', ', $channels) . ". Re-Sync pour rafraîchir la signature du manifest.";
        }
        elseif ($action === 'set_beta') {
            $entryId   = (string)($_POST['id'] ?? '');
            $idx       = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $isBeta    = !empty($_POST['is_beta']);
            $betaNotes = trim($_POST['beta_notes'] ?? '') ?: null;
            if ($isBeta) {
                $manifest['versions'][$idx]['isBeta'] = true;
                if ($betaNotes !== null) {
                    $manifest['versions'][$idx]['betaNotes'] = $betaNotes;
                } else {
                    unset($manifest['versions'][$idx]['betaNotes']);
                }
            } else {
                unset($manifest['versions'][$idx]['isBeta']);
                unset($manifest['versions'][$idx]['betaNotes']);
            }
            saveManifest($manifestPath, $manifest);
            $version = $manifest['versions'][$idx]['version'] ?? '?';
            $message = "Statut BÊTA de v{$version} mis à jour. N'oublie pas de re-signer le manifest (« 🔁 Sync »).";
        }
        elseif ($action === 'toggle_available') {
            $entryId = (string)($_POST['id'] ?? '');
            $idx     = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $manifest['versions'][$idx]['availableForDownload'] = !($manifest['versions'][$idx]['availableForDownload'] ?? true);
            saveManifest($manifestPath, $manifest);
            $version = $manifest['versions'][$idx]['version'] ?? '?';
            $message = "Disponibilité de v{$version} mise à jour.";
        }
        elseif ($action === 'delete') {
            $entryId = (string)($_POST['id'] ?? '');
            $idx     = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $version = $manifest['versions'][$idx]['version'] ?? '?';
            // Cleanup release notes file (best effort)
            $notesFile = "$notesDir/{$entryId}.md";
            if (is_file($notesFile)) @unlink($notesFile);
            $manifest['versions'] = array_values(array_filter(
                $manifest['versions'],
                fn($v) => ($v['id'] ?? '') !== $entryId
            ));
            saveManifest($manifestPath, $manifest);
            $message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu).";
        }
        elseif ($action === 'sync' || $action === 'sync_versions') {
            require_once "$root/tools/SignManifest.php";
            $signer = new \PSLauncher\Tools\SignManifest($root);
            $scope = $action === 'sync_versions' ? 'versions' : 'all';
            $force = !empty($_POST['force']);
            $result = $signer->run($scope, $force);
            $label = $scope === 'versions' ? 'versions Proserve' : 'manifest complet';
            $forceLabel = $force ? ' [FORCE: cache ignoré]' : ' [cache utilisé pour les ZIPs inchangés]';
            $message = "Sortie du script (scope: {$label}{$forceLabel}) :\n" . implode("\n", $result['log']);
            if (!$result['ok']) $messageType = 'error';
        }
        elseif ($action === 'sync_one') {
            $entryId = (string)($_POST['id'] ?? '');
            $idx     = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $version = $manifest['versions'][$idx]['version'] ?? '?';
            require_once "$root/tools/SignManifest.php";
            $signer = new \PSLauncher\Tools\SignManifest($root);
            $force = !empty($_POST['force']);
            // SignManifest::run filtre par numéro de version ; en cas d'entries
            // de même version sur des channels différents, le hash sera recalculé
            // pour toutes celles qui matchent (chacune pointe sur son propre ZIP,
            // donc le résultat est correct, juste un peu plus de boulot).
            $result = $signer->run('versions', $force, $version);
            $forceLabel = $force ? ' [FORCE]' : '';
            $message = "Hash de v{$version}{$forceLabel} :\n" . implode("\n", $result['log']);
            if (!$result['ok']) $messageType = 'error';
        }
        elseif ($action === 'set_skip_hash') {
            $entryId  = (string)($_POST['id'] ?? '');
            $idx      = find_entry_index_by_id($manifest, $entryId);
            if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
            $skipHash = !empty($_POST['skip']);
            $version  = $manifest['versions'][$idx]['version'] ?? '?';
            if ($skipHash) {
                $manifest['versions'][$idx]['download']['hashAlgorithm'] = 'none';
                $manifest['versions'][$idx]['download']['sha256'] = '';
            } else {
                unset($manifest['versions'][$idx]['download']['hashAlgorithm']);
                if (empty($manifest['versions'][$idx]['download']['sha256'])) {
                    $manifest['versions'][$idx]['download']['sha256'] = 'REPLACE_AFTER_BUILD';
                }
            }
            saveManifest($manifestPath, $manifest);

            // Re-signature auto pour garder le manifest valide après le changement.
            require_once "$root/tools/SignManifest.php";
            $signer = new \PSLauncher\Tools\SignManifest($root);
            $resign = $signer->run('versions', false);
            $message = ($skipHash
                ? "Vérif SHA-256 désactivée pour v{$version} et manifest re-signé."
                : "Vérif SHA-256 réactivée pour v{$version}. Clique « 🔁 Hash » sur cette ligne pour calculer le SHA-256.")
                . "\n" . implode("\n", $resign['log']);
            if (!$resign['ok']) $messageType = 'error';
        }
    } catch (Exception $e) {
        $message = $e->getMessage();
        $messageType = 'error';
    }
}

$manifest  = loadManifest($manifestPath);
// Liste des ZIPs : tout est flat dans builds/, indexé par filename simple.
$zips      = is_dir($buildsDir) ? array_map('basename', glob("$buildsDir/*.zip") ?: []) : [];
$zipSizes  = [];
foreach ($zips as $z) $zipSizes[$z] = filesize("$buildsDir/$z");

// Pour l'édition : pré-charge les release notes existantes (indexées par
// id stable maintenant). Fallback sur l'ancien format {version}.md pour les
// notes créées avant v0.27.1, qu'on n'a pas encore migrées.
$existingNotes = [];
foreach ($manifest['versions'] ?? [] as $v) {
    $id = $v['id'] ?? '';
    $byId      = $id !== '' ? "$notesDir/{$id}.md" : null;
    $byVersion = "$notesDir/{$v['version']}.md";
    if ($byId !== null && is_file($byId)) {
        $existingNotes[$id] = file_get_contents($byId);
    } elseif (is_file($byVersion)) {
        $existingNotes[$id] = file_get_contents($byVersion);
    } else {
        $existingNotes[$id] = '';
    }
}

Layout::header('Versions', 'versions');
?>
<h1>Versions</h1>

<?php Layout::flash($message, $messageType); ?>

<div class="card">
    <h2>Workflow d'une nouvelle release</h2>
    <ol class="muted">
        <li>Ajoute l'entrée du manifest avec le formulaire ci-dessous (version, date min de license, release notes, channels).</li>
        <li>Upload le ZIP en SFTP dans <code>www/PS_Launcher/builds/</code> (le nom doit matcher celui que tu as saisi, par défaut <code>proserve-{version}.zip</code>).</li>
        <li>Clique <strong>🔁 Sync (sign-manifest)</strong> pour calculer le SHA-256, mettre à jour <code>sizeBytes</code>, bumper <code>latest</code>, et signer le manifest avec Ed25519.</li>
        <li>Les clients PS_Launcher détecteront la nouvelle version au prochain « Vérifier les MAJ », filtrés selon le channel de leur license.</li>
    </ol>
    <p class="muted" style="margin: 12px 0 0;">
        💡 La gestion des channels (création / édition / suppression) se fait sur la page <a href="channels.php">Channels</a>.
    </p>
</div>

<div class="card">
    <h2>Ajouter une version</h2>
    <form method="post">
        <?= Layout::csrfField() ?>
        <input type="hidden" name="action" value="add">
        <div class="row">
            <div class="col field">
                <label>Version (X.Y.Z)</label>
                <input type="text" name="version" placeholder="1.4.8" required pattern="\d+\.\d+\.\d+">
            </div>
            <div class="col field">
                <label>Date de release</label>
                <input type="datetime-local" name="released_at">
            </div>
            <div class="col field">
                <label>min_license_date (license requise)</label>
                <input type="date" name="min_license_date" required>
            </div>
        </div>
        <div class="field">
            <label>Nom du fichier ZIP <span class="muted" style="font-weight: normal; font-size: 11px;">(optionnel, par défaut <code>proserve-{version}.zip</code>)</span></label>
            <input type="text" name="zip_filename" placeholder="proserve-1.4.8-police.zip" maxlength="120">
        </div>
        <div class="field">
            <label>Nom de l'exécutable <span class="muted" style="font-weight: normal; font-size: 11px;">(le .exe à lancer dans le dossier d'install — varie selon la version d'Unreal Engine du build)</span></label>
            <input type="text" name="executable" placeholder="PROSERVE_UE_5_7.exe" value="PROSERVE_UE_5_7.exe" required pattern="[A-Za-z0-9_.\-]+\.exe" maxlength="120">
        </div>
        <div class="field">
            <label>Channels qui voient cette version <span class="muted" style="font-weight: normal; font-size: 11px;">(coche au moins « default » pour rendre publique, ou un channel privé pour la cibler)</span></label>
            <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 8px 0;">
                <?php foreach ($channelRegistry['channels'] as $c):
                    $cn = $c['name'] ?? '';
                    $isDefault = $cn === 'default';
                ?>
                    <label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
                        <input type="checkbox" name="channels[]" value="<?= htmlspecialchars($cn) ?>" <?= $isDefault ? 'checked' : '' ?>>
                        <code><?= htmlspecialchars($cn) ?></code>
                        <span class="muted" style="font-size: 11px;"><?= htmlspecialchars($c['label'] ?? $cn) ?></span>
                    </label>
                <?php endforeach; ?>
            </div>
        </div>
        <div class="field">
            <label>Release notes (Markdown)</label>
            <textarea name="notes" rows="8" placeholder="# Proserve v1.4.8&#10;&#10;## Nouveautés&#10;- ..." style="font-family: 'Cascadia Code', Consolas, monospace;"></textarea>
        </div>
        <div class="row">
            <div class="col field">
                <label><input type="checkbox" name="available" checked> Disponible au téléchargement</label>
            </div>
            <div class="col field">
                <label><input type="checkbox" name="is_beta"> Version BÊTA <span class="muted" style="font-weight: normal; font-size: 11px;">(visible uniquement par les licenses « can_see_betas »)</span></label>
            </div>
        </div>
        <div class="field">
            <label>Notes pour les testeurs <span class="muted" style="font-weight: normal; font-size: 11px;">(affichées en tooltip dans le launcher quand BÊTA est coché)</span></label>
            <input type="text" name="beta_notes" placeholder="Tester en priorité : nouvelle UI scènes, intégration capteurs.">
        </div>
        <button class="btn btn-success" type="submit">Ajouter au manifest</button>
    </form>
</div>

<div class="card">
    <div class="toolbar">
        <h2 style="margin: 0; flex: 1;">Manifest actuel</h2>
        <span class="muted">
            Signature :
            <?= empty($manifest['signature'])
                ? "<span class='badge badge-warning'>NON SIGNÉ</span>"
                : "<span class='badge badge-success'>SIGNÉ Ed25519</span>" ?>
            <?php if (!empty($manifest['latest'])): ?>
                • latest : <code>v<?= htmlspecialchars($manifest['latest']) ?></code>
            <?php endif; ?>
        </span>
        <form method="post" style="display:inline">
            <?= Layout::csrfField() ?>
            <input type="hidden" name="action" value="sync_versions">
            <button class="btn btn-primary" type="submit">🔁 Hasher les versions + signer</button>
            <label style="margin-left:12px;font-size:12px;color:#888" title="Recalcule TOUS les SHA-256 même si les ZIPs n'ont pas changé. Lent. Cocher seulement en cas de doute sur le cache.">
                <input type="checkbox" name="force" value="1"> Force re-hash
            </label>
        </form>
    </div>

    <table>
        <thead>
            <tr>
                <th>Version</th>
                <th>Channels</th>
                <th>Release</th>
                <th>min_license</th>
                <th>ZIP</th>
                <th>Hash</th>
                <th>Visible</th>
                <th style="text-align:right">Actions</th>
            </tr>
        </thead>
        <tbody>
        <?php foreach ($manifest['versions'] ?? [] as $v):
            $zipBase     = basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '');
            $zipExists   = in_array($zipBase, $zips, true);
            $hashed      = !empty($v['download']['sha256']) && !str_starts_with($v['download']['sha256'], 'REPLACE');
            $hashSkipped = strtolower((string)($v['download']['hashAlgorithm'] ?? 'sha256')) === 'none';
        ?>
            <?php
                // Channels du version : array si défini, sinon implicite ['default']
                $vChannels = isset($v['channels']) && is_array($v['channels']) && !empty($v['channels'])
                    ? $v['channels']
                    : ['default'];
            ?>
            <tr>
                <td>
                    <strong>v<?= htmlspecialchars($v['version']) ?></strong>
                    <?php if (!empty($v['isBeta'])): ?>
                        <span class="badge badge-warning" title="<?= htmlspecialchars($v['betaNotes'] ?? 'Version BÊTA') ?>" style="margin-left: 4px;">BÊTA</span>
                    <?php endif; ?>
                </td>
                <td>
                    <?php foreach ($vChannels as $ch): ?>
                        <code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; font-size: 11px; margin-right: 4px;"><?= htmlspecialchars($ch) ?></code>
                    <?php endforeach; ?>
                </td>
                <td class="muted"><?= htmlspecialchars(substr($v['releasedAt'] ?? '', 0, 10)) ?></td>
                <td class="muted"><?= htmlspecialchars($v['minLicenseDate'] ?? '—') ?></td>
                <td>
                    <?php if ($zipExists): ?>
                        <span class="badge badge-success">présent</span>
                        <span class="muted"><?= Layout::formatBytes($zipSizes[$zipBase] ?? 0) ?></span>
                    <?php else: ?>
                        <span class="badge badge-warning">absent</span>
                        <div class="muted" style="font-size: 11px;">attendu : <code><?= htmlspecialchars($zipBase) ?></code></div>
                    <?php endif; ?>
                </td>
                <td>
                    <?php if ($hashSkipped): ?>
                        <span class="badge badge-warning" title="hashAlgorithm=none : la vérif SHA-256 est désactivée pour cette release">SKIP</span>
                    <?php elseif ($hashed): ?>
                        <code title="<?= htmlspecialchars($v['download']['sha256']) ?>"><?= substr($v['download']['sha256'], 0, 12) ?>…</code>
                    <?php else: ?>
                        <span class="badge badge-warning">à calculer</span>
                    <?php endif; ?>
                </td>
                <td>
                    <form method="post" style="display:inline">
                        <?= Layout::csrfField() ?>
                        <input type="hidden" name="action" value="toggle_available">
                        <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                        <button class="btn btn-secondary" type="submit">
                            <?= ($v['availableForDownload'] ?? true) ? '✓' : '✗' ?>
                        </button>
                    </form>
                </td>
                <td style="text-align: right; white-space: nowrap;">
                    <?php if ($zipExists && !$hashSkipped): ?>
                    <form method="post" style="display:inline" title="Recalcule le SHA-256 de cette version uniquement (utilise le cache si le ZIP n'a pas changé)">
                        <?= Layout::csrfField() ?>
                        <input type="hidden" name="action" value="sync_one">
                        <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                        <button class="btn btn-secondary" type="submit">🔁 Hash</button>
                    </form>
                    <?php endif; ?>
                    <details style="display: inline-block; margin: 0 4px;">
                        <summary class="btn btn-secondary">⚙ Hash</summary>
                        <div style="margin-top: 8px; min-width: 280px;">
                            <p class="muted" style="font-size: 12px; margin: 0 0 8px;">
                                <?php if ($hashSkipped): ?>
                                    La vérif SHA-256 est <strong>désactivée</strong> pour cette release.
                                    Le manifest sert <code>hashAlgorithm: "none"</code>. La sécurité repose
                                    uniquement sur la signature Ed25519 du manifest + HTTPS.
                                <?php else: ?>
                                    Désactive la vérif SHA-256 chez les clients pour cette release
                                    (utile sur très gros builds où on accepte le compromis perf/sécurité).
                                <?php endif; ?>
                            </p>
                            <form method="post" style="display:inline">
                                <?= Layout::csrfField() ?>
                                <input type="hidden" name="action" value="set_skip_hash">
                                <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                                <input type="hidden" name="skip" value="<?= $hashSkipped ? '0' : '1' ?>">
                                <button class="btn <?= $hashSkipped ? 'btn-success' : 'btn-warning' ?>" type="submit">
                                    <?= $hashSkipped ? '✓ Réactiver la vérif' : '⏭ Désactiver la vérif' ?>
                                </button>
                            </form>
                            <?php if ($zipExists): ?>
                            <form method="post" style="margin-top: 8px;" title="Recalcule en ignorant le cache même si le ZIP n'a pas changé">
                                <?= Layout::csrfField() ?>
                                <input type="hidden" name="action" value="sync_one">
                                <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                                <input type="hidden" name="force" value="1">
                                <button class="btn btn-secondary" type="submit">🔄 Force re-hash</button>
                            </form>
                            <?php endif; ?>
                        </div>
                    </details>
                    <details style="display: inline-block; margin: 0 4px;">
                        <summary class="btn btn-secondary">Méta</summary>
                        <form method="post" style="margin-top: 8px; min-width: 360px;">
                            <?= Layout::csrfField() ?>
                            <input type="hidden" name="action" value="edit_meta">
                            <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                            <div class="field">
                                <label>min_license_date</label>
                                <input type="date" name="min_license_date" value="<?= htmlspecialchars($v['minLicenseDate'] ?? '') ?>" required>
                            </div>
                            <div class="field">
                                <label>Date de release (UTC)</label>
                                <input type="datetime-local" name="released_at" value="<?= htmlspecialchars(substr($v['releasedAt'] ?? '', 0, 16)) ?>">
                            </div>
                            <div class="field">
                                <label>Renommer le ZIP <span class="muted" style="font-weight: normal; font-size: 11px;">(actuel : <code><?= htmlspecialchars(basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '')) ?></code>)</span></label>
                                <input type="text" name="zip_filename" placeholder="laisse vide pour ne rien changer">
                                <span class="muted" style="font-size: 11px;">⚠️ Renommer = invalide le sha256, il faudra re-uploader le nouveau ZIP en SFTP puis re-Sync.</span>
                            </div>
                            <button class="btn btn-primary" type="submit">Enregistrer</button>
                        </form>
                    </details>
                    <details style="display: inline-block; margin: 0 4px;">
                        <summary class="btn btn-secondary">Notes</summary>
                        <form method="post" style="margin-top: 8px;">
                            <?= Layout::csrfField() ?>
                            <input type="hidden" name="action" value="edit_notes">
                            <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                            <textarea name="notes" rows="10" style="font-family: 'Cascadia Code', Consolas, monospace; min-width: 400px;"><?= htmlspecialchars($existingNotes[$v['id'] ?? ''] ?? '') ?></textarea>
                            <br><br>
                            <button class="btn btn-primary" type="submit">Enregistrer les notes</button>
                        </form>
                    </details>
                    <details style="display: inline-block; margin: 0 4px;">
                        <summary class="btn <?= !empty($v['isBeta']) ? 'btn-warning' : 'btn-secondary' ?>"><?= !empty($v['isBeta']) ? '🟡 BÊTA' : 'Bêta' ?></summary>
                        <form method="post" style="margin-top: 8px; min-width: 360px;">
                            <?= Layout::csrfField() ?>
                            <input type="hidden" name="action" value="set_beta">
                            <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                            <div class="field">
                                <label><input type="checkbox" name="is_beta" value="1" <?= !empty($v['isBeta']) ? 'checked' : '' ?>>
                                    Cette version est une BÊTA
                                </label>
                            </div>
                            <div class="field">
                                <label>Notes pour les testeurs</label>
                                <input type="text" name="beta_notes" value="<?= htmlspecialchars($v['betaNotes'] ?? '') ?>"
                                       placeholder="Tester en priorité…">
                            </div>
                            <button class="btn btn-primary" type="submit">Enregistrer</button>
                            <p class="muted" style="font-size: 11px; margin: 8px 0 0;">⚠️ Re-signe le manifest (« 🔁 Sync ») après modif pour que les launchers acceptent.</p>
                        </form>
                    </details>
                    <details style="display: inline-block; margin: 0 4px;">
                        <summary class="btn btn-secondary">Channels</summary>
                        <form method="post" style="margin-top: 8px; min-width: 280px;">
                            <?= Layout::csrfField() ?>
                            <input type="hidden" name="action" value="set_channels">
                            <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                            <div class="field">
                                <?php foreach ($channelRegistry['channels'] as $c):
                                    $cn = $c['name'] ?? '';
                                    $checked = in_array($cn, $vChannels, true) ? 'checked' : '';
                                ?>
                                    <label style="display: flex; align-items: center; gap: 6px; padding: 4px 0; cursor: pointer;">
                                        <input type="checkbox" name="channels[]" value="<?= htmlspecialchars($cn) ?>" <?= $checked ?>>
                                        <code><?= htmlspecialchars($cn) ?></code>
                                        <span class="muted" style="font-size: 11px;"><?= htmlspecialchars($c['label'] ?? $cn) ?></span>
                                    </label>
                                <?php endforeach; ?>
                            </div>
                            <button class="btn btn-primary" type="submit">Enregistrer</button>
                            <p class="muted" style="font-size: 11px; margin: 8px 0 0;">⚠️ Re-signe le manifest après modif.</p>
                        </form>
                    </details>
                    <form method="post" style="display:inline" onsubmit="return confirm('Retirer v<?= htmlspecialchars($v['version']) ?> du manifest ?\n(Le ZIP reste dans builds/, supprime-le en SFTP si voulu.)')">
                        <?= Layout::csrfField() ?>
                        <input type="hidden" name="action" value="delete">
                        <input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
                        <button class="btn btn-danger" type="submit">Retirer</button>
                    </form>
                </td>
            </tr>
        <?php endforeach; ?>
        <?php if (empty($manifest['versions'])): ?>
            <tr><td colspan="8" class="muted" style="text-align:center; padding: 32px;">Aucune version dans le manifest.</td></tr>
        <?php endif; ?>
        </tbody>
    </table>
</div>

<div class="card">
    <h2>ZIPs présents dans builds/</h2>
    <?php if (empty($zips)): ?>
        <p class="muted">Aucun ZIP. Upload tes fichiers via SFTP dans <code>www/PS_Launcher/builds/</code>, puis clique « Sync » ci-dessus.</p>
    <?php else: ?>
        <table>
            <thead><tr><th>Fichier</th><th>Taille</th><th>Référencé ?</th></tr></thead>
            <tbody>
            <?php
            $referencedNames = [];
            foreach ($manifest['versions'] ?? [] as $v) {
                $referencedNames[] = basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '');
            }
            foreach ($zips as $z):
                $referenced = in_array($z, $referencedNames, true);
            ?>
                <tr>
                    <td><code><?= htmlspecialchars($z) ?></code></td>
                    <td class="muted"><?= Layout::formatBytes($zipSizes[$z]) ?></td>
                    <td><?= $referenced
                        ? "<span class='badge badge-success'>oui</span>"
                        : "<span class='badge badge-warning'>orphelin</span>" ?></td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>
</div>
<?php Layout::footer();
