"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.videoStreamingRoutes = videoStreamingRoutes;
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const mime_types_1 = require("mime-types");
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const client_1 = require("@prisma/client");
const database_1 = require("../../../config/database");
const env_1 = require("../../../config/env");
const logger_1 = require("../../../utils/logger");
const roles_1 = require("../../../utils/roles");
/**
 * Check if the request is from an authenticated admin user.
 * Supports JWT from Authorization header or ?token= query parameter
 * (needed for <video src> and <img src> which can't send headers).
 */
async function isAdminRequest(request) {
    try {
        // Extract token from Authorization header (priority) or query param (fallback)
        let token;
        const authHeader = request.headers.authorization;
        if (authHeader?.startsWith('Bearer ')) {
            token = authHeader.substring(7);
        }
        else {
            const query = request.query;
            token = query.token;
        }
        if (!token)
            return false;
        // Verify JWT signature
        const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] });
        // Check admin role from token (multi-role aware)
        if (!(0, roles_1.hasAnyRole)(payload, roles_1.MEDIA_ROLES))
            return false;
        // Verify user is still active in DB
        const user = await database_1.prisma.user.findUnique({
            where: { id: payload.id },
            select: { status: true },
        });
        return user?.status === client_1.UserStatus.ACTIVE;
    }
    catch {
        return false;
    }
}
/**
 * Parse range header for video seeking
 * Example: "bytes=0-1024" or "bytes=1024-"
 */
function parseRange(range, fileSize) {
    const parts = range.replace(/bytes=/, '').split('-');
    const start = parseInt(parts[0], 10);
    const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
    if (isNaN(start) || isNaN(end) || start > end || end >= fileSize) {
        return null;
    }
    return { start, end };
}
/**
 * Get video file stats safely
 */
async function getFileStats(filePath) {
    return new Promise((resolve) => {
        (0, fs_1.stat)(filePath, (err, stats) => {
            if (err) {
                resolve(null);
            }
            else {
                resolve({ size: stats.size });
            }
        });
    });
}
async function videoStreamingRoutes(fastify) {
    /**
     * Stream video file with HTTP range support for seeking
     * GET /api/videos/:id/stream
     * Public endpoint (no auth) - videos are public by default
     */
    fastify.get('/:id/stream', async (request, reply) => {
        try {
            const videoId = parseInt(request.params.id);
            if (isNaN(videoId)) {
                return reply.code(400).send({ message: 'Invalid video ID' });
            }
            // Admin bypass: skip publication filter for authenticated admin users
            const admin = await isAdminRequest(request);
            const video = await database_1.prisma.video.findFirst({
                where: admin
                    ? { id: videoId }
                    : { id: videoId, isPublished: true, isLocked: false },
            });
            if (!video) {
                return reply.code(404).send({ message: 'Video not found or not published' });
            }
            // Security: Validate path doesn't contain traversal attempts
            if (video.path.includes('..') || video.filename.includes('..')) {
                logger_1.logger.warn(`Path traversal attempt detected: ${video.path}/${video.filename}`);
                return reply.code(403).send({ message: 'Access denied' });
            }
            // Construct full file path
            // Handle both new format (full path) and legacy format (directory only)
            const filePath = video.path.endsWith(video.filename)
                ? video.path
                : (0, path_1.join)(video.path, video.filename);
            // Check file exists
            try {
                await (0, promises_1.access)(filePath);
            }
            catch {
                logger_1.logger.error(`Video file not found on disk: ${filePath}`);
                return reply.code(404).send({ message: 'Video file not found' });
            }
            // Get file stats
            const stats = await getFileStats(filePath);
            if (!stats) {
                return reply.code(500).send({ message: 'Failed to read video file' });
            }
            const fileSize = stats.size;
            // Determine MIME type
            const mimeType = (0, mime_types_1.lookup)(video.filename) || 'video/mp4';
            // Handle range requests for seeking
            const rangeHeader = request.headers.range;
            if (rangeHeader) {
                const range = parseRange(rangeHeader, fileSize);
                if (!range) {
                    return reply.code(416).send({ message: 'Invalid range' });
                }
                const { start, end } = range;
                const contentLength = end - start + 1;
                // Set partial content headers
                reply.code(206);
                reply.header('Content-Range', `bytes ${start}-${end}/${fileSize}`);
                reply.header('Accept-Ranges', 'bytes');
                reply.header('Content-Length', contentLength);
                reply.header('Content-Type', mimeType);
                reply.header('Cache-Control', 'public, max-age=31536000'); // 1 year
                // Stream the requested range
                const stream = (0, fs_1.createReadStream)(filePath, { start, end });
                return reply.send(stream);
            }
            else {
                // No range header - stream entire file
                reply.header('Content-Length', fileSize);
                reply.header('Content-Type', mimeType);
                reply.header('Accept-Ranges', 'bytes');
                reply.header('Cache-Control', 'public, max-age=31536000'); // 1 year
                const stream = (0, fs_1.createReadStream)(filePath);
                return reply.send(stream);
            }
        }
        catch (error) {
            logger_1.logger.error('Video streaming error:', error);
            return reply.code(500).send({
                message: error instanceof Error ? error.message : 'Failed to stream video',
            });
        }
    });
    /**
     * Serve video thumbnail
     * GET /api/videos/:id/thumbnail
     * Public endpoint - returns thumbnail image or 404
     */
    fastify.get('/:id/thumbnail', async (request, reply) => {
        try {
            const videoId = parseInt(request.params.id);
            if (isNaN(videoId)) {
                return reply.code(400).send({ message: 'Invalid video ID' });
            }
            // Admin bypass: skip publication filter for authenticated admin users
            const admin = await isAdminRequest(request);
            const video = await database_1.prisma.video.findFirst({
                where: admin
                    ? { id: videoId }
                    : { id: videoId, isPublished: true, isLocked: false },
            });
            if (!video) {
                return reply.code(404).send({ message: 'Video not found or not published' });
            }
            // Check if thumbnail exists
            if (!video.thumbnailPath) {
                return reply.code(404).send({ message: 'Thumbnail not found' });
            }
            // Security: Validate path
            if (video.thumbnailPath.includes('..')) {
                logger_1.logger.warn(`Path traversal attempt detected: ${video.thumbnailPath}`);
                return reply.code(403).send({ message: 'Access denied' });
            }
            // Check file exists
            try {
                await (0, promises_1.access)(video.thumbnailPath);
            }
            catch {
                logger_1.logger.error(`Thumbnail file not found on disk: ${video.thumbnailPath}`);
                return reply.code(404).send({ message: 'Thumbnail file not found' });
            }
            // Determine MIME type
            const mimeType = (0, mime_types_1.lookup)(video.thumbnailPath) || 'image/jpeg';
            // Read and send thumbnail
            const thumbnailBuffer = await (0, promises_1.readFile)(video.thumbnailPath);
            reply.header('Content-Type', mimeType);
            reply.header('Content-Length', thumbnailBuffer.length);
            reply.header('Cache-Control', 'public, max-age=31536000'); // 1 year
            return reply.send(thumbnailBuffer);
        }
        catch (error) {
            logger_1.logger.error('Thumbnail serving error:', error);
            return reply.code(500).send({
                message: error instanceof Error ? error.message : 'Failed to serve thumbnail',
            });
        }
    });
    /**
     * Get public video metadata for embedding
     * GET /api/videos/:id/metadata
     * Public endpoint - returns essential metadata for video players
     */
    fastify.get('/:id/metadata', async (request, reply) => {
        try {
            const videoId = parseInt(request.params.id);
            if (isNaN(videoId)) {
                return reply.code(400).send({ message: 'Invalid video ID' });
            }
            // Admin bypass: skip publication filter for authenticated admin users
            const admin = await isAdminRequest(request);
            const video = await database_1.prisma.video.findFirst({
                where: admin
                    ? { id: videoId }
                    : { id: videoId, isPublished: true, isLocked: false },
            });
            if (!video) {
                return reply.code(404).send({ message: 'Video not found or not published' });
            }
            // Construct public URLs (use relative paths for nginx proxy routing)
            const streamUrl = `/media/videos/${video.id}/stream`;
            const thumbnailUrl = video.thumbnailPath
                ? `/media/videos/${video.id}/thumbnail`
                : null;
            // Return public metadata
            return {
                id: video.id,
                title: video.title || video.filename,
                durationSeconds: video.durationSeconds,
                width: video.width,
                height: video.height,
                orientation: video.orientation,
                hasAudio: video.hasAudio,
                quality: video.quality,
                streamUrl,
                thumbnailUrl,
                createdAt: video.createdAt,
            };
        }
        catch (error) {
            logger_1.logger.error('Metadata retrieval error:', error);
            return reply.code(500).send({
                message: error instanceof Error ? error.message : 'Failed to retrieve metadata',
            });
        }
    });
    /**
     * Health check for streaming routes
     */
    fastify.get('/stream/health', async () => {
        return {
            status: 'ok',
            service: 'video-streaming',
        };
    });
}
//# sourceMappingURL=video-streaming.routes.js.map