// inject-ig.js — patches fetch/XHR no Instagram pra capturar Reels/Posts/Stories.
//
// Endpoints relevantes:
//   /api/v1/feed/user/{user_id}/                  (perfil feed)
//   /api/v1/feed/reels_media/?reel_ids=...        (stories de um user)
//   /api/v1/feed/reels_tray/                      (stories tray)
//   /api/v1/users/web_profile_info/?username=...  (info perfil + alguns medias)
//   /api/v1/clips/user/                           (Reels do perfil)
//   /api/v1/feed/timeline/                        (feed home)
//   /graphql/query/                               (legacy GraphQL — vários shapes)
//
// IG retorna `video_versions[]` (sortido por bandwidth) e `image_versions2.candidates[]`.

(function () {
  if (window.__IS_IG_INJECT__) return;
  window.__IS_IG_INJECT__ = true;

  // Log de boot SEMPRE visível pra confirmar que o inject carregou (importante
  // pra debug com user — se ele não vê essa linha, o manifest não tá injetando).
  try {
    console.log('%c[IS-Ext/IG] inject ativo @ ' + location.pathname + ' (readyState=' + document.readyState + ')', 'color:#a855f7;font-weight:bold');
  } catch (_) {}

  const POST_TYPE = 'IS_EXT_IG_ITEMS';
  // Cobertura ampla: qualquer endpoint /api/v1/* (todos os shapes — feed, clips, reels,
  // highlights, web_profile_info, etc) + /api/graphql + /graphql/query. Custo de varrer
  // requests irrelevantes é baixo — se extractItems não achar media, retorna vazio.
  //
  // Expansão veio de relatos do user de que a aba /username/reels/ não estava puxando
  // — o IG usa shapes/endpoints diferentes na aba Reels do perfil (clips/user) que
  // a regex anterior já cobria, mas vamos largar a peneira pra cobrir variants futuros.
  const ENDPOINTS_RE = /\/api\/(v1|graphql)\/|\/graphql\/query\b/i;
  // Debug: localStorage.setItem('is_ext_dbg', '1') ativa logs detalhados
  const DBG = (() => { try { return localStorage.getItem('is_ext_dbg') === '1'; } catch (_) { return false; } })();

  function safeParse(text) {
    try { return JSON.parse(text); } catch (_) { return null; }
  }

  // IG aninha medias em vários paths diferentes — coleta tudo recursivamente.
  function collectMediaNodes(obj, out, depth = 0) {
    if (!obj || depth > 12) return;
    if (Array.isArray(obj)) {
      for (const v of obj) collectMediaNodes(v, out, depth + 1);
      return;
    }
    if (typeof obj !== 'object') return;

    // Shape 1: items[] no feed/perfil
    if (Array.isArray(obj.items)) {
      for (const it of obj.items) out.push(it);
    }
    // Shape 2: media direto (com video/image)
    if (obj.media && typeof obj.media === 'object' && (obj.media.video_versions || obj.media.carousel_media || obj.media.image_versions2)) {
      out.push(obj.media);
    }
    // Shape 3: edges -> node (GraphQL — Reels também usa em alguns shapes)
    if (Array.isArray(obj.edges)) {
      for (const e of obj.edges) {
        if (e?.node) out.push(e.node);
      }
    }
    // Shape 4: carousel children dentro de item
    if (Array.isArray(obj.carousel_media)) {
      for (const cm of obj.carousel_media) out.push(cm);
    }
    // Shape 5: aba Reels do perfil — endpoint /api/v1/clips/user/ retorna
    // { items: [{ media: {video_versions,user,...}, clip_metadata: {...} }] }
    // O item EXTERNO não tem `id` mas envolve `.media`. Já capturado via Shape 2
    // dentro da recursão, MAS forçamos aqui pra evitar caso media seja shape novo
    // sem nenhuma das chaves esperadas em Shape 2.
    if (obj.clip_metadata && obj.media && typeof obj.media === 'object') {
      out.push(obj.media);
    }
    // Shape 6: paths novos — alguns retornos do GraphQL trazem em `xdt_api__v1__clips__home__connection`
    // ou similar. Largamos a peneira: qualquer objeto com `video_versions` ou `image_versions2`
    // direto vira candidato.
    if (depth > 0 && (Array.isArray(obj.video_versions) || obj.image_versions2?.candidates)) {
      out.push(obj);
    }
    // Recurse
    for (const k of Object.keys(obj)) {
      const v = obj[k];
      if (v && typeof v === 'object' && k !== 'image_versions2' && k !== 'video_versions') {
        collectMediaNodes(v, out, depth + 1);
      }
    }
  }

  function bestVideoUrl(item) {
    // Reels API moderno
    if (Array.isArray(item.video_versions) && item.video_versions.length) {
      // Maior bandwidth tende a ser o primeiro (ou marcado como type:101)
      const sorted = [...item.video_versions].sort((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0));
      return sorted[0].url;
    }
    // Fallback: item externo do clips/user envolve em `.media`
    if (item.media && Array.isArray(item.media.video_versions) && item.media.video_versions.length) {
      const sorted = [...item.media.video_versions].sort((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0));
      return sorted[0].url;
    }
    // GraphQL legacy
    if (item.video_url) return item.video_url;
    if (item.video_resources) {
      const r = item.video_resources.find(x => x.profile === 'MAIN') || item.video_resources[0];
      return r?.src || null;
    }
    return null;
  }

  function bestImageUrl(item) {
    // Tenta múltiplas localizações conhecidas. IG mudou shape várias vezes —
    // ordem: image_versions2 (top), image_versions2 (.media), display_url,
    // display_resources, thumbnail_src/url, first_frame_url/clip_metadata.
    const m = item?.media || {};
    const tryCandidates = (cands) => {
      if (!Array.isArray(cands) || !cands.length) return null;
      const sorted = [...cands].sort((a, b) => (b.width || 0) - (a.width || 0));
      return sorted[0].url || null;
    };
    let url = tryCandidates(item.image_versions2?.candidates)
           || tryCandidates(m.image_versions2?.candidates)
           // Alguns shapes têm `additional_candidates` em separado
           || tryCandidates(item.image_versions2?.additional_candidates?.first_frame
              ? [item.image_versions2.additional_candidates.first_frame] : null)
           || tryCandidates(m.image_versions2?.additional_candidates?.first_frame
              ? [m.image_versions2.additional_candidates.first_frame] : null);
    if (url) return url;
    // GraphQL legacy
    if (item.display_url) return item.display_url;
    if (m.display_url) return m.display_url;
    if (item.display_resources) return item.display_resources[item.display_resources.length - 1]?.src || null;
    if (m.display_resources) return m.display_resources[m.display_resources.length - 1]?.src || null;
    // Reels-specific: thumbnail_src / first_frame_url
    if (item.thumbnail_src) return item.thumbnail_src;
    if (m.thumbnail_src) return m.thumbnail_src;
    if (item.thumbnail_url) return item.thumbnail_url;
    if (m.thumbnail_url) return m.thumbnail_url;
    // Algumas respostas trazem o thumb dentro de clips_metadata
    if (item.clips_metadata?.original_sound_info?.thumbnail_url) return item.clips_metadata.original_sound_info.thumbnail_url;
    // Última tentativa: derivar da própria URL do vídeo se for assinada (tem ?_ig_*).
    // Não fazemos — pode bater 403 e arruinar mais que ajudar.
    return null;
  }

  function mediaKind(item) {
    // 1 = image, 2 = video, 8 = carousel
    const m = item.media; // wrapper do clips/user
    if (item.media_type === 8 || item.__typename === 'GraphSidecar' || m?.media_type === 8) return 'carousel';
    if (item.media_type === 2 || item.is_video || Array.isArray(item.video_versions)) return 'video';
    if (m?.media_type === 2 || m?.is_video || Array.isArray(m?.video_versions)) return 'video';
    if (item.media_type === 1 || item.__typename === 'GraphImage' || m?.media_type === 1) return 'image';
    if (Array.isArray(item.video_versions) && item.video_versions.length) return 'video';
    if (item.image_versions2?.candidates?.length || m?.image_versions2?.candidates?.length) return 'image';
    return null;
  }

  function extractItems(body) {
    const nodes = [];
    collectMediaNodes(body, nodes);
    const out = [];
    const seen = new Set();

    // Resolve field olhando primeiro no item, depois no .media interno (clips/user wrapper)
    const pick = (item, ...keys) => {
      for (const k of keys) {
        if (item?.[k] != null) return item[k];
        if (item?.media?.[k] != null) return item.media[k];
      }
      return null;
    };

    for (const item of nodes) {
      if (!item || typeof item !== 'object') continue;
      const id = pick(item, 'id', 'pk', 'code', 'shortcode');
      if (!id) continue;
      const key = String(id);
      if (seen.has(key)) continue;
      seen.add(key);

      const kind = mediaKind(item);
      if (!kind) continue;

      // Pra carousel, expande os filhos como itens separados — mais simples de baixar.
      const carouselArr = item.carousel_media || item.media?.carousel_media;
      if (kind === 'carousel' && Array.isArray(carouselArr)) {
        for (let i = 0; i < carouselArr.length; i++) {
          const child = carouselArr[i];
          const childKind = mediaKind(child);
          if (!childKind) continue;
          const url = childKind === 'video' ? bestVideoUrl(child) : bestImageUrl(child);
          if (!url) continue;
          out.push({
            platform: 'instagram',
            id: `${key}_${i}`,
            parent_id: key,
            kind: childKind,
            url,
            cover: childKind === 'video' ? bestImageUrl(child) : url,
            width: child.original_width || null,
            height: child.original_height || null,
            duration_ms: (child.video_duration || 0) * 1000,
            author: pick(item, 'user')?.username || item.owner?.username || null,
            author_name: pick(item, 'user')?.full_name || null,
            desc: pick(item, 'caption')?.text || item.edge_media_to_caption?.edges?.[0]?.node?.text || '',
            taken_at: (pick(item, 'taken_at') || 0) * 1000 || null,
            captured_at: Date.now()
          });
        }
        continue;
      }

      const url = kind === 'video' ? bestVideoUrl(item) : bestImageUrl(item);
      if (!url) continue;
      const m = item.media || {};
      out.push({
        platform: 'instagram',
        id: key,
        kind,
        url,
        // Para imagens, usa a própria URL como thumb (panel mostra preview).
        // Para vídeos, busca o thumbnail/cover.
        cover: kind === 'video' ? bestImageUrl(item) : url,
        width: item.original_width || m.original_width || null,
        height: item.original_height || m.original_height || null,
        duration_ms: ((item.video_duration || m.video_duration) || 0) * 1000,
        author: item.user?.username || item.owner?.username || m.user?.username || null,
        author_name: item.user?.full_name || m.user?.full_name || null,
        desc: item.caption?.text || m.caption?.text || item.edge_media_to_caption?.edges?.[0]?.node?.text || '',
        taken_at: (item.taken_at || m.taken_at || 0) * 1000 || null,
        captured_at: Date.now()
      });
    }
    if (DBG) console.log('[IS-Ext/IG] extracted items:', out.length, 'from', nodes.length, 'nodes');
    return out;
  }

  // Cache global de items vistos — usado pra Re-listar repopular o painel mesmo
  // quando o user já tinha limpo (ou trocou de aba e voltou). Persiste enquanto
  // a página estiver carregada; quando user navega pra outro perfil, é limpo.
  const _seenItems = new Map();   // key: `${platform}:${id}` → item
  let _lastPath = location.pathname;

  // Contador de requests interceptadas pra diagnóstico (logado após 6s)
  let _interceptCount = 0;

  function postItems(items, source_url) {
    if (!items?.length) return;
    _interceptCount++;
    for (const it of items) {
      if (it?.id) _seenItems.set(`${it.platform}:${it.id}`, it);
    }
    window.postMessage({ __is_ext: true, type: POST_TYPE, items, source_url }, '*');
  }

  // Diagnóstico: se passar 6 segundos do load sem nenhum item interceptado,
  // log alerta — ajuda a debugar quando IG mudou shape ou bloqueia interceptação.
  setTimeout(() => {
    if (_interceptCount === 0) {
      console.warn('%c[IS-Ext/IG] ⚠ Nenhum endpoint API foi interceptado em 6s. Possíveis causas:\n  • IG mudou a forma de carregar dados (precisa atualizar a extensão)\n  • Outra extensão tá conflitando com window.fetch\n  • A página não carregou (offline / login screen)', 'color:#f59e0b;font-weight:bold');
    } else {
      console.log(`%c[IS-Ext/IG] ✓ ${_interceptCount} endpoint(s) interceptado(s)`, 'color:#22c55e');
    }
  }, 6000);

  // Re-emite TODOS os items vistos. Usado pelo botão "Re-listar" do painel.
  function reemitCachedItems() {
    if (_seenItems.size === 0) return 0;
    const list = Array.from(_seenItems.values());
    window.postMessage({ __is_ext: true, type: POST_TYPE, items: list, source_url: location.href }, '*');
    return list.length;
  }

  // Limpa cache quando user troca de perfil (SPA navigation).
  setInterval(() => {
    if (location.pathname !== _lastPath) {
      // Considera "trocou de contexto" quando o PRIMEIRO segmento muda.
      // /razuk/reels → /razuk/p/X mantém cache. /razuk → /outro limpa.
      const oldFirst = (_lastPath || '').split('/').filter(Boolean)[0] || '';
      const newFirst = (location.pathname || '').split('/').filter(Boolean)[0] || '';
      if (oldFirst.toLowerCase() !== newFirst.toLowerCase()) {
        _seenItems.clear();
        if (DBG) console.log('[IS-Ext/IG] navegou — cache de items limpo');
      }
      _lastPath = location.pathname;
    }
  }, 800);

  // ── Scan de SSR JSON inline ─────────────────────────────────────────────
  // Quando o user dá F5 num perfil, o IG já manda o feed inicial inline em
  // <script type="application/json"> dentro do HTML — esses dados nunca passam
  // por fetch/XHR, então a interceptação anterior perdia tudo até o user
  // rolar a página e disparar novas requests.
  //
  // Escaneamos:
  //   - todos os <script type="application/json">
  //   - todos os <script> sem type (alguns shapes do IG vem como inline JS)
  // Faz parsing e busca items em qualquer profundidade.
  function scanSsrScripts(label = 'ssr') {
    try {
      const scripts = document.querySelectorAll('script[type="application/json"], script[type="application/ld+json"]');
      let total = 0;
      for (const s of scripts) {
        const text = s.textContent || '';
        if (!text || text.length < 50) continue;
        const body = safeParse(text);
        if (!body) continue;
        const items = extractItems(body);
        if (items.length) {
          postItems(items, location.href);
          total += items.length;
        }
      }
      if (DBG && total > 0) console.log(`[IS-Ext/IG] SSR scan (${label}): +${total} items`);
      return total;
    } catch (_) { return 0; }
  }

  // Roda na carga e também quando a SPA do IG injetar mais scripts (React hydration,
  // navegação client-side). MutationObserver detecta novos <script> adicionados.
  //
  // Importante: com manifest run_at=document_start + world=MAIN, esse código roda
  // ANTES do <head>/<body> existir. Por isso esperamos DOMContentLoaded pra começar
  // o scan e o observer (senão querySelectorAll retorna nada e o observer falha).
  function bootSsrScan() {
    const start = () => {
      scanSsrScripts('boot');
      // SPA: quando IG navega de /username pra /username/reels via client-side,
      // novos <script type="application/json"> podem ser injetados. Observa.
      try {
        const obs = new MutationObserver((muts) => {
          let touched = false;
          for (const m of muts) {
            for (const node of m.addedNodes) {
              if (node.nodeName === 'SCRIPT' && (node.type === 'application/json' || node.type === 'application/ld+json')) {
                touched = true;
                break;
              }
            }
            if (touched) break;
          }
          if (touched) scanSsrScripts('mutation');
        });
        obs.observe(document.documentElement || document, { childList: true, subtree: true });
      } catch (e) {
        if (DBG) console.warn('[IS-Ext/IG] MutationObserver falhou:', e.message);
      }
      // Um scan extra depois do load completo (alguns scripts entram tardiamente)
      if (document.readyState !== 'complete') {
        window.addEventListener('load', () => scanSsrScripts('load'), { once: true });
      } else {
        setTimeout(() => scanSsrScripts('post-load'), 300);
      }
    };
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', start, { once: true });
    } else {
      start();
    }
  }

  // ── Patch fetch (modo simples, atribuição direta) ──────────────────────
  // Tentamos antes um esquema com defineProperty getter/setter pra resistir a
  // sobrescrita do IG, mas isso quebrou downloads em algumas páginas. Voltamos
  // pra atribuição direta — funcionava bem antes da v1.1.11.
  const origFetch = window.fetch;
  window.fetch = async function (input, init) {
    const url = typeof input === 'string' ? input : (input?.url || '');
    const response = await origFetch.apply(this, arguments);
    try {
      if (ENDPOINTS_RE.test(url)) {
        const clone = response.clone();
        clone.text().then((text) => {
          const body = safeParse(text);
          const items = extractItems(body);
          if (items.length) {
            console.log(`%c[IS-Ext/IG] +${items.length} items de ${url.split('?')[0].split('/').slice(-3).join('/')}`, 'color:#22c55e');
            postItems(items, url);
          } else if (DBG) {
            console.log('[IS-Ext/IG] no items extracted from', url.split('?')[0]);
          }
        }).catch(() => {});
      }
    } catch (_) {}
    return response;
  };
  console.log('[IS-Ext/IG] fetch hook armado');

  // ── Patch XHR ──────────────────────────────────────────────────────────
  const origOpen = XMLHttpRequest.prototype.open;
  const origSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function (method, url) {
    this.__is_url = url;
    return origOpen.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function () {
    const xhr = this;
    if (ENDPOINTS_RE.test(xhr.__is_url || '')) {
      xhr.addEventListener('load', () => {
        try {
          const text = xhr.responseText;
          if (!text) return;
          const body = safeParse(text);
          const items = extractItems(body);
          if (items.length) {
            console.log(`%c[IS-Ext/IG/XHR] +${items.length} items de ${String(xhr.__is_url).split('?')[0].split('/').slice(-3).join('/')}`, 'color:#22c55e');
            postItems(items, xhr.__is_url);
          }
        } catch (_) {}
      });
    }
    return origSend.apply(this, arguments);
  };

  // ── Fetch bridge ─────────────────────────────────────────────────────
  // IG CDN frequentemente rejeita credentials:'include' (CORS preflight).
  // Tenta 'omit' primeiro (signed URL não precisa de cookies), depois 'include'.
  //
  // VALIDAÇÕES (vieram depois de um vídeo corrompido ter ido pro Drive da Valentina):
  //  - content-length vs buf.byteLength: detecta download truncado
  //  - magic bytes 'ftyp' (offset 4): detecta arquivo não-MP4 / dados quebrados
  //  - tamanho mínimo 1KB: detecta resposta de erro disfarçada
  //  - retry até 3x com backoff curto: cobre falhas transientes do CDN
  async function doFetch(url, { maxAttempts = 3 } = {}) {
    const errors = [];
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      for (const credentials of ['omit', 'include']) {
        const tag = `A${attempt}/${credentials}`;
        try {
          const r = await origFetch.call(window, url, { credentials });
          if (!r.ok) { errors.push(`${tag}:HTTP ${r.status}`); continue; }
          const ct = r.headers.get('content-type') || 'video/mp4';
          if (/^text\/html/i.test(ct)) { errors.push(`${tag}:CT=${ct}`); continue; }
          const buf = await r.arrayBuffer();

          // Validação mínima: rejeita apenas se for OBVIAMENTE lixo (zero bytes
          // ou < 100 bytes pra um vídeo). Não checa content-length nem ftyp magic
          // — o IG usa streaming/chunked em alguns shapes e essas checagens
          // estavam rejeitando downloads legítimos.
          if (buf.byteLength < 100) {
            errors.push(`${tag}:empty ${buf.byteLength}b`);
            continue;
          }
          return { buf, mime: ct };
        } catch (e) {
          errors.push(`${tag}:${e?.message || e}`);
        }
      }
      if (attempt < maxAttempts) await new Promise(r => setTimeout(r, 300 * attempt));
    }
    throw new Error(`download falhou após ${maxAttempts} tentativas: ${errors.join(' | ')}`);
  }

  window.addEventListener('message', async (event) => {
    if (event.source !== window) return;
    const d = event.data;
    if (!d || d.__is_ext_fetch !== true) return;
    const { url, requestId } = d;
    try {
      const { buf, mime } = await doFetch(url);
      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));
      }
      const b64 = btoa(s);
      window.postMessage({ __is_ext_fetch_reply: true, requestId, b64, mime }, '*');
    } catch (e) {
      window.postMessage({ __is_ext_fetch_reply: true, requestId, error: e?.message || String(e) }, '*');
    }
  });

  window.postMessage({ __is_ext: true, type: 'IS_EXT_IG_READY' }, '*');
  console.log('[IS-Ext/IG inject] patches + fetch bridge instalados');

  // Dispara o scan de SSR JSON — captura items que vêm no HTML inicial (F5/reload)
  // antes mesmo do user rolar a página. Crucial pra UX: sem isso, recarregar
  // o perfil mostrava painel vazio até o user fazer scroll.
  bootSsrScan();

  // Re-scan sob demanda: o painel envia 'IS_EXT_RESCAN' quando user clica em
  // "↻ Re-listar". Faz 2 coisas:
  //   1) Re-emite TODOS os items já vistos (cache _seenItems) — repopula o painel
  //      instantaneamente com o que já tinha sido capturado.
  //   2) Reroda o scan do SSR JSON da página atual (caso o IG tenha injetado
  //      conteúdo novo via hydration/SPA).
  window.addEventListener('message', (event) => {
    if (event.source !== window) return;
    if (event.data?.type === 'IS_EXT_RESCAN') {
      const cached = reemitCachedItems();
      const fresh = scanSsrScripts('rescan-request');
      console.log(`[IS-Ext/IG] re-scan: cache=${cached} items, SSR=${fresh} items`);
    }
  });
})();
