let isScraping = false;
let activeScrapeRunId = 0;
let stopRequestedRunId = 0;
let statusOverlay = null;
let activeOptions = getDefaultOptions();
let collectedProfiles = [];
let collectedProfileKeys = new Set();

console.log('FB Comments Export: content script loaded');

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'START_SCRAPING') {
    if (isScraping) {
      sendResponse({ ok: false, reason: 'already_scraping' });
      return false;
    }

    activeOptions = normalizeOptions(request.options);
    isScraping = true;
    activeScrapeRunId += 1;
    stopRequestedRunId = 0;

    sendResponse({ ok: true, started: true });
    startScraping(activeOptions);
    return true;
  }

  if (request.action === 'STOP_SCRAPING') {
    stopRequestedRunId = activeScrapeRunId;
    isScraping = false;
    activeScrapeRunId += 1;
    updateStatus('Stopping...');
    sendResponse({ ok: true, stopped: true });
    return false;
  }
});

function getDefaultOptions() {
  return {
    filter: 'relevant',
    includeReplies: false,
    scanComments: true,
    scanLikes: false,
    autoPause: true,
    pauseEvery: 25,
    pauseMs: 7000,
    resolveMissingUid: false,
    useLocalResolver: true,
    localResolverUrl: 'http://127.0.0.1:8787',
    maxComments: 1000,
    language: 'en'
  };
}

function normalizeOptions(options = {}) {
  const defaults = getDefaultOptions();
  const maxComments = Number(options.maxComments || defaults.maxComments);

  return {
    filter: options.filter || defaults.filter,
    includeReplies: Boolean(options.includeReplies),
    scanComments: options.scanComments !== false,
    scanLikes: Boolean(options.scanLikes),
    autoPause: options.autoPause !== false,
    pauseEvery: Math.max(5, Math.min(500, Number(options.pauseEvery || defaults.pauseEvery))),
    pauseMs: Math.max(1000, Math.min(120000, Number(options.pauseMs || defaults.pauseMs))),
    resolveMissingUid: Boolean(options.resolveMissingUid),
    useLocalResolver: options.useLocalResolver !== false,
    localResolverUrl: options.localResolverUrl || defaults.localResolverUrl,
    maxComments: Math.max(1, Math.min(1000000, Number.isFinite(maxComments) ? maxComments : defaults.maxComments)),
    language: options.language === 'vi' ? 'vi' : 'en'
  };
}

async function startScraping(options) {
  const runId = activeScrapeRunId;
  collectedProfiles = [];
  collectedProfileKeys = new Set();

  updateStatus('Starting scraper...');

  if (options.scanComments && isReelsPage()) {
    const openedReelsComments = await openReelsCommentsPanel(options);
    if (!openedReelsComments) {
      notifyFinished(0, false);
      isScraping = false;
      return;
    }

    const commentsReady = await waitForReelsCommentsReady(15000);
    if (!commentsReady) {
      updateStatus('Reels comments did not finish loading. Try again after the panel is visible.', 0);
      notifyFinished(0, false);
      isScraping = false;
      return;
    }
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.scanComments) {
    await applyCommentFilter(options.filter);
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.scanComments) {
    updateStatus('Scrolling to load comments...');
    await autoScroll(options.maxComments, options);
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.scanComments) {
    updateStatus(options.includeReplies ? 'Expanding comments and replies...' : 'Expanding comments...');
    await expandAllComments(options);
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.scanComments) {
    updateStatus('Parsing comment authors...');
    addProfileRows(parseComments(options));
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.scanLikes && collectedProfiles.length < options.maxComments) {
    await scanReactionProfiles(options);
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (options.resolveMissingUid) {
    await resolveMissingProfileIds(options);
  }

  if (!isRunActive(runId)) {
    await finishScrape(runId, { partial: isStopRequestedForRun(runId), options });
    return;
  }

  if (collectedProfiles.length > 0) {
    const rows = collectedProfiles.slice(0, options.maxComments);
    updateStatus(`Exporting ${rows.length} users...`, rows.length);
    exportFiles(rows, options);
    notifyFinished(rows.length, false);
  } else {
    updateStatus('No users found. Open the post detail view and try again.', 0);
    notifyFinished(0, false);
  }

  isScraping = false;
}

function isRunActive(runId) {
  return isScraping && runId === activeScrapeRunId;
}

function isStopRequestedForRun(runId) {
  return stopRequestedRunId === runId;
}

async function finishScrape(runId, options = {}) {
  const partial = Boolean(options.partial);

  if (!partial) {
    updateStatus('Stopped.');
    return;
  }

  const finalOptions = options.options || activeOptions;

  if (finalOptions.scanComments) {
    addProfileRows(parseComments(finalOptions));
  }

  if (collectedProfiles.length > 0) {
    const rows = collectedProfiles.slice(0, finalOptions.maxComments);
    updateStatus(`Stopped. Exporting ${rows.length} users...`, rows.length);
    exportFiles(rows, finalOptions);
    notifyFinished(rows.length, true);
  } else {
    updateStatus('Stopped. No users found.', 0);
    notifyFinished(0, true);
  }

  isScraping = false;
}

async function applyCommentFilter(filter) {
  const phrases = getFilterPhrases(filter);

  if (phrases.length === 0) {
    return;
  }

  updateStatus('Trying to apply selected filter...');

  const controls = Array.from(document.querySelectorAll('button, [role="button"], a[role="link"], span'));
  const visibleControls = controls.filter(isVisibleElement);

  for (const control of visibleControls) {
    const text = normalizeText(control.innerText || control.textContent || '');
    if (!text) {
      continue;
    }

    const matches = phrases.some((phrase) => text.includes(phrase));
    if (!matches) {
      continue;
    }

    try {
      control.click();
      await sleep(900);
      return;
    } catch (error) {
      return;
    }
  }
}

function getFilterPhrases(filter) {
  const map = {
    relevant: ['most relevant', 'phu hop nhat', 'phu hop'],
    oldest: ['oldest', 'cu nhat'],
    newest: ['newest', 'new comments', 'moi nhat'],
    all: ['all comments', 'tat ca binh luan', 'tat ca']
  };

  return map[filter] || [];
}

async function autoScroll(maxComments, options) {
  const scrollElement = getScrollContainer();
  const step = 1300;
  const settleDelayMs = 850;
  let stableRounds = 0;
  let scrollRounds = 0;
  let previousScrollHeight = scrollElement.scrollHeight;
  let previousCommentCount = countCommentCandidates();

  while (isScraping) {
    if (previousCommentCount >= maxComments) {
      return;
    }

    const maxScrollTop = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
    const nextScrollTop = Math.min(scrollElement.scrollTop + step, maxScrollTop);

    scrollElement.scrollTop = nextScrollTop;
    if (scrollElement === document.scrollingElement || scrollElement === document.documentElement || scrollElement === document.body) {
      window.scrollTo(0, nextScrollTop);
    }

    await sleep(settleDelayMs);
    scrollRounds += 1;
    if (!isScraping) {
      return;
    }

    const currentScrollHeight = scrollElement.scrollHeight;
    const currentCommentCount = countCommentCandidates();
    const reachedBottom = scrollElement.scrollTop + scrollElement.clientHeight >= currentScrollHeight - 6;
    const noHeightChange = currentScrollHeight === previousScrollHeight;
    const noCommentChange = currentCommentCount === previousCommentCount;

    if (reachedBottom && noHeightChange && noCommentChange) {
      stableRounds += 1;
    } else {
      stableRounds = 0;
    }

    previousScrollHeight = currentScrollHeight;
    previousCommentCount = currentCommentCount;
    updateStatus(`Loading... found about ${currentCommentCount} comment blocks`, currentCommentCount);

    if (options.autoPause && scrollRounds % options.pauseEvery === 0) {
      updateStatus('Auto pause to reduce scan pressure...', collectedProfiles.length);
      await sleep(options.pauseMs);
    }

    if (stableRounds >= 8) {
      return;
    }
  }
}

function getScrollContainer() {
  if (isReelsPage()) {
    const reelsScrollElement = findReelsCommentScrollElement();
    if (reelsScrollElement) {
      return reelsScrollElement;
    }
  }

  const modalRoots = Array.from(document.querySelectorAll('div[role="dialog"], div[aria-modal="true"]'));

  for (const root of modalRoots) {
    const scrollable = findBestScrollableElement(root);
    if (scrollable) {
      return scrollable;
    }
  }

  const commentScope = document.querySelector('div[role="article"], div[data-type="transactional"]');

  if (commentScope) {
    let current = commentScope.parentElement;

    while (current && current !== document.body) {
      if (isScrollableElement(current)) {
        return current;
      }
      current = current.parentElement;
    }
  }

  return document.scrollingElement || document.documentElement || document.body;
}

function findBestScrollableElement(root) {
  if (!root) {
    return null;
  }

  if (isScrollableElement(root)) {
    return root;
  }

  let best = null;
  let bestScore = 0;
  const elements = root.querySelectorAll('*');

  for (const element of elements) {
    if (!isScrollableElement(element)) {
      continue;
    }

    let score = element.scrollHeight - element.clientHeight;
    const rect = element.getBoundingClientRect();

    if (element.getAttribute('role') === 'dialog' || element.getAttribute('aria-modal') === 'true') {
      score += 5000;
    }

    if (rect.height > 220 && rect.width > 220) {
      score += 1000;
    }

    if (score > bestScore) {
      best = element;
      bestScore = score;
    }
  }

  return best;
}

function isScrollableElement(element) {
  if (!element || element === document.body || element === document.documentElement) {
    return false;
  }

  const style = window.getComputedStyle(element);
  return /(auto|scroll)/i.test(style.overflowY) && element.scrollHeight > element.clientHeight + 20;
}

async function expandAllComments(options) {
  const keywords = [
    'view more comments',
    'view previous comments',
    'see more comments',
    'see more',
    'more comments',
    'xem them binh luan',
    'xem binh luan truoc',
    'xem them',
    'xem tat ca',
    'xem cac binh luan',
    'hien them binh luan'
  ];

  if (options.includeReplies) {
    keywords.push(
      'reply',
      'replies',
      'view replies',
      'view more replies',
      'phan hoi',
      'tra loi',
      'xem phan hoi',
      'xem them phan hoi',
      'xem cau tra loi'
    );
  }

  let pass = 0;
  let clickedInPass = true;
  let clickCount = 0;

  while (clickedInPass && isScraping && countCommentCandidates() < options.maxComments) {
    pass += 1;
    clickedInPass = false;

    const candidates = Array.from(document.querySelectorAll('button, [role="button"], a[role="link"]'));

    for (const element of candidates) {
      if (!isScraping || countCommentCandidates() >= options.maxComments) {
        break;
      }

      if (!isVisibleElement(element)) {
        continue;
      }

      const text = normalizeText(element.innerText || element.textContent || element.getAttribute('aria-label') || '');
      if (!text || !keywords.some((keyword) => text.includes(keyword))) {
        continue;
      }

      try {
        element.click();
        clickedInPass = true;
        clickCount += 1;
        await sleep(450);

        if (options.autoPause && clickCount % options.pauseEvery === 0) {
          updateStatus('Auto pause after expansion batch...', collectedProfiles.length);
          await sleep(options.pauseMs);
        }
      } catch (error) {
        // Facebook can detach nodes while rendering. Ignore and keep scanning.
      }
    }

    if (clickedInPass) {
      const currentCount = countCommentCandidates();
      updateStatus(`Expanding pass ${pass}. Found about ${currentCount} comment blocks`, collectedProfiles.length);
      await sleep(850);
    }

    if (pass >= 30) {
      return;
    }
  }
}

async function scanReactionProfiles(options) {
  updateStatus('Opening likes / reactions...', collectedProfiles.length);

  let dialog = getCurrentReactionSurface();
  if (dialog) {
    updateStatus('Using already-open reactions list...', collectedProfiles.length);
  }

  const sourceScope = getReactionSourceScope();
  if (!dialog) {
    const opened = await openReactionsDialog(sourceScope);
    if (!opened) {
      updateStatus('Could not open reactions list. Facebook may be hiding it for this post.', collectedProfiles.length);
      return;
    }

    await sleep(1200);
    dialog = getCurrentReactionSurface();
  }

  if (!dialog) {
    updateStatus('Reaction dialog was not found.', collectedProfiles.length);
    return;
  }

  const tabs = getReactionTabs(dialog);
  const allTab = tabs.find((tab) => isAllReactionTab(tab));
  if (allTab) {
    try {
      allTab.click();
      await sleep(900);
    } catch (error) {
      // If All is already active or detached, keep scanning the current view.
    }
  }

  await scanCurrentReactionTab(dialog, options, 'All reactions', 18);
}

async function scanCurrentReactionTab(dialog, options, label, maxStableRounds = 10) {
  const scrollElement = findBestReactionScrollElement(dialog);
  let stableRounds = 0;
  let previousCount = collectedProfiles.length;
  let previousScrollState = getScrollState(dialog);

  while (isScraping && collectedProfiles.length < options.maxComments) {
    addProfileRows(parseProfileRowsFromLinks(dialog));
    await clickReactionSeeMore(dialog);
    scrollReactionDialog(dialog, scrollElement);
    await sleep(900);

    const currentCount = collectedProfiles.length;
    const currentScrollState = getScrollState(dialog);

    if (currentCount === previousCount && currentScrollState === previousScrollState) {
      stableRounds += 1;
    } else {
      stableRounds = 0;
    }

    previousCount = currentCount;
    previousScrollState = currentScrollState;
    updateStatus(`Scanning ${label}... collected ${currentCount} users`, currentCount);

    if (options.autoPause && currentCount > 0 && currentCount % options.pauseEvery === 0) {
      updateStatus('Auto pause during reaction scan...', currentCount);
      await sleep(options.pauseMs);
    }

    if (stableRounds >= maxStableRounds) {
      return;
    }
  }
}

function findBestReactionScrollElement(dialog) {
  if (dialog === document || dialog === document.body || dialog === document.documentElement) {
    return document.scrollingElement || document.documentElement || document.body;
  }

  return findBestScrollableElement(dialog) || dialog;
}

function scrollReactionDialog(dialog, primaryScrollElement) {
  const scrollables = [
    primaryScrollElement,
    ...Array.from(dialog.querySelectorAll('*')).filter(isScrollableElement)
  ].filter(Boolean);

  const uniqueScrollables = Array.from(new Set(scrollables));

  for (const element of uniqueScrollables) {
    if (element === document || element === document.body || element === document.documentElement || element === document.scrollingElement) {
      const scroller = document.scrollingElement || document.documentElement || document.body;
      const nextTop = Math.min(scroller.scrollTop + 900, Math.max(0, scroller.scrollHeight - scroller.clientHeight));
      scroller.scrollTop = nextTop;
      window.scrollTo(0, nextTop);
      continue;
    }

    const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
    element.scrollTop = Math.min(element.scrollTop + 900, maxScrollTop);
    element.dispatchEvent(new Event('scroll', { bubbles: true }));
  }
}

function getScrollState(dialog) {
  if (dialog === document || dialog === document.body || dialog === document.documentElement) {
    const scroller = document.scrollingElement || document.documentElement || document.body;
    return `${scroller.scrollTop}/${scroller.scrollHeight}/${scroller.clientHeight}`;
  }

  return Array.from(dialog.querySelectorAll('*'))
    .filter(isScrollableElement)
    .map((element) => `${element.scrollTop}/${element.scrollHeight}/${element.clientHeight}`)
    .join('|');
}

async function clickReactionSeeMore(dialog) {
  const controls = Array.from(dialog.querySelectorAll('button, [role="button"], a[role="link"]'))
    .filter(isVisibleElement);

  for (const control of controls) {
    const text = normalizeText(control.innerText || control.textContent || control.getAttribute('aria-label') || '');
    if (!/(see more|show more|view more|xem them)/i.test(text)) {
      continue;
    }

    try {
      control.click();
      await sleep(500);
    } catch (error) {
      // Ignore detached controls.
    }
  }
}

function getReactionTabs(dialog) {
  const tabLabels = [
    'all',
    'tat ca',
    'like',
    'likes',
    'thich',
    'love',
    'care',
    'haha',
    'wow',
    'sad',
    'angry',
    'yeu thich',
    'thuong thuong',
    'buon',
    'phan no'
  ];

  return Array.from(dialog.querySelectorAll('[role="tab"], button, [role="button"], a[role="link"]'))
    .filter(isVisibleElement)
    .filter((element) => {
      const text = normalizeText(element.innerText || element.textContent || element.getAttribute('aria-label') || '');
      return text && text.length <= 40 && tabLabels.some((label) => text === label || text.startsWith(`${label} `));
    });
}

function isAllReactionTab(tab) {
  const text = normalizeText(tab.innerText || tab.textContent || tab.getAttribute('aria-label') || '');
  return text === 'all' || text === 'tat ca' || text.startsWith('all ') || text.startsWith('tat ca ');
}

async function openReactionsDialog(sourceScope) {
  const existingDialogs = new Set(getVisibleDialogs());
  const scope = sourceScope || document;
  const candidateElements = Array.from(scope.querySelectorAll('a[role="link"], a[href], button, [role="button"], span, span[aria-label], div[aria-label]'))
    .filter((element) => isVisibleElement(element) && !isInsideNestedArticle(element, scope) && !isInsideReelsCommentPanel(element));

  const seenTargets = new Set();
  const ranked = candidateElements
    .map((element) => {
      const target = getClickableReactionTarget(element, scope);
      return {
        element: target,
        score: Math.max(getReactionOpenScore(element), getReactionOpenScore(target))
      };
    })
    .filter((item) => {
      if (!item.element || seenTargets.has(item.element)) {
        return false;
      }
      seenTargets.add(item.element);
      return true;
    })
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score);

  for (const item of ranked.slice(0, 8)) {
    try {
      item.element.click();
      await sleep(1000);
      const dialog = getNewOrReactionDialog(existingDialogs);
      if (dialog) {
        return true;
      }
    } catch (error) {
      // Try the next likely control.
    }
  }

  return false;
}

function getClickableReactionTarget(element, scope) {
  if (!element) {
    return null;
  }

  if (element.matches?.('a[href], a[role="link"], button, [role="button"]')) {
    return element;
  }

  const target = element.closest?.('a[href], a[role="link"], button, [role="button"]');
  if (target && (scope === document || scope.contains(target))) {
    return target;
  }

  return element;
}

function isInsideReelsCommentPanel(element) {
  if (!isReelsPage()) {
    return false;
  }

  const rect = element.getBoundingClientRect();
  if (window.innerWidth <= 700) {
    const text = normalizeText(element.closest?.('main, section, aside, div')?.innerText || document.body.innerText || '');
    return /(binh luan|comments?)/i.test(text) && rect.top > 80;
  }

  return rect.left > window.innerWidth * 0.78 && rect.width > 180;
}

function getReactionSourceScope() {
  const dialogs = getVisibleDialogs().filter((dialog) => !looksLikeReactionDialog(dialog));
  if (dialogs.length > 0) {
    const topDialog = dialogs[dialogs.length - 1];
    return findBestReactionArticle(topDialog) || topDialog;
  }

  return findBestReactionArticle(document) || document;
}

function findBestReactionArticle(root) {
  const articles = Array.from(root.querySelectorAll('div[role="article"]'))
    .filter((article) => isVisibleElement(article) && !isInsideNestedArticle(article, root));

  if (root !== document) {
    const mainArticle = articles
      .filter((article) => {
        const rect = article.getBoundingClientRect();
        return rect.height >= 160 && rect.top < window.innerHeight * 0.9;
      })
      .sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0];

    if (mainArticle) {
      return mainArticle;
    }
  }

  const scoredArticles = articles
    .map((article) => ({
      article,
      score: getBestReactionScore(article) * 100000 + getViewportCenterScore(article)
    }))
    .filter((item) => item.score > getViewportCenterScore(item.article))
    .sort((a, b) => b.score - a.score);

  return scoredArticles[0]?.article || null;
}

function isInsideNestedArticle(element, scope) {
  if (!scope || scope === document || !(scope instanceof Element)) {
    return false;
  }

  const scopeIsArticle = scope.matches?.('div[role="article"]');

  if (element.matches?.('div[role="article"]')) {
    const parentArticle = element.parentElement?.closest('div[role="article"]');
    return Boolean(parentArticle && scope.contains(parentArticle));
  }

  const nearestArticle = element.closest?.('div[role="article"]');
  if (!nearestArticle) {
    return false;
  }

  if (scopeIsArticle) {
    return nearestArticle !== scope && scope.contains(nearestArticle);
  }

  const parentArticle = nearestArticle.parentElement?.closest('div[role="article"]');
  return Boolean(parentArticle && scope.contains(parentArticle));
}

function getBestReactionScore(scope) {
  const candidates = Array.from(scope.querySelectorAll('a[role="link"], a[href], button, [role="button"], span, span[aria-label], div[aria-label]'))
    .filter((element) => isVisibleElement(element) && !isInsideNestedArticle(element, scope))
    .map((element) => getReactionOpenScore(element));

  return Math.max(0, ...candidates);
}

function getViewportCenterScore(element) {
  const rect = element.getBoundingClientRect();
  const viewportCenterX = window.innerWidth / 2;
  const viewportCenterY = window.innerHeight / 2;
  const elementCenterX = rect.left + rect.width / 2;
  const elementCenterY = rect.top + rect.height / 2;
  const distance = Math.abs(elementCenterX - viewportCenterX) + Math.abs(elementCenterY - viewportCenterY);
  const visibleArea = Math.max(0, Math.min(rect.right, window.innerWidth) - Math.max(rect.left, 0)) *
    Math.max(0, Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0));

  return visibleArea - distance;
}

function getReactionOpenScore(element) {
  const value = cleanCell([
    element.getAttribute('aria-label'),
    element.getAttribute('title'),
    element.innerText,
    element.textContent
  ].filter(Boolean).join(' '));

  if (!value || value.length > 160) {
    return 0;
  }

  const text = normalizeText(value);
  const reactionCount = parseCompactNumber(text);
  const hasNumber = reactionCount > 0;
  const hasReactionWord = /(like|likes|reaction|reactions|reacted|thich|luot thich|cam xuc|bay to cam xuc|love|care|haha|wow|sad|angry|yeu thich|thuong thuong|buon|phan no)/i.test(text);
  const hasReactionHref = /reaction|ufi\/reaction|feedback\/reaction/i.test(element.getAttribute('href') || '');
  const isNumericReactionSummary = hasNumber && !hasReactionWord && looksLikeNumericReactionSummary(element, text);
  const isNamedReactionSummary = hasNumber && !hasReactionWord && looksLikeNamedReactionSummary(element, text);
  const isPlainAction = /^(like|thich|luot thich|love|care|haha|wow|sad|angry|yeu thich|thuong thuong|buon|phan no)$/i.test(text);
  const href = element.getAttribute('href') || '';

  if (isPlainAction || !hasNumber || (!hasReactionWord && !hasReactionHref && !isNumericReactionSummary && !isNamedReactionSummary)) {
    return 0;
  }

  let score = 1;
  score += isNumericReactionSummary || isNamedReactionSummary ? 9 : 7;
  score += Math.min(reactionCount, 1000000) / 1000;
  if (hasReactionHref || /reaction|ufi\/reaction|feedback\/reaction/i.test(href)) {
    score += 5;
  }
  if (element.closest('div[role="dialog"]')) {
    score -= 1;
  }

  return score;
}

function looksLikeNamedReactionSummary(element, text) {
  const hasOthersText = /(\band\s+\d+\s+others\b|\b\d+\s+others\b|\bva\s+\d+\s+nguoi khac\b|\b\d+\s+nguoi khac\b|\b\d+\s+members?\b|\b\d+\s+thanh vien\b|\bva nhung nguoi khac\b)/i.test(text);

  if (!hasOthersText) {
    return false;
  }

  if (/(comment|comments|reply|replies|share|shares|binh luan|tra loi|chia se)/i.test(text)) {
    return false;
  }

  return hasNearbyReactionIcon(element);
}

function looksLikeNumericReactionSummary(element, text) {
  if (!/^\d+(?:[.,]\d+)?\s*(k|m|b|nghin|ngan|trieu|ty|members?|thanh vien)?$/i.test(text)) {
    return false;
  }

  if (looksLikeDate(text) || /(comment|comments|reply|replies|share|shares|binh luan|tra loi|chia se)/i.test(text)) {
    return false;
  }

  const href = element.getAttribute('href') || '';
  if (/(comment_id|reply_comment_id|share|permalink\.php)/i.test(href)) {
    return false;
  }

  let current = element;
  for (let depth = 0; current && depth < 4; depth += 1) {
    const nearbyText = normalizeText(current.innerText || current.textContent || current.getAttribute?.('aria-label') || '');
    const staysSmall = nearbyText.length <= 120;

    if (hasNearbyReactionIcon(current) && staysSmall && !/(comment|comments|reply|replies|share|shares|binh luan|tra loi|chia se)/i.test(nearbyText)) {
      return true;
    }

    current = current.parentElement;
  }

  return false;
}

function hasNearbyReactionIcon(element) {
  let current = element;

  for (let depth = 0; current && depth < 4; depth += 1) {
    if (current.querySelector?.('img, i, svg, [style*="background-image"]')) {
      return true;
    }

    const reactionLabels = Array.from(current.querySelectorAll?.('[aria-label], [title]') || [])
      .map((item) => normalizeText(`${item.getAttribute('aria-label') || ''} ${item.getAttribute('title') || ''}`))
      .join(' ');

    if (/(like|love|care|haha|wow|sad|angry|thich|luot thich|yeu thich|thuong thuong|cam xuc|bay to cam xuc|buon|phan no)/i.test(reactionLabels)) {
      return true;
    }

    current = current.parentElement;
  }

  return false;
}

function getTopDialog() {
  const dialogs = getVisibleDialogs();

  return dialogs[dialogs.length - 1] || null;
}

function getTopReactionDialog() {
  const dialogs = getVisibleDialogs();

  for (let index = dialogs.length - 1; index >= 0; index -= 1) {
    if (looksLikeReactionDialog(dialogs[index])) {
      return dialogs[index];
    }
  }

  return null;
}

function getCurrentReactionSurface() {
  const dialog = getTopReactionDialog();
  if (dialog) {
    return dialog;
  }

  if (looksLikeReactionDialog(document.body || document.documentElement)) {
    return document;
  }

  return null;
}

function getVisibleDialogs() {
  return Array.from(document.querySelectorAll('div[role="dialog"], div[aria-modal="true"]'))
    .filter(isVisibleElement);
}

function getNewOrReactionDialog(existingDialogs) {
  const dialogs = getVisibleDialogs();

  for (let index = dialogs.length - 1; index >= 0; index -= 1) {
    const dialog = dialogs[index];
    if (!existingDialogs.has(dialog) && looksLikeReactionDialog(dialog)) {
      return dialog;
    }
  }

  const topDialog = dialogs[dialogs.length - 1] || null;
  return topDialog && looksLikeReactionDialog(topDialog) ? topDialog : null;
}

function looksLikeReactionDialog(dialog) {
  if (!dialog) {
    return false;
  }

  const text = normalizeText(dialog.innerText || dialog.textContent || '');
  const hasReactionHeading = /(all|like|likes|love|care|haha|wow|sad|angry|tat ca|thich|luot thich|cam xuc|bay to cam xuc|nguoi da bay to cam xuc|nguoi bay to cam xuc|people who reacted)/i.test(text);
  const hasProfileLinks = dialog.querySelectorAll('a[href*="facebook.com"], a[href^="/"]').length > 0;
  const hasCommentControls = !/nguoi da bay to cam xuc|nguoi bay to cam xuc|people who reacted/i.test(text) &&
    /(comment|reply|binh luan|tra loi|write a comment|viet binh luan)/i.test(text);

  return hasReactionHeading && hasProfileLinks && !hasCommentControls;
}

function parseProfileRowsFromLinks(root) {
  const rows = [];
  const seen = new Set();
  const links = Array.from(root.querySelectorAll('a[href]'));

  for (const link of links) {
    if (!isVisibleElement(link) || !isUsableProfileLink(link)) {
      continue;
    }

    const profile = extractProfileInfo(link, findReactionListRow(link) || link);
    if (!profile.userId && !profile.profileUrl) {
      continue;
    }

    const author = extractReactionAuthor(link);
    if (!author || author === 'Unknown' || looksLikeActionText(author)) {
      continue;
    }

    const key = profile.userId || profile.profileUrl || `${author}|${link.getAttribute('href') || ''}`;
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);

    rows.push({
      author,
      userId: profile.userId,
      profileUrl: profile.profileUrl
    });
  }

  return rows;
}

function isUsableProfileLink(link) {
  const href = link.getAttribute('href') || '';
  const profileUrl = normalizeProfileUrl(href);

  if (!profileUrl) {
    return false;
  }

  const slug = getProfileSlug(profileUrl);
  if (slug && isFacebookSystemPath(slug) && !isGroupUserProfileUrl(profileUrl)) {
    return false;
  }

  const label = cleanCell(link.innerText || link.textContent || link.getAttribute('aria-label') || '');
  return !looksLikeActionText(label) && !looksLikeReactionMetaText(label);
}

function extractReactionAuthor(link) {
  const directAuthor = extractAuthor(link);
  if (isValidReactionAuthor(directAuthor)) {
    return directAuthor;
  }

  const row = findReactionListRow(link);
  if (!row) {
    return 'Unknown';
  }

  const textCandidates = Array.from(row.querySelectorAll('span[dir="auto"], strong, a[role="link"], a[href]'))
    .map((element) => cleanCell(element.innerText || element.textContent || element.getAttribute('aria-label') || ''))
    .filter(isValidReactionAuthor);

  if (textCandidates.length > 0) {
    return textCandidates[0];
  }

  const lines = String(row.innerText || row.textContent || '')
    .split('\n')
    .map(cleanCell)
    .filter(isValidReactionAuthor);

  return lines[0] || 'Unknown';
}

function findReactionListRow(link) {
  let current = link.parentElement;
  let best = null;
  let depth = 0;

  while (current && depth < 8) {
    const rect = current.getBoundingClientRect();
    const text = cleanCell(current.innerText || current.textContent || '');
    const hasProfileLink = Boolean(current.querySelector('a[href]'));

    if (hasProfileLink && text && rect.height >= 24 && rect.height <= 180) {
      best = current;
    }

    current = current.parentElement;
    depth += 1;
  }

  return best;
}

function isValidReactionAuthor(value) {
  const text = cleanCell(value || '');
  const normalized = normalizeText(text);

  if (!text || text === 'Unknown' || text.length > 90) {
    return false;
  }

  if (/https?:\/\//i.test(text) || /^\d+([.,]\d+)?[kmb]?\s*$/i.test(text)) {
    return false;
  }

  return !looksLikeActionText(text) && !looksLikeReactionMetaText(normalized);
}

function looksLikeReactionMetaText(value) {
  const text = normalizeText(value);
  return /(mutual friend|mutual friends|ban chung|ban be chung|friend request|cancel request|add friend|message|follow|following|theo doi|huy loi moi|them ban be|ket ban|nhan tin|you can see|people who reacted|nguoi da bay to cam xuc|total number|tong so|reactions to this post)/i.test(text);
}

async function resolveMissingProfileIds(options) {
  const targets = collectedProfiles.filter((row) => row.profileUrl && !row.userId);

  if (targets.length === 0) {
    return;
  }

  updateStatus(`Resolving UID from ${targets.length} profile URLs...`, collectedProfiles.length);

  if (options.useLocalResolver) {
    const resolvedByServer = await resolveMissingProfileIdsWithLocalServer(targets, options);
    if (resolvedByServer) {
      return;
    }

    updateStatus('Local resolver unavailable. Falling back to browser resolver...', collectedProfiles.length);
  }

  let processed = 0;

  for (const row of targets) {
    if (!isScraping) {
      return;
    }

    if (processed >= options.maxComments) {
      return;
    }

    const resolved = await resolveProfileUrl(row.profileUrl);
    if (resolved.userId) {
      row.userId = resolved.userId;
      row.profileUrl = resolved.profileUrl || row.profileUrl;
    }

    processed += 1;
    updateStatus(`Resolving UID... ${processed}/${targets.length}`, collectedProfiles.length);
    await sleep(350);

    if (options.autoPause && processed % options.pauseEvery === 0) {
      updateStatus(`Auto pause while resolving UID... ${processed}/${targets.length}`, collectedProfiles.length);
      await sleep(options.pauseMs);
    }
  }
}

async function resolveMissingProfileIdsWithLocalServer(targets, options) {
  try {
    let processed = 0;

    for (const row of targets) {
      if (!isScraping) {
        return true;
      }

      const response = await chrome.runtime.sendMessage({
        action: 'RESOLVE_UID_BATCH',
        serverUrl: options.localResolverUrl,
        urls: [row.profileUrl],
        pauseMs: 0
      });

      if (!response?.ok) {
        return false;
      }

      const resolved = response.results?.[0];
      if (resolved?.userId) {
        row.userId = resolved.userId;
        row.profileUrl = resolved.profileUrl || row.profileUrl;
        if (resolved.author && !row.author) {
          row.author = resolved.author;
        }
      }

      processed += 1;
      updateStatus(`Local resolver... ${processed}/${targets.length}`, collectedProfiles.length);

      if (options.autoPause && processed % options.pauseEvery === 0) {
        updateStatus(`Auto pause while resolving UID... ${processed}/${targets.length}`, collectedProfiles.length);
        await sleep(options.pauseMs);
      } else {
        await sleep(250);
      }
    }

    return true;
  } catch (error) {
    return false;
  }
}

async function resolveProfileUrl(profileUrl) {
  const normalizedUrl = normalizeProfileUrl(profileUrl) || profileUrl;
  const directId = extractUserIdFromValue(normalizedUrl);

  if (directId) {
    return {
      userId: directId,
      profileUrl: `https://www.facebook.com/profile.php?id=${directId}`
    };
  }

  try {
    const response = await fetch(normalizedUrl, {
      credentials: 'include',
      cache: 'no-store'
    });

    if (!response.ok) {
      return { userId: '', profileUrl: normalizedUrl };
    }

    const html = await response.text();
    const userId = extractTargetUserIdFromHtml(html, normalizedUrl);

    return {
      userId,
      profileUrl: userId ? `https://www.facebook.com/profile.php?id=${userId}` : normalizedUrl
    };
  } catch (error) {
    return { userId: '', profileUrl: normalizedUrl };
  }
}

function parseComments(options) {
  const comments = [];
  const seen = new Set();

  if (isReelsPage()) {
    for (const row of parseReelComments(options)) {
      addComment(comments, seen, row);
    }
  }

  const articles = Array.from(document.querySelectorAll('div[role="article"]'));

  for (const article of articles) {
    if (comments.length >= options.maxComments) {
      break;
    }

    const row = parseDesktopComment(article);
    addComment(comments, seen, row);
  }

  if (comments.length === 0) {
    const transactionalDivs = Array.from(document.querySelectorAll('div[data-type="transactional"]'));

    for (const container of transactionalDivs) {
      if (comments.length >= options.maxComments) {
        break;
      }

      const row = parseMobileComment(container);
      addComment(comments, seen, row);
    }
  }

  return comments.slice(0, options.maxComments);
}

function parseReelComments(options) {
  const comments = [];
  const seenComments = new Set();
  const scope = getReelsCommentScope() || document;
  const authorLinks = Array.from(scope.querySelectorAll('a[href], a[role="link"]'))
    .filter((link) => isVisibleElement(link) && isUsableProfileLink(link));

  for (const authorLink of authorLinks) {
    if (comments.length >= options.maxComments) {
      break;
    }

    const container = findReelCommentContainer(authorLink, scope);
    if (!container) {
      continue;
    }

    const profile = extractProfileInfo(authorLink, container);
    const author = extractReelAuthor(authorLink, container);
    const text = extractReelCommentText(container, author);
    if (!text) {
      continue;
    }

    const key = `${profile.userId || profile.profileUrl || author}|${text}`;
    if (seenComments.has(key)) {
      continue;
    }
    seenComments.add(key);

    comments.push({
      author,
      userId: profile.userId,
      profileUrl: profile.profileUrl,
      text,
      date: extractReelCommentDate(container),
      likes: extractLikeCount(container),
      commentLink: '',
      postUrl: getPostUrl()
    });
  }

  return comments;
}

function findReelCommentContainer(authorLink, scope) {
  let current = authorLink.parentElement;
  let best = null;

  for (let depth = 0; current && current !== scope.parentElement && depth < 9; depth += 1) {
    const text = normalizeText(current.innerText || current.textContent || '');
    const rect = current.getBoundingClientRect();
    const hasCommentActions = /(like|reply|thich|tra loi|phan hoi|see original|xem ban goc|xem ban dich)/i.test(text);
    const plausibleSize = rect.height >= 28 && rect.height <= 260 && rect.width >= 120;

    if (plausibleSize && hasCommentActions) {
      best = current;
    }

    current = current.parentElement;
  }

  return best;
}

function extractReelAuthor(authorLink, container) {
  const directAuthor = extractAuthor(authorLink);
  if (directAuthor && directAuthor !== 'Unknown' && !looksLikeActionText(directAuthor)) {
    return directAuthor;
  }

  const lines = getVisibleTextLines(container);
  return lines.find((line) => isValidReactionAuthor(line)) || 'Unknown';
}

function extractReelCommentText(container, author) {
  const lines = getVisibleTextLines(container);
  const normalizedAuthor = normalizeText(author);

  for (const line of lines) {
    const normalized = normalizeText(line);
    if (!line || normalized === normalizedAuthor) {
      continue;
    }

    if (looksLikeReelCommentMeta(line)) {
      continue;
    }

    return line;
  }

  return '';
}

function extractReelCommentDate(container) {
  const lines = getVisibleTextLines(container);
  const dateLine = lines.find((line) => looksLikeDate(line));
  return dateLine || 'Unknown';
}

function getVisibleTextLines(container) {
  return String(container?.innerText || container?.textContent || '')
    .split('\n')
    .map(cleanCell)
    .filter(Boolean);
}

function looksLikeReelCommentMeta(value) {
  const text = normalizeText(value);

  if (!text) {
    return true;
  }

  if (looksLikeDate(text) || looksLikeActionText(text)) {
    return true;
  }

  return /^(follow|following|theo doi|dang theo doi|edited|da chinh sua|see original|see translation|original audio|xem ban goc|xem ban dich|am thanh goc|\d+|w|d|h|m)$/i.test(text);
}

async function openReelsCommentsPanel(options) {
  if (getReelsCommentScope()) {
    return true;
  }

  updateStatus('Opening Reels comments panel...', collectedProfiles.length);

  const controls = Array.from(document.querySelectorAll('button, [role="button"], a[role="link"], a[href], div[aria-label], span[aria-label]'))
    .filter((element) => isVisibleElement(element) && !isInsideReelsCommentPanel(element));

  const ranked = controls
    .map((element) => {
      const target = getClickableReactionTarget(element, document);
      return {
        element: target,
        score: getReelsCommentButtonScore(element, target)
      };
    })
    .filter((item, index, list) => item.element && item.score > 0 && list.findIndex((other) => other.element === item.element) === index)
    .sort((a, b) => b.score - a.score);

  for (const item of ranked.slice(0, 6)) {
    if (!isScraping) {
      return false;
    }

    try {
      item.element.click();
      await sleep(1200);

      if (getReelsCommentScope()) {
        return true;
      }

      if (options.autoPause) {
        await sleep(250);
      }
    } catch (error) {
      // Detached Reels controls are common while the video UI rerenders.
    }
  }

  const fallbackOpened = await clickReelsSecondSideAction();
  if (fallbackOpened) {
    return true;
  }

  updateStatus('Could not open Reels comments. Click the comment button once, then start again.', collectedProfiles.length);
  return false;
}

async function waitForReelsCommentsReady(timeoutMs = 8000) {
  const startedAt = Date.now();
  let lastCount = 0;

  while (isScraping && Date.now() - startedAt < timeoutMs) {
    const count = countReelsCommentAuthorLinks();
    if (count > 0) {
      updateStatus(`Reels comments loaded. Found about ${count} comment authors`, count);
      await sleep(500);
      return true;
    }

    lastCount = count;
    updateStatus('Waiting for Reels comments to load...', lastCount);
    await sleep(500);
  }

  return countReelsCommentAuthorLinks() > 0;
}

function countReelsCommentAuthorLinks() {
  const scope = getReelsCommentScope();
  if (!scope) {
    return 0;
  }

  return Array.from(scope.querySelectorAll('a[href], a[role="link"]'))
    .filter((link) => isVisibleElement(link) && isUsableProfileLink(link))
    .length;
}

function getReelsCommentButtonScore(element, target) {
  const value = cleanCell([
    element.getAttribute('aria-label'),
    element.getAttribute('title'),
    element.innerText,
    element.textContent,
    target?.getAttribute?.('aria-label'),
    target?.getAttribute?.('title'),
    target?.innerText,
    target?.textContent
  ].filter(Boolean).join(' '));
  const text = normalizeText(value);
  const rect = (target || element).getBoundingClientRect();

  if (!text || rect.left < window.innerWidth * 0.35 || rect.width > 180 || rect.height > 160) {
    return 0;
  }

  if (/(comment|comments|binh luan)/i.test(text)) {
    return 100 + parseCompactNumber(text);
  }

  return 0;
}

async function clickReelsSecondSideAction() {
  const targets = getReelsSideActionTargets();
  const target = targets.find((item, index) => index > 0 && !looksLikeReelsLikeAction(item));
  if (!target) {
    return false;
  }

  const clickTarget = getLooseClickableTarget(target);

  try {
    clickElementLikeUser(clickTarget);
    await sleep(1400);
    return Boolean(getReelsCommentScope());
  } catch (error) {
    return false;
  }
}

function looksLikeReelsLikeAction(element) {
  const text = normalizeText([
    element.getAttribute?.('aria-label'),
    element.getAttribute?.('title'),
    element.innerText,
    element.textContent
  ].filter(Boolean).join(' '));
  const rect = element.getBoundingClientRect();
  const targets = getReelsSideActionTargets();
  const firstTarget = targets[0];

  if (firstTarget === element) {
    return true;
  }

  if (/(^|\s)(like|thich|luot thich)(\s|$)/i.test(text) && !/(comment|binh luan)/i.test(text)) {
    return true;
  }

  if (firstTarget) {
    const firstRect = firstTarget.getBoundingClientRect();
    return Math.abs(firstRect.top - rect.top) < 24;
  }

  return false;
}

function getLooseClickableTarget(element) {
  let current = element;
  let best = element;

  for (let depth = 0; current && depth < 6; depth += 1) {
    const rect = current.getBoundingClientRect();
    const style = window.getComputedStyle(current);
    const clickable = current.matches?.('a[href], a[role="link"], button, [role="button"], [tabindex], [aria-label]') ||
      style.cursor === 'pointer';

    if (clickable && rect.width <= 220 && rect.height <= 220) {
      best = current;
    }

    current = current.parentElement;
  }

  return best;
}

function clickElementLikeUser(element) {
  const rect = element.getBoundingClientRect();
  const x = rect.left + rect.width / 2;
  const y = rect.top + rect.height / 2;
  const target = document.elementFromPoint(x, y) || element;

  for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
    target.dispatchEvent(new MouseEvent(type, {
      bubbles: true,
      cancelable: true,
      view: window,
      clientX: x,
      clientY: y
    }));
  }
}

function getReelsSideActionTargets() {
  const candidates = Array.from(document.querySelectorAll('button, [role="button"], a[role="link"], a[href], span, div'))
    .filter(isVisibleElement)
    .filter((element) => {
      if (isInsideReelsCommentPanel(element)) {
        return false;
      }

      const rect = element.getBoundingClientRect();
      const text = normalizeText(element.innerText || element.textContent || element.getAttribute('aria-label') || element.getAttribute('title') || '');
      const hasCount = parseCompactNumber(text) > 0;

      return hasCount &&
        rect.left > window.innerWidth * 0.45 &&
        rect.right < window.innerWidth * 0.98 &&
        rect.width <= 160 &&
        rect.height <= 150;
    })
    .map(getLooseClickableTarget)
    .filter(Boolean)
    .sort((a, b) => {
      const rectA = a.getBoundingClientRect();
      const rectB = b.getBoundingClientRect();
      return rectA.top - rectB.top;
    });

  const targets = [];
  for (const candidate of candidates) {
    const rect = candidate.getBoundingClientRect();
    const duplicate = targets.some((target) => {
      const targetRect = target.getBoundingClientRect();
      return Math.abs(targetRect.top - rect.top) < 18 && Math.abs(targetRect.left - rect.left) < 80;
    });

    if (!duplicate) {
      targets.push(candidate);
    }
  }

  return targets;
}

function closeDialog(dialog) {
  const closeButtons = Array.from(dialog.querySelectorAll('button, [role="button"], [aria-label]'))
    .filter(isVisibleElement)
    .filter((button) => {
      const text = normalizeText(button.getAttribute('aria-label') || button.getAttribute('title') || button.innerText || button.textContent || '');
      return /^(close|dong|cancel|huy)$/i.test(text) || text.includes('close') || text.includes('dong');
    });

  try {
    closeButtons[0]?.click();
  } catch (error) {
    // Ignore close failures; later candidates may still work.
  }
}

function isReelsPage() {
  try {
    return /^\/reel(s)?\//i.test(window.location.pathname);
  } catch (error) {
    return false;
  }
}

function getReelsCommentScope() {
  const visibleElements = Array.from(document.querySelectorAll('div, section, aside'))
    .filter(isVisibleElement)
    .map((element) => ({
      element,
      score: getReelsCommentScopeScore(element)
    }))
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score);

  return visibleElements[0]?.element || null;
}

function getReelsCommentScopeScore(element) {
  const rect = element.getBoundingClientRect();
  const mobileViewport = window.innerWidth <= 700;

  if (rect.width < 220 || rect.height < 260) {
    return 0;
  }

  if (!mobileViewport && rect.left < window.innerWidth * 0.55) {
    return 0;
  }

  const text = normalizeText(element.innerText || element.textContent || '');
  const hasPanelTitle = /(^|\s)(binh luan|comments?)(\s|$)/i.test(text);
  const hasCommentRows = /(reply|tra loi|phan hoi|see original|xem ban goc|xem ban dich)/i.test(text) &&
    Array.from(element.querySelectorAll('a[href]')).some((link) => isUsableProfileLink(link));

  if (!hasPanelTitle && !hasCommentRows) {
    return 0;
  }

  const profileLinks = Array.from(element.querySelectorAll('a[href]')).filter((link) => isUsableProfileLink(link)).length;
  const titleBonus = hasPanelTitle ? 50000 : 0;
  const mobileBonus = mobileViewport ? 25000 : 0;

  return titleBonus + mobileBonus + profileLinks * 10000 + Math.min(rect.height * rect.width, 1000000);
}

function findReelsCommentScrollElement() {
  const scope = getReelsCommentScope();
  if (!scope) {
    return null;
  }

  return findBestScrollableElement(scope) || (isScrollableElement(scope) ? scope : null);
}

function addComment(comments, seen, row) {
  if (!row || row.author === 'Unknown' || !row.text) {
    return;
  }

  const key = `${row.author}|${row.text}|${row.date}|${row.commentLink}`;
  if (seen.has(key)) {
    return;
  }

  seen.add(key);
  comments.push(row);
}

function addProfileRows(rows) {
  for (const row of rows || []) {
    addProfileRow(row);
  }

  updateStatus(`Collected ${collectedProfiles.length} users...`, collectedProfiles.length);
}

function addProfileRow(row) {
  if (!row || !row.author || row.author === 'Unknown') {
    return false;
  }

  const profile = {
    author: cleanCell(row.author),
    userId: cleanCell(row.userId || ''),
    profileUrl: cleanCell(row.profileUrl || ''),
    comment: cleanCell(row.comment || row.text || '')
  };

  if (!profile.userId && !profile.profileUrl) {
    return false;
  }

  const key = profile.userId || profile.profileUrl || profile.author;
  if (collectedProfileKeys.has(key)) {
    mergeProfileComment(key, profile.comment);
    return false;
  }

  collectedProfileKeys.add(key);
  collectedProfiles.push(profile);
  return true;
}

function mergeProfileComment(key, comment) {
  if (!comment) {
    return;
  }

  const existing = collectedProfiles.find((row) => (row.userId || row.profileUrl || row.author) === key);
  if (!existing) {
    return;
  }

  const currentComments = String(existing.comment || '')
    .split(' | ')
    .map((item) => item.trim())
    .filter(Boolean);

  if (!currentComments.includes(comment)) {
    currentComments.push(comment);
    existing.comment = currentComments.join(' | ');
  }
}

function parseDesktopComment(article) {
  const authorLink = findAuthorLink(article);
  const author = extractAuthor(authorLink);
  const profile = extractProfileInfo(authorLink, article);
  const text = extractCommentText(article, authorLink, author);
  const date = extractDate(article, authorLink);
  const likeCount = extractLikeCount(article);
  const commentLink = buildCommentLink(article);

  return {
    author,
    userId: profile.userId,
    profileUrl: profile.profileUrl,
    text,
    date,
    likes: likeCount,
    commentLink,
    postUrl: getPostUrl()
  };
}

function parseMobileComment(container) {
  const authorEl = container.querySelector('span.f20, h3, strong, a');
  const textEl = container.querySelector('span.f1, div[dir="auto"], p');
  const dateEl = container.querySelector('span.f19, span.f13, div[aria-label*="ago"], a[href*="comment_id"]');
  const authorLink = authorEl?.closest?.('a[href]') || (authorEl?.matches?.('a[href]') ? authorEl : null);
  const profile = extractProfileInfo(authorLink, container);

  const author = cleanCell(authorEl?.innerText || authorEl?.textContent || 'Unknown');
  const text = cleanCell(textEl?.innerText || textEl?.textContent || '');
  const date = cleanCell(dateEl?.getAttribute('aria-label') || dateEl?.innerText || dateEl?.textContent || 'Unknown');

  return {
    author,
    userId: profile.userId,
    profileUrl: profile.profileUrl,
    text,
    date,
    likes: extractLikeCount(container),
    commentLink: buildCommentLink(container),
    postUrl: getPostUrl()
  };
}

function findAuthorLink(article) {
  const links = Array.from(article.querySelectorAll('a[role="link"], a[href]'));

  for (const link of links) {
    const text = cleanCell(link.innerText || link.textContent || '');
    const hasNameSpan = Boolean(link.querySelector('span[dir="auto"]'));
    const isAvatar = Boolean(link.querySelector('img, svg')) && text.length === 0;

    if (!isAvatar && (hasNameSpan || text.length > 1) && !looksLikeActionText(text)) {
      return link;
    }
  }

  return null;
}

function extractAuthor(authorLink) {
  if (!authorLink) {
    return 'Unknown';
  }

  const nameSpan = authorLink.querySelector('span[dir="auto"]');
  return cleanCell(nameSpan?.innerText || authorLink.innerText || authorLink.textContent || 'Unknown') || 'Unknown';
}

function extractProfileInfo(authorLink, scope) {
  const href = authorLink?.getAttribute?.('href') || '';
  const profileUrl = normalizeProfileUrl(href);
  const userId = extractUserId(authorLink, scope, profileUrl);

  return {
    userId,
    profileUrl
  };
}

function normalizeProfileUrl(href) {
  if (!href) {
    return '';
  }

  try {
    const url = new URL(href, window.location.href);
    const host = url.hostname.replace(/^m\./, 'www.');

    if (!host.includes('facebook.com')) {
      return '';
    }

    const userIdFromHref = extractUserIdFromValue(href);
    if (userIdFromHref) {
      return `https://www.facebook.com/profile.php?id=${userIdFromHref}`;
    }

    const profileId = url.searchParams.get('id');
    if (url.pathname.includes('/profile.php') && profileId) {
      return `https://www.facebook.com/profile.php?id=${profileId}`;
    }

    const groupUserPath = url.pathname.match(/^\/groups\/([^/]+)\/user\/([^/?#]+)(?:\/|$)/i);
    if (groupUserPath) {
      return `https://www.facebook.com/groups/${groupUserPath[1]}/user/${groupUserPath[2]}`;
    }

    const groupMemberPath = url.pathname.match(/^\/groups\/([^/]+)\/members\/([^/?#]+)(?:\/|$)/i);
    if (groupMemberPath) {
      return `https://www.facebook.com/groups/${groupMemberPath[1]}/members/${groupMemberPath[2]}`;
    }

    const pathParts = url.pathname.split('/').filter(Boolean);
    const firstPart = pathParts[0] || '';

    if (!firstPart || isFacebookSystemPath(firstPart)) {
      return '';
    }

    return `https://www.facebook.com/${firstPart}`;
  } catch (error) {
    return '';
  }
}

function isGroupUserProfileUrl(profileUrl) {
  try {
    const url = new URL(profileUrl);
    return /^\/groups\/[^/]+\/(?:user|members)\/[^/]+/i.test(url.pathname);
  } catch (error) {
    return false;
  }
}

function extractUserId(authorLink, scope, profileUrl) {
  const values = [];

  if (authorLink) {
    values.push(
      authorLink.getAttribute('href'),
      authorLink.getAttribute('data-hovercard'),
      authorLink.getAttribute('data-profileid'),
      authorLink.dataset?.hovercard,
      authorLink.dataset?.profileid,
      authorLink.outerHTML
    );
  }

  values.push(profileUrl);

  if (scope) {
    values.push(scope.outerHTML);
  }

  for (const value of values) {
    const userId = extractUserIdFromValue(value);
    if (userId) {
      return userId;
    }
  }

  return '';
}

function extractUserIdFromValue(value) {
  if (!value) {
    return '';
  }

  const decoded = safeDecode(String(value));

  try {
    const url = new URL(decoded, window.location.href);
    const groupUserPath = url.pathname.match(/\/groups\/[^/]+\/user\/(\d{5,})(?:\/|$)/i);
    if (groupUserPath) {
      return groupUserPath[1];
    }

    const groupMemberPath = url.pathname.match(/\/groups\/[^/]+\/members\/(\d{5,})(?:\/|$)/i);
    if (groupMemberPath) {
      return groupMemberPath[1];
    }

    const userPath = url.pathname.match(/\/user\/(\d{5,})(?:\/|$)/i);
    if (userPath) {
      return userPath[1];
    }

    const directUserId = url.searchParams.get('user_id') ||
      url.searchParams.get('profile_id');

    if (isNumericId(directUserId)) {
      return directUserId;
    }

    const directId = url.searchParams.get('id');
    if (url.pathname.includes('/profile.php') && isNumericId(directId)) {
      return directId;
    }

    const rootNumericProfile = url.pathname.match(/^\/(\d{5,})\/?$/);
    if (rootNumericProfile) {
      return rootNumericProfile[1];
    }
  } catch (error) {
    // Continue with regex parsing below.
  }

  const patterns = [
    /\/groups\/[^/"'<>]+\/user\/(\d{5,})(?:\/|["'<>?&\s]|$)/i,
    /\/groups\/[^/"'<>]+\/members\/(\d{5,})(?:\/|["'<>?&\s]|$)/i,
    /\/user\/(\d{5,})(?:\/|["'<>?&\s]|$)/i,
    /(?:[?&]|%3F|%26)(?:user_id|profile_id)(?:=|%3D)(\d{5,})/i,
    /\/profile\.php[^"'<>]*?(?:[?&]|%3F|%26)id(?:=|%3D)(\d{5,})/i
  ];

  for (const pattern of patterns) {
    const match = decoded.match(pattern);
    if (match && isNumericId(match[1])) {
      return match[1];
    }
  }

  return '';
}

function extractTargetUserIdFromHtml(html, profileUrl) {
  const decoded = safeDecode(String(html || ''));
  const slug = getProfileSlug(profileUrl);
  const candidates = [];

  collectCandidateIds(decoded, /"profile_owner"\s*:\s*\{[^{}]{0,500}?"id"\s*:\s*"(\d{5,})"/gi, candidates, 12);
  collectCandidateIds(decoded, /"profile_owner"\s*:\s*"(\d{5,})"/gi, candidates, 12);
  collectCandidateIds(decoded, /"profile_id"\s*:\s*"(\d{5,})"/gi, candidates, 10);
  collectCandidateIds(decoded, /"profile_id"\s*:\s*(\d{5,})/gi, candidates, 10);
  collectCandidateIds(decoded, /"entity_id"\s*:\s*"(\d{5,})"/gi, candidates, 6);
  collectCandidateIds(decoded, /"entity_id"\s*:\s*(\d{5,})/gi, candidates, 6);
  collectCandidateIds(decoded, /"owning_profile"\s*:\s*\{[^{}]{0,600}?"id"\s*:\s*"(\d{5,})"/gi, candidates, 8);
  collectCandidateIds(decoded, /\/profile\.php[^"'<>]{0,400}?(?:[?&]|%3F|%26)id(?:=|%3D)(\d{5,})/gi, candidates, 4);

  if (slug) {
    const escapedSlug = escapeRegExp(slug);
    collectCandidateIds(decoded, new RegExp(`(?:${escapedSlug})[^]{0,900}?"id"\\s*:\\s*"(\\d{5,})"`, 'gi'), candidates, 12);
    collectCandidateIds(decoded, new RegExp(`"id"\\s*:\\s*"(\\d{5,})"[^]{0,900}?(?:${escapedSlug})`, 'gi'), candidates, 12);
  }

  const scored = new Map();
  for (const candidate of candidates) {
    const previous = scored.get(candidate.id) || 0;
    scored.set(candidate.id, Math.max(previous, candidate.score));
  }

  const sorted = Array.from(scored.entries()).sort((a, b) => b[1] - a[1]);
  const best = sorted[0];

  return best && best[1] >= 8 ? best[0] : '';
}

function collectCandidateIds(text, pattern, candidates, baseScore) {
  let match;

  while ((match = pattern.exec(text)) !== null) {
    const id = match[1];
    if (!isNumericId(id)) {
      continue;
    }

    const start = Math.max(0, match.index - 500);
    const end = Math.min(text.length, match.index + match[0].length + 500);
    const context = text.slice(start, end);

    if (/"__user"|"actorID"|"viewer"|"currentUser"/i.test(context) && !/"profile_owner"|"profile_id"|"owning_profile"/i.test(context)) {
      continue;
    }

    let score = baseScore;
    if (/"profile_owner"|"profile_id"|"owning_profile"/i.test(context)) {
      score += 4;
    }
    if (/facebook\.com\\?\/profile\.php|facebook\.com\/profile\.php/i.test(context)) {
      score += 2;
    }

    candidates.push({ id, score });
  }
}

function getProfileSlug(profileUrl) {
  try {
    const url = new URL(profileUrl);
    const pathParts = url.pathname.split('/').filter(Boolean);
    const firstPart = pathParts[0] || '';
    return firstPart && firstPart !== 'profile.php' ? firstPart : '';
  } catch (error) {
    return '';
  }
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function isFacebookSystemPath(pathPart) {
  return [
    'ajax',
    'groups',
    'hashtag',
    'help',
    'login',
    'marketplace',
    'messages',
    'notifications',
    'pages',
    'people',
    'permalink.php',
    'photo.php',
    'plugins',
    'profile.php',
    'reel',
    'share',
    'stories',
    'watch'
  ].includes(pathPart.toLowerCase());
}

function extractCommentText(article, authorLink, author) {
  const exactTextDiv = article.querySelector('div[dir="auto"][style*="text-align"]');
  if (exactTextDiv) {
    const exactText = cleanCell(exactTextDiv.innerText || exactTextDiv.textContent || '');
    if (exactText && exactText !== author) {
      return exactText;
    }
  }

  const textNodes = Array.from(article.querySelectorAll('div[dir="auto"], span[dir="auto"]'));

  for (const node of textNodes) {
    if (authorLink && authorLink.contains(node)) {
      continue;
    }

    const text = cleanCell(node.innerText || node.textContent || '');
    if (text && text !== author && !looksLikeActionText(text)) {
      return text;
    }
  }

  const img = article.querySelector('a[href*="photo"] img, div[role="button"] img, img[alt]');
  if (img) {
    return `[Image/Sticker] ${cleanCell(img.getAttribute('alt') || '')}`;
  }

  return '';
}

function extractDate(article, authorLink) {
  const dateLinks = Array.from(article.querySelectorAll('ul li a, a[href*="comment_id"], a[aria-label], span, div'));

  for (const link of dateLinks) {
    if (link === authorLink) {
      continue;
    }

    const label = cleanCell(link.getAttribute('aria-label') || link.innerText || link.textContent || '');
    if (looksLikeDate(label)) {
      return label;
    }
  }

  return 'Unknown';
}

function extractLikeCount(scope) {
  const candidates = Array.from(scope.querySelectorAll('[aria-label], [title], span, div, a'));
  let best = 0;

  for (const element of candidates) {
    if (!isVisibleElement(element)) {
      continue;
    }

    const value = cleanCell([
      element.getAttribute('aria-label'),
      element.getAttribute('title'),
      element.innerText,
      element.textContent
    ].filter(Boolean).join(' '));

    if (!value || value.length > 120) {
      continue;
    }

    const normalized = normalizeText(value);
    const hasReactionWord = /(^|\s)(like|likes|reaction|reactions|thich|cam xuc|me gusta|love|care|haha|wow|sad|angry)(\s|$)/i.test(normalized);
    if (!hasReactionWord) {
      continue;
    }

    best = Math.max(best, parseCompactNumber(value));
  }

  return best;
}

function looksLikeDate(value) {
  const text = normalizeText(value);
  return /(^|\s)(now|just now|vua xong|\d+\s*(s|sec|second|m|min|minute|h|hr|hour|d|day|w|week|mo|month|y|year)|hom qua|gio|phut|ngay|tuan|thang|nam)(\s|$)/i.test(text);
}

function looksLikeActionText(value) {
  const text = normalizeText(value);
  return [
    'like',
    'reply',
    'share',
    'send',
    'add friend',
    'follow',
    'message',
    'see more',
    'view more',
    'see original',
    'see translation',
    'thich',
    'luot thich',
    'tra loi',
    'phan hoi',
    'chia se',
    'xem them',
    'xem ban goc',
    'xem ban dich',
    'theo doi',
    'nhan tin',
    'ket ban'
  ].some((word) => text === word || text.startsWith(`${word} `));
}

function buildCommentLink(scopeElement) {
  const commentId = extractCommentId(scopeElement);

  if (!commentId) {
    return '';
  }

  try {
    const url = new URL(window.location.href);
    url.hash = '';
    url.searchParams.delete('comment_id');
    url.searchParams.set('comment_id', commentId);
    return url.toString();
  } catch (error) {
    return '';
  }
}

function extractCommentId(scopeElement) {
  const candidateScopes = collectCommentScopes(scopeElement);
  const fallbackIds = [];

  for (const candidateScope of candidateScopes) {
    const rawHtml = candidateScope.outerHTML || '';
    const htmlMatch = rawHtml.match(/(?:[?&]|%3F|%26)(?:comment_id|fb_comment_id|cid)(?:=|%3D)([^&#"'<>\s]+)/i);

    if (htmlMatch && htmlMatch[1]) {
      const extractedId = normalizeCommentId(htmlMatch[1]);
      if (isNumericCommentId(extractedId)) {
        return extractedId;
      }
      if (extractedId) {
        fallbackIds.push(extractedId);
      }
    }

    const links = candidateScope.querySelectorAll('a[href], a[data-href], a[data-url]');
    for (const link of links) {
      for (const attribute of ['href', 'data-href', 'data-url']) {
        const commentId = extractCommentIdFromValue(link.getAttribute(attribute));
        if (isNumericCommentId(commentId)) {
          return commentId;
        }
        if (commentId) {
          fallbackIds.push(commentId);
        }
      }
    }
  }

  return fallbackIds.find(isNumericCommentId) || '';
}

function collectCommentScopes(scopeElement) {
  const scopes = [scopeElement];
  let current = scopeElement;

  for (let depth = 0; depth < 3; depth += 1) {
    current = current.parentElement;
    if (!current) {
      break;
    }
    scopes.push(current);
  }

  const parent = scopeElement.parentElement;
  if (parent) {
    if (parent.previousElementSibling) {
      scopes.push(parent.previousElementSibling);
    }
    if (parent.nextElementSibling) {
      scopes.push(parent.nextElementSibling);
    }
  }

  if (scopeElement.previousElementSibling) {
    scopes.push(scopeElement.previousElementSibling);
  }
  if (scopeElement.nextElementSibling) {
    scopes.push(scopeElement.nextElementSibling);
  }

  return scopes;
}

function extractCommentIdFromValue(value) {
  if (!value) {
    return '';
  }

  try {
    const url = new URL(value, window.location.href);
    const directId = url.searchParams.get('comment_id') || url.searchParams.get('fb_comment_id') || url.searchParams.get('cid');
    if (directId) {
      return normalizeCommentId(directId);
    }
  } catch (error) {
    // Non-standard values are parsed below.
  }

  const match = String(value).match(/(?:[?&]|%3F|%26)(?:comment_id|fb_comment_id|cid)(?:=|%3D)([^&#"'<>\s]+)/i);
  return match && match[1] ? normalizeCommentId(match[1]) : '';
}

function normalizeCommentId(value) {
  const decoded = safeDecode(String(value || '').trim());
  const numericMatch = decoded.match(/^\d+$/);
  return numericMatch ? numericMatch[0] : '';
}

function isNumericCommentId(value) {
  return /^\d+$/.test(String(value || ''));
}

function isNumericId(value) {
  return /^\d{5,}$/.test(String(value || ''));
}

function exportFiles(comments, options) {
  const timestamp = getTimestamp();
  const rows = comments.map(toExportRow);

  downloadBlob(toCsv(rows), `fb_profiles_${timestamp}.csv`, 'text/csv;charset=utf-8');
}

function toExportRow(row) {
  return {
    author: row.author || '',
    userId: row.userId || '',
    profileUrl: row.profileUrl || '',
    comment: row.comment || ''
  };
}

function toCsv(users) {
  const rows = ['Author,User ID,Profile URL,Comment'];

  for (const row of users) {
    rows.push([
      csvValue(row.author),
      csvValueForExcelId(row.userId),
      csvValue(row.profileUrl),
      csvValue(row.comment)
    ].join(','));
  }

  return `\uFEFF${rows.join('\n')}\n`;
}

function downloadBlob(content, filename, type) {
  const blob = new Blob([content], { type });
  const downloadUrl = URL.createObjectURL(blob);
  const link = document.createElement('a');

  link.href = downloadUrl;
  link.download = filename;
  link.style.display = 'none';
  (document.body || document.documentElement).appendChild(link);
  link.click();

  setTimeout(() => {
    URL.revokeObjectURL(downloadUrl);
    link.remove();
  }, 1000);
}

function notifyFinished(count, partial) {
  try {
    chrome.runtime.sendMessage({
      action: 'SCRAPING_FINISHED',
      count,
      partial
    });
  } catch (error) {
    // The popup can be closed while the page export still succeeds.
  }
}

function updateStatus(status, count = null) {
  setOverlayStatus(status, count);

  try {
    const maybePromise = chrome.runtime.sendMessage({
      action: 'UPDATE_STATUS',
      status,
      count
    });

    if (maybePromise && typeof maybePromise.catch === 'function') {
      maybePromise.catch(() => {});
    }
  } catch (error) {
    // The popup may be closed; keep scraping and rely on the overlay.
  }
}

function setOverlayStatus(status, count = null) {
  if (!statusOverlay) {
    statusOverlay = document.getElementById('fb-comments-export-status');

    if (!statusOverlay) {
      statusOverlay = document.createElement('div');
      statusOverlay.id = 'fb-comments-export-status';
      statusOverlay.style.position = 'fixed';
      statusOverlay.style.right = '16px';
      statusOverlay.style.bottom = '16px';
      statusOverlay.style.zIndex = '2147483647';
      statusOverlay.style.maxWidth = '340px';
      statusOverlay.style.padding = '10px 12px';
      statusOverlay.style.borderRadius = '8px';
      statusOverlay.style.background = 'rgba(16, 24, 40, 0.94)';
      statusOverlay.style.color = '#fff';
      statusOverlay.style.font = '13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif';
      statusOverlay.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.22)';
      statusOverlay.style.pointerEvents = 'none';
      (document.body || document.documentElement).appendChild(statusOverlay);
    }
  }

  const localizedStatus = localizeContentStatus(status, activeOptions.language);
  const countText = typeof count === 'number'
    ? activeOptions.language === 'vi'
      ? ` (${count} người dùng)`
      : ` (${count} users)`
    : '';
  statusOverlay.textContent = `FB Comments Export: ${localizedStatus}${countText}`;
}

function localizeContentStatus(status, language) {
  if (language !== 'vi') {
    return status;
  }

  const text = String(status || '');
  const replacements = [
    [/^Starting scraper\.\.\.$/i, 'Đang bắt đầu quét...'],
    [/^Stopping\.\.\.$/i, 'Đang dừng...'],
    [/^Trying to apply selected filter\.\.\.$/i, 'Đang áp dụng bộ lọc đã chọn...'],
    [/^Scrolling to load comments\.\.\.$/i, 'Đang cuộn để tải bình luận...'],
    [/^Expanding comments and replies\.\.\.$/i, 'Đang mở bình luận và phản hồi...'],
    [/^Expanding comments\.\.\.$/i, 'Đang mở bình luận...'],
    [/^Parsing comment authors\.\.\.$/i, 'Đang đọc người bình luận...'],
    [/^Auto pause to reduce scan pressure\.\.\.$/i, 'Đang tạm dừng để giảm tốc độ quét...'],
    [/^Auto pause during reaction scan\.\.\.$/i, 'Đang tạm dừng khi quét cảm xúc...'],
    [/^No users found\. Open the post detail view and try again\.$/i, 'Không tìm thấy người dùng. Mở chi tiết bài viết rồi thử lại.'],
    [/^Stopped\. No users found\.$/i, 'Đã dừng. Không tìm thấy người dùng.'],
    [/^Stopped\.$/i, 'Đã dừng.'],
    [/^Opening Reels comments panel\.\.\.$/i, 'Đang mở bảng bình luận Reels...'],
    [/^Could not open Reels comments\. Click the comment button once, then start again\.$/i, 'Không mở được bình luận Reels. Hãy bấm nút bình luận một lần rồi bắt đầu lại.'],
    [/^Waiting for Reels comments to load\.\.\.$/i, 'Đang chờ bình luận Reels tải...'],
    [/^Reels comments did not finish loading\. Try again after the panel is visible\.$/i, 'Bình luận Reels chưa tải xong. Hãy thử lại sau khi bảng bình luận hiện ra.'],
    [/^Could not open reactions list\. Facebook may be hiding it for this post\.$/i, 'Không mở được danh sách cảm xúc. Facebook có thể đang ẩn danh sách này.']
  ];

  for (const [pattern, replacement] of replacements) {
    if (pattern.test(text)) {
      return replacement;
    }
  }

  let match = text.match(/^Loading\.\.\. found about (\d+) comment blocks$/i);
  if (match) {
    return `Đang tải... tìm thấy khoảng ${match[1]} khối bình luận`;
  }

  match = text.match(/^Exporting (\d+) users\.\.\.$/i);
  if (match) {
    return `Đang xuất ${match[1]} người dùng...`;
  }

  match = text.match(/^Stopped\. Exporting (\d+) users\.\.\.$/i);
  if (match) {
    return `Đã dừng. Đang xuất ${match[1]} người dùng...`;
  }

  match = text.match(/^Scanning (.+)\.\.\. collected (\d+) users$/i);
  if (match) {
    return `Đang quét ${match[1]}... đã lấy ${match[2]} người dùng`;
  }

  match = text.match(/^Resolving UID from (\d+) profile URLs\.\.\.$/i);
  if (match) {
    return `Đang đổi UID từ ${match[1]} link profile...`;
  }

  match = text.match(/^Resolving UID\.\.\. (\d+)\/(\d+)$/i);
  if (match) {
    return `Đang đổi UID... ${match[1]}/${match[2]}`;
  }

  match = text.match(/^Collected (\d+) users\.\.\.$/i);
  if (match) {
    return `Đã lấy ${match[1]} người dùng...`;
  }

  match = text.match(/^Reels comments loaded\. Found about (\d+) comment authors$/i);
  if (match) {
    return `Bình luận Reels đã tải. Tìm thấy khoảng ${match[1]} người bình luận`;
  }

  return text;
}

function countCommentCandidates() {
  if (isReelsPage()) {
    return countReelsCommentAuthorLinks();
  }

  return document.querySelectorAll('div[role="article"], div[data-type="transactional"]').length;
}

function isVisibleElement(element) {
  if (!element || !(element instanceof Element)) {
    return false;
  }

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

function parseCompactNumber(value) {
  const text = normalizeText(value).replace(/,/g, '.');
  const match = text.match(/(\d+(?:\.\d+)?)\s*(k|m|b|nghin|ngan|trieu|ty)?/i);

  if (!match) {
    return 0;
  }

  let number = Number(match[1]);
  if (!Number.isFinite(number)) {
    return 0;
  }

  const suffix = match[2] || '';
  if (/k|nghin|ngan/i.test(suffix)) {
    number *= 1000;
  } else if (/m|trieu/i.test(suffix)) {
    number *= 1000000;
  } else if (/b|ty/i.test(suffix)) {
    number *= 1000000000;
  }

  return Math.round(number);
}

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

function cleanCell(value) {
  return String(value || '').replace(/\s+/g, ' ').trim();
}

function csvValue(value) {
  return `"${String(value ?? '').replace(/"/g, '""')}"`;
}

function csvValueForExcelId(value) {
  const text = String(value ?? '').trim();

  if (!text) {
    return '""';
  }

  return `="${text.replace(/"/g, '""')}"`;
}

function getPostUrl() {
  try {
    const url = new URL(window.location.href);
    url.hash = '';
    return url.toString();
  } catch (error) {
    return window.location.href;
  }
}

function getTimestamp() {
  const now = new Date();
  const pad = (value) => String(value).padStart(2, '0');
  return [
    now.getFullYear(),
    pad(now.getMonth() + 1),
    pad(now.getDate()),
    '-',
    pad(now.getHours()),
    pad(now.getMinutes()),
    pad(now.getSeconds())
  ].join('');
}

function safeDecode(value) {
  try {
    return decodeURIComponent(value);
  } catch (error) {
    return value;
  }
}

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