<?php

/**
 * MediaController
 *
 * Serves uploaded files stored outside the webroot (STORAGE_ROOT).
 * This is the sole access point for thesis files, covers, and annexes — they
 * are never exposed as direct filesystem paths from the web server.
 *
 * Security:
 *   - Strict character whitelist on the path parameter (no path traversal)
 *   - realpath() jail: resolved path must stay inside STORAGE_ROOT
 *   - MIME type verified against an allow-list before serving
 *   - Access-type gate for thesis files (blocks 'Interdit' access_type_id=3)
 */
class MediaController
{
    /**
     * Handle a media request. Reads $_GET['path'], validates, and streams the file.
     * Sends appropriate headers and exit() — no return value.
     */
    public function handle(): void
    {
        $requestedPath = $_GET['path'] ?? '';

        // 1. Validate path characters
        if (!preg_match('#^[a-zA-Z0-9/_\-.]+$#', $requestedPath) || $requestedPath === '') {
            http_response_code(400);
            exit;
        }

        // 2. Resolve path + storage jail
        $storageRoot = defined('STORAGE_ROOT') ? STORAGE_ROOT : '/var/www/xamxam/storage';
        $fullPath    = $storageRoot . '/' . $requestedPath;

        $realStorage = realpath($storageRoot);
        $realFull    = realpath($fullPath);

        if (
            $realFull === false
            || $realStorage === false
            || strpos($realFull, $realStorage . '/') !== 0
        ) {
            http_response_code(404);
            exit;
        }

        if (!is_file($realFull)) {
            http_response_code(404);
            exit;
        }

        // 3. Visibility gate for thesis files
        if (preg_match('#^theses/#', $requestedPath)) {
            require_once APP_ROOT . '/src/Database.php';
            require_once APP_ROOT . '/src/ErrorHandler.php';
            try {
                $mediaDb     = Database::getInstance();
                $accessTypeId = $mediaDb->getFileVisibility($requestedPath);
                if ($accessTypeId !== null && $accessTypeId === 3) {
                    http_response_code(403);
                    exit;
                }
            } catch (\Throwable $e) {
                ErrorHandler::log('media_visibility', $e, ['path' => $path]);
            }
        }

        // 4. Verify MIME type
        $finfo    = new finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->file($realFull);

        // finfo may return 'text/plain' for WebVTT files on some systems;
        // re-classify by extension so we don't block them.
        $ext = strtolower(pathinfo($realFull, PATHINFO_EXTENSION));
        if ($mimeType === 'text/plain' && $ext === 'vtt') {
            $mimeType = 'text/vtt';
        }
        // finfo may return application/octet-stream for valid downloadable files
        // that have known extensions — allow them through.
        $knownDownloadExts = ['zip','tar','gz','tgz','mp3','ogg','oga','wav','flac','aac','m4a',
                              'webm','ogv','mov','gif','webp','pdf','vtt'];

        $allowedMimes = [
            // Images
            'image/jpeg', 'image/png', 'image/gif', 'image/webp',
            // Documents
            'application/pdf',
            // Video
            'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime',
            // Audio
            'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav',
            'audio/flac', 'audio/aac', 'audio/x-m4a', 'audio/mp4',
            // Captions
            'text/vtt',
            // Archives
            'application/zip', 'application/x-zip-compressed',
            'application/x-tar', 'application/gzip',
            // Generic binary (allowed when ext is known)
            'application/octet-stream',
        ];

        $isAllowed = in_array($mimeType, $allowedMimes, true)
                  || in_array($ext, $knownDownloadExts, true);

        if (!$isAllowed) {
            http_response_code(403);
            exit;
        }

        // 5. Determine if download was explicitly requested
        $forceDownload = !empty($_GET['download']) && $_GET['download'] === '1';

        // 6. Send response headers
        header('Content-Type: ' . $mimeType);
        header('Content-Length: ' . (int) filesize($realFull));
        header('X-Content-Type-Options: nosniff');

        if ($ext === 'vtt') {
            header('Content-Type: text/vtt; charset=utf-8');
            header('Cache-Control: public, max-age=86400');
        } elseif (in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
            header('Cache-Control: public, max-age=604800');
            if (!$forceDownload) {
                header('Content-Disposition: inline');
            }
        } elseif ($ext === 'pdf') {
            header('Cache-Control: public, max-age=86400');
            header('Content-Disposition: ' . ($forceDownload ? 'attachment' : 'inline'));
        } elseif (in_array($ext, ['mp4','webm','ogv','mov'], true)) {
            // Video: no cache-control range requests should work
            header('Accept-Ranges: bytes');
            header('Cache-Control: public, max-age=86400');
        } elseif (in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
            header('Accept-Ranges: bytes');
            header('Cache-Control: public, max-age=86400');
        } else {
            // Unknown / other: force download
            $safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', basename($realFull));
            header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
            header('Cache-Control: private, no-store');
        }

        // 7. Stream file (with range support for media)
        if (in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
            $this->streamWithRange($realFull, $mimeType);
        } else {
            readfile($realFull);
        }
    }

    /**
     * Stream a file with HTTP Range support (required for HTML5 audio/video seeking).
     */
    private function streamWithRange(string $path, string $mimeType): void
    {
        $size  = (int) filesize($path);
        $start = 0;
        $end   = $size - 1;

        if (isset($_SERVER['HTTP_RANGE'])) {
            $range = $_SERVER['HTTP_RANGE'];
            if (!preg_match('/bytes=\d*-\d*/', $range)) {
                header('HTTP/1.1 416 Range Not Satisfiable');
                header('Content-Range: bytes */' . $size);
                exit;
            }
            [, $range] = explode('=', $range, 2);
            [$start, $end] = array_pad(explode('-', $range, 2), 2, '');
            $start = ($start === '') ? 0 : (int)$start;
            $end   = ($end   === '') ? $size - 1 : (int)$end;
            if ($end >= $size) {
                $end = $size - 1;
            }
            if ($start > $end) {
                http_response_code(416);
                exit;
            }

            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
            header('Content-Length: ' . ($end - $start + 1));
        } else {
            header('Content-Length: ' . $size);
        }

        $fp = fopen($path, 'rb');
        if ($fp === false) {
            http_response_code(500);
            exit;
        }
        fseek($fp, $start);
        $remaining = $end - $start + 1;
        while ($remaining > 0 && !feof($fp)) {
            $chunk = fread($fp, min(8192, $remaining));
            if ($chunk === false) {
                break;
            }
            echo $chunk;
            $remaining -= strlen($chunk);
        }
        fclose($fp);
    }
}
