/**
 * Serve cached videos stored on disk (or by source_url) for Discord attachments/embeds
 * Security: uses HMAC signature on id to prevent enumeration and requires id-based lookups to include a valid signature.
 */
import sql from '../../../utils/db.ts';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { Readable } from 'stream';
import { checkRateLimit, recordRequest } from '../../../utils/rateLimit.ts';
import type { APIContext } from 'astro';

const VIDEO_CACHE_SECRET = import.meta.env.IMAGE_CACHE_SECRET; // share same secret for simplicity
if (!VIDEO_CACHE_SECRET && import.meta.env.PROD) {
  throw new Error('CRITICAL: IMAGE_CACHE_SECRET environment variable is not set in production!');
} else if (!VIDEO_CACHE_SECRET) {
  console.error('WARNING: IMAGE_CACHE_SECRET not set, video signing may be unavailable');
}

export function signVideoId(videoId: string | number): string {
  if (!VIDEO_CACHE_SECRET) throw new Error('VIDEO_CACHE_SECRET not configured');
  const hmac = crypto.createHmac('sha256', VIDEO_CACHE_SECRET);
  hmac.update(String(videoId));
  return hmac.digest('hex').substring(0, 32); // 128-bit signature
}

export function signVideoUrl(sourceUrl: string): string {
  if (!VIDEO_CACHE_SECRET) throw new Error('VIDEO_CACHE_SECRET not configured');
  const hmac = crypto.createHmac('sha256', VIDEO_CACHE_SECRET);
  hmac.update(sourceUrl);
  return hmac.digest('hex').substring(0, 32); // 128-bit signature
}

function verifySignature(id: string | number, signature: string | null): boolean {
  if (!VIDEO_CACHE_SECRET || !signature) return false;
  const expected = signVideoId(id);
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  } catch {
    return false;
  }
}

function verifyUrlSignature(sourceUrl: string, signature: string | null): boolean {
  if (!VIDEO_CACHE_SECRET || !signature) return false;
  const expected = signVideoUrl(sourceUrl);
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  } catch {
    return false;
  }
}

interface StreamResult {
  status: number;
  stream?: ReadableStream;
  total?: number;
  start?: number;
  end?: number;
}

// Helper to stream a file with range support
function streamFile(filePath: string, rangeHeader: string | null): StreamResult {
  // Ensure file exists
  if (!fs.existsSync(filePath)) return { status: 404 };
  const stat = fs.statSync(filePath);
  const total = stat.size;

  if (!rangeHeader) {
    const nodeStream = fs.createReadStream(filePath);
    const stream = Readable.toWeb(nodeStream) as ReadableStream;
    return { status: 200, stream, total, start: 0, end: total - 1 };
  }

  // Parse range header
  const match = /bytes=(\d*)-(\d*)/.exec(rangeHeader);
  if (!match) return { status: 416 };
  let start = match[1] === '' ? 0 : parseInt(match[1], 10);
  let end = match[2] === '' ? total - 1 : parseInt(match[2], 10);
  if (isNaN(start) || isNaN(end) || start > end || start >= total) return { status: 416 };

  const nodeStream = fs.createReadStream(filePath, { start, end });
  const stream = Readable.toWeb(nodeStream) as ReadableStream;
  return { status: 206, stream, total, start, end };
}

export async function GET({ request }: APIContext): Promise<Response> {
  const rateCheck = checkRateLimit(request, { limit: 50, windowMs: 1000, burstLimit: 200, burstWindowMs: 10_000 });
  if (!rateCheck.allowed) return new Response('Rate limit exceeded', { status: 429, headers: { 'Retry-After': '1' } });
  recordRequest(request, 1000);

  try {
    const url = new URL(request.url);
    const id = url.searchParams.get('id');
    const signature = url.searchParams.get('sig');
    const sourceUrl = url.searchParams.get('url');

    if (!id && !sourceUrl) return new Response('Missing id or url parameter', { status: 400 });

    // If id-based, require signature
    if (id && !verifySignature(id, signature)) {
      return new Response('Invalid or missing signature', { status: 403 });
    }

    let row;
    if (id) {
      const r = await sql`SELECT video_id, file_path, content_type, file_size FROM video_cache WHERE video_id = ${id}`;
      row = r[0];
    } else if (sourceUrl) {
      // Require signature for URL-based lookups to prevent enumeration
      if (!verifyUrlSignature(sourceUrl, signature)) {
        return new Response('Invalid or missing signature for URL lookup', { status: 403 });
      }
      const r = await sql`SELECT video_id, file_path, content_type, file_size FROM video_cache WHERE source_url = ${sourceUrl} LIMIT 1`;
      row = r[0];
    }

    if (!row) return new Response('Video not found in cache', { status: 404 });

    // If file_path is present, stream from disk
    if (row.file_path) {
      // Protect against directory traversal by resolving path and ensuring it's under configured storage
      const storageRoot = '/storage';
      const resolved = path.resolve(row.file_path);
      if (!resolved.startsWith(storageRoot)) {
        console.error('[cached-video] file_path outside storage root', resolved);
        return new Response('Forbidden', { status: 403 });
      }

      const range = request.headers.get('range');
      const result = streamFile(resolved, range);
      if (result.status === 404) return new Response('File not found', { status: 404 });
      if (result.status === 416) return new Response('Range Not Satisfiable', { status: 416 });

      const headers = new Headers();
      headers.set('Content-Type', row.content_type || 'video/mp4');
      headers.set('Accept-Ranges', 'bytes');
      headers.set('Cache-Control', 'public, max-age=31536000, immutable');

      if (result.status === 200) {
        headers.set('Content-Length', String(result.total));
        return new Response(result.stream, { status: 200, headers });
      }

      // Partial content
      if (result.start == null || result.end == null) {
        console.error('[cached-video] Missing range bounds in partial response');
        return new Response('Invalid range', { status: 500 });
      }
      headers.set('Content-Range', `bytes ${result.start}-${result.end}/${result.total}`);
      headers.set('Content-Length', String(result.end - result.start + 1));
      return new Response(result.stream, { status: 206, headers });
    }

    // No file_path - fallback to proxying source_url (but this endpoint expects id-based cache)
    return new Response(JSON.stringify({ error: 'No cached file available' }), { status: 404, headers: { 'Content-Type': 'application/json' } });
  } catch (err) {
    console.error('cached-video error', err);
    return new Response('Server error', { status: 500 });
  }
}
