// Service Worker da extensão (MV3) — Fase 3:
//  - Fila persistente em IndexedDB (db.js)
//  - Worker em loop que consome a fila: reserva quota → baixa do CDN → upload Drive
//  - Comunicação com popup via mensagens + chrome.storage pra estado snapshot
//
// MV3 suspende SW após ~30s ocioso. Pra "manter vivo" enquanto há trabalho,
// usamos chrome.alarms (1 min) que reanima o SW e dispara mais um ciclo.

import * as DB from '../lib/db.js';
import * as Drive from '../lib/drive.js';
import * as API from '../lib/api.js';
import { processVideo, closeOffscreen } from '../lib/ffmpeg-bridge.js';
import { DEFAULT_SETTINGS, STORAGE_KEYS } from '../lib/config.js';

console.log('[ScaleReels Downloader] Service Worker iniciado');

const MAX_CONCURRENT = 2;
const RETRY_LIMIT = 2;
let _activeJobs = 0;
let _running = false;

// ── declarativeNetRequest: injeta Referer correto nos requests da CDN ────
// Sem esse Referer o CDN do TikTok/IG retorna HTML de erro 403.
// Usamos dynamic rules pra cobrir requests do próprio SW (fetch) e do chrome.downloads.
async function setupHeaderRules() {
  try {
    const rules = [
      {
        id: 1001,
        priority: 1,
        action: {
          type: 'modifyHeaders',
          requestHeaders: [
            { header: 'Referer', operation: 'set', value: 'https://www.tiktok.com/' },
            { header: 'Origin', operation: 'set', value: 'https://www.tiktok.com' }
          ]
        },
        condition: {
          requestDomains: ['tiktok.com', 'tiktokcdn.com', 'tiktokcdn-us.com', 'tiktokv.com'],
          initiatorDomains: [chrome.runtime.id],
          resourceTypes: ['xmlhttprequest', 'media', 'other', 'sub_frame', 'main_frame']
        }
      },
      {
        id: 1002,
        priority: 1,
        action: {
          type: 'modifyHeaders',
          requestHeaders: [
            { header: 'Referer', operation: 'set', value: 'https://www.instagram.com/' },
            { header: 'Origin', operation: 'set', value: 'https://www.instagram.com' }
          ]
        },
        condition: {
          requestDomains: ['instagram.com', 'cdninstagram.com', 'fbcdn.net'],
          initiatorDomains: [chrome.runtime.id],
          resourceTypes: ['xmlhttprequest', 'media', 'other', 'sub_frame', 'main_frame']
        }
      }
    ];
    await chrome.declarativeNetRequest.updateDynamicRules({
      removeRuleIds: [1001, 1002],
      addRules: rules
    });
    console.log('[IS-Ext] DNR rules de Referer registradas');
  } catch (err) {
    console.error('[IS-Ext] falhou registrar DNR rules:', err);
  }
}
setupHeaderRules();

// ── Lifecycle ───────────────────────────────────────────────────────────
chrome.runtime.onInstalled.addListener((details) => {
  console.log('[IS-Ext] onInstalled:', details.reason);
  if (details.reason === 'install') {
    // Abre a welcome page numa tab nova quando user instala pela 1ª vez
    chrome.tabs.create({ url: chrome.runtime.getURL('src/popup/welcome.html') });
  }
  // Cria alarm pra rebooter periodicamente quando há fila
  chrome.alarms.create('queue-tick', { periodInMinutes: 1 });
});

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'queue-tick') {
    runWorker().catch(e => console.error('[IS-Ext] worker tick err:', e));
  }
});

// ── Settings cache ──────────────────────────────────────────────────────
async function getSettings() {
  const s = await chrome.storage.local.get(STORAGE_KEYS.SETTINGS);
  return { ...DEFAULT_SETTINGS, ...(s[STORAGE_KEYS.SETTINGS] || {}) };
}

// ── Presets cache (moldura/watermark) ───────────────────────────────────
// Chamamos /preferences/frames e /watermarks no máximo a cada 5 min — o backend
// é leve mas evitar hammer. Cache em memória (resetado em SW restart) + storage
// como fallback longo prazo.
const PRESET_CACHE_TTL_MS = 5 * 60 * 1000;
const _presetCache = { frames: null, watermarks: null, frames_at: 0, watermarks_at: 0 };

async function getFramePreset(id) {
  if (!id) return null;
  if (Date.now() - _presetCache.frames_at > PRESET_CACHE_TTL_MS) {
    const r = await API.listFramePresets();
    if (r.ok && Array.isArray(r.data)) {
      _presetCache.frames = r.data;
      _presetCache.frames_at = Date.now();
    }
  }
  return (_presetCache.frames || []).find(p => p.id === id) || null;
}

async function getWatermarkPreset(id) {
  if (!id) return null;
  if (Date.now() - _presetCache.watermarks_at > PRESET_CACHE_TTL_MS) {
    const r = await API.listWatermarkPresets();
    if (r.ok && Array.isArray(r.data)) {
      _presetCache.watermarks = r.data;
      _presetCache.watermarks_at = Date.now();
    }
  }
  return (_presetCache.watermarks || []).find(p => p.id === id) || null;
}

// ── Messaging ───────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg?.type === 'PING') {
    sendResponse({ pong: true, ts: Date.now() });
    return false;
  }

  if (msg?.type === 'ENQUEUE_DOWNLOAD') {
    const items = Array.isArray(msg.items) ? msg.items : [];
    (async () => {
      try {
        const jobs = items.map(it => ({
          id: `${it.platform}:${it.id}`,
          platform: it.platform,
          source_id: String(it.id),
          kind: it.kind || 'video',
          url: it.url,
          cover: it.cover || null,
          author: it.author || null,
          author_name: it.author_name || null,
          desc: (it.desc || '').slice(0, 500),
          duration_ms: it.duration_ms || 0,
          source_url: it.source_url || null
        })).filter(j => j.id && j.url);

        const added = await DB.putManyJobs(jobs);
        const counts = await DB.countByStatus();
        sendResponse({ ok: true, added, total: counts.total, counts });
        // Dispara worker
        runWorker().catch(e => console.error('[IS-Ext] worker err:', e));
      } catch (err) {
        console.error('[IS-Ext] enqueue err:', err);
        sendResponse({ ok: false, error: err.message });
      }
    })();
    return true; // async response
  }

  if (msg?.type === 'GET_QUEUE') {
    (async () => {
      try {
        const all = await DB.listJobs();
        const counts = await DB.countByStatus();
        // Ordena: ativos primeiro, depois recentes
        all.sort((a, b) => {
          const order = ['downloading', 'processing', 'uploading', 'queued', 'failed', 'blocked', 'done'];
          return order.indexOf(a.status) - order.indexOf(b.status) || (b.updated_at || 0) - (a.updated_at || 0);
        });
        sendResponse({ ok: true, items: all.slice(0, 50), counts });
      } catch (err) {
        sendResponse({ ok: false, error: err.message });
      }
    })();
    return true;
  }

  if (msg?.type === 'CLEAR_QUEUE') {
    (async () => {
      const cleared = await DB.clearAll();
      sendResponse({ ok: true, cleared });
    })();
    return true;
  }

  if (msg?.type === 'CLEAR_DONE') {
    (async () => {
      const n = await DB.clearDone();
      sendResponse({ ok: true, cleared: n });
    })();
    return true;
  }

  if (msg?.type === 'RETRY_FAILED') {
    (async () => {
      const failed = await DB.listJobs({ statusIn: ['failed', 'blocked'] });
      for (const j of failed) await DB.updateJob(j.id, { status: 'queued', error: null });
      runWorker().catch(() => {});
      sendResponse({ ok: true, retried: failed.length });
    })();
    return true;
  }

  if (msg?.type === 'RUN_CHECKOUT_POLL') {
    // Popup pediu pra continuar polando o status do PIX mesmo se a janela fechar.
    pollCheckoutInBackground(msg.subscription_id);
    sendResponse({ ok: true });
    return false;
  }

  if (msg?.type === 'INVALIDATE_PRESETS_CACHE') {
    // Popup pediu pra recarregar molduras/watermarks (user criou algo novo no painel)
    if (msg.kind === 'frames' || !msg.kind) {
      _presetCache.frames = null;
      _presetCache.frames_at = 0;
    }
    if (msg.kind === 'watermarks' || !msg.kind) {
      _presetCache.watermarks = null;
      _presetCache.watermarks_at = 0;
    }
    sendResponse({ ok: true });
    return false;
  }

  if (msg?.type === 'GET_IMPORT_PREVIEW') {
    (async () => {
      try {
        const settings = await getSettings();
        // Pega user cacheado pra saber se tem addon
        const userCached = (await chrome.storage.local.get(STORAGE_KEYS.USER_INFO))[STORAGE_KEYS.USER_INFO];
        const hasAddon = !!(userCached?.has_addon_tools || userCached?.role === 'admin');

        // Pega listas leves de presets (id + name) pra dropdown inline no painel
        let framePresets = [];
        let wmPresets = [];
        if (hasAddon) {
          try {
            if (Date.now() - _presetCache.frames_at > PRESET_CACHE_TTL_MS) {
              const r = await API.listFramePresets();
              if (r.ok && Array.isArray(r.data)) {
                _presetCache.frames = r.data;
                _presetCache.frames_at = Date.now();
              }
            }
            framePresets = (_presetCache.frames || []).map(f => ({ id: f.id, name: f.name }));
          } catch (_) {}
          try {
            if (Date.now() - _presetCache.watermarks_at > PRESET_CACHE_TTL_MS) {
              const r = await API.listWatermarkPresets();
              if (r.ok && Array.isArray(r.data)) {
                _presetCache.watermarks = r.data;
                _presetCache.watermarks_at = Date.now();
              }
            }
            // Devolve TODAS as marcas; cliente marca as de texto como não usáveis.
            wmPresets = (_presetCache.watermarks || []).map(w => {
              const kind = w.kind || 'image';
              // Imagem: usável se tem image_data. Texto: usável se tem text (renderizado
              // dinamicamente via OffscreenCanvas no momento do download).
              const usable = kind === 'image' ? !!w.image_data : !!String(w.text || '').trim();
              return { id: w.id, name: w.name, kind, usable };
            });
          } catch (_) {}
        }

        const preview = {
          destination: settings.destination,
          anti_detect: !!settings.anti_detect,
          transcode_h264: !!settings.transcode_h264,
          apply_frame: !!settings.apply_frame,
          apply_watermark: !!settings.apply_watermark,
          auto_scroll: !!settings.auto_scroll,
          frame_preset_id: settings.frame_preset_id || null,
          watermark_preset_id: settings.watermark_preset_id || null,
          frame_presets: framePresets,
          watermark_presets: wmPresets,
          has_addon_tools: hasAddon,
          frame_name: null,
          watermark_name: null
        };
        if (settings.apply_frame && settings.frame_preset_id) {
          const fp = framePresets.find(p => p.id === settings.frame_preset_id);
          preview.frame_name = fp?.name || null;
        }
        if (settings.apply_watermark && settings.watermark_preset_id) {
          const wp = wmPresets.find(p => p.id === settings.watermark_preset_id);
          preview.watermark_name = wp?.name || null;
        }
        sendResponse({ ok: true, preview });
      } catch (err) {
        sendResponse({ ok: false, error: err.message });
      }
    })();
    return true;
  }

  if (msg?.type === 'PATCH_SETTINGS') {
    // Patch settings de qualquer lugar (popup OU strip do content script).
    // Bloqueia features premium se user não tem addon.
    (async () => {
      try {
        const current = await getSettings();
        const userCached = (await chrome.storage.local.get(STORAGE_KEYS.USER_INFO))[STORAGE_KEYS.USER_INFO];
        const hasAddon = !!(userCached?.has_addon_tools || userCached?.role === 'admin');
        const patch = { ...(msg.patch || {}) };
        // Gate premium
        if (!hasAddon) {
          if ('apply_frame' in patch) patch.apply_frame = false;
          if ('apply_watermark' in patch) patch.apply_watermark = false;
        }
        const next = { ...current, ...patch };
        await chrome.storage.local.set({ [STORAGE_KEYS.SETTINGS]: next });
        sendResponse({ ok: true, settings: next });
      } catch (err) {
        sendResponse({ ok: false, error: err.message });
      }
    })();
    return true;
  }

  if (msg?.type === 'GRANT_DRIVE') {
    (async () => {
      try {
        const t = await Drive.ensureDriveAuth({ interactive: true });
        sendResponse({ ok: !!t });
      } catch (err) {
        sendResponse({ ok: false, error: err.message });
      }
    })();
    return true;
  }

  return false;
});

// ── Worker loop ─────────────────────────────────────────────────────────
async function runWorker() {
  if (_running) return; // já tem outra invocação ativa neste SW
  _running = true;
  try {
    while (true) {
      // Verifica se pode pegar mais um (concorrência)
      if (_activeJobs >= MAX_CONCURRENT) {
        await sleep(800);
        continue;
      }
      const job = await DB.claimNextQueued();
      if (!job) break;

      _activeJobs++;
      processJob(job).finally(() => {
        _activeJobs--;
      });
      // Pequeno delay entre claims pra não esmagar o CPU
      await sleep(150);
    }
  } finally {
    _running = false;
    // Espera os jobs ativos terminarem, depois libera o offscreen pra economizar memória
    setTimeout(async () => {
      if (_activeJobs === 0) {
        try { await closeOffscreen(); } catch (_) {}
      }
    }, 5000);
  }
}

async function processJob(job) {
  try {
    // Switch global OFF: pausa o processamento dos jobs novos. Não cancela os que
    // já estão em flight — só para de aceitar trabalho daqui pra frente. Quando o
    // user religar, runWorker pega de onde parou.
    const _enabled = (await getSettings()).extension_enabled !== false;
    if (!_enabled) {
      // Volta o job pra fila pra reprocessar quando religar
      await DB.updateJob(job.id, { status: 'queued', error: 'pausado (extensão desligada)' });
      return;
    }
    console.log(`[IS-Ext] processando ${job.id}`);
    // 1) Reserva quota no backend (incrementa contador free se aplicável; 429 se acabou)
    const reserve = await API.reserveDownload(job.platform, job.source_url || job.url, {
      kind: job.kind,
      author: job.author
    });
    if (!reserve.ok) {
      const status = reserve.status;
      const reason = reserve.data?.reason || `quota error (HTTP ${status})`;
      await DB.updateJob(job.id, {
        status: status === 429 ? 'blocked' : 'failed',
        error: reason
      });
      return;
    }

    const settings = await getSettings();
    const filename = Drive.safeFilename({
      platform: job.platform,
      source_id: job.source_id,
      author: job.author,
      kind: job.kind
    });
    const mime = job.kind === 'image' ? 'image/jpeg' : 'video/mp4';

    // Calcula se precisa de ffmpeg (mode + presets de moldura/watermark)
    let mode = null;
    let framePayload = null;
    let watermarkPayload = null;
    if (job.kind === 'video') {
      if (settings.anti_detect && settings.transcode_h264) mode = 'both';
      else if (settings.transcode_h264) mode = 'transcode';
      else if (settings.anti_detect) mode = 'anti_detect';

      if (settings.apply_frame && settings.frame_preset_id) {
        const fp = await getFramePreset(settings.frame_preset_id);
        if (fp?.image_data) {
          framePayload = {
            dataUrl: fp.image_data,
            viewport_x: fp.viewport_x,
            viewport_y: fp.viewport_y,
            viewport_w: fp.viewport_w,
            viewport_h: fp.viewport_h
          };
        }
      }
      if (settings.apply_watermark && settings.watermark_preset_id) {
        const wp = await getWatermarkPreset(settings.watermark_preset_id);
        // Marca d'água tem 2 tipos: imagem (image_data já é dataURL pronto) ou
        // texto (precisa renderizar via OffscreenCanvas pra virar PNG).
        let wmDataUrl = wp?.image_data || null;
        if (!wmDataUrl && wp?.kind === 'text' && wp?.text) {
          try {
            wmDataUrl = await renderTextWatermarkToDataUrl(wp.text);
          } catch (e) {
            console.warn('[IS-Ext] falha ao renderizar watermark texto:', e.message);
          }
        }
        if (wmDataUrl) {
          watermarkPayload = {
            dataUrl: wmDataUrl,
            position: wp.position || 'br',
            opacity: wp.opacity ?? 0.7,
            manual_enabled: !!wp.manual_enabled,
            manual_x: wp.manual_x,
            manual_y: wp.manual_y,
            manual_scale: wp.manual_scale,
            manual_rotation: wp.manual_rotation
          };
        }
      }
    }
    const needsFFmpeg = !!(mode || framePayload || watermarkPayload);

    // Baixa o blob via page-world da aba (Referer correto, sem problema de CORS/CSP)
    await DB.updateJob(job.id, { status: 'downloading', bytes: 0 });
    let blob = await Drive.fetchAsBlob(job.url);
    // Camada extra de defesa: blob pode chegar aqui se passou pelas validações
    // dos inject scripts e do drive.js, mas vou checar tamanho mínimo pra
    // segurar arquivos com cabeçalho válido mas corpo absurdo.
    if (job.kind === 'video' && blob.size < 10 * 1024) {
      throw new Error(`vídeo absurdamente pequeno (${blob.size}b) — provavelmente download incompleto`);
    }
    let processInfo = null;
    let processedUrl = null;   // se o ffmpeg rodou, é uma blob URL do offscreen
    let processedSize = null;

    // Processamento ffmpeg (opcional) — só roda se a feature tá ligada E os binários instalados
    if (needsFFmpeg) {
      try {
        await DB.updateJob(job.id, { status: 'processing', bytes: blob.size });
        const result = await processVideo({ mode, blob, frame: framePayload, watermark: watermarkPayload });
        // Novo modelo: result NÃO tem mais .blob; tem .url (blob URL no offscreen).
        // Os bytes nunca atravessam mensagem chrome.runtime — só a URL.
        processedUrl = result.url;
        processedSize = result.size;
        processInfo = { mode, ...(result.info || {}), out_size: result.size };
        console.log(`[IS-Ext] ${job.id} processado:`, result.info?.steps?.join(' → ') || '(nada)');
      } catch (procErr) {
        console.warn(`[IS-Ext] ${job.id} processamento pulado: ${procErr.message}`);
        processInfo = { mode, error: procErr.message };
      }
    }

    // ═══ DESTINATION = LOCAL ═══════════════════════════════════════════════
    if (settings.destination === 'local') {
      const safeAuthor = String(job.author || 'unknown').replace(/[\/\\?%*:|"<>]/g, '_').slice(0, 60);
      const subfolder = `${settings.local_subfolder || 'ScaleReels Downloader'}/${job.platform}_@${safeAuthor}`;
      const finalSize = processedSize ?? blob.size;
      await DB.updateJob(job.id, { status: 'uploading', bytes: finalSize });
      let r;
      if (processedUrl) {
        // Chrome baixa direto da blob URL no offscreen — sem round-trip de bytes
        const path = subfolder
          ? `${String(subfolder).replace(/[\\<>:"|?*]/g, '_').replace(/^\/+|\/+$/g, '')}/${filename}`
          : filename;
        const id = await chrome.downloads.download({
          url: processedUrl, filename: path, saveAs: false, conflictAction: 'uniquify'
        });
        // Revoga a URL no offscreen depois de 2min — dá tempo do download terminar
        setTimeout(() => {
          chrome.runtime.sendMessage({
            target: 'offscreen-transcode', type: 'REVOKE_BLOB_URL', url: processedUrl
          }).catch(() => {});
        }, 120000);
        r = { id, filename: path };
      } else {
        // Sem processamento — usa o blob original que tá no SW
        r = await Drive.downloadLocal({ blob, filename, subfolder });
      }
      await DB.updateJob(job.id, { status: 'done', drive_file_id: null, bytes: finalSize, done_at: Date.now(), error: null });
      console.log(`[IS-Ext] OK ${job.id} → Downloads/${r.filename}`);
      return;
    }

    // ═══ DESTINATION = DRIVE ═══════════════════════════════════════════════
    // Se foi processado, pega os bytes via fetch da blob URL (mesma origem extensão)
    if (processedUrl) {
      const r = await fetch(processedUrl);
      blob = await r.blob();
      // Revoga URL imediatamente — já temos os bytes no SW
      chrome.runtime.sendMessage({
        target: 'offscreen-transcode', type: 'REVOKE_BLOB_URL', url: processedUrl
      }).catch(() => {});
    }
    await DB.updateJob(job.id, { status: 'uploading', bytes: blob.size });

    {
      // ──── Upload pro Google Drive ────────────────────────────────────
      let token;
      try {
        token = await Drive.ensureDriveAuth({ interactive: false });
      } catch (authErr) {
        await DB.updateJob(job.id, {
          status: 'failed',
          error: 'Sem autorização Drive. Conecte no popup ou troque pra "Baixar no computador" nas configurações.'
        });
        return;
      }

      const folderId = await Drive.ensureTargetFolder(token, {
        platform: job.platform,
        author: job.author,
        rootFolderName: settings.drive_root_folder
      });

      // Dedup
      const existing = await Drive.listFolderFilenames(token, folderId);
      if (existing.has(filename)) {
        await DB.updateJob(job.id, {
          status: 'done',
          done_at: Date.now(),
          error: 'já estava no Drive (skip)'
        });
        return;
      }

      const uploaded = await Drive.uploadFile(token, { blob, filename, folderId, mimeType: mime });

      await DB.updateJob(job.id, {
        status: 'done',
        drive_file_id: uploaded.id,
        bytes: Number(uploaded.size) || blob.size,
        done_at: Date.now(),
        error: null
      });

      // Notifica ScaleReels API (best-effort — só em modo Drive)
      try {
        await API.notifyImported({
          platform: job.platform,
          source_url: job.source_url || job.url,
          drive_file_id: uploaded.id,
          filename,
          processed: processInfo || null
        });
      } catch (_) {}

      console.log(`[IS-Ext] OK ${job.id} → Drive ${uploaded.id}`);
    }
  } catch (err) {
    console.error(`[IS-Ext] job ${job.id} falhou:`, err);
    const attempts = (job.attempts || 0);
    if (attempts < RETRY_LIMIT) {
      // Devolve pra fila
      await DB.updateJob(job.id, { status: 'queued', error: err.message });
    } else {
      await DB.updateJob(job.id, { status: 'failed', error: err.message });
    }
  }
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// Renderiza um texto de watermark via OffscreenCanvas → PNG transparente.
// Resultado vai como dataUrl pro offscreen ffmpeg, que faz overlay normal igual
// a uma imagem. Antes só aceitávamos watermark do tipo "imagem" — agora texto
// também funciona, sem precisar de fonte embarcada no ffmpeg-core.wasm.
async function renderTextWatermarkToDataUrl(text) {
  const raw = String(text || '').trim();
  if (!raw) throw new Error('texto vazio');
  // Canvas largo o suficiente pra textos médios; ffmpeg redimensiona via scale2ref
  // pra 22% da largura do vídeo (ou manual_scale*22%) — então o tamanho real é o que
  // controla a proporção, não a resolução absoluta.
  const fontSize = 96;
  const padX = 40;
  const padY = 24;
  // Mede o texto antes pra calcular tamanho exato do canvas
  const measureCanvas = new OffscreenCanvas(10, 10);
  const mctx = measureCanvas.getContext('2d');
  mctx.font = `900 ${fontSize}px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`;
  const m = mctx.measureText(raw);
  const textW = Math.ceil(m.width);
  const textH = Math.ceil(fontSize * 1.25);
  const W = textW + padX * 2;
  const H = textH + padY * 2;

  const canvas = new OffscreenCanvas(W, H);
  const ctx = canvas.getContext('2d');
  // Fundo totalmente transparente (ffmpeg controla opacity depois via colorchannelmixer)
  ctx.clearRect(0, 0, W, H);
  // Sombra preta sutil pra legibilidade em qualquer vídeo
  ctx.font = `900 ${fontSize}px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.shadowColor = 'rgba(0,0,0,0.85)';
  ctx.shadowBlur = 14;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 3;
  ctx.fillStyle = '#ffffff';
  ctx.fillText(raw, W / 2, H / 2);
  // Segundo passe sem sombra pra dar mais opacidade no texto branco em si
  ctx.shadowColor = 'transparent';
  ctx.fillText(raw, W / 2, H / 2);

  const blob = await canvas.convertToBlob({ type: 'image/png' });
  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 `data:image/png;base64,${btoa(s)}`;
}

// ── Checkout polling em background ──────────────────────────────────────
// Quando o popup fecha durante um checkout PIX, o setInterval do popup morre.
// O SW assume e continua checando o status até ativar/cancelar (ou 30 min).
// Quando ativa, mostra notificação Chrome.
const _activePolls = new Set();

async function pollCheckoutInBackground(subscriptionId) {
  if (!subscriptionId || _activePolls.has(subscriptionId)) return;
  _activePolls.add(subscriptionId);

  const start = Date.now();
  const MAX_MS = 30 * 60 * 1000;
  try {
    while (Date.now() - start < MAX_MS) {
      await sleep(5000);
      const r = await API.getCheckoutStatus(subscriptionId);
      if (!r.ok) continue;
      const status = r.data?.status;
      if (status === 'active') {
        // Limpa estado salvo do popup
        await chrome.storage.local.remove('is_checkout_state');
        // Refresh /me e quota cache pra próximas chamadas verem o user como ilimitado
        await API.getMe().catch(() => {});
        // Notificação Chrome
        try {
          chrome.notifications?.create({
            type: 'basic',
            iconUrl: chrome.runtime.getURL('public/icons/icon128.png'),
            title: 'ScaleReels Downloader',
            message: 'Pagamento confirmado! Sua extensão está ilimitada.'
          });
        } catch (_) {}
        return;
      }
      if (status === 'cancelled') {
        await chrome.storage.local.remove('is_checkout_state');
        return;
      }
    }
  } finally {
    _activePolls.delete(subscriptionId);
  }
}

// Retomar polling pendente quando SW reinicia (caso o user pague depois de
// fechar o popup E o SW ter sido suspendido)
chrome.runtime.onStartup.addListener(async () => {
  const s = await chrome.storage.local.get('is_checkout_state');
  const cur = s['is_checkout_state'];
  if (cur?.subscription_id && (Date.now() - (cur.created_at || 0)) < 30 * 60 * 1000) {
    pollCheckoutInBackground(cur.subscription_id);
  }
});

// Trigger inicial — caso o SW reanime com jobs queued (after install/update)
runWorker().catch(() => {});
