const isMiningWarsHost = (hostname) => {
  if (!hostname) return false;
  return /(^|\.)gomining\.com$/i.test(hostname);
};

const isMiningWarsPage = () => {
  try {
    if (!isMiningWarsHost(location.hostname)) return false;
    const localPathOk = /\/miner-wars(\/|$)/.test(location.pathname || '');
    if (localPathOk) return true;
    const referrer = document.referrer || '';
    if (/\/miner-wars(\/|$)/.test(referrer)) return true;
    if (window.top && window.top !== window) {
      try {
        return /\/miner-wars(\/|$)/.test(window.top.location.pathname || '');
      } catch (_) {
        return false;
      }
    }
    return false;
  } catch (_) {
    return false;
  }
};

const WS_THROTTLE_MS = 20000;
const WS_LOG_THROTTLE_MS = 20000;
const WS_NAME_LOG_THROTTLE_MS = 20000;
const wsEventThrottle = new Map();
const wsLogThrottle = new Map();
const wsLogSuppressed = new Map();
const wsNameLogThrottle = new Map();
const API_LOG_THROTTLE_MS = 20000;
const apiLogThrottle = new Map();
const apiLogSuppressed = new Map();
let hookDomInjectAttempted = false;
const displayWsName = (eventName) => {
  if (!eventName) return eventName;
  const key = String(eventName);
  if (key === 'allUsersStateV2' || key === 'allUsersState') return 'WEBSOCKET_SCORE';
  return key;
};
function shouldThrottleWsEvent(eventName) {
  if (!eventName) return false;
  const key = String(eventName).toLowerCase();
  if (key !== 'allusersstatev2' && key !== 'allusersstate') return false;
  const now = Date.now();
  const last = wsEventThrottle.get(key) || 0;
  const throttled = now - last < WS_THROTTLE_MS;
  if (!throttled) {
    wsEventThrottle.set(key, now);
  }
  return throttled;
}

function shouldLogWsEvent(eventName) {
  if (!eventName) return false;
  const key = String(eventName).toLowerCase();
  const now = Date.now();
  const last = wsLogThrottle.get(key) || 0;
  if (now - last < WS_LOG_THROTTLE_MS) {
    wsLogSuppressed.set(key, (wsLogSuppressed.get(key) || 0) + 1);
    return false;
  }
  wsLogThrottle.set(key, now);
  return true;
}

function shouldForwardWsEvent(eventName) {
  if (!eventName) return false;
  const key = String(eventName).toLowerCase();
  if (key.startsWith('chat:')) return false;
  return true;
}

function logWsSummary(eventName, label = 'wsEvent') {
  if (!shouldLogWsEvent(eventName)) return;
  const key = String(eventName).toLowerCase();
  const suppressed = wsLogSuppressed.get(key) || 0;
  wsLogSuppressed.set(key, 0);
  const summary = suppressed > 0 ? ` (+${suppressed} suppressed)` : '';
  console.log(`[MW] ${label}`, displayWsName(eventName) + summary);
}

function logWsEventName(eventName, data) {
  if (!eventName) return;
  const key = String(eventName).toLowerCase();
  if (key !== 'allusersstatev2' && key !== 'allusersstate') return;
  const now = Date.now();
  const last = wsNameLogThrottle.get(key) || 0;
  if (now - last < WS_NAME_LOG_THROTTLE_MS) return;
  wsNameLogThrottle.set(key, now);
  const payload = data && data.rawEvent ? data.rawEvent[1] : null;
  console.log('[MW] allUsersState event', {
    name: eventName,
    type: payload === null ? 'null' : typeof payload,
    size: Array.isArray(payload) ? payload.length : null,
    keys: payload && typeof payload === 'object' ? Object.keys(payload) : null
  });
}

function shouldLogApiCall(endpoint) {
  const key = String(endpoint || '');
  if (!key) return true;
  const now = Date.now();
  const last = apiLogThrottle.get(key) || 0;
  if (now - last < API_LOG_THROTTLE_MS) {
    apiLogSuppressed.set(key, (apiLogSuppressed.get(key) || 0) + 1);
    return false;
  }
  apiLogThrottle.set(key, now);
  return true;
}

function logApiSummary(endpoint, label = 'API_CALL') {
  if (!shouldLogApiCall(endpoint)) return;
  const key = String(endpoint || '');
  const suppressed = apiLogSuppressed.get(key) || 0;
  apiLogSuppressed.set(key, 0);
  const summary = suppressed > 0 ? ` (+${suppressed} suppressed)` : '';
  console.log(`[MW] ${label}`, `${key}${summary}`);
}

function sendWsDebug(eventName, payload) {
  try {
    if (chrome?.runtime?.sendMessage) {
      chrome.runtime.sendMessage({
        type: 'MW_WS_DEBUG',
        eventName,
        payload,
        at: Date.now()
      });
    }
  } catch (_) {}
}

function injectPageHookDom(reason = '') {
  if (hookDomInjectAttempted) return;
  hookDomInjectAttempted = true;
  try {
    const src = chrome?.runtime?.getURL ? chrome.runtime.getURL('page-hook.js') : null;
    if (!src) {
      sendWsDebug('pageHook:domInjectError', { reason, message: 'no runtime URL' });
      return;
    }
    sendWsDebug('pageHook:domInjectAttempt', { reason, src });
    const script = document.createElement('script');
    script.src = src;
    script.async = false;
    script.dataset.mwHook = '1';
    script.addEventListener('load', () => {
      sendWsDebug('pageHook:domInjected', { reason, src });
      script.remove();
    });
    script.addEventListener('error', () => {
      sendWsDebug('pageHook:domInjectError', { reason, src });
    });
    (document.head || document.documentElement).appendChild(script);
  } catch (err) {
    sendWsDebug('pageHook:domInjectException', { reason, message: err?.message || String(err || '') });
  }
}

function shouldLogApiCall(endpoint) {
  const key = String(endpoint || '');
  if (!key) return true;
  const now = Date.now();
  const last = apiLogThrottle.get(key) || 0;
  if (now - last < API_LOG_THROTTLE_MS) return false;
  apiLogThrottle.set(key, now);
  return true;
}

if (globalThis.__MW_CONTENT_SCRIPT_LOADED__) {
  console.log('[MW] Content script already loaded, skipping duplicate init.');
} else {
  const contextId = globalThis.__MW_CONTENT_SCRIPT_CONTEXT_ID__ || `mw_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  globalThis.__MW_CONTENT_SCRIPT_LOADED__ = true;
  globalThis.__MW_CONTENT_SCRIPT_CONTEXT_ID__ = contextId;
  try {
    window.__MW_CONTENT_SCRIPT_LOADED__ = true;
    window.__MW_CONTENT_SCRIPT_CONTEXT_ID__ = contextId;
  } catch (_) {}

  console.log('[MW] Content script injected');
  const mwEnabled = isMiningWarsPage();
  if (!mwEnabled) {
    console.log('[MW] Content script idle (not Mining Wars page).');
  } else {
    ensureLiveTableStyles();
    sendWsDebug('content-ready', {
      href: location.href,
      host: location.hostname,
      path: location.pathname
    });
  }

  // Request the side panel to open on Mining Wars pages.
  if (mwEnabled && chrome && chrome.runtime && chrome.runtime.sendMessage) {
    chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL' }).catch(() => {});
  }

  // Ask background to inject the hook in the MAIN world (bypasses CSP).
  if (mwEnabled && chrome && chrome.runtime && chrome.runtime.sendMessage) {
    chrome.runtime.sendMessage({ type: 'INJECT_HOOK' }).catch(() => {});
  }
  if (mwEnabled) {
    injectPageHookDom('content-init');
  }

  // Connect to background to forward boost events.
let bgPort = null;
  if (mwEnabled) {
    try {
      bgPort = chrome.runtime.connect({ name: 'content' });
      console.log('[MW] Connected to background port (content)');
    } catch (err) {
      console.warn('[MW] Failed to connect to background port', err);
      bgPort = null;
    }
  }

if (bgPort) {
  bgPort.onMessage.addListener((msg) => {
    if (!msg) return;
    if (msg.type === 'COMMAND' && msg.requestId) {
      handleExtractionCommand(msg.payload?.type, msg.payload)
        .then((result) => {
          bgPort.postMessage({ type: 'COMMAND_RESPONSE', requestId: msg.requestId, ok: true, data: result });
        })
        .catch((err) => {
          bgPort.postMessage({ type: 'COMMAND_RESPONSE', requestId: msg.requestId, ok: false, error: err?.message || 'Command failed.' });
        });
    }
  });
}

if (mwEnabled && chrome && chrome.runtime && chrome.runtime.sendMessage) {
  chrome.runtime.sendMessage({ type: 'REGISTER_CONTENT_SCRIPT' }, (res) => {
    if (chrome.runtime.lastError) {
      console.warn('[MW] Failed to register content script', chrome.runtime.lastError.message);
    } else {
      console.log('[MW] Content script registered', res);
    }
  });
}

function sendBoostEvent(data) {
  if (!data.eventId) {
    data.eventId = `ev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  }
  try {
    if (bgPort) {
      bgPort.postMessage({ action: 'boostEvent', data });
    }
  } catch (_) {}
  // Always also send via message as a fallback
  try {
    chrome.runtime.sendMessage({ type: 'MW_EVENT_FROM_CONTENT', data });
  } catch (_) {}
}

function sendWsEvent(data) {
  try {
    const eventName = data && data.rawEvent ? data.rawEvent[0] : data?.eventName;
    const throttled = shouldThrottleWsEvent(eventName);
    if (!shouldForwardWsEvent(eventName)) return;
    logWsEventName(eventName, data);
    if (!throttled) {
      logWsSummary(eventName, 'content forwarding wsEvent');
    }
    const payload = data && typeof data === 'object'
      ? { ...data, sourceUrl: window.location.href }
      : { rawEvent: data, sourceUrl: window.location.href };
    if (bgPort) {
      bgPort.postMessage({ action: 'wsEvent', data: payload });
    }
    try {
      chrome.runtime.sendMessage({
        type: 'MW_WS_DEBUG',
        eventName,
        forwarded: true,
        hasRawEvent: Boolean(payload && payload.rawEvent),
        at: Date.now()
      });
    } catch (_) {}
  } catch (_) {}
  try {
    chrome.runtime.sendMessage({ type: 'MW_WS_FROM_CONTENT', data: payload });
  } catch (_) {}
}

const LIVE_SCORE_XPATHS = {
  percent: '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[3]/span',
  score: '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[2]/span'
};
let lastLiveScoreSnapshot = { percent: '', score: '' };
function readXpathValue(path) {
  if (!path) return '';
  try {
    const result = document.evaluate(path, document, null, XPathResult.STRING_TYPE, null);
    return (result?.stringValue || '').trim();
  } catch (_) {
    return '';
  }
}
function captureLiveScoreSnapshot() {
  if (!mwEnabled || !chrome?.runtime?.sendMessage) return;
  const percentText = readXpathValue(LIVE_SCORE_XPATHS.percent);
  const scoreText = readXpathValue(LIVE_SCORE_XPATHS.score);
  if (!percentText && !scoreText) return;
  if (percentText === lastLiveScoreSnapshot.percent && scoreText === lastLiveScoreSnapshot.score) return;
  lastLiveScoreSnapshot = { percent: percentText, score: scoreText };
  try {
    chrome.runtime.sendMessage({
      type: 'MW_LIVE_SCORE_SNAPSHOT',
      data: {
        percent: percentText,
        score: scoreText,
        ts: Date.now()
      }
    });
  } catch (_) {}
}
setInterval(captureLiveScoreSnapshot, 10000);
setTimeout(captureLiveScoreSnapshot, 2000);

function ensureLiveTableStyles() {
  if (document.getElementById('mw-live-table-width')) return;
  const style = document.createElement('style');
  style.id = 'mw-live-table-width';
  style.textContent = `
    .miner-wars-leaderboard,
    .miner-wars-leaderboard__inner,
    .leaderboard-clans,
    .leaderboard-players {
      width: 200% !important;
      max-width: none !important;
      min-width: 0 !important;
    }
    .miner-wars-leaderboard .scrollable,
    .leaderboard-clans .scrollable {
      max-width: none !important;
    }
  `;
  const target = document.head || document.documentElement;
  target?.appendChild(style);
}

let tokenExpiredNotified = false;
let apiForwardDisabled = false;

function notifyTokenExpired(reason = 'Authentication expired; refresh Mining Wars page.') {
  if (tokenExpiredNotified) return;
  tokenExpiredNotified = true;
  const payload = { reason, timestamp: Date.now() };
  try {
    if (bgPort) {
      bgPort.postMessage({ action: 'tokenExpired', data: payload });
    }
  } catch (_) {}
  try {
    chrome.runtime.sendMessage({ type: 'MW_TOKEN_EXPIRED', data: payload });
  } catch (_) {}
}

function sendLiveSummaryState(data) {
  if (!data) return;
  const payload = { ...data, timestamp: Date.now() };
  try {
    if (bgPort) {
      bgPort.postMessage({ action: 'liveSummary', data: payload });
    }
  } catch (_) {}
  try {
    chrome.runtime.sendMessage({ type: 'MW_LIVE_SUMMARY', data: payload });
  } catch (_) {}
}

// Listen for page-level messages from the injected hook.
window.addEventListener('message', (event) => {
  if (!event || !event.data) return;

  // Debug: surface raw WS payloads that look like socket.io events even if type wrapper is missing.
  if (Array.isArray(event.data) && event.data.length && typeof event.data[0] === 'string') {
    if (shouldThrottleWsEvent(event.data[0])) return;
    if (!shouldForwardWsEvent(event.data[0])) return;
    if (event.data[0] === 'allUsersStateV2' || event.data[0] === 'abilityUsage' || event.data[0] === 'abilityUsageV2' || event.data[0] === 'abilityUsageV3') {
      sendWsEvent({ rawEvent: event.data });
    }
  }

  if (event.data.type === 'MW_ABILITY_USAGE') {
    const payload = event.data.payload || {};
    console.log('[MW] abilityUsage received', payload);
    const eid = payload.id || payload.abilityId || `ev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    sendBoostEvent({
      eventId: eid,
      user: payload.userAlias || payload.user || 'Unknown',
      ability: payload.name || payload.abilityName || payload.id || 'Ability',
      abilityId: payload.id,
      roundId: payload.roundId,
      logType: 'Ability',
      timestamp: Date.now()
    });
  }

  if (event.data.type === 'MW_WS_EVENT' || event.data.type === 'MW_WS_EVENT_SAFE' || event.data.type === 'MW_WS_EVENT_MINI') {
    const payload = event.data.payload || {};
    if (event.data.type === 'MW_WS_EVENT_MINI') {
      sendWsDebug('wsMini', {
        name: payload && payload[0],
        size: Array.isArray(payload[1]) ? payload[1].length : null
      });
    }
    sendWsEvent({ rawEvent: payload });
  }

  if (event.data.type === 'MW_WS_RECV') {
    const raw = event.data.data;
    if (typeof raw === 'string' && raw.startsWith('42')) {
      try {
        const arr = JSON.parse(raw.slice(2));
        if (Array.isArray(arr) && typeof arr[0] === 'string') {
          sendWsEvent({ rawEvent: arr });
          if (arr[0] === 'abilityUsage' && arr[1]) {
            const payload = arr[1];
            const eid = payload.id || payload.abilityId || `ev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
            sendBoostEvent({
              eventId: eid,
              user: payload.userAlias || payload.user || 'Unknown',
              ability: payload.name || payload.abilityName || payload.id || 'Ability',
              abilityId: payload.id,
              roundId: payload.roundId,
              logType: 'Ability',
              timestamp: Date.now()
            });
          }
        }
      } catch (_) {}
    }
  }

  if (event.data.type === 'MW_WS_META') {
    const payload = event.data.payload || {};
    sendWsDebug('wsMeta', payload);
  }

  if (event.data.type === 'MW_WS_RAW') {
    const payload = event.data.payload || {};
    sendWsDebug('wsRaw', payload);
  }

  if (event.data.type === 'MW_WS_NAME') {
    const payload = event.data.payload || {};
    sendWsDebug('wsName', payload);
  }

  if (event.data.type === 'MW_WS_ERROR') {
    const payload = event.data.payload || {};
    sendWsDebug('wsError', payload);
  }

  if (event.data.type === 'MW_ROUND_EVENT') {
    const payload = event.data.payload || {};
    const name = payload.name || 'round';
    const data = payload.data || {};
    sendBoostEvent({
      user: 'System',
      ability: name,
      abilityId: name,
      roundId: data.id || data.roundId,
      logType: 'Round',
      timestamp: Date.now()
    });
    if (name === 'winner') {
      scheduleWinnerClanCapture({ roundId: data.id || data.roundId, clanId: data.clanId });
    }
  }

  if (event.data.type === 'MW_ROUND_RESET') {
    sendBoostEvent({
      user: 'System',
      ability: 'Round Reset',
      abilityId: 'roundReset',
      logType: 'Round',
      timestamp: Date.now(),
      reset: true
    });
  }

  // Optional clan roster injection from page context
  if (event.data.type === 'MW_CLAN_ROSTER') {
    sendWsEvent({ action: 'rosterUpdate', roster: event.data.payload });
    try {
      chrome.runtime.sendMessage({ type: 'MW_ROSTER_FROM_CONTENT', data: { roster: event.data.payload } });
    } catch (_) {}
  }

  if (event.data.type === 'MW_HOOK_STATUS') {
    const payload = event.data.payload || {};
    const status = payload.status || 'unknown';
    sendWsDebug(`pageHook:${status}`, payload);
  }

  // DOM-scraped leaderboard data (comprehensive fallback)
  if (event.data.type === 'MW_DOM_LEADERBOARD') {
    console.log('[MW] Forwarding DOM leaderboard data:', event.data.clans?.length, 'clans');
    try {
      chrome.runtime.sendMessage({
        type: 'MW_DOM_LEADERBOARD',
        data: {
          clans: event.data.clans,
          timestamp: event.data.timestamp
        }
      });
    } catch (_) {}
    // Also send via port for real-time updates
    if (bgPort) {
      try {
        bgPort.postMessage({
          action: 'domLeaderboard',
          data: {
            clans: event.data.clans,
            timestamp: event.data.timestamp
          }
        });
      } catch (_) {}
    }
  }

  // API call monitoring from page-hook.js
  if (event.data.type === 'MW_API_CALL') {
    if (apiForwardDisabled) return;
    if (!chrome?.runtime?.id || !chrome?.runtime?.sendMessage) {
      apiForwardDisabled = true;
      return;
    }
    logApiSummary(event.data.payload?.url, 'Forwarding API call');
    try {
      chrome.runtime.sendMessage({
        type: 'MW_API_CALL',
        payload: event.data.payload
      });
    } catch (err) {
      const message = err?.message || String(err || '');
      if (message.includes('Extension context invalidated')) {
        apiForwardDisabled = true;
        return;
      }
      console.warn('[MW Content] Failed to forward API call:', err);
    }
  }
});

// Bridge: also listen for CustomEvents dispatched from page context
window.addEventListener('MW_WS_EVENT_BRIDGE', (e) => {
  try {
    const payload = e && e.detail;
    if (shouldThrottleWsEvent(payload && payload[0])) return;
    if (!shouldForwardWsEvent(payload && payload[0])) return;
    if (payload) sendWsEvent({ rawEvent: payload });
  } catch (_) {}
});

window.addEventListener('MW_ABILITY_USAGE_BRIDGE', (e) => {
  try {
    const payload = e && e.detail;
    if (!payload) return;
    console.log('[MW] BRIDGE abilityUsage received', payload);
    const eid = payload.id || payload.abilityId || `ev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    sendBoostEvent({
      eventId: eid,
      user: payload.userAlias || payload.user || 'Unknown',
      ability: payload.name || payload.abilityName || payload.id || 'Ability',
      abilityId: payload.id,
      roundId: payload.roundId,
      logType: 'Ability',
      timestamp: Date.now()
    });
  } catch (_) {}
});

const LIVE_SUMMARY_TIMER_XPATHS = [
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[3]/span',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[3]'
];
const LIVE_SUMMARY_ROUND_ID_XPATHS = [
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[2]/span',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[2]',
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[2]/text()",
  "//a[contains(@class,'miner-wars-main__info--left')]//span[contains(., ',')]"
];
const LIVE_SUMMARY_MULTIPLIER_XPATHS = [
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div/span',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div[contains(@class,"multiplier")]',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div/div[contains(., "x")]'
];
const LIVE_SUMMARY_MULTIPLIER_SELECTORS = [
  '.miner-wars-main__multiplier',
  '.miner-wars-main__multiplier span',
  '.miner-wars-main__info--left .miner-wars-main__multiplier span',
  '.miner-wars-main__info--left [class*="multiplier"]',
  '[class*="multiplier"] span',
  '[class*="multiplier"]'
];
const LIVE_SUMMARY_LEAGUE_XPATHS = [
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div[2]',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div[contains(@class,"league")]',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[2]/div[contains(@class,"miner-wars-main__league")]',
  '/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/miner-wars-user-panel/div[1]/div[1]/div/span',
  "//miner-wars-user-panel//div[contains(@class,'badge-game--alert')]/div[1]/div/span",
  "//div[contains(@class,'badge-game--alert') or contains(@class,'badge-game--alert-')]//span[contains(@class,'text-uppercase')]"
];
const LIVE_SUMMARY_LEAGUE_SELECTORS = [
  '.miner-wars-main__league',
  '.miner-wars-main__info--league',
  '.miner-wars-main__info--left .miner-wars-main__league',
  '.miner-wars-main__info--left .league',
  '.miner-wars-main__user-stats.badge-game--alert .text-uppercase',
  'miner-wars-user-panel .badge-game--alert span.text-uppercase',
  '.miner-wars-main__user-stats .text-uppercase',
  '.badge-game--alert span',
  '.badge-game--alert-odyssey span',
  '.badge-game--alert-eclipse span',
  '.badge-game--alert-horizon span'
];
const LEAGUE_TEXT_GUESS_REGEX = /\b(odyssey|eclipse|horizon|dune(?:\s+(?:[ivxlcdm]+|\d+))?)\b/i;
const LEAGUE_TEXT_NODE_SELECTORS = ['span', 'div', 'p', 'strong', 'h1', 'h2', 'b'];
const LIVE_SUMMARY_POLL_MS = 2000;
const LIVE_SUMMARY_FORCE_INTERVAL_MS = 7000;
const WINNER_MODAL_CLAN_NAME_XPATHS = [
  "/html/body/app-root/main/miner-wars/div/div/modal-container/div[2]/div/modal-round-finished/modal-game/modal/div/div[4]/div[1]/div[1]/participant-info/info-card/div/div[1]/div[2]/p",
  "//modal-round-finished//participant-info[1]//info-card//div[contains(@class,'participant-info__main')]//p[1]",
  "//modal-round-finished//participant-info[1]//info-card//div[contains(@class,'participant-info__main')]//p[contains(@class,'text-truncate')]",
  "//*[contains(translate(text(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'winner') and contains(translate(text(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'clan')]/following::node()[1]"
];
const WINNER_MODAL_CLAN_NAME_SELECTORS = [
  'modal-round-finished participant-info:nth-of-type(1) info-card .participant-info__main .fs-sm',
  'modal-round-finished participant-info:nth-of-type(1) info-card .participant-info__main .text-truncate',
  'participant-info info-card .participant-info__main p',
  'participant-info info-card .participant-info__main span',
  '.participant-info__main .text-truncate',
  '.participant-info__main p',
  '.participant-info__main span',
  '.modal-game .participant-info__main p',
  '.modal-game .text-truncate',
  'modal-round-finished participant-info info-card .participant-info__main p',
  'modal-round-finished .participant-info__main .text-truncate',
  'modal-round-finished info-card .participant-info__main span'
];
const WINNER_MODAL_PLAYER_NAME_SELECTORS = [
  'modal-round-finished participant-info:nth-of-type(2) info-card .participant-info__main p',
  'modal-round-finished participant-info:nth-of-type(2) info-card .participant-info__main .text-truncate',
  '.modal-round-finished participant-info:nth-of-type(2) .participant-info__main p',
  '.modal-round-finished participant-info:nth-of-type(2) .participant-info__main span',
  '.participant-info:nth-of-type(2) .participant-info__main .fs-sm',
  '.participant-info:nth-of-type(2) .participant-info__main .text-truncate'
];
const WINNER_MODAL_PLAYER_NAME_XPATHS = [
  "/html/body/app-root/main/miner-wars/div/div/modal-container/div[2]/div/modal-round-finished/modal-game/modal/div/div[4]/div[1]/div[2]/participant-info/info-card/div/div[1]/div[2]/p",
  "//modal-round-finished//participant-info[2]//info-card//div[contains(@class,'participant-info__main')]//p[1]",
  "//modal-round-finished//participant-info[2]//info-card//div[contains(@class,'participant-info__main')]//p[contains(@class,'text-truncate')]"
];
const WINNER_CLAN_POLL_INTERVAL_MS = 400;
const WINNER_CLAN_MAX_WAIT_MS = 15000;
const WINNER_MODAL_FALLBACK_INTERVAL_MS = 1500;
let liveSummaryTimerHandle = null;
let liveSummaryCache = {
  timerText: null,
  roundId: null,
  multiplierText: null,
  leagueText: null,
  leagueId: null
};
let liveSummaryLastBroadcast = 0;

  function getTextByXPathsQuiet(xpaths, contextNode = document) {
    if (!Array.isArray(xpaths)) return '';
    const doc = (contextNode && contextNode.ownerDocument) || document;
    for (const xpath of xpaths) {
    if (!xpath) continue;
    try {
      const result = doc.evaluate(xpath, contextNode || doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      const node = result && result.singleNodeValue;
      if (node && node.textContent) {
        const text = node.textContent.trim();
        if (text) return text;
      }
    } catch (_) {
      // ignore
    }
  }
  return '';
}

function getTimerTextByXPaths(xpaths, contextNode = document) {
  if (!Array.isArray(xpaths)) return '';
  const doc = (contextNode && contextNode.ownerDocument) || document;
  for (const xpath of xpaths) {
    if (!xpath) continue;
    try {
      const result = doc.evaluate(xpath, contextNode || doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      const node = result && result.singleNodeValue;
      if (!node || !node.textContent) continue;
      const text = node.textContent.trim();
      if (!text) continue;
      if (/\d{1,2}:\d{2}(?::\d{2})?/.test(text)) return text;
    } catch (_) {
      // ignore
    }
  }
  return '';
}

function extractNumericToken(text) {
  if (!text) return '';
  const matches = String(text).match(/\d[\d,\.]{4,}/g);
  if (!matches || !matches.length) return '';
  return matches[matches.length - 1];
}

function extractBlockIdToken(text) {
  if (!text) return '';
  const match = String(text).match(/\b\d{6,7}\b/);
  return match ? match[0] : '';
}

function extractRoundFromBlockLabel() {
  try {
    const labelXpath = "//*[contains(translate(normalize-space(text()), 'BLOCK', 'block'), 'block') and contains(translate(normalize-space(text()), 'ID', 'id'), 'id')]";
    const result = document.evaluate(labelXpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (let i = 0; i < result.snapshotLength; i += 1) {
      const node = result.snapshotItem(i);
      if (!node) continue;
      const container = node.parentElement || node;
      const containerToken = extractBlockIdToken(container.textContent || '');
      if (containerToken) return containerToken;
      const nextToken = extractBlockIdToken(node.nextElementSibling?.textContent || '');
      if (nextToken) return nextToken;
      const parentToken = extractBlockIdToken(container.parentElement?.textContent || '');
      if (parentToken) return parentToken;
    }
  } catch (_) {
    // ignore
  }
  return '';
}

function extractRoundFromTimerArea() {
  for (const xpath of LIVE_SUMMARY_TIMER_XPATHS) {
    const node = evaluateXPath(xpath);
    if (!node) continue;
    let current = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
    let hops = 0;
    while (current && hops < 5) {
      const text = (current.textContent || '').trim();
      if (text) {
        const labeled = text.match(/round[^0-9]{0,10}(\d[\d,\.]{4,})/i);
        if (labeled && labeled[1]) return labeled[1];
        if (/round/i.test(text)) {
          const token = extractNumericToken(text);
          if (token) return token;
        }
      }
      current = current.parentElement;
      hops += 1;
    }
  }
  return '';
}

function extractRoundFromInfoPanel() {
  const infoAnchor = document.querySelector('a.miner-wars-main__info--left');
  if (!infoAnchor) return '';
  const spans = infoAnchor.querySelectorAll('span');
  for (const span of spans) {
    if (!span || !span.textContent) continue;
    if (span.classList.contains('miner-wars-main__timer')) continue;
    const text = span.textContent.trim();
    if (!text) continue;
    const token = extractNumericToken(text);
    if (token) return token;
  }
  return '';
}

function extractLiveRoundIdText() {
  const xpathValue = getTextByXPathsQuiet(LIVE_SUMMARY_ROUND_ID_XPATHS, document);
  if (xpathValue) return xpathValue;
  const blockLabelValue = extractRoundFromBlockLabel();
  if (blockLabelValue) return blockLabelValue;
  const infoPanelValue = extractRoundFromInfoPanel();
  if (infoPanelValue) return infoPanelValue;
  const timerAreaValue = extractRoundFromTimerArea();
  if (timerAreaValue) return timerAreaValue;
  const gameRoot = document.querySelector('miner-wars-game');
  if (!gameRoot) return '';
  const selectors = ['span', 'div', 'p', 'strong', 'h1', 'h2', 'b'];
  const candidates = gameRoot.querySelectorAll(selectors.join(','));
  let bestText = '';
  let bestWeight = 0;
  candidates.forEach((node) => {
    if (!node || !node.textContent) return;
    const text = node.textContent.trim();
    if (!text) return;
    const digits = text.replace(/[^\d]/g, '');
    if (digits.length < 5) return;
    if (/[a-zA-Z]/.test(text) && !/round/i.test(text)) return;
    const weight = digits.length;
    if (weight > bestWeight) {
      bestWeight = weight;
      bestText = text;
    }
  });
  return bestText;
}

function normalizeRoundIdentifier(value) {
  if (value === null || typeof value === 'undefined') return null;
  if (typeof value === 'number') return String(value);
  const text = String(value).trim();
  if (!text) return null;
  const dominant = extractDominantNumericId(text);
  return dominant || text;
}

function extractWinnerClanNameFromModal() {
  return extractWinnerClanNameWithMeta().clanName;
}

function extractWinnerClanNameWithMeta(rootOverride) {
  const meta = { clanName: '', rootFound: false, modalRoot: null };
  const modalRoot = rootOverride || findWinnerModalRoot(rootOverride);
  if (modalRoot) meta.rootFound = true;
  meta.modalRoot = modalRoot || null;
  if (modalRoot) {
    enrichWinnerMeta(meta, modalRoot);
  }
  const scopedNode = modalRoot || rootOverride || null;
  const completeWith = (value) => {
    if (!value) return false;
    meta.clanName = value;
    meta.roundId = resolveModalRoundId(modalRoot);
    return true;
  };

  const selectorScopes = [];
  if (scopedNode) {
    selectorScopes.push({
      scope: scopedNode,
      selectors: WINNER_MODAL_CLAN_NAME_SELECTORS.filter((sel) => sel && !/modal-round-finished/.test(sel))
    });
  }
  selectorScopes.push({ scope: document, selectors: WINNER_MODAL_CLAN_NAME_SELECTORS });
  for (const { scope, selectors } of selectorScopes) {
    if (!scope) continue;
    for (const selector of selectors) {
      if (!selector) continue;
      const node = querySelectorDeep(selector, scope);
      if (node && node.textContent) {
        const cleaned = normalizeModalText(node.textContent);
        if (cleaned && completeWith(cleaned)) return meta;
      }
    }
  }

  const xpathScopes = [];
  if (scopedNode) xpathScopes.push(scopedNode);
  xpathScopes.push(document);
  for (const scope of xpathScopes) {
    if (!scope) continue;
    for (const xpath of WINNER_MODAL_CLAN_NAME_XPATHS) {
      if (!xpath) continue;
      const context = xpath.startsWith('/') ? document : scope;
      const value = textFromXPath(xpath, context);
      if (value) {
        const cleaned = normalizeModalText(value);
        if (cleaned && completeWith(cleaned)) return meta;
      }
    }
  }

  if (!modalRoot) {
    meta.roundId = resolveModalRoundId(null);
    return meta;
  }

  const cardDriven = extractWinnerClanFromParticipantCards(modalRoot);
  if (cardDriven && completeWith(cardDriven)) return meta;

  const labelDriven = extractWinnerClanByLabel(modalRoot);
  if (labelDriven && completeWith(labelDriven)) return meta;

  const heuristic = extractWinnerClanByHeuristics(modalRoot);
  if (heuristic && completeWith(heuristic)) return meta;

  const textFallback = extractWinnerClanFromModalText(modalRoot);
  if (textFallback && completeWith(textFallback)) return meta;

  meta.roundId = resolveModalRoundId(modalRoot);
  return meta;
}

function resolveModalRoundId(modalRoot) {
  const candidateFromDom = liveSummaryCache?.domRoundId || liveSummaryCache?.roundId || null;
  if (!modalRoot) return candidateFromDom;
  try {
    const headerText = modalRoot.querySelector('.modal-header')?.innerText || '';
    const headerId = extractDominantNumericId(headerText);
    if (headerId && headerId.length >= 5) return headerId;
    const globalText = modalRoot.innerText || '';
    const globalId = extractDominantNumericId(globalText);
    if (globalId && globalId.length >= 5) return globalId;
  } catch (_) {}
  return candidateFromDom;
}

function extractWinnerClanFromModalText(modalRoot) {
  const fullText = getDeepInnerText(modalRoot);
  if (!fullText) return '';
  const section = extractSectionBetween(fullText, /WINNING\s*CLAN/i, /WINNING\s*PLAYER/i);
  if (section) {
    const candidate = pickBestLine(section);
    if (candidate) return candidate;
  }
  return pickBestLine(fullText);
}

function parseWinnerSectionStats(sectionText = '') {
  const normalized = (sectionText || '').replace(/\r/g, '\n');
  const lines = normalized
    .split(/\n+/)
    .map((line) => normalizeModalText(line))
    .filter((line) => line && !/winning\s+(clan|player)/i.test(line));
  const numericValue = (regex) => {
    const match = normalized.match(regex);
    if (!match) return null;
    const parsed = Number(match[1].replace(/,/g, ''));
    return Number.isFinite(parsed) ? parsed : null;
  };
  let name = lines.find((line) => {
    if (!line) return false;
    if (/TH\/s/i.test(line)) return false;
    if (/%/.test(line)) return false;
    if (/BTC|GMT|Sent\s+to\s+reward/i.test(line)) return false;
    return /[A-Za-z]/.test(line);
  }) || null;
  const th = numericValue(/([\d.,]+)\s*TH\/s/i);
  const pct = numericValue(/([\d.,]+)\s*%/i);
  const rewardMatch = normalized.match(/([\d.,]+)\s*(GMT|BTC)/i);
  const rewardValue = rewardMatch ? Number(rewardMatch[1].replace(/,/g, '')) : null;
  const rewardUnit = rewardMatch ? rewardMatch[2].toUpperCase() : null;
  const rewardText = rewardMatch ? `${rewardMatch[1]} ${rewardUnit}` : null;
  return { name, th, pct, rewardValue, rewardUnit, rewardText };
}

function extractWinnerStatsFromModal(modalRoot) {
  if (!modalRoot) return {};
  const fullText = getDeepInnerText(modalRoot);
  const clanSection = extractSectionBetween(fullText || '', /WINNING\s*CLAN/i, /WINNING\s*PLAYER/i);
  const playerSection = extractSectionBetween(fullText || '', /WINNING\s*PLAYER/i, /SENT\s+TO\s+REWARD/i);
  const clanStats = parseWinnerSectionStats(clanSection);
  const playerStats = parseWinnerSectionStats(playerSection);
  const playerNameFromSelectors = getTextBySelectors(WINNER_MODAL_PLAYER_NAME_SELECTORS, modalRoot);
  const playerNameFromXPath = getTextByXPathsQuiet(WINNER_MODAL_PLAYER_NAME_XPATHS, modalRoot);
  if (!playerStats.name) {
    playerStats.name = normalizeModalText(playerNameFromSelectors || playerNameFromXPath || '') || playerStats.name;
  }
  return {
    clanNameCandidate: clanStats.name || null,
    clanTh: clanStats.th,
    clanPct: clanStats.pct,
    playerName: playerStats.name || null,
    playerTh: playerStats.th,
    playerPct: playerStats.pct,
    playerRewardValue: playerStats.rewardValue || null,
    playerRewardUnit: playerStats.rewardUnit || null,
    playerRewardText: playerStats.rewardText || null
  };
}

function enrichWinnerMeta(meta, modalRoot) {
  if (!meta || !modalRoot) return;
  const stats = extractWinnerStatsFromModal(modalRoot);
  if (!stats) return;
  if (stats.clanNameCandidate && !meta.clanName) {
    meta.clanName = stats.clanNameCandidate;
  }
  if (stats.clanTh !== null && typeof stats.clanTh !== 'undefined') {
    meta.clanTh = stats.clanTh;
  }
  if (stats.clanPct !== null && typeof stats.clanPct !== 'undefined') {
    meta.clanPct = stats.clanPct;
  }
  if (stats.playerName) {
    meta.playerName = stats.playerName;
  }
  if (stats.playerTh !== null && typeof stats.playerTh !== 'undefined') {
    meta.playerTh = stats.playerTh;
  }
  if (stats.playerPct !== null && typeof stats.playerPct !== 'undefined') {
    meta.playerPct = stats.playerPct;
  }
  if (stats.playerRewardText) {
    meta.playerRewardText = stats.playerRewardText;
  }
  if (typeof stats.playerRewardValue !== 'undefined' && stats.playerRewardValue !== null) {
    meta.playerRewardValue = stats.playerRewardValue;
  }
  if (stats.playerRewardUnit) {
    meta.playerRewardUnit = stats.playerRewardUnit;
  }
}

function extractSectionBetween(text, startRegex, endRegex) {
  const startMatch = text.match(startRegex);
  if (!startMatch) return '';
  const startIndex = text.search(startRegex);
  const afterStart = text.slice(startIndex + startMatch[0].length);
  const endMatch = afterStart.search(endRegex);
  if (endMatch === -1) return afterStart;
  return afterStart.slice(0, endMatch);
}

function pickBestLine(text) {
  const lines = text
    .split(/\n+/)
    .map((line) => normalizeModalText(line))
    .filter((line) => line && !/winning\s+(clan|player)/i.test(line));
  for (const line of lines) {
    if (isModalStatLine(line)) continue;
    if (!/[A-Za-z]/.test(line)) continue;
    return line;
  }
  return '';
}

function extractWinnerClanByLabel(modalRoot) {
  const nodes = collectElementsDeep(modalRoot, ['p', 'span', 'div', 'h1', 'h2', 'h3', 'strong']);
  let inSection = false;
  for (const node of nodes) {
    const text = normalizeModalText(node.textContent);
    if (!text) continue;
    if (/winning\s+clan/i.test(text)) {
      inSection = true;
      continue;
    }
    if (/winning\s+player/i.test(text)) break;
    if (!inSection) continue;
    if (isModalStatLine(text)) continue;
    if (!/[A-Za-z]/.test(text)) continue;
    return text;
  }
  return '';
}

function extractWinnerClanFromParticipantCards(modalRoot) {
  if (!modalRoot) return '';
  const scope = modalRoot || document;
  const selectors = [
    'participant-info info-card .participant-info__main p',
    '.participant-info__main p',
    '.participant-info__main .text-truncate',
    'participant-info .participant-info__main p'
  ];
  for (const selector of selectors) {
    const node = querySelectorDeep(selector, scope);
    if (!node || !node.textContent) continue;
    const text = normalizeModalText(node.textContent);
    if (!text) continue;
    if (/winning\s+(clan|player)/i.test(text)) continue;
    if (isModalStatLine(text)) continue;
    if (WINNER_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text))) continue;
    if (!/[A-Za-z]/.test(text)) continue;
    return text;
  }
  return '';
}

function extractWinnerClanByHeuristics(modalRoot) {
  const candidates = [];
  const nodes = collectElementsDeep(modalRoot, ['p', 'span', 'div']);
  for (const node of nodes) {
    const text = normalizeModalText(node.textContent);
    if (!text) continue;
    if (/winning\s+(clan|player)/i.test(text)) continue;
    if (isModalStatLine(text)) continue;
    if (!/[A-Za-z]/.test(text)) continue;
    candidates.push({ text, weight: text.length });
  }
  candidates.sort((a, b) => b.weight - a.weight);
  return candidates.length ? candidates[0].text : '';
}

function normalizeModalText(text) {
  return (text || '').replace(/[\u200B-\u200D\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
}

function isModalStatLine(text) {
  if (!text) return true;
  if (/sent\s+to\s+reward/i.test(text)) return true;
  if (/continue\s+game/i.test(text)) return true;
  if (/^[-+]?[\d,\.\s%]+$/.test(text) && !/[A-Za-z]/.test(text)) return true;
  return false;
}

function querySelectorDeep(selector, root = document) {
  const queue = [root];
  const visited = new Set();
  while (queue.length) {
    const current = queue.shift();
    if (!current || visited.has(current)) continue;
    visited.add(current);
    if (current.querySelector) {
      try {
        const found = current.querySelector(selector);
        if (found) return found;
      } catch (_) {}
    }
    const children = getShadowDescendants(current);
    for (const child of children) {
      queue.push(child);
    }
  }
  return null;
}

function findWinnerModalRoot(startNode) {
  const selectors = [
    '.modal-game__container',
    '.modal-game.modal-round-finished',
    '.modal-game',
    'modal-round-finished',
    '.modal-round-finished'
  ];
  const searchRoots = [];
  if (startNode) {
    if (startNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) searchRoots.push(startNode);
    if (startNode.nodeType === Node.ELEMENT_NODE) {
      searchRoots.push(startNode);
      if (startNode.shadowRoot) searchRoots.push(startNode.shadowRoot);
      const closest = startNode.closest('.modal-game, .modal-game__container, modal-round-finished');
      if (closest) {
        searchRoots.push(closest);
        if (closest.shadowRoot) searchRoots.push(closest.shadowRoot);
      }
    }
  }
  searchRoots.push(document);
  if (document.body && document.body.shadowRoot) searchRoots.push(document.body.shadowRoot);
  for (const root of searchRoots) {
    if (!root || !root.querySelector) continue;
    for (const sel of selectors) {
      const node = root.querySelector(sel);
      if (node) {
        if (node.shadowRoot) {
          const body = node.shadowRoot.querySelector('.modal-body, .modal-content');
          if (body) return body;
        }
        const body = node.querySelector('.modal-body, .modal-content');
        return body || node;
      }
    }
  }
  const container = querySelectorDeep('modal-container');
  if (container) {
    const root = container.shadowRoot || container;
    const body = root.querySelector('.modal-body, .modal-content');
    if (body) return body;
    return root;
  }
  return null;
}

function getShadowDescendants(node) {
  const list = [];
  const elements = node.querySelectorAll ? node.querySelectorAll('*') : [];
  elements.forEach((el) => {
    if (el.shadowRoot) list.push(el.shadowRoot);
  });
  if (node.shadowRoot) list.push(node.shadowRoot);
  return list;
}

function collectElementsDeep(root, tagNames) {
  const targets = new Set(tagNames.map((name) => name.toUpperCase()));
  const results = [];
  traverseDomDeep(root, (node) => {
    if (node.nodeType === Node.ELEMENT_NODE && targets.has(node.tagName)) {
      results.push(node);
    }
  });
  return results;
}

function getDeepInnerText(root) {
  const chunks = [];
  traverseDomDeep(root, (node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      const value = (node.textContent || '').trim();
      if (value) chunks.push(value);
    }
  });
  return chunks.join('\n');
}

function traverseDomDeep(startNode, callback) {
  const visited = new Set();
  const queue = [startNode];
  while (queue.length) {
    const node = queue.shift();
    if (!node || visited.has(node)) continue;
    visited.add(node);
    callback(node);
    const shadow = node.shadowRoot;
    if (shadow) queue.push(shadow);
    const childNodes = node.childNodes ? Array.from(node.childNodes).filter((n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.DOCUMENT_FRAGMENT_NODE || n.nodeType === Node.TEXT_NODE) : [];
    queue.push(...childNodes);
  }
}

function reportWinnerClanName(payload) {
  if (!payload) return;
  console.log('[MW] reportWinnerClanName', payload.roundId, payload.clanName, payload.clanId);
  try {
    if (bgPort) {
      bgPort.postMessage({ action: 'winnerClanName', data: payload });
    }
  } catch (_) {}
  try {
    chrome.runtime.sendMessage({ type: 'MW_WINNER_CLAN_NAME', data: payload });
  } catch (_) {}
}

const pendingWinnerClanWatchers = new Map();
let winnerModalObserver = null;
let lastModalCaptureKey = null;
const winnerModalTextWatchers = new WeakMap();
let winnerModalFallbackHandle = null;

function scheduleWinnerClanCapture(metadata = {}) {
  const roundIdRaw = metadata.roundId || metadata.id || liveSummaryCache.roundId || null;
  const normalizedRoundId = normalizeRoundIdentifier(roundIdRaw);
  if (!normalizedRoundId) {
    console.warn('[MW] Winner modal schedule skipped (no round id)', metadata);
    return;
  }
  console.log('[MW] scheduleWinnerClanCapture', normalizedRoundId, metadata);
  if (pendingWinnerClanWatchers.has(normalizedRoundId)) {
    const existing = pendingWinnerClanWatchers.get(normalizedRoundId);
    if (!existing.clanId && metadata.clanId) {
      existing.clanId = metadata.clanId;
    }
    return;
  }
  const watcher = {
    clanId: metadata.clanId || null,
    timeoutId: null,
    deadline: Date.now() + WINNER_CLAN_MAX_WAIT_MS,
    attempts: 0,
    loggedNoModal: false
  };
  console.log('[MW] Winner modal watcher started', normalizedRoundId);
  const poll = () => {
    watcher.attempts += 1;
    const result = extractWinnerClanNameWithMeta();
    const clanName = result.clanName;
    if (!result.rootFound && !watcher.loggedNoModal) {
      console.log('[MW] Winner modal root not ready', normalizedRoundId);
      watcher.loggedNoModal = true;
    }
    if (clanName && !isPlaceholderWinner(clanName)) {
      console.log('[MW] Winner modal captured', normalizedRoundId, clanName);
      reportWinnerClanName({
        roundId: result.roundId || liveSummaryCache?.domRoundId || liveSummaryCache?.roundId || normalizedRoundId,
        upstreamRoundId: normalizedRoundId,
        clanId: watcher.clanId || null,
        clanName: clanName.trim(),
        timestamp: Date.now()
      });
      if (watcher.timeoutId) {
        clearTimeout(watcher.timeoutId);
      }
      pendingWinnerClanWatchers.delete(normalizedRoundId);
      return;
    }
    if (Date.now() >= watcher.deadline) {
      console.warn('[MW] Winner modal capture timed out', normalizedRoundId);
      pendingWinnerClanWatchers.delete(normalizedRoundId);
      return;
    }
    watcher.timeoutId = window.setTimeout(poll, WINNER_CLAN_POLL_INTERVAL_MS);
  };
  pendingWinnerClanWatchers.set(normalizedRoundId, watcher);
  poll();
}

function stopWinnerModalTextWatcher(modalRoot) {
  if (!modalRoot) return;
  const entry = winnerModalTextWatchers.get(modalRoot);
  if (!entry) return;
  if (entry.timeoutId) {
    clearTimeout(entry.timeoutId);
  }
  try {
    entry.observer?.disconnect();
  } catch (_) {}
  winnerModalTextWatchers.delete(modalRoot);
}

function emitWinnerClanFromMeta(meta, options = {}) {
  if (!meta || !meta.clanName || isPlaceholderWinner(meta.clanName)) {
    return false;
  }
  const clanName = meta.clanName.trim();
  if (!clanName) return false;
  const roundId =
    normalizeRoundIdentifier(
      meta.roundId ||
        options.roundIdHint ||
        liveSummaryCache?.domRoundId ||
        liveSummaryCache?.roundId ||
        null
    ) || null;
  const dedupeKey = `${roundId || 'n/a'}|${clanName}`;
  if (lastModalCaptureKey === dedupeKey) {
    if (options.modalRoot) {
      stopWinnerModalTextWatcher(options.modalRoot);
    }
    return false;
  }
  lastModalCaptureKey = dedupeKey;
  reportWinnerClanName({
    roundId,
    upstreamRoundId: options.roundIdHint || options.sourceRoundId || null,
    clanId: options.clanId || null,
    clanName,
    clanTh: typeof meta.clanTh !== 'undefined' ? meta.clanTh : null,
    clanPct: typeof meta.clanPct !== 'undefined' ? meta.clanPct : null,
    playerName: meta.playerName || null,
    playerTh: typeof meta.playerTh !== 'undefined' ? meta.playerTh : null,
    playerPct: typeof meta.playerPct !== 'undefined' ? meta.playerPct : null,
    playerRewardText: meta.playerRewardText || null,
    playerRewardValue: typeof meta.playerRewardValue !== 'undefined' ? meta.playerRewardValue : null,
    playerRewardUnit: meta.playerRewardUnit || null,
    timestamp: Date.now(),
    source: options.source || 'modal-observer'
  });
  if (options.modalRoot) {
    stopWinnerModalTextWatcher(options.modalRoot);
  }
  return true;
}

function ensureWinnerModalTextWatcher(modalRoot, options = {}) {
  if (!modalRoot || typeof MutationObserver === 'undefined') return;
  if (winnerModalTextWatchers.has(modalRoot)) return;
  console.log('[MW] Winner modal awaiting clan name text', options.roundIdHint || '');
  const observer = new MutationObserver(() => {
    const latestMeta = extractWinnerClanNameWithMeta(modalRoot);
    if (emitWinnerClanFromMeta(latestMeta, { ...options, modalRoot })) {
      stopWinnerModalTextWatcher(modalRoot);
    }
  });
  observer.observe(modalRoot, { childList: true, characterData: true, subtree: true });
  const timeoutId = window.setTimeout(() => {
    console.warn('[MW] Winner modal text watcher timed out', options.roundIdHint || 'unknown');
    stopWinnerModalTextWatcher(modalRoot);
  }, options.timeoutMs || WINNER_CLAN_MAX_WAIT_MS);
  winnerModalTextWatchers.set(modalRoot, { observer, timeoutId });
}

function startWinnerModalObserver() {
  if (winnerModalObserver || typeof MutationObserver === 'undefined') return;
  winnerModalObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (!mutation.addedNodes || !mutation.addedNodes.length) return;
      mutation.addedNodes.forEach((node) => {
        if (!(node instanceof Element)) return;
        if (
          node.matches('.modal-game, .modal-game__container, modal-round-finished, .modal-round-finished') ||
          node.querySelector('.modal-game, .modal-game__container, modal-round-finished, .modal-round-finished')
        ) {
          handleWinnerModalCandidate(node);
        }
      });
    });
  });
  winnerModalObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
}

function startWinnerModalFallbackLoop() {
  if (winnerModalFallbackHandle) return;
  winnerModalFallbackHandle = window.setInterval(() => {
    try {
      const meta = extractWinnerClanNameWithMeta();
      if (!meta?.modalRoot) return;
      if (!meta?.clanName || isPlaceholderWinner(meta.clanName)) return;
      emitWinnerClanFromMeta(meta, { source: 'modal-fallback', roundIdHint: meta?.roundId, modalRoot: meta.modalRoot });
    } catch (err) {
      console.warn('[MW] Winner modal fallback error', err);
    }
  }, WINNER_MODAL_FALLBACK_INTERVAL_MS);
}

function handleWinnerModalCandidate(node) {
  try {
    const root = findWinnerModalRoot(node);
    if (!root) return;
    let meta = extractWinnerClanNameWithMeta(root);
    if (meta?.modalRoot && meta.modalRoot !== root && (!meta?.clanName || isPlaceholderWinner(meta.clanName))) {
      const fallbackMeta = extractWinnerClanNameWithMeta(meta.modalRoot);
      if (fallbackMeta?.clanName && !isPlaceholderWinner(fallbackMeta.clanName)) {
        meta = fallbackMeta;
      } else if (fallbackMeta) {
        meta.modalRoot = fallbackMeta.modalRoot || meta.modalRoot;
        if (!meta.roundId && fallbackMeta.roundId) {
          meta.roundId = fallbackMeta.roundId;
        }
      }
    }
    const modalRoot = meta.modalRoot || root;
    const roundIdHint =
      meta?.roundId || liveSummaryCache?.domRoundId || liveSummaryCache?.roundId || null;
    if (emitWinnerClanFromMeta(meta, { modalRoot, source: 'modal-observer', roundIdHint })) {
      return;
    }
    if (modalRoot) {
      ensureWinnerModalTextWatcher(modalRoot, {
        roundIdHint,
        source: 'modal-text-watcher'
      });
    }
  } catch (err) {
    console.warn('[MW] Failed to capture modal winner', err);
  }
}

function parseTimerToSeconds(text) {
  if (!text) return null;
  const cleaned = String(text).trim();
  const match = cleaned.match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/);
  if (!match) return null;
  const parts = match.slice(1).filter(Boolean).map((part) => Number(part));
  if (!parts.every((n) => Number.isFinite(n))) return null;
  if (parts.length === 2) {
    return parts[0] * 60 + parts[1];
  }
  if (parts.length === 3) {
    return parts[0] * 3600 + parts[1] * 60 + parts[2];
  }
  return null;
}

function pollLiveSummaryFromDom() {
  try {
    let changed = false;
    const payload = {};
    const timerTextRaw = getTimerTextByXPaths(LIVE_SUMMARY_TIMER_XPATHS, document);
    const timerText = (timerTextRaw || '').replace(/\s+/g, ' ').trim();
    if (timerText && timerText !== liveSummaryCache.timerText) {
      liveSummaryCache.timerText = timerText;
      payload.timerText = timerText;
      payload.timerSeconds = parseTimerToSeconds(timerText);
      changed = true;
    }
    const roundIdText = extractLiveRoundIdText();
    const roundIdValue = extractDominantNumericId(roundIdText);
    if (roundIdValue && roundIdValue !== liveSummaryCache.roundId) {
      liveSummaryCache.roundId = roundIdValue;
      payload.roundIdValue = roundIdValue;
      payload.roundIdText = (roundIdText || '').trim() || roundIdValue;
      changed = true;
    }
    const multiplierRaw = typeof extractLiveMultiplierText === 'function' ? extractLiveMultiplierText() : '';
    const multiplierText = normalizeMultiplierText(multiplierRaw);
    if (multiplierText && multiplierText !== liveSummaryCache.multiplierText) {
      liveSummaryCache.multiplierText = multiplierText;
      payload.multiplier = multiplierText;
      payload.multiplierValue = parseInt(multiplierText, 10) || null;
      changed = true;
    }
    const leagueRaw = extractLiveLeagueText();
    const leagueText = leagueRaw ? leagueRaw.replace(/\s+/g, ' ').trim() : '';
    const leagueId = parseLeagueIdFromText(leagueText);
    if (Number.isFinite(leagueId) && leagueId !== liveSummaryCache.leagueId) {
      liveSummaryCache.leagueId = leagueId;
      liveSummaryCache.leagueText = leagueText || null;
      payload.league = leagueId;
      payload.leagueId = leagueId;
      changed = true;
    } else if (leagueText && leagueText !== liveSummaryCache.leagueText) {
      liveSummaryCache.leagueText = leagueText;
      payload.league = leagueText;
      changed = true;
    }
    const now = Date.now();
    if (!changed && now - liveSummaryLastBroadcast >= LIVE_SUMMARY_FORCE_INTERVAL_MS) {
      if (!payload.timerText && liveSummaryCache.timerText) {
        payload.timerText = liveSummaryCache.timerText;
        payload.timerSeconds = parseTimerToSeconds(liveSummaryCache.timerText);
      }
      if (!payload.roundIdValue && liveSummaryCache.roundId) {
        payload.roundIdValue = liveSummaryCache.roundId;
        payload.roundIdText = liveSummaryCache.roundId;
      }
      if (!payload.multiplier && liveSummaryCache.multiplierText) {
        payload.multiplier = liveSummaryCache.multiplierText;
        payload.multiplierValue = parseInt(liveSummaryCache.multiplierText, 10) || null;
      }
      if (!payload.league && Number.isFinite(liveSummaryCache.leagueId)) {
        payload.league = liveSummaryCache.leagueId;
        payload.leagueId = liveSummaryCache.leagueId;
      } else if (!payload.league && liveSummaryCache.leagueText) {
        payload.league = liveSummaryCache.leagueText;
      }
      if (payload.timerText || payload.roundIdValue) {
        changed = true;
      }
    }
    if (changed) {
      liveSummaryLastBroadcast = now;
      sendLiveSummaryState(payload);
    }
  } catch (err) {
    console.warn('[MW] Failed to poll live summary timer', err);
  }
}

function startLiveSummaryPolling() {
  if (liveSummaryTimerHandle) return;
  pollLiveSummaryFromDom();
  liveSummaryTimerHandle = window.setInterval(() => {
    try {
      pollLiveSummaryFromDom();
    } catch (err) {
      console.warn('[MW Redesign] live summary poll failed, clearing interval:', err?.message);
      clearInterval(liveSummaryTimerHandle);
      liveSummaryTimerHandle = null;
    }
  }, LIVE_SUMMARY_POLL_MS);
}

startLiveSummaryPolling();
startWinnerModalObserver();
startWinnerModalFallbackLoop();

window.addEventListener('MW_ROUND_EVENT_BRIDGE', (e) => {
  try {
    const payload = e && e.detail;
    const name = payload?.name || 'round';
    const data = payload?.data || {};
    sendBoostEvent({
      user: 'System',
      ability: name,
      abilityId: name,
      roundId: data.id || data.roundId,
      logType: 'Round',
      timestamp: Date.now()
    });
    if (name === 'winner' || name === 'winnerNftId') {
      scheduleWinnerClanCapture({ roundId: data.id || data.roundId, clanId: data.clanId });
    }
  } catch (_) {}
});

window.addEventListener('MW_ROUND_RESET_BRIDGE', () => {
  sendBoostEvent({
    user: 'System',
    ability: 'Round Reset',
    abilityId: 'roundReset',
    logType: 'Round',
    timestamp: Date.now(),
    reset: true
  });
});

window.addEventListener('MW_CLAN_ROSTER_BRIDGE', (e) => {
  try {
    const payload = e && e.detail;
    if (payload) {
      sendWsEvent({ action: 'rosterUpdate', roster: payload });
    }
  } catch (_) {}
});

let latestAllUsersState = null;

function sanitizeText(value) {
  if (!value) return '';
  return value.replace(/\s+/g, ' ').trim();
}

function guessLeagueTextFromRoot(root) {
  if (!root) return '';
  const selector = LEAGUE_TEXT_NODE_SELECTORS.join(',');
  const nodes = selector ? root.querySelectorAll(selector) : [];
  for (const node of nodes) {
    if (!node) continue;
    const text = sanitizeText(node.textContent);
    if (!text) continue;
    const match = text.match(LEAGUE_TEXT_GUESS_REGEX);
    if (match && match[0]) {
      const league = match[0];
      return league.length === text.length ? text : league;
    }
  }
  return '';
}

const PLACEHOLDER_CLAN_NAME_PATTERNS = [/gomining\s*-\s*personal account/i];
const WINNER_PLACEHOLDER_PATTERNS = [
  /^determining\s+the\s+winners?\.{0,}$/i,
  /^awaiting\s+the\s+next\s+round\.{0,}$/i,
  /^awaiting\s+(?:winner|winners|results)\.?$/i,
  /^waiting\s+for\s+(?:results|winners|leaderboard)\.?$/i,
  /^winner\s+determining\.?$/i,
  /^winner\s+calculating\.?$/i,
  /^awaiting\s+.*leaderboard\.?$/i,
  /^loading\.{0,}$/i,
  /^mapping\s+throttled\s+for\s+round\s+finished!?$/i
];
const WINNER_PLACEHOLDER_LITERALS = new Set([
  'round finished',
  'winner round finished',
  'mapping throttled for round finished',
  'winner determining the winners',
  'determining the winners'
]);
const WINNER_PLACEHOLDER_PHRASES = [
  'round finished',
  'determining the winners',
  'awaiting the next round',
  'awaiting the winners',
  'awaiting winners',
  'awaiting leaderboard',
  'waiting for leaderboard',
  'mapping throttled',
  'winner determining',
  'winner calculating',
  'loading'
];

function getTextBySelectorsQuiet(selectors, contextNode = document) {
  const list = Array.isArray(selectors) ? selectors : [selectors];
  for (const selector of list) {
    try {
      const node = contextNode.querySelector(selector);
      if (node && node.textContent) {
        return node.textContent.trim();
      }
    } catch (_) {
      // ignore
    }
  }
  return '';
}

function normalizeMultiplierText(raw) {
  if (!raw) return '';
  const text = String(raw).replace(/\s+/g, ' ').trim();
  if (!text) return '';
  if (text.includes(':')) return '';
  const match = text.match(/(\d+)/);
  if (match) return `${match[1]}x`;
  return text;
}

function extractLiveMultiplierText() {
  const fromXPath = getTextByXPathsQuiet(LIVE_SUMMARY_MULTIPLIER_XPATHS, document);
  if (fromXPath) return fromXPath;
  for (const selector of LIVE_SUMMARY_MULTIPLIER_SELECTORS) {
    const nodes = document.querySelectorAll(selector);
    for (const node of nodes) {
      const text = (node.textContent || '').trim();
      if (!text) continue;
      if (/x\s*\d+|\d+\s*x/i.test(text)) return text;
    }
  }
  return '';
}

function romanToNumber(roman) {
  if (!roman) return null;
  const map = { i: 1, v: 5, x: 10, l: 50, c: 100, d: 500, m: 1000 };
  const chars = roman.toLowerCase().split('');
  let total = 0;
  let prev = 0;
  for (let i = chars.length - 1; i >= 0; i -= 1) {
    const value = map[chars[i]] || 0;
    if (value < prev) total -= value;
    else total += value;
    prev = value;
  }
  return total || null;
}

function parseLeagueIdFromText(raw) {
  if (!raw) return null;
  const text = String(raw).replace(/\s+/g, ' ').trim().toLowerCase();
  if (!text) return null;
  if (text.includes('odyssey')) return 1;
  if (text.includes('eclipse')) return 3;
  if (text.includes('horizon')) return 4;
  const duneMatch = text.match(/dune\s*([0-9]+|[ivxlcdm]+)/i);
  if (duneMatch) {
    const rawId = duneMatch[1];
    const duneNum = /^\d+$/.test(rawId) ? Number(rawId) : romanToNumber(rawId);
    if (Number.isFinite(duneNum) && duneNum > 0) {
      return 5 + (Math.trunc(duneNum) - 1);
    }
  }
  return null;
}

function extractLiveLeagueText() {
  const fromXPath = getTextByXPathsQuiet(LIVE_SUMMARY_LEAGUE_XPATHS, document);
  if (fromXPath) return fromXPath;
  const fromSelector = getTextBySelectorsQuiet(LIVE_SUMMARY_LEAGUE_SELECTORS, document);
  if (fromSelector) return fromSelector;
  const gameRoot = document.querySelector('miner-wars-game') || document.body;
  const guessed = guessLeagueTextFromRoot(gameRoot);
  if (guessed) return guessed;
  return '';
}

function extractMyClanFromLivePanel() {
  let nameText = getTextByXPathsQuiet(LIVE_MY_CLAN_NAME_XPATHS, document) || '';
  let clanId = null;
  const linkNode = nodeFromXPaths(LIVE_MY_CLAN_LINK_XPATHS, document);
  if (linkNode) {
    const href = linkNode.getAttribute ? linkNode.getAttribute('href') : '';
    clanId = parseClanIdFromUrl(href);
    if (!nameText) {
      nameText = (linkNode.textContent || '').trim();
    }
  }
  if (!nameText) {
    const fromRound = textFromXPath(ROUND_DETAIL_XPATHS.meClan);
    if (fromRound) nameText = fromRound.trim();
  }
  if (nameText) {
    nameText = nameText.replace(/\s+/g, ' ').replace(/\s+\d+(\.\d+)?%\s*$/, '').trim();
    if (/^[0-9.,]+[kKmM]?$/.test(nameText)) {
      nameText = '';
    }
  }
  return { name: nameText || null, clanId: clanId || null };
}

function normalizeClanName(value) {
  const text = sanitizeText(value || '');
  if (!text) return '';
  if (PLACEHOLDER_CLAN_NAME_PATTERNS.some((pattern) => pattern.test(text))) {
    return '';
  }
  return text;
}

function isPlaceholderWinner(value) {
  const raw = typeof value === 'string' ? value : `${value ?? ''}`;
  const normalized = normalizeModalText ? normalizeModalText(raw) : sanitizeText(raw);
  if (!normalized) return true;
  if (normalized.length <= 2) return true;
  const literalKey = normalized.replace(/[!\.\u2026]+$/g, '').trim().toLowerCase();
  const literalNoDigits = literalKey.replace(/[\d,:%]/g, '').replace(/\s+/g, ' ').trim();
  if (
    (literalKey && WINNER_PLACEHOLDER_LITERALS.has(literalKey)) ||
    (literalNoDigits && WINNER_PLACEHOLDER_LITERALS.has(literalNoDigits))
  ) {
    return true;
  }
  if (WINNER_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(normalized))) {
    return true;
  }
  const loose = normalized.replace(/[\d#,:%]/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
  if (loose) {
    for (const phrase of WINNER_PLACEHOLDER_PHRASES) {
      if (loose.includes(phrase)) {
        return true;
      }
    }
  }
  return false;
}

function parseFloatFromString(val) {
  if (!val) return null;
  const cleaned = String(val).replace(/,/g, '').trim();
  const numeric = parseFloat(cleaned);
  return Number.isFinite(numeric) ? numeric : null;
}

function parseClanIdFromUrl(url) {
  if (!url) return null;
  const match = url.match(/\/(?:clan|pool)\/(\d+)/i);
  return match ? match[1] : null;
}

function parseClanIdFromText(text) {
  if (!text) return null;
  const match = text.match(/clan\s*#?\s*(\d{3,6})/i);
  return match ? match[1] : null;
}

function parseMetricFromText(text, regex) {
  if (!text) return null;
  const match = text.match(regex);
  if (!match) return null;
  return parseFloatFromString(match[1]);
}

function parsePositionFromText(text) {
  if (!text) return null;
  const patterns = [
    /#\s*(\d{1,3})/i,
    /Rank(?:ing)?\s*(\d{1,3})/i,
    /Position\s*(\d{1,3})/i,
    /(\d{1,3})(?:st|nd|rd|th)?\s*(?:place|rank)/i
  ];
  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match) {
      const parsed = parseInt(match[1], 10);
      if (!isNaN(parsed)) return parsed;
    }
  }
  return null;
}

function findClanNameFromCard(card) {
  if (!card) return null;
  const selectors = ['[class*="name"]', 'h1', 'h2', 'h3', 'h4', 'p'];
  for (const selector of selectors) {
    const el = card.querySelector(selector);
    const candidate = sanitizeText(el?.textContent);
    if (candidate && candidate.length > 1 && !/(TH|BTC|blocks?|Show more)/i.test(candidate)) {
      return candidate;
    }
  }
  const fallback = sanitizeText(card.textContent || '');
  const lines = fallback.split('\n').map((line) => line.trim()).filter(Boolean);
  return lines[0] || null;
}

const CLAN_DETAIL_REVEAL_BUTTON_XPATHS = [
  "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'member')]",
  "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'show more')]",
  "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'load more')]"
];
const CLAN_DETAIL_PAGINATION_NEXT_XPATHS = [
  "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'next page')]",
  "//button[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'next')]",
  "//a[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'next')]",
  "//button[contains(@aria-label, 'Next')]",
  "//a[contains(@aria-label, 'Next')]"
];
const CLAN_DETAIL_MAX_BUTTON_CLICKS = 5;
const CLAN_DETAIL_VIEW_MIN_DELAY_MS = 5000;
const CLAN_DETAIL_VIEW_MAX_DELAY_MS = 9000;
const CLAN_DETAIL_MAX_MEMBER_PAGES = 25;
const CLAN_MEMBER_PAGE_DELAY_RANGE_MS = { min: 4000, max: 10000 };
const ROUND_HISTORY_CARD_SELECTORS = [
  'round-history-card',
  'miner-wars-round-history round-history-card',
  'section round-history-card',
  'round-history-card button',
  'button.round-history-card'
];
const ROUND_HISTORY_BACK_SELECTORS = [
  'round-header button',
  'round-header a',
  'button[aria-label*="back" i]',
  'button[class*="back"]',
  'a[aria-label*="back" i]',
  'a[class*="back"]'
];
const ROUND_DETAIL_XPATHS = {
  roundHeader: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round",
  roundId: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[1]/a/div[2]/span",
  clanName: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/span",
  clanPct: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/div[1]/span",
  clanBtc: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/div[2]/span",
  topClanName: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[2]/info-card/div/div[1]/div[2]/span",
  topClanPct: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[2]/info-card/div/div[1]/div[2]/div",
  topClanPower: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[2]/info-card/div/div[2]/div/div[1]/div/span",
  endedAt: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/round-header/header-game/div/div[2]/div/span",
  winnerName: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/div[1]/p[1]",
  winnerPct: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/div[2]/span",
  winnerGmt: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[1]/div[2]/div[3]/span",
  winnerPower: "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-round/div/div[2]/div[1]/div/participant-info[1]/info-card/div/div[2]/div/div/div/span",
  meName: "//*[@id='current-user-info-block']/div/div[1]/div[2]/div[1]/p[1]",
  meClan: "//*[@id='current-user-info-block']/div/div[1]/div[2]/div[1]/p[2]",
  mePct: "//*[@id='current-user-info-block']/div/div[1]/div[2]/div[2]/span",
  mePower: "//*[@id='current-user-info-block']/div/div[2]/div/div/div/span"
};
const LIVE_MY_CLAN_PCT_XPATH = "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[3]/span";
const LIVE_MY_CLAN_NAME_XPATHS = [
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[2]/span",
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[2]",
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[1]/span",
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a/div[1]"
];
const LIVE_MY_CLAN_LINK_XPATHS = [
  "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-main/miner-wars-game/div/div[1]/div/div[3]/a",
  "//miner-wars-game//a[contains(@href, '/miner-wars/pool')][1]"
];

function getClanDetailViewDelayMs() {
  const jitter = Math.random() * (CLAN_DETAIL_VIEW_MAX_DELAY_MS - CLAN_DETAIL_VIEW_MIN_DELAY_MS);
  return CLAN_DETAIL_VIEW_MIN_DELAY_MS + jitter;
}

function randomDelayMs(range) {
  if (!range) return 0;
  const min = Math.max(0, Number(range.min) || 0);
  const max = Math.max(min, Number(range.max) || min);
  return min + Math.random() * (max - min);
}

async function waitOnClanDetailView(logFn) {
  const dwellMs = getClanDetailViewDelayMs();
  const message = `Waiting ${(dwellMs / 1000).toFixed(1)}s on detail view to avoid rapid navigation.`;
  if (typeof logFn === 'function') {
    logFn(message);
  } else {
    console.log('[MW]', message);
  }
  await sleep(dwellMs);
  return dwellMs;
}
function evaluateXPath(xpath, context = document) {
  try {
    return document.evaluate(xpath, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  } catch (_) {
    return null;
  }
}

function nodeFromXPaths(xpaths, context = document) {
  const list = Array.isArray(xpaths) ? xpaths : [xpaths];
  for (const xpath of list) {
    const node = evaluateXPath(xpath, context);
    if (node) return node;
  }
  return null;
}

function textFromXPath(xpath, context = document) {
  const node = evaluateXPath(xpath, context);
  if (!node) return '';
  const value = node.nodeType === Node.ATTRIBUTE_NODE ? node.value : node.textContent;
  return (value || '').trim();
}

function extractDominantNumericId(text) {
  if (!text) return null;
  const matches = String(text).match(/\d[\d,\.]*/g);
  if (!matches || !matches.length) return null;
  const normalized = matches
    .map((token) => token.replace(/[^\d]/g, ''))
    .filter((token) => token.length > 0);
  if (!normalized.length) return null;
  const longMatch = normalized.find((token) => token.length >= 7);
  if (longMatch) return longMatch;
  return normalized[normalized.length - 1] || normalized[0] || null;
}

function parseIntFromText(text, fallback = 0) {
  if (!text) return fallback;
  const match = String(text).replace(/[, ]/g, '').match(/-?\d+/);
  return match ? parseInt(match[0], 10) : fallback;
}

function parseFloatFromText(text, fallback = 0) {
  if (!text) return fallback;
  const match = String(text).replace(/[, ]/g, '').match(/-?\d+(?:\.\d+)?/);
  return match ? parseFloat(match[0]) : fallback;
}

function parsePercentFromText(text) {
  return parseFloatFromText(text, null);
}

function parseDurationToSeconds(text) {
  if (!text || typeof text !== 'string') return null;
  const parts = text.split(':').map((p) => parseInt(p, 10)).filter((n) => !isNaN(n));
  if (parts.length === 2) {
    return parts[0] * 60 + parts[1];
  }
  if (parts.length === 3) {
    return parts[0] * 3600 + parts[1] * 60 + parts[2];
  }
  return null;
}

function parseEndedAtLabel(label) {
  if (!label) return null;
  let text = label.trim();
  if (text.toLowerCase().startsWith('ended at')) {
    text = text.slice(8).trim();
  }
  try {
    const normalized = text.replace(/\s+/g, ' ');
    const currentYear = new Date().getUTCFullYear();
    const parsed = new Date(`${normalized} ${currentYear} UTC`);
    if (isNaN(parsed.getTime())) return null;
    return parsed.toISOString();
  } catch (_) {
    return null;
  }
}

function parseBtcReward(text) {
  if (!text) return { raw: '', value: null };
  const raw = text.trim();
  if (!raw || raw.toLowerCase().includes('calculating')) {
    return { raw, value: null };
  }
  const match = raw.replace(',', '.').match(/(\d+(?:\.\d+)?)/);
  return { raw, value: match ? parseFloat(match[1]) : null };
}

function findHistoryCards() {
  for (const selector of ROUND_HISTORY_CARD_SELECTORS) {
    const nodes = document.querySelectorAll(selector);
    if (nodes && nodes.length) {
      return Array.from(nodes);
    }
  }
  return [];
}

function simulateDomClick(target) {
  if (!target) return;
  const eventOptions = { bubbles: true, cancelable: true, composed: true };
  const pointerOptions = { ...eventOptions, pointerId: 1, pointerType: 'mouse' };
  try { target.dispatchEvent(new PointerEvent('pointerdown', pointerOptions)); } catch (_) {}
  try { target.dispatchEvent(new MouseEvent('mousedown', eventOptions)); } catch (_) {}
  try { target.dispatchEvent(new PointerEvent('pointerup', pointerOptions)); } catch (_) {}
  try { target.dispatchEvent(new MouseEvent('mouseup', eventOptions)); } catch (_) {}
  try { target.dispatchEvent(new MouseEvent('click', eventOptions)); }
  catch (_) { if (typeof target.click === 'function') target.click(); }
}

function extractHistoryCardMeta(card) {
  const meta = {};
  if (!card) return meta;
  const button = card.querySelector('button') || card;
  const spans = button.querySelectorAll('span');
  if (spans[0]) {
    const idText = spans[0].textContent || '';
    const match = idText.replace(/,/g, '').match(/(\d+)/);
    meta.roundId = match ? match[1] : null;
    meta.roundIdRaw = idText.trim();
    meta.isLive = /live/i.test(idText);
  }
  if (spans[1]) {
    meta.totalTime = (spans[1].textContent || '').trim();
    meta.durationSeconds = parseDurationToSeconds(meta.totalTime);
  }
  if (spans[2]) {
    meta.multiplierText = (spans[2].textContent || '').trim();
    const multVal = parseIntFromText(meta.multiplierText, 1);
    meta.multiplierValue = multVal > 0 ? multVal : 1;
  }
  const rewardEl = button.querySelector('div span span span');
  if (rewardEl) {
    meta.historyReward = rewardEl.textContent || '';
  }
  const badge = button.querySelector('[class*="current" i], [class*="active" i]');
  if (badge) {
    meta.isLive = true;
  }
  return meta;
}

function findRoundDetailBackButton() {
  for (const selector of ROUND_HISTORY_BACK_SELECTORS) {
    const btn = document.querySelector(selector);
    if (btn) return btn;
  }
  const candidates = Array.from(document.querySelectorAll('button, a'));
  for (const el of candidates) {
    const text = (el.textContent || '').trim().toLowerCase();
    if (!text) continue;
    if (text.includes('back') || text.includes('history')) {
      return el;
    }
  }
  return null;
}

async function navigateBackToHistory(previousUrl, log) {
  let mode = 'history';
  const backBtn = findRoundDetailBackButton();
  if (backBtn) {
    mode = 'button';
    if (typeof log === 'function') log('Clicking round detail back button...');
    backBtn.click();
  } else if (previousUrl && window.location.href !== previousUrl) {
    if (typeof log === 'function') log('Using history.back() to return to history list...');
    window.history.back();
  } else if (previousUrl) {
    mode = 'reload';
    if (typeof log === 'function') log('Reloading round history page...');
    window.location.href = previousUrl;
  }
  const restored = await waitForCondition(() => {
    const cards = findHistoryCards();
    const pathMatch = previousUrl ? window.location.href === previousUrl : true;
    return cards.length > 0 && pathMatch;
  }, 20000, 400);
  if (!restored && typeof log === 'function') {
    log(`Failed to return to round history via ${mode}.`);
  }
  return restored;
}

async function openRoundHistoryCard(card, log) {
  if (!card) throw new Error('Card not found.');
  const currentUrl = window.location.href;
  const button = card.querySelector('button') || card;
  if (button.scrollIntoView) {
    button.scrollIntoView({ block: 'center' });
  }
  if (typeof log === 'function') log(`Triggering card click at ${window.location.href}`);
  simulateDomClick(button);
  if (typeof log === 'function') log('Waiting for round detail view to render...');
  const detailPattern = /\/miner-wars\/(?:round|round-history)(?:\/\d+)?/;
  const navigated = await waitForCondition(() => {
    const pathMatch = detailPattern.test(window.location.pathname);
    const headerNode = evaluateXPath(ROUND_DETAIL_XPATHS.roundHeader);
    const detailElement = document.querySelector('miner-wars-round');
    const headerVisible = Boolean(headerNode);
    const componentVisible = Boolean(detailElement && detailElement.offsetParent !== null);
    return Boolean(pathMatch || headerVisible || componentVisible);
  }, 20000, 300);
  if (!navigated) {
    const failurePath = window.location.href;
    log?.(`Round detail wait timed out. URL: ${failurePath}`);
    throw new Error(`Navigation to round detail timed out (last path: ${failurePath})`);
  }
  if (typeof log === 'function') log('Round detail view detected.');
  await waitOnClanDetailView((msg) => log?.(msg));
  return currentUrl;
}

function extractRoundDetailFromDom(meta = {}) {
  const detail = {};
  const roundIdFromDomText = textFromXPath(ROUND_DETAIL_XPATHS.roundId);
  const roundIdFromDom = extractDominantNumericId(roundIdFromDomText) || roundIdFromDomText;
  const roundIdFromMeta = extractDominantNumericId(meta.roundIdRaw) || meta.roundId || null;
  const roundIdFromPath = (window.location.pathname.match(/(\d+)/) || [])[0] || null;
  detail.round_id = roundIdFromDom || roundIdFromMeta || roundIdFromPath || null;
  detail.time_scrape = new Date().toISOString();
  detail.total_time = meta.totalTime || null;
  detail.duration_seconds = meta.durationSeconds || null;
  detail.multiplier = meta.multiplierText || null;
  detail.multiplier_value = meta.multiplierValue || null;
  detail.history_clan_reward = meta.historyReward || null;
  const endedRaw = textFromXPath(ROUND_DETAIL_XPATHS.endedAt);
  detail.ended_at_raw = endedRaw || null;
  detail.ended_at = endedRaw ? parseEndedAtLabel(endedRaw) : null;
  detail.clan_name_new = textFromXPath(ROUND_DETAIL_XPATHS.clanName) || null;
  detail.clan_percentage = textFromXPath(ROUND_DETAIL_XPATHS.clanPct) || null;
  detail.clan_percentage_value = parsePercentFromText(detail.clan_percentage);
  const reward = parseBtcReward(textFromXPath(ROUND_DETAIL_XPATHS.clanBtc));
  detail.btc_reward_clan = reward.raw || null;
  detail.btc_reward_clan_btc = reward.value;
  detail.highest_score_clan_name = textFromXPath(ROUND_DETAIL_XPATHS.topClanName) || null;
  detail.highest_clan_percentage = textFromXPath(ROUND_DETAIL_XPATHS.topClanPct) || null;
  detail.highest_clan_percentage_value = parsePercentFromText(detail.highest_clan_percentage);
  detail.highest_clan_power = parseIntFromText(textFromXPath(ROUND_DETAIL_XPATHS.topClanPower), null);
  detail.highest_clan_power_int = detail.highest_clan_power;
  detail.winner_user_name = textFromXPath(ROUND_DETAIL_XPATHS.winnerName) || null;
  detail.winner_user_percentage = textFromXPath(ROUND_DETAIL_XPATHS.winnerPct) || null;
  detail.winner_user_percentage_value = parsePercentFromText(detail.winner_user_percentage);
  detail.winner_user_gmt_reward = textFromXPath(ROUND_DETAIL_XPATHS.winnerGmt) || null;
  detail.winner_user_power = parseIntFromText(textFromXPath(ROUND_DETAIL_XPATHS.winnerPower), null);
  detail.my_user_name = textFromXPath(ROUND_DETAIL_XPATHS.meName) || null;
  detail.my_user_clan = textFromXPath(ROUND_DETAIL_XPATHS.meClan) || null;
  detail.my_user_percentage = textFromXPath(ROUND_DETAIL_XPATHS.mePct) || null;
  detail.my_user_percentage_value = parsePercentFromText(detail.my_user_percentage);
  detail.my_user_power = parseIntFromText(textFromXPath(ROUND_DETAIL_XPATHS.mePower), null);
  detail.source_url = window.location.href;
  return detail;
}

async function scrapeRoundHistoryWithDetails(options = {}) {
  const logs = [];
  const records = [];
  if (!window.location.pathname.includes('/round-history')) {
    throw new Error('Open the Miner Wars round history page before scraping.');
  }
  let cards = findHistoryCards();
  if (!cards.length) {
    await sleep(800);
    cards = findHistoryCards();
  }
  if (!cards.length) {
    throw new Error('Unable to locate round history cards.');
  }
  logs.push(`Detected ${cards.length} history cards before filtering.`);
  const limit = Number(options.limit) || 12;
  const initialScroll = document.scrollingElement ? document.scrollingElement.scrollTop : 0;
  for (let idx = 0; idx < cards.length && records.length < limit; idx++) {
    const card = cards[idx];
    const meta = extractHistoryCardMeta(card);
    if (meta.isLive) {
      logs.push(`Skipping active/live card ${idx + 1} (${meta.roundId || 'unknown'}).`);
      continue;
    }
    logs.push(`Card ${idx + 1}: opening round ${meta.roundId || 'unknown'} (duration ${meta.totalTime || 'n/a'}, multiplier ${meta.multiplierText || meta.multiplierValue || 'n/a'}).`);
    let previousUrl;
    try {
      previousUrl = await openRoundHistoryCard(card, (m) => logs.push(`[Card ${idx + 1}] ${m}`));
      logs.push(`[Card ${idx + 1}] Round detail visible. Extracting data...`);
      const detail = extractRoundDetailFromDom(meta);
      logs.push(`[Card ${idx + 1}] Extracted raw detail keys: ${Object.keys(detail).join(', ')}`);
      if (detail.round_id) {
        records.push(detail);
        logs.push(`[Card ${idx + 1}] Captured round ${detail.round_id}.`);
      } else {
        logs.push(`[Card ${idx + 1}] Round ID missing in detail view.`);
      }
    } catch (err) {
      logs.push(`Failed to capture card ${idx + 1}: ${err?.message || err}`);
    } finally {
      if (previousUrl) {
        logs.push(`[Card ${idx + 1}] Returning to history list...`);
        await navigateBackToHistory(previousUrl, (m) => logs.push(`[Card ${idx + 1}] ${m}`));
        await sleep(600);
        cards = findHistoryCards();
      }
    }
  }
  if (document.scrollingElement) {
    document.scrollingElement.scrollTop = initialScroll;
  }
  return { rounds: records, logs };
}
const CLAN_SUMMARY_FIELD_XPATHS = {
  clanName: [
    "//miner-wars-pool//*[contains(@class,'clan-name')]/text()",
    "//miner-wars-pool//h1/text()",
    "//div[contains(@class,'pool') and contains(@class,'title')]//text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool//h1/text()"
  ],
  clanTh: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div[1]/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[1]/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[1]/div[1]",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[1]/div[1]/span",
    "//miner-wars-pool//span[contains(@class,'clan') and contains(translate(., 'TH', 'th'), 'th')]/text()",
    "//miner-wars-pool//*[contains(translate(., 'TH', 'th'), 'th')]/text()"
  ],
  position: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[5]/div/div/div[2]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div/div/div[2]/span/text()",
    "//miner-wars-pool//span[contains(@class,'rank')]/text()",
    "//miner-wars-pool//*[contains(translate(., '#', '#'), '#')]/text()"
  ],
  currentBlocks: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div[2]/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[2]/div[1]/span/text()",
    "//miner-wars-pool//span[contains(translate(., 'BLOCK', 'block'), 'block')]/text()"
  ],
  btc: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div[2]/div[2]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[2]/div[2]/span/text()",
    "//miner-wars-pool//*[contains(translate(., 'BTC', 'btc'), 'btc')]/text()"
  ],
  league: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[5]/div/div/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div/div/div[1]/span/text()"
  ],
  users: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[5]/div/div/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div[2]/div[3]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[1]/div[2]/span/text()"
  ],
  earnedGmt: [
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[5]/div/div/div[1]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[4]/div[2]/div[3]/span/text()",
    "/html/body/app-root/main/miner-wars/div/div/div/miner-wars-pool/div/div[3]/div[2]/div[3]/span/text()"
  ],
  freeToJoin: [
    "//miner-wars-pool//button[contains(translate(., 'JOIN', 'join'), 'join')]/text()"
  ]
};

const CLAN_SUMMARY_CSS_SELECTORS = {
  clanTh: [
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(5) > div:nth-child(1) > div.text-nowrap.badge-game.badge-game--dark.badge-game--with-icon.d-inline-flex > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(4) > div:nth-child(1) > div.text-nowrap.badge-game.badge-game--dark.badge-game--with-icon.d-inline-flex > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(3) > div:nth-child(1) > div.text-nowrap.badge-game.badge-game--dark.badge-game--with-icon.d-inline-flex > span'
  ],
  position: [
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(5) > div > div > div:nth-child(2) > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(4) > div > div > div:nth-child(2) > span'
  ],
  currentBlocks: [
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(5) > div:nth-child(2) > div:nth-child(1) > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(4) > div:nth-child(2) > div:nth-child(1) > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span'
  ],
  btc: [
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(5) > div:nth-child(2) > div:nth-child(2) > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(4) > div:nth-child(2) > div:nth-child(2) > span',
    '#app > miner-wars > div > div > div > miner-wars-pool > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(2) > span'
  ]
};

const SUMMARY_LOG_MAX = 200;
let summaryExtractionLogs = [];
let summaryLoggingEnabled = false;

function resetSummaryExtractionLogs() {
  summaryExtractionLogs = [];
  summaryLoggingEnabled = true;
}

function logSummaryExtraction(message) {
  if (!summaryLoggingEnabled || !message) return;
  const entry = `[MW summary] ${message}`;
  console.log(entry);
  if (summaryExtractionLogs.length < SUMMARY_LOG_MAX) {
    summaryExtractionLogs.push(entry);
  }
}

function getSummaryExtractionLogs() {
  summaryLoggingEnabled = false;
  return summaryExtractionLogs.slice();
}
const CLAN_MEMBER_ROW_XPATHS = [
  "//miner-wars-pool//*[contains(@class,'member')]//*[contains(@class,'row')]",
  "//miner-wars-pool//*[contains(@class,'member-list')]//*[contains(@class,'card')]",
  "//miner-wars-pool//table[contains(@class,'member')]//tr",
  "//miner-wars-pool//*[contains(@class,'clan-members')]//*[contains(@class,'member')]",
  "//div[contains(@class,'miner-wars')]//table[contains(@class,'member')]//tr"
];
const CLAN_MEMBER_CONTAINER_SELECTORS = [
  '[class*="member-list"]',
  '[class*="clan-members"]',
  '[class*="members-list"]',
  '[class*="member-table"]',
  '.pool-members',
  'miner-wars-pool'
];

function getLiveClanRoot() {
  const host = document.querySelector('miner-wars-pool');
  if (host && host.shadowRoot) return host.shadowRoot;
  return host || document.body || document;
}

function queryInRoot(root, selector) {
  if (root && typeof root.querySelector === 'function') {
    return root.querySelector(selector);
  }
  return document.querySelector(selector);
}

function queryAllInRoot(root, selector) {
  if (root && typeof root.querySelectorAll === 'function') {
    return root.querySelectorAll(selector);
  }
  return document.querySelectorAll(selector);
}

function getMemberListContainer(rootOverride = null) {
  const root = rootOverride || getLiveClanRoot();
  for (const selector of CLAN_MEMBER_CONTAINER_SELECTORS) {
    const el = queryInRoot(root, selector);
    if (el) return el;
  }
  const xpathCandidates = [
    "//miner-wars-pool//*[contains(@class,'member-list')]",
    "//miner-wars-pool//*[contains(@class,'members')]",
    "//div[contains(@class,'miner-wars')]//*[contains(@class,'member-list')]"
  ];
  return findFirstElementByXPaths(xpathCandidates, root) || (root || document.body);
}

function getMemberListSignature(rootOverride = null) {
  const container = getMemberListContainer(rootOverride);
  if (!container) return '';
  return sanitizeText(container.textContent || '').slice(0, 4000);
}

function extractLeaderboardData() {
  const selectors = ['miner-wars-leaderboard info-card', 'participant-info info-card', 'info-card'];
  const nodes = [];
  selectors.forEach((selector) => {
    document.querySelectorAll(selector).forEach((node) => nodes.push(node));
  });
  const uniqueNodes = Array.from(new Set(nodes));
  const clans = [];
  const seen = new Set();
  uniqueNodes.forEach((card) => {
    if (!card) return;
    const text = sanitizeText(card.textContent || '');
    if (!text) return;
    const link = card.querySelector('a[href*="/clan/"]');
    const clanUrl = link?.href;
    const clanId = parseClanIdFromUrl(clanUrl) || parseClanIdFromText(text);
    if (!clanId || seen.has(clanId)) return;
    seen.add(clanId);
    const clanName = sanitizeText(link?.textContent || findClanNameFromCard(card));
    const clanTh = parseMetricFromText(text, /([\d,\.]+)\s*TH\b/i);
    const currentBlocks = parseMetricFromText(text, /([\d,\.]+)\s*(?:blocks?|block power)/i);
    const btc = parseMetricFromText(text, /([\d,\.]+)\s*BTC\b/i);
    const position = parsePositionFromText(text);
    clans.push({
      clanId,
      clanName,
      clanTh,
      currentBlocks,
      btc,
      position,
      clanLink: clanUrl
    });
  });
  console.log('[MW] extracted leaderboard cards', clans.length);
  return {
    time_scrape: new Date().toISOString(),
    url: window.location.href,
    clans
  };
}

function findClanNameFromPool(root) {
  const selectors = ['[data-clan-name]', '.clan-name', '.pool-clan-name', 'h1', 'h2'];
  for (const selector of selectors) {
    const el = root?.querySelector(selector);
    const text = normalizeClanName(el?.textContent);
    if (text) return text;
  }
  return normalizeClanName(document.title) || null;
}

function findDescription(root) {
  const selectors = ['[class*="description"]', '[data-description]', '.clan-description'];
  for (const selector of selectors) {
    const el = root?.querySelector(selector);
    const text = sanitizeText(el?.textContent);
    if (text && text.length > 10) return text;
  }
  const paragraphs = root?.querySelectorAll('p') || [];
  let longest = '';
  paragraphs.forEach((p) => {
    const val = sanitizeText(p.textContent);
    if (val.length > longest.length) longest = val;
  });
  return longest || null;
}

function findFreeToJoinStatus(root = document) {
  const btnText = sanitizeText(root.querySelector('join-clan-btn button span span')?.textContent);
  if (btnText) return btnText;
  return sanitizeText(root.querySelector('join-clan-btn button')?.textContent);
}

function extractClanMembersViaXPath(root = document) {
  const members = [];
  const seen = new Set();
  const baseRoot = root || getLiveClanRoot();
  CLAN_MEMBER_ROW_XPATHS.forEach((xpath) => {
    const nodes = getNodesByXPath(xpath, baseRoot || document);
    nodes.forEach((node) => {
      if (!node || seen.has(node)) return;
      seen.add(node);
      const text = sanitizeText(node.textContent || '');
      if (!text) return;
      const parsed = parseMemberElement(node, text);
      if (parsed) members.push(parsed);
    });
  });
  return members;
}

function extractClanMembers(root) {
  const baseRoot = root || getLiveClanRoot();
  const viaXPath = extractClanMembersViaXPath(baseRoot || document);
  if (viaXPath.length) return viaXPath;
  const container = baseRoot || document.body;
  const selectors = ['.member-card', '.member-row', '.user-row', '.pool-member', '.clan-member', 'tr', 'li'];
  const candidates = new Set();
  selectors.forEach((selector) => {
    (container.querySelectorAll ? container.querySelectorAll(selector) : queryAllInRoot(null, selector)).forEach((el) => {
      if (el && el.textContent && el.textContent.length > 20) {
        candidates.add(el);
      }
    });
  });
  const members = [];
  candidates.forEach((el) => {
    const text = sanitizeText(el.textContent || '');
    if (!text || !/(joined|efficiency|boost|th|w\/th)/i.test(text)) return;
    const parsed = parseMemberElement(el, text);
    if (parsed) members.push(parsed);
  });
  return members;
}

function findNextMemberPageButton(rootOverride = null) {
  const button = findFirstElementByXPaths(CLAN_DETAIL_PAGINATION_NEXT_XPATHS, rootOverride || getLiveClanRoot());
  if (!button) return null;
  const ariaDisabled = button.getAttribute && button.getAttribute('aria-disabled');
  if (button.disabled || ariaDisabled === 'true') return null;
  return button;
}

async function waitBeforeMemberPageNavigation(logFn) {
  const delay = randomDelayMs(CLAN_MEMBER_PAGE_DELAY_RANGE_MS);
  if (!delay) return;
  const message = `Waiting ${(delay / 1000).toFixed(1)}s before moving to the next member page...`;
  if (typeof logFn === 'function') {
    logFn(message);
  } else {
    console.log('[MW members]', message);
  }
  await sleep(delay);
}

async function advanceMemberPagination(previousSignature, logFn, { enableDelay = false, rootOverride = null } = {}) {
  const logger = typeof logFn === 'function' ? logFn : (msg) => console.log('[MW members]', msg);
  const button = findNextMemberPageButton(rootOverride);
  if (!button) return false;
  if (enableDelay) {
    await waitBeforeMemberPageNavigation(logger);
  }
  try {
    button.scrollIntoView({ block: 'center', behavior: 'auto' });
    if (typeof button.click === 'function') {
      button.click();
    } else {
      button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
    }
  } catch (err) {
    console.warn('[MW] Failed to advance member page', err);
    return false;
  }
  logger('Advancing to next member page...');
  const changed = await waitForCondition(() => {
    const signature = getMemberListSignature(rootOverride);
    return signature && signature !== previousSignature;
  }, 6000, 300);
  if (!changed) {
    logger('Next member page did not load new data.');
    return false;
  }
  await sleep(300);
  return true;
}

async function collectAllClanMembers(logFn, { enableDelay = true, rootOverride = null } = {}) {
  const logger = typeof logFn === 'function' ? logFn : (msg) => console.log('[MW members]', msg);
  const root = rootOverride || getLiveClanRoot();
  await clickButtonsByXPathList(CLAN_DETAIL_REVEAL_BUTTON_XPATHS, logger, root);
  const aggregated = [];
  const seenMembers = new Set();
  let page = 1;
  while (page <= CLAN_DETAIL_MAX_MEMBER_PAGES) {
    const members = extractClanMembers(root);
    let added = 0;
    members.forEach((member) => {
      const name = sanitizeText(member.userName || '');
      if (!name) return;
      const key = `${name.toLowerCase()}|${sanitizeText(member.joinedDate || '')}`;
      if (seenMembers.has(key)) return;
      seenMembers.add(key);
      aggregated.push(member);
      added += 1;
    });
    logger(`Captured ${added} new member${added === 1 ? '' : 's'} from page ${page}. Total ${aggregated.length}.`);
    const signature = getMemberListSignature(root);
    const advanced = await advanceMemberPagination(signature, logger, { enableDelay, rootOverride: root });
    if (!advanced) break;
    page += 1;
  }
  return aggregated;
}

function parseMemberElement(el, text) {
  const userName = guessMemberName(el, text);
  if (!userName) return null;
  const joinedDate = findJoinedDate(text);
  const userTh = parseMetricFromText(text, /([\d,\.]+)\s*TH\b/i);
  const userEfficiency = parseMetricFromText(text, /([\d,\.]+)\s*W\/TH\b/i);
  const boostNodes = Array.from(el.querySelectorAll('[class*="boost"], span, div'));
  const boosts = boostNodes
    .map((node) => sanitizeText(node.textContent))
    .filter((value) => value && /boost/i.test(value) && value.length < 60);
  const boostText = boosts.length ? boosts.join(' | ') : null;
  return {
    userName,
    joinedDate,
    userTh,
    userEfficiency,
    boosts: boostText
  };
}

const MONTH_NAME = '(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)';

function extractNameBeforeDateSegment(value) {
  if (!value) return null;
  const regex = new RegExp(`^(.*?)(?=\\s+${MONTH_NAME}\\s+\\d{1,2}(?:,\\s*\\d{4})?)`, 'i');
  const match = value.match(regex);
  if (match && match[1]) {
    return sanitizeText(match[1]);
  }
  const fallbackRegex = new RegExp(`^(.*?)(?=\\s+${MONTH_NAME})`, 'i');
  const fallbackMatch = value.match(fallbackRegex);
  if (fallbackMatch && fallbackMatch[1]) {
    return sanitizeText(fallbackMatch[1]);
  }
  return null;
}

function guessMemberName(el, text) {
  const selectors = ['h1', 'h2', 'h3', 'h4', 'strong', '.name', '.member-name', '.user-name'];
  for (const selector of selectors) {
    const candidate = sanitizeText(el.querySelector(selector)?.textContent);
    const beforeDate = extractNameBeforeDateSegment(candidate);
    if (beforeDate) return beforeDate;
    if (candidate && candidate.length > 1 && !/(TH|BTC|blocks?|joined|GMT|boost)/i.test(candidate)) {
      return candidate;
    }
  }
  const cleanedLines = text.split('\n').map((line) => line.trim()).filter(Boolean);
  for (const line of cleanedLines) {
    const beforeDate = extractNameBeforeDateSegment(line);
    if (beforeDate) return beforeDate;
  }
  return sanitizeText(cleanedLines[0] || '');
}

function findJoinedDate(text) {
  const match = text.match(/Joined\s*[:\-]?\s*([^\|]+)/i);
  if (!match) return null;
  return sanitizeText(match[1].split(/[\n\r]/)[0]);
}

async function extractClanPageData(options = {}) {
  const {
    skipReadyWait = false,
    log,
    waitBetweenMemberPages = false,
    skipMembers = false
  } = options || {};
  const logger = typeof log === 'function' ? log : (msg) => console.log('[MW detail]', msg);
  if (!skipReadyWait) {
    await waitForClanPageDataReady();
  }
  const root = getLiveClanRoot();
  resetSummaryExtractionLogs();
  const summary = buildSummaryFromDetail({}, root);
  const members = skipMembers
    ? []
    : await collectAllClanMembers(logger, { enableDelay: true, rootOverride: root, waitBetweenMemberPages });
  console.log('[MW] extracted clan summary', summary.clanId, 'members', members.length);
  return {
    summary,
    members,
    logs: getSummaryExtractionLogs()
  };
}

function resolveUrl(base, relative) {
  if (!relative) return null;
  try {
    return new URL(relative, base).toString();
  } catch (_) {
    return null;
  }
}

function urlsMatch(a, b) {
  try {
    const ua = new URL(a, window.location.href);
    const ub = new URL(b, window.location.href);
    return ua.href === ub.href;
  } catch (_) {
    return a === b;
  }
}

function isValidClanId(candidate) {
  return typeof candidate === 'string' && /^\d{4,10}$/.test(candidate);
}

function findClanIdCandidates(value) {
  if (!value) return null;
  const normalized = value.replace(/\s+/g, ' ');
  const fragments = [
    normalized.match(/#\s*(\d{4,10})\b/),
    normalized.match(/(?:\/(?:miner-wars\/)?)?(?:pool|clan)\/(\d{4,10})\b/i),
    normalized.match(/clan\s*#?\s*(\d{4,10})\b/i),
    normalized.match(/\bID[:\s]*(\d{4,10})\b/i),
    normalized.match(/\b(\d{4,10})\b/)
  ];
  for (const match of fragments) {
    if (match && match[1] && isValidClanId(match[1])) {
      return match[1];
    }
  }
  return null;
}

function getAttributeValues(element) {
  if (!element || !element.attributes) return [];
  return Array.from(element.attributes)
    .map((attr) => sanitizeText(attr.value))
    .filter(Boolean);
}

function findClanIdFromAttributes(element) {
  if (!element) return null;
  const values = getAttributeValues(element);
  for (const value of values) {
    const clanId = findClanIdCandidates(value);
    if (clanId) return clanId;
  }
  return null;
}

const LEADERBOARD_CARD_SELECTORS = [
  'miner-wars-leaderboard info-card',
  '.miner-wars-leaderboard info-card',
  '.participant-info.info-card',
  '.info-card',
  '.leaderboard-card',
  '.clan-card',
  '.leaderboard-scrollable .info-card',
  '.cdk-virtual-scroll-viewport .info-card'
];

function findClanIdFromCard(card) {
  if (!card) return null;
  const attrId = card.dataset?.clanId || card.dataset?.id || card.getAttribute('data-clan-id') || card.getAttribute('data-id');
  if (attrId) {
    const normalized = attrId.toString().match(/\d{4,6}/);
    if (normalized) return normalized[0];
  }
  const computed = findClanIdFromAttributes(card);
  if (computed) return computed;
  const badgeTargets = Array.from(card.querySelectorAll('span,div,p,button'));
  for (const target of badgeTargets) {
    const candidate = findClanIdCandidates(sanitizeText(target.textContent || ''));
    if (candidate) return candidate;
  }
  const badgeSelectors = [
    '.participant-info__header p',
    '.participant-info__header span',
    '.participant-info__header .badge-text',
    '.participant-info__label',
    '.participant-rank-badge',
    'button span',
    'p span'
  ];
  for (const selector of badgeSelectors) {
    const badge = card.querySelector(selector);
    if (badge) {
      const candidate = findClanIdCandidates(sanitizeText(badge.textContent || ''));
      if (candidate) return candidate;
    }
  }
  const descendants = Array.from(card.querySelectorAll('a, [routerlink], [ng-reflect-router-link], [data-clan-id], [data-id], [id]'));
  for (const desc of descendants) {
    const candidate = findClanIdFromAttributes(desc);
    if (candidate) return candidate;
  }
  const textContent = sanitizeText(card.textContent || '');
  const innerId = findClanIdCandidates(textContent);
  if (innerId) return innerId;
  const renderedId = findClanIdCandidates(card.innerHTML);
  return renderedId;
}

function findClanIdFromDocument(root = document.body) {
  const scopes = [];
  if (root) scopes.push(root);
  if (document.body && document.body !== root) scopes.push(document.body);
  if (document.documentElement) scopes.push(document.documentElement);
  const titleCandidate = findClanIdCandidates(sanitizeText(document.title || ''));
  if (titleCandidate) return titleCandidate;
  for (const scope of scopes) {
    if (!scope) continue;
    const textCandidate = findClanIdCandidates(sanitizeText(scope.textContent || ''));
    if (textCandidate) return textCandidate;
    const elements = scope.querySelectorAll
      ? scope.querySelectorAll('[data-clan-id],[data-id],a[href],button[id],span[id]')
      : [];
    for (const el of elements) {
      const attrCandidate = findClanIdFromAttributes(el);
      if (attrCandidate) return attrCandidate;
      const text = sanitizeText(el.textContent || '');
      const inlineCandidate = findClanIdCandidates(text);
      if (inlineCandidate) return inlineCandidate;
    }
  }
  const bodyMatch = document.body?.innerHTML ? findClanIdCandidates(document.body.innerHTML) : null;
  return bodyMatch || null;
}

const SCAN_SCROLL_DELAY = 250;
const SCAN_MAX_ATTEMPTS = 45;
const SCAN_HYDRATE_WHEEL_DELTA = 300;
const VIRTUAL_SCROLL_STEP = 5;

function getLeaderboardCards() {
  const cards = new Set();
  LEADERBOARD_CARD_SELECTORS.forEach((selector) => {
    document.querySelectorAll(selector).forEach((card) => {
      if (card) cards.add(card);
    });
  });
  return Array.from(cards);
}

function gatherClanUrlsFromDom(intoSet) {
  let addedFromCards = 0;
  const cards = getLeaderboardCards();
  cards.forEach((card) => {
    const clanId = findClanIdFromCard(card);
    if (!clanId) return;
    const normalized = `${window.location.origin}/miner-wars/pool/${clanId}`;
    intoSet.add(normalized);
    addedFromCards += 1;
  });
  if (addedFromCards === 0) {
    const anchorSelectors = [
      'a[href*="/miner-wars/pool/"]',
      'a[href*="/miner-wars/clan/"]',
      '[role="link"][href*="/miner-wars/pool/"]'
    ];
    anchorSelectors.forEach((selector) => {
      document.querySelectorAll(selector).forEach((anchor) => {
        const href = anchor.getAttribute('href');
        const resolved = resolveUrl(window.location.href, href);
        const clanId = parseClanIdFromUrl(resolved);
        if (!clanId) return;
        intoSet.add(`${window.location.origin}/miner-wars/pool/${clanId}`);
      });
    });
  }
  if (intoSet.size === 0 && document.body) {
    const fallbackMatches = document.body.innerHTML.match(/\/miner-wars\/(?:pool|clan)\/(\d{4,6})/gi) || [];
    fallbackMatches.forEach((match) => {
      const resolved = resolveUrl(window.location.href, match);
      const clanId = parseClanIdFromUrl(resolved);
      if (clanId) {
        intoSet.add(`${window.location.origin}/miner-wars/pool/${clanId}`);
      }
    });
  }
}

function getCardFingerprint(card) {
  if (!card) return '';
  const nameSelectors = [
    '.participant-info__main p',
    '.participant-info__header p',
    '.participant-info__name',
    '.card-game-inner .name'
  ];
  for (const selector of nameSelectors) {
    const element = card.querySelector(selector);
    if (element && element.textContent) {
      return sanitizeText(element.textContent).slice(0, 100);
    }
  }
  return sanitizeText(card.textContent || '').slice(0, 100);
}

function getCardIdentifier(card) {
  if (!card) return '';
  const nameSelectors = [
    '[class*="participant-info__main"] p',
    '[class*="participant-info__name"]',
    '[class*="card-game-inner"] .name',
    'h1',
    'h2',
    'h3'
  ];
  for (const selector of nameSelectors) {
    const element = card.querySelector(selector);
    if (element && element.textContent) {
      const value = sanitizeText(element.textContent);
      if (value) return value;
    }
  }
  return sanitizeText(card.textContent || '').slice(0, 80);
}

function findCardClickableTarget(card) {
  if (!card) return null;
  const link = card.querySelector('a[href], button, [role="link"]');
  return link || card;
}

function extractClanIdFromCurrentDetail() {
  const pathMatch = window.location.pathname.match(/\/miner-wars\/(?:pool|clan)\/(\d+)/);
  if (pathMatch) return pathMatch[1];
  const bodyMatch = document.body.innerHTML.match(/\/miner-wars\/(?:pool|clan)\/(\d+)/);
  return bodyMatch ? bodyMatch[1] : null;
}

function extractLeaderboardSnapshot(card) {
  if (!card) return {};
  const text = sanitizeText(card.textContent || '');
  return {
    clanId: findClanIdFromCard(card),
    clanName: findClanNameFromCard(card),
    position: parsePositionFromText(text),
    clanTh: parseMetricFromText(text, /([\d,\.]+)\s*TH\b/i),
    currentBlocks: parseMetricFromText(text, /([\d,\.]+)\s*(?:blocks?|block power)/i),
    btc: parseMetricFromText(text, /([\d,\.]+)\s*BTC\b/i)
  };
}

function findElementByXPath(xpath, contextNode = document) {
  try {
    const doc = resolveContextDoc(contextNode);
    const evaluator = doc.evaluate(xpath, contextNode || doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    return evaluator.singleNodeValue;
  } catch (_) {
    return null;
  }
}

function findFirstElementByXPaths(xpaths, contextNode = document) {
  if (!Array.isArray(xpaths)) return null;
  for (const xpath of xpaths) {
    const el = findElementByXPath(xpath, contextNode);
    if (el) return el;
  }
  return null;
}

function resolveContextDoc(contextNode) {
  if (contextNode && contextNode.nodeType === Node.DOCUMENT_NODE) {
    return contextNode;
  }
  if (contextNode && contextNode.ownerDocument) {
    return contextNode.ownerDocument;
  }
  return document;
}

function expandXPathCandidates(xpath) {
  const variants = [xpath];
  const marker = '/miner-wars-pool';
  const idx = xpath.indexOf(marker);
  if (idx >= 0) {
    let remainder = xpath.slice(idx + marker.length);
    remainder = remainder.replace(/^\/+/, '');
    if (remainder) {
      variants.push(`.//${remainder}`);
    }
  }
  return variants;
}

function getFirstTextByXPaths(xpaths, contextNode = document) {
  if (!Array.isArray(xpaths)) return null;
  const doc = resolveContextDoc(contextNode);
  const evalContext = contextNode && contextNode.nodeType !== Node.DOCUMENT_NODE ? contextNode : doc;
  for (const xpath of xpaths) {
    const candidates = expandXPathCandidates(xpath);
    for (const candidate of candidates) {
      try {
        const node = doc.evaluate(candidate, evalContext || doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        if (node) {
          const text = sanitizeText(node.textContent || node.innerText || '');
          if (text) return text;
          logSummaryExtraction(`XPath matched but empty text: ${candidate}`);
        }
      } catch (err) {
        logSummaryExtraction(`XPath failed ${candidate}: ${err?.message || err}`);
      }
    }
  }
  return null;
}

function getNodesByXPath(xpath, contextNode = document) {
  const nodes = [];
  const doc = resolveContextDoc(contextNode);
  const evalContext = contextNode && contextNode.nodeType !== Node.DOCUMENT_NODE ? contextNode : doc;
  try {
    const snapshot = doc.evaluate(xpath, evalContext || doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (let i = 0; i < snapshot.snapshotLength; i += 1) {
      const node = snapshot.snapshotItem(i);
      if (node) nodes.push(node);
    }
  } catch (_) {
    // ignore
  }
  return nodes;
}

function getTextBySelectors(selectors = [], contextNode = document) {
  if (!Array.isArray(selectors) || !selectors.length) return null;
  const root = contextNode || getLiveClanRoot() || document;
  for (const selector of selectors) {
    try {
      const el = root.querySelector ? root.querySelector(selector) : null;
      if (el && el.textContent) {
        const text = sanitizeText(el.textContent);
        if (text) return text;
        logSummaryExtraction(`Selector matched but empty text: ${selector}`);
      }
    } catch (err) {
      logSummaryExtraction(`Selector failed ${selector}: ${err?.message || err}`);
    }
  }
  if (contextNode !== document && document.querySelector) {
    for (const selector of selectors) {
      try {
        const el = document.querySelector(selector);
        if (el && el.textContent) {
          const text = sanitizeText(el.textContent);
          if (text) return text;
        }
      } catch (err) {
        logSummaryExtraction(`Selector failed in document ${selector}: ${err?.message || err}`);
      }
    }
  }
  return null;
}

async function clickButtonsByXPathList(xpaths, log, rootOverride = null) {
  if (!Array.isArray(xpaths) || !xpaths.length) return;
  const logger = typeof log === 'function' ? log : () => {};
  const root = rootOverride || getLiveClanRoot();
  for (const xpath of xpaths) {
    let clicks = 0;
    while (clicks < CLAN_DETAIL_MAX_BUTTON_CLICKS) {
      const button = findElementByXPath(xpath, root);
      if (!button) break;
      try {
        button.scrollIntoView({ block: 'center', behavior: 'auto' });
        if (typeof button.click === 'function') {
          button.click();
        } else {
          button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
        }
        clicks += 1;
        logger(`Clicked XPath button (${xpath}) [${clicks}/${CLAN_DETAIL_MAX_BUTTON_CLICKS}].`);
        await sleep(400);
      } catch (err) {
        console.warn('Failed to click XPath button', xpath, err);
        break;
      }
    }
  }
}

async function waitForClanDetailReady(log) {
  const ready = await waitForCondition(() => {
    return Boolean(document.querySelector('[data-clan-name], .clan-name, .pool-clan-name, miner-wars-pool'));
  }, 8000, 200);
  if (!ready) {
    log('Detail view did not render expected clan metadata.');
  }
}

async function waitForClanPageDataReady() {
  const ready = await waitForCondition(() => {
    const pool = document.querySelector('miner-wars-pool');
    const nameEl = document.querySelector('[data-clan-name], .clan-name, .pool-clan-name');
    const normalizedName = nameEl ? normalizeClanName(nameEl.textContent || '') : '';
    const idFromDoc = findClanIdFromDocument(pool || document.body);
    return Boolean(pool || normalizedName || idFromDoc);
  }, 10000, 250);
  if (!ready) {
    console.warn('[MW] Clan page did not render expected identifiers within timeout.');
  }
}

async function waitForLeaderboardReady() {
  await waitForCondition(() => {
    return Boolean(
      document.querySelector('miner-wars-leaderboard info-card') ||
        document.querySelector('miner-wars-leaderboard') ||
        document.querySelector('.miner-wars-leaderboard')
    );
  }, 8000, 200);
}

function buildSummaryFromDetail(cardSnapshot = {}, rootOverride = null) {
  const root = rootOverride || getLiveClanRoot();
  const summary = extractClanSummaryFromDoc(root, window.location.href);
  if (!summary.time_scrape) summary.time_scrape = new Date().toISOString();
  summary.clanId =
    summary.clanId ||
    cardSnapshot.clanId ||
    extractClanIdFromCurrentDetail() ||
    findClanIdFromDocument(root);
  const snapshotClanName = normalizeClanName(cardSnapshot.clanName);
  const summaryClanName = normalizeClanName(summary.clanName);
  summary.clanName = summaryClanName || snapshotClanName || null;
  summary.position = summary.position || cardSnapshot.position || null;
  summary.clanTh = summary.clanTh || cardSnapshot.clanTh || null;
  summary.currentBlocks = summary.currentBlocks || cardSnapshot.currentBlocks || null;
  summary.btc = summary.btc || cardSnapshot.btc || null;
  summary.url = window.location.href;
  applySummaryXPaths(summary, root);
  if (!summary.clanName && snapshotClanName) {
    summary.clanName = snapshotClanName;
  }
  return summary;
}

function applySummaryXPaths(summary, contextNode = document) {
  if (!summary) return;
  const applyText = (key, parser) => {
    const text = getFirstTextByXPaths(CLAN_SUMMARY_FIELD_XPATHS[key], contextNode);
    if (!text) return null;
    if (typeof parser === 'function') {
      const parsed = parser(text);
      return parsed;
    }
    return text;
  };
  const applyWithLog = (key, parser) => {
    const value = applyText(key, parser);
    if (value !== null && typeof value !== 'undefined') {
      logSummaryExtraction(`XPath ${key}: ${value}`);
    } else {
      logSummaryExtraction(`XPath ${key}: no match`);
    }
    return value;
  };
  const clanName = applyWithLog('clanName');
  const normalizedName = normalizeClanName(clanName);
  if (normalizedName) summary.clanName = normalizedName;
  const clanTh = applyWithLog('clanTh', (text) => parseMetricFromText(text, /([\d,\.]+)/i));
  if (typeof clanTh === 'number' && !Number.isNaN(clanTh)) {
    summary.clanTh = clanTh;
  } else {
    const cssValue = getTextBySelectors(CLAN_SUMMARY_CSS_SELECTORS.clanTh, contextNode);
    if (cssValue) {
      const parsedCss = parseMetricFromText(cssValue, /([\d,\.]+)/i);
      if (typeof parsedCss === 'number' && !Number.isNaN(parsedCss)) {
        logSummaryExtraction(`CSS clanTh: ${parsedCss} from "${cssValue}"`);
        summary.clanTh = parsedCss;
      } else {
        logSummaryExtraction(`CSS clanTh selector returned non-numeric "${cssValue}"`);
      }
    }
  }
  const position = applyWithLog('position', (text) => parsePositionFromText(text));
  if (typeof position === 'number' && !Number.isNaN(position)) {
    summary.position = position;
  } else {
    const cssValue = getTextBySelectors(CLAN_SUMMARY_CSS_SELECTORS.position, contextNode);
    if (cssValue) {
      const parsedPos = parsePositionFromText(cssValue);
      if (typeof parsedPos === 'number' && !Number.isNaN(parsedPos)) {
        logSummaryExtraction(`CSS position: ${parsedPos} from "${cssValue}"`);
        summary.position = parsedPos;
      } else {
        logSummaryExtraction(`CSS position selector returned "${cssValue}"`);
      }
    }
  }
  const currentBlocks = applyWithLog('currentBlocks', (text) => parseMetricFromText(text, /([\d,\.]+)/i));
  if (typeof currentBlocks === 'number' && !Number.isNaN(currentBlocks)) {
    summary.currentBlocks = currentBlocks;
  } else {
    const cssValue = getTextBySelectors(CLAN_SUMMARY_CSS_SELECTORS.currentBlocks, contextNode);
    if (cssValue) {
      const parsedBlocks = parseMetricFromText(cssValue, /([\d,\.]+)/i);
      if (typeof parsedBlocks === 'number' && !Number.isNaN(parsedBlocks)) {
        logSummaryExtraction(`CSS blocks: ${parsedBlocks} from "${cssValue}"`);
        summary.currentBlocks = parsedBlocks;
      } else {
        logSummaryExtraction(`CSS blocks selector returned "${cssValue}"`);
      }
    }
  }
  const btc = applyWithLog('btc', (text) => parseMetricFromText(text, /([\d,\.]+)/i));
  if (typeof btc === 'number' && !Number.isNaN(btc)) {
    summary.btc = btc;
  } else {
    const cssValue = getTextBySelectors(CLAN_SUMMARY_CSS_SELECTORS.btc, contextNode);
    if (cssValue) {
      const parsedBtc = parseMetricFromText(cssValue, /([\d,\.]+)/i);
      if (typeof parsedBtc === 'number' && !Number.isNaN(parsedBtc)) {
        logSummaryExtraction(`CSS btc: ${parsedBtc} from "${cssValue}"`);
        summary.btc = parsedBtc;
      } else {
        logSummaryExtraction(`CSS btc selector returned "${cssValue}"`);
      }
    }
  }
  const league = applyWithLog('league');
  if (league) summary.league = league;
  const users = applyWithLog('users', (text) => parseMetricFromText(text, /(\d{1,4}(?:,\d{3})*)/));
  if (typeof users === 'number' && !Number.isNaN(users)) summary.users = users;
  const earnedGmt = applyWithLog('earnedGmt', (text) => parseMetricFromText(text, /([\d,\.]+)/));
  if (typeof earnedGmt === 'number' && !Number.isNaN(earnedGmt)) summary.earnedGmt = earnedGmt;
  const freeToJoin = applyWithLog('freeToJoin');
  if (freeToJoin) summary.freeToJoin = freeToJoin;
  fillSummaryFallbacks(summary, contextNode);
}

function extractNumericByPattern(root, matchPattern, numberPattern = /([\d,\.]+)/) {
  if (!root || !root.querySelectorAll) return null;
  const elements = root.querySelectorAll('span,div,strong');
  for (const el of elements) {
    const text = sanitizeText(el.textContent || '');
    if (!text) continue;
    if (matchPattern.test(text)) {
      const numMatch = text.match(numberPattern);
      if (numMatch && numMatch[1]) {
        return parseMetricFromText(numMatch[1], /([\d,\.]+)/);
      }
    }
  }
  return null;
}

function fillSummaryFallbacks(summary, root) {
  const context = root || getLiveClanRoot() || document;
  populateSummaryFromBadges(summary, context);
  if (summary.clanTh === undefined || summary.clanTh === null || Number.isNaN(summary.clanTh)) {
    const thVal = extractNumericByPattern(context, /TH\/?s/i);
    if (typeof thVal === 'number' && !Number.isNaN(thVal)) {
      console.log('[MW summary fallback] clanTh from pattern', thVal);
      summary.clanTh = thVal;
    }
  }
  if (summary.currentBlocks === undefined || summary.currentBlocks === null || Number.isNaN(summary.currentBlocks)) {
    const blocksVal = extractNumericByPattern(context, /blocks?/i);
    if (typeof blocksVal === 'number' && !Number.isNaN(blocksVal)) {
      console.log('[MW summary fallback] currentBlocks from pattern', blocksVal);
      summary.currentBlocks = blocksVal;
    }
  }
  if (summary.btc === undefined || summary.btc === null || Number.isNaN(summary.btc)) {
    const btcVal = extractNumericByPattern(context, /BTC|₿/i);
    if (typeof btcVal === 'number' && !Number.isNaN(btcVal)) {
      console.log('[MW summary fallback] btc from pattern', btcVal);
      summary.btc = btcVal;
    }
  }
  if (summary.position === undefined || summary.position === null || Number.isNaN(summary.position)) {
    const elements = context.querySelectorAll('span,div');
    for (const el of elements) {
      const text = sanitizeText(el.textContent || '');
      if (!text) continue;
      const match = text.match(/#\s*(\d{1,3})/);
      if (match) {
        const pos = parseInt(match[1], 10);
        if (!Number.isNaN(pos)) {
          summary.position = pos;
          break;
        }
      }
    }
  }
}

function populateSummaryFromBadges(summary, context) {
  if (!context || !context.querySelectorAll) return;
  const badgeSelectors = [
    '.badge-game span',
    '.badge-game .d-inline-block',
    '.badge-game--with-icon span'
  ];
  const badges = [];
  const seen = new Set();
  badgeSelectors.forEach((selector) => {
    context.querySelectorAll(selector).forEach((node) => {
      if (!node || seen.has(node)) return;
      seen.add(node);
      badges.push(node);
    });
  });
  if (!badges.length) return;
  const texts = badges.map((node) => sanitizeText(node.textContent || '')).filter(Boolean);
  const parseValue = (text) => {
    const match = text.match(/([\d,\.]+)/);
    if (!match) return null;
    return parseMetricFromText(match[1], /([\d,\.]+)/);
  };
  if ((summary.clanTh === undefined || summary.clanTh === null || Number.isNaN(summary.clanTh)) && texts[0]) {
    const thVal = parseValue(texts[0]);
    if (typeof thVal === 'number' && !Number.isNaN(thVal)) {
      logSummaryExtraction(`Badges clanTh: ${thVal} from "${texts[0]}"`);
      summary.clanTh = thVal;
    }
  }
  if ((summary.users === undefined || summary.users === null || Number.isNaN(summary.users)) && texts[1]) {
    const membersVal = parseValue(texts[1]);
    if (typeof membersVal === 'number' && !Number.isNaN(membersVal)) {
      logSummaryExtraction(`Badges members: ${membersVal} from "${texts[1]}"`);
      summary.users = membersVal;
    }
  }
  if ((summary.currentBlocks === undefined || summary.currentBlocks === null || Number.isNaN(summary.currentBlocks)) && texts[2]) {
    const blocksVal = parseValue(texts[2]);
    if (typeof blocksVal === 'number' && !Number.isNaN(blocksVal)) {
      logSummaryExtraction(`Badges blocks: ${blocksVal} from "${texts[2]}"`);
      summary.currentBlocks = blocksVal;
    }
  }
  if ((summary.btc === undefined || summary.btc === null || Number.isNaN(summary.btc)) && texts[3]) {
    const btcVal = parseValue(texts[3]);
    if (typeof btcVal === 'number' && !Number.isNaN(btcVal)) {
      logSummaryExtraction(`Badges btc: ${btcVal} from "${texts[3]}"`);
      summary.btc = btcVal;
    }
  }
}

async function captureClanDetailFromCard(card, log, leaderboardLocation, cardSnapshot = {}) {
  if (!card) return null;
  const clickable = findCardClickableTarget(card);
  if (!clickable) {
    log('Card missing clickable target; skipping.');
    return null;
  }
  const beforePath = window.location.pathname + window.location.search;
  clickable.scrollIntoView({ block: 'center', behavior: 'auto' });
  if (typeof clickable.click === 'function') {
    clickable.click();
  } else {
    clickable.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
  }
  const navigated = await waitForCondition(() => (window.location.pathname + window.location.search) !== beforePath, 8000);
  if (!navigated) {
    log('Click did not navigate to a detail view; skipping.');
    return null;
  }
  await waitForClanDetailReady(log);
  await waitForClanPageDataReady();
  await waitOnClanDetailView(log);
  const detailFromPage = await extractClanPageData({ skipReadyWait: true, log, waitBetweenMemberPages: true });
  const detailRoot = getLiveClanRoot();
  const summary = detailFromPage?.summary || buildSummaryFromDetail(cardSnapshot, detailRoot);
  if (cardSnapshot?.clanId && !summary?.clanId) {
    summary.clanId = cardSnapshot.clanId;
  }
  if (cardSnapshot?.clanName && !summary?.clanName) {
    summary.clanName = cardSnapshot.clanName;
  }
  if (!summary?.clanId) {
    log('Detail view did not expose a clan ID.');
  } else {
    log(`Captured clan ${summary.clanName || summary.clanId} via live navigation.`);
  }
  const members = Array.isArray(detailFromPage?.members) && detailFromPage.members.length
    ? detailFromPage.members
    : extractClanMembers(detailRoot);
  if (summary?.clanId) {
    summary.url = summary.url || `${window.location.origin}/miner-wars/pool/${summary.clanId}`;
  }
  const detail = {
    summary,
    members,
    logs: Array.isArray(detailFromPage?.logs) ? detailFromPage.logs : []
  };
  await sleep(250);
  const targetLocation = leaderboardLocation || beforePath;
  window.history.back();
  await waitForCondition(() => (window.location.pathname + window.location.search) === targetLocation, 8000);
  await sleep(250);
  return detail;
}

async function collectClanDetailsViaClicks(urls, log, clickCaptureLimit = Infinity) {
  const leaderboardLocation = window.location.pathname + window.location.search;
  const scrollContainer = findLeaderboardScrollable();
  const scrollStep = scrollContainer ? Math.max(150, Math.round(scrollContainer.clientHeight * 0.4)) : 400;
  const initialScroll = scrollContainer ? scrollContainer.scrollTop : window.scrollY || window.pageYOffset || 0;
  const refreshCards = () => getLeaderboardCards();
  const processedCards = new Set();
  const processedKeys = new Set();
  let attemptsWithoutNew = 0;
  let totalAttempts = 0;
  const clanDetails = [];
  log('Preparing click-through collection for leaderboard cards.');
  while (totalAttempts < SCAN_MAX_ATTEMPTS && attemptsWithoutNew < 6) {
    if (processedCards.size >= clickCaptureLimit) break;
    const cards = refreshCards();
    let clickedCard = null;
    for (const card of cards) {
      if (processedCards.size >= clickCaptureLimit) break;
      const identifier = getCardIdentifier(card);
      if (!identifier || processedKeys.has(identifier)) continue;
      const fingerprint = getCardFingerprint(card);
      processedCards.add(fingerprint);
      processedKeys.add(identifier);
      clickedCard = card;
      break;
    }
    if (processedCards.size >= clickCaptureLimit || !clickedCard) {
      attemptsWithoutNew += 1;
      log(`No new rendered card detected (attempt ${totalAttempts + 1}).`);
      moveScroll();
      totalAttempts += 1;
      await sleep(SCAN_SCROLL_DELAY);
      continue;
    }
    attemptsWithoutNew = 0;
    totalAttempts += 1;
    log(`Clicking card ${processedCards.size} (fingerprint: ${processedCards.size ? processedCards.size : 'unknown'}).`);
    const snapshot = extractLeaderboardSnapshot(clickedCard);
    if (snapshot?.clanId) {
      urls.add(`${window.location.origin}/miner-wars/pool/${snapshot.clanId}`);
    }
    const detail = await captureClanDetailFromCard(clickedCard, log, leaderboardLocation, snapshot);
    if (detail?.summary?.clanId) {
      urls.add(`${window.location.origin}/miner-wars/pool/${detail.summary.clanId}`);
      clanDetails.push(detail);
    }
    moveScroll();
    await sleep(SCAN_SCROLL_DELAY);
  }
  if (scrollContainer) {
    scrollContainer.scrollTop = initialScroll;
  } else {
    window.scrollTo(0, initialScroll);
  }
  log(`Click-through attempts completed (${processedCards.size} unique cards examined, ${urls.size} clans captured).`);

  function moveScroll() {
    if (scrollContainer) {
      const nextTop = Math.min(scrollContainer.scrollHeight - scrollContainer.clientHeight, scrollContainer.scrollTop + scrollStep);
      scrollContainer.scrollTop = Math.max(0, nextTop);
      scrollContainer.dispatchEvent(new Event('scroll', { bubbles: true }));
      triggerVirtualScroll(scrollContainer);
    } else {
      window.scrollBy(0, scrollStep);
    }
  }

  return clanDetails;
}

function sanitizeButtonText(value) {
  return sanitizeText(value || '');
}

function findShowMoreButton() {
  const patterns = [/show\s*more/i, /load\s*more/i, /more\s*clans/i, /show\s*all/i, /view\s*all/i];
  return Array.from(document.querySelectorAll('button, a'))
    .find((btn) => patterns.some((pattern) => pattern.test(sanitizeButtonText(btn.textContent))));
}

async function expandLeaderboardCards() {
  const maxClicks = 5;
  let clicks = 0;
  while (clicks < maxClicks) {
    const btn = findShowMoreButton();
    if (!btn) break;
    btn.click();
    clicks += 1;
    await sleep(350);
  }
  return clicks;
}

function triggerVirtualScroll(container, delta = SCAN_HYDRATE_WHEEL_DELTA) {
  try {
    const event = new WheelEvent('wheel', {
      deltaY: delta,
      bubbles: true,
      cancelable: true
    });
    container.dispatchEvent(event);
  } catch (err) {
    // ignore unsupported environments
  }
}

function isVirtualViewport(el) {
  if (!el) return false;
  const tag = el.tagName?.toLowerCase();
  return tag === 'cdk-virtual-scroll-viewport' || typeof el.scrollToIndex === 'function';
}

function advanceLeaderboardScroll(container, step = 1) {
  if (!container) return 'none';
  if (isVirtualViewport(container)) {
    const prevIndex = container.__mwVirtualIndex ?? 0;
    const nextIndex = prevIndex + step;
    container.__mwVirtualIndex = nextIndex;
    if (typeof container.scrollToIndex === 'function') {
      container.scrollToIndex(nextIndex);
    } else if (typeof container.scrollToOffset === 'function') {
      container.scrollToOffset(nextIndex * (container.clientHeight || 1));
    } else if (container.scrollTop !== undefined) {
      container.scrollTop = nextIndex * (container.clientHeight || step * 40);
    }
    container.dispatchEvent(new Event('scroll', { bubbles: true }));
    triggerVirtualScroll(container);
    return `virtual:${nextIndex}`;
  }
  const scrollStep = Math.max(150, Math.round(container.clientHeight * 0.4));
  const nextTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollStep);
  container.scrollTop = Math.max(0, nextTop);
  container.dispatchEvent(new Event('scroll', { bubbles: true }));
  triggerVirtualScroll(container);
  return 'physical';
}

function resetLeaderboardVirtualIndex(container) {
  if (isVirtualViewport(container)) {
    container.__mwVirtualIndex = 0;
  }
}

function findLeaderboardScrollable() {
  const preferred = [
    '.miner-wars-leaderboard__inner',
    '.miner-wars-leaderboard',
    '.leaderboard-scrollable',
    '.leaderboard-container'
  ];
  for (const selector of preferred) {
    const el = document.querySelector(selector);
    if (el && el.scrollHeight > el.clientHeight) {
      return el;
    }
  }
  const overflowCandidates = Array.from(document.querySelectorAll('div'))
    .filter((el) => {
      const style = window.getComputedStyle(el);
      return /(auto|scroll)/.test(style.overflowY || '') && el.scrollHeight > el.clientHeight;
    });
  if (overflowCandidates.length) {
    return overflowCandidates[0];
  }
  if (document.scrollingElement) {
    return document.scrollingElement;
  }
  return document.body;
}

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

async function waitForCondition(predicate, timeout = 6000, interval = 200) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    try {
      if (predicate()) return true;
    } catch (_) {}
    await sleep(interval);
  }
  return false;
}

async function collectClanPageUrlsFromLeaderboard(options = {}) {
  const { clickCaptureLimit = Infinity } = options || {};
  const urls = new Set();
  const scanLogs = [];
  const log = (message) => scanLogs.push(message);
  gatherClanUrlsFromDom(urls);
  log(`Hydrated leaderboard cards: ${urls.size} clans detected before scrolling.`);
  const scrollContainer = findLeaderboardScrollable();
  const isVirtual = scrollContainer && isVirtualViewport(scrollContainer);
  if (scrollContainer) {
    const descriptor = `${scrollContainer.tagName}${scrollContainer.id ? `#${scrollContainer.id}` : ''}${scrollContainer.className ? `.${scrollContainer.className.split(' ').join('.')}` : ''}`;
    log(`Leaderboard container detected (${descriptor}) using ${isVirtual ? 'virtual' : 'physical'} scroll.`);
  } else {
    log('No scrollable leaderboard container detected. Falling back to window-level scrolling.');
  }
  if (scrollContainer) {
    const initialScroll = isVirtual ? (scrollContainer.__mwVirtualIndex ?? 0) : scrollContainer.scrollTop;
    const scrollStep = isVirtual ? 0 : Math.max(200, Math.round(scrollContainer.clientHeight * 0.75));
    scrollContainer.scrollTop = 0;
    const expandedClicks = await expandLeaderboardCards();
    if (expandedClicks) {
      log(`Expanded leaderboard via Show more button (${expandedClicks} click${expandedClicks !== 1 ? 's' : ''}).`);
      gatherClanUrlsFromDom(urls);
      log(`Re-hydrated leaderboard cards after expansion: ${urls.size} clans detected.`);
    } else {
      log('No Show more button detected or leaderboard already fully expanded.');
    }
    for (let attempt = 0; attempt < SCAN_MAX_ATTEMPTS; attempt++) {
      const before = urls.size;
      const movement = advanceLeaderboardScroll(scrollContainer, isVirtual ? VIRTUAL_SCROLL_STEP : 1);
      log(`Scroll attempt ${attempt + 1} moved (${movement}).`);
      await sleep(SCAN_SCROLL_DELAY);
      gatherClanUrlsFromDom(urls);
      const added = urls.size - before;
      log(`Scroll attempt ${attempt + 1}: +${added} new clan${added !== 1 ? 's' : ''} (total ${urls.size}).`);
      const atBottom = scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1;
      if (urls.size === before && atBottom) {
        break;
      }
      if (atBottom) {
        await sleep(SCAN_SCROLL_DELAY);
        if (urls.size === before) {
          break;
        }
      }
    }
    scrollContainer.scrollTop = initialScroll;
  }
  let clanDetails = [];
  if (clickCaptureLimit > 0) {
    log('Starting click-through collection to capture clan details via live navigation.');
    clanDetails = await collectClanDetailsViaClicks(urls, log, clickCaptureLimit);
    log(`Final clan count after click-through: ${urls.size}.`);
  } else {
    log('Skipping live click-through capture (disabled via options).');
  }
  if (urls.size) {
    return { urls: Array.from(urls), logs: scanLogs, clans: clanDetails };
  }
  const fallbackRegex = /\/miner-wars\/(?:pool|clan)\/\d+/g;
  const fallbackMatches = Array.from(new Set((document.body.innerHTML.match(fallbackRegex) || [])));
  fallbackMatches.forEach((match) => {
    const resolved = resolveUrl(window.location.href, match) || `${window.location.origin}${match}`;
    const clanId = parseClanIdFromUrl(resolved);
    const normalized = clanId ? `${window.location.origin}/miner-wars/pool/${clanId}` : resolved;
    urls.add(normalized);
  });
  if (fallbackMatches.length) {
    log(`Fallback regex detected ${urls.size} clans from page source.`);
  } else {
    log('No clans detected via DOM selectors or fallback regex.');
  }
  return { urls: Array.from(urls), logs: scanLogs, clans: clanDetails };
}

async function fetchDocument(url) {
  const response = await fetch(url, { credentials: 'include' });
  if (!response.ok) {
    if (response.status === 403) {
      notifyTokenExpired(`Request blocked (${response.status}). Refresh Mining Wars page.`);
    }
    throw new Error(`Failed to load ${url} (${response.status})`);
  }
  const text = await response.text();
  const parser = new DOMParser();
  return parser.parseFromString(text, 'text/html');
}

function extractClanSummaryFromDoc(root, url) {
  const summaryText = sanitizeText(root.textContent || document.body.textContent || '');
  const summary = {
    time_scrape: new Date().toISOString(),
    url,
    clanId: parseClanIdFromUrl(url),
    clanName: findClanNameFromPool(root),
    clanTh: parseMetricFromText(summaryText, /([\d,\.]+)\s*TH\b/i),
    position: parsePositionFromText(summaryText),
    league: sanitizeText((summaryText.match(/League\s*[:\-]?\s*([A-Za-z]+)/i) || [])[1]),
    users: parseMetricFromText(summaryText, /(\d{1,4}(?:,\d{3})*)\s*(?:users|members)/i),
    earnedGmt: parseMetricFromText(summaryText, /([\d,\.]+)\s*GMT\b/i),
    description: findDescription(root),
    freeToJoin: findFreeToJoinStatus(root),
    currentBlocks: parseMetricFromText(summaryText, /([\d,\.]+)\s*(?:blocks?|block power)/i),
    btc: parseMetricFromText(summaryText, /([\d,\.]+)\s*BTC\b/i)
  };
  applySummaryXPaths(summary, root);
  if (!summary.clanName) {
    summary.clanName = findClanNameFromPool(root);
  }
  return summary;
}

function findClanMemberPageLinks(doc, baseUrl) {
  const links = new Set();
  doc.querySelectorAll('a[href]').forEach((anchor) => {
    const href = anchor.getAttribute('href');
    if (!href) return;
    if (!/page=\d+/i.test(href)) return;
    const resolved = resolveUrl(baseUrl, href);
    if (resolved) links.add(resolved);
  });
  return Array.from(links);
}

async function scrapeClanPagesFromUrl(startUrl) {
  const visited = new Set();
  const queue = [startUrl];
  const pages = [];
  while (queue.length) {
    const pageUrl = queue.shift();
    if (!pageUrl || visited.has(pageUrl)) continue;
    visited.add(pageUrl);
    const doc = await fetchDocument(pageUrl);
    const root = doc.querySelector('miner-wars-pool') || doc.body;
    resetSummaryExtractionLogs();
    const summary = extractClanSummaryFromDoc(root, pageUrl);
    const summaryLogs = getSummaryExtractionLogs();
    const members = extractClanMembers(root);
    pages.push({ url: pageUrl, summary, members, logs: summaryLogs });
    const additional = findClanMemberPageLinks(doc, pageUrl);
    additional.forEach((link) => {
      if (!visited.has(link)) queue.push(link);
    });
  }
  if (!pages.length) {
    throw new Error('Failed to scrape clan pages.');
  }
  const mergedMembers = [];
  const seenMembers = new Set();
  pages.forEach((page) => {
    page.members.forEach((member) => {
      const key = (member.userName || '').toLowerCase();
      if (!key || seenMembers.has(key)) return;
      seenMembers.add(key);
      mergedMembers.push(member);
    });
  });
  const result = {
    summary: pages[0].summary,
    members: mergedMembers,
    pageCount: pages.length,
    source: startUrl,
    logs: pages[0].logs || []
  };
  return result;
}

function readAccessTokenCookie() {
  try {
    const match = document.cookie.match(/(?:^|;\s*)access_token=([^;]+)/);
    return match ? decodeURIComponent(match[1]) : null;
  } catch (err) {
    return null;
  }
}

async function fetchClanApiJson(url, token, body) {
  const headers = {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'x-device-type': 'desktop'
  };
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }
  const res = await fetch(url, {
    method: 'POST',
    headers,
    credentials: 'include',
    body: body ? JSON.stringify(body) : undefined
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    if (res.status === 403 && text.includes('JWT_TOKEN_EXPIRED')) {
      notifyTokenExpired(`Request blocked (${res.status}). Refresh Mining Wars page.`);
    }
    throw new Error(text || `Clan API failed (${res.status})`);
  }
  return res.json();
}

async function fetchClanApiData(payload = {}) {
  const token = readAccessTokenCookie();
  const meta = {
    tokenFound: Boolean(token),
    usedAuthHeader: Boolean(token)
  };
  const myClan = await fetchClanApiJson('https://api.gomining.com/api/nft-game/clan/get-my', token);
  meta.myClanStatus = 200;
  const clanId = myClan?.data?.id ?? myClan?.data?.clanId ?? null;
  if (!clanId) {
    throw new Error('Clan ID not found in get-my response.');
  }
  const limit = Number.isFinite(Number(payload.limit)) ? Number(payload.limit) : 10;
  const skip = Number.isFinite(Number(payload.skip)) ? Number(payload.skip) : 0;
  const count = Number.isFinite(Number(payload.count)) ? Number(payload.count) : 0;
  const body = {
    clanId,
    pagination: { limit, skip, count },
    filters: { filterType: 'none' },
    sort: { sortType: 'none' }
  };
  const clanData = await fetchClanApiJson('https://api.gomining.com/api/nft-game/clan/get-by-id', token, body);
  meta.clanStatus = 200;
  return { clanId, myClan, clanData, meta };
}

const EXTRACTION_COMMANDS = new Set([
  'EXTRACT_LEADERBOARD',
  'EXTRACT_CLAN_PAGE',
  'FETCH_CLAN_API',
  'SCRAPE_LEADERBOARD_CLANS',
  'COLLECT_CLAN_URLS',
  'EXTRACT_CLAN_FROM_URL',
  'SCRAPE_CLAN_FROM_URL',
  'SCRAPE_ROUND_HISTORY',
  'NAVIGATE_TO_CLAN_URL',
  'RESTORE_PREVIOUS_URL',
  // Return the display name of the currently logged-in player (best-effort)
  'EXTRACT_MY_USERNAME',
  // Return the current clan name and optional clan id from the live Mining Wars screen
  'EXTRACT_MY_CLAN_NAME',
  // Return the current clan percentage from the live Mining Wars screen
  'EXTRACT_MY_CLAN_PERCENT'
]);

async function handleExtractionCommand(type, payload = {}) {
  switch (type) {
    case 'EXTRACT_LEADERBOARD':
      return extractLeaderboardData();
    case 'EXTRACT_CLAN_PAGE':
      return await extractClanPageData(payload);
    case 'FETCH_CLAN_API':
      return await fetchClanApiData(payload);
    case 'SCRAPE_LEADERBOARD_CLANS':
      return scrapeLeaderboardClans();
    case 'COLLECT_CLAN_URLS':
      return await collectClanPageUrlsFromLeaderboard();
    case 'EXTRACT_CLAN_FROM_URL':
      if (!payload?.url) throw new Error('Missing URL');
      return extractClanFromUrl(payload.url, payload);
    case 'SCRAPE_CLAN_FROM_URL':
      if (!payload?.url) throw new Error('Missing URL');
      return scrapeClanPagesFromUrl(payload.url);
    case 'SCRAPE_ROUND_HISTORY':
      return scrapeRoundHistoryWithDetails(payload);
    case 'NAVIGATE_TO_CLAN_URL':
      if (!payload?.url) throw new Error('Missing URL');
      return navigateToClanUrl(payload.url);
    case 'RESTORE_PREVIOUS_URL':
      return restorePreviousUrl(payload);

    // Best-effort: try to find the currently logged-in player's display name from the page
    case 'EXTRACT_MY_USERNAME': {
      try {
        // Prefer round detail "me" selector if present
        const fromRound = textFromXPath(ROUND_DETAIL_XPATHS.meName);
        if (fromRound) return (fromRound || '').trim();

        // Try some common profile selectors seen in the app
        const selCandidates = [
          '.page-top__btn-profile',
          '[data-qa="page-top__btn-profile"]',
          '.profile-top',
          '.page-top__avatar',
          '.user-profile',
          '.header .user',
          '.user-name'
        ];
        for (const s of selCandidates) {
          const el = document.querySelector(s);
          if (el && el.textContent && el.textContent.trim()) return el.textContent.trim();
        }

        // Fallback: meta tags or title
        const meta = document.querySelector('meta[name="username"]') || document.querySelector('meta[property="profile:username"]');
        if (meta && meta.content) return meta.content.trim();
        if (document.title && document.title.includes('-')) {
          // Some pages include "username - Miner Wars"
          return document.title.split('-')[0].trim();
        }

        return null;
      } catch (err) {
        return null;
      }
    }
    case 'EXTRACT_MY_CLAN_NAME': {
      try {
        return extractMyClanFromLivePanel();
      } catch (err) {
        return { name: null, clanId: null };
      }
    }
    case 'EXTRACT_MY_CLAN_PERCENT': {
      try {
        const percent = textFromXPath(LIVE_MY_CLAN_PCT_XPATH);
        return { percentText: percent || null };
      } catch (err) {
        return { percentText: null };
      }
    }

    default:
      throw new Error(`Unsupported command ${type}`);
  }
}

async function navigateToClanUrl(target) {
  const targetUrl = resolveUrl(window.location.href, target);
  if (!targetUrl) throw new Error('Invalid navigation target.');
  const previousUrl = window.location.href;
  const previousScroll = document.scrollingElement ? document.scrollingElement.scrollTop : 0;
  if (!urlsMatch(previousUrl, targetUrl)) {
    window.location.href = targetUrl;
    const navigated = await waitForCondition(() => urlsMatch(window.location.href, targetUrl), 15000, 200);
    if (!navigated) throw new Error('Navigation to clan page timed out.');
  }
  await waitForClanPageDataReady();
  await waitOnClanDetailView();
  return { previousUrl, previousScroll };
}

async function restorePreviousUrl(payload = {}) {
  const targetUrl = payload.url;
  const targetScroll = typeof payload.scroll === 'number' ? payload.scroll : null;
  if (targetUrl) {
    if (!urlsMatch(window.location.href, targetUrl)) {
      window.location.href = targetUrl;
      await waitForCondition(() => urlsMatch(window.location.href, targetUrl), 15000, 200);
    }
  } else {
    window.history.back();
  }
  if (document.scrollingElement && targetScroll !== null) {
    document.scrollingElement.scrollTop = targetScroll;
  }
  await waitForLeaderboardReady();
  return { ok: true };
}

async function extractClanFromUrl(url, options = {}) {
  const targetUrl = resolveUrl(window.location.href, url);
  if (!targetUrl) {
    throw new Error('Invalid clan URL.');
  }
  const previousUrl = window.location.href;
  const previousScroll = document.scrollingElement ? document.scrollingElement.scrollTop : 0;
  const logDetail = (message) => console.log('[MW detail]', message);
  const scrapeActiveClanPage = async () => {
    await waitForClanPageDataReady();
    await waitOnClanDetailView(logDetail);
    return await extractClanPageData({ skipReadyWait: true, log: logDetail, waitBetweenMemberPages: Boolean(options?.waitBetweenMemberPages) });
  };
  if (urlsMatch(previousUrl, targetUrl)) {
    return await scrapeActiveClanPage();
  }
  window.location.href = targetUrl;
  const navigated = await waitForCondition(() => urlsMatch(window.location.href, targetUrl), 12000, 200);
  if (!navigated) {
    throw new Error('Navigation to clan page timed out.');
  }
  const detail = await scrapeActiveClanPage();
  window.history.back();
  const restored = await waitForCondition(() => urlsMatch(window.location.href, previousUrl), 12000, 200);
  if (!restored) {
    console.warn('[MW] Could not automatically return to the leaderboard. Remaining on clan page.');
  } else {
    if (document.scrollingElement) {
      document.scrollingElement.scrollTop = previousScroll;
    }
    await waitForLeaderboardReady();
  }
  await sleep(500);
  return detail;
}

async function scrapeLeaderboardClans() {
  const { urls = [] } = await collectClanPageUrlsFromLeaderboard();
  console.log('[MW] scraping clans', urls.length);
  const clans = [];
  for (const url of urls) {
    try {
      const clanData = await scrapeClanPagesFromUrl(url);
      clans.push(clanData);
    } catch (err) {
      console.warn('[MW] Clan scrape failed for', url, err);
    }
  }
  return { clans };
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (!msg?.type) return;
  console.log('[MW] Received message', msg.type);

  // Handle API calls with authentication
  if (msg.type === 'API_CALL') {
    const logApi = shouldLogApiCall(msg.endpoint);
    if (logApi) {
      console.log('[MW Content] Received API_CALL:', msg.endpoint);
      console.log('[MW Content] Payload:', msg.payload);
    }
    (async () => {
      try {
        // Read access token from cookies for Authorization header
        const readToken = () => {
          const match = document.cookie.match(/(?:^|; )access_token=([^;]+)/);
          return match ? decodeURIComponent(match[1]) : '';
        };

        const token = readToken();
        const headers = { 'Content-Type': 'application/json' };
        if (token) {
          headers.Authorization = `Bearer ${token}`;
          if (logApi) {
            console.log('[MW Content] Added Authorization header with token');
          }
        } else {
          if (logApi) {
            console.warn('[MW Content] No access_token cookie found');
          }
        }

        if (logApi) {
          console.log('[MW Content] Making fetch request with credentials and auth header');
        }
        const response = await fetch(msg.endpoint, {
          method: msg.method || 'POST',
          headers,
          body: msg.payload ? JSON.stringify(msg.payload) : undefined,
          credentials: 'include' // Include cookies
        });

        if (logApi) {
          console.log('[MW Content] Fetch response status:', response.status);
        }
        const data = await response.json();
        if (logApi) {
          console.log('[MW Content] Response data:', data);
          console.log('[MW Content] Sending response back:', { success: response.ok, status: response.status });
        }

        sendResponse({
          success: response.ok,
          status: response.status,
          data
        });
      } catch (err) {
        console.error('[MW Content] API call error:', err);
        sendResponse({
          success: false,
          error: err.message || 'API call failed'
        });
      }
    })();
    return true; // Keep channel open for async response
  }

  if (EXTRACTION_COMMANDS.has(msg.type)) {
    handleExtractionCommand(msg.type, msg)
      .then((data) => sendResponse({ ok: true, data }))
      .catch((err) => sendResponse({ ok: false, error: err?.message || 'Extraction failed.' }));
    return true;
  }
});

// Inject a page script to tap WebSocket messages inside the page context (CSP-safe via src).
function injectHook() {
  // No-op: handled via background scripting.executeScript to bypass CSP.
}

injectHook();

}
window.addEventListener('unhandledrejection', (event) => {
  const reason = event && event.reason;
  const text = typeof reason === 'string'
    ? reason
    : (reason?.message || reason?.description || reason?.statusMessage || '');
  if (text && text.toLowerCase().includes('jwt token is expired')) {
    notifyTokenExpired(text);
  }
});

window.addEventListener('error', (event) => {
  const text = event?.error?.message || event?.message || '';
  if (text && text.toLowerCase().includes('jwt token is expired')) {
    notifyTokenExpired(text);
  }
});

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    parseWinnerSectionStats,
    extractWinnerStatsFromModal
  };
}
