<?php
declare(strict_types=1);

const SESSION_COOKIE_CANDIDATES = ['sidintranet', 'sidcms', 'mysyde', 'PHPSESSID', 'sidawo'];
const LOGIN_REDIRECT = '/';
const USERDATA_BASE_DIR = __DIR__ . '/../../userdata';
const USE_X_SENDFILE = true;
const ALLOW_PHP_STREAM_FALLBACK = true;
const MEDIA_CACHE_MAX_AGE = 3600;
const TRUSTED_PROXY_HEADER = true;
const STREAM_CHUNK_SIZE = 1048576;
const ENABLE_INLINE_SVG_FOR_IMAGE_REQUESTS = true;
const ENABLE_TEMP_SESSION_DEBUG = false;
// Optional hardening: e.g. ['icons/', 'logos/']; empty array allows all paths.
const INLINE_SVG_ALLOWED_PATH_PREFIXES = [];

// Main flow:
// 1. Start session (cookie hardening)
// 2. Enforce authentication
// 3. Validate + deliver requested file (X-Sendfile preferred, PHP fallback)
bootstrapSession();
maybeOutputSessionDebug();
requireAuthenticatedUser();
serveRequestedFile();

function bootstrapSession(): void
{
    ini_set('session.use_only_cookies', '1');
    ini_set('session.use_strict_mode', '1');

    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/',
        'secure' => isSecureConnection(),
        'httponly' => true,
        'samesite' => 'Lax',
    ]);

    $availableCookies = [];
    foreach (SESSION_COOKIE_CANDIDATES as $cookieName) {
        if (isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] !== '') {
            $availableCookies[] = $cookieName;
        }
    }

    if ($availableCookies === []) {
        session_start();
        return;
    }

    foreach ($availableCookies as $cookieName) {
        if (session_name() !== $cookieName) {
            session_name($cookieName);
        }
        session_id((string)$_COOKIE[$cookieName]);
        session_start();

        if (isCurrentSessionAuthenticated()) {
            return;
        }

        session_write_close();
    }

    // No authenticated session found: reopen the first available session.
    $fallbackCookie = $availableCookies[0];
    if (session_name() !== $fallbackCookie) {
        session_name($fallbackCookie);
    }
    session_id((string)$_COOKIE[$fallbackCookie]);

    session_start();
}

function requireAuthenticatedUser(): void
{
    $loginId = (int)($_SESSION['login_id'] ?? 0);
    $adminId = (int)(
        $_SESSION['id']
        ?? $_SESSION['main_admin_user_id']
        ?? $_SESSION['admin_user_id']
        ?? 0
    );
    $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? '');
    $hasAdminSid = (
        session_name() === 'sidcms'
        && $sidCmsCookie !== ''
        && (
            hash_equals($sidCmsCookie, session_id())
            || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? ''))
        )
    );
    $hasAdminAuthFlags = isAdminSessionFlagSet();

    if ($loginId <= 0 && !($hasAdminSid && ($adminId > 0 || $hasAdminAuthFlags))) {
        header('Location: ' . LOGIN_REDIRECT, true, 302);
        exit;
    }

    // Release session lock early so parallel media requests do not block.
    session_write_close();
}

function maybeOutputSessionDebug(): void
{
    if (!ENABLE_TEMP_SESSION_DEBUG) {
        return;
    }

    $debugFlag = strtolower(trim((string)($_GET['__debug_session'] ?? '0')));
    if (!in_array($debugFlag, ['1', 'true', 'yes'], true)) {
        return;
    }

    $sessionId = session_id();
    $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? '');
    $loginId = (int)($_SESSION['login_id'] ?? 0);
    $adminId = (int)(
        $_SESSION['id']
        ?? $_SESSION['main_admin_user_id']
        ?? $_SESSION['admin_user_id']
        ?? 0
    );

    $hasAdminSid = (
        session_name() === 'sidcms'
        && $sidCmsCookie !== ''
        && (
            hash_equals($sidCmsCookie, $sessionId)
            || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? ''))
        )
    );
    $hasAdminAuthFlags = isAdminSessionFlagSet();

    $payload = [
        'debug' => true,
        'session_name' => session_name(),
        'session_id' => $sessionId,
        'cookie_candidates_present' => array_values(array_filter(
            SESSION_COOKIE_CANDIDATES,
            static fn(string $name): bool => !empty($_COOKIE[$name])
        )),
        'auth_evaluation' => [
            'login_id' => $loginId,
            'admin_id' => $adminId,
            'has_admin_sid' => $hasAdminSid,
            'has_admin_auth_flags' => $hasAdminAuthFlags,
            'would_pass_auth' => ($loginId > 0) || ($hasAdminSid && ($adminId > 0 || $hasAdminAuthFlags)),
        ],
        'session' => $_SESSION,
        'cookies' => $_COOKIE,
        'request' => [
            'method' => (string)($_SERVER['REQUEST_METHOD'] ?? ''),
            'uri' => (string)($_SERVER['REQUEST_URI'] ?? ''),
            'remote_addr' => (string)($_SERVER['REMOTE_ADDR'] ?? ''),
        ],
    ];

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

    header('Content-Type: application/json; charset=UTF-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    echo json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function isCurrentSessionAuthenticated(): bool
{
    $loginId = (int)($_SESSION['login_id'] ?? 0);
    if ($loginId > 0) {
        return true;
    }

    if (session_name() !== 'sidcms') {
        return false;
    }

    $adminId = (int)(
        $_SESSION['id']
        ?? $_SESSION['main_admin_user_id']
        ?? $_SESSION['admin_user_id']
        ?? 0
    );
    if ($adminId <= 0 && !isAdminSessionFlagSet()) {
        return false;
    }

    $sidCmsCookie = (string)($_COOKIE['sidcms'] ?? '');
    if ($sidCmsCookie === '') {
        return false;
    }

    return hash_equals($sidCmsCookie, session_id())
        || hash_equals($sidCmsCookie, (string)($_SESSION['sidcms'] ?? ''));
}

function isAdminSessionFlagSet(): bool
{
    if (!empty($_SESSION['IsAuthorized'])) {
        return true;
    }

    if ((int)($_SESSION['session_login'] ?? 0) > 0) {
        return true;
    }

    if ((int)($_SESSION['admin_login'] ?? 0) > 0) {
        return true;
    }

    return false;
}

function serveRequestedFile(): void
{
    $requestMethod = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    if (!in_array($requestMethod, ['GET', 'HEAD'], true)) {
        header('Allow: GET, HEAD', true, 405);
        exit;
    }

    $relativePath = requestedRelativePath();
    if ($relativePath === '') {
        http_response_code(400);
        echo 'Bad Request';
        exit;
    }

    $baseDir = realpath(USERDATA_BASE_DIR);
    if ($baseDir === false || !is_dir($baseDir)) {
        http_response_code(500);
        echo 'Storage Not Configured';
        exit;
    }

    $resolvedPath = realpath($baseDir . DIRECTORY_SEPARATOR . $relativePath);
    if ($resolvedPath === false || !is_file($resolvedPath) || !is_readable($resolvedPath)) {
        http_response_code(404);
        echo 'Not Found';
        exit;
    }

    if (!isInsideBasePath($resolvedPath, $baseDir)) {
        http_response_code(403);
        echo 'Forbidden';
        exit;
    }

    deliverFile($resolvedPath, $relativePath, $requestMethod === 'HEAD');
}

function requestedRelativePath(): string
{
    // PHP already decodes $_GET values; do not decode again.
    $rawPath = (string)($_GET['file'] ?? '');
    $rawPath = trim(str_replace('\\', '/', $rawPath));
    $rawPath = ltrim($rawPath, '/');
    $rawPath = preg_replace('#/+#', '/', $rawPath) ?? '';

    if ($rawPath === '' || str_contains($rawPath, "\0")) {
        return '';
    }

    $segments = explode('/', $rawPath);
    foreach ($segments as $segment) {
        if ($segment === '' || $segment === '.' || $segment === '..') {
            return '';
        }
    }

    return $rawPath;
}

function isInsideBasePath(string $resolvedPath, string $baseDir): bool
{
    $base = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    return strncmp($resolvedPath, $base, strlen($base)) === 0;
}

function deliverFile(string $absolutePath, string $relativePath, bool $headOnly): void
{
    $fileSize = filesize($absolutePath);
    if ($fileSize === false || $fileSize < 0) {
        http_response_code(500);
        echo 'Unable to read file metadata';
        exit;
    }

    $lastModifiedTs = filemtime($absolutePath);
    if ($lastModifiedTs === false) {
        $lastModifiedTs = time();
    }

    $mimeType = detectSafeMimeType($absolutePath, $relativePath);
    $isInlineSvg = ($mimeType === 'image/svg+xml');
    $isMedia = isMediaMimeType($mimeType);
    $etag = buildEtag($absolutePath, $fileSize, $lastModifiedTs);

    $ifNoneMatch = trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''));
    if ($ifNoneMatch !== '' && requestEtagMatches($ifNoneMatch, $etag)) {
        sendNotModified($etag, $lastModifiedTs, $isMedia, $isInlineSvg);
    }

    $ifModifiedSince = trim((string)($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? ''));
    if ($ifNoneMatch === '' && $ifModifiedSince !== '' && requestNotModifiedSince($ifModifiedSince, $lastModifiedTs)) {
        sendNotModified($etag, $lastModifiedTs, $isMedia, $isInlineSvg);
    }

    $useXSendfile = shouldUseXSendfile();

    $rangeRequest = ['status' => 'none'];
    if ($isMedia && !$useXSendfile) {
        $rangeRequest = parseRangeHeader((string)($_SERVER['HTTP_RANGE'] ?? ''), $fileSize);
        if ($rangeRequest['status'] === 'invalid') {
            sendRangeNotSatisfiable($fileSize, $etag, $lastModifiedTs, $isMedia);
        }
    }

    sendSharedSecurityHeaders($isInlineSvg);
    sendCacheHeaders($isMedia);

    header('Content-Type: ' . $mimeType);
    header('ETag: "' . $etag . '"');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT');
    header('Content-Disposition: inline; filename="' . sanitizeFilenameForHeader(basename($absolutePath)) . '"');

    if ($isMedia) {
        header('Accept-Ranges: bytes');
    }

    if ($useXSendfile) {
        header('X-Sendfile: ' . $absolutePath);
        exit;
    }

    if ($rangeRequest['status'] !== 'none' || ALLOW_PHP_STREAM_FALLBACK || !USE_X_SENDFILE) {
        streamFileWithPhp($absolutePath, $fileSize, $headOnly, $rangeRequest);
    }

    // X-Sendfile requested, but not available and fallback disabled.
    http_response_code(503);
    echo 'File delivery backend unavailable';
    exit;
}

function sendSharedSecurityHeaders(bool $isInlineSvg = false): void
{
    if ($isInlineSvg) {
        // Inline SVG only for image requests with a strict CSP.
        header("Content-Security-Policy: default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'none'; sandbox");
        header('X-Content-Type-Options: nosniff');
        return;
    }

    header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox");
    header('X-Content-Type-Options: nosniff');
}

function sendCacheHeaders(bool $isMedia): void
{
    if ($isMedia) {
        header('Cache-Control: private, max-age=' . (string)MEDIA_CACHE_MAX_AGE . ', must-revalidate');
        return;
    }

    header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');
}

function sendNotModified(string $etag, int $lastModifiedTs, bool $isMedia, bool $isInlineSvg = false): void
{
    http_response_code(304);
    sendSharedSecurityHeaders($isInlineSvg);
    sendCacheHeaders($isMedia);
    header('ETag: "' . $etag . '"');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT');
    exit;
}

function sendRangeNotSatisfiable(int $fileSize, string $etag, int $lastModifiedTs, bool $isMedia): void
{
    http_response_code(416);
    sendSharedSecurityHeaders();
    sendCacheHeaders($isMedia);
    header('ETag: "' . $etag . '"');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTs) . ' GMT');
    header('Content-Range: bytes */' . (string)$fileSize);
    exit;
}

function shouldUseXSendfile(): bool
{
    if (!USE_X_SENDFILE) {
        return false;
    }

    if (!function_exists('apache_get_modules')) {
        return false;
    }

    $modules = apache_get_modules();
    return in_array('mod_xsendfile', $modules, true) || in_array('mod_xsendfile.c', $modules, true);
}

function streamFileWithPhp(string $absolutePath, int $fileSize, bool $headOnly, array $rangeRequest): void
{
    $start = 0;
    $end = $fileSize > 0 ? $fileSize - 1 : 0;
    $statusCode = 200;

    if ($rangeRequest['status'] === 'range') {
        $start = $rangeRequest['start'];
        $end = $rangeRequest['end'];
        $statusCode = 206;
        header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
    }

    $length = ($fileSize > 0) ? ($end - $start + 1) : 0;

    http_response_code($statusCode);
    header('Content-Length: ' . (string)$length);

    if ($headOnly || $length <= 0) {
        exit;
    }

    $handle = fopen($absolutePath, 'rb');
    if ($handle === false) {
        http_response_code(500);
        echo 'Unable to open file stream';
        exit;
    }

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

    if ($start > 0) {
        fseek($handle, $start);
    }

    $bytesRemaining = $length;
    while ($bytesRemaining > 0 && !feof($handle)) {
        $chunkSize = (int)min(STREAM_CHUNK_SIZE, $bytesRemaining);
        $buffer = fread($handle, $chunkSize);
        if ($buffer === false || $buffer === '') {
            break;
        }

        echo $buffer;
        flush();
        $bytesRemaining -= strlen($buffer);
    }

    fclose($handle);
    exit;
}

function parseRangeHeader(string $rangeHeader, int $fileSize): array
{
    $rangeHeader = trim($rangeHeader);
    if ($rangeHeader === '') {
        return ['status' => 'none'];
    }

    if ($fileSize <= 0 || !str_starts_with($rangeHeader, 'bytes=')) {
        return ['status' => 'invalid'];
    }

    $rangeValue = trim(substr($rangeHeader, 6));
    if ($rangeValue === '' || str_contains($rangeValue, ',')) {
        return ['status' => 'invalid'];
    }

    if (!preg_match('/^(\d*)-(\d*)$/', $rangeValue, $matches)) {
        return ['status' => 'invalid'];
    }

    $startRaw = $matches[1];
    $endRaw = $matches[2];

    if ($startRaw === '' && $endRaw === '') {
        return ['status' => 'invalid'];
    }

    if ($startRaw === '') {
        $suffixLength = (int)$endRaw;
        if ($suffixLength <= 0) {
            return ['status' => 'invalid'];
        }

        $suffixLength = min($suffixLength, $fileSize);
        $start = $fileSize - $suffixLength;
        $end = $fileSize - 1;

        return ['status' => 'range', 'start' => $start, 'end' => $end];
    }

    $start = (int)$startRaw;
    if ($start >= $fileSize) {
        return ['status' => 'invalid'];
    }

    if ($endRaw === '') {
        $end = $fileSize - 1;
    } else {
        $end = (int)$endRaw;
        if ($end < $start) {
            return ['status' => 'invalid'];
        }
        $end = min($end, $fileSize - 1);
    }

    return ['status' => 'range', 'start' => $start, 'end' => $end];
}

function requestEtagMatches(string $ifNoneMatchHeader, string $etag): bool
{
    $ifNoneMatchHeader = trim($ifNoneMatchHeader);
    if ($ifNoneMatchHeader === '*') {
        return true;
    }

    $parts = explode(',', $ifNoneMatchHeader);
    foreach ($parts as $part) {
        $candidate = trim($part);
        if ($candidate === '') {
            continue;
        }

        if (str_starts_with($candidate, 'W/')) {
            $candidate = substr($candidate, 2);
        }

        $candidate = trim($candidate, " \t\n\r\0\x0B\"");
        if ($candidate === $etag) {
            return true;
        }
    }

    return false;
}

function requestNotModifiedSince(string $ifModifiedSinceHeader, int $lastModifiedTs): bool
{
    $requestTs = strtotime($ifModifiedSinceHeader);
    if ($requestTs === false) {
        return false;
    }

    return $lastModifiedTs <= $requestTs;
}

function buildEtag(string $absolutePath, int $fileSize, int $lastModifiedTs): string
{
    return sha1($absolutePath . '|' . $fileSize . '|' . $lastModifiedTs);
}

function isMediaMimeType(string $mimeType): bool
{
    $normalized = strtolower(trim($mimeType));
    if ($normalized === '') {
        return false;
    }

    if (str_starts_with($normalized, 'image/') || str_starts_with($normalized, 'video/') || str_starts_with($normalized, 'audio/')) {
        return true;
    }

    return in_array($normalized, [
        'application/dash+xml',
        'application/x-mpegurl',
        'application/vnd.apple.mpegurl',
        'vnd.apple.mpegurl',
    ], true);
}

function detectSafeMimeType(string $absolutePath, string $relativePath): string
{
    $mimeType = 'application/octet-stream';

    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if ($finfo !== false) {
        $detected = finfo_file($finfo, $absolutePath);
        finfo_close($finfo);

        if (is_string($detected) && $detected !== '') {
            $mimeType = $detected;
        }
    }

    $dangerousMimeTypes = [
        'text/html',
        'application/xhtml+xml',
        'image/svg+xml',
        'text/xml',
        'application/xml',
        'text/xsl',
        'application/javascript',
        'text/javascript',
    ];

    if ($mimeType === 'image/svg+xml' && shouldServeSvgInline($relativePath)) {
        return $mimeType;
    }

    if (in_array($mimeType, $dangerousMimeTypes, true)) {
        return 'application/octet-stream';
    }

    return $mimeType;
}

function shouldServeSvgInline(string $relativePath): bool
{
    if (!ENABLE_INLINE_SVG_FOR_IMAGE_REQUESTS) {
        return false;
    }

    $fetchDest = strtolower(trim((string)($_SERVER['HTTP_SEC_FETCH_DEST'] ?? '')));
    if ($fetchDest !== '' && $fetchDest !== 'image') {
        return false;
    }

    if ($fetchDest === '') {
        $accept = strtolower(trim((string)($_SERVER['HTTP_ACCEPT'] ?? '')));
        if ($accept === '' || !str_contains($accept, 'image/')) {
            return false;
        }
    }

    $normalizedPath = strtolower(ltrim(str_replace('\\', '/', $relativePath), '/'));
    if ($normalizedPath === '') {
        return false;
    }

    if (INLINE_SVG_ALLOWED_PATH_PREFIXES === []) {
        return true;
    }

    foreach (INLINE_SVG_ALLOWED_PATH_PREFIXES as $prefix) {
        $normalizedPrefix = strtolower(ltrim(str_replace('\\', '/', (string)$prefix), '/'));
        if ($normalizedPrefix === '') {
            continue;
        }

        $normalizedPrefix = rtrim($normalizedPrefix, '/') . '/';
        if (str_starts_with($normalizedPath, $normalizedPrefix)) {
            return true;
        }
    }

    return false;
}

function sanitizeFilenameForHeader(string $filename): string
{
    return preg_replace('/[^A-Za-z0-9._-]/', '_', $filename) ?? 'download';
}

function isSecureConnection(): bool
{
    $https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
    if ($https !== '' && $https !== 'off') {
        return true;
    }

    if ((int)($_SERVER['SERVER_PORT'] ?? 80) === 443) {
        return true;
    }

    if (TRUSTED_PROXY_HEADER) {
        $forwardedProto = strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
        return $forwardedProto === 'https';
    }

    return false;
}
