import { spawn } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { logger } from '../../../utils/logger';

const HLS_ROOT = '/media/local/hls';
const FFMPEG_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour cap; long-form video can exceed default 30s

export interface HlsVariant {
  name: string;        // '360p' | '720p' | '1080p'
  height: number;      // short-side height in pixels
  bitrate: number;     // target video bitrate in kbps
  path: string;        // relative path under {videoId}/, e.g. '360p/index.m3u8'
}

export interface HlsTranscodeOptions {
  videoId: number;
  sourcePath: string;
  durationSeconds: number;
  sourceWidth: number;
  sourceHeight: number;
  /** Optional progress callback, called with 0-100 as transcoding advances. */
  onProgress?: (percent: number) => void | Promise<void>;
}

export interface HlsTranscodeResult {
  manifestPath: string;          // absolute path to master.m3u8 on disk
  manifestRelativePath: string;  // e.g. {videoId}/master.m3u8 (relative to HLS_ROOT)
  variants: HlsVariant[];
}

// Bitrate ladder. Each rung is { name, height, videoKbps, maxKbps, bufKbps, audioKbps }.
// Ladder is keyed off short-side height so a 1080×1920 vertical short still
// gets 360p/720p/1080p *short-side* renditions (width follows via scale=-2:H).
const LADDER = [
  { name: '360p',  height: 360,  videoKbps: 800,  maxKbps: 856,  bufKbps: 1200, audioKbps: 96  },
  { name: '720p',  height: 720,  videoKbps: 2800, maxKbps: 2996, bufKbps: 4200, audioKbps: 128 },
  { name: '1080p', height: 1080, videoKbps: 5000, maxKbps: 5350, bufKbps: 7500, audioKbps: 128 },
];

/**
 * Transcode a source video to HLS adaptive bitrate using a single FFmpeg
 * invocation. One decode pass produces all variants in parallel, with
 * keyframes aligned every 2s so hls.js can switch renditions cleanly.
 *
 * Output layout:
 *   /media/local/hls/{videoId}/master.m3u8
 *   /media/local/hls/{videoId}/360p/index.m3u8
 *   /media/local/hls/{videoId}/360p/seg_00000.ts
 *   ...
 */
export async function transcodeToHls(opts: HlsTranscodeOptions): Promise<HlsTranscodeResult> {
  const { videoId, sourcePath, durationSeconds, sourceWidth, sourceHeight, onProgress } = opts;

  // Pick variants up to the source's short-side resolution. Always include 360p.
  const shortSide = Math.min(sourceWidth, sourceHeight);
  const variants = LADDER.filter((v, i) => i === 0 || v.height <= shortSide);

  const outDir = path.join(HLS_ROOT, String(videoId));
  await fs.rm(outDir, { recursive: true, force: true });
  await fs.mkdir(outDir, { recursive: true });

  // Build the filter_complex graph: split video N ways, scale each.
  const splitTargets = variants.map((_, i) => `[v${i}]`).join('');
  const scaleFilters = variants
    .map((v, i) => `[v${i}]scale=-2:${v.height}[v${i}o]`)
    .join('; ');
  const filterComplex = `[0:v]split=${variants.length}${splitTargets}; ${scaleFilters}`;

  // Per-stream encode args.
  const streamArgs: string[] = [];
  variants.forEach((v, i) => {
    streamArgs.push(
      '-map', `[v${i}o]`,
      `-c:v:${i}`, 'libx264',
      `-b:v:${i}`, `${v.videoKbps}k`,
      `-maxrate:v:${i}`, `${v.maxKbps}k`,
      `-bufsize:v:${i}`, `${v.bufKbps}k`,
    );
  });

  // Audio: map source audio once per variant so each rendition has its own audio track.
  variants.forEach((v, i) => {
    streamArgs.push('-map', 'a:0?', `-c:a:${i}`, 'aac', `-b:a:${i}`, `${v.audioKbps}k`, '-ac', '2');
  });

  // var_stream_map associates video+audio streams per variant.
  const varStreamMap = variants
    .map((v, i) => `v:${i},a:${i},name:${v.name}`)
    .join(' ');

  const ffmpegArgs = [
    '-hide_banner',
    '-y',
    '-i', sourcePath,
    '-filter_complex', filterComplex,
    ...streamArgs,
    '-preset', 'veryfast',
    '-profile:v', 'main',
    '-sc_threshold', '0',
    '-g', '48',
    '-keyint_min', '48',
    '-force_key_frames', 'expr:gte(t,n_forced*2)',
    '-hls_time', '4',
    '-hls_playlist_type', 'vod',
    '-hls_flags', 'independent_segments',
    '-hls_segment_filename', path.join(outDir, '%v', 'seg_%05d.ts'),
    '-master_pl_name', 'master.m3u8',
    '-var_stream_map', varStreamMap,
    path.join(outDir, '%v', 'index.m3u8'),
  ];

  // Pre-create per-variant subdirs so FFmpeg can write segments.
  for (const v of variants) {
    await fs.mkdir(path.join(outDir, v.name), { recursive: true });
  }

  logger.info(`[hls] Starting transcode for video ${videoId} (${variants.length} variants: ${variants.map(v => v.name).join(', ')})`);

  await runFfmpeg(ffmpegArgs, durationSeconds, onProgress);

  // FFmpeg's HLS muxer writes per-variant playlists into directories named
  // by the var_stream_map's name. Confirm master.m3u8 exists.
  const masterPath = path.join(outDir, 'master.m3u8');
  try {
    await fs.access(masterPath);
  } catch {
    // Cleanup partial output.
    await fs.rm(outDir, { recursive: true, force: true });
    throw new Error('FFmpeg completed but master.m3u8 was not produced');
  }

  const result: HlsTranscodeResult = {
    manifestPath: masterPath,
    manifestRelativePath: path.join(String(videoId), 'master.m3u8'),
    variants: variants.map(v => ({
      name: v.name,
      height: v.height,
      bitrate: v.videoKbps,
      path: path.join(v.name, 'index.m3u8'),
    })),
  };

  logger.info(`[hls] Transcode complete for video ${videoId}`);
  return result;
}

/**
 * Spawn FFmpeg with a hard timeout and stderr-based progress parsing.
 * FFmpeg writes "time=HH:MM:SS.cc" lines to stderr periodically; we parse
 * those and call onProgress with 0-100.
 */
function runFfmpeg(
  args: string[],
  durationSeconds: number,
  onProgress?: (percent: number) => void | Promise<void>,
): Promise<void> {
  return new Promise((resolve, reject) => {
    const child = spawn('ffmpeg', args);
    let stderrTail = '';
    let lastReportedPercent = -1;

    const timeout = setTimeout(() => {
      child.kill('SIGKILL');
      reject(new Error(`FFmpeg timeout after ${FFMPEG_TIMEOUT_MS / 1000}s`));
    }, FFMPEG_TIMEOUT_MS);

    child.stderr.on('data', (data: Buffer) => {
      const text = data.toString();
      // Keep a tail for error reporting on non-zero exit.
      stderrTail = (stderrTail + text).slice(-4000);

      if (!onProgress || durationSeconds <= 0) return;
      // Parse "time=HH:MM:SS.cc" — newest occurrence in this chunk.
      const matches = text.match(/time=(\d+):(\d+):(\d+(?:\.\d+)?)/g);
      if (!matches || matches.length === 0) return;
      const last = matches[matches.length - 1];
      const m = last.match(/time=(\d+):(\d+):(\d+(?:\.\d+)?)/);
      if (!m) return;
      const elapsed = Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]);
      const percent = Math.min(99, Math.floor((elapsed / durationSeconds) * 100));
      if (percent > lastReportedPercent) {
        lastReportedPercent = percent;
        // Fire-and-forget; queue can swallow errors here.
        Promise.resolve(onProgress(percent)).catch(() => {});
      }
    });

    child.on('close', (code) => {
      clearTimeout(timeout);
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`FFmpeg exited with code ${code}: ${stderrTail.slice(-1000)}`));
      }
    });

    child.on('error', (err) => {
      clearTimeout(timeout);
      reject(err);
    });
  });
}

/**
 * Remove the HLS output directory for a video (e.g. on transcode failure or video deletion).
 */
export async function cleanupHlsOutput(videoId: number): Promise<void> {
  const dir = path.join(HLS_ROOT, String(videoId));
  await fs.rm(dir, { recursive: true, force: true });
}
