<?php
declare(strict_types=1);

/**
 * functional/audio/stream.php
 * Відповідає за:
 * - стрімінг аудіо-файлів (тільки власнику)
 * - підтримка HTTP Range (перемотування)
 *
 * ВАЖЛИВО:
 * - не виводимо текст/HTML у відповіді
 * - коректний 206/416 для Range
 * - private/no-store (приватний контент)
 */

require_once __DIR__ . '/_boot.php';

/* Allow only GET/HEAD */
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
if (!in_array($method, ['GET', 'HEAD'], true)) {
    http_response_code(405);
    header('Allow: GET, HEAD');
    exit;
}

/* прибираємо будь-який буфер, щоб Range не ламався */
while (ob_get_level() > 0) { @ob_end_clean(); }

/* Disable output compression for streaming */
@ini_set('zlib.output_compression', '0');
@ini_set('output_buffering', '0');

/* do not lock session during streaming */
if (session_status() === PHP_SESSION_ACTIVE) {
    @session_write_close();
}

@set_time_limit(0);
@ignore_user_abort(true);

$id = function_exists('get_int') ? get_int('id', 0) : (int)($_GET['id'] ?? 0);
if ($id <= 0) {
    http_response_code(404);
    exit;
}

$userId = (int)($me['id'] ?? 0);
if ($userId <= 0) {
    http_response_code(404);
    exit;
}

/* мінімальний SELECT */
$st = $pdo->prepare("SELECT rel_path, mime, title FROM audio_tracks WHERE id=? AND user_id=? LIMIT 1");
$st->execute([$id, $userId]);
$t = $st->fetch(PDO::FETCH_ASSOC);

if (!$t) {
    http_response_code(404);
    exit;
}

$rel = (string)($t['rel_path'] ?? '');
$rel = '/' . ltrim($rel, '/');
if ($rel === '/' || $rel === '') {
    http_response_code(404);
    exit;
}

/* ===== strict rel-path: only inside user's music dir ===== */
$expectedPrefix = audio_user_music_dir_rel($me);
$expectedPrefix = '/' . trim($expectedPrefix, '/');
if (!str_starts_with($rel, $expectedPrefix . '/')) {
    // якщо хочеш не-строго — скажи, але для безпеки краще так
    http_response_code(404);
    exit;
}

/* absolute + realpath */
$abs = audio_rel_to_abs($rel);
$absReal = realpath($abs);

if (!$absReal || !is_file($absReal) || !is_readable($absReal)) {
    http_response_code(404);
    exit;
}

/* ===== path safety: файл має лежати в /upload (інакше відмова) ===== */
$audioRoot = realpath(audio_storage_root_abs());
if (!$audioRoot) {
    http_response_code(404);
    exit;
}
$audioRoot = rtrim(str_replace('\\', '/', $audioRoot), '/') . '/';
$absNorm = str_replace('\\', '/', $absReal);

if (strpos($absNorm, $audioRoot) !== 0) {
    http_response_code(404);
    exit;
}

$size = (int)@filesize($absReal);
if ($size <= 0) {
    http_response_code(404);
    exit;
}

/* metadata */
$mtime = (int)@filemtime($absReal);
$lastModified = $mtime > 0 ? gmdate('D, d M Y H:i:s', $mtime) . ' GMT' : gmdate('D, d M Y H:i:s') . ' GMT';
$etag = '"' . sha1($absNorm . '|' . $size . '|' . $mtime) . '"';

/* Determine MIME (prefer finfo) */
$mime = (string)($t['mime'] ?? '');
if (function_exists('finfo_open')) {
    try {
        $fi = finfo_open(FILEINFO_MIME_TYPE);
        if ($fi) {
            $m = finfo_file($fi, $absReal);
            finfo_close($fi);
            if (is_string($m) && $m !== '') $mime = $m;
        }
    } catch (Throwable $e) {}
}
if ($mime === '') $mime = 'application/octet-stream';

/* filename */
$baseName = (string)($t['title'] ?? '');
$baseName = trim(preg_replace('/\s+/u', ' ', $baseName) ?? $baseName);
if ($baseName === '') $baseName = 'track_' . $id;
$baseName = preg_replace('~[^\pL\pN\-\._ ]+~u', '_', $baseName) ?? $baseName;
$baseName = trim($baseName, " .\t\n\r\0\x0B");
if ($baseName === '') $baseName = 'track_' . $id;

$ext = strtolower((string)pathinfo($absReal, PATHINFO_EXTENSION));
$ext = preg_replace('~[^a-z0-9]+~', '', $ext) ?? '';
if ($ext === '') {
    // minimal mime->ext fallback
    $map = [
        'audio/mpeg' => 'mp3',
        'audio/mp3' => 'mp3',
        'audio/wav' => 'wav',
        'audio/x-wav' => 'wav',
        'audio/ogg' => 'ogg',
        'audio/aac' => 'aac',
        'audio/mp4' => 'm4a',
        'audio/x-m4a' => 'm4a',
        'audio/flac' => 'flac',
    ];
    $ext = $map[strtolower($mime)] ?? 'bin';
}
$downloadName = $baseName . '.' . $ext;

/* headers */
header('Content-Type: ' . $mime);
header('Accept-Ranges: bytes');
header('Content-Encoding: identity');
header('X-Content-Type-Options: nosniff');
header('X-Accel-Buffering: no');

header('ETag: ' . $etag);
header('Last-Modified: ' . $lastModified);

/* приватність / кеш */
header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');

/* inline disposition */
header("Content-Disposition: inline; filename=\"" . str_replace('"', '', $downloadName) . "\"");

/* 304 support (only when no Range) */
$rangeHeader = (string)($_SERVER['HTTP_RANGE'] ?? '');
$ifNoneMatch = (string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? '');
$ifModifiedSince = (string)($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '');

if ($rangeHeader === '') {
    if ($ifNoneMatch !== '' && trim($ifNoneMatch) === $etag) {
        http_response_code(304);
        exit;
    }
    if ($ifModifiedSince !== '' && $mtime > 0) {
        $ims = strtotime($ifModifiedSince);
        if ($ims !== false && $ims >= $mtime) {
            http_response_code(304);
            exit;
        }
    }
}

/* If-Range: if mismatch -> ignore Range and send full */
$ifRange = (string)($_SERVER['HTTP_IF_RANGE'] ?? '');
if ($rangeHeader !== '' && $ifRange !== '') {
    $ifRange = trim($ifRange);
    $ok = false;

    // ETag form
    if ($ifRange === $etag) {
        $ok = true;
    } else {
        // Date form
        $ir = strtotime($ifRange);
        if ($ir !== false && $mtime > 0 && $ir >= $mtime) $ok = true;
    }

    if (!$ok) {
        $rangeHeader = '';
    }
}

/* Range parsing */
$start = 0;
$end   = $size - 1;
$send206 = false;

if ($rangeHeader !== '') {
    // multi-range not supported (reject)
    if (strpos($rangeHeader, ',') !== false) {
        http_response_code(416);
        header("Content-Range: bytes */{$size}");
        exit;
    }

    if (preg_match('~bytes=(\d*)-(\d*)~i', $rangeHeader, $m)) {
        $rStart = ($m[1] !== '') ? (int)$m[1] : null;
        $rEnd   = ($m[2] !== '') ? (int)$m[2] : null;

        if ($rStart === null && $rEnd !== null) {
            // bytes=-500 => last 500 bytes
            if ($rEnd <= 0) {
                http_response_code(416);
                header("Content-Range: bytes */{$size}");
                exit;
            }
            $rStart = max(0, $size - $rEnd);
            $rEnd   = $size - 1;
        } elseif ($rStart !== null && $rEnd === null) {
            // bytes=500-
            $rEnd = $size - 1;
        }

        if ($rStart === null || $rStart < 0 || $rStart >= $size) {
            http_response_code(416);
            header("Content-Range: bytes */{$size}");
            exit;
        }
        if ($rEnd === null) $rEnd = $size - 1;
        if ($rEnd < $rStart) {
            http_response_code(416);
            header("Content-Range: bytes */{$size}");
            exit;
        }

        $start = $rStart;
        $end   = min($size - 1, $rEnd);
        $send206 = true;
    }
}

$length = ($end - $start) + 1;

$fp = @fopen($absReal, 'rb');
if (!$fp) {
    http_response_code(500);
    exit;
}

if ($send206) {
    http_response_code(206);
    header("Content-Range: bytes {$start}-{$end}/{$size}");
    header('Content-Length: ' . (string)$length);
    @fseek($fp, $start);
} else {
    header('Content-Length: ' . (string)$size);
}

if ($method === 'HEAD') {
    fclose($fp);
    exit;
}

/* stream */
$chunk = 1024 * 256; // 256KB
$left = $length;

while ($left > 0 && !feof($fp)) {
    if (connection_aborted()) break;

    $read = ($left > $chunk) ? $chunk : $left;
    $buf = fread($fp, $read);
    if ($buf === false || $buf === '') break;

    echo $buf;
    $left -= strlen($buf);

    @flush();
}

fclose($fp);
exit;