import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { createReadStream } from 'fs';
import { readFile, access, stat } from 'fs/promises';
import path from 'path';
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';

const HLS_ROOT = '/media/local/hls';
// 2-hour TTL on segment URLs — longer than typical viewing session, so a
// player that has the manifest cached doesn't have to re-sign mid-playback.
const SEGMENT_TTL_SECONDS = 2 * 60 * 60;
// Master/variant playlists share the same TTL for consistency.
const MANIFEST_TTL_SECONDS = SEGMENT_TTL_SECONDS;

// Whitelist sanitizer for path components in URLs (variant + filename).
const SAFE_PATH_RE = /^[a-zA-Z0-9._-]+$/;

/**
 * Identify whether a request is from an authenticated admin/media-role user.
 * Mirrors the logic in video-streaming.routes.ts so admin HLS access works
 * the same way (Bearer JWT or signed URL params).
 */
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;

    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;
  }
}

/**
 * Sanitize a manifest line: rewrite a relative URI (`360p/index.m3u8` or
 * `seg_00001.ts`) into an absolute path beneath the given basePath, with a
 * fresh signed-URL query string. Lines starting with `#` or empty are
 * passed through untouched.
 *
 * The path emitted to the player is the *client-side* `/media/*` path that
 * nginx rewrites to `/api/*` before reaching this server. Signatures are
 * computed against the *server-side* `/api/*` path because that's what
 * `verifyMediaSignature(request.url, ...)` sees on the inbound request.
 */
function rewriteManifestLines(
  manifestText: string,
  clientBasePath: string,  // e.g. `/media/videos/123/hls` (browser-facing)
  serverBasePath: string,  // e.g. `/api/videos/123/hls` (post-nginx-rewrite, used for signing)
  prefixSegmentsWith: string,  // e.g. `360p/` for variant playlists; '' for master
  uid: string,
  ttlSeconds: number,
): string {
  return manifestText
    .split('\n')
    .map((line) => {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) return line;
      const clientUrl = `${clientBasePath}/${prefixSegmentsWith}${trimmed}`;
      const serverPath = `${serverBasePath}/${prefixSegmentsWith}${trimmed}`;
      const signed = signMediaPath(serverPath, uid, ttlSeconds);
      const sep = clientUrl.includes('?') ? '&' : '?';
      return `${clientUrl}${sep}sig=${signed.sig}&exp=${signed.exp}&uid=${signed.uid}`;
    })
    .join('\n');
}

/** Lookup video + HLS state from DB, returning a lightweight record. */
async function loadVideoForHls(videoId: number, scope: 'admin' | 'public') {
  if (scope === 'public') {
    return prisma.video.findFirst({
      where: { id: videoId, isPublished: true, isLocked: false },
      select: {
        id: true,
        accessLevel: true,
        hlsStatus: true,
        hlsManifestPath: true,
      },
    });
  }
  return prisma.video.findUnique({
    where: { id: videoId },
    select: {
      id: true,
      accessLevel: true,
      hlsStatus: true,
      hlsManifestPath: true,
    },
  });
}

/**
 * Subscription check for non-free videos on public endpoints. Mirrors
 * public.routes.ts content-gating logic.
 */
async function checkPublicAccess(
  request: FastifyRequest,
  reply: FastifyReply,
  accessLevel: string | null | undefined,
): Promise<boolean> {
  if (!accessLevel || accessLevel === 'free') return true;

  const userId = (request as any).user?.id;
  if (!userId) {
    reply.code(403).send({
      message: 'This content requires a subscription',
      accessLevel,
      requiresAuth: true,
    });
    return false;
  }
  const subscription = await prisma.userSubscription.findFirst({
    where: { userId, status: 'active' },
    include: { plan: true },
  });
  if (!subscription) {
    reply.code(403).send({
      message: 'This content requires an active subscription',
      accessLevel,
      requiresSubscription: true,
    });
    return false;
  }
  if (accessLevel === 'premium' && (subscription.plan?.tier ?? 0) < 2) {
    reply.code(403).send({
      message: 'This content requires a premium subscription',
      accessLevel,
      requiresUpgrade: true,
    });
    return false;
  }
  return true;
}

/** Resolve a path inside the HLS root, blocking traversal. */
function resolveHlsPath(...parts: string[]): string {
  const candidate = path.resolve(path.join(HLS_ROOT, ...parts));
  if (!candidate.startsWith(path.resolve(HLS_ROOT))) {
    throw new Error('Path traversal blocked');
  }
  return candidate;
}

export async function hlsRoutes(fastify: FastifyInstance) {
  // ───────────────────────────────────────────────────────────────────
  // Admin: master, variant, segment
  // ───────────────────────────────────────────────────────────────────

  fastify.get<{ Params: { id: string } }>(
    '/videos/:id/hls/master.m3u8',
    async (request, reply) => {
      if (!(await isAdminRequest(request))) {
        return reply.code(401).send({ message: 'Authentication required' });
      }
      return serveMaster(request, reply, 'admin');
    },
  );

  fastify.get<{ Params: { id: string; variant: string } }>(
    '/videos/:id/hls/:variant/index.m3u8',
    async (request, reply) => {
      if (!(await isAdminRequest(request))) {
        return reply.code(401).send({ message: 'Authentication required' });
      }
      return serveVariant(request, reply, 'admin');
    },
  );

  fastify.get<{ Params: { id: string; variant: string; filename: string } }>(
    '/videos/:id/hls/:variant/:filename',
    async (request, reply) => {
      // Segments are only authorized via the signed URL embedded in the
      // variant playlist — we do NOT also accept Bearer auth here (keeps the
      // hot path tiny and avoids a DB lookup per segment).
      const query = request.query as Record<string, string | undefined>;
      const result = verifyMediaSignature(request.url, query);
      if (!result.valid) {
        return reply.code(403).send({ message: 'Invalid or expired signature' });
      }
      return serveSegment(request, reply);
    },
  );

  // ───────────────────────────────────────────────────────────────────
  // Public: master, variant, segment (gated by publish + access level)
  // ───────────────────────────────────────────────────────────────────

  fastify.get<{ Params: { id: string } }>(
    '/public/:id/hls/master.m3u8',
    async (request, reply) => {
      return serveMaster(request, reply, 'public');
    },
  );

  fastify.get<{ Params: { id: string; variant: string } }>(
    '/public/:id/hls/:variant/index.m3u8',
    async (request, reply) => {
      return serveVariant(request, reply, 'public');
    },
  );

  fastify.get<{ Params: { id: string; variant: string; filename: string } }>(
    '/public/:id/hls/:variant/:filename',
    async (request, reply) => {
      const query = request.query as Record<string, string | undefined>;
      const result = verifyMediaSignature(request.url, query);
      if (!result.valid) {
        return reply.code(403).send({ message: 'Invalid or expired signature' });
      }
      return serveSegment(request, reply);
    },
  );
}

// ─────────────────────────────────────────────────────────────────────
// Handlers
// ─────────────────────────────────────────────────────────────────────

async function serveMaster(
  request: FastifyRequest<{ Params: { id: string } }>,
  reply: FastifyReply,
  scope: 'admin' | 'public',
) {
  const videoId = parseInt(request.params.id);
  if (isNaN(videoId)) {
    return reply.code(400).send({ message: 'Invalid video ID' });
  }

  const video = await loadVideoForHls(videoId, scope);
  if (!video || video.hlsStatus !== 'READY' || !video.hlsManifestPath) {
    return reply.code(404).send({ message: 'HLS manifest not available for this video' });
  }

  if (scope === 'public') {
    if (!(await checkPublicAccess(request, reply, video.accessLevel))) return; // reply already sent
  }

  // Read master.m3u8 from disk.
  let masterPath: string;
  try {
    masterPath = resolveHlsPath(String(videoId), 'master.m3u8');
    await access(masterPath);
  } catch (err) {
    logger.warn(`HLS master.m3u8 missing on disk for video ${videoId}: ${err}`);
    return reply.code(404).send({ message: 'HLS manifest file missing' });
  }
  const masterText = await readFile(masterPath, 'utf-8');

  // The master's variant URIs look like "360p/index.m3u8". Rewrite each to
  // an absolute, server-signed URL pointing at our variant endpoint. We emit
  // browser-facing `/media/*` paths (rewritten to `/api/*` by nginx) but
  // sign against the server-side `/api/*` path the verifier will see.
  const clientBase = scope === 'admin'
    ? `/media/videos/${videoId}/hls`
    : `/media/public/${videoId}/hls`;
  const serverBase = scope === 'admin'
    ? `/api/videos/${videoId}/hls`
    : `/api/public/${videoId}/hls`;
  const uid = scope === 'admin'
    ? ((request.query as Record<string, string | undefined>).uid ?? 'admin')
    : `public-${videoId}`;

  const rewritten = rewriteManifestLines(masterText, clientBase, serverBase, '', uid, MANIFEST_TTL_SECONDS);

  reply
    .header('Content-Type', 'application/vnd.apple.mpegurl')
    .header('Cache-Control', 'no-store')
    .send(rewritten);
}

async function serveVariant(
  request: FastifyRequest<{ Params: { id: string; variant: string } }>,
  reply: FastifyReply,
  scope: 'admin' | 'public',
) {
  const videoId = parseInt(request.params.id);
  const variant = request.params.variant;
  if (isNaN(videoId) || !SAFE_PATH_RE.test(variant)) {
    return reply.code(400).send({ message: 'Invalid params' });
  }

  const video = await loadVideoForHls(videoId, scope);
  if (!video || video.hlsStatus !== 'READY') {
    return reply.code(404).send({ message: 'HLS manifest not available' });
  }
  if (scope === 'public') {
    if (!(await checkPublicAccess(request, reply, video.accessLevel))) return;
  }

  let variantPath: string;
  try {
    variantPath = resolveHlsPath(String(videoId), variant, 'index.m3u8');
    await access(variantPath);
  } catch {
    return reply.code(404).send({ message: 'Variant playlist not found' });
  }
  const variantText = await readFile(variantPath, 'utf-8');

  const clientBase = scope === 'admin'
    ? `/media/videos/${videoId}/hls`
    : `/media/public/${videoId}/hls`;
  const serverBase = scope === 'admin'
    ? `/api/videos/${videoId}/hls`
    : `/api/public/${videoId}/hls`;
  const uid = scope === 'admin'
    ? ((request.query as Record<string, string | undefined>).uid ?? 'admin')
    : `public-${videoId}`;

  const rewritten = rewriteManifestLines(
    variantText,
    clientBase,
    serverBase,
    `${variant}/`,
    uid,
    SEGMENT_TTL_SECONDS,
  );

  reply
    .header('Content-Type', 'application/vnd.apple.mpegurl')
    .header('Cache-Control', 'no-store')
    .send(rewritten);
}

async function serveSegment(
  request: FastifyRequest<{ Params: { id: string; variant: string; filename: string } }>,
  reply: FastifyReply,
) {
  const videoId = parseInt(request.params.id);
  const { variant, filename } = request.params;

  if (isNaN(videoId) || !SAFE_PATH_RE.test(variant) || !SAFE_PATH_RE.test(filename)) {
    return reply.code(400).send({ message: 'Invalid params' });
  }

  let segmentPath: string;
  try {
    segmentPath = resolveHlsPath(String(videoId), variant, filename);
    await access(segmentPath);
  } catch {
    return reply.code(404).send({ message: 'Segment not found' });
  }

  const stats = await stat(segmentPath);
  const isPlaylist = filename.endsWith('.m3u8');
  const contentType = isPlaylist ? 'application/vnd.apple.mpegurl' : 'video/mp2t';

  reply
    .header('Content-Type', contentType)
    .header('Content-Length', stats.size)
    // Segments are content-addressed in the sense that {videoId}/{variant}/{name}
    // never changes content; safe to cache aggressively at the browser. The
    // signature in the URL keeps cache keys per-session.
    .header('Cache-Control', 'private, max-age=3600');

  return reply.send(createReadStream(segmentPath));
}
