/**
 * VideoEncoder — Combines frame images into video using FFmpeg CLI.
 *
 * Uses child_process.spawn to invoke ffmpeg directly.
 * Requires ffmpeg to be available on PATH.
 */
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';

export interface AudioTrackInput {
  url: string;
  startFrame: number;
  volume: number; // 0 to 1
}

export interface EncodeOptions {
  /** Directory containing numbered frame images */
  framesDir: string;
  /** Frame image pattern (e.g., 'frame-%06d.png') */
  framePattern?: string;
  /** Output file path */
  outputPath: string;
  /** Frames per second */
  fps: number;
  /** Output codec: 'h264' for MP4, 'vp8'/'vp9' for WebM */
  codec: 'h264' | 'vp8' | 'vp9';
  /** Width (for scaling, optional) */
  width?: number;
  /** Height (for scaling, optional) */
  height?: number;
  /** Quality: CRF value (lower = better, 18-28 typical) */
  crf?: number;
  /** Audio tracks to mix */
  audioTracks?: AudioTrackInput[];
  /** Total frames (used to force exact video length) */
  durationInFrames?: number;
  /** Progress callback */
  onProgress?: (percent: number) => void;
}

/**
 * Encode frame images to video using FFmpeg.
 */
export async function encodeVideo(options: EncodeOptions): Promise<void> {
  const {
    framesDir,
    framePattern = 'frame-%06d.png',
    outputPath,
    fps,
    codec,
    crf = 23,
    audioTracks = [],
    durationInFrames,
    onProgress,
  } = options;

  const ffmpegPath = findFFmpegPath();
  if (!ffmpegPath) {
    throw new Error(
      'FFmpeg not found. Install it via: brew install ffmpeg (macOS) or set FFMPEG_PATH env var.'
    );
  }

  const inputPattern = path.join(framesDir, framePattern);

  // Build ffmpeg args
  const args: string[] = [
    '-y',                          // Overwrite output
    '-framerate', String(fps),     // Input framerate
    '-i', inputPattern,            // Input 0: Image pattern
  ];

  // Add audio inputs
  audioTracks.forEach(track => {
    args.push('-i', track.url);
  });

  // Codec-specific settings
  if (codec === 'h264') {
    args.push(
      '-c:v', 'libx264',
      '-crf', String(crf),
      '-preset', 'medium',
      '-pix_fmt', 'yuv420p',       // Compatibility with most players
      '-movflags', '+faststart',   // Web-optimized MP4
    );
  } else if (codec === 'vp8') {
    args.push(
      '-c:v', 'libvpx',
      '-crf', String(crf),
      '-b:v', '0',                 // Constant quality mode
      '-pix_fmt', 'yuv420p',
    );
  } else if (codec === 'vp9') {
    args.push(
      '-c:v', 'libvpx-vp9',
      '-crf', String(crf),
      '-b:v', '0',
      '-pix_fmt', 'yuv420p',
    );
  }

  // Audio codec
  args.push('-c:a', 'aac', '-b:a', '192k');

  // Build Audio Filter Complex
  if (audioTracks.length > 0) {
    const filterParts: string[] = [];
    const mixInputs: string[] = [];

    audioTracks.forEach((track, index) => {
      const inputIndex = index + 1; // 0 is video
      const delayMs = Math.round((track.startFrame / fps) * 1000);
      const outLabel = `[a${inputIndex}]`;
      
      // adelay applies delay to all channels (L|R)
      filterParts.push(`[${inputIndex}:a]adelay=${delayMs}|${delayMs},volume=${track.volume}${outLabel}`);
      mixInputs.push(outLabel);
    });

    // Mix all processed audio streams
    const mixFilter = `${mixInputs.join('')}amix=inputs=${audioTracks.length}:duration=longest:dropout_transition=2[aout]`;
    filterParts.push(mixFilter);

    args.push('-filter_complex', filterParts.join(';'));
    args.push('-map', '0:v');       // Map video from input 0
    args.push('-map', '[aout]');    // Map audio from complex filter
  }

  // Force video length
  if (durationInFrames) {
    args.push('-frames:v', String(Math.round(durationInFrames)));
  }

  args.push(outputPath);

  // Count total frames for progress
  const frameFiles = fs.readdirSync(framesDir)
    .filter(f => f.startsWith('frame-'))
    .length;

  return new Promise<void>((resolve, reject) => {
    const proc = spawn(ffmpegPath, args, {
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    let stderr = '';

    proc.stderr?.on('data', (data: Buffer) => {
      const chunk = data.toString();
      stderr += chunk;

      // Parse progress from ffmpeg stderr
      // FFmpeg outputs: frame=  123 fps= 30 ...
      const frameMatch = chunk.match(/frame=\s*(\d+)/);
      if (frameMatch && onProgress && frameFiles > 0) {
        const rendered = parseInt(frameMatch[1], 10);
        onProgress(Math.min(100, Math.round((rendered / frameFiles) * 100)));
      }
    });

    proc.on('close', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`FFmpeg exited with code ${code}: ${stderr.slice(-500)}`));
      }
    });

    proc.on('error', (err) => {
      reject(new Error(`FFmpeg spawn error: ${err.message}`));
    });
  });
}

/**
 * Clean up frame images after encoding.
 */
export function cleanupFrames(framesDir: string): void {
  if (fs.existsSync(framesDir)) {
    fs.rmSync(framesDir, { recursive: true, force: true });
  }
}

/**
 * Find FFmpeg executable on the system.
 */
function findFFmpegPath(): string | null {
  // 1. Environment variable
  if (process.env.FFMPEG_PATH) {
    return process.env.FFMPEG_PATH;
  }

  // 2. Check PATH
  const { execSync } = require('child_process');
  try {
    const result = execSync('which ffmpeg', { encoding: 'utf-8' }).trim();
    if (result) return result;
  } catch {
    // Not on PATH
  }

  // 3. Common paths
  const candidates = [
    '/usr/local/bin/ffmpeg',
    '/opt/homebrew/bin/ffmpeg',
    '/usr/bin/ffmpeg',
  ];

  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }

  return null;
}
