import { execFile } from 'child_process';
import { promisify } from 'util';

const execFileAsync = promisify(execFile);

export const runFFmpeg = async (args, options = {}) => {
  const { stdio = 'pipe', ...otherOptions } = options;
  try {
    const { stdout, stderr } = await execFileAsync('ffmpeg', args, {
      stdio,
      ...otherOptions,
    });
    return { stdout, stderr };
  } catch (error) {
    throw new Error(`FFmpeg error: ${error.message}\nStderr: ${error.stderr}`);
  }
};

const runFFprobe = async (args) => {
  try {
    const { stdout } = await execFileAsync('ffprobe', args);
    return { stdout };
  } catch (error) {
    throw new Error(`FFprobe error: ${error.message}\nStderr: ${error.stderr}`);
  }
};

export const getMediaDuration = async (inputPath) => {
  const args = [
    '-v', 'error',
    '-show_entries', 'format=duration',
    '-of', 'default=noprint_wrappers=1:nokey=1',
    inputPath,
  ];
  const { stdout } = await runFFprobe(args);
  return parseFloat(stdout.trim());
};

/**
 * Return structured media metadata for an input file.
 * Result shape:
 *   { fps, codec, resolution, durationMs, fileSizeBytes }
 * Any field may be null if ffprobe cannot determine it.
 */
export const getMediaInfo = async (inputPath) => {
  const args = [
    '-v', 'quiet',
    '-print_format', 'json',
    '-show_streams',
    '-show_format',
    inputPath,
  ];
  const { stdout } = await runFFprobe(args);
  const info = JSON.parse(stdout);

  const videoStream = (info.streams || []).find(s => s.codec_type === 'video');
  const fmt         = info.format || {};

  let fps = null;
  if (videoStream?.r_frame_rate) {
    const [num, den] = videoStream.r_frame_rate.split('/').map(Number);
    if (den > 0) fps = Math.round((num / den) * 1000) / 1000;
  }

  const audioStreams = (info.streams || []).filter(s => s.codec_type === 'audio');
  const hasAudio = audioStreams.length > 0;

  const audioMetadata = audioStreams.map(s => {
    const bitDepth = s.bits_per_raw_sample
      ? parseInt(s.bits_per_raw_sample, 10)
      : s.bit_depth
        ? parseInt(s.bit_depth, 10)
        : null;
    return {
      index: s.index ?? 0,
      codec: s.codec_name || null,
      channels: s.channels ? parseInt(s.channels, 10) : null,
      channel_layout: s.channel_layout || null,
      sample_rate: s.sample_rate ? parseInt(s.sample_rate, 10) : null,
      bit_depth: bitDepth,
      bit_rate: s.bit_rate ? parseInt(s.bit_rate, 10) : null,
      language: s.tags?.language || null,
      title: s.tags?.title || null,
      disposition: s.disposition || {},
    };
  });

  return {
    fps,
    codec: videoStream?.codec_name || null,
    resolution: videoStream ? `${videoStream.width}x${videoStream.height}` : null,
    durationMs: fmt.duration ? Math.round(parseFloat(fmt.duration) * 1000) : null,
    fileSizeBytes: fmt.size ? parseInt(fmt.size, 10) : null,
    hasAudio,
    audioMetadata: audioMetadata.length > 0 ? audioMetadata : null,
  };
};

export const extractFrameAtTime = async (inputPath, outputPath, timeCode) => {
  const args = [
    '-ss', timeCode,
    '-i', inputPath,
    '-vframes', '1',
    '-q:v', '2',
    '-y',
    outputPath,
  ];
  await runFFmpeg(args);
};

const NVENC_CODECS  = new Set(['h264_nvenc', 'hevc_nvenc']);
const VAAPI_CODECS  = new Set(['h264_vaapi', 'hevc_vaapi']);
const HW_CODECS     = new Set([...NVENC_CODECS, ...VAAPI_CODECS]);

export const transcodeVideo = async (inputPath, outputPath, options = {}) => {
  const {
    videoCodec   = 'libx264',
    videoPreset  = 'fast',
    videoBitrate = '750k',   // average/target for VBR
    videoMinRate = null,     // VBR minimum  e.g. '500k'
    videoMaxRate = null,     // VBR maximum  e.g. '1000k'
    videoBufSize = null,     // VBR buffer   e.g. '2000k' (2× maxrate recommended)
    rateControl  = null,     // 'cbr' | 'vbr' | 'cqp' — optional override for HW codecs
    audioCodec   = 'aac',
    audioBitrate = '128k',
    hasAudio     = true,
  } = options;

  // libx264 / yuv420p require even dimensions.
  const vf = "scale='trunc(iw/2)*2:trunc(ih/2)*2',format=yuv420p";

  const args = [
    '-analyzeduration', '100M',
    '-probesize',       '100M',
    '-i', inputPath,
    '-vf', vf,
    '-c:v', videoCodec,
    '-preset', videoPreset,
    '-b:v', videoBitrate,
  ];

  // VBR min/max/bufsize for libx264 ABR mode.
  // When minrate+maxrate are set, libx264 operates in ABR with hard limits
  // rather than strict CBR — quality varies per-scene within the envelope.
  if (videoMinRate) args.push('-minrate', videoMinRate);
  if (videoMaxRate) args.push('-maxrate', videoMaxRate);
  if (videoBufSize) args.push('-bufsize', videoBufSize);

  // NVENC/VAAPI hardware rate control flags
  if (rateControl) {
    if (NVENC_CODECS.has(videoCodec)) {
      args.push('-rc', rateControl);
    } else if (VAAPI_CODECS.has(videoCodec)) {
      args.push('-rc_mode', rateControl.toUpperCase());
    }
  }

  if (hasAudio) {
    args.push('-c:a', audioCodec, '-b:a', audioBitrate);
  } else {
    args.push('-an');
  }

  args.push('-movflags', '+faststart', '-y', outputPath);

  await runFFmpeg(args);
};

export const isHwCodec = (codec) => HW_CODECS.has(codec);

// Single-frame poster — used as a fallback "proxy" for still-image assets
// so the library can show them without a transcoded video.
export const transcodeImage = async (inputPath, outputPath) => {
  await runFFmpeg([
    '-i', inputPath,
    '-vf', "scale='min(1920,iw)':-2",
    '-q:v', '3',
    '-y', outputPath,
  ]);
};

export const trimSegment = async (inputPath, outputPath, inPoint, outPoint) => {
  const info = await getMediaInfo(inputPath);
  const fps = info.fps || 30;
  const inSeconds = inPoint / fps;
  const frameCount = outPoint - inPoint;

  // `-frames:v` bounds the video output but says nothing about audio. Without
  // -c:a copy + -shortest the audio track is either dropped or runs past
  // the trimmed video. We need audio in the final concat output, so passthrough
  // the audio codec and stop on the shortest stream.
  const args = [
    '-ss', inSeconds.toString(),
    '-i', inputPath,
    '-frames:v', frameCount.toString(),
    '-c:a', 'copy',
    '-shortest',
    '-start_number', '0',
    '-y',
    outputPath,
  ];
  await runFFmpeg(args);
};

// Segment an existing MP4/MOV into HLS (fMP4) — init.mp4 + segment_*.m4s +
// playlist.m3u8. Keeps the original codec (no re-encode) so this is cheap to
// run after the proxy transcode. fMP4 segments stay <5 MB at our proxy
// bitrate, which sidesteps RustFS's broken byte-range path on large objects.
export const segmentToHls = async (inputPath, outputDir, options = {}) => {
  const {
    segmentDurationSec = 4,
    playlistName       = 'playlist.m3u8',
    initName           = 'init.mp4',
    segmentPattern     = 'segment_%05d.m4s',
  } = options;

  const args = [
    '-i', inputPath,
    '-c', 'copy',
    '-f', 'hls',
    '-hls_time', String(segmentDurationSec),
    '-hls_playlist_type', 'vod',
    '-hls_segment_type', 'fmp4',
    '-hls_flags', 'independent_segments',
    '-hls_fmp4_init_filename', initName,
    '-hls_segment_filename', `${outputDir}/${segmentPattern}`,
    '-y',
    `${outputDir}/${playlistName}`,
  ];
  await runFFmpeg(args);
};

export const concatSegments = async (segmentListFile, outputPath) => {
  const args = [
    '-f', 'concat',
    '-safe', '0',
    '-i', segmentListFile,
    '-c', 'copy',
    '-y',
    outputPath,
  ];
  await runFFmpeg(args);
};
