<?php
/**
 * Gardien des téléchargements de builds. Sert un .zip seulement si l'URL est
 * accompagnée d'une signature HMAC valide non expirée, signée par la même
 * clé que /api/download-url/{version}. Supporte les Range: requests pour la
 * reprise (dégradé : on utilise readfile + fseek manuellement).
 *
 * Pour les hébergements où on veut désactiver la protection (tests internes), il
 * suffit de retirer le RewriteRule du .htaccess voisin — Apache servira alors
 * directement le fichier statique avec son support natif des Range.
 */
declare(strict_types=1);

ini_set('display_errors', '0');
set_time_limit(0);
ignore_user_abort(true);

require __DIR__ . '/../api/lib/Crypto.php';
$config = require __DIR__ . '/../api/config.php';
$secret = $config['hmac_secret'] ?? '';

function deny(string $reason, int $code = 403): never
{
    http_response_code($code);
    header('Content-Type: text/plain; charset=utf-8');
    echo "Forbidden: $reason\n";
    exit;
}

$file = trim((string)($_GET['file'] ?? ''), '/');
if ($file === '' || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $file)) deny('invalid file');

$path = __DIR__ . '/' . $file;
if (!is_file($path)) deny('not found', 404);

$exp = (int)($_GET['exp'] ?? 0);
$lic = (string)($_GET['lic'] ?? '');
$sig = (string)($_GET['sig'] ?? '');

if ($exp <= 0 || $lic === '' || $sig === '') deny('missing params');
if ($exp < time()) deny('expired');
if ($secret === '') deny('server config error', 500);

$relPath  = '/builds/' . $file;
$expected = \PSLauncher\Crypto::hmacHex($relPath . '|' . $exp . '|' . $lic, $secret);
if (!hash_equals($expected, $sig)) deny('bad signature');

// Sert le fichier avec support Range
$size  = filesize($path);
$mtime = filemtime($path) ?: 0;
$start = 0;
$end   = $size - 1;

header('Content-Type: application/zip');
header('Accept-Ranges: bytes');
// ⚠️ NE PAS utiliser md5_file($path) ici : sur un fichier 14 Go, ça relit
// l'intégralité du ZIP à chaque requête (~30 s-5 min selon disque). Avec
// 8 segments parallèles + HEAD probe, ça multipliait par 9 le temps avant
// le premier byte. On utilise un ETag dérivé size+mtime, équivalent à ce
// qu'Apache fait nativement pour les fichiers statiques. Change quand on
// upload un nouveau ZIP, donc cache invalidation correcte.
header('ETag: "' . dechex($size) . '-' . dechex($mtime) . '"');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
header('Cache-Control: private, max-age=3600');

if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
    $start = (int)$m[1];
    if ($m[2] !== '') $end = min((int)$m[2], $size - 1);
    if ($start > $end || $start >= $size) {
        http_response_code(416);
        header("Content-Range: bytes */$size");
        exit;
    }
    http_response_code(206);
    header("Content-Range: bytes $start-$end/$size");
}

$length = $end - $start + 1;
header('Content-Length: ' . $length);

$fp = fopen($path, 'rb');
fseek($fp, $start);
$bufSize = 1 << 20; // 1 MiB
$remaining = $length;
while ($remaining > 0 && !feof($fp) && !connection_aborted()) {
    $chunk = fread($fp, (int)min($bufSize, $remaining));
    if ($chunk === false) break;
    echo $chunk;
    @ob_flush();
    @flush();
    $remaining -= strlen($chunk);
}
fclose($fp);
