/*
 * netcapture.js — roda no MUNDO DA PAGINA (world: MAIN), antes das chamadas.
 * Grampeia fetch, XMLHttpRequest e WebSocket. Captura URL + metodo + status +
 * CORPO DO ENVIO (o que o botao manda) + CORPO DA RESPOSTA das chamadas de API do
 * TikTok, e manda pro content script via window.postMessage.
 *
 * NAO captura cabecalhos (evita gravar token/cookie). Passivo: nao altera as chamadas,
 * so observa. So captura chamadas de DADOS (ignora imagem/css/js). So roda quando LIGADO.
 */
;(function () {
  const TAG = 'LIVECAM_NETCAP';

  // so captura quando LIGADO (o content script isolado avisa via postMessage).
  // desligado, os wrappers so chamam o original — custo desprezivel.
  // "token": segredo aleatorio que o content script manda no capctl e a gente ecoa em cada
  // captura — assim outro script da pagina nao consegue forjar/filtrar nosso canal so pela flag.
  let enabled = false;
  let token = '';
  window.addEventListener('message', (ev) => {
    if (ev.source === window && ev.data && ev.data.__livecam_capctl) {
      enabled = !!ev.data.enabled;
      if (typeof ev.data.token === 'string') token = ev.data.token;
    }
  });

  function post(entry) {
    try { window.postMessage({ __livecam_netcap: true, tag: TAG, token, entry }, '*'); } catch (_) {}
  }
  // resolve URL relativa ('/api/...') pra absoluta — sem isso, chamada same-origin
  // com caminho relativo (o padrao das SPAs) nunca passaria no filtro /tiktok/
  function abs(url) {
    try { return new URL(url, location.href).href; } catch (_) { return String(url || ''); }
  }
  function snip(s, max) {
    if (typeof s !== 'string') return '';
    max = max || 6000;
    return s.length > max ? s.slice(0, max) + '…[truncado]' : s;
  }
  // corpo de ENVIO: so pega se for texto (string/URLSearchParams). FormData/Blob/ArrayBuffer
  // a gente ignora (nao interessa e pode ser binario grande).
  function reqBodyOf(body) {
    if (body == null) return '';
    if (typeof body === 'string') return snip(body, 2000);
    try { if (body instanceof URLSearchParams) return snip(body.toString(), 2000); } catch (_) {}
    return '';
  }
  // chamada de DADOS relevante (nao asset), num dominio do TikTok
  function interesting(url) {
    if (!url || !/tiktok/i.test(url)) return false;
    if (/\.(js|css|png|jpe?g|gif|webp|svg|woff2?|ttf|mp4|ico|map)(\?|$)/i.test(url)) return false;
    // RUIDO: polls repetitivos (list/status/ping/recommended/etc) — nao sao acoes e
    // enchem o buffer/estouram o export. Ignora ANTES do filtro de sinal.
    if (/live_product\/list|live_room_info|room\/status|room\/ping|\/ping\/anchor|recommended_product|request_demo_display|coupon\/section|creator\/live\/vouchers|notification\/unread|live_relation\/popup|sea_content\/creator|arch\/config|config_center|monitor_web|slardar|mon\.tiktokv|mcs-sg|browser-settings/i.test(url)) return false;
    // SINAL: acoes (top/add/delete/seq), violacao/governanca, pedido/carrinho/compra,
    // cor/variante e o encerrar (finish/stop/leave) — tudo isso cai em 'live'/'product'.
    return /\/api\/|\/aweme\/|oec|graphql|violat|penalt|governance|order|sku|product|promotion|streamer|live|showcase|pin|cart|checkout|buy|trade|settle|variant|colou?r/i.test(url);
  }

  // ---- fetch ----
  const origFetch = window.fetch;
  if (typeof origFetch === 'function') {
    window.fetch = function (...args) {
      const req = args[0];
      const url = abs(typeof req === 'string' ? req : (req && req.url) || '');
      const method = (args[1] && args[1].method) || (req && req.method) || 'GET';
      const want = enabled && interesting(url);
      // corpo do ENVIO: normalmente vem em args[1].body; mas a SPA do TikTok chama
      // fetch(new Request(url, {body})) — aí o corpo esta DENTRO do Request (args[0]).
      // Nesse caso clonamos o Request ANTES do fetch (o stream do corpo e consumido 1x).
      let reqBody = reqBodyOf(args[1] && args[1].body);
      let reqClone = null;
      if (want && !reqBody && req && typeof req !== 'string' && typeof req.clone === 'function') {
        try { reqClone = req.clone(); } catch (_) {}
      }
      const p = origFetch.apply(this, args);
      if (want) {
        const reqBodyP = reqBody
          ? Promise.resolve(reqBody)
          : (reqClone ? reqClone.text().then((t) => snip(t, 2000)).catch(() => '') : Promise.resolve(''));
        p.then((res) => {
          let resClone = null;
          try { resClone = res.clone(); } catch (_) {}
          reqBodyP.then((rb) => {
            if (!resClone) { post({ kind: 'fetch', url, method, status: res.status, reqBody: rb, body: '', ts: Date.now() }); return; }
            resClone.text()
              .then((body) => post({ kind: 'fetch', url, method, status: res.status, reqBody: rb, body: snip(body), ts: Date.now() }))
              .catch(() => {});
          });
        }).catch(() => {});
      }
      return p;
    };
  }

  // ---- XMLHttpRequest ----
  const origOpen = XMLHttpRequest.prototype.open;
  const origSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function (method, url) {
    this.__cl_url = url;
    this.__cl_method = method;
    return origOpen.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function (body) {
    const url = abs(this.__cl_url || '');
    if (enabled && interesting(url)) {
      const reqBody = reqBodyOf(body);
      const metodo = this.__cl_method || 'GET'; // fecha o valor DESTE envio no closure
      // { once:true }: se a pagina reusar o mesmo XHR pra outra chamada, um listener antigo
      // grudado casaria a URL antiga com a resposta nova (captura fantasma). Uma vez so.
      this.addEventListener('load', function () {
        try {
          post({ kind: 'xhr', url, method: metodo, status: this.status, reqBody, body: snip(this.responseText), ts: Date.now() });
        } catch (_) {}
      }, { once: true });
    }
    return origSend.apply(this, arguments);
  };

  // ---- WebSocket ----
  // eventos em tempo real da live (venda/violacao podem chegar por aqui). So captura
  // frames de TEXTO — frame binario (protobuf) a gente ignora pra nao gravar lixo.
  const OrigWS = window.WebSocket;
  if (typeof OrigWS === 'function') {
    function WrappedWS(url, protocols) {
      const ws = protocols === undefined ? new OrigWS(url) : new OrigWS(url, protocols);
      try {
        const wsUrl = String(url || '');
        ws.addEventListener('message', (ev) => {
          if (!enabled) return;
          if (typeof ev.data !== 'string') return; // ignora binario/protobuf
          post({ kind: 'ws', url: wsUrl, method: 'WS', status: 0, body: snip(ev.data), ts: Date.now() });
        });
      } catch (_) {}
      return ws;
    }
    WrappedWS.prototype = OrigWS.prototype;
    WrappedWS.CONNECTING = OrigWS.CONNECTING; WrappedWS.OPEN = OrigWS.OPEN;
    WrappedWS.CLOSING = OrigWS.CLOSING; WrappedWS.CLOSED = OrigWS.CLOSED;
    try { window.WebSocket = WrappedWS; } catch (_) {}
  }
})();
