<?php
/**
 * get.php — on-demand HDD -> SSD mp4 cache + server
 *
 * Flow:
 *   /files/xx.mp4
 *   -> nginx serves it statically if it's already on SSD (try_files)
 *   -> else this script copies xx.mp4 from HDD to SSD (atomic), then serves
 *   -> the HDD source is kept, NOT deleted
 *
 * Concurrency:
 *   - per-file lock  : same file never copied twice at once
 *   - global slots   : at most MAX_PARALLEL_COPIES copies running server-wide
 *   - lock order is ALWAYS file lock -> slot lock (never the reverse)
 *
 * NOTE: nginx already adds `Access-Control-Allow-Origin: *` globally, so we do
 * NOT set it on the success path here (would become a duplicate header).
 */

/* ================= CONFIG ================= */
const HDD_DIR             = '/ssd/hdd'; // slow storage: permanent mp4 home (no trailing slash)
const WAIT_TIMEOUT        = 600;        // max wait if another process is copying the SAME file (s)
const READ_CHUNK          = 1 << 20;    // 1 MB stream chunk

const MAX_PARALLEL_COPIES = 2;         // global cap on simultaneous HDD->SSD copies
                                        //   single CMR HDD : 2
                                        //   single SMR HDD : 1
                                        //   RAID / NVMe    : 8-16
const SLOT_TIMEOUT        = 600;        // max wait for a free copy slot (s)

define('BASE_DIR', __DIR__);
define('SSD_DIR',  BASE_DIR . '/files');   // fast cache — MUST live on the SSD
define('LOCK_DIR', BASE_DIR . '/locks');
/* ========================================== */

set_time_limit(0);
ignore_user_abort(true);

function fail(int $code, string $msg): void {
    http_response_code($code);
    header('Content-Type: text/plain; charset=utf-8');
    echo $msg;
    exit;
}

/** Serve an mp4 with HTTP Range support (seek / streaming). */
function serveMp4(string $path): void {
    $size = @filesize($path);
    if ($size === false) fail(500, 'Cannot stat file');

    $fp = @fopen($path, 'rb');
    if (!$fp) fail(500, 'Cannot open file');

    header('Content-Type: video/mp4');
    header('Accept-Ranges: bytes');

    $start = 0; $end = $size - 1; $status = 200;

    $range = $_SERVER['HTTP_RANGE'] ?? '';
    if ($range !== '' && preg_match('/^bytes=(\d*)-(\d*)$/', trim($range), $r)) {
        if ($r[1] === '' && $r[2] !== '') {
            $start = max(0, $size - (int)$r[2]);           // suffix range
        } elseif ($r[1] !== '') {
            $start = (int)$r[1];
            if ($r[2] !== '') $end = (int)$r[2];
        }
        $end = min($end, $size - 1);
        if ($start > $end || $start >= $size) {
            header('Content-Range: bytes */' . $size);
            http_response_code(416);
            fclose($fp);
            exit;
        }
        $status = 206;
        header("Content-Range: bytes $start-$end/$size");
    }

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

    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD') { fclose($fp); exit; }

    if ($start > 0) fseek($fp, $start);
    $remaining = $length;
    while ($remaining > 0 && !feof($fp)) {
        $buf = fread($fp, (int)min(READ_CHUNK, $remaining));
        if ($buf === false) break;
        echo $buf;
        flush();
        $remaining -= strlen($buf);
        if (connection_aborted()) break;
    }
    fclose($fp);
    exit;
}

/**
 * Grab one of the N global copy slots.
 * Returns the locked file handle, or null if $timeout seconds passed.
 */
function acquireSlot(int $n, int $timeout) {
    $deadline = time() + $timeout;
    do {
        for ($i = 0; $i < $n; $i++) {
            $fp = @fopen(LOCK_DIR . "/slot_$i.lock", 'c');
            if (!$fp) continue;
            if (flock($fp, LOCK_EX | LOCK_NB)) return $fp;   // got it
            fclose($fp);
        }
        usleep(200_000);   // 0.2s before retrying the whole ring
    } while (time() < $deadline);

    return null;
}

/** Release a lock handle (safe to call with null). */
function releaseLock($fp): void {
    if (!$fp) return;
    @flock($fp, LOCK_UN);
    @fclose($fp);
}

/* ---------- 1. parse + sanitize ---------- */
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
if (!preg_match('#^/files/([A-Za-z0-9_-]{1,128})\.mp4$#', $uri, $m)) {
    fail(404, 'Not found');
}
$id     = $m[1];
$target = SSD_DIR . '/' . $id . '.mp4';

/* ---------- 2. already cached? (normally nginx handles this) ---------- */
if (is_file($target)) serveMp4($target);

/* ---------- 3. dirs ---------- */
foreach ([SSD_DIR, LOCK_DIR] as $d) {
    if (!is_dir($d) && !@mkdir($d, 0755, true)) fail(500, 'Cannot create working dirs');
}

/* ---------- 4. per-file lock (no duplicate copies of the same id) ---------- */
$lockFp = fopen(LOCK_DIR . '/' . $id . '.lock', 'c');
if (!$lockFp) fail(500, 'Cannot open lock file');

if (!flock($lockFp, LOCK_EX | LOCK_NB)) {
    // someone else is already copying this exact file — just wait for it
    $deadline = time() + WAIT_TIMEOUT;
    while (time() < $deadline) {
        if (is_file($target)) serveMp4($target);
        sleep(1);
    }
    releaseLock($lockFp);
    fail(504, 'Copy is taking too long, try again');
}
if (is_file($target)) { releaseLock($lockFp); serveMp4($target); }

/* ---------- 5. source on HDD ---------- */
$src = HDD_DIR . '/' . $id . '.mp4';
if (!is_file($src)) { releaseLock($lockFp); fail(404, 'Video not found'); }

/* ---------- 6. global slot (throttle simultaneous copies) ---------- */
$slotFp = acquireSlot(MAX_PARALLEL_COPIES, SLOT_TIMEOUT);
if (!$slotFp) { releaseLock($lockFp); fail(503, 'Server busy, try again'); }

/* ---------- 7. copy HDD -> SSD (tmp then atomic rename) ---------- */
$tmp = SSD_DIR . '/.tmp_' . $id . '_' . getmypid() . '.mp4';
@unlink($tmp);

if (!@copy($src, $tmp)) {
    @unlink($tmp);
    releaseLock($slotFp);
    releaseLock($lockFp);
    fail(500, 'Copy to SSD failed (space/perm?)');
}
if (!@rename($tmp, $target)) {
    @unlink($tmp);
    releaseLock($slotFp);
    releaseLock($lockFp);
    fail(500, 'Move into place failed');
}

@touch(SSD_DIR . '/' . $id . '.mp4.last');

releaseLock($slotFp);   // free the slot as early as possible
releaseLock($lockFp);

/* ---------- 8. serve ---------- */
serveMp4($target);