<?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/playback_failures.php';

ini_set('display_errors', 0);
error_reporting(E_ALL);

function streamFailureTypeForStatus($statusCode) {
    $statusCode = (int)$statusCode;
    if ($statusCode === 400) return 'invalid_id';
    if ($statusCode === 403) return 'forbidden_path';
    if ($statusCode === 404) return 'not_found';
    if ($statusCode === 416) return 'range_error';
    return 'stream_error';
}

function streamError($statusCode, $message, $context = []) {
    $failureSongId = isset($context['song_id']) ? (int)$context['song_id'] : (isset($GLOBALS['id']) ? (int)$GLOBALS['id'] : 0);
    $failureType = isset($context['failure_type']) ? (string)$context['failure_type'] : streamFailureTypeForStatus($statusCode);
    recordPlaybackFailure($failureSongId, $failureType, 'stream', [
        'http_status' => (int)$statusCode,
        'context' => $message,
    ]);

    if (function_exists('writeAppLog')) {
        try {
            $level = (int)$statusCode >= 500 ? 'error' : 'warning';
            writeAppLog($level, 'stream_error', $message, [
                'song_id' => $failureSongId,
                'failure_type' => $failureType,
                'http_status' => (int)$statusCode,
            ]);
        } catch (Throwable $e) {
        }
    }

    while (ob_get_level()) {
        @ob_end_clean();
    }
    http_response_code((int)$statusCode);
    if (!headers_sent()) {
        header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
        header('Pragma: no-cache');
        header('Expires: 0');
        header('Vary: Cookie');
        header('Content-Type: text/plain; charset=utf-8');
        header('X-Content-Type-Options: nosniff');
    }
    echo ensureUtf8String($message);
    exit;
}

function previewSecondsSetting() {
    $previewSeconds = (int)getSetting('guest_preview_duration', '30');
    if ($previewSeconds <= 0) $previewSeconds = 30;
    return $previewSeconds;
}

function previewByteLimit($song, $fileSize, $previewSeconds = null) {
    $previewSeconds = $previewSeconds !== null ? (int)$previewSeconds : previewSecondsSetting();
    $bitrate = (int)($song['bitrate'] ?? 0);
    if ($bitrate > 0) {
        $bitsPerSecond = $bitrate < 10000 ? $bitrate * 1000 : $bitrate;
        $bytes = (int)ceil(($bitsPerSecond / 8) * $previewSeconds);
        return max(256 * 1024, min($fileSize, $bytes + (96 * 1024)));
    }
    $ext = strtolower((string)($song['extension'] ?? ''));
    $fallbackByExt = [
        'mp3' => 1024 * 1024,
        'm4a' => 1024 * 1024,
        'aac' => 1024 * 1024,
        'ogg' => 1024 * 1024,
        'oga' => 1024 * 1024,
        'wav' => 6 * 1024 * 1024,
        'flac' => 6 * 1024 * 1024,
    ];
    return min($fileSize, $fallbackByExt[$ext] ?? (1024 * 1024));
}

$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
    streamError(400, '잘못된 음악 요청입니다.', ['song_id' => 0, 'failure_type' => 'invalid_id']);
}

try {
    $pdo = getDBConnection();
    $stmt = $pdo->prepare("SELECT file_path, extension, duration, bitrate FROM songs WHERE id = :id AND is_missing = 0");
    $stmt->execute([':id' => $id]);
    $song = $stmt->fetch();
    if (!$song) {
        streamError(404, '곡을 찾을 수 없거나 파일 없음 상태입니다.', ['song_id' => $id, 'failure_type' => 'not_found']);
    }

    $winFilePath = resolveFilesystemPath($song['file_path'], 'readable_file');
    if ($winFilePath === null) {
        $mark = $pdo->prepare("UPDATE songs SET is_missing = 1 WHERE id = :id");
        $mark->execute([':id' => $id]);
        streamError(404, '음악 파일을 찾을 수 없습니다.', ['song_id' => $id, 'failure_type' => 'not_found']);
    }

    $scanDirPath = resolveFilesystemPath(getScanPathSetting(), 'dir') ?? toWinPath(getScanPathSetting());
    $realFile = realpath($winFilePath) ?: $winFilePath;
    $realScanDir = $scanDirPath ? (realpath($scanDirPath) ?: $scanDirPath) : false;
    $normalizedFilePath = rtrim(strtolower(str_replace('\\', '/', toUtf8Path((string)$realFile))), '/');
    $normalizedScanDir = rtrim(strtolower(str_replace('\\', '/', toUtf8Path((string)$realScanDir))), '/');
    if (!$realFile || !$realScanDir || ($normalizedFilePath !== $normalizedScanDir && strpos($normalizedFilePath . '/', $normalizedScanDir . '/') !== 0)) {
        streamError(403, '허용되지 않은 파일 경로입니다.', ['song_id' => $id, 'failure_type' => 'forbidden_path']);
    }

    $fileSize = filesize($realFile);
    if ($fileSize === false || $fileSize <= 0) {
        streamError(416, '스트리밍할 수 없는 파일입니다.', ['song_id' => $id, 'failure_type' => 'empty_file']);
    }

    $ext = strtolower((string)$song['extension']);
    $mimeTypes = [
        'mp3' => 'audio/mpeg',
        'flac' => 'audio/flac',
        'wav' => 'audio/wav',
        'm4a' => 'audio/mp4',
        'mp4' => 'audio/mp4',
        'aac' => 'audio/aac',
        'ogg' => 'audio/ogg',
        'oga' => 'audio/ogg',
    ];
    $mimeType = $mimeTypes[$ext] ?? 'application/octet-stream';
    $guestPreviewEnabled = settingBool('guest_preview_enabled', true);
    $isPreview = !isLoggedIn() && $guestPreviewEnabled;
    if (session_status() === PHP_SESSION_ACTIVE) {
        session_write_close();
    }
    $previewSeconds = previewSecondsSetting();
    $streamSize = $isPreview ? previewByteLimit($song, $fileSize, $previewSeconds) : $fileSize;

    while (ob_get_level()) {
        ob_end_clean();
    }

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

    if (!empty($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/i', $_SERVER['HTTP_RANGE'], $matches)) {
        if ($matches[1] === '' && $matches[2] === '') {
            header("Content-Range: bytes */$streamSize");
            streamError(416, '잘못된 Range 요청입니다.', ['song_id' => $id, 'failure_type' => 'range_error']);
        }

        if ($matches[1] === '') {
            $suffixLength = (int)$matches[2];
            $start = max(0, $streamSize - $suffixLength);
        } else {
            $start = (int)$matches[1];
        }

        if ($matches[2] !== '') {
            $end = min((int)$matches[2], $streamSize - 1);
        }

        if ($start > $end || $start >= $streamSize) {
            header("Content-Range: bytes */$streamSize");
            streamError(416, '요청한 구간을 재생할 수 없습니다.', ['song_id' => $id, 'failure_type' => 'range_error']);
        }
        $status = 206;
    }

    $length = $end - $start + 1;
    http_response_code($status);
    header('Accept-Ranges: bytes');
    header('Content-Type: ' . $mimeType);
    header('Content-Length: ' . $length);
    if ($status === 206) {
        header('Content-Range: bytes ' . $start . '-' . $end . '/' . $streamSize);
    }
    header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');
    header('Vary: Cookie');
    if ($isPreview) {
        header('X-MelonLite-Preview: 1');
        header('X-MelonLite-Preview-Seconds: ' . $previewSeconds);
    }
    header('X-Content-Type-Options: nosniff');

    $fp = @fopen($realFile, 'rb');
    if (!$fp) {
        streamError(500, '파일을 열 수 없습니다.', ['song_id' => $id, 'failure_type' => 'open_failed']);
    }
    if (@fseek($fp, $start) !== 0) {
        fclose($fp);
        streamError(416, '요청한 구간을 재생할 수 없습니다.', ['song_id' => $id, 'failure_type' => 'range_error']);
    }

    $sent = 0;
    $bufferSize = 1024 * 256;
    while (!feof($fp) && $sent < $length && connection_status() === CONNECTION_NORMAL) {
        $read = min($bufferSize, $length - $sent);
        $data = @fread($fp, $read);
        if ($data === false || $data === '') break;
        $sent += strlen($data);
        echo $data;
        @flush();
    }
    fclose($fp);
    exit;
} catch (Throwable $e) {
    streamError(500, '스트리밍 오류가 발생했습니다.', ['song_id' => $id, 'failure_type' => 'stream_error']);
}
