(() => {
  if (window.__FLOWINSTA_BR_CONTENT_V600__) return;
  window.__FLOWINSTA_BR_CONTENT_V600__ = true;

  const TARGET_COLLECT_LIMIT = 60;
  const POST_METADATA_LIMIT = 5;
  const POST_METADATA_DELAY_MS = 3500;

  console.log('[FlowInsta BR] Content script v6.0.4 carregado');

  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg.type === 'COLLECT_TARGETS') {
      collectTargets(msg.settings, msg.processed || [], msg.activeType || '', msg.exhausted || [], msg.sourceAccount || '')
        .then(sendResponse)
        .catch((err) => sendResponse({ targets: [], error: err.message }));
      return true;
    }

    if (msg.type === 'EXECUTE_ACTION') {
      handleAction(msg)
        .then(sendResponse)
        .catch((err) => sendResponse({ success: false, error: err.message }));
      return true;
    }

    if (msg.type === 'EXTRACT_POST_AUTHOR') {
      extractPostAuthor(msg.settings, msg.processed || [])
        .then(sendResponse)
        .catch((err) => sendResponse({ author: null, error: err.message }));
      return true;
    }

    return false;
  });

  async function dismissInstagramBlockingDialogs() {
    try {
      const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')).filter(isVisible);
      for (const dialog of dialogs) {
        const text = normalizeText(dialog.innerText || dialog.textContent || '');
        const isBlockingPrompt =
          text.includes('ativar notificacoes') ||
          text.includes('ative as notificacoes') ||
          text.includes('turn on notifications') ||
          text.includes('allow notifications') ||
          text.includes('notifications from instagram') ||
          text.includes('salvar suas informacoes de login') ||
          text.includes('save your login info') ||
          text.includes('adicionar instagram a sua tela inicial') ||
          text.includes('add instagram to your home screen');
        if (!isBlockingPrompt) continue;

        const buttons = Array.from(dialog.querySelectorAll('button, [role="button"]')).filter(isVisible);
        const dismiss = buttons.find((btn) => {
          const label = normalizeText(btn.innerText || btn.textContent || btn.getAttribute('aria-label') || '');
          if (!label) return false;
          if (label.includes('ativar') || label.includes('turn on') || label.includes('permitir') || label.includes('allow')) return false;
          return label.includes('agora nao') ||
            label.includes('not now') ||
            label.includes('depois') ||
            label.includes('cancelar') ||
            label.includes('fechar') ||
            label.includes('close') ||
            label.includes('dispensar') ||
            label.includes('dismiss');
        });

        if (dismiss) {
          humanClick(dismiss);
          await sleep(500);
          return { dismissed: true, method: 'button' };
        }

        document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }));
        document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }));
        await sleep(300);
        return { dismissed: true, method: 'escape' };
      }
    } catch (err) {
      return { dismissed: false, error: String(err?.message || err) };
    }
    return { dismissed: false };
  }

  async function collectTargets(settings, processed, activeType, exhausted, sourceAccount) {
    const targets = new Set();
    const processedSet = new Set((processed || []).map(normalizeUsername).filter(Boolean));
    // v5.9.9: exclui o proprio concorrente da lista de candidatos - antes
    // nao tinha essa exclusao aqui (o agent.ps1 ja tinha), e um link do
    // proprio perfil do concorrente podia acabar virando "alvo" e sendo
    // seguido (confirmado em producao: follow @rgpmarketingdigital sendo
    // o proprio concorrente, nao um seguidor dele). Colocar aqui no
    // processedSet exclui em toda funcao que ja usa isValidTarget.
    const cleanSourceAccount = normalizeUsername(sourceAccount || '');
    if (cleanSourceAccount) processedSet.add(cleanSourceAccount);
    // v5.9.8: quem ja teve TODAS as acoes habilitadas concluidas (nao so
    // "processado hoje") - usado so pra decidir se ja vale a pena parar de
    // rolar, sem impedir que esses nomes entrem em targets (isValidTarget
    // continua sendo a fonte de verdade pra isso via processedSet).
    const exhaustedSet = new Set((exhausted || []).map(normalizeUsername).filter(Boolean));
    const hasUsefulTarget = () => Array.from(targets).some((t) => !exhaustedSet.has(t));
    // v5.9.9: quando o dialogo mostra "Sugestoes para voce" em vez da
    // lista real (tela vazia de seguidores), os links ali sao sugestoes
    // genericas da Instagram, nao seguidores de verdade do concorrente -
    // confirmado que achadinhodanina_shop/ju_casimiro07 foram coletados
    // assim e nao seguem o concorrente de verdade. Restringir a busca ao
    // dialogo nao basta nesse caso, porque as sugestoes ficam dentro dele.
    const dialogShowsSuggestions = () => {
      const dialog = document.querySelector('[role="dialog"]');
      const scope = dialog || document.body;
      const text = normalizeText(scope?.innerText || scope?.textContent || '').replace(/\s+/g, ' ');
      return text.includes('sugestoes para voce') || text.includes('suggested for you');
    };

    await waitForCollectionContent(12000).catch(() => null);
    await dismissInstagramBlockingDialogs();

    if (isRateLimitedPage()) {
      return {
        targets: [],
        postLinks: [],
        rateLimited: true,
        debug: { href: location.href, reason: 'instagram_429_page' }
      };
    }

    if (activeType === 'competitor' && dialogShowsSuggestions()) {
      return {
        targets: [],
        postLinks: [],
        debug: { href: location.href, reason: 'followers_list_shows_suggestions_only' }
      };
    }

    const tagName = extractHashtagFromUrl(location.href);
    if (tagName) {
      const apiTargets = await fetchTagUsersFromAPI(tagName, processedSet);
      if (apiTargets?.rateLimited) {
        return {
          targets: [],
          postLinks: [],
          rateLimited: true,
          debug: { href: location.href, reason: 'instagram_429_tag_api' }
        };
      }
      for (const username of apiTargets || []) {
        if (isValidTarget(username, processedSet)) targets.add(username);
        if (targets.size >= TARGET_COLLECT_LIMIT) break;
      }
    }

    // v5.9.10: extractFromPageJSON varre TODA a pagina (scripts, window._sharedData,
    // window.__additionalData), sem nenhuma restricao ao dialogo - bem
    // diferente de addVisibleProfileLinks, que ja fica presa ao dialogo.
    // Confirmado em producao: o mesmo username (ju_casimiro07) apareceu
    // como "seguidor" de 3 concorrentes diferentes e nao segue nenhum -
    // sinal de que vem de algum bloco de dados generico/pessoal da conta
    // logada, embutido na pagina independente de qual perfil esta sendo
    // visto. O agent.ps1 nunca teve essa funcao e nunca teve esse
    // vazamento - pra concorrente, a extensao passa a depender só da
    // varredura de link visivel (ja restrita ao dialogo), igual o Agent.
    const useJsonExtraction = activeType !== 'competitor';
    if (useJsonExtraction) extractFromPageJSON(targets, processedSet);
    const jsonTargets = targets.size;

    addVisibleProfileLinks(targets, processedSet);
    await scrollForMoreResults();
    if (useJsonExtraction) extractFromPageJSON(targets, processedSet);
    addVisibleProfileLinks(targets, processedSet);

    // v5.9.11: a Instagram pode carregar o bloco "Sugestoes para voce" de
    // forma atrasada - depois da checagem inicial (linha ~126), mas antes
    // do loop de rolagem extra sequer comecar. Confirmado em producao:
    // emerson_rodriguesoriginall (visto na tela de sugestoes) escapou por
    // essa janela. Checa de novo aqui, no mesmo ponto onde os links dessa
    // primeira leva ja foram escaneados.
    if (activeType === 'competitor' && dialogShowsSuggestions()) {
      targets.clear();
    }

    if (activeType === 'competitor') {
      // v5.9.8: se os alvos achados ate aqui ja estiverem todos esgotados
      // (follow/like/comment ja feitos ha dias, so nao "processados hoje"),
      // continua rolando ate achar alguem com acao pendente ou bater o
      // teto de seguranca - antes parava assim que achava qualquer nome,
      // trazendo sempre os mesmos 1-2 ja feitos.
      let round = 0;
      let lastSize = targets.size;
      while (!hasUsefulTarget() && round < 6) {
        // v5.9.9: se as sugestoes aparecerem no meio da rolagem, para e
        // descarta tudo - nao vale a pena continuar coletando ali.
        if (dialogShowsSuggestions()) {
          targets.clear();
          break;
        }
        await scrollForMoreResults();
        addVisibleProfileLinks(targets, processedSet);
        if (targets.size <= lastSize && round > 0) break; // parou de crescer, nao adianta insistir
        lastSize = targets.size;
        round += 1;
      }
    }

    const postLinks = getPostLinks(24);
    let rateLimited = false;

    if (targets.size < 20) {
      for (const href of postLinks.slice(0, POST_METADATA_LIMIT)) {
        const postUsersResult = await getUsersFromPostMetadata(href, processedSet);
        if (postUsersResult?.rateLimited) {
          rateLimited = true;
          break;
        }
        for (const username of postUsersResult?.users || []) {
          if (isValidTarget(username, processedSet)) targets.add(username);
        }
        if (targets.size >= TARGET_COLLECT_LIMIT) break;
        await sleep(POST_METADATA_DELAY_MS);
      }
    }

    if (rateLimited || isRateLimitedPage()) {
      return {
        targets: [],
        postLinks,
        rateLimited: true,
        debug: { href: location.href, reason: 'instagram_429_metadata' }
      };
    }

    const filtered = [];
    for (const target of targets) {
      if (await passesFilters(target, settings)) {
        filtered.push(target);
      }
      if (filtered.length >= TARGET_COLLECT_LIMIT) break;
    }

    return {
      targets: filtered,
      postLinks,
      debug: {
        href: location.href,
        jsonTargets,
        visibleTargets: targets.size,
        postLinks: postLinks.length
      }
    };
  }

  async function fetchTagUsersFromAPI(tagName, processedSet) {
    const endpoints = [
      `https://www.instagram.com/api/v1/tags/web_info/?tag_name=${encodeURIComponent(tagName)}`,
      `https://www.instagram.com/explore/tags/${encodeURIComponent(tagName)}/?__a=1&__d=dis`
    ];

    for (const url of endpoints) {
      try {
        const response = await fetch(url, {
          credentials: 'include',
          headers: {
            'Accept': 'application/json',
            'X-IG-App-ID': '936619743392459',
            'X-Requested-With': 'XMLHttpRequest'
          }
        });

        if (response.status === 429) return { rateLimited: true };
        if (!response.ok) continue;

        let json;
        try {
          json = JSON.parse(await response.text());
        } catch {
          continue;
        }

        const usernames = new Set();
        extractUsernamesFromObj(json, usernames, processedSet, 0);

        const sections = json?.data?.top?.sections || json?.edge_hashtag_to_top_posts?.edges || [];
        for (const section of sections) {
          const medias = section?.layout_content?.medias || (section?.node ? [section] : []);
          for (const mediaItem of medias) {
            const username = mediaItem?.media?.user?.username || mediaItem?.node?.owner?.username;
            const clean = normalizeUsername(username);
            if (isValidTarget(clean, processedSet)) usernames.add(clean);
            if (usernames.size >= TARGET_COLLECT_LIMIT) break;
          }
          if (usernames.size >= TARGET_COLLECT_LIMIT) break;
        }

        if (usernames.size > 0) {
          console.log(`[FlowInsta BR] API de tags retornou ${usernames.size} usuários`);
          return [...usernames].slice(0, TARGET_COLLECT_LIMIT);
        }
      } catch {}
    }

    return [];
  }

  function extractHashtagFromUrl(href) {
    const searchMatch = String(href || '').match(/[?&]q=%23([^&]+)/i);
    if (searchMatch) return sanitizeHashtag(decodeURIComponent(searchMatch[1]));

    const tagMatch = String(href || '').match(/\/tags\/([^/?]+)/i);
    if (tagMatch) return sanitizeHashtag(decodeURIComponent(tagMatch[1]));

    return null;
  }

  function extractFromPageJSON(targets, processedSet) {
    try {
      const scripts = Array.from(document.querySelectorAll('script'));
      for (const script of scripts) {
        const text = script.textContent || '';
        if (!text.includes('"username"')) continue;

        const patterns = [
          /"owner"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
          /"user"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
          /"node"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
          /"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g
        ];

        for (const pattern of patterns) {
          let match;
          while ((match = pattern.exec(text))) {
            const username = normalizeUsername(match[1]);
            if (isValidTarget(username, processedSet)) targets.add(username);
            if (targets.size >= TARGET_COLLECT_LIMIT) break;
          }
          if (targets.size >= TARGET_COLLECT_LIMIT) break;
        }
        if (targets.size >= TARGET_COLLECT_LIMIT) break;
      }
    } catch {}

    try {
      if (window._sharedData) extractUsernamesFromObj(window._sharedData, targets, processedSet, 0);
    } catch {}

    try {
      if (window.__additionalData) extractUsernamesFromObj(window.__additionalData, targets, processedSet, 0);
    } catch {}
  }

  function extractUsernamesFromObj(obj, targets, processedSet, depth) {
    if (depth > 6 || !obj || typeof obj !== 'object' || targets.size >= TARGET_COLLECT_LIMIT) return;

    if (Array.isArray(obj)) {
      for (const item of obj) {
        extractUsernamesFromObj(item, targets, processedSet, depth + 1);
        if (targets.size >= TARGET_COLLECT_LIMIT) break;
      }
      return;
    }

    for (const key of Object.keys(obj)) {
      if (key === 'username' && typeof obj[key] === 'string') {
        const username = normalizeUsername(obj[key]);
        if (isValidTarget(username, processedSet)) targets.add(username);
      } else {
        extractUsernamesFromObj(obj[key], targets, processedSet, depth + 1);
      }
      if (targets.size >= TARGET_COLLECT_LIMIT) break;
    }
  }

  function addVisibleProfileLinks(targets, processedSet) {
    // v5.9.7: se um dialogo estiver aberto (caso do diálogo de seguidores
    // de concorrente), restringe a busca a ele - antes buscava a pagina
    // inteira e podia pegar links de "Sugestoes para voce" ou de outros
    // blocos da pagina, que nao sao seguidores de verdade do concorrente.
    const dialog = document.querySelector('[role="dialog"]');
    const scope = dialog || document;
    const links = Array.from(scope.querySelectorAll('a[href^="/"], a[href^="https://www.instagram.com/"], a[href^="https://instagram.com/"]'));

    for (const link of links) {
      const username = usernameFromHref(link.getAttribute('href') || link.href || '');
      if (username && isValidTarget(username, processedSet)) {
        targets.add(username);
      }
      if (targets.size >= TARGET_COLLECT_LIMIT) break;
    }
  }

  async function getUsersFromPostMetadata(href, processedSet) {
    const users = new Set();
    const postUrl = normalizeInstagramHref(href);
    if (!postUrl) return { users: [] };

    const endpoints = [
      `${postUrl}?__a=1&__d=dis`,
      `${postUrl}?__a=1`,
      postUrl
    ];

    for (const url of endpoints) {
      try {
        const response = await fetch(url, {
          credentials: 'include',
          headers: { 'X-Requested-With': 'XMLHttpRequest' }
        });
        if (response.status === 429) return { rateLimited: true, users: [] };
        if (!response.ok) continue;

        const text = await response.text();
        collectUsernamesFromPostText(text, users, processedSet);
        if (users.size >= TARGET_COLLECT_LIMIT) break;
      } catch {}
    }

    if (!users.size) {
      const authorResult = await getPostAuthorFromHtml(href);
      const author = authorResult?.author || authorResult;
      if (author && isValidTarget(author, processedSet)) users.add(normalizeUsername(author));
    }

    return { users: [...users].slice(0, TARGET_COLLECT_LIMIT) };
  }

  function collectUsernamesFromPostText(text, users, processedSet) {
    try {
      const parsed = JSON.parse(text);
      extractUsernamesFromObj(parsed, users, processedSet, 0);
    } catch {}

    const patterns = [
      /"owner"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
      /"user"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
      /"commenter"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g,
      /"alternateName"\s*:\s*"@([a-zA-Z0-9._]{2,30})"/g,
      /"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/g
    ];

    for (const pattern of patterns) {
      let match;
      while ((match = pattern.exec(text))) {
        const username = normalizeUsername(match[1]);
        if (isValidTarget(username, processedSet)) users.add(username);
        if (users.size >= TARGET_COLLECT_LIMIT) return;
      }
    }
  }

  async function getPostAuthorFromHtml(href) {
    const metadataAuthor = await getAuthorFromPostMetadata(href);
    if (metadataAuthor?.rateLimited) return metadataAuthor;
    if (metadataAuthor) return { author: metadataAuthor };

    try {
      const response = await fetch(href, { credentials: 'include' });
      if (response.status === 429) return { rateLimited: true };
      if (!response.ok) return null;

      const html = await response.text();
      const patterns = [
        /"owner"\s*:\s*\{[\s\S]{0,800}?"username"\s*:\s*"([^"]+)"/g,
        /"user"\s*:\s*\{[\s\S]{0,800}?"username"\s*:\s*"([^"]+)"/g,
        /"alternateName"\s*:\s*"@([^"]+)"/g,
        /"username"\s*:\s*"([a-zA-Z0-9._]{1,30})"/g
      ];

      for (const pattern of patterns) {
        let match;
        while ((match = pattern.exec(html))) {
          const username = String(match[1] || '').trim();
          if (usernameFromHref(`/${username}/`)) return { author: username };
        }
      }
    } catch {}

    return null;
  }

  async function getAuthorFromPostMetadata(href) {
    const postUrl = normalizeInstagramHref(href);
    if (!postUrl) return null;

    const endpoints = [
      `${postUrl}?__a=1&__d=dis`,
      `${postUrl}?__a=1`
    ];

    for (const url of endpoints) {
      try {
        const response = await fetch(url, {
          credentials: 'include',
          headers: { 'X-Requested-With': 'XMLHttpRequest' }
        });
        if (response.status === 429) return { rateLimited: true };
        if (!response.ok) continue;

        const text = await response.text();
        const username = usernameFromJsonText(text);
        if (username && usernameFromHref(`/${username}/`)) return username;
      } catch {}
    }

    return null;
  }

  function usernameFromJsonText(text) {
    try {
      const parsed = JSON.parse(text);
      const username = firstUsernameFromObj(parsed, 0);
      if (username) return username;
    } catch {}

    const patterns = [
      /"owner"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/,
      /"user"\s*:\s*\{[\s\S]{0,1200}?"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/,
      /"username"\s*:\s*"([a-zA-Z0-9._]{2,30})"/
    ];

    for (const pattern of patterns) {
      const match = text.match(pattern);
      if (match) return normalizeUsername(match[1]);
    }

    return null;
  }

  function firstUsernameFromObj(obj, depth) {
    if (depth > 8 || !obj || typeof obj !== 'object') return null;
    if (typeof obj.username === 'string') return normalizeUsername(obj.username);

    if (Array.isArray(obj)) {
      for (const item of obj) {
        const found = firstUsernameFromObj(item, depth + 1);
        if (found) return found;
      }
      return null;
    }

    for (const key of Object.keys(obj)) {
      const found = firstUsernameFromObj(obj[key], depth + 1);
      if (found) return found;
    }

    return null;
  }

  async function extractPostAuthor(settings, processed) {
    const processedSet = new Set((processed || []).map(normalizeUsername).filter(Boolean));
    await waitForCollectionContent(10000).catch(() => null);
    await sleep(900);

    if (isRateLimitedPage()) {
      return { author: null, rateLimited: true, source: 'instagram_429_page' };
    }

    const candidates = [];
    const selectors = [
      'article header a[href^="/"]',
      'main header a[href^="/"]',
      'article a[href^="/"]',
      'main a[href^="/"]',
      'a[href^="https://www.instagram.com/"]',
      'a[href^="https://instagram.com/"]'
    ];

    for (const selector of selectors) {
      document.querySelectorAll(selector).forEach((link) => {
        const username = usernameFromHref(link.getAttribute('href') || link.href || '');
        if (username) candidates.push(username);
      });
      const found = firstValidCandidate(candidates, settings, processedSet);
      if (found) return { author: found, source: selector };
    }

    const html = document.documentElement.innerHTML || '';
    const patterns = [
      /"owner"\s*:\s*\{[\s\S]{0,800}?"username"\s*:\s*"([^"]+)"/g,
      /"user"\s*:\s*\{[\s\S]{0,800}?"username"\s*:\s*"([^"]+)"/g,
      /"alternateName"\s*:\s*"@([^"]+)"/g,
      /"username"\s*:\s*"([a-zA-Z0-9._]{1,30})"/g
    ];

    for (const pattern of patterns) {
      let match;
      while ((match = pattern.exec(html))) {
        candidates.push(match[1]);
      }
      const found = firstValidCandidate(candidates, settings, processedSet);
      if (found) return { author: found, source: 'html' };
    }

    return { author: null, source: 'none', candidates: candidates.slice(0, 10) };
  }

  function firstValidCandidate(candidates, settings, processedSet) {
    const ownUser = normalizeUsername(settings?.account_username || '');
    for (const candidate of candidates) {
      const username = normalizeUsername(candidate);
      if (!isValidTarget(username, processedSet)) continue;
      if (ownUser && username === ownUser) continue;
      return username;
    }
    return null;
  }

  function getPostLinks(limit = 18) {
    const seen = new Set();
    const links = [];
    document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"]').forEach((link) => {
      const href = normalizeInstagramHref(link.getAttribute('href') || link.href || '');
      if (!href || seen.has(href)) return;
      seen.add(href);
      links.push(href);
    });
    return links.slice(0, limit);
  }

  function normalizeInstagramHref(href) {
    try {
      const url = href.startsWith('http') ? new URL(href) : new URL(href, location.origin);
      if (!/(^|\.)instagram\.com$/.test(url.hostname)) return null;
      if (!/^\/(p|reel)\//.test(url.pathname)) return null;
      return `https://www.instagram.com${url.pathname}`;
    } catch {
      return null;
    }
  }

  async function scrollForMoreResults() {
    const startY = window.scrollY;
    for (let i = 0; i < 6; i++) {
      window.scrollBy({ top: Math.max(420, window.innerHeight * 0.75), behavior: 'smooth' });
      await sleep(900);
    }
    window.scrollTo({ top: startY, behavior: 'smooth' });
    await sleep(500);
  }

  function waitForCollectionContent(timeout = 12000) {
    return new Promise((resolve) => {
      const isReady = () =>
        document.querySelector('main a[href*="/p/"], main a[href*="/reel/"], article, header, main');

      if (isReady()) return resolve(true);

      const observer = new MutationObserver(() => {
        if (isReady()) {
          observer.disconnect();
          resolve(true);
        }
      });

      observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
      setTimeout(() => {
        observer.disconnect();
        resolve(false);
      }, timeout);
    });
  }

  function usernameFromHref(href) {
    try {
      const rawPath = href.startsWith('http')
        ? new URL(href).pathname
        : href.split('?')[0].split('#')[0];

      const match = rawPath.match(/^\/([a-zA-Z0-9._]{1,30})\/?$/);
      if (!match) return null;

      const username = match[1];
      return isSystemPage(username) ? null : username;
    } catch {
      return null;
    }
  }

  async function passesFilters(username, settings) {
    if (!username || username.length < 2) return false;

    const lower = username.toLowerCase();
    const skipWords = ['official', 'official_', '_official', 'bot', 'spam', 'promo'];
    if (skipWords.some((word) => lower.includes(word))) return false;

    const ownUser = (settings?.account_username || '').replace('@', '').toLowerCase();
    if (ownUser && lower === ownUser) return false;
    if (/\.[a-z]{2,}$/.test(lower)) return false;

    return true;
  }

  function isValidTarget(username, processedSet) {
    username = normalizeUsername(username);
    if (!username || processedSet.has(username)) return false;
    if (!/^[a-zA-Z0-9._]{2,30}$/.test(username)) return false;
    if (username.includes('..') || username.startsWith('.') || username.endsWith('.')) return false;
    if (/\.[a-z]{2,}$/.test(username)) return false;
    return !isSystemPage(username);
  }

  function normalizeUsername(value) {
    return String(value || '')
      .replace(/^@/, '')
      .trim()
      .toLowerCase();
  }

  function sanitizeHashtag(value) {
    return String(value || '')
      .replace(/^#/, '')
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-zA-Z0-9_]/g, '')
      .trim()
      .toLowerCase();
  }

  function isSystemPage(page) {
    const system = [
      'explore', 'reels', 'stories', 'accounts', 'direct', 'p', 'tv', 'about',
      'privacy', 'terms', 'help', 'api', 'graphql', 'static', 'legal', 'press',
      'jobs', 'developer', 'directory', 'web', 'challenge', 'oauth', 'emails',
      'settings', 'create', 'notifications', 'popular', 'blog', 'careers',
      'locations', 'instagramlite', 'threads', 'meta', 'search', 'keyword',
      'reel', 'login', 'signup', 'register', 'null', 'undefined'
    ];
    return system.includes(String(page || '').toLowerCase());
  }

  async function handleAction(msg) {
    const { action, target, settings } = msg;
    await dismissInstagramBlockingDialogs();

    switch (action) {
      case 'follow':
        return await doFollow(target, settings);
      case 'unfollow':
        return await doUnfollow(target, settings);
      case 'like':
        return await doLike(target, settings);
      case 'comment':
        return await doComment(target, msg.commentText || '');
      default:
        return { success: false, error: 'acao_desconhecida' };
    }
  }

  async function doFollow(username, settings) {
    try {
      await waitForElement('header, main', 9000);
      await sleep(randInt(900, 1600));
      await dismissInstagramBlockingDialogs();

      if (isRateLimitedPage()) return { success: false, reason: 'instagram_429' };

      const ownUser = (settings?.account_username || '').replace('@', '').toLowerCase();
      if (ownUser && ownUser === String(username || '').toLowerCase()) {
        return { success: false, reason: 'own_account' };
      }

      if (isOwnProfilePage()) return { success: false, reason: 'own_account' };

      const followerCount = getFollowerCount();
      if (followerCount !== null) {
        if (settings?.min_followers && followerCount < Number(settings.min_followers)) {
          return { success: false, reason: 'below_min_followers' };
        }
        if (settings?.max_followers && followerCount > Number(settings.max_followers)) {
          return { success: false, reason: 'above_max_followers' };
        }
      }

      if (isAlreadyFollowing()) return { success: false, reason: 'already_following' };

      const button = findFollowButton();
      if (!button) return { success: false, reason: 'button_not_found' };

      await dismissInstagramBlockingDialogs();
      humanClick(button);
      await sleep(randInt(1800, 3200));

      const success = await waitForFollowConfirmation();
      return { success, action: 'follow', target: username, reason: success ? undefined : 'click_not_confirmed' };
    } catch (err) {
      return { success: false, error: err.message };
    }
  }

  async function doUnfollow(username, settings) {
    try {
      await waitForElement('header, main', 9000);
      await sleep(randInt(600, 1000));
      await dismissInstagramBlockingDialogs();

      const vipList = settings?.vip_list || [];
      if (vipList.includes(username)) return { success: false, reason: 'vip_protected' };
      if (settings?.protect_followers && isFollowingBack()) return { success: false, reason: 'follows_back' };

      const button = findUnfollowButton();
      if (!button) return { success: false, reason: 'not_following' };

      await dismissInstagramBlockingDialogs();
      humanClick(button);
      await sleep(randInt(600, 1000));

      await dismissInstagramBlockingDialogs();
      const confirmButton = await waitForUnfollowConfirmButton(7000);
      if (!confirmButton) return { success: false, reason: 'unfollow_confirm_not_found' };
      humanClick(confirmButton);
      await sleep(randInt(700, 1100));

      const confirmed = await waitForUnfollowConfirmation(7000);
      return confirmed
        ? { success: true, action: 'unfollow', target: username }
        : { success: false, reason: 'unfollow_not_confirmed' };
    } catch (err) {
      return { success: false, error: err.message };
    }
  }

  async function doLike(username, settings) {
    try {
      await waitForElement('article, main', 7000);
      await sleep(randInt(500, 1000));
      await dismissInstagramBlockingDialogs();

      if (isRateLimitedPage()) return { success: false, reason: 'instagram_429' };
      if (isPrimaryPostLiked()) return { success: false, reason: 'already_liked' };

      // Tentativa 1: regra rígida - botão com aria-label EXATO "Curtir" ou "Like"
      // fora de qualquer área de comentários e dentro do grupo de ações do post
      const strictButton = findStrictLikeButton();
      if (strictButton) {
        await dismissInstagramBlockingDialogs();
        humanClick(strictButton);
        if (await waitForPrimaryPostLiked(5000)) {
          return { success: true, action: 'like', target: username, method: 'strict_like_button' };
        }
      }

      // Tentativa 2: método original com score >= 35 (fallback)
      const button = findLikeButton();
      if (button && button !== strictButton) {
        await dismissInstagramBlockingDialogs();
        humanClick(button);
        if (await waitForPrimaryPostLiked(6000)) {
          return { success: true, action: 'like', target: username, method: 'main_like_button' };
        }
      }

      // Tentativa 3: double-click na mídia (mais seguro - nunca curte comentário)
      const mediaTarget = findPrimaryPostMedia();
      if (mediaTarget) {
        await dismissInstagramBlockingDialogs();
        humanDoubleClick(mediaTarget);
        if (await waitForPrimaryPostLiked(6000)) {
          return { success: true, action: 'like', target: username, method: 'media_double_click' };
        }
      }

      return { success: false, reason: (strictButton || button) ? 'like_not_confirmed' : 'like_button_not_found' };
    } catch (err) {
      return { success: false, error: err.message };
    }
  }

  // Busca o botão de curtir com critério mais rígido:
  // aria-label exato 'curtir' ou 'like', dentro do grupo de ações do post,
  // fora do painel de comentários e fora de qualquer listagem de comentários
  function findStrictLikeButton() {
    const scope = getPrimaryPostScope();
    const candidates = Array.from((scope || document).querySelectorAll(
      'button[aria-label], [role="button"][aria-label], button [aria-label], [role="button"] [aria-label], svg[aria-label]'
    ));
    for (const candidate of candidates) {
      const btn = candidate.closest?.('button, [role="button"]') || candidate;
      if (!isVisible(btn) || btn.disabled || btn.getAttribute('aria-disabled') === 'true') continue;
      const label = normalizeText(candidate.getAttribute('aria-label') || btn.getAttribute('aria-label') || '');
      // Deve ser EXATAMENTE 'curtir' ou 'like' - sem nenhum texto adicional
      if (label !== 'curtir' && label !== 'like') continue;
      // Não pode estar dentro do painel de rolagem de comentários
      if (isInsideScrollableCommentPane(btn, scope || document)) continue;
      // Não pode estar dentro de um item de lista (comentário individual)
      if (btn.closest('li, [role="listitem"]')) continue;
      // Exige a barra compacta de ações do post; um coração isolado é comentário.
      if (!findNearestPostActionGroup(btn, scope || document)) continue;
      return btn;
    }
    return null;
  }

  function findPrimaryPostMedia() {
    const scope = getPrimaryPostScope();
    if (!scope) return null;

    const mediaItems = Array.from(scope.querySelectorAll('video, img, canvas'))
      .filter(item => {
        if (!isVisible(item)) return false;
        if (item.closest('header, footer, nav')) return false;
        if (isInsideScrollableCommentPane(item, scope)) return false;
        const rect = item.getBoundingClientRect();
        return rect.width >= 180 && rect.height >= 180;
      })
      .sort((a, b) => {
        const ra = a.getBoundingClientRect();
        const rb = b.getBoundingClientRect();
        return (rb.width * rb.height) - (ra.width * ra.height);
      });

    return mediaItems[0] || null;
  }

  function findLikeButton() {
    return findPrimaryPostActionButton(['like']);
  }

  function findUnlikeButton() {
    return findPrimaryPostActionButton(['unlike']);
  }

  function findPrimaryPostActionButton(kinds) {
    const scope = getPrimaryPostScope();
    const candidates = Array.from((scope || document).querySelectorAll('button[aria-label], [role="button"][aria-label], button [aria-label], [role="button"] [aria-label], svg[aria-label]'));
    const buttons = [];
    for (const candidate of candidates) {
      const button = candidate.closest?.('button, [role="button"]') || candidate;
      if (!button || !isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
      const label = normalizeText(candidate.getAttribute('aria-label') || button.getAttribute('aria-label') || '');
      if (matchesPostActionLabel(label, kinds)) buttons.push(button);
    }

    const uniqueButtons = [...new Set(buttons)];
    const scored = uniqueButtons
      .map(button => ({ button, score: scorePrimaryPostActionButton(button, scope) }))
      .filter(item => item.score >= 35)
      .sort((a, b) => b.score - a.score);

    return scored[0]?.button || null;
  }

  function getPrimaryPostArticle() {
    const articles = Array.from(document.querySelectorAll('main article, article'));
    return articles.find(article => isVisible(article)) || null;
  }

  function getPrimaryPostScope() {
    const article = getPrimaryPostArticle();
    const main = Array.from(document.querySelectorAll('main')).find(item => isVisible(item));
    if (!article) return main || document;
    const articleLabels = getActionLabels(article);
    const hasArticleActions = hasPostActionGroup(articleLabels);
    return hasArticleActions ? article : (main || article);
  }

  function isPrimaryPostLikeButton(button, scope) {
    return scorePrimaryPostActionButton(button, scope) >= 35;
  }

  function scorePrimaryPostActionButton(button, scope) {
    if (!scope || !scope.contains(button) || isCommentLikeButton(button, scope)) return -100;
    if (isInsideScrollableCommentPane(button, scope) && !findNearestPostActionGroup(button, scope)) return -80;

    let score = 15;
    const section = findNearestPostActionGroup(button, scope);
    if (!section || !scope.contains(section)) return score;

    const { actionLabels, hasCommentAction, hasShareAction, hasSaveAction } = getSectionActionSummary(section);

    if (hasCommentAction) score += 25;
    if (hasShareAction) score += 25;
    if (hasSaveAction) score += 10;
    if (actionLabels.length >= 3) score += 10;

    return score;
  }

  function isCommentLikeButton(button, scope) {
    if (!scope || !scope.contains(button)) return false;
    const actionGroup = findNearestPostActionGroup(button, scope);
    if (actionGroup) return false;
    if (isInsideScrollableCommentPane(button, scope)) return true;
    const section = button.closest('section');
    if (section && scope.contains(section)) {
      const { hasCommentAction, hasShareAction } = getSectionActionSummary(section);
      if (hasCommentAction && hasShareAction) return false;
    }
    const listItem = button.closest('li, ul, ol, [role="list"], [role="listitem"]');
    if (listItem && scope.contains(listItem) && normalizeText(listItem.innerText || listItem.textContent || '').includes('responder')) return true;

    let parent = button.parentElement;
    for (let depth = 0; parent && parent !== scope && depth < 6; depth += 1) {
      const parentText = normalizeText(parent.innerText || parent.textContent || '');
      if (parentText.includes('responder') || parentText.includes('reply')) return true;
      parent = parent.parentElement;
    }
    return false;
  }

  function getActionLabels(scope) {
    return Array.from(scope?.querySelectorAll?.('[aria-label]') || [])
      .map(item => normalizeText(item.getAttribute('aria-label')));
  }

  function getSectionActionSummary(section) {
    const actionLabels = getActionLabels(section);
    const hasCommentAction = actionLabels.some(label => label === 'comentar' || label === 'comment');
    const hasShareAction = actionLabels.some(label => label.includes('compartilhar') || label.includes('share') || label.includes('send') || label.includes('enviar'));
    const hasSaveAction = actionLabels.some(label => label.includes('salvar') || label.includes('save'));
    return { actionLabels, hasCommentAction, hasShareAction, hasSaveAction };
  }

  function hasPostActionGroup(labels) {
    const hasLike = labels.some(label => matchesPostActionLabel(label, ['like', 'unlike']));
    const hasComment = labels.some(label => label === 'comentar' || label === 'comment' || label.includes('comentario') || label.includes('comment'));
    const hasShare = labels.some(label => label.includes('compartilhar') || label.includes('share') || label.includes('send') || label.includes('enviar'));
    return hasLike && hasComment && hasShare;
  }

  function findNearestPostActionGroup(button, scope) {
    let parent = button.parentElement;
    for (let depth = 0; parent && parent !== scope && depth < 8; depth += 1) {
      if (isScrollableCommentPaneElement(parent)) return null;
      const labels = getActionLabels(parent);
      if (hasPostActionGroup(labels) && isCompactPostActionGroup(parent, button)) return parent;
      parent = parent.parentElement;
    }

    const groups = Array.from((scope || document).querySelectorAll('section, div'));
    return groups.find(group =>
      group.contains(button) &&
      hasPostActionGroup(getActionLabels(group)) &&
      isCompactPostActionGroup(group, button)
    ) || null;
  }

  function isCompactPostActionGroup(group, likeButton) {
    if (!group || !likeButton || !group.contains(likeButton)) return false;
    const groupRect = group.getBoundingClientRect();
    const likeRect = likeButton.getBoundingClientRect();
    if (!groupRect.width || !groupRect.height || !likeRect.width || !likeRect.height) return false;

    const compactHorizontal = groupRect.height <= 180;
    const compactVertical = groupRect.width <= 180 && groupRect.height <= 520;
    if (!compactHorizontal && !compactVertical) return false;

    const controls = Array.from(group.querySelectorAll('[aria-label]'))
      .map(item => ({
        label: normalizeText(item.getAttribute('aria-label') || ''),
        rect: (item.closest?.('button, [role="button"]') || item).getBoundingClientRect()
      }))
      .filter(entry => entry.rect.width > 0 && entry.rect.height > 0);

    const likeX = likeRect.left + (likeRect.width / 2);
    const likeY = likeRect.top + (likeRect.height / 2);
    const nearby = entry => {
      const x = entry.rect.left + (entry.rect.width / 2);
      const y = entry.rect.top + (entry.rect.height / 2);
      const dx = Math.abs(x - likeX);
      const dy = Math.abs(y - likeY);
      return (compactHorizontal && dy <= 90 && dx <= 520) ||
        (compactVertical && dx <= 90 && dy <= 420);
    };

    const hasNearbyComment = controls.some(entry =>
      nearby(entry) && (entry.label === 'comentar' || entry.label === 'comment' || entry.label.includes('comentario'))
    );
    const hasNearbyShare = controls.some(entry =>
      nearby(entry) && (entry.label.includes('compartilhar') || entry.label.includes('share') || entry.label.includes('send') || entry.label.includes('enviar'))
    );
    return hasNearbyComment && hasNearbyShare;
  }

  function matchesPostActionLabel(label, kinds) {
    const clean = normalizeText(label);
    if (!clean) return false;
    const wantsLike = kinds.includes('like');
    const wantsUnlike = kinds.includes('unlike');
    const unlike = clean.includes('descurtir') ||
      clean.includes('remover curtida') ||
      clean.includes('remove like') ||
      clean.includes('unlike');
    const like = !unlike && (
      clean === 'curtir' ||
      clean === 'like' ||
      clean.startsWith('curtir ') ||
      clean.startsWith('like ') ||
      clean.includes('curtir publicação') ||
      clean.includes('curtir postagem') ||
      clean.includes('like this post') ||
      clean.includes('like post')
    );
    return (wantsLike && like) || (wantsUnlike && unlike);
  }

  function isInsideScrollableCommentPane(element, scope) {
    let parent = element.parentElement;
    while (parent && parent !== scope) {
      const style = window.getComputedStyle(parent);
      const overflowY = style.overflowY || style.overflow || '';
      if (isScrollableCommentPaneElement(parent, overflowY)) {
        const text = normalizeText(parent.innerText || parent.textContent || '');
        if (text.includes('responder') || text.includes('reply')) return true;
      }
      parent = parent.parentElement;
    }
    return false;
  }

  function isScrollableCommentPaneElement(element, precomputedOverflowY) {
    if (!element || element === document.body) return false;
    const style = precomputedOverflowY === undefined ? window.getComputedStyle(element) : null;
    const overflowY = precomputedOverflowY === undefined ? (style.overflowY || style.overflow || '') : precomputedOverflowY;
    const scrollable = /auto|scroll/i.test(overflowY) || element.scrollHeight > element.clientHeight + 40;
    if (!scrollable) return false;
    const text = normalizeText(element.innerText || element.textContent || '');
    return text.includes('responder') || text.includes('reply');
  }

  function isPrimaryPostLiked() {
    return !!findUnlikeButton();
  }

  async function waitForPrimaryPostLiked(timeout = 6000) {
    const end = Date.now() + effectiveTimeout(timeout);
    while (Date.now() < end) {
      if (isPrimaryPostLiked()) return true;
      await sleep(300);
    }
    return false;
  }

  async function doComment(username, commentText) {
    try {
      await waitForElement('article, main', 7000);
      await sleep(randInt(800, 1400));
      await dismissInstagramBlockingDialogs();

      if (isRateLimitedPage()) return { success: false, reason: 'instagram_429' };

      const text = String(commentText || '').trim();
      if (!text) return { success: false, reason: 'empty_comment' };

      // Usa somente o compositor de comentarios da publicação aberta.
      let box = null;
      let inserted = false;
      for (let attempt = 0; attempt < 3; attempt++) {
        box = await findCommentBox(getPrimaryPostScope());
        if (!box) return { success: false, reason: 'comment_box_not_found' };

        await dismissInstagramBlockingDialogs();
        box.focus();
        humanClick(box);
        await sleep(randInt(300, 600));

        // O editor Lexical do Instagram reconhece a colagem como entrada real.
        if (box.tagName === 'TEXTAREA' || box.tagName === 'INPUT') box.value = '';
        else box.textContent = '';
        await sleep(100);

        const clipData = new DataTransfer();
        clipData.setData('text/plain', text);
        box.dispatchEvent(new ClipboardEvent('paste', {
          bubbles: true,
          cancelable: true,
          clipboardData: clipData
        }));
        await sleep(400);

        const boxText = normalizeText(getEditableText(box));
        if (boxText.includes(normalizeText(text).slice(0, 20))) {
          inserted = true;
          break;
        }

        // Fallback mantido exatamente como na v3.12.
        box.focus();
        document.execCommand?.('selectAll', false, null);
        document.execCommand?.('delete', false, null);
        document.execCommand?.('insertText', false, text);
        await sleep(300);
        const boxText2 = normalizeText(getEditableText(box));
        if (boxText2.includes(normalizeText(text).slice(0, 20))) {
          inserted = true;
          break;
        }

        await sleep(500);
      }

      if (!inserted) return { success: false, reason: 'comment_text_not_inserted' };

      await sleep(randInt(700, 1200));

      // O botao precisa pertencer ao mesmo compositor do campo.
      await dismissInstagramBlockingDialogs();
      const submit = await waitForSubmitCommentButton(box, 7000);
      if (!submit) return { success: false, reason: 'comment_submit_not_found' };

      const visibleBefore = countVisibleCommentText(text, box);
      submit.scrollIntoView?.({ block: 'center', inline: 'nearest' });
      await sleep(200);
      humanClick(submit);

      // Sucesso somente quando o texto novo aparece nos comentarios em até 8s.
      const confirmed = await waitForCommentVisible(text, box, 8000, visibleBefore);
      if (!confirmed) return { success: false, reason: 'comment_not_visible_after_8s' };

      return { success: true, action: 'comment', target: username };
    } catch (err) {
      return { success: false, error: err.message };
    }
  }

  async function findCommentBox(scope = getPrimaryPostScope()) {
    const end = Date.now() + 7000;
    while (Date.now() < end) {
      const box = findVisibleCommentBox(scope || document);
      if (box) return box;
      await sleep(350);
    }
    return null;
  }

  function findVisibleCommentBox(scope) {
    const items = Array.from(scope.querySelectorAll(
      'textarea[aria-label*="coment"], textarea[aria-label*="comment" i], textarea[placeholder*="coment" i], textarea[placeholder*="comment" i], textarea, [contenteditable="true"][role="textbox"], [contenteditable="true"]'
    ));
    return bestCommentBoxCandidate(items, scope === document ? null : scope);
  }

  function bestCommentBoxCandidate(items, scope) {
    return items
      .map(item => ({ item, score: scoreCommentBoxCandidate(item, scope) }))
      .filter(entry => entry.score > 0)
      .sort((a, b) => b.score - a.score)[0]?.item || null;
  }

  function isCommentBoxCandidate(item, article) {
    return scoreCommentBoxCandidate(item, article) > 0;
  }

  function scoreCommentBoxCandidate(item, scope) {
    if (!item || !isVisible(item) || item.disabled || item.getAttribute('aria-disabled') === 'true') return 0;
    if (scope && !scope.contains(item)) return 0;
    if (item.closest('header, nav, [role="dialog"] header')) return 0;

    const label = normalizeText(`${item.getAttribute('aria-label') || ''} ${item.getAttribute('placeholder') || ''} ${item.getAttribute('data-placeholder') || ''}`);
    const text = normalizeText(item.innerText || item.textContent || '');
    if (label.includes('pesquisar') || label.includes('search')) return 0;

    const isTextArea = item.tagName === 'TEXTAREA';
    const isInput = item.tagName === 'INPUT';
    const isEditable = item.isContentEditable || item.getAttribute('contenteditable') === 'true';
    const hasCommentLabel = label.includes('coment') || label.includes('comment') || text.includes('adicione um comentario') || text.includes('add a comment');
    if (!hasCommentLabel) return 0;

    const rect = item.getBoundingClientRect();
    let score = 20;
    if (hasCommentLabel) score += 60;
    if (isTextArea || isInput) score += 20;
    if (isEditable) score += 15;
    if (normalizeText(item.getAttribute('role') || '') === 'textbox') score += 10;
    if (!isInsideScrollableCommentPane(item, scope || document)) score += 10;
    score += Math.min(20, Math.max(0, rect.top / Math.max(1, window.innerHeight)) * 20);
    return score;
  }

  function findPrimaryCommentActionButton(scope) {
    if (!scope) return null;
    const items = Array.from(scope.querySelectorAll(
      'button[aria-label="Comentar"], button[aria-label="Comment"], [role="button"][aria-label="Comentar"], [role="button"][aria-label="Comment"], [aria-label="Comentar"], [aria-label="Comment"]'
    ));
    for (const item of items) {
      const button = item.closest?.('button, [role="button"]') || item;
      if (!button || !isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
      if (isCommentLikeButton(button, scope)) continue;
      return button;
    }
    return null;
  }

  async function writeCommentText(element, value) {
    const text = String(value || '').trim();
    if (!text) return false;

    for (let attempt = 0; attempt < 3; attempt += 1) {
      element.scrollIntoView?.({ block: 'center', inline: 'nearest' });
      await sleep(attempt === 0 ? 100 : 250);
      element.focus();
      humanClick(element);
      await sleep(150);
      setEditableValue(element, text);
      await sleep(250);
      if (editableContainsText(element, text)) return true;
    }

    return false;
  }

  function setEditableValue(element, value) {
    if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
      const proto = element.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement?.prototype : window.HTMLInputElement?.prototype;
      const setter = Object.getOwnPropertyDescriptor(proto || element.constructor.prototype, 'value')?.set;
      element.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: value }));
      if (setter) setter.call(element, value);
      else element.value = value;
      element.setSelectionRange?.(value.length, value.length);
      element.dispatchEvent(new Event('input', { bubbles: true }));
      element.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
      element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value.slice(-1) || ' ' }));
      element.dispatchEvent(new Event('change', { bubbles: true }));
      return;
    }

    element.focus();
    const selection = window.getSelection?.();
    const range = document.createRange?.();
    if (selection && range) {
      range.selectNodeContents(element);
      selection.removeAllRanges();
      selection.addRange(range);
    }
    element.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: value }));
    document.execCommand?.('delete', false, null);
    const inserted = document.execCommand?.('insertText', false, value);
    if (!inserted || !editableContainsText(element, value)) {
      element.textContent = '';
      element.appendChild(document.createTextNode(value));
    }
    element.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
    element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value.slice(-1) || ' ' }));
    element.dispatchEvent(new Event('change', { bubbles: true }));
  }

  function getEditableText(element) {
    if (!element) return '';
    if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') return element.value || '';
    return element.innerText || element.textContent || '';
  }

  function editableContainsText(element, value) {
    const actual = normalizeText(getEditableText(element));
    const expected = normalizeText(value);
    return !!expected && actual.includes(expected);
  }

  async function waitForSubmitCommentButton(box, timeout = 7000) {
    const end = Date.now() + effectiveTimeout(timeout);
    while (Date.now() < end) {
      const submit = findSubmitCommentButton(box);
      if (submit) return submit;
      await sleep(300);
    }
    return null;
  }

  function findSubmitCommentButton(box) {
    if (!box) return null;

    const form = box.closest('form');
    if (form) {
      const button = Array.from(form.querySelectorAll('button, [role="button"]')).find(isSubmitCommentButton);
      if (button) return button;
    }

    let container = box.parentElement;
    for (let depth = 0; container && depth < 7; depth += 1) {
      const buttons = Array.from(container.querySelectorAll('button, [role="button"]'))
        .filter(isSubmitCommentButton);
      if (buttons.length === 1) return buttons[0];
      if (container.matches('article, main, [role="dialog"]')) break;
      container = container.parentElement;
    }

    return null;
  }

  function isSubmitCommentButton(button) {
    if (!isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') return false;
    const text = normalizeText(button.innerText || button.textContent || button.getAttribute('aria-label') || '');
    if (!text) return false;
    return text === 'publicar' || text === 'postar' || text === 'post' || text === 'publish';
  }

  async function waitForCommentVisible(text, box, timeout = 8000, minimumPreviousCount = 0) {
    const end = Date.now() + effectiveTimeout(timeout);
    while (Date.now() < end) {
      if (countVisibleCommentText(text, box) > minimumPreviousCount) return true;
      await sleep(350);
    }
    return false;
  }

  function isCommentTextVisible(text, box) {
    return countVisibleCommentText(text, box) > 0;
  }

  function countVisibleCommentText(text, box) {
    const article = box?.closest?.('article') || getPrimaryPostArticle();
    const scope = getPrimaryPostScope() || article;
    if (!scope) return 0;
    const needle = normalizeText(text).slice(0, 80);
    if (!needle) return 0;

    const candidates = Array.from(scope.querySelectorAll(
      'li, [role="listitem"], article ul > div, article div, article p, article span, main ul > div, main div, main p, main span'
    ));
    const matches = candidates.filter(item => {
      if (!isVisible(item) || item.contains(box) || item.matches('textarea, input, [contenteditable="true"]')) return false;
      const ownText = normalizeText(item.innerText || item.textContent || '');
      if (!ownText.includes(needle)) return false;
      return !Array.from(item.children).some(child => {
        if (!isVisible(child) || child.contains(box)) return false;
        return normalizeText(child.innerText || child.textContent || '').includes(needle);
      });
    });
    return matches.length;
  }

  function findFollowButton() {
    const buttons = document.querySelectorAll('button, [role="button"]');
    for (const button of buttons) {
      if (!isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
      const text = normalizeText(button.innerText);
      if (text === 'seguir' || text === 'follow' || text === 'seguir de volta' || text === 'follow back') {
        return button;
      }
    }
    return null;
  }

  function findUnfollowButton() {
    const buttons = document.querySelectorAll('button, [role="button"]');
    for (const button of buttons) {
      if (!isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') continue;
      const text = normalizeText(button.innerText);
      if (text === 'seguindo' || text === 'following') return button;
    }
    return null;
  }

  async function waitForUnfollowConfirmButton(timeout = 7000) {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      const buttons = document.querySelectorAll('[role="dialog"] button, [role="dialog"] [role="button"], button, [role="button"]');
      for (const item of buttons) {
        if (!isVisible(item) || item.disabled || item.getAttribute('aria-disabled') === 'true') continue;
        const text = normalizeText(item.innerText || item.textContent || '');
        if (text === 'deixar de seguir' || text === 'unfollow' || text.includes('deixar de seguir')) {
          return item;
        }
      }
      await sleep(200);
    }
    return null;
  }

  async function waitForUnfollowConfirmation(timeout = 7000) {
    const start = Date.now();
    while (Date.now() - start < timeout) {
      if (!isAlreadyFollowing() && !isPendingFollow()) return true;
      await sleep(350);
    }
    return false;
  }

  function isAlreadyFollowing() {
    const buttons = document.querySelectorAll('button, [role="button"]');
    for (const button of buttons) {
      const text = normalizeText(button.innerText);
      if (text === 'seguindo' || text === 'following') return true;
    }
    return false;
  }

  async function waitForFollowConfirmation() {
    const start = Date.now();
    while (Date.now() - start < 5000) {
      if (isAlreadyFollowing() || isPendingFollow()) return true;
      await sleep(400);
    }
    return false;
  }

  function isPendingFollow() {
    const buttons = document.querySelectorAll('button, [role="button"]');
    for (const button of buttons) {
      const text = normalizeText(button.innerText);
      if (text === 'solicitado' || text === 'requested') return true;
    }
    return false;
  }

  function isFollowingBack() {
    const text = normalizeText(document.body.innerText);
    return text.includes('segue você') || text.includes('follows you');
  }

  function isPrivateAccount() {
    const text = normalizeText(document.body.innerText);
    return text.includes('essa conta e privada') || text.includes('this account is private');
  }

  function isOwnProfilePage() {
    const text = normalizeText(document.body.innerText);
    return text.includes('editar perfil') || text.includes('edit profile');
  }

  function getFollowerCount() {
    try {
      const links = document.querySelectorAll('a[href*="/followers/"]');
      for (const link of links) {
        const raw = link.querySelector('span[title]')?.getAttribute('title') ||
          link.querySelector('span')?.innerText ||
          link.innerText ||
          '';
        const number = parseCompactNumber(raw);
        if (number !== null) return number;
      }
    } catch {}
    return null;
  }

  function parseCompactNumber(raw) {
    const text = normalizeText(raw).replace(/\s+/g, '');
    const match = text.match(/([\d.,]+)(mi|mil|k|m)?/);
    if (!match) return null;

    let value = parseFloat(match[1].replace(/\./g, '').replace(',', '.'));
    if (!Number.isFinite(value)) return null;

    const suffix = match[2];
    if (suffix === 'mil' || suffix === 'k') value *= 1000;
    if (suffix === 'mi' || suffix === 'm') value *= 1000000;

    return Math.round(value);
  }

  function waitForElement(selector, timeout = 6000) {
    return new Promise((resolve, reject) => {
      const existing = document.querySelector(selector);
      if (existing) return resolve(existing);

      const observer = new MutationObserver(() => {
        const found = document.querySelector(selector);
        if (found) {
          observer.disconnect();
          resolve(found);
        }
      });

      observer.observe(document.body, { childList: true, subtree: true });
      setTimeout(() => {
        observer.disconnect();
        reject(new Error(`Timeout: ${selector}`));
      }, timeout);
    });
  }

  function humanClick(element) {
    const rect = element.getBoundingClientRect();
    const x = rect.left + rect.width / 2 + (Math.random() * 4 - 2);
    const y = rect.top + rect.height / 2 + (Math.random() * 4 - 2);

    ['mouseover', 'mouseenter', 'mousemove', 'mousedown', 'mouseup'].forEach((type) => {
      element.dispatchEvent(new MouseEvent(type, { bubbles: true, clientX: x, clientY: y }));
    });

    if (typeof element.click === 'function') {
      element.click();
    } else {
      element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
    }
  }

  function humanDoubleClick(element) {
    const rect = element.getBoundingClientRect();
    const x = rect.left + rect.width / 2 + (Math.random() * 8 - 4);
    const y = rect.top + rect.height / 2 + (Math.random() * 8 - 4);

    element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
    ['mouseover', 'mouseenter', 'mousemove'].forEach((type) => {
      element.dispatchEvent(new MouseEvent(type, { bubbles: true, clientX: x, clientY: y }));
    });

    for (let detail = 1; detail <= 2; detail += 1) {
      element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y, detail }));
      element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y, detail }));
      element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y, detail }));
    }

    element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, clientX: x, clientY: y, detail: 2 }));
  }

  function normalizeText(value) {
    return String(value || '')
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .trim()
      .toLowerCase();
  }

  function isRateLimitedPage() {
    const text = normalizeText(`${document.title || ''}\n${document.body?.innerText || ''}`);
    return text.includes('http error 429') ||
      text.includes('error 429') ||
      text.includes('too many requests') ||
      text.includes('muitas solicitacoes');
  }

  function isVisible(element) {
    const rect = element.getBoundingClientRect();
    const style = window.getComputedStyle(element);
    return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
  }

  if (window.__FLOWINSTA_EXPOSE_TESTS__) {
    window.__FLOWINSTA_TESTS__ = {
      doLike,
      doComment,
      findLikeButton,
      findUnlikeButton,
      findPrimaryPostMedia,
      findSubmitCommentButton,
      isCommentTextVisible,
      scorePrimaryPostActionButton,
      isInsideScrollableCommentPane
    };
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, window.__FLOWINSTA_FAST_TESTS__ ? Math.min(ms, 5) : ms));
  }

  function randInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  function effectiveTimeout(timeout) {
    return window.__FLOWINSTA_FAST_TESTS__ ? Math.min(timeout, 600) : timeout;
  }
})();

