import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { createReadStream, stat } from 'fs';
import { access, readFile } from 'fs/promises';
import { join, resolve } from 'path';
import { lookup } from 'mime-types';
import jwt from 'jsonwebtoken';
import { UserRole, UserStatus } from '@prisma/client';
import { prisma } from '../../../config/database';
import { env } from '../../../config/env';
import { logger } from '../../../utils/logger';
import { hasAnyRole, MEDIA_ROLES, getUserRoles } from '../../../utils/roles';
import { signMediaPath, verifyMediaSignature } from '../../../utils/signed-url';

/**
 * Check if the request is from an authenticated admin user.
 * Accepts either (1) Bearer JWT or (2) path-scoped signed URL params
 * (`?sig=&exp=&uid=`). The legacy `?token=<JWT>` path was removed on
 * 2026-04-12 — full JWTs in query strings were leaking via access logs
 * and referer headers.
 */
async function isAdminRequest(request: FastifyRequest): Promise<boolean> {
  try {
    let userId: string | undefined;

    const authHeader = request.headers.authorization;
    const query = request.query as Record<string, string | undefined>;

    if (authHeader?.startsWith('Bearer ')) {
      const payload = jwt.verify(authHeader.substring(7), env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] }) as {
        id: string; role: UserRole; roles?: UserRole[];
      };
      if (!hasAnyRole(payload, MEDIA_ROLES)) return false;
      userId = payload.id;
    } else if (query.sig && query.exp && query.uid) {
      const result = verifyMediaSignature(request.url, query);
      if (!result.valid) return false;
      userId = result.userId;
    }

    if (!userId) return false;

    // Verify user still active AND has media role (signed URLs carry only uid,
    // so we re-check the role from DB to avoid stale-role privilege escalation).
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { status: true, role: true, roles: true },
    });
    if (!user || user.status !== UserStatus.ACTIVE) return false;
    return hasAnyRole({ role: user.role as UserRole, roles: getUserRoles(user) }, MEDIA_ROLES);
  } catch {
    return false;
  }
}

/**
 * Parse range header for video seeking
 * Example: "bytes=0-1024" or "bytes=1024-"
 */
function parseRange(range: string, fileSize: number): { start: number; end: number } | null {
  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: string): Promise<{ size: number } | null> {
  return new Promise((resolve) => {
    stat(filePath, (err, stats) => {
      if (err) {
        resolve(null);
      } else {
        resolve({ size: stats.size });
      }
    });
  });
}

export async function videoStreamingRoutes(fastify: FastifyInstance) {
  /**
   * 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: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
      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 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 is within allowed media directory
        const MEDIA_BASE = '/media/local';
        const candidatePath = video.path.endsWith(video.filename)
          ? video.path
          : join(video.path, video.filename);
        const filePath = resolve(candidatePath);
        if (!filePath.startsWith(resolve(MEDIA_BASE))) {
          logger.warn(`Path traversal attempt detected: ${filePath}`);
          return reply.code(403).send({ message: 'Access denied' });
        }

        // Check file exists
        try {
          await access(filePath);
        } catch {
          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 = 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 = 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 = createReadStream(filePath);
          return reply.send(stream);
        }
      } catch (error) {
        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: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
      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 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 is within allowed media directory
        const resolvedThumb = resolve(video.thumbnailPath);
        if (!resolvedThumb.startsWith(resolve('/media/local'))) {
          logger.warn(`Path traversal attempt detected: ${resolvedThumb}`);
          return reply.code(403).send({ message: 'Access denied' });
        }

        // Check file exists
        try {
          await access(resolvedThumb);
        } catch {
          logger.error(`Thumbnail file not found on disk: ${resolvedThumb}`);
          return reply.code(404).send({ message: 'Thumbnail file not found' });
        }

        // Determine MIME type
        const mimeType = lookup(resolvedThumb) || 'image/jpeg';

        // Read and send thumbnail
        const thumbnailBuffer = await 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.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: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
      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 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;

        // HLS manifest URL — only present when transcoding has completed.
        // We emit browser-facing `/media/*` paths (rewritten by nginx to
        // `/api/*` and proxied to media-api). For admin previews we sign
        // against the post-rewrite server-side path so the verifier matches.
        let hlsManifestUrl: string | null = null;
        if (video.hlsStatus === 'READY' && video.hlsManifestPath) {
          const clientPath = admin
            ? `/media/videos/${video.id}/hls/master.m3u8`
            : `/media/public/${video.id}/hls/master.m3u8`;
          const serverPath = admin
            ? `/api/videos/${video.id}/hls/master.m3u8`
            : `/api/public/${video.id}/hls/master.m3u8`;
          if (admin) {
            let uid = 'admin';
            try {
              const authHeader = request.headers.authorization;
              if (authHeader?.startsWith('Bearer ')) {
                const payload = jwt.verify(
                  authHeader.substring(7),
                  env.JWT_ACCESS_SECRET,
                  { algorithms: ['HS256'] },
                ) as { id?: string };
                if (payload.id) uid = payload.id;
              } else {
                const q = request.query as Record<string, string | undefined>;
                if (q.uid) uid = q.uid;
              }
            } catch { /* keep default */ }
            const signed = signMediaPath(serverPath, uid, 2 * 60 * 60);
            hlsManifestUrl = `${clientPath}?sig=${signed.sig}&exp=${signed.exp}&uid=${signed.uid}`;
          } else {
            // Public manifest: nginx-rewritten path; the public master route
            // is unsigned (gated by isPublished + access level on the server).
            hlsManifestUrl = clientPath;
          }
        }

        // 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,
          hlsStatus: video.hlsStatus,
          hlsManifestUrl,
          createdAt: video.createdAt,
        };
      } catch (error) {
        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',
    };
  });
}
