// Public FFmpeg.wasm export pipeline.
// Internally split into:
//   ./ffmpeg/loader       — lazy-loaded wasm singleton
//   ./ffmpeg/filter-graph — pure graph + argv builders
//   ./ffmpeg/index.ts     — orchestrator that drives ffmpeg.exec()
//
// Why split: the original 347-line monolith mixed the wasm lifecycle with
// command building with the execution loop, which made it hard to swap
// renderers (e.g. server-side ffmpeg) or unit-test the graph builder.

import { loadFFmpeg } from './ffmpeg/loader';
import {
  buildFilterGraph,
  buildArgs,
  collectAssetPaths,
  applySubtitleFilter,
  applyTextOverlays,
} from './ffmpeg/filter-graph';
import type {
  Clip,
  ExportSettings,
  MediaAsset,
  SubtitleCue,
  SubtitleStyle,
  TextOverlay,
  Track,
} from '@/types';
import { DEFAULT_SUBTITLE_STYLE } from '@/types';

export { resetFFmpeg } from './ffmpeg/loader';

export interface ExtractAudioResult {
  /** Decoded 16kHz mono AudioBuffer ready for Whisper. */
  audioBuffer: AudioBuffer;
  /** Raw wav bytes (16-bit PCM, 16kHz, mono) — what ffmpeg produced. */
  wavBytes: Uint8Array;
}

/**
 * Extract the audio track from a video file and return a decoded 16kHz
 * mono AudioBuffer. We pipe through FFmpeg.wasm so it works on every
 * browser (Chrome's decodeAudioData won't accept most video containers).
 */
export async function extractAudioFromVideo(
  source: File | Blob | string,
  onLog?: (msg: string) => void,
): Promise<ExtractAudioResult> {
  const { ffmpeg, fetchFile } = await loadFFmpeg(onLog);

  const ext = typeof source === 'string'
    ? 'mp4'
    : source instanceof File
      ? (source.name.split('.').pop() ?? 'mp4')
      : 'mp4';
  const inName = `vid_in.${ext}`;
  const outName = 'audio.wav';
  const data = await fetchFile(source);
  await ffmpeg.writeFile(inName, data);

  try {
    await ffmpeg.exec([
      '-i', inName,
      '-vn',
      '-acodec', 'pcm_s16le',
      '-ar', '16000',
      '-ac', '1',
      outName,
    ]);
  } finally {
    try { await ffmpeg.deleteFile(inName); } catch { /* ignore */ }
  }

  const wav = await ffmpeg.readFile(outName);
  const wavBytes = wav instanceof Uint8Array ? wav : new TextEncoder().encode(String(wav));
  try { await ffmpeg.deleteFile(outName); } catch { /* ignore */ }

  // Decode the wav through the browser's AudioContext.
  const AC = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
  const ctx = new AC();
  const copy = wavBytes.slice().buffer;
  const audioBuffer = await ctx.decodeAudioData(copy);
  try { await ctx.close(); } catch { /* ignore */ }

  return { audioBuffer, wavBytes };
}

export interface ExportInput {
  assets: MediaAsset[];
  tracks: Track[];
  clips: Clip[];
  settings: ExportSettings;
  subtitles?: SubtitleCue[];
  subtitleStyle?: SubtitleStyle;
  textOverlays?: TextOverlay[];
  onProgress?: (pct: number, log?: string) => void;
}

/**
 * Render the current timeline to an mp4 Blob using FFmpeg.wasm.
 *
 * Steps:
 *   1. Lazy-load the wasm core (cached singleton)
 *   2. Write every referenced asset into the wasm virtual filesystem
 *   3. Build the filter_complex graph (see ./filter-graph)
 *   4. Run `ffmpeg.exec(...)` with progress reporting
 *   5. Read the output, copy into a fresh buffer (Blob type contract),
 *      and best-effort clean up the tmp files.
 */
export async function exportProject(input: ExportInput): Promise<Blob> {
  const { assets, tracks, clips, settings, onProgress, subtitles, subtitleStyle, textOverlays } = input;
  if (clips.length === 0) throw new Error('ไม่มีคลิปบนไทม์ไลน์');

  const { ffmpeg, fetchFile } = await loadFFmpeg((m) => onProgress?.(0, m));
  const report = (pct: number, log?: string) => onProgress?.(pct, log);

  // Stage every distinct asset into the wasm FS once.
  const used = collectAssetPaths(clips, assets);
  const written = new Set<string>();
  for (const { asset } of used) {
    if (written.has(asset.id)) continue;
    const data = await fetchFile(await (await fetch(asset.url)).blob());
    await ffmpeg.writeFile(`${asset.id}.bin`, data);
    written.add(asset.id);
  }

  // If we have subtitles, write an ASS file with the user's style settings.
  let assFile: string | null = null;
  if (subtitles && subtitles.length > 0) {
    assFile = 'subs.ass';
    const style = subtitleStyle ?? DEFAULT_SUBTITLE_STYLE;
    const assContent = buildAss(subtitles, style, settings.width, settings.height);
    await ffmpeg.writeFile(assFile, new TextEncoder().encode(assContent));
  }

  // Compose the graph + argv.
  let graph = buildFilterGraph(clips, assets, tracks, settings);
  // Floating text overlays → drawtext chain.
  if (textOverlays && textOverlays.length > 0 && graph.finalVideoLabel) {
    graph = applyTextOverlays(graph, textOverlays, settings);
  }
  // If subtitles are present, burn them into the video.
  if (assFile && graph.finalVideoLabel) {
    const sub = applySubtitleFilter(graph, assFile, settings);
    graph = { ...sub.graph };
  }
  const outName = 'out.mp4';
  const built = buildArgs(graph, settings, outName);

  report(10, `คำสั่ง: ffmpeg ${built.args.slice(0, 8).join(' ')} …`);

  // Drive `exec` with progress throttled to 1% increments.
  let lastPct = 10;
  const progHandler = ({ progress }: { progress: number }) => {
    if (Number.isFinite(progress)) {
      const pct = 10 + Math.round(progress * 85);
      if (pct > lastPct) {
        lastPct = pct;
        report(pct);
      }
    }
  };
  ffmpeg.on('progress', progHandler);

  try {
    await ffmpeg.exec(built.args);
  } finally {
    ffmpeg.off('progress', progHandler);
  }

  const data = await ffmpeg.readFile(outName);
  report(100, 'ส่งออกสำเร็จ');

  // Best-effort cleanup so repeated exports don't pile up in wasm memory.
  try {
    for (const id of written) await ffmpeg.deleteFile(`${id}.bin`);
    await ffmpeg.deleteFile(outName);
    if (assFile) await ffmpeg.deleteFile(assFile);
  } catch {
    /* ignore */
  }

  // Copy into a fresh ArrayBuffer to satisfy Blob's BlobPart type contract
  // across TS lib variants (some want SharedArrayBuffer, some don't).
  const src = data instanceof Uint8Array ? data : new TextEncoder().encode(String(data));
  const copy = new Uint8Array(src.byteLength);
  copy.set(src);
  return new Blob([copy.buffer], { type: 'video/mp4' });
}

/**
 * Build an ASS subtitle file from cues + style. This is a minimal but
 * complete ASS generator: header + style block + dialogue events.
 */
export function buildAss(
  cues: SubtitleCue[],
  style: SubtitleStyle,
  videoWidth: number,
  videoHeight: number,
): string {
  const toAssColor = (hex: string): string => {
    // Convert #RRGGBB or #RRGGBBAA to &HBBGGRR& (ASS uses &HAABBGGRR)
    let h = hex.replace('#', '');
    if (h.length === 3) h = h.split('').map((c) => c + c).join('');
    if (h.length === 6) h += '00';
    if (h.length < 8) return '&H00FFFFFF';
    const r = h.slice(0, 2);
    const g = h.slice(2, 4);
    const b = h.slice(4, 6);
    const a = h.slice(6, 8);
    return `&H${a}${b}${g}${r}`.toUpperCase();
  };

  const fontSize = Math.round(style.fontSize * (videoHeight / 720));
  const marginV = Math.round(videoHeight * 0.05);
  const marginLR = Math.round(videoWidth * 0.04);

  const alignment = style.position === 'top' ? 8 : style.position === 'middle' ? 5 : 2;

  const fg = toAssColor(style.color);
  const outline = toAssColor(style.outlineColor);
  const bg = style.background === 'none' ? '&H00000000' : toAssColor(style.background);

  // ASS uses centiseconds for time.
  const ts = (s: number) => {
    const h = Math.floor(s / 3600);
    const m = Math.floor((s % 3600) / 60);
    const sec = Math.floor(s % 60);
    const cs = Math.floor((s - Math.floor(s)) * 100);
    return `${h}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${cs.toString().padStart(2, '0')}`;
  };

  const escape = (s: string) => s.replace(/[{}\\]/g, (m) => `\\${m}`);

  const events = cues
    .map((c) => {
      const text = escape(c.text.replace(/\n/g, '\\N'));
      return `Dialogue: 0,${ts(c.start)},${ts(c.end)},Default,,0,0,0,,${text}`;
    })
    .join('\n');

  return [
    '[Script Info]',
    'ScriptType: v4.00+',
    'Collisions: Normal',
    'PlayResX: 1280',
    'PlayResY: 720',
    'ScaledBorderAndShadow: yes',
    'WrapStyle: 2',
    '',
    '[V4+ Styles]',
    'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
    `Style: Default,${style.fontFamily.split(',')[0]},${fontSize},${fg},${fg},${outline},${bg},${style.bold ? -1 : 0},0,0,0,100,100,0,0,1,${style.outlineWidth},1,${alignment},${marginLR},${marginLR},${marginV},1`,
    '',
    '[Events]',
    'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
    events,
    '',
  ].join('\n');
}