// Offscreen document — ffmpeg.wasm + camera profiles + frame/watermark overlays.
//
// Pipeline (cada passo é uma chamada do ffmpeg; só roda se a opção estiver ativa):
//   1) base       → transcode H.264 / -c copy   (sempre roda algo: copy quando nada selecionado)
//   2) frame      → apply_frame_mockup          (overlay PNG/JPG/GIF + viewport)
//   3) watermark  → apply_light_watermark_image (overlay com opacity, posição configurável)
//   4) anti-detect→ strip + inject metadata     (sempre por último, com -c copy)
//
// Cada passada lê 'cur.mp4' do FS e grava 'next.mp4'; depois 'next' vira o novo 'cur'.

import { chooseProfile, randomRecentDate, formatCreationTime, randomLocation } from '../lib/camera-profiles.js';

let _ffmpeg = null;
let _loading = null;

async function loadFFmpeg() {
  if (_ffmpeg) return _ffmpeg;
  if (_loading) return _loading;
  _loading = (async () => {
    const coreUrl = chrome.runtime.getURL('public/ffmpeg/ffmpeg-core.js');
    const wasmUrl = chrome.runtime.getURL('public/ffmpeg/ffmpeg-core.wasm');

    const check = await fetch(coreUrl, { method: 'HEAD' });
    if (!check.ok) {
      throw new Error('Binários ffmpeg.wasm não encontrados em public/ffmpeg/. Veja README.md dessa pasta.');
    }

    await new Promise((resolve, reject) => {
      const s = document.createElement('script');
      s.src = coreUrl;
      s.onload = resolve;
      s.onerror = () => reject(new Error('Falha ao carregar ffmpeg-core.js'));
      document.head.appendChild(s);
    });

    if (typeof self.createFFmpegCore !== 'function') {
      throw new Error('ffmpeg-core.js carregou mas não expôs createFFmpegCore (build incompatível)');
    }

    // Single-thread build (@ffmpeg/core-st@0.11.0): roda 100% na main thread,
    // sem SharedArrayBuffer/Atomics.wait → não trava em "Blocking on main thread".
    _ffmpeg = await self.createFFmpegCore({
      locateFile: (path) => {
        if (path.endsWith('.wasm')) return wasmUrl;
        return path;
      },
      print: (m) => console.log('[ffmpeg]', m),
      printErr: (m) => console.warn('[ffmpeg/err]', m),
      // Emscripten default: quando ffmpeg lê stdin (ex: "Overwrite [y/N]?"), chama
      // window.prompt() o que aparece como dialog do browser. Forçamos EOF imediato
      // pra não bloquear nem abrir dialog. Combinado com -y nos args, ffmpeg sobrescreve.
      stdin: () => null,
      noInitialRun: true
    });
    console.log('[Offscreen] ffmpeg.wasm pronto — FS shape:', typeof _ffmpeg.FS,
      typeof _ffmpeg.FS === 'object' ? Object.keys(_ffmpeg.FS).filter(k => typeof _ffmpeg.FS[k] === 'function').slice(0, 8) : '(fn)');
    return _ffmpeg;
  })();
  try { return await _loading; } finally { _loading = null; }
}

// Compat duplo:
//   - @ffmpeg/ffmpeg (wrapper antigo): ffmpeg.FS('writeFile', name, data)
//   - @ffmpeg/core (Emscripten direto): ffmpeg.FS.writeFile(name, data)
// Detecta qual está disponível e usa o certo.
function fsCall(ffmpeg, method, ...args) {
  if (typeof ffmpeg.FS === 'function') return ffmpeg.FS(method, ...args);
  if (ffmpeg.FS && typeof ffmpeg.FS[method] === 'function') return ffmpeg.FS[method](...args);
  // Último recurso: alguns builds expõem como FS_writeFile etc.
  const flat = `FS_${method}`;
  if (typeof ffmpeg[flat] === 'function') return ffmpeg[flat](...args);
  throw new Error(`FS.${method} indisponível no módulo ffmpeg (typeof FS=${typeof ffmpeg.FS})`);
}
function writeFS(ffmpeg, name, data) { return fsCall(ffmpeg, 'writeFile', name, data); }
function readFS(ffmpeg, name) { return fsCall(ffmpeg, 'readFile', name); }
function unlinkSafe(ffmpeg, name) {
  try { fsCall(ffmpeg, 'unlink', name); } catch (_) {}
}

// @ffmpeg/core 0.10/0.11 multi-thread NÃO expõe callMain, então reproduzimos ele
// montando argv no heap do wasm e chamando ccall('main', ...). Se callMain estiver
// disponível (builds futuros / wrappers), usa direto.
function callFFmpegMain(ffmpeg, args) {
  if (typeof ffmpeg.callMain === 'function') {
    return ffmpeg.callMain(args);
  }
  if (typeof ffmpeg.ccall !== 'function') {
    throw new Error('ffmpeg.ccall indisponível — build incompatível');
  }
  const fullArgs = ['ffmpeg', ...args.map(String)];
  const strPtrs = [];
  for (const arg of fullArgs) {
    const bytes = new TextEncoder().encode(arg);
    const ptr = ffmpeg._malloc(bytes.length + 1);
    ffmpeg.HEAPU8.set(bytes, ptr);
    ffmpeg.HEAPU8[ptr + bytes.length] = 0;
    strPtrs.push(ptr);
  }
  const argvPtr = ffmpeg._malloc(strPtrs.length * 4);
  for (let i = 0; i < strPtrs.length; i++) {
    ffmpeg.HEAPU32[(argvPtr >> 2) + i] = strPtrs[i];
  }
  // _proxy_main é async (spawn pthread + retorna), _main é síncrono. Forçamos _main
  // pra que ffmpeg termine antes do ccall retornar e a gente possa ler o output.
  const mainFn = 'main';
  let code = 0;
  try {
    code = ffmpeg.ccall(mainFn, 'number', ['number', 'number'], [fullArgs.length, argvPtr]);
  } catch (e) {
    if (e?.name === 'ExitStatus') code = e.status;
    else throw e;
  } finally {
    for (const p of strPtrs) ffmpeg._free(p);
    ffmpeg._free(argvPtr);
  }
  return code;
}

async function bufferToFS(ffmpeg, name, arrayBuffer) {
  writeFS(ffmpeg, name, new Uint8Array(arrayBuffer));
}

// ── Detector de dimensões de imagem (PNG/JPG/GIF/WebP) ──────────────────
// ffmpeg.wasm não traz ffprobe; usamos createImageBitmap do DOM (offscreen tem).
async function imageDims(arrayBuffer, mime) {
  const blob = new Blob([arrayBuffer], { type: mime });
  const bmp = await createImageBitmap(blob);
  return { w: bmp.width, h: bmp.height };
}

// ── Constrói filter_complex de moldura — porta do backend Python ────────
function buildFrameFilter({ frame_w, frame_h, viewport_x, viewport_y, viewport_w, viewport_h }) {
  let vx = Math.max(0, Math.min(1, +viewport_x));
  let vy = Math.max(0, Math.min(1, +viewport_y));
  let vw = Math.max(0.05, Math.min(1, +viewport_w));
  let vh = Math.max(0.05, Math.min(1, +viewport_h));
  if (vx + vw > 1) vw = Math.max(0.05, 1 - vx);
  if (vy + vh > 1) vh = Math.max(0.05, 1 - vy);

  const vp_x = Math.round(vx * frame_w);
  const vp_y = Math.round(vy * frame_h);
  let vp_w = Math.round(vw * frame_w);
  let vp_h = Math.round(vh * frame_h);
  vp_w = Math.max(2, Math.min(frame_w - vp_x, vp_w));
  vp_h = Math.max(2, Math.min(frame_h - vp_y, vp_h));

  const clear_x = Math.max(0, vp_x - 6);
  const clear_y = Math.max(0, vp_y - 6);
  const clear_w = Math.max(2, Math.min(frame_w - clear_x, vp_w + 12));
  const clear_h = Math.max(2, Math.min(frame_h - clear_y, vp_h + 12));
  const label_x = Math.max(0, Math.min(frame_w - 2, vp_x + 4));
  const label_y = Math.max(0, Math.min(frame_h - 2, vp_y + 4));
  const label_w = Math.max(2, Math.min(frame_w - label_x, 180));
  const label_h = Math.max(2, Math.min(frame_h - label_y, 36));

  return (
    `[0:v]scale=${vp_w}:${vp_h}:force_original_aspect_ratio=increase,crop=${vp_w}:${vp_h}[vid];` +
    `[1:v]format=rgba,` +
    `drawbox=x=${clear_x}:y=${clear_y}:w=${clear_w}:h=${clear_h}:color=black@0.0:t=fill:replace=1,` +
    `drawbox=x=${label_x}:y=${label_y}:w=${label_w}:h=${label_h}:color=black@0.0:t=fill:replace=1[frame];` +
    `color=size=${frame_w}x${frame_h}:color=black@1.0[bg];` +
    `[bg][vid]overlay=${vp_x}:${vp_y}[base];` +
    `[base][frame]overlay=0:0:format=auto:shortest=1,format=yuv420p[v]`
  );
}

// ── Constrói filter_complex de watermark — porta do backend Python ──────
function overlayXY(position) {
  // tl, tr, bl, br, center
  const margin = 24;
  switch ((position || 'br').toLowerCase()) {
    case 'tl': return [`${margin}`, `${margin}`];
    case 'tr': return [`main_w-overlay_w-${margin}`, `${margin}`];
    case 'bl': return [`${margin}`, `main_h-overlay_h-${margin}`];
    case 'center': return [`(main_w-overlay_w)/2`, `(main_h-overlay_h)/2`];
    case 'br':
    default: return [`main_w-overlay_w-${margin}`, `main_h-overlay_h-${margin}`];
  }
}

function buildWatermarkFilter({ position, opacity, manual_enabled, manual_x, manual_y, manual_scale, manual_rotation }) {
  const op = (Math.max(0.05, Math.min(1, +opacity || 1))).toFixed(2);
  if (manual_enabled) {
    const scale_factor = Math.max(0.03, Math.min(0.85, 0.22 * (+manual_scale || 1)));
    const rotation_rad = ((+manual_rotation || 0) * Math.PI / 180);
    const mx = (+manual_x ?? 0.5);
    const my = (+manual_y ?? 0.92);
    return (
      `[1:v][0:v]scale2ref=w=main_w*${scale_factor.toFixed(5)}:h=-1[wm][base];` +
      `[wm]format=rgba,colorchannelmixer=aa=${op},rotate=${rotation_rad.toFixed(6)}:c=none:ow=rotw(iw):oh=roth(ih)[wm2];` +
      `[base][wm2]overlay=x=main_w*${mx.toFixed(5)}-overlay_w/2:y=main_h*${my.toFixed(5)}-overlay_h/2:format=auto:shortest=1,format=yuv420p[v]`
    );
  }
  const [x_expr, y_expr] = overlayXY(position);
  return (
    `[1:v][0:v]scale2ref=w=main_w*0.22:h=-1[wm][base];` +
    `[wm]format=rgba,colorchannelmixer=aa=${op}[wm2];` +
    `[base][wm2]overlay=x=${x_expr}:y=${y_expr}:format=auto:shortest=1,format=yuv420p[v]`
  );
}

// ── Args do anti-detect (strip + inject metadata) ───────────────────────
// Estratégia: NÃO usar como step separado. Injetar essas flags como output options
// no MESMO ffmpeg call que faz transcode/frame/watermark. Uma chamada do ffmpeg em
// vez de duas. Evita o rc=1 que estava vindo de inconsistência entre o output de
// uma chamada e o -c copy + metadata da próxima.
function buildAntiDetectArgs(profileLabel) {
  const profile = chooseProfile(profileLabel);
  const recDate = randomRecentDate();
  const dateStr = formatCreationTime(recDate);
  // Args mínimos comprovados — Python testou esses por anos:
  //   -map_metadata -1 strip toda metadata do input
  //   -metadata <key>=<val> injeta os fakes
  // Removemos location/location-eng/map_chapters/encoder:s:v/handler:s:v porque o
  // ffmpeg-core@0.11 single-thread rejeita combinações específicas (rc=1).
  return {
    args: [
      '-map_metadata', '-1',
      '-metadata', `make=${profile.make}`,
      '-metadata', `model=${profile.model}`,
      '-metadata', `software=${profile.software}`,
      '-metadata', `creation_time=${dateStr}`
    ],
    info: { profile: profile.label, creation_time: dateStr }
  };
}

function ext(mime) {
  if (mime?.includes('jpeg')) return '.jpg';
  if (mime?.includes('gif')) return '.gif';
  if (mime?.includes('webp')) return '.webp';
  return '.png';
}

// ── Pipeline principal ─────────────────────────────────────────────────
async function processVideo({ mode, videoBuf, videoMime, frame, watermark, profileLabel }) {
  console.log('[Offscreen/proc] iniciando — mode:', mode, 'frame:', !!frame?.buffer, 'wm:', !!watermark?.buffer, 'inputBytes:', videoBuf?.byteLength);
  const ffmpeg = await loadFFmpeg();
  console.log('[Offscreen/proc] ffmpeg pronto');

  // Estado inicial: vídeo cru em cur.mp4
  await bufferToFS(ffmpeg, 'cur.mp4', videoBuf);
  console.log('[Offscreen/proc] cur.mp4 escrito,', videoBuf.byteLength, 'bytes');
  let currentName = 'cur.mp4';
  let stepIdx = 0;
  const info = { steps: [] };

  // Cada step é "best-effort": se um falhar, segue com o último output bom em vez
  // de jogar tudo fora. Isso evita que uma falha em anti_detect (rc=1 por metadata
  // key incompatível com o build) faça o user perder a moldura/watermark que já
  // rodaram com sucesso.
  const runStep = (args, label) => {
    stepIdx++;
    const nextName = `step${stepIdx}.mp4`;
    console.log(`[Offscreen/proc] step ${stepIdx} (${label}) → ${nextName}`);
    const t0 = Date.now();
    let code = -1;
    try {
      code = callFFmpegMain(ffmpeg, args.concat([nextName]));
    } catch (e) {
      console.error(`[Offscreen/proc] step ${label} THREW:`, e?.message || e);
    }
    const dt = Date.now() - t0;
    if (code !== 0) {
      console.warn(`[Offscreen/proc] step ${stepIdx} (${label}) FALHOU rc=${code} em ${dt}ms — mantendo output anterior`);
      unlinkSafe(ffmpeg, nextName);  // arquivo parcial (se houver)
      info.failed_steps = info.failed_steps || [];
      info.failed_steps.push(label);
      return;
    }
    console.log(`[Offscreen/proc] step ${stepIdx} (${label}) OK em ${dt}ms`);
    unlinkSafe(ffmpeg, currentName);
    currentName = nextName;
    info.steps.push(label);
  };

  // Quais steps vão rodar? Pra saber qual será o ÚLTIMO encoding (onde injetamos
  // a metadata fake de anti-detect, em vez de um step extra que falhava com rc=1).
  const needsTranscodeBase = mode === 'transcode' || mode === 'both';
  const needsFrame = !!(frame && frame.buffer);
  const needsWatermark = !!(watermark && watermark.buffer);
  const needsAntiDetect = mode === 'anti_detect' || mode === 'both';

  const ad = needsAntiDetect ? buildAntiDetectArgs(profileLabel) : null;
  // Calcula qual será a última etapa de encoding pra anexar metadata lá.
  const lastEncodingStep = needsWatermark ? 'watermark'
                         : needsFrame ? 'frame'
                         : needsTranscodeBase ? 'transcode'
                         : null;
  const appendMeta = (label) => (ad && lastEncodingStep === label) ? ad.args : [];

  // STEP 1: transcode base (libx264 + AAC + faststart)
  // Qualidade máxima (custa tempo no PC do user, mas é one-shot):
  //   - preset slow: comprime melhor que veryfast no mesmo CRF
  //   - crf 20: alta qualidade (vs 23 default)
  //   - maxrate/bufsize: IG aceita até ~25Mbps; 14M dá margem sem inchar muito
  //   - profile high + level 4.1: aceito pelo IG e melhor que baseline
  //   - audio 192k 48kHz stereo: padrão IG (vs 128k anterior)
  if (needsTranscodeBase) {
    runStep(
      ['-y', '-i', currentName,
        '-c:v', 'libx264', '-preset', 'slow', '-crf', '20', '-pix_fmt', 'yuv420p',
        '-profile:v', 'high', '-level', '4.1',
        '-maxrate', '14M', '-bufsize', '28M',
        '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
        '-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
        '-movflags', '+faststart',
        ...appendMeta('transcode')
      ],
      'transcode_h264'
    );
  }

  // STEP 2: moldura mockup 9:16
  if (needsFrame) {
    const fext = ext(frame.mime);
    await bufferToFS(ffmpeg, `frame${fext}`, frame.buffer);
    const dims = await imageDims(frame.buffer, frame.mime);
    const filter = buildFrameFilter({
      frame_w: dims.w, frame_h: dims.h,
      viewport_x: frame.viewport_x, viewport_y: frame.viewport_y,
      viewport_w: frame.viewport_w, viewport_h: frame.viewport_h
    });
    const isGif = frame.mime?.includes('gif');
    const loopArgs = isGif ? ['-stream_loop', '-1'] : ['-loop', '1'];
    runStep(
      ['-y', '-i', currentName, ...loopArgs, '-i', `frame${fext}`,
        '-filter_complex', filter,
        '-map', '[v]', '-map', '0:a?',
        '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', '20',
        '-profile:v', 'high', '-level', '4.1',
        '-maxrate', '14M', '-bufsize', '28M',
        '-c:a', 'copy', '-shortest', '-movflags', '+faststart',
        ...appendMeta('frame')
      ],
      'frame_mockup'
    );
    unlinkSafe(ffmpeg, `frame${fext}`);
  }

  // STEP 3: watermark
  if (needsWatermark) {
    const wext = ext(watermark.mime);
    await bufferToFS(ffmpeg, `wm${wext}`, watermark.buffer);
    const filter = buildWatermarkFilter({
      position: watermark.position,
      opacity: watermark.opacity,
      manual_enabled: watermark.manual_enabled,
      manual_x: watermark.manual_x,
      manual_y: watermark.manual_y,
      manual_scale: watermark.manual_scale,
      manual_rotation: watermark.manual_rotation
    });
    const isGif = watermark.mime?.includes('gif');
    const loopArgs = isGif ? ['-stream_loop', '-1'] : ['-loop', '1'];
    runStep(
      ['-y', '-i', currentName, ...loopArgs, '-i', `wm${wext}`,
        '-filter_complex', filter,
        '-map', '[v]', '-map', '0:a?',
        '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', '20',
        '-profile:v', 'high', '-level', '4.1',
        '-maxrate', '14M', '-bufsize', '28M',
        '-c:a', 'copy', '-shortest', '-movflags', '+faststart',
        ...appendMeta('watermark')
      ],
      'watermark'
    );
    unlinkSafe(ffmpeg, `wm${wext}`);
  }

  // STEP 4 (só roda se anti_detect foi pedido SEM nenhuma re-codificação):
  // -c copy + metadata. É o caso "limpar metadata sem mexer no encoding".
  if (needsAntiDetect && !lastEncodingStep) {
    runStep(['-y', '-i', currentName, ...ad.args, '-c', 'copy'], 'anti_detect');
  }
  if (ad) Object.assign(info, ad.info);

  // Se nenhum step rodou, o currentName ainda é 'cur.mp4' (input cru) — só lê isso.
  const data = readFS(ffmpeg, currentName);
  const outBlob = new Blob([data.buffer], { type: 'video/mp4' });
  unlinkSafe(ffmpeg, currentName);

  return { blob: outBlob, info };
}

// ── Listener de mensagens do SW ─────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg?.target !== 'offscreen-transcode') return false;

  if (msg.type === 'PING') { sendResponse({ pong: true }); return false; }

  if (msg.type === 'CREATE_BLOB_URL') {
    // chrome.downloads NÃO é disponível em offscreen — só cria a URL aqui (DOM works)
    // e devolve pro SW chamar chrome.downloads.download.
    try {
      // SW manda em base64 (chrome.runtime.sendMessage JSON-ifica ArrayBuffer)
      const bin = atob(msg.b64 || '');
      const u8 = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
      const blob = new Blob([u8], { type: msg.mime || 'video/mp4' });
      const url = URL.createObjectURL(blob);
      sendResponse({ ok: true, url });
    } catch (err) {
      console.error('[Offscreen] createBlobUrl err:', err);
      sendResponse({ ok: false, error: err.message || String(err) });
    }
    return false; // sync
  }

  if (msg.type === 'REVOKE_BLOB_URL') {
    try { URL.revokeObjectURL(msg.url); } catch (_) {}
    sendResponse({ ok: true });
    return false;
  }

  if (msg.type === 'PROCESS_VIDEO') {
    (async () => {
      try {
        const videoBuf = b64ToArrayBuffer(msg.videoB64);
        const frame = msg.frame?.b64
          ? { ...msg.frame, buffer: b64ToArrayBuffer(msg.frame.b64) }
          : null;
        const watermark = msg.watermark?.b64
          ? { ...msg.watermark, buffer: b64ToArrayBuffer(msg.watermark.b64) }
          : null;

        const result = await processVideo({
          mode: msg.mode,
          videoBuf,
          videoMime: msg.videoMime || 'video/mp4',
          frame,
          watermark,
          profileLabel: msg.profileLabel || null
        });
        // Em vez de mandar os bytes do output de volta via base64 (lento, 2x o tamanho
        // e arriscado em arquivos >20MB), guardamos o blob aqui no offscreen e devolvemos
        // só a blob URL. O SW usa essa URL direto em chrome.downloads.download OU faz
        // fetch(url) quando precisa dos bytes pro upload do Drive.
        const url = URL.createObjectURL(result.blob);
        console.log(`[Offscreen] processado, blob URL criada (${result.blob.size} bytes)`);
        sendResponse({
          ok: true,
          url,
          mime: 'video/mp4',
          size: result.blob.size,
          info: result.info
        });
      } catch (err) {
        console.error('[Offscreen] erro:', err);
        sendResponse({ ok: false, error: err.message || String(err) });
      }
    })();
    return true; // async
  }
  return false;
});

// ── Helpers de base64 ────────────────────────────────────────────────────
function b64ToArrayBuffer(b64) {
  const bin = atob(b64 || '');
  const u8 = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
  return u8.buffer;
}

async function blobToB64String(blob) {
  const buf = await blob.arrayBuffer();
  const u8 = new Uint8Array(buf);
  let s = '';
  const chunk = 0x8000;
  for (let i = 0; i < u8.length; i += chunk) {
    s += String.fromCharCode.apply(null, u8.subarray(i, i + chunk));
  }
  return btoa(s);
}

console.log('[Offscreen] transcode.js carregado, aguardando mensagens');
