const SERVER_URL = 'https://app.flowinstabr.com.br';
const SERVER_WS = 'wss://app.flowinstabr.com.br/ws';
const INSTAGRAM_BASE = 'https://www.instagram.com';
const INSTAGRAM_MATCHES = [
  'https://www.instagram.com/*',
  'https://instagram.com/*'
];
const AUTOMATION_ALARM = 'flowinstabr-next-action';
const RATE_LIMIT_BACKOFF_MS = 45 * 60 * 1000;
const TARGET_REFILL_THRESHOLD = 10;
const TARGET_BATCH_SIZE = 60;
const MIN_USEFUL_TARGETS = 10;
const MAX_POST_AUTHOR_FALLBACKS = 5;
const POST_AUTHOR_DELAY_MS = 12000;
const MAX_EXPANSION_DEPTH = 2;
const MAX_EXPANSION_BASES = 3;
const EXPANSION_TARGET_GOAL = 60;
const EXPANSION_FOLLOWERS_PER_PROFILE = 10;
const COMMENT_EXTRAS = [
  'top', 'demais', 'muito bom', 'parabens', 'adorei', 'show',
  'valeu', 'que legal', 'excelente', 'continue assim', 'mandou bem',
  'conteudo bom', 'muito util', 'obrigado pela dica'
];

// v5.9.19: pedido do Marcelo - as linhas de log [AUTO] nao mostravam a
// que horas cada acao aconteceu de verdade, so o tempo relativo ate a
// proxima ("Proxima ação em Xs"), dificultando reconstruir a linha do
// tempo real ao revisar o console depois. Ao inves de mexer em cada uma
// das centenas de chamadas console.log('[AUTO] ...') espalhadas pelo
// arquivo, prefixa automaticamente com hora:minuto:segundo aqui, uma vez
// so, cobrindo todas elas de uma vez.
const _rawConsoleLog = console.log.bind(console);
const _rawConsoleWarn = console.warn.bind(console);
function _withTimestamp(args) {
  if (typeof args[0] === 'string' && args[0].startsWith('[AUTO]')) {
    const hhmmss = new Date().toLocaleTimeString('pt-BR');
    return [`[${hhmmss}] ${args[0]}`, ...args.slice(1)];
  }
  return args;
}
console.log = (...args) => _rawConsoleLog(..._withTimestamp(args));
console.warn = (...args) => _rawConsoleWarn(..._withTimestamp(args));

let ws = null;
let running = false;
let currentSettings = null;
let currentAccountId = null;
let currentAccountUsername = null;
let currentToken = null;
let wsReconnectTimer = null;
let wsPingTimer = null;
let automationBusy = false;
let automationTabId = null; // Comentario interno corrigido.
let collectTabId = null;   // ID da aba usada para coleta - reutilizada entre coletas

let targetQueue = [];
// v5.9.4: mapa username -> fonte (competitor/hashtag/explore) de onde cada
// alvo da fila foi coletado, so pra dar rastreabilidade no log - nao muda
// nenhuma logica de selecao/fila, so acompanha pra fins de diagnostico.
let targetSourceMap = {};
// Comentario interno corrigido.
// Comentario interno corrigido.
let followedToday = new Set();
let likedToday = new Set();
let commentedToday = new Set();
// Comentario interno corrigido.
let processedToday = new Set();
let alreadyFollowed = new Set(); // Comentario interno corrigido.
let alreadyFollowedLoaded = false;
let alreadyLiked = new Set();     // Comentario interno corrigido.
let alreadyLikedLoaded = false;
let alreadyCommented = new Set(); // Comentario interno corrigido.
let alreadyCommentedLoaded = false;
// v5.8.14: contas ja confirmadas como privadas. Mesmo principio do Agent
// Windows (agent.ps1) - like/comment nunca vao funcionar numa conta
// privada (nao da pra ver o post), entao sao filtrados da fila usando
// este cache. Nao e limpo no reset diario (privacidade da conta e um
// atributo estrutural, nao um contador de uso).
let privateAccounts = new Set();
let unfollowQueue = []; // Fila separada de perfis para fazer unfollow
let lastUnfollowLoadAt = 0; // v5.9.14: throttle de 10min pra recarregar a fila periodicamente
let actionCountToday = { follow: 0, unfollow: 0, like: 0, comment: 0 };
// v5.9.17: confirmado por Marcelo que a Instagram as vezes bloqueia uma acao
// especifica (ex: follow) de verdade - o clique acontece mas a acao nunca se
// confirma, mesmo tentando manualmente fora da automacao. Antes o sistema so
// via isso como "esse alvo especifico falhou" e ia pro proximo candidato,
// sem nunca reconhecer que o problema e da conta/acao, nao do alvo -
// queimando candidatos a toa enquanto o bloqueio durar. Agora conta falhas
// consecutivas do tipo "clique confirmado mas sem efeito" (reasons abaixo)
// por tipo de acao, e poe essa acao especifica em espera por um tempo
// quando o padrao se repete demais. A partir da v6.0.3, qualquer bloqueio
// pausa a automacao inteira ate o cooldown terminar.
const NOT_CONFIRMED_REASONS = {
  follow: 'click_not_confirmed',
  unfollow: 'unfollow_not_confirmed',
  like: 'like_not_confirmed',
  comment: 'comment_not_visible_after_8s'
};
let notConfirmedStreak = { follow: 0, unfollow: 0, like: 0, comment: 0 };
let actionBlockedUntil = { follow: 0, unfollow: 0, like: 0, comment: 0 };
const NOT_CONFIRMED_STREAK_LIMIT = 5;
const ACTION_BLOCK_COOLDOWN_MS = 45 * 60 * 1000;

// v5.9.18: converte o mapa interno (timestamps numericos) pra um objeto so
// com as acoes ATUALMENTE pausadas, em ISO, pra mandar pro servidor mostrar
// no painel (Resumo). Acoes cujo prazo ja passou nao entram.
function activeActionBlocksSnapshot() {
  const now = Date.now();
  const out = {};
  for (const action of ['follow', 'unfollow', 'like', 'comment']) {
    if ((actionBlockedUntil[action] || 0) > now) out[action] = new Date(actionBlockedUntil[action]).toISOString();
  }
  return out;
}

function activeActionBlockWaitMs() {
  const now = Date.now();
  const activeUntil = Object.values(actionBlockedUntil || {})
    .map(value => Number(value || 0))
    .filter(value => value > now);
  if (!activeUntil.length) return 0;
  return Math.max(1000, Math.min(...activeUntil) - now);
}

function clearQueuedTargets(reason) {
  targetQueue = [];
  targetSourceMap = {};
  actionCycleIndex = 0;
  if (reason) console.log(`[AUTO] Fila de alvos limpa: ${reason}`);
}

let lastResetDate = new Date().toDateString();
let lastNoTargetReportAt = 0;
let lastActionFamily = 'unfollow';
// Comentario interno corrigido.
let segTypeIndex = 0;
let actionCycleIndex = 0;
let settingsRevision = 0;
let sourcePickIndex = { hashtag: 0, competitor: 0, location: 0 };
let rateLimitUntil = 0;
let consecutiveRateLimitHits = 0;
let sourceTypeCooldownUntil = {};
let sourceScarcityDepth = { hashtag: 0, competitor: 0, location: 0, explore: 0 };
let sourceScarcityStreak = { hashtag: 0, competitor: 0, location: 0, explore: 0 };
// v5.4.1: cooldown por concorrente especifico (nao so por tipo de fonte) -
// mesmo principio ja aplicado no Agent Windows (agent.ps1) - depois de 3
// falhas seguidas no MESMO concorrente, ele fica de fora da selecao por um
// tempo, em vez de poder ser sorteado de novo e travar a coleta nele.
let competitorFailureCounts = {};
let competitorCooldownUntil = {};
// v5.9.7: cache local da lista de concorrentes restritos vinda do
// servidor - individual por conta, compartilhada com o Agent Windows
// dessa mesma conta. So reconsulta a cada 10min, nao a cada coleta.
let serverRestrictedCompetitors = new Set();
let lastRestrictedSyncAt = 0;
// v5.9.19: confirmado por Marcelo que rodar Extensao e Agent na mesma conta
// (mesmo que em dias/horarios diferentes) faz cada um contar as acoes do
// dia separadamente, so na memoria local - nenhum dos dois sabia o que o
// outro ja tinha feito hoje. O Agent ja e seguro (busca o total real do
// servidor a cada ciclo, incluindo o que a Extensao fez). A Extensao e que
// so confiava no contador local. Agora sincroniza com o servidor (contagem
// real, somando os dois dispositivos) a cada 10min, pegando sempre o maior
// valor entre o local e o do servidor - nunca deixa o contador andar pra
// tras, so corrige pra cima se o outro dispositivo ja tiver feito mais.
let lastDailyCountSyncAt = 0;

async function saveAutomationState() {
  await chrome.storage.local.set({
    automationRunning: running,
    settings: currentSettings,
    targetQueue,
    processedToday: [...processedToday],
    followedToday: [...followedToday],
    likedToday: [...likedToday],
    commentedToday: [...commentedToday],
    actionCountToday,
    lastResetDate,
    lastActionFamily,
    segTypeIndex,
    actionCycleIndex,
    sourcePickIndex,
    rateLimitUntil,
    consecutiveRateLimitHits,
    sourceTypeCooldownUntil,
    sourceScarcityDepth,
    sourceScarcityStreak,
    competitorFailureCounts,
    competitorCooldownUntil,
    actionBlockedUntil,
    privateAccounts: [...privateAccounts]
  });
}

async function restoreAutomationState() {
  const data = await chrome.storage.local.get([
    'token',
    'account',
    'settings',
    'automationRunning',
    'targetQueue',
    'processedToday',
    'followedToday',
    'likedToday',
    'commentedToday',
    'actionCountToday',
    'lastResetDate',
    'lastActionFamily',
    'segTypeIndex',
    'actionCycleIndex',
    'sourcePickIndex',
    'rateLimitUntil',
    'consecutiveRateLimitHits',
    'sourceTypeCooldownUntil',
    'sourceScarcityDepth',
    'sourceScarcityStreak',
    'competitorFailureCounts',
    'competitorCooldownUntil',
    'actionBlockedUntil',
    'privateAccounts'
  ]);

  currentToken = data.token || currentToken;
  currentAccountId = data.account?.id || currentAccountId;
  currentAccountUsername = data.account?.username || currentAccountUsername;
  currentSettings = normalizeSettings(data.settings || currentSettings);
  // Comentario interno corrigido.
  if (currentAccountUsername && currentSettings && !currentSettings.account_username) {
    currentSettings.account_username = currentAccountUsername;
  }
  running = typeof data.automationRunning === 'boolean' ? data.automationRunning : running;
  targetQueue = Array.isArray(data.targetQueue) ? data.targetQueue : targetQueue;
  processedToday = new Set(Array.isArray(data.processedToday) ? data.processedToday : [...processedToday]);
  followedToday = new Set(Array.isArray(data.followedToday) ? data.followedToday : [...followedToday]);
  likedToday = new Set(Array.isArray(data.likedToday) ? data.likedToday : [...likedToday]);
  commentedToday = new Set(Array.isArray(data.commentedToday) ? data.commentedToday : [...commentedToday]);
  actionCountToday = { follow: 0, unfollow: 0, like: 0, comment: 0, ...(data.actionCountToday || {}) };
  lastResetDate = data.lastResetDate || lastResetDate;
  lastActionFamily = data.lastActionFamily === 'normal' || data.lastActionFamily === 'unfollow' ? data.lastActionFamily : 'unfollow';
  segTypeIndex = typeof data.segTypeIndex === 'number' ? data.segTypeIndex : 0;
  actionCycleIndex = typeof data.actionCycleIndex === 'number' ? data.actionCycleIndex : 0;
  sourcePickIndex = { hashtag: 0, competitor: 0, location: 0, ...(data.sourcePickIndex || {}) };
  rateLimitUntil = Number(data.rateLimitUntil || 0);
  consecutiveRateLimitHits = typeof data.consecutiveRateLimitHits === 'number' ? data.consecutiveRateLimitHits : 0;
  sourceTypeCooldownUntil = data.sourceTypeCooldownUntil && typeof data.sourceTypeCooldownUntil === 'object' ? data.sourceTypeCooldownUntil : {};
  sourceScarcityDepth = { hashtag: 0, competitor: 0, location: 0, explore: 0, ...(data.sourceScarcityDepth && typeof data.sourceScarcityDepth === 'object' ? data.sourceScarcityDepth : {}) };
  sourceScarcityStreak = { hashtag: 0, competitor: 0, location: 0, explore: 0, ...(data.sourceScarcityStreak && typeof data.sourceScarcityStreak === 'object' ? data.sourceScarcityStreak : {}) };
  competitorFailureCounts = data.competitorFailureCounts && typeof data.competitorFailureCounts === 'object' ? data.competitorFailureCounts : {};
  competitorCooldownUntil = data.competitorCooldownUntil && typeof data.competitorCooldownUntil === 'object' ? data.competitorCooldownUntil : {};
  actionBlockedUntil = { follow: 0, unfollow: 0, like: 0, comment: 0, ...(data.actionBlockedUntil && typeof data.actionBlockedUntil === 'object' ? data.actionBlockedUntil : {}) };
  privateAccounts = new Set(Array.isArray(data.privateAccounts) ? data.privateAccounts : [...privateAccounts]);

  if (currentToken && currentAccountId) connectWS(currentToken, currentAccountId);
}

function checkDailyReset() {
  const today = new Date().toDateString();
  if (today !== lastResetDate) {
    actionCountToday = { follow: 0, unfollow: 0, like: 0, comment: 0 };
    lastActionFamily = 'unfollow';
    processedToday.clear();
    followedToday.clear();
    likedToday.clear();
    commentedToday.clear();
    // Comentario interno corrigido.
    alreadyLikedLoaded    = false;
    alreadyCommentedLoaded = false;
    alreadyLiked.clear();
    alreadyCommented.clear();
    targetQueue = [];
    targetSourceMap = {};
    lastResetDate = today;
    segTypeIndex = 0;
    actionCycleIndex = 0;
    console.log('[AUTO] Reset diario de contadores');
  }
}

function connectWS(token, accountId) {
  currentToken = token;
  currentAccountId = accountId;

  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;

  ws = new WebSocket(SERVER_WS);

  ws.onopen = () => {
    console.log('[WS] Conectado');
    clearTimeout(wsReconnectTimer);
    clearInterval(wsPingTimer);
    ws.send(JSON.stringify({ event: 'AUTH', token, accountId }));
    notifyPopup({ type: 'WS_STATUS', connected: true });

    wsPingTimer = setInterval(() => {
      if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ event: 'PING', action_blocks: activeActionBlocksSnapshot() }));
    }, 25000);
  };

  ws.onmessage = (event) => {
    let msg;
    try { msg = JSON.parse(event.data); } catch { return; }

    switch (msg.event) {
      case 'CONNECTED':
        console.log('[WS] Autenticado:', msg.username || currentAccountId);
        // Atualizar username da conta a partir do WS se vier
        if (msg.username && !currentAccountUsername) {
          currentAccountUsername = msg.username;
          if (currentSettings) currentSettings.account_username = msg.username;
        }
        // Comentario interno corrigido.
        loadFollowedTargets();
        loadUnfollowTargets();
        loadLikedTargets();
        loadCommentedTargets();
        syncTodayCountsFromServer(true);
        syncSettingsFromServer();
        break;
      case 'START_AUTOMATION': {
        const incoming = normalizeSettings(msg.settings || currentSettings);
        // Comentario interno corrigido.
        if (!incoming.account_username && currentAccountUsername) {
          incoming.account_username = currentAccountUsername;
        }
        // Se o servidor enviou o username junto (campo account_username no settings)
        if (incoming.account_username && !currentAccountUsername) {
          currentAccountUsername = incoming.account_username;
        }
        currentSettings = incoming;
        startAutomation();
        break;
      }
      // v5.9.14: diferente de START_AUTOMATION, so atualiza a configuracao
      // em memoria - nao mexe em running/agendamento. Disparado quando o
      // cliente salva a configuracao no painel enquanto a automacao ja
      // esta rodando (antes isso nunca chegava ate a extensao rodando, so
      // tinha efeito depois de pausar/retomar ou reiniciar).
      case 'SETTINGS_UPDATED': {
        const incoming = normalizeSettings(msg.settings || currentSettings);
        if (!incoming.account_username && currentAccountUsername) {
          incoming.account_username = currentAccountUsername;
        }
        if (incoming.account_username && !currentAccountUsername) {
          currentAccountUsername = incoming.account_username;
        }
        currentSettings = incoming;
        settingsRevision += 1;
        clearQueuedTargets('configuracao atualizada no painel');
        console.log('[AUTO] Configuracao atualizada em tempo real. Rodada atual sera reiniciada antes da proxima acao.');
        chrome.alarms.clear(AUTOMATION_ALARM).finally(async () => {
          await saveAutomationState();
          if (running && !automationBusy) await scheduleNext(1000);
        });
        break;
      }
      case 'STOP_AUTOMATION':
        stopAutomation();
        break;
      case 'AUTH_ERROR':
        console.error('[WS] Auth error:', msg.message);
        stopAutomation();
        if (ws) ws.close();
        break;
    }
  };

  ws.onclose = () => {
    clearInterval(wsPingTimer);
    notifyPopup({ type: 'WS_STATUS', connected: false });
    if (currentToken && currentAccountId) {
      wsReconnectTimer = setTimeout(() => connectWS(currentToken, currentAccountId), 15000);
    }
  };

  ws.onerror = (event) => console.error('[WS] Erro:', event);
}

async function syncSettingsFromServer() {
  if (!currentToken || !currentAccountId) return;
  try {
    const res = await fetch(`${SERVER_URL}/api/follow-settings/${currentAccountId}`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const incoming = normalizeSettings(await res.json());
    if (!incoming.account_username && currentAccountUsername) {
      incoming.account_username = currentAccountUsername;
    }
    if (JSON.stringify(incoming) === JSON.stringify(currentSettings)) return;

    currentSettings = incoming;
    settingsRevision += 1;
    clearQueuedTargets('configuracao sincronizada apos reconexao');
    console.log('[AUTO] Configuracao sincronizada com o servidor apos reconexao.');
    await saveAutomationState();
    if (running && !automationBusy) await scheduleNext(1000);
  } catch (err) {
    console.warn('[AUTO] Nao foi possivel sincronizar a configuracao apos reconexao:', err.message || err);
  }
}

function disconnectWS() {
  clearTimeout(wsReconnectTimer);
  clearInterval(wsPingTimer);
  stopAutomation();
  if (ws) {
    ws.close();
    ws = null;
  }
}

async function startAutomation() {
  if (!currentSettings) {
    console.warn('[AUTO] Configuração ausente. Abra o painel e salve a conta.');
    return;
  }

  currentSettings = normalizeSettings(currentSettings);
  running = true;
  console.log('[AUTO] Iniciando');
  notifyPopup({ type: 'AUTOMATION_STATUS', running: true });
  await saveAutomationState();
  await scheduleNext(1500);
}

async function stopAutomation() {
  running = false;
  await chrome.alarms.clear(AUTOMATION_ALARM);
  console.log('[AUTO] Pausado');
  notifyPopup({ type: 'AUTOMATION_STATUS', running: false });
  await saveAutomationState();
}

async function scheduleNext(forcedDelayMs = null) {
  if (!running || !currentSettings) return;
  checkDailyReset();

  if (isRateLimitBackoffActive()) {
    const remaining = rateLimitUntil - Date.now();
    console.warn(`[AUTO] Instagram em espera por bloqueio temporario. Nova tentativa em ${Math.ceil(remaining / 60000)} min.`);
    await createNextAlarm(remaining);
    await saveAutomationState();
    return;
  }

  const actionBlockWait = activeActionBlockWaitMs();
  if (actionBlockWait > 0) {
    console.warn(`[AUTO] Automacao pausada por suspeita de bloqueio da Instagram. Nova tentativa em ${Math.ceil(actionBlockWait / 60000)} min.`);
    await createNextAlarm(actionBlockWait);
    await saveAutomationState();
    return;
  }

  if (!isWithinSchedule(currentSettings.time_start, currentSettings.time_end)) {
    const minutesLeft = minutesUntilScheduleOpens(currentSettings.time_start, currentSettings.time_end);
    const sleepMs = Math.min(minutesLeft * 60000, 15 * 60000);
    console.log(`[AUTO] Fora do horário (${currentSettings.time_start} - ${currentSettings.time_end}), aguardando... (abre em ${minutesLeft} min)`);
    await createNextAlarm(sleepMs);
    await saveAutomationState();
    return;
  }

  if (allDailyLimitsReached(currentSettings)) {
    console.log('[AUTO] Todos os limites diarios atingidos');
    await stopAutomation();
    return;
  }

  if (targetQueue.length < TARGET_REFILL_THRESHOLD) await collectTargets();

  if (isRateLimitBackoffActive()) {
    await saveAutomationState();
    return;
  }

  const delay = forcedDelayMs || automationDelay(currentSettings);
  await createNextAlarm(delay);
  await saveAutomationState();
}

async function createNextAlarm(delayMs) {
  await chrome.alarms.clear(AUTOMATION_ALARM);
  chrome.alarms.create(AUTOMATION_ALARM, { when: Date.now() + Math.max(1000, delayMs) });
  console.log(`[AUTO] Proxima ação em ${Math.round(delayMs / 1000)}s`);
}

async function runAutomationTick() {
  if (automationBusy) return;
  automationBusy = true;

  try {
    await restoreAutomationState();
    if (!running || !currentSettings) return;
    checkDailyReset();

    // v5.9.19: confirmado por Marcelo (via console) que uma acao ainda
    // acontecia mesmo depois do horario configurado - a checagem de
    // horario/bloqueio so existia dentro de scheduleNext(), chamado DEPOIS
    // de executeNextAction() ja ter rodado. Ou seja: toda vez que o alarme
    // disparava, uma acao acontecia primeiro, e SO DEPOIS o sistema
    // descobria que estava fora do horario - aí so esperava 60s (nao ate o
    // horario reabrir de verdade) e repetia, fazendo uma acao a cada
    // ~60-90s a noite toda inteira. Agora verifica ANTES de executar
    // qualquer coisa, bloqueando a acao em si, nao so o agendamento.
    if (isRateLimitBackoffActive()) {
      const remaining = rateLimitUntil - Date.now();
      console.warn(`[AUTO] Instagram em espera por bloqueio temporario. Nova tentativa em ${Math.ceil(remaining / 60000)} min.`);
      await createNextAlarm(remaining);
      await saveAutomationState();
      return;
    }

    const actionBlockWait = activeActionBlockWaitMs();
    if (actionBlockWait > 0) {
      console.warn(`[AUTO] Automacao pausada por suspeita de bloqueio da Instagram. Nova tentativa em ${Math.ceil(actionBlockWait / 60000)} min.`);
      await createNextAlarm(actionBlockWait);
      await saveAutomationState();
      return;
    }

    if (!isWithinSchedule(currentSettings.time_start, currentSettings.time_end)) {
      const minutesLeft = minutesUntilScheduleOpens(currentSettings.time_start, currentSettings.time_end);
      const sleepMs = Math.min(minutesLeft * 60000, 15 * 60000);
      console.log(`[AUTO] Fora do horário (${currentSettings.time_start} - ${currentSettings.time_end}), aguardando... (abre em ${minutesLeft} min)`);
      await createNextAlarm(sleepMs);
      await saveAutomationState();
      return;
    }

    const revisionAtStart = settingsRevision;
    await syncTodayCountsFromServer();
    if (revisionAtStart !== settingsRevision) {
      console.log('[AUTO] Configuracao mudou durante a sincronizacao. Rodada abortada para reavaliar.');
      await scheduleNext(1000);
      return;
    }
    await executeNextAction(revisionAtStart);
    if (revisionAtStart !== settingsRevision) {
      console.log('[AUTO] Configuracao mudou durante a rodada. Proxima acao sera reavaliada com as novas regras.');
      await scheduleNext(1000);
      return;
    }
    if (isRateLimitBackoffActive()) return;
    await scheduleNext();
  } finally {
    automationBusy = false;
  }
}

// Comentario interno corrigido.
function actionProgress(settings, action) {
  const limit = getDailyLimit(settings, action);
  if (limit <= 0) return 1;
  return (actionCountToday[action] || 0) / limit;
}

function getBestBalancedAction(settings) {
  const actions = getEnabledActions(settings)
    .filter(action => actionCountToday[action] < getDailyLimit(settings, action));
  if (!actions.length) return '';
  return actions.sort((a, b) => {
    const progressDiff = actionProgress(settings, a) - actionProgress(settings, b);
    if (progressDiff !== 0) return progressDiff;
    return (actionCountToday[a] || 0) - (actionCountToday[b] || 0);
  })[0] || '';
}

function getNextPendingAction(settings) {
  if ((settings?.action_distribution_mode || 'balanced') === 'balanced') {
    return getBestBalancedAction(settings) || '';
  }

  const actions = getEnabledActions(settings);
  for (let i = 0; i < actions.length; i++) {
    const candidate = actions[(actionCycleIndex + i) % actions.length];
    const limit = getDailyLimit(settings, candidate);
    if (limit > 0 && actionCountToday[candidate] < limit) return candidate;
  }
  return actions[0] || '';
}

// v5.3.6: unfollow não pode sequestrar a rodada inteira.
// Alterna manutencao de unfollow com follow/like/comment quando ambos existem.
async function executeNextAction(revisionAtStart = settingsRevision) {
  if (revisionAtStart !== settingsRevision) return;
  if (activeActionBlockWaitMs() > 0) return;
  const normalPending = hasPendingNormalAction(currentSettings);

  if (await shouldRunUnfollowBeforeNormal(normalPending)) {
    if (revisionAtStart !== settingsRevision || activeActionBlockWaitMs() > 0) return;
    const didUnfollow = await executeUnfollowAction(false);
    if (didUnfollow || isRateLimitBackoffActive()) return;
  }

  if (normalPending) {
    const didNormal = await executeNormalActionRound(revisionAtStart);
    if (didNormal || isRateLimitBackoffActive()) return;
  }

  if (revisionAtStart !== settingsRevision || activeActionBlockWaitMs() > 0) return;
  await executeUnfollowAction(true);
}

function hasPendingNormalAction(settings) {
  const actions = getEnabledActions(settings);
  return actions.some(action => {
    const limit = getDailyLimit(settings, action);
    return limit > 0 && actionCountToday[action] < limit;
  });
}

async function shouldRunUnfollowBeforeNormal(normalPending) {
  const unfollowLimit = getDailyLimit(currentSettings, 'unfollow');
  if (unfollowLimit <= 0 || actionCountToday.unfollow >= unfollowLimit) return false;
  if ((actionBlockedUntil.unfollow || 0) > Date.now()) return false;
  if (unfollowQueue.length <= 0) {
    // v5.9.14: antes so retornava false aqui e nunca mais tentava recarregar
    // (a fila so era recarregada la no fallback final, que quase nunca era
    // alcancado quando follow/like continuavam dando certo). Agora tenta
    // recarregar (com throttle de 10min embutido em loadUnfollowTargets)
    // toda vez que a fila estiver vazia, garantindo que perfis que atingem
    // o prazo configurado ao longo do dia entrem na fila sem precisar
    // esperar um fallback raro ou reiniciar a extensao.
    await loadUnfollowTargets();
    if (unfollowQueue.length <= 0) return false;
  }
  if (!normalPending) return true;
  if ((currentSettings?.action_distribution_mode || 'balanced') === 'balanced') {
    const normalActions = getEnabledActions(currentSettings)
      .filter(action => actionCountToday[action] < getDailyLimit(currentSettings, action));
    const bestNormalProgress = normalActions.length
      ? Math.min(...normalActions.map(action => actionProgress(currentSettings, action)))
      : 1;
    return actionProgress(currentSettings, 'unfollow') <= bestNormalProgress;
  }
  return lastActionFamily === 'normal';
}

async function executeUnfollowAction(loadIfEmpty) {
  const unfollowLimit = getDailyLimit(currentSettings, 'unfollow');
  if (unfollowLimit <= 0 || actionCountToday.unfollow >= unfollowLimit) return false;
  if ((actionBlockedUntil.unfollow || 0) > Date.now()) return false;

  if (loadIfEmpty && unfollowQueue.length === 0) {
    await loadUnfollowTargets();
  }

  const unfollowTarget = unfollowQueue.shift();
  if (!unfollowTarget) return false;

  const result = await performAction('unfollow', unfollowTarget);
  if (result?.success) {
    lastActionFamily = 'unfollow';
    return true;
  }
  return false;
}

async function executeNormalActionRound(revisionAtStart = settingsRevision) {
  const maxAttempts = 8;

  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    if (revisionAtStart !== settingsRevision) {
      console.log('[AUTO] Configuracao mudou no meio da rodada. Abortando antes de escolher nova acao.');
      return false;
    }
    if (activeActionBlockWaitMs() > 0) return false;
    const nextAction = getNextPendingAction(currentSettings);
    if (!nextAction) return false;
    let target = dequeueTarget(nextAction);
    if (!target) {
      console.log('[AUTO] Fila vazia, coletando novos alvos...');
      await collectTargets();
      if (revisionAtStart !== settingsRevision) {
        console.log('[AUTO] Configuracao mudou durante a coleta. Alvos antigos descartados.');
        return false;
      }
      target = dequeueTarget(nextAction);
      if (!target) return false;
    }

    if (revisionAtStart !== settingsRevision || activeActionBlockWaitMs() > 0) return false;

    const success = (currentSettings.action_mode || 'one_per_contact') === 'all_per_contact'
      ? await executeAllActionsForTarget(target, revisionAtStart)
      : await executeOneActionForTarget(target, revisionAtStart);

    if (success) {
      lastActionFamily = 'normal';
      return true;
    }
    if (isRateLimitBackoffActive()) return false;

    console.log(`[AUTO] @${target} não confirmou ação. Tentando outro alvo...`);
    if (targetQueue.length < TARGET_REFILL_THRESHOLD) await collectTargets();
    await sleep(1200);
  }

  console.log('[AUTO] Muitas falhas seguidas nesta rodada. Aguardando proximo ciclo.');
  return false;
}

async function executeOneActionForTarget(target, revisionAtStart = settingsRevision) {
  if (revisionAtStart !== settingsRevision || activeActionBlockWaitMs() > 0) return false;
  const actions = getEnabledActions(currentSettings);
  if (!actions.length) return false;

  let action = getNextPendingAction(currentSettings);
  if (!actions.includes(action) || actionCountToday[action] >= getDailyLimit(currentSettings, action)) action = null;

  if (!action) {
    console.log('[AUTO] Limites diarios das ações ativas atingidos');
    return false;
  }

  const result = await performAction(action, target);
  if (result?.success || result?.silent) {
    if (result?.success) actionCycleIndex = (actions.indexOf(action) + 1) % actions.length;
    return result?.success ? true : false;
  }
  return false;
}

async function executeAllActionsForTarget(target, revisionAtStart = settingsRevision) {
  const actions = getEnabledActions(currentSettings);
  let anySuccess = false;
  for (const action of actions) {
    if (revisionAtStart !== settingsRevision || activeActionBlockWaitMs() > 0) break;
    const limit = getDailyLimit(currentSettings, action);
    if (limit <= 0 || actionCountToday[action] >= limit) continue;
    const result = await performAction(action, target);
    if (result?.success) anySuccess = true;
    if (isRateLimitBackoffActive()) break;
    if (activeActionBlockWaitMs() > 0) break;
    await sleep(randomDelay(currentSettings?.speed));
  }
  return anySuccess;
}

async function performAction(action, target) {
  let reservationId = '';
  try {
    // Comentario interno corrigido.
    // Comentario interno corrigido.
    let tab = null;
    if (automationTabId) {
      tab = await chrome.tabs.get(automationTabId).catch(() => null);
    }
    if (!tab) {
      tab = await getOrCreateInstagramTab(`${INSTAGRAM_BASE}/${target}/`, false, true);
      automationTabId = tab.id;
    } else {
      await chrome.tabs.update(tab.id, { url: `${INSTAGRAM_BASE}/${target}/` });
      await waitForTabReady(tab.id, 9000);
    }
    await sleep(3000);
    await dismissInstagramBlockingDialogsInTab(tab.id);

    if (await isInstagramRateLimited(tab.id)) {
      await enterRateLimitBackoff(`instagram_429_before_${action}`);
      return { success: false, rateLimited: true, reason: 'instagram_429' };
    }

    let result;
    if (action === 'like' || action === 'comment') {
      const postInfo = await getLatestPostUrl(tab);
      if (postInfo.privateAccount) {
        privateAccounts.add(target);
        await saveAutomationState();
        await reportAction(action, target, 'skipped', 'private_account');
        console.log(`[AUTO] ${action} @${target}: perfil privado`);
        return { success: false, reason: 'private_account' };
      }
      if (!postInfo.url) {
        await reportAction(action, target, 'skipped', 'no_post_found');
        console.log(`[AUTO] ${action} @${target}: sem post encontrado`);
        return { success: false, reason: 'no_post_found' };
      }

      await chrome.tabs.update(tab.id, { url: postInfo.url });
      await waitForTabReady(tab.id, 9000);
      await sleep(2200);
      await dismissInstagramBlockingDialogsInTab(tab.id);

      // Comentario interno corrigido.
      // O Lexical/Instagram ignora KeyboardEvent Enter em abas em background
      if (action === 'comment') {
        await chrome.tabs.update(tab.id, { active: true });
        await sleep(400);
      }

      if (await isInstagramRateLimited(tab.id)) {
        await enterRateLimitBackoff(`instagram_429_before_${action}`);
        return { success: false, rateLimited: true, reason: 'instagram_429' };
      }

      const reservation = await reserveDailyAction(action, target);
      if (!reservation.allowed) {
        if (reservation.reason === 'daily_limit_reached') {
          actionCountToday[action] = Math.max(
            actionCountToday[action] || 0,
            Number(reservation.limit || getDailyLimit(currentSettings, action))
          );
          await saveAutomationState();
        }
        console.log(`[AUTO] ${action} @${target} cancelado pelo limite global: ${reservation.reason}`);
        return { success: false, silent: true, limitReached: true, reason: reservation.reason };
      }
      reservationId = reservation.reservation_id;

      result = await sendToInstagramTab(tab.id, {
        type: 'EXECUTE_ACTION',
        action,
        target,
        commentText: action === 'comment' ? buildComment(currentSettings.comment_templates) : '',
        settings: currentSettings
      });
    } else {
      const reservation = await reserveDailyAction(action, target);
      if (!reservation.allowed) {
        if (reservation.reason === 'daily_limit_reached') {
          actionCountToday[action] = Math.max(
            actionCountToday[action] || 0,
            Number(reservation.limit || getDailyLimit(currentSettings, action))
          );
          await saveAutomationState();
        }
        console.log(`[AUTO] ${action} @${target} cancelado pelo limite global: ${reservation.reason}`);
        return { success: false, silent: true, limitReached: true, reason: reservation.reason };
      }
      reservationId = reservation.reservation_id;

      result = await sendToInstagramTab(tab.id, {
        type: 'EXECUTE_ACTION',
        action,
        target,
        settings: currentSettings
      });
    }

    if (result?.success) {
      consecutiveRateLimitHits = 0;
      notConfirmedStreak[action] = 0;
      actionCountToday[action] = (actionCountToday[action] || 0) + 1;
      const recorded = await reportAction(action, target, 'ok', '', reservationId);
      reservationId = '';
      if (!recorded) {
        console.error('[AUTO] Ação executada, mas o servidor não confirmou o registro. Automação pausada por segurança.');
        await stopAutomation();
      }
      console.log(`[AUTO] ${action} @${target} (${actionCountToday[action]}/${getDailyLimit(currentSettings, action)}) [fonte: ${targetSourceMap[target] || 'desconhecida'}]`);
      // Adiciona ao cache persistente correto
      if (action === 'follow')  alreadyFollowed.add(target);
      if (action === 'like')    alreadyLiked.add(target);
      if (action === 'comment') alreadyCommented.add(target);
      return { success: true, action, target };
    } else {
      const reason = result?.reason || result?.error || 'unknown';
      if (reason === 'instagram_429' || reason === 'rate_limited') {
        await reportAction(action, target, 'failed', reason, reservationId);
        reservationId = '';
        await enterRateLimitBackoff(reason);
        return { success: false, rateLimited: true, reason };
      }
      if (reason === NOT_CONFIRMED_REASONS[action]) {
        notConfirmedStreak[action] = (notConfirmedStreak[action] || 0) + 1;
        if (notConfirmedStreak[action] >= NOT_CONFIRMED_STREAK_LIMIT) {
          actionBlockedUntil[action] = Date.now() + ACTION_BLOCK_COOLDOWN_MS;
          notConfirmedStreak[action] = 0;
          const untilStr = new Date(actionBlockedUntil[action]).toLocaleTimeString('pt-BR');
          console.warn(`[AUTO] "${action}" falhou em confirmar ${NOT_CONFIRMED_STREAK_LIMIT}x seguidas em alvos diferentes - provavel bloqueio da Instagram. Automacao inteira pausada ate ${untilStr}.`);
          await saveAutomationState();
          if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ event: 'ACTION_BLOCK_STATUS', blocks: activeActionBlocksSnapshot() }));
          }
        }
      } else {
        // Motivo diferente (already_liked, button_not_found, etc.) nao e
        // sinal de bloqueio - so reseta a contagem.
        notConfirmedStreak[action] = 0;
      }
      // Comentario interno corrigido.
      if (action === 'follow' && reason === 'already_following') {
        alreadyFollowed.add(target);
        processedToday.add(target);
        followedToday.add(target); // Comentario interno corrigido.
        await reportAction(action, target, 'skipped', reason, reservationId);
        reservationId = '';
        return { success: false, reason, silent: true };
      }
      if (reason === 'private_account' && !privateAccounts.has(target)) {
        privateAccounts.add(target);
        await saveAutomationState();
      }
      const status = ['not_following', 'profile_unavailable', 'already_following', 'already_liked', 'own_account', 'private_account', 'vip_protected', 'follows_back'].includes(reason)
        ? 'skipped'
        : 'failed';
      await reportAction(action, target, status, reason, reservationId);
      reservationId = '';
      console.log(`[AUTO] ${action} falhou @${target}: ${reason} [fonte: ${targetSourceMap[target] || 'desconhecida'}]`);
      return { success: false, reason };
    }
  } catch (err) {
    console.error(`[AUTO] Erro ao executar ${action}:`, err.message || err);
    if (reservationId) {
      await reportAction(action, target, 'failed', err.message || 'exception', reservationId);
    } else {
      await reportAction(action, target, 'failed', err.message || 'exception');
    }
    return { success: false, reason: err.message || 'exception' };
  }
}

async function getLatestPostUrl(tab) {
  try {
    const [{ result }] = await chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func: () => {
        const clean = (value) => String(value || '')
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '')
          .trim()
          .toLowerCase();
        const bodyText = clean(document.body ? document.body.innerText : '');
        const isPrivate =
          bodyText.includes('essa conta e privada') ||
          bodyText.includes('esta conta e privada') ||
          bodyText.includes('este perfil e privado') ||
          bodyText.includes('esse perfil e privado') ||
          bodyText.includes('this account is private') ||
          bodyText.includes('this profile is private');
        if (isPrivate) return { url: null, privateAccount: true };
        const links = Array.from(document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"]'));
        return { url: links.length ? links[0].href : null, privateAccount: false };
      }
    }).catch(() => [{ result: { url: null, privateAccount: false } }]);
    return result || { url: null, privateAccount: false };
  } catch {
    return { url: null, privateAccount: false };
  }
}

async function dismissInstagramBlockingDialogsInTab(tabId) {
  try {
    const [injection] = await chrome.scripting.executeScript({
      target: { tabId },
      func: async () => {
        const clean = (value) => String(value || '')
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '')
          .trim()
          .toLowerCase();
        const isVisible = (el) => {
          if (!el) return false;
          const rect = el.getBoundingClientRect();
          const style = getComputedStyle(el);
          return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
        };
        const 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 }));
        };

        const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')).filter(isVisible);
        for (const dialog of dialogs) {
          const text = clean(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 = clean(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 new Promise((resolve) => setTimeout(resolve, 500));
            return { ok: true, 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 new Promise((resolve) => setTimeout(resolve, 300));
          return { ok: true, dismissed: true, method: 'escape' };
        }
        return { ok: true, dismissed: false };
      }
    });
    const result = injection?.result || { ok: false, reason: 'no_result' };
    if (result.dismissed) console.log(`[AUTO] Modal do Instagram fechado automaticamente (${result.method}).`);
    return result;
  } catch (err) {
    console.warn('[AUTO] Falha ao tentar fechar modal do Instagram:', err?.message || err);
    return { ok: false, reason: String(err?.message || err) };
  }
}

async function blockInstagramNotificationsPermission() {
  try {
    if (!chrome.contentSettings?.notifications?.set) return;
    await chrome.contentSettings.notifications.set({
      primaryPattern: 'https://www.instagram.com/*',
      setting: 'block'
    });
    await chrome.contentSettings.notifications.set({
      primaryPattern: 'https://instagram.com/*',
      setting: 'block'
    });
  } catch (err) {
    console.warn('[AUTO] Nao foi possivel bloquear notificacoes do Instagram:', err?.message || err);
  }
}

function buildComment(templates) {
  const bases = Array.isArray(templates) && templates.length
    ? templates
    : ['Que post incrivel', 'Adorei esse conteúdo', 'Muito bom mesmo'];
  const base = String(randomFrom(bases) || '').trim();
  const extraCount = 2 + Math.floor(Math.random() * 2);
  const extras = COMMENT_EXTRAS.slice().sort(() => Math.random() - 0.5).slice(0, extraCount);
  return `${base} ${extras.join(' ')}`.trim().slice(0, 220);
}

// v5.4.1: abre o dialogo de seguidores clicando no controle visivel do
// perfil ja carregado. O Instagram nem sempre expoe esse controle como
// a[href*="/followers/"], entao tambem procuramos por texto/role clicavel.
// Injetada via
// chrome.scripting.executeScript (permissao "scripting" ja no manifest) em
// vez de entrar no content.js, que tem hash travado contra a versao
// validada da v4.0.6 (ver tests/system-regression.test.js).
async function openCompetitorFollowersDialogInTab(tabId) {
  try {
    const [injection] = await chrome.scripting.executeScript({
      target: { tabId },
      func: async () => {
        const clean = (value) => String(value || '')
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '')
          .trim()
          .toLowerCase();
        const isVisible = (el) => {
          if (!el) return false;
          const rect = el.getBoundingClientRect();
          const style = getComputedStyle(el);
          return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
        };
        const dismissBlockingDialogs = async () => {
          const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')).filter(isVisible);
          for (const dialog of dialogs) {
            const text = clean(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 = clean(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) {
              dismiss.click();
              await new Promise((resolve) => setTimeout(resolve, 500));
              return true;
            }
            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 new Promise((resolve) => setTimeout(resolve, 300));
            return true;
          }
          return false;
        };
        const followersScope = () =>
          document.querySelector('main header') || document.querySelector('header') || document.querySelector('main') || document;
        const pageState = () => {
          const text = clean(document.body ? document.body.innerText : '');
          const restrictionPatterns = [
            'acao bloqueada', 'nos restringimos', 'restringimos determinadas atividades',
            'adicionamos uma restricao', 'tente novamente mais tarde',
            'try again later', 'action blocked', 'we restrict certain activity',
            "we've limited", 'we have limited'
          ];
          if (restrictionPatterns.some((p) => text.includes(clean(p)))) return 'instagram_restriction';
          if (text.includes('esta conta e privada') || text.includes('essa conta e privada') || text.includes('este perfil e privado') || text.includes('esse perfil e privado') || text.includes('this account is private') || text.includes('this profile is private')) return 'private_account';
          if (text.includes('entre para ver') || text.includes('faca login') || text.includes('log in to see')) return 'login_required';
          if (text.includes('pagina nao esta disponivel') || text.includes('page is not available') || text.includes('usuario nao encontrado') || text.includes('user not found')) return 'profile_unavailable';
          return '';
        };
        const isFollowersText = (value) => {
          const text = clean(value).replace(/\s+/g, ' ');
          return text.includes('seguidores') || text.includes('seguidor') || text.includes('followers') || text.includes('follower');
        };
        const shortText = (el) => clean((el && (el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('title'))) || '')
          .replace(/\s+/g, ' ')
          .slice(0, 160);
        const clickableFollowerTarget = (el) => {
          if (!el) return null;
          const direct = el.closest ? el.closest('a,button,[role="link"],[role="button"]') : null;
          let target = direct || el;
          if (direct) {
            const drect = direct.getBoundingClientRect();
            const dtext = shortText(direct);
            if (drect.width > 360 || drect.height > 120 || dtext.length > 120) target = el;
          }
          return isVisible(target) ? target : null;
        };
        const followersDiagnostics = () => {
          const scope = followersScope();
          const header = document.querySelector('main header') || document.querySelector('header');
          const candidates = Array.prototype.slice.call(scope.querySelectorAll('a,button,[role="link"],[role="button"],span,div,li'))
            .filter((el) => isVisible(el) && isFollowersText(shortText(el)))
            .slice(0, 8)
            .map((el) => ({
              tag: el.tagName,
              role: el.getAttribute('role') || '',
              href: el.getAttribute('href') || '',
              text: shortText(el).slice(0, 80)
            }));
          return {
            url: location.href,
            title: document.title || '',
            headerText: clean(header ? (header.innerText || header.textContent || '') : '').replace(/\s+/g, ' ').slice(0, 220),
            followersLinks: document.querySelectorAll('a[href*="/followers/"]').length,
            candidates
          };
        };
        const findFollowersControl = () => {
          const scope = followersScope();
          const candidates = [];
          const add = (el) => {
            const target = clickableFollowerTarget(el);
            if (!target || candidates.some((item) => item.el === target)) return;
            const rect = target.getBoundingClientRect();
            const text = shortText(target);
            if (rect.width > 420 || rect.height > 140 || !text || text.length > 140) return;
            let score = 1000 + (rect.width * rect.height / 100);
            const href = target.getAttribute?.('href') || '';
            if (href.includes('/followers/')) score -= 600;
            if (/[0-9]/.test(text)) score -= 160;
            if (text.includes('seguidores') || text.includes('followers')) score -= 120;
            candidates.push({ el: target, score });
          };

          Array.prototype.slice.call(scope.querySelectorAll('a[href*="/followers/"]')).forEach(add);
          Array.prototype.slice.call(scope.querySelectorAll('a,button,[role="link"],[role="button"],span,div,li'))
            .slice(0, 700)
            .forEach((el) => {
              if (!isVisible(el)) return;
              const text = shortText(el);
              if (!text || text.length > 120 || !isFollowersText(text)) return;
              add(el);
            });

          candidates.sort((a, b) => a.score - b.score);
          const picked = candidates.find((item) => {
            const rect = item.el.getBoundingClientRect();
            return rect.width > 0 && rect.height > 0;
          });
          return picked ? picked.el : null;
        };
        const 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 }));
        };
        const waitForDialog = (timeout) => new Promise((resolve) => {
          const deadline = Date.now() + timeout;
          const check = () => {
            if (document.querySelector('[role="dialog"]')) return resolve(true);
            if (Date.now() >= deadline) return resolve(false);
            setTimeout(check, 250);
          };
          check();
        });

        const waitForFollowersControl = async (timeout) => {
          const deadline = Date.now() + timeout;
          while (Date.now() < deadline) {
            const state = pageState();
            if (state) return { state };
            const control = findFollowersControl();
            if (control) return { control };
            await new Promise((resolve) => setTimeout(resolve, 300));
          }
          return {};
        };

        // v5.9.0/v5.9.2: o Instagram por vezes abre o dialogo numa sub-view
        // restrita ("seguidores em comum" / mutualOnly), so 1-2 contas com
        // um link "Ver todos os seguidores". Sem clicar nesse link a coleta
        // so ve essa lista minuscula (sempre as mesmas contas) achando que
        // e a lista inteira.
        // v5.9.2: o link nao e um <a>/<button> semantico (confirmado em log
        // de producao - so achava o botao "Fechar"). Alem disso, clicar
        // sozinho nao basta: precisa esperar mais e rolar varias vezes ate
        // a lista real terminar de carregar (confirmado manualmente).
        // v5.9.3: nao restringe mais a busca ao primeiro [role="dialog"] do
        // DOM - logs de producao mostraram que "ver todos os seguidores"
        // nunca aparecia entre os candidatos. Se houver mais de um elemento
        // com role="dialog" na pagina ao mesmo tempo, o querySelector (que
        // pega so o primeiro) pode escopar o dialogo errado. isVisible() ja
        // filtra o que esta realmente renderizado, entao buscar o
        // documento inteiro e seguro.
        const findSeeAllFollowersLink = () => {
          const scope = document;
          const isSeeAllLink = (text) => text.includes('ver todos os seguidores') || text.includes('see all followers') || (text.includes('ver todos') && text.includes('seguidor'));
          let best = null;
          let bestArea = Infinity;
          for (const el of scope.querySelectorAll('a,button,[role="link"],[role="button"],span,div,li')) {
            if (!isVisible(el)) continue;
            const text = clean(el.innerText || el.textContent || '').replace(/\s+/g, ' ');
            if (!text || text.length > 80 || !isSeeAllLink(text)) continue;
            const rect = el.getBoundingClientRect();
            if (rect.height > 80) continue;
            const area = rect.width * rect.height;
            if (area > 0 && area < bestArea) { bestArea = area; best = el; }
          }
          return best;
        };
        // v5.9.15: quando o link "ver todos os seguidores" nao e achado, nao
        // tinha nenhum detalhe do que a busca encontrou no lugar - impossivel
        // diagnosticar por que falhou numa conta especifica (ex: adtail.ag)
        // sem essa informacao. Agora guarda os textos visiveis curtos dentro
        // do dialogo, pra comparar da proxima vez que acontecer.
        const debugDialogTexts = () => {
          const dialog = document.querySelector('[role="dialog"]') || document;
          const texts = [];
          for (const el of dialog.querySelectorAll('a,button,[role="link"],[role="button"],span,div,li')) {
            if (!isVisible(el)) continue;
            const text = clean(el.innerText || el.textContent || '').replace(/\s+/g, ' ');
            if (!text || text.length > 60 || texts.includes(text)) continue;
            texts.push(text);
            if (texts.length >= 12) break;
          }
          return texts;
        };
        const scrollFollowersDialogUntilLoaded = async () => {
          const root = document;
          const countLinks = () => root.querySelectorAll('a[href]').length;
          const scrollable = () => {
            const candidates = Array.from(root.querySelectorAll('[role="dialog"], div'))
              .filter((el) => el.scrollHeight > el.clientHeight + 80)
              .sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight));
            return candidates[0] || null;
          };
          let lastCount = countLinks();
          let stableRounds = 0;
          const maxRounds = 6;
          for (let round = 0; round < maxRounds; round += 1) {
            const el = scrollable();
            const stepCount = 3 + Math.floor(Math.random() * 2);
            const totalPx = 500 + Math.floor(Math.random() * 400);
            const perStep = totalPx / stepCount;
            for (let i = 0; i < stepCount; i += 1) {
              if (el) el.scrollTop = Math.min(el.scrollHeight, el.scrollTop + perStep);
              else window.scrollBy(0, perStep);
              await new Promise((resolve) => setTimeout(resolve, 180 + Math.floor(Math.random() * 220)));
            }
            await new Promise((resolve) => setTimeout(resolve, 1000 + Math.floor(Math.random() * 700)));
            const count = countLinks();
            if (count <= lastCount) {
              stableRounds += 1;
              if (stableRounds >= 2) break;
            } else {
              stableRounds = 0;
            }
            lastCount = count;
          }
        };
        const escapeMutualOnlyFollowersView = async () => {
          const isMutualOnly = () => location.pathname.includes('/followers/mutualOnly');
          let link = findSeeAllFollowersLink();
          // v5.9.16: se estiver na sub-view restrita mas nao achou o link
          // de primeira, da mais uma ou duas chances antes de desistir -
          // confirmado em producao que o dialogo por vezes ainda esta
          // vazio ("Textos visiveis: seguidores | fechar") nesse instante
          // exato, antes do conteudo real (lista ou link) terminar de
          // renderizar. So espera quando realmente esta em mutualOnly sem
          // link, pra nao atrasar o caso normal (dialogo ja carregado,
          // nada pra escapar).
          for (let retry = 0; retry < 2 && !link && isMutualOnly(); retry += 1) {
            await new Promise((resolve) => setTimeout(resolve, 1200 + Math.floor(Math.random() * 800)));
            link = findSeeAllFollowersLink();
          }
          if (!isMutualOnly() && !link) return { ok: true, skipped: true };
          if (!link) return { ok: false, reason: 'see_all_link_not_found', debugTexts: debugDialogTexts() };
          humanClick(link);
          // Depois do clique, a lista real demora alguns segundos pra
          // comecar a carregar - espera ate 10s (nao mais so 5s).
          const deadline = Date.now() + 10000;
          let left = false;
          while (Date.now() < deadline) {
            if (!isMutualOnly()) { left = true; break; }
            await new Promise((resolve) => setTimeout(resolve, 300));
          }
          if (!left) return { ok: false, reason: 'still_mutual_only' };
          await new Promise((resolve) => setTimeout(resolve, 500 + Math.floor(Math.random() * 400)));
          // Mesmo fora do mutualOnly, a lista completa so aparece rolando
          // repetidas vezes - uma rolada unica nao basta.
          await scrollFollowersDialogUntilLoaded();
          return { ok: true };
        };

        // v5.9.5: a Instagram por vezes confirma explicitamente que o
        // proprio concorrente restringe quem ve a lista completa de
        // seguidores ("Somente @username pode ver todos os seguidores").
        // Isso e configuracao da conta, nao falha temporaria.
        const isFollowersListRestricted = () => {
          const dialog = document.querySelector('[role="dialog"]');
          const scope = dialog || document;
          const text = clean(scope.innerText || scope.textContent || '').replace(/\s+/g, ' ');
          // v5.9.9: alem de "Somente @X pode ver todos os seguidores", a
          // Instagram tambem mostra tela vazia com "Sugestoes para voce"
          // dentro do mesmo dialogo - confirmado que gente coletada assim
          // nao segue o concorrente de verdade (sao sugestoes genericas).
          return text.includes('pode ver todos os seguidores') ||
            text.includes('can see all followers') ||
            text.includes('sugestoes para voce') ||
            text.includes('suggested for you');
        };

        await dismissBlockingDialogs();
        const located = await waitForFollowersControl(7000);
        if (located.state) return { ok: false, reason: located.state, detail: followersDiagnostics() };
        if (!located.control) return { ok: false, reason: 'followers_link_not_found', detail: followersDiagnostics() };
        await dismissBlockingDialogs();
        humanClick(located.control);
        if (await waitForDialog(8000)) {
          const restricted = isFollowersListRestricted();
          const expanded = await escapeMutualOnlyFollowersView();
          return { ok: true, restricted, mutualOnlyExpanded: expanded.ok, mutualOnlyReason: expanded.reason, mutualOnlyDebugTexts: expanded.debugTexts };
        }

        // Se o controle ainda estiver visivel, o clique claramente nao teve
        // efeito - tenta mais uma vez antes de desistir.
        const control = findFollowersControl();
        if (control) {
          humanClick(control);
          if (await waitForDialog(5000)) {
            const restricted = isFollowersListRestricted();
            const expanded = await escapeMutualOnlyFollowersView();
            return { ok: true, retried: true, restricted, mutualOnlyExpanded: expanded.ok, mutualOnlyReason: expanded.reason, mutualOnlyDebugTexts: expanded.debugTexts };
          }
        }
        return { ok: false, reason: 'followers_dialog_not_open', detail: followersDiagnostics() };
      }
    });
    return (injection && injection.result) || { ok: false, reason: 'injection_failed' };
  } catch (err) {
    return { ok: false, reason: 'injection_exception', detail: String((err && err.message) || err) };
  }
}

async function collectTargets() {
  if (!currentSettings) return;

  const availableTypes = getSegTypes(currentSettings);
  if (!availableTypes.length) {
    console.log('[AUTO] Nenhuma fonte de coleta configurada');
    return;
  }

  let totalQueued = 0;
  const maxAttempts = Math.min(availableTypes.length, 4);
  for (let attempt = 0; attempt < maxAttempts && targetQueue.length < TARGET_BATCH_SIZE; attempt += 1) {
    const queued = await collectTargetsFromActiveSource();
    totalQueued += queued;
    if (isRateLimitBackoffActive()) return;
    if (queued >= MIN_USEFUL_TARGETS || targetQueue.length >= TARGET_REFILL_THRESHOLD) break;
    console.log('[AUTO] Fonte rendeu pouco alvo; tentando proxima fonte configurada no mesmo ciclo.');
  }

  if (totalQueued === 0) {
    console.log('[AUTO] Nenhuma fonte conseguiu gerar alvo util neste ciclo.');
  }
}

async function collectTargetsFromActiveSource() {
  if (!currentSettings) return 0;

  try {
    const activeType = getActiveSegType(currentSettings);
    // v5.9.7: antes de escolher concorrente, sincroniza com o servidor
    // (throttle de 10min embutido) pra tambem respeitar o que o Agent
    // Windows dessa mesma conta ja descobriu.
    if (activeType === 'competitor') await syncRestrictedCompetitorsFromServer();
    const url = targetCollectionUrl(currentSettings);
    if (!url) {
      console.log('[AUTO] Nenhuma fonte de coleta configurada');
      advanceSegTypeIndex(currentSettings);
      return 0;
    }

    console.log(`[AUTO] Coletando de: ${url}`);
    // Comentario interno corrigido.
    // Comentario interno corrigido.
    let collectTab = null;
    if (collectTabId) {
      collectTab = await chrome.tabs.get(collectTabId).catch(() => null);
    }
    if (!collectTab) {
      collectTab = await getOrCreateInstagramTab(url, false, false, automationTabId);
      collectTabId = collectTab.id;
    } else {
      await chrome.tabs.update(collectTab.id, { url });
      await waitForTabReady(collectTab.id, 9000);
    }
    const tab = collectTab;
    await sleep(3200 + Math.floor(Math.random() * 1600));
    await dismissInstagramBlockingDialogsInTab(tab.id);

    if (await isInstagramRateLimited(tab.id)) {
      await skipSourceTypeForNow(activeType, 'instagram_429_on_collection');
      return 0;
    }

    let competitorUsername = '';
    if (activeType === 'competitor') {
      competitorUsername = (new URL(url).pathname.split('/').filter(Boolean)[0] || '').toLowerCase();
      const dialogResult = await openCompetitorFollowersDialogInTab(tab.id);
      if (!dialogResult.ok) {
        console.warn(`[AUTO] Concorrente @${competitorUsername}: dialogo de seguidores nao abriu (${dialogResult.reason}).`);
        if (dialogResult.reason === 'private_account') {
          // Perfil privado e condicao permanente - poe em espera direto,
          // sem gastar 2 tentativas a mais tentando de novo pra nada.
          competitorCooldownUntil[competitorUsername] = Date.now() + 20 * 60 * 1000;
          await saveAutomationState();
        } else {
          await registerCompetitorFailure(competitorUsername, dialogResult.reason);
        }
        await reportNoTargets(`${dialogResult.reason}_competitor`);
        advanceSegTypeIndex(currentSettings);
        return 0;
      }
      await registerCompetitorSuccess(competitorUsername);
      if (dialogResult.restricted) {
        await registerCompetitorRestricted(competitorUsername);
      }
      if (dialogResult.mutualOnlyExpanded === false) {
        console.warn(`[AUTO] Concorrente @${competitorUsername}: nao consegui expandir para lista completa de seguidores (${dialogResult.mutualOnlyReason}) - seguindo com o que estiver visivel.`);
        if (Array.isArray(dialogResult.mutualOnlyDebugTexts) && dialogResult.mutualOnlyDebugTexts.length) {
          console.warn(`[AUTO] Textos visiveis no dialogo: ${dialogResult.mutualOnlyDebugTexts.join(' | ')}`);
        }
      }
    }

    // v5.9.8: pra concorrente, monta quem ja teve TODAS as acoes habilitadas
    // concluidas (caches de longo prazo, nao so "processado hoje") - o
    // content.js usa isso pra saber se vale a pena continuar rolando em
    // vez de parar assim que achar qualquer nome, mesmo ja esgotado.
    let exhausted = [];
    if (activeType === 'competitor') {
      const actionsEnabled = Array.isArray(currentSettings?.actions_enabled) ? currentSettings.actions_enabled : ['follow'];
      const union = new Set([...alreadyFollowed, ...alreadyLiked, ...alreadyCommented]);
      exhausted = Array.from(union).filter((u) => {
        if (actionsEnabled.includes('follow') && !alreadyFollowed.has(u)) return false;
        if (actionsEnabled.includes('like') && !alreadyLiked.has(u)) return false;
        if (actionsEnabled.includes('comment') && !alreadyCommented.has(u)) return false;
        return true;
      });
    }

    const result = await sendToInstagramTab(tab.id, {
      type: 'COLLECT_TARGETS',
      settings: currentSettings,
      activeType,
      sourceAccount: competitorUsername,
      processed: [...processedToday],
      exhausted
    });

    if (result?.rateLimited) {
      await skipSourceTypeForNow(activeType, 'instagram_429_collect_targets');
      return 0;
    }

    const directTargets = Array.isArray(result?.targets) ? result.targets : [];
    let cleanTargets = normalizeCollectedTargets(directTargets);

    if (cleanTargets.length === 0 && Array.isArray(result?.postLinks) && result.postLinks.length > 0) {
      const fallbackLinks = result.postLinks.slice(0, MAX_POST_AUTHOR_FALLBACKS);
      console.log(`[AUTO] Sem alvos diretos; abrindo poucos posts para identificar autores (${fallbackLinks.length})`);
      const postAuthors = await collectAuthorsFromPostPages(tab, fallbackLinks, url, activeType);
      cleanTargets = normalizeCollectedTargets(postAuthors);
      if (isRateLimitBackoffActive() || isSourceTypeCoolingDown(activeType)) return 0;
    }

    if (cleanTargets.length < MIN_USEFUL_TARGETS) {
      const currentDepth = Math.min(Number(sourceScarcityDepth[activeType] || 0), MAX_EXPANSION_DEPTH);
      const expansionDepth = Math.min(currentDepth + 1, MAX_EXPANSION_DEPTH);
      if (expansionDepth > 0 && cleanTargets.length > 0) {
        console.log(`[AUTO] Fonte ${activeType} escassa (${cleanTargets.length} alvos). Expandindo ${expansionDepth} nivel(is).`);
        const expanded = await expandTargetsByFollowers(cleanTargets, activeType, expansionDepth, url);
        cleanTargets = normalizeCollectedTargets([...cleanTargets, ...expanded]);
        if (isRateLimitBackoffActive() || isSourceTypeCoolingDown(activeType)) return 0;
      }
    }

    let queuedCount = 0;
    if (cleanTargets.length > 0) {
      const queued = cleanTargets.slice(0, Math.max(0, TARGET_BATCH_SIZE - targetQueue.length));
      targetQueue.push(...queued);
      for (const t of queued) { targetSourceMap[t] = activeType; }
      queuedCount = queued.length;
      console.log(`[AUTO] ${cleanTargets.length} alvos coletados via ${activeType}; ${queuedCount} adicionados na fila.`);
      // Comentario interno corrigido.
      advanceSegTypeIndex(currentSettings);
    } else {
      const debug = result?.debug ? ` debug=${JSON.stringify(result.debug)}` : '';
      console.log(`[AUTO] Nenhum alvo coletado nesta tentativa${debug}`);
      await reportNoTargets('no_targets_collected');
      // Comentario interno corrigido.
      advanceSegTypeIndex(currentSettings);
    }
    updateSourceScarcity(activeType, queuedCount);
    await saveAutomationState();
    return queuedCount;
  } catch (err) {
    console.error('[AUTO] Erro ao coletar alvos:', err.message || err);
    return 0;
  }
}

function updateSourceScarcity(type, queuedCount) {
  if (!type) return;
  if (queuedCount >= MIN_USEFUL_TARGETS) {
    sourceScarcityStreak[type] = 0;
    if ((sourceScarcityDepth[type] || 0) > 0) sourceScarcityDepth[type] = Math.max(0, (sourceScarcityDepth[type] || 0) - 1);
    return;
  }

  sourceScarcityStreak[type] = Number(sourceScarcityStreak[type] || 0) + 1;
  if (sourceScarcityStreak[type] >= 1) {
    sourceScarcityDepth[type] = Math.min(MAX_EXPANSION_DEPTH, Number(sourceScarcityDepth[type] || 0) + 1);
  }
  console.log(`[AUTO] Fonte ${type} marcada como escassa (${queuedCount}/${MIN_USEFUL_TARGETS}). Profundidade atual: ${sourceScarcityDepth[type] || 0}.`);
}

async function expandTargetsByFollowers(seedTargets, activeType, maxDepth, returnUrl) {
  const collected = [];
  let frontier = normalizeCollectedTargets(seedTargets).slice(0, MAX_EXPANSION_BASES);
  const seen = new Set(frontier);

  for (let depth = 1; depth <= maxDepth && frontier.length > 0 && collected.length < EXPANSION_TARGET_GOAL; depth += 1) {
    const nextFrontier = [];
    for (const username of frontier) {
      if (collected.length >= EXPANSION_TARGET_GOAL) break;
      const followers = await collectFollowersFromProfile(username, activeType, depth, returnUrl);
      for (const follower of followers) {
        if (seen.has(follower)) continue;
        seen.add(follower);
        collected.push(follower);
        nextFrontier.push(follower);
        if (collected.length >= EXPANSION_TARGET_GOAL) break;
      }
      await sleep(2500 + Math.floor(Math.random() * 2500));
      if (isRateLimitBackoffActive() || isSourceTypeCoolingDown(activeType)) break;
    }
    frontier = nextFrontier.slice(0, MAX_EXPANSION_BASES);
  }

  if (returnUrl) {
    const tab = collectTabId ? await chrome.tabs.get(collectTabId).catch(() => null) : null;
    if (tab) {
      await chrome.tabs.update(tab.id, { url: returnUrl }).catch(() => null);
      await waitForTabReady(tab.id, 8000).catch(() => null);
    }
  }

  if (collected.length) {
    console.log(`[AUTO] Expansao via seguidores gerou ${collected.length} alvos extras (${activeType}).`);
  }
  return collected;
}

async function collectFollowersFromProfile(username, activeType, depth, returnUrl) {
  const clean = String(username || '').replace('@', '').trim().toLowerCase();
  if (!isCandidateTarget(clean) || isOwnAccount(clean)) return [];

  const url = `${INSTAGRAM_BASE}/${encodeURIComponent(clean)}/`;
  try {
    let tab = collectTabId ? await chrome.tabs.get(collectTabId).catch(() => null) : null;
    if (!tab) {
      tab = await getOrCreateInstagramTab(url, false, false, automationTabId);
      collectTabId = tab.id;
    } else {
      await chrome.tabs.update(tab.id, { url });
      await waitForTabReady(tab.id, 10000);
    }

    await sleep(2500 + Math.floor(Math.random() * 1800));
    await dismissInstagramBlockingDialogsInTab(tab.id);

    if (await isInstagramRateLimited(tab.id)) {
      await skipSourceTypeForNow(activeType, `instagram_429_expansion_depth_${depth}`);
      return [];
    }

    const dialogResult = await openCompetitorFollowersDialogInTab(tab.id);
    if (!dialogResult.ok) {
      console.log(`[AUTO] Expansao nivel ${depth}: nao abriu seguidores de @${clean} (${dialogResult.reason}).`);
      return [];
    }

    const result = await sendToInstagramTab(tab.id, {
      type: 'COLLECT_TARGETS',
      settings: currentSettings,
      activeType: 'competitor',
      sourceAccount: clean,
      processed: [...processedToday, ...targetQueue],
      exhausted: []
    });

    if (result?.rateLimited) {
      await skipSourceTypeForNow(activeType, `instagram_429_expansion_collect_depth_${depth}`);
      return [];
    }

    const followers = normalizeCollectedTargets(result?.targets || []).slice(0, EXPANSION_FOLLOWERS_PER_PROFILE);
    console.log(`[AUTO] Expansao nivel ${depth}: @${clean} gerou ${followers.length} seguidores.`);
    return followers;
  } catch (err) {
    console.warn(`[AUTO] Falha na expansao de @${clean}:`, err.message || err);
    return [];
  }
}

// Comentario interno corrigido.
async function collectAuthorsFromPostPages(tab, postLinks, returnUrl, activeType) {
  const authors = [];
  const visited = new Set();

  for (const href of postLinks) {
    const postUrl = normalizeInstagramUrl(href);
    if (!postUrl || visited.has(postUrl)) continue;
    visited.add(postUrl);

    try {
      console.log(`[AUTO] Abrindo post para coletar autor: ${postUrl}`);
      await chrome.tabs.update(tab.id, { url: postUrl });
      await waitForTabReady(tab.id, 12000);
      await sleep(2000 + Math.floor(Math.random() * 1200));

      if (await isInstagramRateLimited(tab.id)) {
        await skipSourceTypeForNow(activeType, 'instagram_429_on_post');
        break;
      }

      const result = await sendToInstagramTab(tab.id, {
        type: 'EXTRACT_POST_AUTHOR',
        settings: currentSettings,
        processed: [...processedToday, ...authors]
      });

      if (result?.rateLimited) {
        await skipSourceTypeForNow(activeType, 'instagram_429_extract_post_author');
        break;
      }

      const author = String(result?.author || '').replace('@', '').trim();
      if (isCandidateTarget(author) && !processedToday.has(author) && !isOwnAccount(author)) {
        authors.push(author);
        console.log(`[AUTO] Autor coletado: @${author}`);
      } else {
        console.log(`[AUTO] Autor não aproveitado (${result?.source || 'sem fonte'}): ${author || 'vazio'}`);
      }

      if (authors.length >= MAX_POST_AUTHOR_FALLBACKS) break;
      await sleep(POST_AUTHOR_DELAY_MS + Math.floor(Math.random() * 4000) - 2000);
    } catch (err) {
      console.warn('[AUTO] Falha ao coletar autor do post:', err.message || err);
    }
  }

  if (returnUrl) {
    await chrome.tabs.update(tab.id, { url: returnUrl }).catch(() => null);
    await waitForTabReady(tab.id, 8000).catch(() => null);
  }

  return authors;
}

function isRateLimitBackoffActive() {
  return Number(rateLimitUntil || 0) > Date.now();
}

async function enterRateLimitBackoff(reason) {
  if (isRateLimitBackoffActive()) {
    const remaining = rateLimitUntil - Date.now();
    console.warn(`[AUTO] Bloqueio temporario já ativo. Nova tentativa em ${Math.ceil(remaining / 60000)} min.`);
    await createNextAlarm(remaining);
    await saveAutomationState();
    return;
  }

  // Backoff progressivo: 10, 20, 40, 60... min, com teto em 60. Um 429 logo
  // apos o cooldown normalmente significa que o bloqueio dura mais que isso;
  // repetir sempre o mesmo intervalo so faz a Extensão bater 429 de novo.
  consecutiveRateLimitHits += 1;
  const waitMinutes = Math.min(10 * Math.pow(2, consecutiveRateLimitHits - 1), 60);
  const backoffMs = waitMinutes * 60 * 1000;
  rateLimitUntil = Date.now() + backoffMs;
  targetQueue = [];
  targetSourceMap = {};
  console.warn(`[AUTO] Instagram bloqueou temporariamente (${reason}). Tentativa ${consecutiveRateLimitHits}, pausando ${waitMinutes} min.`);
  await reportAction('follow', 'instagram_429', 'skipped', reason);
  await createNextAlarm(backoffMs);
  await saveAutomationState();
}

async function isInstagramRateLimited(tabId) {
  try {
    const tab = await chrome.tabs.get(tabId).catch(() => null);
    const tabText = `${tab?.title || ''} ${tab?.url || ''}`.toLowerCase();
    if (tabText.includes('429') || tabText.includes('too many requests')) return true;

    const [{ result }] = await chrome.scripting.executeScript({
      target: { tabId },
      func: () => {
        const text = `${document.title || ''}\n${document.body?.innerText || ''}`.toLowerCase();
        return text.includes('http error 429') ||
          text.includes('error 429') ||
          text.includes('too many requests') ||
          text.includes('muitas solicitacoes');
      }
    }).catch(() => [{ result: false }]);

    return !!result;
  } catch {
    return false;
  }
}

function normalizeCollectedTargets(targets) {
  const seen = new Set();
  return (Array.isArray(targets) ? targets : [])
    .map((target) => String(target || '').replace('@', '').trim().toLowerCase())
    .filter((target) => {
      if (seen.has(target)) return false;
      seen.add(target);
      return isCandidateTarget(target) && !processedToday.has(target) && !isOwnAccount(target);
    });
}

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

async function reportNoTargets(reason) {
  const now = Date.now();
  if (now - lastNoTargetReportAt < 5 * 60 * 1000) return;
  lastNoTargetReportAt = now;
  await reportAction('follow', 'collection', 'skipped', reason);
}

function getActiveSegType(settings) {
  const types = getSegTypes(settings);
  if (types.length === 0) return '';
  return types[segTypeIndex % types.length];
}

// Comentario interno corrigido.
function advanceSegTypeIndex(settings) {
  const types = getSegTypes(settings);
  if (types.length > 1) {
    segTypeIndex = (segTypeIndex + 1) % types.length;
  }
}

// Comentario interno corrigido.
function getSegTypes(settings) {
  // Suporte ao novo campo seg_types (array) e ao antigo seg_type (string)
  let types;
  if (Array.isArray(settings.seg_types) && settings.seg_types.length > 0) {
    types = settings.seg_types;
  } else if (settings.seg_type) {
    types = [settings.seg_type];
  } else {
    types = ['hashtag'];
  }
  const available = types.filter((t) => !isSourceTypeCoolingDown(t));
  return available;
}

// Marca uma fonte especifica (hashtag/competitor/location/explore) como "em
// espera" por um tempo, em vez de travar a Extensão inteira. Um 429 na busca
// de alvos (pagina de busca, dialogo de seguidores) nao significa que a conta
// esta bloqueada - so que aquele padrao de acesso especifico foi sinalizado
// agora. Enquanto isso, as outras fontes configuradas continuam rodando.
function setSourceTypeCooldown(type, minutes = 20) {
  if (!type) return;
  sourceTypeCooldownUntil[type] = Date.now() + minutes * 60 * 1000;
}

function isSourceTypeCoolingDown(type) {
  const until = Number(sourceTypeCooldownUntil[type] || 0);
  return until > Date.now();
}

function isCompetitorCoolingDown(username) {
  if (!username) return false;
  if (serverRestrictedCompetitors.has(username)) return true;
  const until = Number(competitorCooldownUntil[username] || 0);
  return until > Date.now();
}

async function syncTodayCountsFromServer(force = false) {
  if (!force && Date.now() - lastDailyCountSyncAt < 10 * 60 * 1000) return;
  if (!currentToken || !currentAccountId) return;
  lastDailyCountSyncAt = Date.now();
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/summary`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (!Array.isArray(data.today)) return;
    let changed = false;
    for (const row of data.today) {
      const type = row.type;
      const serverTotal = Number(row.total) || 0;
      if (['follow', 'unfollow', 'like', 'comment'].includes(type) && serverTotal > (actionCountToday[type] || 0)) {
        actionCountToday[type] = serverTotal;
        changed = true;
      }
    }
    if (changed) {
      console.log(`[AUTO] Contagem do dia sincronizada com o servidor (inclui ações feitas por outro dispositivo nessa conta hoje).`);
      await saveAutomationState();
    }
  } catch {
    // Falha silenciosa - tenta de novo no proximo ciclo (throttle acima).
  }
}


async function syncRestrictedCompetitorsFromServer(force = false) {
  if (!currentToken || !currentAccountId) return;
  if (!force && Date.now() - lastRestrictedSyncAt < 10 * 60 * 1000) return;
  lastRestrictedSyncAt = Date.now();
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/competitors/restricted`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (Array.isArray(data.restricted)) {
      serverRestrictedCompetitors = new Set(data.restricted);
    }
  } catch {
    // falha de rede aqui nao deve travar a coleta - mantem o cache atual.
  }
}

async function registerCompetitorFailure(username, reason) {
  if (!username) return;
  const count = Number(competitorFailureCounts[username] || 0) + 1;
  if (count >= 3) {
    competitorCooldownUntil[username] = Date.now() + 20 * 60 * 1000;
    delete competitorFailureCounts[username];
    console.warn(`[AUTO] Concorrente @${username} falhou ${count}x seguidas (${reason}) - em espera 20min.`);
  } else {
    competitorFailureCounts[username] = count;
  }
  await saveAutomationState();
}

async function registerCompetitorSuccess(username) {
  if (!username) return;
  if (competitorFailureCounts[username]) {
    delete competitorFailureCounts[username];
    await saveAutomationState();
  }
}

async function registerCompetitorRestricted(username) {
  // v5.9.7: mesma logica do agent.ps1 - a Instagram confirmou de forma
  // explicita ("Somente @username pode ver todos os seguidores") que essa
  // conta restringe a lista de seguidores pra qualquer visitante. Nao e
  // falha temporaria, poe em espera bem mais longa (30 dias) direto, e
  // avisa o servidor - assim o Agent Windows dessa mesma conta ja nasce
  // sabendo, sem precisar redescobrir sozinho, e o dashboard mostra pro cliente.
  if (!username) return;
  competitorCooldownUntil[username] = Date.now() + 30 * 24 * 60 * 60 * 1000;
  serverRestrictedCompetitors.add(username);
  if (competitorFailureCounts[username]) delete competitorFailureCounts[username];
  console.warn(`[AUTO] Concorrente @${username} restringe a lista de seguidores (confirmado pela propria Instagram) - em espera 30 dias.`);
  await saveAutomationState();
  if (currentToken && currentAccountId) {
    try {
      await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/competitors/restricted`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${currentToken}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ competitor_username: username, detected_by: 'extension' })
      });
    } catch {
      console.log(`[AUTO] Nao consegui avisar o servidor sobre @${username} restrito (sem conexao?) - segue valendo localmente.`);
    }
  }
}

async function skipSourceTypeForNow(type, reason, minutes = 20) {
  setSourceTypeCooldown(type, minutes);
  console.warn(`[AUTO] Fonte "${type}" em espera por ${minutes} min (${reason}). Tentando outra fonte configurada.`);
  targetQueue = [];
  targetSourceMap = {};
  advanceSegTypeIndex(currentSettings);
  await reportNoTargets(`${reason}_${type}`);
  await saveAutomationState();
}

// Comentario interno corrigido.
function targetCollectionUrl(settings) {
  const activeType = getActiveSegType(settings);
  const hashtags = Array.isArray(settings.hashtags) ? settings.hashtags : [];
  const competitors = Array.isArray(settings.competitors) ? settings.competitors : [];
  const locations = Array.isArray(settings.locations) ? settings.locations : [];

  if (!activeType) return '';

  if (activeType === 'hashtag' && hashtags.length > 0) {
    const tag = sanitizeHashtag(pickSourceItem('hashtag', hashtags, settings));
    if (tag) {
      // Nova URL do Instagram para busca de hashtags (2025+)
      return `${INSTAGRAM_BASE}/explore/search/keyword/?q=%23${encodeURIComponent(tag)}`;
    }
  }

  if (activeType === 'competitor' && competitors.length > 0) {
    // v5.4.1: tira da selecao os concorrentes que ja falharam 3x seguidas
    // recentemente (isCompetitorCoolingDown) antes de escolher - assim um
    // concorrente com problema especifico (perfil privado, etc) nao volta a
    // ser sorteado toda hora e trava a coleta nele.
    const eligibleCompetitors = normalizeList(competitors).filter((c) => !isCompetitorCoolingDown(c));
    const competitor = pickSourceItem('competitor', eligibleCompetitors, settings).replace('@', '').trim();
    // v5.4.1: antes navegava direto pra /username/followers/ (deep link) -
    // padrao que usuario real nunca faz (sempre chega la clicando no
    // numero de seguidores de dentro do perfil ja carregado). O clique de
    // verdade agora acontece em background.js via chrome.scripting.executeScript
    // (openCompetitorFollowersDialogInTab), sem tocar no content.js.
    if (competitor) return `${INSTAGRAM_BASE}/${encodeURIComponent(competitor)}/`;
    return '';
  }

  if (activeType === 'location' && locations.length > 0) {
    const place = pickSourceItem('location', locations, settings).trim();
    if (place) return `${INSTAGRAM_BASE}/explore/search/keyword/?q=${encodeURIComponent(place)}`;
  }

  if (activeType === 'explore') {
    return `${INSTAGRAM_BASE}/explore/`;
  }

  return '';
}

function pickSourceItem(kind, list, settings) {
  const clean = normalizeList(list);
  if (!clean.length) return '';
  if ((settings?.source_pick_mode || 'random') !== 'list_order') return randomFrom(clean);

  const key = kind === 'competitor' ? 'competitor' : (kind === 'location' ? 'location' : 'hashtag');
  const current = Number(sourcePickIndex[key] || 0);
  const value = clean[current % clean.length];
  sourcePickIndex[key] = (current + 1) % clean.length;
  return value;
}

function actionCacheFor(action) {
  if (action === 'follow') return followedToday;
  if (action === 'like') return likedToday;
  if (action === 'comment') return commentedToday;
  return processedToday; // fallback
}

function dequeueTarget(action) {
  const cache = actionCacheFor(action);
  while (targetQueue.length > 0) {
    const target = targetQueue.shift();
    // Verifica cache do dia
    if (!isCandidateTarget(target) || isOwnAccount(target) || cache.has(target)) continue;
    // Comentario interno corrigido.
    if (action === 'follow'  && alreadyFollowed.has(target))  continue;
    if (action === 'like'    && alreadyLiked.has(target))     continue;
    if (action === 'comment' && alreadyCommented.has(target)) continue;
    // v5.8.14: perfil ja confirmado privado - like/comment nunca funcionam
    // ali (nao da pra ver o post). Follow continua elegivel (pode mandar
    // solicitacao de seguir).
    if ((action === 'like' || action === 'comment') && privateAccounts.has(target)) continue;
    cache.add(target);
    processedToday.add(target);
    return target;
  }
  return null;
}

async function getOrCreateInstagramTab(url, active = false, currentWindow = false, excludeTabId = null) {
  const tabs = await getInstagramTabs(currentWindow ? { currentWindow: true } : {}, excludeTabId);
  let tab = tabs[0] || null;

  if (!tab) tab = await chrome.tabs.create({ url, active });
  else if (url) tab = await chrome.tabs.update(tab.id, { url });

  await waitForTabReady(tab.id, 9000);
  return tab;
}

async function getInstagramTabs(options = {}, excludeTabId = null) {
  const groups = await Promise.all(
    INSTAGRAM_MATCHES.map((url) => chrome.tabs.query({ ...options, url }))
  );

  const seen = new Set();
  return groups.flat().filter((tab) => {
    if (!tab?.id || seen.has(tab.id)) return false;
    if (excludeTabId && tab.id === excludeTabId) return false; // Comentario interno corrigido.
    seen.add(tab.id);
    return true;
  });
}

async function waitForTabReady(tabId, timeout = 9000) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const tab = await chrome.tabs.get(tabId).catch(() => null);
    if (tab?.status === 'complete') return tab;
    await sleep(500);
  }
  return null;
}

async function sendToInstagramTab(tabId, message) {
  try {
    return await chrome.tabs.sendMessage(tabId, message);
  } catch {
    await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] });
    await sleep(1000);
    return await chrome.tabs.sendMessage(tabId, message);
  }
}

// Comentario interno corrigido.
// Comentario interno corrigido.
async function loadFollowedTargets() {
  if (alreadyFollowedLoaded || !currentToken || !currentAccountId) return;
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/followed-targets`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (Array.isArray(data.targets)) {
      data.targets.forEach(t => alreadyFollowed.add(t));
      alreadyFollowedLoaded = true;
      console.log(`[AUTO] Cache de seguidos carregado: ${alreadyFollowed.size} perfis`);
    }
  } catch {
    console.log('[AUTO] Cache de seguidos: usando apenas memoria local');
  }
}

// Comentario interno corrigido.
async function loadLikedTargets() {
  if (alreadyLikedLoaded || !currentToken || !currentAccountId) return;
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/liked-targets`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (Array.isArray(data.targets)) {
      data.targets.forEach(t => alreadyLiked.add(t));
      alreadyLikedLoaded = true;
      console.log(`[AUTO] Cache de curtidos carregado: ${alreadyLiked.size} perfis (${data.days} dias)`);
    }
  } catch {
    console.log('[AUTO] Cache de curtidos: usando apenas memoria local');
  }
}

// Comentario interno corrigido.
async function loadCommentedTargets() {
  if (alreadyCommentedLoaded || !currentToken || !currentAccountId) return;
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/commented-targets`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (Array.isArray(data.targets)) {
      data.targets.forEach(t => alreadyCommented.add(t));
      alreadyCommentedLoaded = true;
      console.log(`[AUTO] Cache de comentados carregado: ${alreadyCommented.size} perfis (${data.days} dias)`);
    }
  } catch {
    console.log('[AUTO] Cache de comentados: usando apenas memoria local');
  }
}

// Carrega do backend os perfis para fazer unfollow.
// v5.9.14: antes so carregava 1x por sessao (unfollowTargetsLoaded=true pra
// sempre depois da primeira carga com resultado) - uma vez que a fila
// inicial fosse toda processada, nunca mais recarregava, mesmo que mais
// gente fosse completando o prazo configurado ao longo do dia. Agora
// recarrega a cada 10min (mesmo padrao usado pra sync de concorrentes
// restritos), garantindo que perfis que atingem o prazo depois tambem
// entrem na fila sem precisar reiniciar a extensao.
async function loadUnfollowTargets(force = false) {
  if (!currentToken || !currentAccountId) return;
  if (!force && Date.now() - lastUnfollowLoadAt < 10 * 60 * 1000) return;
  const limit = getDailyLimit(currentSettings, 'unfollow');
  if (!limit || limit <= 0) return;
  const days = Math.min(Math.max(Number(currentSettings?.unfollow_after_days) || 7, 5), 60);
  lastUnfollowLoadAt = Date.now();
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/unfollow-targets?days=${days}`, {
      headers: { Authorization: `Bearer ${currentToken}` }
    });
    if (!res.ok) return;
    const data = await res.json();
    if (Array.isArray(data.targets)) {
      unfollowQueue = data.targets;
      console.log(`[AUTO] Fila de unfollow carregada: ${unfollowQueue.length} perfis (prazo: ${days} dias)`);
      if (unfollowQueue.length === 0) {
        console.log('[AUTO] Fila de unfollow vazia - aguardando perfis atingirem o prazo configurado');
      }
    }
  } catch {
    console.log('[AUTO] Fila de unfollow: falha ao carregar do backend');
  }
}

async function reserveDailyAction(type, target) {
  if (!currentToken || !currentAccountId) {
    return { allowed: false, reason: 'quota_auth_missing' };
  }
  try {
    const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/reserve`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${currentToken}`
      },
      body: JSON.stringify({ type, target })
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      return { allowed: false, reason: data.reason || 'quota_reservation_failed', ...data };
    }
    return data;
  } catch (err) {
    console.error('[AUTO] Não foi possível reservar o limite global:', err.message || err);
    return { allowed: false, reason: 'quota_reservation_failed' };
  }
}

async function reportAction(type, target, status, reason, reservationId = '') {
  // Garantir que reason nunca seja undefined/null - sempre string
  const safeReason = String(reason || '');
  let reported = false;

  // A reserva exige confirmação HTTP. Assim a Extensao sabe se o servidor
  // consumiu ou liberou a cota antes de continuar.
  if (reservationId && currentToken && currentAccountId) {
    try {
      const res = await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/log`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${currentToken}`
        },
        body: JSON.stringify({
          type,
          target,
          status,
          reason: safeReason,
          reservation_id: reservationId
        })
      });
      reported = res.ok;
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        console.error('[AUTO] Servidor recusou o registro reservado:', data.reason || res.status);
      }
    } catch (err) {
      console.error('[AUTO] Erro ao confirmar ação reservada:', err.message || err);
    }
  } else if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ event: 'ACTION_DONE', type, target, status, reason: safeReason }));
    reported = true;
  }

  if (!reported && currentToken && currentAccountId) {
    try {
      await fetch(`${SERVER_URL}/api/reports/${currentAccountId}/log`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${currentToken}`
        },
        body: JSON.stringify({ type, target, status, reason: safeReason })
      });
      reported = true;
    } catch (err) {
      console.error('[AUTO] Erro ao registrar log via API:', err.message || err);
    }
  }

  notifyPopup({ type: 'ACTION_LOG', action: { type, target, status, reason: safeReason } });
  return reported;
}

function isWithinSchedule(start, end) {
  return isWithinScheduleAt(start, end, new Date());
}

function isWithinScheduleAt(start, end, now) {
  const nowMin = now.getHours() * 60 + now.getMinutes();
  const startMin = minutesFromTime(start, '00:00');
  const endMin = minutesFromTime(end, '23:59');

  if (startMin <= endMin) return nowMin >= startMin && nowMin <= endMin;
  return nowMin >= startMin || nowMin <= endMin;
}

function minutesFromTime(value, fallback) {
  const [hourRaw, minuteRaw] = String(value || fallback).split(':');
  const hour = Number(hourRaw);
  const minute = Number(minuteRaw);

  if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
    const [fallbackHour, fallbackMinute] = fallback.split(':').map(Number);
    return fallbackHour * 60 + fallbackMinute;
  }

  return Math.min(Math.max(hour, 0), 23) * 60 + Math.min(Math.max(minute, 0), 59);
}

function randomDelay(speed) {
  const ranges = {
    slow: [50000, 100000],
    normal: [25000, 55000],
    fast: [10000, 25000]
  };
  const [min, max] = ranges[speed] || ranges.normal;
  return Math.floor(Math.random() * (max - min)) + min;
}

function automationDelay(settings) {
  const speedDelay = randomDelay(settings?.speed);
  const limit = Math.max(totalDailyActionLimit(settings), 1);
  const windowMinutes = scheduleWindowMinutes(settings?.time_start, settings?.time_end);
  const baseDelay = (windowMinutes * 60 * 1000) / limit;
  const speedFactor = { slow: 1.2, normal: 1, fast: 0.75 }[settings?.speed] || 1;
  const jitter = 0.8 + Math.random() * 0.4;
  return Math.max(speedDelay, Math.floor(baseDelay * speedFactor * jitter));
}

function getEnabledActions(settings) {
  const actions = Array.isArray(settings?.actions_enabled)
    ? settings.actions_enabled
    : ['follow'];
  return actions.filter(action => {
    if (!['follow', 'like', 'comment'].includes(action)) return false;
    if (getDailyLimit(settings, action) <= 0) return false;
    if ((actionBlockedUntil[action] || 0) > Date.now()) return false;
    return true;
  });
}

function getDailyLimit(settings, action) {
  if (action === 'follow') return Number(settings?.follows_per_day) || 0;
  if (action === 'like') return Number(settings?.likes_per_day) || 0;
  if (action === 'comment') return Number(settings?.comments_per_day) || 0;
  if (action === 'unfollow') return Number(settings?.unfollows_per_day) || 0;
  return 0;
}

function totalDailyActionLimit(settings) {
  return getEnabledActions(settings).reduce((sum, action) => sum + getDailyLimit(settings, action), 0)
    + getDailyLimit(settings, 'unfollow');
}

function allDailyLimitsReached(settings) {
  const actions = getEnabledActions(settings);
  const normalDone = actions.length
    ? actions.every(action => actionCountToday[action] >= getDailyLimit(settings, action))
    : true;
  const unfollowLimit = getDailyLimit(settings, 'unfollow');
  const unfollowDone = unfollowLimit <= 0 || actionCountToday.unfollow >= unfollowLimit;
  return normalDone && unfollowDone;
}

function scheduleWindowMinutes(start, end) {
  const startMin = minutesFromTime(start, '08:00');
  const endMin = minutesFromTime(end, '22:00');
  if (startMin === endMin) return 24 * 60;
  if (startMin < endMin) return Math.max(endMin - startMin, 60);
  return Math.max((24 * 60 - startMin) + endMin, 60);
}

// v5.9.20: pedido do Marcelo - antes, fora do horario configurado, o
// sistema checava de novo a cada 60s a noite inteira (uma linha de log a
// cada minuto, ate 8h+ seguidas). Agora calcula direto quantos minutos
// faltam pro horario abrir e dorme esse tempo de uma vez (com um teto de
// 15min, pra continuar razoavelmente responsivo se alguem mudar o horario
// pelo painel durante a espera).
function minutesUntilScheduleOpens(start, end, now = new Date()) {
  const nowMin = now.getHours() * 60 + now.getMinutes();
  const startMin = minutesFromTime(start, '00:00');
  const endMin = minutesFromTime(end, '23:59');

  if (startMin <= endMin) {
    if (nowMin < startMin) return startMin - nowMin;
    return (24 * 60 - nowMin) + startMin;
  }
  return Math.max(startMin - nowMin, 1);
}

function normalizeSettings(settings) {
  const normalized = { ...(settings || {}) };
  normalized.hashtags = normalizeList(normalized.hashtags);
  normalized.competitors = normalizeList(normalized.competitors);
  normalized.locations = normalizeList(normalized.locations);
  normalized.vip_list = normalizeList(normalized.vip_list);
  normalized.comment_templates = normalizeList(normalized.comment_templates).slice(0, 3);
  normalized.actions_enabled = Array.isArray(normalized.actions_enabled)
    ? normalized.actions_enabled.filter(action => ['follow', 'like', 'comment'].includes(action))
    : ['follow'];
  normalized.action_mode = normalized.action_mode === 'all_per_contact' ? 'all_per_contact' : 'one_per_contact';
  normalized.action_distribution_mode = normalized.action_distribution_mode === 'sequential' ? 'sequential' : 'balanced';
  normalized.time_start = normalized.time_start || normalized.timeStart || '08:00';
  normalized.time_end = normalized.time_end || normalized.timeEnd || '22:00';
  normalized.follows_per_day = Number(normalized.follows_per_day || normalized.followsPerDay || 80);
  normalized.likes_per_day = Number(normalized.likes_per_day || normalized.likesPerDay || 0);
  normalized.comments_per_day = Number(normalized.comments_per_day || normalized.commentsPerDay || 0);
  normalized.source_pick_mode = normalized.source_pick_mode === 'list_order' ? 'list_order' : 'random';
  normalized.speed = normalized.speed || 'normal';
  // Suporte a seg_types (novo) e seg_type (legado)
  if (!normalized.seg_types || normalized.seg_types.length === 0) {
    if (normalized.seg_type) normalized.seg_types = [normalized.seg_type];
    else normalized.seg_types = ['hashtag'];
  }
  normalized.seg_type = normalized.seg_types[0] || 'hashtag';
  // Comentario interno corrigido.
  normalized.account_username = normalized.account_username || currentAccountUsername || '';
  return normalized;
}

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

function isOwnAccount(username) {
  const own = String(currentAccountUsername || currentSettings?.account_username || '')
    .replace('@', '')
    .trim()
    .toLowerCase();
  return !!own && String(username || '').trim().toLowerCase() === own;
}

function isCandidateTarget(username) {
  const clean = String(username || '').trim();
  if (!/^[a-zA-Z0-9._]{2,30}$/.test(clean)) return false;
  if (clean.includes('..') || clean.startsWith('.') || clean.endsWith('.')) return false;
  // Comentario interno corrigido.
  if (/\.[a-z]{2,}$/.test(clean)) return false;
  return !isSystemPage(clean);
}

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'
  ];
  return system.includes(String(page || '').toLowerCase());
}

function normalizeList(value) {
  if (Array.isArray(value)) return value;
  if (typeof value === 'string') {
    try {
      const parsed = JSON.parse(value);
      if (Array.isArray(parsed)) return parsed;
    } catch {}
    return value.split(',').map((item) => item.trim()).filter(Boolean);
  }
  return [];
}

function randomFrom(list) {
  return list[Math.floor(Math.random() * list.length)] || '';
}

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

function notifyPopup(msg) {
  chrome.runtime.sendMessage(msg).catch(() => {});
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  switch (msg.type) {
    case 'CONNECT_WS':
      connectWS(msg.token, msg.accountId);
      break;
    case 'DISCONNECT_WS':
      disconnectWS();
      break;
    case 'START_AUTOMATION':
      blockInstagramNotificationsPermission();
      currentSettings = normalizeSettings(msg.settings);
      currentAccountId = msg.accountId;
      if (msg.accountUsername && msg.accountUsername !== currentAccountUsername) {
        targetQueue = [];
        targetSourceMap = {};
        processedToday.clear();
        segTypeIndex = 0;
        actionCycleIndex = 0;
        sourcePickIndex = { hashtag: 0, competitor: 0, location: 0 };
      }
      currentAccountUsername = msg.accountUsername || currentAccountUsername;
      // Garantir account_username nas settings
      if (currentAccountUsername && currentSettings) {
        currentSettings.account_username = currentAccountUsername;
      }
      currentToken = msg.token;
      if (currentToken && currentAccountId) connectWS(currentToken, currentAccountId);
      startAutomation();
      break;
    case 'STOP_AUTOMATION':
      stopAutomation();
      break;
    case 'GET_STATUS':
      sendResponse({
        running,
        followsToday: actionCountToday.follow,
        likesToday: actionCountToday.like,
        commentsToday: actionCountToday.comment
      });
      break;
  }
  return true;
});

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === AUTOMATION_ALARM) runAutomationTick();
});

restoreAutomationState().then(() => {
  blockInstagramNotificationsPermission();
  if (running && currentSettings) scheduleNext(3000);
});





