// ============ Console Interceptor for Complete Logging ============
const CONSOLE_LOG_STORAGE_KEY = 'mw_console_log';
const MAX_CONSOLE_LOG_SIZE = 100000; // characters

// Store for console logs
let consoleLogBuffer = '';

const LOG_THROTTLE_MS = 20000;
const logThrottleState = new Map();
const getLogThrottleKey = (level, args) => {
  const first = args && args.length ? args[0] : '';
  if (typeof first === 'string' && first.trim()) {
    const trimmed = first.trim();
    return `${level}|${trimmed.slice(0, 140)}`;
  }
  const typeTag = first && typeof first === 'object' ? 'object' : typeof first;
  return `${level}|<${typeTag}>`;
};
const getLogThrottleDecision = (level, args) => {
  const key = getLogThrottleKey(level, args);
  const now = Date.now();
  const entry = logThrottleState.get(key) || { lastAt: 0, suppressed: 0 };
  if (entry.lastAt && now - entry.lastAt < LOG_THROTTLE_MS) {
    entry.suppressed += 1;
    logThrottleState.set(key, entry);
    return { allow: false, summary: null };
  }
  const elapsedSec = entry.lastAt ? Math.max(1, (now - entry.lastAt) / 1000) : null;
  const summary = entry.suppressed > 0
    ? `[MW][THROTTLE] Suppressed ${entry.suppressed} logs for "${key}" over ${Math.round(elapsedSec)}s (~${(entry.suppressed / elapsedSec).toFixed(1)}/s).`
    : null;
  logThrottleState.set(key, { lastAt: now, suppressed: 0 });
  return { allow: true, summary };
};

// Function to append to console log buffer
function appendConsoleLog(level, args) {
  const timestamp = new Date().toISOString();
  const message = args.map(arg => {
    if (typeof arg === 'object') {
      try {
        return JSON.stringify(arg, null, 2);
      } catch (e) {
        return String(arg);
      }
    }
    return String(arg);
  }).join(' ');

  const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
  consoleLogBuffer += logLine;

  // Keep buffer size manageable
  if (consoleLogBuffer.length > MAX_CONSOLE_LOG_SIZE) {
    consoleLogBuffer = consoleLogBuffer.slice(-MAX_CONSOLE_LOG_SIZE);
  }

  // Store in localStorage
  try {
    localStorage.setItem(CONSOLE_LOG_STORAGE_KEY, consoleLogBuffer);
  } catch (err) {
    // Ignore storage errors
  }
}

// Intercept console methods
const originalConsole = {
  log: console.log,
  error: console.error,
  warn: console.warn,
  info: console.info,
  debug: console.debug
};

// Force all locale date/time rendering to UTC+0 (GoMining time)
(() => {
  const UTC_TZ = 'UTC';
  const originals = {
    toLocaleString: Date.prototype.toLocaleString,
    toLocaleTimeString: Date.prototype.toLocaleTimeString,
    toLocaleDateString: Date.prototype.toLocaleDateString
  };
  const withUtc = (options) => {
    if (options && typeof options === 'object') {
      if (options.timeZone) return options;
      return { ...options, timeZone: UTC_TZ };
    }
    return { timeZone: UTC_TZ };
  };
  Date.prototype.toLocaleString = function(locales, options) {
    return originals.toLocaleString.call(this, locales, withUtc(options));
  };
  Date.prototype.toLocaleTimeString = function(locales, options) {
    return originals.toLocaleTimeString.call(this, locales, withUtc(options));
  };
  Date.prototype.toLocaleDateString = function(locales, options) {
    return originals.toLocaleDateString.call(this, locales, withUtc(options));
  };
})();

console.log = function(...args) {
  const decision = getLogThrottleDecision('log', args);
  if (!decision.allow) return;
  if (decision.summary) {
    appendConsoleLog('log', [decision.summary]);
    originalConsole.log.call(console, decision.summary);
  }
  appendConsoleLog('log', args);
  originalConsole.log.apply(console, args);
};

console.error = function(...args) {
  const decision = getLogThrottleDecision('error', args);
  if (!decision.allow) return;
  if (decision.summary) {
    appendConsoleLog('error', [decision.summary]);
    originalConsole.error.call(console, decision.summary);
  }
  appendConsoleLog('error', args);
  originalConsole.error.apply(console, args);
};

console.warn = function(...args) {
  const decision = getLogThrottleDecision('warn', args);
  if (!decision.allow) return;
  if (decision.summary) {
    appendConsoleLog('warn', [decision.summary]);
    originalConsole.warn.call(console, decision.summary);
  }
  appendConsoleLog('warn', args);
  originalConsole.warn.apply(console, args);
};

console.info = function(...args) {
  const decision = getLogThrottleDecision('info', args);
  if (!decision.allow) return;
  if (decision.summary) {
    appendConsoleLog('info', [decision.summary]);
    originalConsole.info.call(console, decision.summary);
  }
  appendConsoleLog('info', args);
  originalConsole.info.apply(console, args);
};

console.debug = function(...args) {
  const decision = getLogThrottleDecision('debug', args);
  if (!decision.allow) return;
  if (decision.summary) {
    appendConsoleLog('debug', [decision.summary]);
    originalConsole.debug.call(console, decision.summary);
  }
  appendConsoleLog('debug', args);
  originalConsole.debug.apply(console, args);
};

// Clear console log on startup
localStorage.removeItem(CONSOLE_LOG_STORAGE_KEY);
consoleLogBuffer = '';
console.log('[MW] Console logging initialized - all console output will be captured');

import { fetchLiveRounds, fetchLicenses, upsertClanMatch, insertFinishedRound, fetchClanMappings } from './data-hooks.js';
import {
  describeRoundRecord,
  extractRoundFinishedData,
  extractTop10Players,
  extractTop10Clans,
  waitForRoundFinishedComplete
} from './live-utils.js';

// DOM element references - will be initialized after DOMContentLoaded
let tabButtons;
let panels;
let themeSelect;
let resizer;
let logoClickBound = false;
let autoDefaultTabApplied = false;
const shell = document.documentElement;
const HAS_CHROME_RUNTIME = typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.onMessage;
const WEBSITE_MODE = !HAS_CHROME_RUNTIME;

const themeMap = { neon: 'neon', pastel: 'pastel', dark: 'dark', cavern: 'cavern' };
const ORDER_STORAGE_KEY = 'redesign-card-order';
const CALC_STORAGE_KEY = 'mw_calc_inputs';
const CALC_PROFILE_DEFAULT = 'balanced';
const CALC_PROFILE_MODE_KEY = 'calc_profile_mode';
const CALC_PROFILE_BUDGET_KEY = 'calc_profile_budget_week';
const CALC_PROFILE_ROUND_CAP_KEY = 'calc_profile_round_cap';
const CALC_PROFILE_STRICT_KEY = 'calc_profile_strict';
const LIVE_PROFILE_CLAN_KEY = 'mw_live_profile_clan';
const HEADER_COLLAPSE_STORAGE_KEY = 'mw_header_collapsed';
const LIVE_POWER_SOURCE_KEY = 'mw_live_power_source';
const LIVE_POWER_SOURCE_DEFAULT = 'roster';
const LIVE_POWER_FIELDS = [
  'power',
  'currentPower',
  'roundPower',
  'powerCurrent',
  'powerNow',
  'totalPower',
  'powerTotal'
];
const LIVE_POWER_SOURCE_ORDER = ['roster', 'clanPower', ...LIVE_POWER_FIELDS];
const LIVE_POWER_SOURCE_LABELS = {
  roster: 'Roster (current)',
  clanPower: 'Clan power (ws)',
  power: 'power',
  currentPower: 'currentPower',
  roundPower: 'roundPower',
  powerCurrent: 'powerCurrent',
  powerNow: 'powerNow',
  totalPower: 'totalPower',
  powerTotal: 'powerTotal'
};
const LIVE_SCORE_SOURCE_KEY = 'mw_live_score_source';
const LIVE_SCORE_SOURCE_DEFAULT = 'pps_ws';
const LIVE_BOOST_API_SYNC_KEY = 'mw_live_boost_api_sync';
const LIVE_SCORE_FIELDS = ['pps_ws'];
const LIVE_SCORE_SOURCE_ORDER = [...LIVE_SCORE_FIELDS];
const LIVE_SCORE_SOURCE_LABELS = {
  pps_ws: 'PPS+WS'
};
const LIVE_SCORE_TOTAL_FIELDS = [
  'pps_ws'
];
const LIVE_SCORE_SOURCE_KIND = {
  pps_ws: 'total'
};
const LIVE_SCORE_HOLD_MS = 5000;
const BOOST_HELPER_CONFIG_KEY = 'mw_boost_helper_config_v1';
const FLOATING_BOOST_HELPER_STATE_KEY = 'mw_floating_boost_helper_state_v1';
const LIVE_SCORE_SORT_HYSTERESIS_PCT = 0.005;
const LIVE_SCORE_SORT_HYSTERESIS_MIN = 50000;
const LIVE_RANK_HOLD_MS = 5000;
const LIVE_SCORE_SORT_ALPHA = 0.25;
const LIVE_BOOST_ICON_TTL_MS = 120000;
const LIVE_SORT_REFRESH_MS = 5000;
let liveScoreHoldCache = new Map(); // Hold last known score briefly
let liveRankHoldCache = new Map(); // Hold last known rank briefly
let liveScoreSortCache = new Map(); // Smoothed scores for stable ordering
let liveLastRoundSeconds = null;
let liveCacheApplying = false;
let liveStableSortOrder = [];
let liveStableSortAt = 0;
let lastAllUsersStateV2Raw = null;
let lastAllUsersStateV2At = 0;
const LEADERBOARD_API_KEY = 'clan-leaderboard';
const LEADERBOARD_DEFAULT_LEAGUE_ID = 16;
const LEADERBOARD_DEFAULT_LIMIT = 50;
const LEADERBOARD_DEFAULT_SKIP = 0;
const LEADERBOARD_LEAGUE_STORAGE_KEY = 'mw_leaderboard_league_id';
let leaderboardState = {
  loaded: false,
  loading: false,
  error: null,
  data: null,
  lastFetchedAt: null,
  payload: null
};
const LIVE_DISPLAY_MODE_KEY = 'mw_live_display_mode';
const LIVE_DISPLAY_MODE_DEFAULT = 'computed';
const LIVE_DISPLAY_MODE_ORDER = ['computed', 'raw'];
const LIVE_DISPLAY_MODE_LABELS = {
  computed: 'Computed',
  raw: 'Raw'
};
const LIVE_API_STATE_ENABLED = !WEBSITE_MODE;
const LIVE_API_STATE_REFRESH_MS = 60000; // 1 minute (fallback)
const LIVE_API_REFRESH_KEY = 'mw_live_api_refresh_interval';
const LIVE_API_REFRESH_MIN_MS = 300000;
const LIVE_API_REFRESH_MAX_MS = 400000;
const LIVE_API_REFRESH_OPTIONS = [LIVE_API_REFRESH_MIN_MS];
const LIVE_API_REFRESH_DEFAULT = LIVE_API_REFRESH_MIN_MS;
const LIVE_API_ALARM_ENABLED = false;
const LIVE_API_ALARM_START_TIMEOUT_MS = 3000;
const LIVE_API_TIMER_DELTA_MIN = 20;
const LIVE_API_TIMER_DELTA_MAX = 120;
const LIVE_PPS_DEBUG_CLAN_DEFAULT = 'kicking blocks';
const LIVE_PPS_DEBUG_CLAN_KEY = 'mw_live_boost_debug_clan_v1';
let livePpsDebugClan = LIVE_PPS_DEBUG_CLAN_DEFAULT;
const CLAN_BLOCK_PPS_SAMPLE_LIMIT = 10;
let liveApiRefreshIntervalMs = getLiveApiRefreshInterval();
let liveApiStatusWindowMs = liveApiRefreshIntervalMs * 2;
const LIVE_WS_ENABLED = true;
const LIVE_CACHE_MIN_INTERVAL_MS = 15000;
const LIVE_CACHE_FORCE_INTERVAL_MS = 60000;
let liveApiPeriodicTimer = null;
let liveApiNextAt = 0;
let liveApiNextDelayMs = 0;
let liveApiCountdownTimer = null;
let liveApiAlarmActive = false;
let liveApiAlarmLastStartAt = 0;
let liveApiInitialCallDone = false;
const LIVE_SCORE_MERGE_KEYS = [...LIVE_SCORE_FIELDS, 'activeBoostScore', 'active_boost_score'];
const LIVE_POWER_MERGE_KEYS = ['clanPower', 'clan_power', 'roster_power', 'rosterPower', ...LIVE_POWER_FIELDS];
const LIVE_PPS_MERGE_KEYS = ['base_pps', 'basePps'];
let orderStore = {};
let calculatorSimEnabled = false;
let livePowerSource = commGetLivePowerSource();
let liveScoreSource = commGetLiveScoreSource();
let liveDisplayMode = commGetLiveDisplayMode();
let liveBoostApiSyncEnabled = true;
try {
  const storedDisplayMode = localStorage.getItem(LIVE_DISPLAY_MODE_KEY);
  if (!storedDisplayMode || storedDisplayMode === 'raw') {
    liveDisplayMode = LIVE_DISPLAY_MODE_DEFAULT;
    localStorage.setItem(LIVE_DISPLAY_MODE_KEY, liveDisplayMode);
  }
} catch {}
try {
  localStorage.setItem(LIVE_BOOST_API_SYNC_KEY, '1');
} catch {}
let liveApiStateLookup = new Map();
let liveApiStateRaw = [];
let liveApiStateTransformed = [];
let liveApiStateUpdatedAt = 0;
let liveApiStateLastAttemptAt = 0;
let liveApiStatePending = false;
let liveCacheLastSentAt = 0;
let liveCacheLastHash = '';
let liveCacheTimer = null;
let liveCachePollTimer = null;
let liveCachePollPending = false;
let liveCacheLastAppliedAt = 0;
let liveCacheLastAppliedKey = '';
let liveApiLastOkAt = 0;
let liveApiLastStatus = null;
let liveApiLastErrorAt = 0;
let liveTotalScoreSeries = [];
let liveTotalScoreSeriesRound = null;
const LIVE_TOTAL_SCORE_MAX_POINTS = 240;
const LIVE_TOTAL_SCORE_DEFAULT_MINUTES = 10;
const LIVE_TOTAL_SCORE_RENDER_MIN_MS = 4000;
const LIVE_BOOST_TS_SKEW_MS = 120000;
const LIVE_TOTAL_SCORE_WINDOW_MODE_KEY = 'mw_live_total_score_window_mode';
const LIVE_TOTAL_SCORE_WINDOW_MODE_DEFAULT = 'first10';
const LIVE_TOTAL_SCORE_WINDOW_MODE_LABELS = {
  first10: 'First 10 minutes',
  first30: 'First 30 minutes',
  start: 'Start till now',
  last10: 'Last 10 minutes'
};
let liveTotalScoreChartState = { lastRenderAt: 0, lastRenderMinute: null, lastBaselineKey: null };
let liveTotalScoreWindowMode = commGetLiveTotalScoreWindowMode();
let liveTotalScoreWindowEditing = false;
let liveWsAddedScoreByClan = new Map();
let liveWsAddedScoreUpdatedAt = 0;
let liveWsAddedScoreBaselineByClan = new Map();
let liveWsDebugRateByClan = new Map();
let liveWsBoostDebugByClan = new Map();
let liveWsBoostActivationByClan = new Map();
let liveScorePopupOverlay = null;
let liveScorePopupBody = null;
let liveScorePopupTitle = null;
let liveScorePopupState = null;
let liveScorePopupTimer = null;
let liveBoostPopupOverlay = null;
let liveBoostPopupBody = null;
let liveBoostPopupTitle = null;
let liveXpathScoreValue = null;
let liveXpathPercentValue = null;
let liveXpathTotalScore = null;
let liveXpathUpdatedAt = 0;
let liveApiScoreRateMap = new Map();
let liveApiStablePpsByClan = new Map();
let liveApiLastStablePpsByClan = new Map();
let liveApiStoredBasePps = new Map();
let liveApiBasePpsValidCounts = new Map();
let liveApiBasePpsByClan = new Map();
let liveApiDebugRawByClan = new Map();
let liveApiTimerSeconds = null;
let liveApiTimerSecondsPrev = null;
let liveApiTimerUpdatedAt = 0;
let liveApiLastRoundFetchId = null;
let liveApiAutoFetchBlockId = null;
let liveApiLastLeagueId = null;
let liveApiRoundKey = null;
let liveApiPpsDebugLoggedAt = 0;
let liveApiWinsPercentMap = new Map();
let liveApiWinsPercentMeta = {
  cycleId: null,
  leagueId: null,
  updatedAt: 0,
  totalRounds: 0
};
let liveWsScoreRateMap = new Map();
let liveWsStablePpsByClan = new Map();
let liveWsBasePpsLastInsertAt = 0;
const LIVE_WS_BASE_PPS_MIN_INTERVAL_MS = 60000;
const LIVE_WINS_PERCENT_REFRESH_MS = 5 * 60 * 1000;
const LIVE_WINS_ROUND_PAGE_SIZE = 1000;
const LIVE_WINS_ROUND_MAX = 5000;
let liveWsLastAt = 0;
let liveWsLastEventName = null;
let liveSummaryLastAt = 0;
let lastWsDebugStatus = null;
const LIVE_API_STATUS_WINDOW_MS = () => liveApiStatusWindowMs;
const LIVE_WS_STATUS_WINDOW_MS = 20000;
const LIVE_SUMMARY_STATUS_WINDOW_MS = 20000;
let currentRoundPowerDebugOpen = false;
let currentRoundScoreDebugOpen = false;
let liveProfileClanName = '';
let liveMyClanPercent = null;
let liveMyClanPercentValue = null;
let liveMyClanPercentUpdatedAt = 0;
let liveMyClanPercentPending = false;
let liveBoostPercentTotal = null;
let liveBoostPercentUpdatedAt = 0;
const LIVE_BOOST_PERCENT_TTL_MS = 4000;
const liveBoostPercentCache = new Map();
let liveBoostRoundKey = null;
let liveBoostSortOrder = [];
let liveBoostSortUpdatedAt = 0;
const LIVE_BOOST_SORT_MIN_MS = 10000;
let liveBoostSortRoundId = null;
let liveBoostEntryCache = new Map();
const LIVE_BOOST_HOURLY_STORAGE_KEY = 'mw_live_boost_hourly_v1';
const LIVE_BOOST_HOURLY_MAX_HOURS = 168;
const LIVE_BOOST_HOURLY_RETENTION_HOURS = 336;
const LIVE_BOOST_HOURLY_MIN_INTERVAL_MS = 15000;
const LIVE_BOOST_HOURLY_SYNC_MIN_MS = 120000;
const BOOST_USE_CYCLE_STORAGE_KEY = 'mw_boost_use_cycle_v1';
const LIVE_RECENT_ROUNDS_CACHE_KEY = 'mw_recent_rounds_cache_v1';
const FLOATING_CHAT_STATE_KEY = 'mw_floating_chat_state_v1';
const FLOATING_WINS_STATE_KEY = 'mw_floating_wins_state_v1';
const BOOST_IMPACT_ROUNDS_CACHE_MS = 5 * 60 * 1000;
const MULTIPLIER_TIME_CACHE_MS = 5 * 60 * 1000;
const MULTIPLIER_TIME_LEAGUE_KEY = 'mw_multiplier_time_league';
const MULTIPLIER_TIME_CYCLE_FROM_KEY = 'mw_multiplier_time_cycle_from';
const MULTIPLIER_TIME_CYCLE_TO_KEY = 'mw_multiplier_time_cycle_to';
const MULTIPLIER_TIME_HIDE_1X_KEY = 'mw_multiplier_time_hide_1x';
const MULTIPLIER_TIME_HIDE_1X_RATIO_KEY = 'mw_multiplier_time_hide_1x_ratio';
let simTimerSeconds = null;
let simTimerLastSync = 0;
let simTimerSource = null;
let simTimerBlockId = null;
let latestRoundTimerValue = null;
let latestRoundTimerSeconds = null;
let lastLiveRoundTimerSeconds = null;
let miniCommContainer = null;
let miniCommDraft = '';
let miniCommDraftClanId = null;
let lastCurrentRoundLivePayload = null;
let lastLiveSummaryBlockId = null;
let lastLiveSummaryTimer = null;
let lastLiveSummaryMultiplier = null;
let lastLiveSummaryMultiplierBlockId = null;
let currentRoundInfo = { blockId: '-', timer: '-:-', multiplier: '-' };
let currentRoundClanIds = new Map();
let currentRoundClanBoosts = new Map();
let liveCycleId = null;
let liveCycleNumber = null;
let liveRosterBaseLeaders = [];
let liveBoostHourlyStats = null;
let liveBoostHourlyPrevAddedByClan = new Map();
let liveBoostHourlyLastSampleAt = 0;
let liveBoostHourlySyncLastAt = 0;
let boostImpactRoundsCache = null;
let boostImpactRoundsFetchedAt = 0;
let multiplierTimeCache = new Map();
let liveClanPresenceCache = new Map();
let liveTotalScoreCurrent = null;
const LIVE_CLAN_PRESENCE_TTL_MS = 15000;
let floatingChatContainer = null;
let floatingChatToggle = null;
let floatingChatLauncher = null;
let floatingChatWinsBtn = null;
let floatingChatBoostHelperBtn = null;
let floatingBoostHelperContainer = null;
let floatingChatState = null;
let floatingChatUnread = false;
let floatingChatLastSeenId = null;
let floatingWinsContainer = null;
let floatingWinsState = null;
let boostHelperPopupTimer = null;
let boostHelperActiveRuleIds = new Set();
let boostHelperLastRoundKey = null;
let boostHelperRoundStartAt = null;
let boostHelperMyClanBoosts = null;
const BOOST_HELPER_SCORE_SAMPLE_KEY_V1 = 'mw_boost_helper_score_samples_v1';
const BOOST_HELPER_SCORE_SAMPLE_KEY = 'mw_boost_helper_score_samples_v2';
const BOOST_HELPER_MULTIPLIERS = [1, 2, 4, 8, 16, 32, '64+'];
let telegramLastSent = null;
let discordLastSent = null;
let telegramSentInFlight = new Set();
let discordSentInFlight = new Set();
let commRoundStartDetected = { blockId: null, detectedAt: 0 };
let commCurrentRoundLastAt = 0;

// Round State baseline system - global variables
let roundStateBaseline = null; // Map of clanId -> {clanId, clanName, baselineScore, accumulatedScore, totalScore, power, position}
let currentRoundBlockId = null; // Track which block the baseline is for
let baselineFetchAttempted = false; // Track if we already tried to fetch baseline
let baselineFetchLastAttemptTime = 0; // Timestamp of last fetch attempt (for cooldown)
let baselineNoParticipantsUntil = 0; // Cooldown when baseline returns empty

// Diagnostic function to check baseline status (available globally)
window.checkBaselineStatus = () => {
  console.log('===== BASELINE STATUS =====');
  console.log('Baseline Exists:', !!roundStateBaseline);
  console.log('Baseline Size:', roundStateBaseline?.size || 0);
  console.log('Current Round Block ID:', currentRoundBlockId);
  console.log('Fetch Attempted:', baselineFetchAttempted);
  console.log('Current Round Info:', currentRoundInfo);
  if (roundStateBaseline && roundStateBaseline.size > 0) {
    const entries = Array.from(roundStateBaseline.values()).slice(0, 5);
    console.log('Sample baseline entries (first 5):', entries);
  }
  return {
    exists: !!roundStateBaseline,
    size: roundStateBaseline?.size || 0,
    blockId: currentRoundBlockId,
    attempted: baselineFetchAttempted
  };
};

let liveRosterMyClan = null;
let commClanBlockPpsSampleColumnsAvailable = null;
let liveRosterTotals = null;
let liveRosterTotalsPending = false;
const LIVE_ROSTER_TOTALS_TTL_MS = 5 * 60 * 1000;
let liveRosterFetchedAt = 0;
let liveRosterPending = false;
let liveRosterCycleId = null;
let liveRosterLeagueId = null;
let lastLiveSummaryLeague = null;
let lastLiveClanSnapshot = window.lastLiveClanSnapshot || null;
let liveMyClanName = null;
let liveMyClanId = null;
let liveMyClanNameUpdatedAt = 0;
let liveMyClanNamePending = false;
let statsRoundHistoryLoaded = false;
let statsRoundHistoryLoading = false;
let statsWinTablesLoaded = false;
let statsWinTablesLoading = false;
let statsActivePanel = 'effectiveness';
let statsRoundHistoryExporting = false;
let clanSelectionExporting = false;
let statsRoundHistoryFinishedLookup = new Map();
let statsWinRowsCache = [];
let statsWinRowsCacheMultiplier = null;
let statsBucketPopupOverlay = null;
let statsBucketPopupTitle = null;
let statsBucketPopupBody = null;
let statsRoundHistoryPage = 1;
let statsRoundHistoryLoadedPage = null;
let personalDataLoading = false;
let personalDataLoadedUser = null;
const STATS_ROUND_HISTORY_PER_PAGE = 25;
const STATS_ROUND_HISTORY_MAX_PAGES = 50;
const PERSONAL_ROUND_PAGE_SIZE = 1000;
const PERSONAL_ROUND_MAX = 10000;
const PERSONAL_REWARDS_REFRESH_MS = 2 * 60 * 60 * 1000;
const PERSONAL_REWARDS_RETRY_MS = 60 * 1000;
const PERSONAL_REWARDS_PAGE_SIZE = 25;
const PERSONAL_REWARDS_MAX_RECORDS = 200;
const PERSONAL_USER_RESOLVE_KEY = 'mw_personal_user_id_resolve_at';
const PERSONAL_USER_RESOLVE_TTL_MS = 10 * 60 * 1000;
const PERSONAL_REWARDS_LAST_FETCH_PREFIX = 'mw_personal_rewards_last';
const PERSONAL_REWARDS_CLAN_LAST_FETCH_PREFIX = 'mw_personal_rewards_clan_last';
const PROFIT_SNAPSHOT_COST_KEY = 'mw_profit_snapshot_cost';
const PROFIT_SNAPSHOT_BASE_TH_KEY = 'mw_profit_snapshot_base_th';
const PROFIT_SNAPSHOT_USER_TH_KEY = 'mw_profit_snapshot_user_th';
let personalRewardsTimer = null;
let personalRewardsRetryTimer = null;
let personalRewardsRunning = false;
let personalClanRewardsRunning = false;
let personalRewardsNeedsAuthRetry = false;
let personalClanRewardsNeedsAuthRetry = false;
let personalApiHealthRunning = false;
let personalRewardsPage = 1;
let personalClanRewardsPage = 1;
let personalRewardsCache = [];
let personalClanRewardsCache = [];
let profitabilityLoaded = false;
let profitabilityLoading = false;
let profitabilityClanRowsCache = [];
let profitabilityRoundRowsCache = [];
const PROFIT_ROUND_LIMIT = 1000;
const PROFIT_CYCLE_FILTER_KEY = 'mw_profit_cycle_filter';
const PROFIT_CYCLE_ALL_KEY = 'mw_profit_cycle_all';
const PROFIT_LEAGUE_FILTER_KEY = 'mw_profit_league_filter';
const PERSONAL_ALIAS_KEY = 'mw_personal_alias';
const PERSONAL_USER_ID_KEY = 'mw_personal_user_id';
let roundHistoryPopupOverlay = null;
let roundHistoryPopupBody = null;
let roundHistoryPopupTitle = null;
let clanSelectionLoaded = false;
let clanSelectionLoading = false;
let clanSelectionRowsCache = [];
let clanSelectionFilterTimer = null;
let clanSelectionRawPopupOverlay = null;
let clanSelectionRawPopupTitle = null;
let clanSelectionRawPopupBody = null;
let clanSelectionVisibleRows = [];
let clanSelectionMembersPopupOverlay = null;
let clanSelectionMembersPopupTitle = null;
let clanSelectionMembersPopupBody = null;
let clanSelectionMembersPopupStatus = null;
let clanSelectionMembersPageSelect = null;
let clanSelectionMembersPrevBtn = null;
let clanSelectionMembersNextBtn = null;
let clanSelectionMembersState = null;
let subscriptionSuggestStatus = { message: '', error: false };
let commKorrelLogContextOverride = null;
const CLAN_SELECTION_MEMBERS_PER_PAGE = 50;
const CLAN_SELECTION_FILTERS_KEY = 'mw_clan_selection_filters';
const CLAN_SELECTION_EXTRA_FILTERS_KEY = 'mw_clan_selection_extra_filters';
const CLAN_SELECTION_WHALE_THRESHOLD = 350;
const CLAN_SELECTION_DUNE_START = 5;
const CLAN_SELECTION_DUNE_END = 20;
const clanSelectionMemberStats = new Map();
let clanSelectionMemberStatsContext = null;
const CLAN_SELECTION_ROUND_LIMIT = 10000;
const clanSelectionCycleBlockCounts = new Map();
let clanSelectionCycleBlockCountsContext = null;
const CLAN_SELECTION_FILTER_KEYS = [
  'cycle_id',
  'league_id',
  'clan_name',
  'clan_power',
  'users_count'
];
const CLAN_SELECTION_LEAGUE_LABELS = {
  1: 'Odyssey',
  3: 'Eclipse',
  4: 'Horizon'
};
const COMM_KNOWN_LEAGUE_IDS = (() => {
  const ids = new Set(
    Object.keys(CLAN_SELECTION_LEAGUE_LABELS)
      .map((key) => commParseNumber(key))
      .filter((value) => Number.isFinite(value))
      .map((value) => Math.trunc(value))
  );
  for (let leagueId = CLAN_SELECTION_DUNE_START; leagueId <= CLAN_SELECTION_DUNE_END; leagueId += 1) {
    ids.add(leagueId);
  }
  return Array.from(ids).sort((a, b) => a - b);
})();

function commBuildClanSelectionCycleBlockKey(cycleId, leagueId, clanId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId) || !Number.isFinite(clanId)) return null;
  return `${Math.trunc(cycleId)}:${Math.trunc(leagueId)}:${Math.trunc(clanId)}`;
}

async function commEnsureClanSelectionCycleBlockCounts(filters) {
  const cycleId = commParseNumber(filters?.cycle_id);
  if (!Number.isFinite(cycleId)) {
    clanSelectionCycleBlockCounts.clear();
    clanSelectionCycleBlockCountsContext = null;
    return;
  }
  const leagueFilterRaw = (filters?.league_id || '').toString().trim();
  const leagueFilter = leagueFilterRaw.toLowerCase();
  const contextKey = `${Math.trunc(cycleId)}|${leagueFilter || 'all'}`;
  if (clanSelectionCycleBlockCountsContext === contextKey && clanSelectionCycleBlockCounts.size) return;
  clanSelectionCycleBlockCounts.clear();
  clanSelectionCycleBlockCountsContext = contextKey;
  const queryParts = [
    'select=winner_clan_id,league_id,cycle_id,active',
    `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
    'winner_clan_id=not.is.null',
    'active=eq.false',
    `limit=${CLAN_SELECTION_ROUND_LIMIT}`
  ];
  if (leagueFilter) {
    if (leagueFilter === 'dune-all') {
      queryParts.unshift(`league_id=gte.${CLAN_SELECTION_DUNE_START}`);
      queryParts.unshift(`league_id=lte.${CLAN_SELECTION_DUNE_END}`);
    } else {
      const leagueParsed = commParseNumber(leagueFilterRaw);
      if (Number.isFinite(leagueParsed)) {
        queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueParsed))}`);
      }
    }
  }
  const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
  if (!Array.isArray(rows)) {
    clanSelectionCycleBlockCountsContext = null;
    return;
  }
  rows.forEach((row) => {
    const clanId = commParseNumber(row?.winner_clan_id);
    const leagueId = commParseNumber(row?.league_id);
    const cycleValue = commParseNumber(row?.cycle_id);
    const key = commBuildClanSelectionCycleBlockKey(cycleValue, leagueId, clanId);
    if (!key) return;
    const count = clanSelectionCycleBlockCounts.get(key) || 0;
    clanSelectionCycleBlockCounts.set(key, count + 1);
  });
}

function commGetClanAvgCycleValue(row, raw) {
  const source = raw || parseMaybeJson(row?.raw) || {};
  const cycleId = commParseNumber(row?.cycle_id ?? source?.cycleId);
  const leagueId = commParseNumber(row?.league_id ?? source?.leagueId);
  const clanId = commParseNumber(row?.clan_id ?? source?.id);
  const key = commBuildClanSelectionCycleBlockKey(cycleId, leagueId, clanId);
  if (key && clanSelectionCycleBlockCountsContext) {
    if (clanSelectionCycleBlockCounts.has(key)) return clanSelectionCycleBlockCounts.get(key);
    return 0;
  }
  return null;
}

function commGetClanWeeksActive(row, raw) {
  const source = raw || parseMaybeJson(row?.raw) || {};
  const createdAtValue = source?.createdAt ?? source?.created_at ?? row?.created_at;
  if (!createdAtValue) return null;
  const createdMs = new Date(createdAtValue).getTime();
  if (!Number.isFinite(createdMs)) return null;
  const weekMs = 7 * 24 * 60 * 60 * 1000;
  const weeks = (Date.now() - createdMs) / weekMs;
  if (!Number.isFinite(weeks) || weeks <= 0) return null;
  return weeks;
}

// Supabase config for clan communication tools (from root app)
const COMM_SUPABASE_URL = 'https://hkybarlwyujbzrvcwpvz.supabase.co/rest/v1';
const COMM_SUPABASE_PROJECT_URL = COMM_SUPABASE_URL.replace(/\/rest\/v1$/, '');
const COMM_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhreWJhcmx3eXVqYnpydmN3cHZ6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQzMjI2NTYsImV4cCI6MjA3OTg5ODY1Nn0.XGHy7oE5teR6mrWN4VATID6dF0yLNpeUoZTkHEJFapY';
const COMM_HEADERS = {
  'Content-Type': 'application/json',
  Prefer: 'return=representation',
  apikey: COMM_SUPABASE_KEY,
  Authorization: `Bearer ${COMM_SUPABASE_KEY}`
};
const COMM_TABLE_CLANS = 'clan_comm_clans';
const COMM_TABLE_MEMBERS = 'clan_comm_members';
const COMM_TABLE_MESSAGES = 'clan_comm_messages';
const COMM_TABLE_SNAPSHOTS = 'clan_comm_snapshots';
const COMM_TABLE_SNAPSHOT_MEMBERS = 'clan_comm_snapshot_members';
const COMM_TABLE_CLAN_STATS = 'Clan';
const COMM_TABLE_CLAN_ROSTER_CLANS = 'clan_roster_clans';
const COMM_TABLE_CLAN_ROSTER_MEMBERS = 'clan_roster_members';
const COMM_TABLE_ROSTER_COMPLETION = 'clan_roster_completion';
const COMM_TABLE_ROUND_CYCLE_API = 'rounds_cycle_api';
const COMM_TABLE_ROUND_COMPLETION = 'rounds_cycle_completion';
const COMM_TABLE_ROUNDS_FINISHED = 'ROUNDS_Finished';
const COMM_TABLE_CLAN_BLOCK_PPS = 'clan_block_pps';
const COMM_TABLE_HOME_NEWS = 'home_news';
const COMM_TABLE_LICENSE_PRICING = 'license_pricing';
const COMM_TABLE_LICENSE_KEYS = 'license_keys';
const COMM_TABLE_LICENSE_ASSIGNMENTS = 'license_assignments';
const COMM_TABLE_SUBSCRIPTION_SUGGESTIONS = 'subscription_suggestions';
const COMM_TABLE_APP_NOTICE = 'app_notice';
const COMM_TABLE_KORREL_LOGS = 'korrel_tools_log';
const COMM_TABLE_API_LIMITS = 'api_limits';
const COMM_TABLE_API_CALL_LOG = 'api_call_log';
const COMM_TABLE_USER_REWARDS = 'user_rewards_by_user';
const COMM_TABLE_USER_REWARDS_CLAN = 'user_rewards_by_clan';
const COMM_TABLE_USER_SETTINGS = 'app_user_settings';
const COMM_TABLE_LIVE_CACHE = 'live_round_cache';
const COMM_TABLE_LIVE_BOOST_HOURLY = 'live_boost_hourly';
const COMM_BOOST_HELPER_FIELD = 'boost_helper_template';
const COMM_TABLE_BOOST_HELPER_TEMPLATES = 'clan_boost_helper_templates';
const COMM_NOTICE_FIELD = 'notice_text';
const COMM_SESSION_KEY = 'mw_sidebar_clan_comm_session_v1';
const COMM_ALERT_MUTE_KEY = 'mw_sidebar_comm_alert_mute';
const COMM_HIGH_ALERT_KEY = 'mw_sidebar_comm_high_alert';
const COMM_API_DEFAULT_LIMIT_KEY = 'mw_korrel_api_default_limit';
const COMM_API_MIN_INTERVAL_KEY = 'mw_korrel_api_min_interval_ms';
const COMM_API_BULK_AMOUNT_KEY = 'mw_korrel_api_bulk_amount';
const COMM_API_CYCLE_OVERRIDE_KEY = 'mw_korrel_api_cycle_override';
const COMM_API_LEAGUE_OVERRIDE_KEY = 'mw_korrel_api_league_override';
const COMM_API_ROSTER_BATCH_KEY = 'mw_korrel_roster_batch';
const COMM_API_ROSTER_MEMBER_LIMIT_KEY = 'mw_korrel_roster_member_limit';
const COMM_API_ROSTER_VERBOSE_KEY = 'mw_korrel_roster_verbose';
const COMM_API_ROSTER_PROGRESS_KEY = 'mw_korrel_roster_progress';
const COMM_API_ROSTER_AUTO_DONE_KEY = 'mw_korrel_roster_auto_done';
const COMM_API_ROSTER_AUTO_LAST_KEY = 'mw_korrel_roster_auto_last';
const COMM_KORREL_LOG_USER_KEY = 'mw_korrel_log_user_id';
const COMM_LICENSE_STORAGE_KEY = 'mw_sidebar_license_v1';
const COMM_LICENSE_TOKEN_KEY = 'mw_sidebar_license_token_v1';
const COMM_LICENSE_SENDER_KEY = 'mw_license_sender_address';
const COMM_GOOGLE_PROFILE_KEY = 'mw_sidebar_google_profile_v1';
const COMM_KORREL_FORCE_NON_ADMIN_KEY = 'mw_korrel_force_non_admin';
const COMM_AUTH_SESSION_KEY = 'mw_sidebar_auth_session_v1';
const COMM_SUPABASE_AUTH_URL = 'https://hkybarlwyujbzrvcwpvz.supabase.co/auth/v1';
const COMM_LICENSE_ENDPOINT = `${COMM_SUPABASE_PROJECT_URL}/functions/v1/validate-license`;
const COMM_LICENSE_ISSUE_ENDPOINT = `${COMM_SUPABASE_PROJECT_URL}/functions/v1/issue-license`;
const COMM_LICENSE_ISSUE_ENDPOINT_ALT = `${COMM_SUPABASE_PROJECT_URL.replace('.supabase.co', '.functions.supabase.co')}/issue-license`;
const COMM_LICENSE_WALLET_ADDRESS = '0xB4FdD75cad29b2f92241cD6931f6CD7e7f0304F7';

let commSession = null;
let commMessages = [];
let commMessageIds = new Set();
let commMembers = [];
let commMembersLastFetch = 0;
let commSnapshots = [];
let commSnapshotsLoading = false;
let commNoticeText = '';
let commNoticeAvailable = true;
let commChatTimer = null;
let commMetaTimer = null;
let commAlertMuted = false;
let commHighAlertEnabled = false;
let commHighAlertPending = false;
let commHighAlertLastBlockId = null;
let commHighAlertLastFiredAt = 0;
let gominingChatTimer = null;
let gominingChatMessages = [];
let gominingChatMessageIds = new Set();
let gominingChatLastFetchAt = 0;
let gominingChatLastError = null;
let gominingChatPollPending = false;
let boostNotificationSeen = new Set();
let boostNotificationCounts = new Map();
let commKorrelApiLastClanId = null;
let commKorrelApiDetailData = [];
let commKorrelApiLastRawPayload = null;
let commKorrelApiLastRawLabel = '';
let commKorrelApiDefaultLimit = 25;
let commKorrelApiMinIntervalMs = 60000;
let commKorrelApiBulkAmount = 50;
let commKorrelApiCycleOverride = null;
let commKorrelApiLeagueOverride = null;
let commKorrelRosterBatchSize = 25;
let commKorrelRosterMemberLimit = 25;
let commKorrelRosterRunning = false;
let commKorrelRosterPatchRunning = false;
let commKorrelRosterProgress = null;
let commKorrelRosterStatus = null;
let commKorrelRosterVerbose = true;
let commKorrelRosterResumeTimer = null;
let commKorrelRosterResumeAt = 0;
let commKorrelRosterAutoDoneCycleId = null;
let commKorrelRosterAutoLastAttemptAt = 0;
let commKorrelRosterAutoNextAt = 0;
let commKorrelAutoRoundFirst = true;
let commKorrelRosterAutoSkipReason = null;
let commKorrelRosterAutoSkipAt = 0;
let commKorrelOtherLeagueNextAt = 0;
let commKorrelAutoTimer = null;
let commKorrelOverviewTimer = null;
let commKorrelOverviewLoading = false;
let commApiLimitTimer = null;
let commSettingsSyncTimer = null;
let commSettingsSyncQueued = false;
let commSettingsSyncRunning = false;
const commKorrelApiLastCallAt = new Map();
let commKorrelRoundCount = 0;
let commKorrelLastRoundBlockId = null;
let commKorrelRoundSnapshotPending = false;
let commKorrelStartupSnapshotTimer = null;
let commKorrelStartupSnapshotDone = false;
let commKorrelCountdownTimer = null;
let commKorrelNextSnapshotAt = null;
let commKorrelStatusTimer = null;
let commKorrelLastStoredBlockNumber = null;
let commKorrelLastStoredCycleId = null;
let commKorrelLastStoredCheckAt = 0;
let commKorrelCurrentLeagueId = null;
let commKorrelCurrentCycle = null;
let commKorrelOtherLeagueLastAt = 0;
let commKorrelOtherLeagueIndex = 0;
let commKorrelOtherLeagueRunning = false;
let commKorrelRoundQueue = [];
let commKorrelRoundQueueIndex = 0;
let commKorrelRoundQueueCycleId = null;
let commKorrelRoundQueueKey = '';
let commKorrelBackfillNextAt = 0;
let commKorrelBackfillRunning = false;
let commKorrelBackfillLastAt = 0;
let commKorrelBackfillLastLeagueId = null;
let commKorrelBackfillLastCycleId = null;
let commKorrelRoundCompletionState = null;
let commKorrelRemoteLogLastId = 0;
let commKorrelRemoteLogFetching = false;
let commKorrelRemoteLogTimer = null;
let commApiLimitConfig = {
  minIntervalMs: null,
  maxIntervalMs: null,
  maxCallsPer10Min: null
};
let commApiLimitLastFetchAt = 0;
let commApiLimitFetching = false;
let commApiCallWindowUser = null;
let commApiCallWindow = [];
let commApiLogInsertFailedAt = 0;
let commAutoClanPending = false;
let commAutoClanLastAttempt = 0;
let commApiLimitWaitUntil = 0;
let commApiLimitWaitEndpoint = '';
let commApiLimitCountdownTimer = null;
const COMM_AUTO_CLAN_COOLDOWN_MS = 60000;
let commApiLogInsertWarnedAt = 0;
let commApiLogInsertError = null;
let commRoundRefreshColumnAvailable = null;
let commRosterCompletionColumnsAvailable = null;
const commKorrelLogTrimAt = new Map();
const COMM_KORREL_ROUND_SNAPSHOT_INTERVAL = 7;
const COMM_KORREL_ROUND_SNAPSHOT_LIMIT = 10;
const COMM_KORREL_ROUND_SNAPSHOT_ENDPOINT = 'https://api.gomining.com/api/nft-game/round/find-by-cycleId';
const COMM_KORREL_AUTO_SNAPSHOT_ENABLED = false;
const COMM_KORREL_ROUND_COMPLETION_TTL_MS = 5 * 60 * 1000;
const COMM_KORREL_ROUND_COMPLETION_STALE_MS = 4 * 60 * 60 * 1000;
const COMM_KORREL_ROUND_COMPLETION_LOCK_MS = 10 * 60 * 1000;
const COMM_KORREL_ROUND_REFRESH_MS = 2 * 60 * 60 * 1000;
const COMM_KORREL_ROUND_OVERLAP_BASE = 0;
const COMM_KORREL_ROUND_OVERLAP_PER_HOUR = 8;
const COMM_KORREL_ROUND_OVERLAP_MAX = 200;
const COMM_KORREL_ROUND_ROSTER_READY_PCT = 98;
const COMM_KORREL_ROSTER_FULL_PCT = 50;
const COMM_KORREL_ROSTER_FULL_COOLDOWN_MS = 2 * 24 * 60 * 60 * 1000;
const COMM_KORREL_ROSTER_FULL_MIN_INTERVAL_MS = 15 * 60 * 1000;
const COMM_KORREL_PROGRESS_LIMIT = 50000;
const COMM_API_LIMIT_REFRESH_MS = 60000;
const COMM_API_CALL_WINDOW_MS = 10 * 60 * 1000;
const COMM_KORREL_PROGRESS_REFRESH_MS = 120000;
const COMM_KORREL_TOTAL_ROUNDS_PER_CYCLE = 1008;
const COMM_KORREL_REMOTE_LOG_INTERVAL_MS = 30000;
const COMM_KORREL_LOG_MAX_PER_USER = 50;
const COMM_KORREL_LOG_FETCH_LIMIT = 200;
const COMM_KORREL_STATUS_INTERVAL_MS = 300000;
const COMM_KORREL_AUTO_MIN_INTERVAL_MS = 15 * 60 * 1000;
const COMM_KORREL_AUTO_MAX_INTERVAL_MS = 30 * 60 * 1000;
const COMM_KORREL_AUTO_CHECK_INTERVAL_MS = 30000;
const COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS = [1, 2, 4, 8, 16, 32, 64, 128];
const COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS_TEXT = '1,2,4,8,16,32,64,128';
const COMM_KORREL_ROSTER_DB_LIMIT = 5000;
const COMM_KORREL_ROSTER_MIN_INTERVAL_MS = 12000;

const COMM_API_INPUT_SCHEMAS = {
  'https://api.gomining.com/api/nft-game/clan/get-my': {
    title: 'clan/get-my',
    method: 'POST',
    rows: [
      { field: '(none)', values: '-', notes: 'Send empty JSON body.' }
    ]
  },
  'https://api.gomining.com/api/nft-game/clan/get-by-id': {
    title: 'clan/get-by-id',
    method: 'POST',
    rows: [
      { field: 'clanId', values: 'number', notes: 'Required.' },
      { field: 'pagination.limit', values: 'number', notes: 'Optional; default 10.' },
      { field: 'pagination.skip', values: 'number', notes: 'Optional; default 0.' },
      { field: 'pagination.count', values: 'number', notes: 'Optional; default 0.' },
      { field: 'filters.filterType', values: 'string', notes: 'Optional; observed: "none".' },
      { field: 'sort.sortType', values: 'string', notes: 'Optional; observed: "none".' }
    ]
  },
  'https://api.gomining.com/api/nft-game/rewards-by-user': {
    title: 'rewards-by-user',
    method: 'POST',
    rows: [
      { field: 'filters.type', values: 'string', notes: 'Required; try "all" or "user".' },
      { field: 'filters.cycleId', values: 'number', notes: 'Optional.' },
      { field: 'filters.leagueId', values: 'number', notes: 'Optional.' },
      { field: 'filters.roundId', values: 'number', notes: 'Optional.' },
      { field: 'filters.userId', values: 'number', notes: 'Optional.' },
      { field: 'pagination.limit', values: 'number', notes: 'Optional; default 10.' },
      { field: 'pagination.skip', values: 'number', notes: 'Optional; default 0.' },
      { field: 'pagination.count', values: 'number', notes: 'Optional; default 0.' }
    ]
  },
  'https://api.gomining.com/api/nft-game/round/find-by-cycleId': {
    title: 'round/find-by-cycleId',
    method: 'POST',
    rows: [
      { field: 'cycleId', values: 'number', notes: 'Required.' },
      { field: 'leagueId', values: 'number', notes: 'Optional.' },
      { field: 'multipliers', values: 'array<number>', notes: 'Required. Comma-separated numbers.' },
      { field: 'pagination.limit', values: 'number', notes: 'Optional; default 10.' },
      { field: 'pagination.skip', values: 'number', notes: 'Optional; default 0.' },
      { field: 'pagination.count', values: 'number', notes: 'Optional; default 0.' }
    ]
  },
  'https://api.gomining.com/api/nft-game/round/get-last': {
    title: 'round/get-last',
    method: 'GET',
    rows: [
      { field: '(none)', values: '-', notes: 'GET with no body.' }
    ]
  },
  'https://api.gomining.com/api/nft-game/round/get-state': {
    title: 'round/get-state',
    method: 'POST',
    rows: [
      { field: 'type', values: 'string', notes: 'Observed: "current".' },
      { field: 'leagueId', values: 'number', notes: 'Optional league id.' }
    ]
  }
};

const commKorrelApiInputValues = {};
let homeNewsEntry = null;
let homePricingRows = [];
let homeGoogleProfile = null;
let homeAuthSession = null;
let homeAuthStatus = { message: '', error: false };
let homeLicenseState = { valid: false, reason: 'Not checked', expires_at: null, message: '' };
let appNoticeEntry = null;

// Map nftGameAbilityId UUIDs to boost type names (from allUsersStateV2 events)
const ABILITY_ID_TO_TYPE = {
  '2ec728e7-f31f-4df3-b486-5e82a4976563': 'timewrap',
  '3f735c47-cead-4d15-8dad-7db4a7b3f4e7': 'boost_user_1x',
  '646d3c76-fe06-414f-8d39-eb0aa7da429a': 'echo_1x',
  '8a9b57a9-8a17-4647-8b2f-a164dc52b1f4': 'clanpowerup',
  'a74dd2e0-14a9-46e3-86e5-1f1e8ab1f5c4': 'focus_10x',
  '17831b5e-afdf-4cb7-98d7-3a60d81e19ae': 'boost_user_10x',
  '2ebfd30d-2950-46fc-b3e9-498a924fd021': 'echo_10x',
  '5d6f8166-0f20-486f-b920-e898ca94dcc1': 'powerup_100x',
  'e7af0c32-1409-4b22-9e0b-99e7ea4939df': 'boost_user_100x',
  '370be7b3-d843-4e2f-a23a-eeea37648f99': 'boost_clan_1x',
  'a7554d7f-0e50-45e0-ab01-6ac8db629bfd': 'boost_clan_10x',
  '8b6fad11-cfc1-4922-ad17-30c41cee39b9': 'boost_clan_100x',
  '0ffad19c-8091-4527-a600-b7c2f238147d': 'echo_100x',
  '592724c5-1f59-43b2-af13-2de8d2c97a9d': 'focus_1x',
  'bea03b75-6dbf-4a99-a2c9-24dfe79a3ea1': 'focus_100x',
  // stealer boosts removed (not used)
};


// Boost helpers: mapping known boost type keys to friendly normalized keys and icons
// Icons updated to match gomining website styling
const BOOST_ICON_MAP = {
  powerup: '&#x1F32C;&#xFE0F;',
  power_up: '&#x1F32C;&#xFE0F;',
  powerup_delay: '&#x1F32C;&#xFE0F;',
  powerboost: '&#x1F680;',
  power_boost: '&#x1F680;',
  powerup_100x: '&#x1F32C;&#xFE0F;',
  instant: '&#x26A1;',
  instantboost: '&#x26A1;',
  instant_boost: '&#x26A1;',
  echo: '&#x23F3;',
  echoboost: '&#x23F3;',
  echo_boost: '&#x23F3;',
  echo_1x: '&#x23F3;',
  echo_10x: '&#x23F3;',
  echo_100x: '&#x23F3;',
  boost: '&#x1F680;',
  boost_user_1x: '&#x1F680;',
  boost_user_10x: '&#x1F680;',
  boost_user_100x: '&#x1F680;',
  boost_clan_1x: '&#x26A1;',
  boost_clan_10x: '&#x26A1;',
  boost_clan_100x: '&#x26A1;',
  focus_1x: '&#x1F3AF;',
  focus_10x: '&#x1F3AF;',
  focus_100x: '&#x1F3AF;',
  timewrap: '&#x1F3AF;',
  time_wrap: '&#x1F3AF;',
  service: '&#x1F6E0;&#xFE0F;',
  clanpowerup: '&#x1F6E1;&#xFE0F;',
  clan_power_up: '&#x1F6E1;&#xFE0F;',
  focus: '&#x1F3AF;'
};







// Multipliers for boost counts (display and aggregation)
const BOOST_COUNT_MULTIPLIER = {
  echoboost: 10,
  echo: 10,
  echo_1x: 1,
  echo_10x: 10,
  echo_100x: 100,
  instantboost: 100,
  instant: 100,
  instant_boost: 100,
  boost_user_1x: 1,
  boost_user_10x: 1,
  boost_user_100x: 1,
  boost_clan_1x: 1,
  boost_clan_10x: 10,
  boost_clan_100x: 100,
  focus_1x: 1,
  focus_10x: 10,
  focus_100x: 100,
  timewrap: 1,
  time_wrap: 1,
  powerup_100x: 1
};


const BOOST_DISPLAY_NAME = {
  powerup: 'Power-up',
  power_up: 'Power-up',
  powerup_delay: 'Power-up',
  powerboost: 'Power boost',
  power_boost: 'Power boost',
  powerup_100x: 'Power-up',
  instant: 'Instant boost',
  instantboost: 'Instant boost',
  instant_boost: 'Instant boost',
  echo: 'Echo boost',
  echoboost: 'Echo boost',
  echo_boost: 'Echo boost',
  echo_1x: 'Echo boost',
  echo_10x: 'Super echo boost',
  echo_100x: 'Ultra echo boost',
  boost: 'Power boost',
  boost_user_1x: 'Power boost',
  boost_user_10x: 'Super power boost',
  boost_user_100x: 'Ultra power boost',
  boost_clan_1x: 'Power boost',
  boost_clan_10x: 'Super power boost',
  boost_clan_100x: 'Ultra power boost',
  focus_1x: 'Focus boost',
  focus_10x: 'Super focus boost',
  focus_100x: 'Ultra focus boost',
  timewrap: 'Focus boost',
  time_wrap: 'Focus boost',
  service: 'Miner service',
  clanpowerup: 'Clan power-up',
  clan_power_up: 'Clan power-up',
  focus: 'Focus boost'
};

const BOOST_DISPLAY_ORDER = [
  'powerup',
  'clanpowerup',
  'powerboost',
  'instant',
  'echo',
  'focus'
];

const LIVE_PPS_FACTOR = 1.647; // 28 W/TH / 17 W/TH
const BASE_PPS_MIN_FACTOR = 1.4;
const BASE_PPS_MAX_FACTOR = 2;

function commNormalizeLivePowerSource(value) {
  const raw = (value || '').toString().trim();
  if (!raw) return LIVE_POWER_SOURCE_DEFAULT;
  if (LIVE_POWER_SOURCE_ORDER.includes(raw)) return raw;
  return LIVE_POWER_SOURCE_DEFAULT;
}

function commGetLivePowerSource() {
  try {
    return commNormalizeLivePowerSource(localStorage.getItem(LIVE_POWER_SOURCE_KEY));
  } catch {
    return LIVE_POWER_SOURCE_DEFAULT;
  }
}

function commSetLivePowerSource(value) {
  try {
    localStorage.setItem(LIVE_POWER_SOURCE_KEY, commNormalizeLivePowerSource(value));
  } catch {}
}

function commExtractLivePowerCandidates(entry) {
  const candidates = {};
  if (!entry) return candidates;
  candidates.clanPower = commParseNumber(entry?.clanPower ?? entry?.clan_power);
  LIVE_POWER_FIELDS.forEach((field) => {
    let value = commParseNumber(entry?.[field]);
    if (!Number.isFinite(value)) {
      const snake = field.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
      value = commParseNumber(entry?.[snake]);
    }
    candidates[field] = value;
  });
  return candidates;
}

function commResolveLivePowerFromCandidates(candidates, source) {
  const normalized = commNormalizeLivePowerSource(source);
  if (normalized === 'roster') return null;
  if (normalized === 'clanPower') return candidates?.clanPower ?? null;
  if (candidates && Object.prototype.hasOwnProperty.call(candidates, normalized)) {
    return candidates[normalized];
  }
  return null;
}

function commNormalizeLiveScoreSource(value) {
  const raw = (value || '').toString().trim();
  if (!raw) return LIVE_SCORE_SOURCE_DEFAULT;
  if (LIVE_SCORE_SOURCE_ORDER.includes(raw)) return raw;
  return LIVE_SCORE_SOURCE_DEFAULT;
}

function commGetLiveBoostApiSync() {
  try {
    const stored = localStorage.getItem(LIVE_BOOST_API_SYNC_KEY);
    if (stored == null) return true;
    return stored === '1';
  } catch {
    return true;
  }
}

function commSetLiveBoostApiSync(value) {
  const enabled = !!value;
  try {
    localStorage.setItem(LIVE_BOOST_API_SYNC_KEY, enabled ? '1' : '0');
  } catch {
    // ignore
  }
  return enabled;
}

function commGetLiveScoreSource() {
  try {
    return commNormalizeLiveScoreSource(localStorage.getItem(LIVE_SCORE_SOURCE_KEY));
  } catch {
    return LIVE_SCORE_SOURCE_DEFAULT;
  }
}

function commSetLiveScoreSource(value) {
  try {
    localStorage.setItem(LIVE_SCORE_SOURCE_KEY, commNormalizeLiveScoreSource(value));
  } catch {}
}

function commExtractLiveScoreCandidates(entry) {
  const candidates = {};
  if (!entry) return candidates;
  LIVE_SCORE_FIELDS.forEach((field) => {
    let value = commParseNumber(entry?.[field]);
    if (!Number.isFinite(value)) {
      const snake = field.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
      value = commParseNumber(entry?.[snake]);
    }
    candidates[field] = value;
  });
  const fallbackAdded = Number.isFinite(candidates.currentAddedScore)
    ? candidates.currentAddedScore
    : candidates.current_added_score;
  if (Number.isFinite(fallbackAdded)) {
    if (!Number.isFinite(candidates.currentAddedScore)) {
      candidates.currentAddedScore = fallbackAdded;
    }
    if (!Number.isFinite(candidates.current_added_score)) {
      candidates.current_added_score = fallbackAdded;
    }
  }
  return candidates;
}

function commResolveLiveScoreFromCandidates(candidates, source) {
  const normalized = commNormalizeLiveScoreSource(source);
  if (!candidates) return null;
  if (Object.prototype.hasOwnProperty.call(candidates, normalized)) {
    const direct = candidates[normalized];
    if (Number.isFinite(direct)) return direct;
  }
  if (normalized === 'currentAddedScore') {
    const alias = candidates.current_added_score;
    return Number.isFinite(alias) ? alias : null;
  }
  if (normalized === 'current_added_score') {
    const alias = candidates.currentAddedScore;
    return Number.isFinite(alias) ? alias : null;
  }
  return null;
}

function commNormalizeLiveDisplayMode(value) {
  const raw = (value || '').toString().trim().toLowerCase();
  if (!raw) return LIVE_DISPLAY_MODE_DEFAULT;
  if (LIVE_DISPLAY_MODE_ORDER.includes(raw)) return raw;
  return LIVE_DISPLAY_MODE_DEFAULT;
}

function commGetLiveDisplayMode() {
  try {
    return commNormalizeLiveDisplayMode(localStorage.getItem(LIVE_DISPLAY_MODE_KEY));
  } catch {
    return LIVE_DISPLAY_MODE_DEFAULT;
  }
}

function commSetLiveDisplayMode(value) {
  try {
    localStorage.setItem(LIVE_DISPLAY_MODE_KEY, commNormalizeLiveDisplayMode(value));
  } catch {}
}

function commNormalizeLiveTotalScoreWindowMode(value) {
  const raw = (value || '').toString().trim().toLowerCase();
  if (!raw) return LIVE_TOTAL_SCORE_WINDOW_MODE_DEFAULT;
  if (Object.prototype.hasOwnProperty.call(LIVE_TOTAL_SCORE_WINDOW_MODE_LABELS, raw)) return raw;
  return LIVE_TOTAL_SCORE_WINDOW_MODE_DEFAULT;
}

function commGetLiveTotalScoreWindowMode() {
  try {
    return commNormalizeLiveTotalScoreWindowMode(localStorage.getItem(LIVE_TOTAL_SCORE_WINDOW_MODE_KEY));
  } catch {
    return LIVE_TOTAL_SCORE_WINDOW_MODE_DEFAULT;
  }
}

function commSetLiveTotalScoreWindowMode(value) {
  try {
    localStorage.setItem(LIVE_TOTAL_SCORE_WINDOW_MODE_KEY, commNormalizeLiveTotalScoreWindowMode(value));
  } catch {}
}

function commNormalizeDebugClan(value) {
  return value == null ? '' : String(value).trim().toLowerCase();
}

function commShouldDebugClan(name) {
  const target = commNormalizeDebugClan(livePpsDebugClan);
  if (!target) return false;
  if (target === '*' || target === 'all') return true;
  const candidate = commNormalizeDebugClan(name);
  if (!candidate) return false;
  return candidate === target || candidate.includes(target);
}

function commGetBoostDebugClan() {
  try {
    const stored = localStorage.getItem(LIVE_PPS_DEBUG_CLAN_KEY);
    return stored == null ? null : String(stored);
  } catch {
    return null;
  }
}

function commSetBoostDebugClan(value) {
  livePpsDebugClan = value ? String(value) : '';
  try {
    if (livePpsDebugClan) {
      localStorage.setItem(LIVE_PPS_DEBUG_CLAN_KEY, livePpsDebugClan);
    } else {
      localStorage.removeItem(LIVE_PPS_DEBUG_CLAN_KEY);
    }
  } catch {}
}

function commIsBoostDebugActive() {
  return Boolean(commNormalizeDebugClan(livePpsDebugClan));
}

const storedBoostDebugClan = commGetBoostDebugClan();
if (storedBoostDebugClan !== null) {
  livePpsDebugClan = storedBoostDebugClan || '';
}

function commLoadTelegramConfig() {
  try {
    const token = localStorage.getItem(TELEGRAM_BOT_TOKEN_KEY) || '';
    const chatId = localStorage.getItem(TELEGRAM_CHAT_ID_KEY) || '';
    const enabled = localStorage.getItem(TELEGRAM_ENABLED_KEY) === '1';
    const winnerEnabled = localStorage.getItem(TELEGRAM_WINNER_ENABLED_KEY) === '1';
    const notifyMultiplierEnabled = localStorage.getItem(TELEGRAM_NOTIFY_MULTIPLIER_ENABLED_KEY) !== '0';
    const notifyLowBoostEnabled = localStorage.getItem(TELEGRAM_NOTIFY_LOW_BOOST_ENABLED_KEY) === '1';
    const notifyLowBoostMaxCount = Math.max(0, Math.floor(Number(localStorage.getItem(TELEGRAM_NOTIFY_LOW_BOOST_MAX_KEY) || '0') || 0));
    const notifyClanPowerEnabled = localStorage.getItem(TELEGRAM_NOTIFY_CLAN_POWER_ENABLED_KEY) === '1';
    const rawAllowed = localStorage.getItem(TELEGRAM_ALLOWED_MULTS_KEY);
    const allowed = rawAllowed ? JSON.parse(rawAllowed) : [1, 2, 4, 8, 16, 32, 64, '64+'];
    return {
      token: String(token || '').trim(),
      chatId: String(chatId || '').trim(),
      enabled,
      winnerEnabled,
      notifyMultiplierEnabled,
      notifyLowBoostEnabled,
      notifyLowBoostMaxCount,
      notifyClanPowerEnabled,
      allowed: Array.isArray(allowed) ? allowed : [1, 2, 4, 8, 16, 32, 64, '64+']
    };
  } catch {
    return {
      token: '',
      chatId: '',
      enabled: false,
      winnerEnabled: false,
      notifyMultiplierEnabled: true,
      notifyLowBoostEnabled: false,
      notifyLowBoostMaxCount: 0,
      notifyClanPowerEnabled: false,
      allowed: [1, 2, 4, 8, 16, 32, 64, '64+']
    };
  }
}

function commSaveTelegramConfig(cfg) {
  try {
    localStorage.setItem(TELEGRAM_BOT_TOKEN_KEY, String(cfg.token || ''));
    localStorage.setItem(TELEGRAM_CHAT_ID_KEY, String(cfg.chatId || ''));
    localStorage.setItem(TELEGRAM_ENABLED_KEY, cfg.enabled ? '1' : '0');
    localStorage.setItem(TELEGRAM_WINNER_ENABLED_KEY, cfg.winnerEnabled ? '1' : '0');
    localStorage.setItem(TELEGRAM_NOTIFY_MULTIPLIER_ENABLED_KEY, cfg.notifyMultiplierEnabled ? '1' : '0');
    localStorage.setItem(TELEGRAM_NOTIFY_LOW_BOOST_ENABLED_KEY, cfg.notifyLowBoostEnabled ? '1' : '0');
    localStorage.setItem(TELEGRAM_NOTIFY_LOW_BOOST_MAX_KEY, String(Math.max(0, Math.floor(Number(cfg.notifyLowBoostMaxCount || 0) || 0))));
    localStorage.setItem(TELEGRAM_NOTIFY_CLAN_POWER_ENABLED_KEY, cfg.notifyClanPowerEnabled ? '1' : '0');
    localStorage.setItem(TELEGRAM_ALLOWED_MULTS_KEY, JSON.stringify(cfg.allowed || []));
  } catch {}
}

function commLoadTelegramLastSent() {
  try {
    const raw = localStorage.getItem(TELEGRAM_LAST_SENT_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

function commSaveTelegramLastSent(data) {
  try {
    localStorage.setItem(TELEGRAM_LAST_SENT_KEY, JSON.stringify(data || {}));
  } catch {}
}

function commIsAllowedTelegramMultiplier(multiplier, allowedList) {
  if (!Number.isFinite(multiplier)) return false;
  const allowed = Array.isArray(allowedList) ? allowedList : [];
  if (allowed.includes(multiplier)) return true;
  if (multiplier >= 64 && allowed.includes('64+')) return true;
  return false;
}

function commLoadDiscordConfig() {
  try {
    const pushToken = localStorage.getItem(DISCORD_PUSH_TOKEN_KEY) || '';
    const enabled = localStorage.getItem(DISCORD_ENABLED_KEY) === '1';
    const winnerEnabled = localStorage.getItem(DISCORD_WINNER_ENABLED_KEY) === '1';
    const notifyMultiplierEnabled = localStorage.getItem(DISCORD_NOTIFY_MULTIPLIER_ENABLED_KEY) !== '0';
    const notifyLowBoostEnabled = localStorage.getItem(DISCORD_NOTIFY_LOW_BOOST_ENABLED_KEY) === '1';
    const notifyLowBoostMaxCount = Math.max(0, Math.floor(Number(localStorage.getItem(DISCORD_NOTIFY_LOW_BOOST_MAX_KEY) || '0') || 0));
    const notifyClanPowerEnabled = localStorage.getItem(DISCORD_NOTIFY_CLAN_POWER_ENABLED_KEY) === '1';
    const rawAllowed = localStorage.getItem(DISCORD_ALLOWED_MULTS_KEY);
    const allowed = rawAllowed ? JSON.parse(rawAllowed) : [1, 2, 4, 8, 16, 32, 64, '64+'];
    return {
      pushToken: String(pushToken || '').trim(),
      enabled,
      winnerEnabled,
      notifyMultiplierEnabled,
      notifyLowBoostEnabled,
      notifyLowBoostMaxCount,
      notifyClanPowerEnabled,
      allowed: Array.isArray(allowed) ? allowed : [1, 2, 4, 8, 16, 32, 64, '64+']
    };
  } catch {
    return {
      pushToken: '',
      enabled: false,
      winnerEnabled: false,
      notifyMultiplierEnabled: true,
      notifyLowBoostEnabled: false,
      notifyLowBoostMaxCount: 0,
      notifyClanPowerEnabled: false,
      allowed: [1, 2, 4, 8, 16, 32, 64, '64+']
    };
  }
}

function commSaveDiscordConfig(cfg) {
  try {
    localStorage.setItem(DISCORD_PUSH_TOKEN_KEY, String(cfg.pushToken || ''));
    localStorage.setItem(DISCORD_ENABLED_KEY, cfg.enabled ? '1' : '0');
    localStorage.setItem(DISCORD_WINNER_ENABLED_KEY, cfg.winnerEnabled ? '1' : '0');
    localStorage.setItem(DISCORD_NOTIFY_MULTIPLIER_ENABLED_KEY, cfg.notifyMultiplierEnabled ? '1' : '0');
    localStorage.setItem(DISCORD_NOTIFY_LOW_BOOST_ENABLED_KEY, cfg.notifyLowBoostEnabled ? '1' : '0');
    localStorage.setItem(DISCORD_NOTIFY_LOW_BOOST_MAX_KEY, String(Math.max(0, Math.floor(Number(cfg.notifyLowBoostMaxCount || 0) || 0))));
    localStorage.setItem(DISCORD_NOTIFY_CLAN_POWER_ENABLED_KEY, cfg.notifyClanPowerEnabled ? '1' : '0');
    localStorage.setItem(DISCORD_ALLOWED_MULTS_KEY, JSON.stringify(cfg.allowed || []));
  } catch {}
}

function commLoadDiscordLastSent() {
  try {
    const raw = localStorage.getItem(DISCORD_LAST_SENT_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

function commSaveDiscordLastSent(data) {
  try {
    localStorage.setItem(DISCORD_LAST_SENT_KEY, JSON.stringify(data || {}));
  } catch {}
}

function commBuildDiscordPushUrl(suffix = 'push') {
  const rawPushApiUrl = String(DISCORD_PUSH_API_URL || '').trim();
  if (!rawPushApiUrl) return '';
  const base = rawPushApiUrl.replace(/\/+$/, '');
  const normalizedSuffix = String(suffix || 'push').replace(/^\/+/, '');
  const hasPushBase = /\/push$/i.test(base);

  if (!normalizedSuffix || normalizedSuffix === 'push') {
    return hasPushBase ? base : `${base}/push`;
  }

  if (hasPushBase) {
    if (normalizedSuffix.startsWith('push/')) {
      return `${base}/${normalizedSuffix.slice(5)}`;
    }
    return `${base}/${normalizedSuffix}`;
  }

  if (normalizedSuffix.startsWith('push/')) {
    return `${base}/${normalizedSuffix}`;
  }
  return `${base}/push/${normalizedSuffix}`;
}

function commNormalizeDiscordPushStatus(raw) {
  const src = raw && typeof raw === 'object' ? raw : {};
  const sentCount = Number.isFinite(Number(src.sent_count)) ? Number(src.sent_count) : 0;
  const lastSentAt = src.last_sent_at ? String(src.last_sent_at) : '';
  const lastError = src.last_error ? String(src.last_error) : '';
  const lastErrorAt = src.last_error_at ? String(src.last_error_at) : '';
  const rows = Array.isArray(src.last_messages) ? src.last_messages : [];
  const lastMessages = rows.slice(0, 5).map((row) => {
    const item = row && typeof row === 'object' ? row : {};
    return {
      sentAt: item.sent_at ? String(item.sent_at) : '',
      message: item.message ? String(item.message) : '',
      channelId: item.channel_id ? String(item.channel_id) : '',
      guildId: item.guild_id ? String(item.guild_id) : ''
    };
  });
  return { sentCount, lastSentAt, lastError, lastErrorAt, lastMessages };
}

function commBuildDiscordPushStatusUrls() {
  const urls = [];
  const preferred = commBuildDiscordPushUrl('status');
  if (preferred) urls.push(preferred);

  const rawPushApiUrl = String(DISCORD_PUSH_API_URL || '').trim().replace(/\/+$/, '');
  if (rawPushApiUrl) {
    const legacy = `${rawPushApiUrl}/push/status`;
    if (!urls.includes(legacy)) urls.push(legacy);
  }
  return urls;
}

async function commFetchDiscordPushStatus(ownerId, pushToken) {
  const normalizedOwnerId = String(ownerId || '').trim();
  const normalizedPushToken = String(pushToken || '').trim();
  const statusUrls = commBuildDiscordPushStatusUrls();
  if (!statusUrls.length) throw new Error('Push URL missing');
  if (!normalizedOwnerId) throw new Error('No owner ID (login required)');
  if (!normalizedPushToken) throw new Error('Push token missing');

  let lastNotFoundError = null;
  for (const statusUrl of statusUrls) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 12000);
    try {
      const res = await fetch(statusUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          owner_id: normalizedOwnerId,
          push_token: normalizedPushToken
        }),
        signal: controller.signal
      });
      const rawBody = await res.text().catch(() => '');
      let parsed = null;
      if (rawBody) {
        try { parsed = JSON.parse(rawBody); } catch {}
      }
      if (!res.ok) {
        const err = new Error(parsed?.error || parsed?.message || rawBody || `HTTP ${res.status}`);
        err.status = Number(res.status) || 0;
        if (err.status === 404) {
          lastNotFoundError = err;
          continue;
        }
        throw err;
      }
      if (!parsed || typeof parsed !== 'object' || parsed.ok !== true) {
        throw new Error('Invalid /push/status response. Check proxy route for /push/status.');
      }
      return {
        statusCode: Number(res.status) || 200,
        ...commNormalizeDiscordPushStatus(parsed)
      };
    } finally {
      clearTimeout(timeoutId);
    }
  }
  if (lastNotFoundError) throw lastNotFoundError;
  throw new Error('Push status endpoint unavailable.');
}

function commIsAllowedDiscordMultiplier(multiplier, allowedList) {
  if (!Number.isFinite(multiplier)) return false;
  const allowed = Array.isArray(allowedList) ? allowedList : [];
  if (allowed.includes(multiplier)) return true;
  if (multiplier >= 64 && allowed.includes('64+')) return true;
  return false;
}

function commParseMultiplierForNotify(value) {
  if (value == null) return null;
  const raw = String(value).toLowerCase().replace(/x/g, '').replace(/[^0-9.]/g, '').trim();
  if (!raw) return null;
  const parsed = Number(raw);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

function commIsTrustedBlockId(value) {
  const normalized = normalizeBlockId(value);
  if (!normalized) return false;
  return /^9\d{5,}$/.test(normalized);
}

function commResolveNotificationRoundId() {
  const summaryId = normalizeBlockId(lastLiveSummaryBlockId);
  const currentId = normalizeBlockId(
    currentRoundInfo?.blockId
    ?? lastCurrentRoundLivePayload?.blockId
    ?? liveBoostRoundKey
    ?? null
  );
  const summaryTrusted = commIsTrustedBlockId(summaryId) ? summaryId : null;
  const currentTrusted = commIsTrustedBlockId(currentId) ? currentId : null;

  if (summaryTrusted && currentTrusted) {
    const summaryNum = Number(summaryTrusted);
    const currentNum = Number(currentTrusted);
    if (Number.isFinite(summaryNum) && Number.isFinite(currentNum)) {
      return String(Math.max(summaryNum, currentNum));
    }
    return currentTrusted;
  }
  if (currentTrusted) return currentTrusted;
  if (summaryTrusted) return summaryTrusted;
  return null;
}

function commResolveMultiplierForNotify(roundId) {
  const targetRoundId = normalizeBlockId(roundId);
  if (!targetRoundId || !commIsTrustedBlockId(targetRoundId)) return null;

  const now = Date.now();
  const candidates = [];
  const addCandidate = (value, sourceRoundId, updatedAt, maxAgeMs = null) => {
    const normalizedSourceRound = normalizeBlockId(sourceRoundId);
    if (!normalizedSourceRound || !commIsTrustedBlockId(normalizedSourceRound)) return;
    if (normalizedSourceRound !== targetRoundId) return;
    if (Number.isFinite(updatedAt) && updatedAt > 0 && Number.isFinite(maxAgeMs) && maxAgeMs > 0) {
      if ((now - updatedAt) > maxAgeMs) return;
    }
    const parsed = commParseMultiplierForNotify(value);
    if (!Number.isFinite(parsed)) return;
    candidates.push(parsed);
  };

  // Live summary can lag, but is stable.
  addCandidate(lastLiveSummaryMultiplier, lastLiveSummaryMultiplierBlockId, liveSummaryLastAt, 90_000);
  // Current round stream is usually faster; prefer highest valid value from same round.
  addCandidate(currentRoundInfo?.multiplier, currentRoundInfo?.blockId, commCurrentRoundLastAt, 25_000);
  addCandidate(lastCurrentRoundLivePayload?.multiplier, lastCurrentRoundLivePayload?.blockId, commCurrentRoundLastAt, 25_000);

  if (!candidates.length) return null;
  return Math.max(...candidates);
}

function commMarkRoundStart(blockId) {
  const normalized = normalizeBlockId(blockId);
  if (!normalized) return;
  commRoundStartDetected = { blockId: normalized, detectedAt: Date.now() };
}

function commSetCurrentRoundWithoutStartMark(blockId) {
  const normalized = normalizeBlockId(blockId);
  if (!normalized) return;
  commRoundStartDetected = { blockId: normalized, detectedAt: 0 };
}

function commResolveRoundElapsedLabel(roundId, fallbackTimer) {
  const normalizedRoundId = normalizeBlockId(roundId);
  const parsedFallback = parseRoundTimerSeconds(fallbackTimer);
  const hasDetectedStart = normalizedRoundId
    && commRoundStartDetected.blockId === normalizedRoundId
    && Number.isFinite(commRoundStartDetected.detectedAt)
    && commRoundStartDetected.detectedAt > 0;

  if (hasDetectedStart) {
    const elapsedSec = Math.max(0, Math.floor((Date.now() - commRoundStartDetected.detectedAt) / 1000));
    if (Number.isFinite(parsedFallback)) {
      // If timer jumps to a stale value at round transition, trust local elapsed right after start.
      if (elapsedSec <= 30 && parsedFallback >= 120) return formatRoundTimerSeconds(elapsedSec);
      return formatRoundTimerSeconds(parsedFallback);
    }
    return formatRoundTimerSeconds(elapsedSec);
  }

  if (Number.isFinite(parsedFallback)) return formatRoundTimerSeconds(parsedFallback);
  return fallbackTimer != null ? String(fallbackTimer) : '-';
}

function commNormalizePushMessageLine(text) {
  const raw = String(text ?? '').trim();
  if (!raw) return '';
  let single = raw.replace(/\r?\n+/g, ' - ');
  single = single.replace(/\s+/g, ' ').trim();
  if (single.length > 1800) single = `${single.slice(0, 1797)}...`;
  return single;
}

async function commSendDiscordMessage(text) {
  const cfg = commLoadDiscordConfig();
  if (!cfg.enabled) throw new Error('Discord bot disabled.');
  const pushApiUrl = commBuildDiscordPushUrl('push');
  const ownerId = String(getHomeAuthUserId() || '').trim();
  const pushToken = String(cfg.pushToken || '').trim();
  const normalizedMessage = commNormalizePushMessageLine(text);

  if (!ownerId) throw new Error('No owner ID found. Log in first.');
  if (!pushApiUrl || !pushToken) {
    throw new Error('Configure Discord bot push (Push token).');
  }
  if (!normalizedMessage) throw new Error('Cannot send an empty notification.');
  for (let attempt = 1; attempt <= DISCORD_PUSH_MAX_ATTEMPTS; attempt += 1) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), DISCORD_PUSH_TIMEOUT_MS);
    try {
      const pushRes = await fetch(pushApiUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          owner_id: ownerId,
          push_token: pushToken,
          message: normalizedMessage
        }),
        signal: controller.signal
      });
      const rawBody = await pushRes.text().catch(() => '');
      let parsedBody = null;
      if (rawBody) {
        try { parsedBody = JSON.parse(rawBody); } catch {}
      }
      if (!pushRes.ok) {
        const httpErr = new Error(parsedBody?.error || parsedBody?.message || rawBody || `Discord push error (${pushRes.status})`);
        httpErr.status = pushRes.status;
        throw httpErr;
      }
      return true;
    } catch (err) {
      const status = Number(err?.status);
      const isTimeout = err?.name === 'AbortError';
      const isNetwork = err instanceof TypeError;
      const isServerErr = Number.isFinite(status) && status >= 500;
      const isRetryable = isTimeout || isNetwork || isServerErr;
      if (attempt < DISCORD_PUSH_MAX_ATTEMPTS && isRetryable) {
        await new Promise((resolve) => setTimeout(resolve, DISCORD_PUSH_RETRY_DELAY_MS * attempt));
        continue;
      }
      if (isTimeout) {
        const timeoutMsg = 'Discord push timeout. Try again in a moment.';
        throw new Error(timeoutMsg);
      }
      if (isNetwork) {
        const netMsg = 'Failed to fetch Discord push endpoint.';
        throw new Error(netMsg);
      }
      throw err;
    } finally {
      clearTimeout(timeoutId);
    }
  }
  throw new Error('Discord push failed.');
}

function commPostMultiplierToDiscord() {
  const cfg = commLoadDiscordConfig();
  if (!cfg.enabled) return;
  if (cfg.notifyMultiplierEnabled === false) return;
  const roundId = commResolveNotificationRoundId();
  if (!roundId) return;
  const multValue = commResolveMultiplierForNotify(roundId);
  if (!Number.isFinite(multValue)) return;
  if (!commIsAllowedDiscordMultiplier(multValue, cfg.allowed)) return;
  const last = discordLastSent || commLoadDiscordLastSent() || {};
  const sentList = Array.isArray(last?.sent) ? last.sent : [];
  const sentKey = `${roundId}:${multValue}`;
  if (sentList.includes(sentKey)) return;
  if (discordSentInFlight.has(sentKey)) return;
  discordSentInFlight.add(sentKey);
  const timeLabel = commResolveRoundElapsedLabel(
    roundId,
    currentRoundInfo?.timer || lastCurrentRoundLivePayload?.timer || '-'
  );
  const message = `Multiplier: ${multValue}x - Round: ${roundId} - Round elapsed: ${timeLabel}`;
  commSendDiscordMessage(message)
    .then(() => {
      const next = { ...last, sent: [...sentList, sentKey], updatedAt: Date.now() };
      discordLastSent = next;
      commSaveDiscordLastSent(next);
    })
    .catch((err) => {
      console.warn('[MW] Discord post failed:', err?.message || err);
    })
    .finally(() => {
      discordSentInFlight.delete(sentKey);
    });
}

function commBuildDiscordWinMessage(data) {
  const clanName = data?.clan_name ?? data?.clanName ?? data?.winningClan ?? 'Unknown clan';
  const player = data?.winner_player_name ?? data?.playerName ?? data?.userAlias ?? data?.userName ?? 'Unknown';
  const blockId = data?.block_id ?? data?.blockId ?? data?.roundId ?? '-';
  const multiplier = data?.multiplier ?? data?.multiplierValue ?? data?.multiplier_text ?? '-';
  const roundTime = data?.round_time ?? data?.roundTime ?? data?.timer ?? '-';
  const reward = data?.winner_reward_gmt ?? data?.reward_gmt ?? data?.rewardGmt ?? data?.reward ?? null;
  const rewardLabel = Number.isFinite(commParseNumber(reward)) ? formatDecimal(commParseNumber(reward), 6) : (reward ?? '-');
  return [
    '[WIN] Clan won the round!',
    `Clan: ${clanName}`,
    `Player: ${player}`,
    `Round: ${blockId}`,
    `Multiplier: ${multiplier}`,
    `Round time: ${roundTime}`,
    `Reward: ${rewardLabel}`
  ].join('\n');
}

function commPostWinnerToDiscord(data) {
  const cfg = commLoadDiscordConfig();
  if (!cfg.enabled || !cfg.winnerEnabled) return;
  if (!commIsMyClanWinner(data)) return;
  const blockId = data?.block_id ?? data?.blockId ?? data?.roundId ?? 'unknown';
  const last = discordLastSent || commLoadDiscordLastSent() || {};
  const sentList = Array.isArray(last?.sent) ? last.sent : [];
  const sentKey = `win:${blockId}`;
  if (sentList.includes(sentKey)) return;
  if (discordSentInFlight.has(sentKey)) return;
  discordSentInFlight.add(sentKey);
  const message = commBuildDiscordWinMessage(data);
  commSendDiscordMessage(message)
    .then(() => {
      const next = { ...last, sent: [...sentList, sentKey], updatedAt: Date.now() };
      discordLastSent = next;
      commSaveDiscordLastSent(next);
    })
    .catch((err) => {
      console.warn('[MW] Discord win post failed:', err?.message || err);
    })
    .finally(() => {
      discordSentInFlight.delete(sentKey);
    });
}

async function commSendTelegramMessage(text) {
  const cfg = commLoadTelegramConfig();
  if (!cfg.enabled) throw new Error('Telegram bot disabled.');
  if (!cfg.token || !cfg.chatId) throw new Error('Telegram token or channel ID missing.');
  const normalizedMessage = commNormalizePushMessageLine(text);
  if (!normalizedMessage) throw new Error('Cannot send an empty notification.');
  const url = `https://api.telegram.org/bot${cfg.token}/sendMessage`;
  const payload = {
    chat_id: cfg.chatId,
    text: normalizedMessage,
    disable_web_page_preview: true
  };
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok || data?.ok === false) {
    throw new Error(data?.description || `Telegram error (${res.status})`);
  }
  return data;
}

function commPostMultiplierToTelegram() {
  const cfg = commLoadTelegramConfig();
  if (!cfg.enabled) return;
  if (cfg.notifyMultiplierEnabled === false) return;
  const roundId = commResolveNotificationRoundId();
  if (!roundId) return;
  const multValue = commResolveMultiplierForNotify(roundId);
  if (!Number.isFinite(multValue)) return;
  if (!commIsAllowedTelegramMultiplier(multValue, cfg.allowed)) return;
  const last = telegramLastSent || commLoadTelegramLastSent() || {};
  const sentList = Array.isArray(last?.sent) ? last.sent : [];
  const sentKey = `${roundId}:${multValue}`;
  if (sentList.includes(sentKey)) return;
  if (telegramSentInFlight.has(sentKey)) return;
  telegramSentInFlight.add(sentKey);
  const timeLabel = commResolveRoundElapsedLabel(
    roundId,
    currentRoundInfo?.timer || lastCurrentRoundLivePayload?.timer || '-'
  );
  const message = `Multiplier: ${multValue}x - Round: ${roundId} - Round elapsed: ${timeLabel}`;
  commSendTelegramMessage(message)
    .then(() => {
      const next = { ...last, sent: [...sentList, sentKey], updatedAt: Date.now() };
      telegramLastSent = next;
      commSaveTelegramLastSent(next);
    })
    .catch((err) => {
      console.warn('[MW] Telegram post failed:', err?.message || err);
    })
    .finally(() => {
      telegramSentInFlight.delete(sentKey);
    });
}

function commNormalizeClanMatch(value) {
  if (!value) return '';
  return normalizeClanName(commCleanClanDisplayName(String(value)));
}

function commIsMyClanWinner(data) {
  const winnerName = commNormalizeClanMatch(data?.clan_name ?? data?.clanName ?? data?.winningClan ?? null);
  const winnerId = commParseNumber(data?.clan_id ?? data?.clanId ?? data?.winner_clan_id ?? null);
  const myIdRaw = commParseNumber(liveMyClanId ?? commSession?.clanId ?? null);
  const myId = Number.isFinite(myIdRaw) ? Math.trunc(myIdRaw) : null;
  const myName = commNormalizeClanMatch(liveMyClanName ?? commSession?.clanName ?? null);
  if (Number.isFinite(winnerId) && Number.isFinite(myId)) {
    return Math.trunc(winnerId) === Math.trunc(myId);
  }
  if (winnerName && myName) {
    return winnerName === myName;
  }
  return false;
}

function commBuildTelegramWinMessage(data) {
  const clanName = data?.clan_name ?? data?.clanName ?? data?.winningClan ?? 'Unknown clan';
  const player = data?.winner_player_name ?? data?.playerName ?? data?.userAlias ?? data?.userName ?? 'Unknown';
  const blockId = data?.block_id ?? data?.blockId ?? data?.roundId ?? '-';
  const multiplier = data?.multiplier ?? data?.multiplierValue ?? data?.multiplier_text ?? '-';
  const roundTime = data?.round_time ?? data?.roundTime ?? data?.timer ?? '-';
  const reward = data?.winner_reward_gmt ?? data?.reward_gmt ?? data?.rewardGmt ?? data?.reward ?? null;
  const rewardLabel = Number.isFinite(commParseNumber(reward)) ? formatDecimal(commParseNumber(reward), 6) : (reward ?? '-');
  return [
    '[WIN] Clan won the round!',
    `Clan: ${clanName}`,
    `Player: ${player}`,
    `Round: ${blockId}`,
    `Multiplier: ${multiplier}`,
    `Round time: ${roundTime}`,
    `Reward: ${rewardLabel}`
  ].join('\n');
}

function commPostWinnerToTelegram(data) {
  const cfg = commLoadTelegramConfig();
  if (!cfg.enabled || !cfg.winnerEnabled) return;
  if (!commIsMyClanWinner(data)) return;
  const blockId = data?.block_id ?? data?.blockId ?? data?.roundId ?? 'unknown';
  const last = telegramLastSent || commLoadTelegramLastSent() || {};
  const sentList = Array.isArray(last?.sent) ? last.sent : [];
  const sentKey = `win:${blockId}`;
  if (sentList.includes(sentKey)) return;
  if (telegramSentInFlight.has(sentKey)) return;
  telegramSentInFlight.add(sentKey);
  const message = commBuildTelegramWinMessage(data);
  commSendTelegramMessage(message)
    .then(() => {
      const next = { ...last, sent: [...sentList, sentKey], updatedAt: Date.now() };
      telegramLastSent = next;
      commSaveTelegramLastSent(next);
    })
    .catch((err) => {
      console.warn('[MW] Telegram win post failed:', err?.message || err);
    })
    .finally(() => {
      telegramSentInFlight.delete(sentKey);
    });
}

function commBuildBoostHelperNotificationMessage(activeEntries, context) {
  const entries = Array.isArray(activeEntries) ? activeEntries : [];
  if (!entries.length) return '';
  const roundLabel = normalizeBlockId(context?.roundIdLabel ?? null) || String(context?.roundIdLabel || '-');
  const multiplierLabel = String(context?.multiplierLabel || '-');
  const timeLabel = String(context?.timerLabel || '-');
  const maxItems = 5;
  const formatLabel = (value) => {
    let label = String(value || 'Notification').replace(/\s+/g, ' ').trim();
    if (/^My clan used\s+/i.test(label)) {
      label = label.replace(/^My clan used\s+/i, 'My clan used : ');
    }
    return label;
  };
  const lines = entries.slice(0, maxItems).map((entry) => {
    const label = formatLabel(entry?.label || entry?.groupTitle || 'Notification');
    return `R: ${roundLabel} - ${multiplierLabel} - ${timeLabel} - ${label}`;
  });
  if (entries.length > maxItems) {
    lines.push(`+${entries.length - maxItems} more active notifications`);
  }
  let msg = lines.join('\n').trim();
  if (msg.length > 1800) msg = `${msg.slice(0, 1797)}...`;
  return msg;
}

function commPostBoostHelperNotifications(activeEntries, context) {
  if (!Array.isArray(activeEntries) || !activeEntries.length) return;
  const message = commBuildBoostHelperNotificationMessage(activeEntries, context);
  if (!message) return;

  const discordCfg = commLoadDiscordConfig();
  if (discordCfg?.enabled) {
    commSendDiscordMessage(message).catch((err) => {
      console.warn('[MW] Discord boost-helper post failed:', err?.message || err);
    });
  }

  const telegramCfg = commLoadTelegramConfig();
  if (telegramCfg?.enabled) {
    commSendTelegramMessage(message).catch((err) => {
      console.warn('[MW] Telegram boost-helper post failed:', err?.message || err);
    });
  }
}

function commBuildBotOptionNotificationLine(context, label) {
  const roundLabel = normalizeBlockId(context?.roundIdLabel ?? null) || String(context?.roundIdLabel || '-');
  const multiplierLabel = String(context?.multiplierLabel || '-');
  const timeLabel = String(context?.timerLabel || '-');
  return `R: ${roundLabel} - ${multiplierLabel} - ${timeLabel} - ${label}`;
}

function commCollectBotOptionEntries(cfg) {
  const entries = [];
  const maxBoostCount = Math.max(0, Math.floor(Number(cfg?.notifyLowBoostMaxCount || 0) || 0));
  if (cfg?.notifyLowBoostEnabled) {
    const usedCount = boostHelperGetBoostCountInWindow({ from: 0, to: 90 });
    if (usedCount != null && usedCount <= maxBoostCount) {
      entries.push({
        key: `low_boost:${maxBoostCount}`,
        label: `Few/no boosts in first 90s (<= ${maxBoostCount})`
      });
    }
  }
  if (cfg?.notifyClanPowerEnabled) {
    const used = boostHelperGetMyClanBoostCount('clanpowerup');
    if (used >= 1) {
      entries.push({
        key: 'clan_power_up',
        label: 'My clan used a Clan power-up'
      });
    }
  }
  return entries;
}

function commPostConfiguredOptionNotificationsForDiscord(context) {
  const cfg = commLoadDiscordConfig();
  if (!cfg?.enabled || !cfg?.pushToken) return;
  const roundId = normalizeBlockId(context?.roundIdLabel ?? null);
  if (!roundId) return;
  const entries = commCollectBotOptionEntries(cfg);
  if (!entries.length) return;
  entries.forEach((entry) => {
    const sentKey = `opt:${roundId}:${entry.key}`;
    const last = discordLastSent || commLoadDiscordLastSent() || {};
    const sentList = Array.isArray(last?.sent) ? last.sent : [];
    if (sentList.includes(sentKey) || discordSentInFlight.has(sentKey)) return;
    discordSentInFlight.add(sentKey);
    const message = commBuildBotOptionNotificationLine(context, entry.label);
    commSendDiscordMessage(message)
      .then(() => {
        const latest = discordLastSent || commLoadDiscordLastSent() || {};
        const latestSent = Array.isArray(latest?.sent) ? latest.sent.slice() : [];
        if (!latestSent.includes(sentKey)) latestSent.push(sentKey);
        const next = { ...latest, sent: latestSent, updatedAt: Date.now() };
        discordLastSent = next;
        commSaveDiscordLastSent(next);
      })
      .catch((err) => {
        console.warn('[MW] Discord option post failed:', err?.message || err);
      })
      .finally(() => {
        discordSentInFlight.delete(sentKey);
      });
  });
}

function commPostConfiguredOptionNotificationsForTelegram(context) {
  const cfg = commLoadTelegramConfig();
  if (!cfg?.enabled || !cfg?.token || !cfg?.chatId) return;
  const roundId = normalizeBlockId(context?.roundIdLabel ?? null);
  if (!roundId) return;
  const entries = commCollectBotOptionEntries(cfg);
  if (!entries.length) return;
  entries.forEach((entry) => {
    const sentKey = `opt:${roundId}:${entry.key}`;
    const last = telegramLastSent || commLoadTelegramLastSent() || {};
    const sentList = Array.isArray(last?.sent) ? last.sent : [];
    if (sentList.includes(sentKey) || telegramSentInFlight.has(sentKey)) return;
    telegramSentInFlight.add(sentKey);
    const message = commBuildBotOptionNotificationLine(context, entry.label);
    commSendTelegramMessage(message)
      .then(() => {
        const latest = telegramLastSent || commLoadTelegramLastSent() || {};
        const latestSent = Array.isArray(latest?.sent) ? latest.sent.slice() : [];
        if (!latestSent.includes(sentKey)) latestSent.push(sentKey);
        const next = { ...latest, sent: latestSent, updatedAt: Date.now() };
        telegramLastSent = next;
        commSaveTelegramLastSent(next);
      })
      .catch((err) => {
        console.warn('[MW] Telegram option post failed:', err?.message || err);
      })
      .finally(() => {
        telegramSentInFlight.delete(sentKey);
      });
  });
}

function commPostConfiguredBotOptionNotifications(context) {
  commPostConfiguredOptionNotificationsForDiscord(context);
  commPostConfiguredOptionNotificationsForTelegram(context);
}

function getLiveApiRefreshInterval() {
  return LIVE_API_REFRESH_DEFAULT;
}

function setLiveApiRefreshInterval(value) {
  const next = LIVE_API_REFRESH_DEFAULT;
  liveApiRefreshIntervalMs = next;
  liveApiStatusWindowMs = next * 2;
}

function commGetLiveApiRefreshDelayMs() {
  const min = LIVE_API_REFRESH_MIN_MS;
  const max = LIVE_API_REFRESH_MAX_MS;
  const span = Math.max(0, max - min);
  return min + Math.floor(Math.random() * (span + 1));
}

function commUpdateLiveApiCountdown() {
  const el = typeof document !== 'undefined'
    ? document.getElementById('liveApiNextCountdown')
    : null;
  if (!el) return;
  if (!liveApiNextAt) {
    el.textContent = '-';
    return;
  }
  const remaining = liveApiNextAt - Date.now();
  if (!Number.isFinite(remaining) || remaining <= 0) {
    el.textContent = 'now';
    return;
  }
  el.textContent = commFormatDurationShort(remaining);
}

function commEnsureLiveApiCountdownTimer() {
  if (liveApiCountdownTimer) return;
  liveApiCountdownTimer = setInterval(() => {
    commUpdateLiveApiCountdown();
  }, 1000);
}

function commSetLiveApiNextAt(delayMs) {
  if (!Number.isFinite(delayMs) || delayMs <= 0) {
    liveApiNextAt = 0;
    liveApiNextDelayMs = 0;
    return;
  }
  liveApiNextDelayMs = delayMs;
  liveApiNextAt = Date.now() + delayMs;
  commEnsureLiveApiCountdownTimer();
  commUpdateLiveApiCountdown();
}

function commStartLiveApiRefreshTimerLocal() {
  const delay = commGetLiveApiRefreshDelayMs();
  commSetLiveApiNextAt(delay);
  liveApiPeriodicTimer = setTimeout(() => {
    liveApiPeriodicTimer = null;
    commRefreshLiveApiState({ force: false }).catch(() => {});
    commStartLiveApiRefreshTimer();
  }, delay);
}

function commRequestLiveApiAlarmStart() {
  if (!chrome?.runtime?.sendMessage) {
    return Promise.reject(new Error('Chrome runtime messaging not available'));
  }
  return new Promise((resolve, reject) => {
    let settled = false;
    const timeoutId = setTimeout(() => {
      if (settled) return;
      settled = true;
      reject(new Error('Live API alarm start timed out'));
    }, LIVE_API_ALARM_START_TIMEOUT_MS);
    const handleResponse = (response) => {
      if (settled) return;
      settled = true;
      clearTimeout(timeoutId);
      const lastError = chrome.runtime?.lastError;
      if (lastError) {
        reject(lastError);
        return;
      }
      resolve(response?.ok === true);
    };
    try {
      const maybePromise = chrome.runtime.sendMessage({ type: 'MW_LIVE_API_ALARM_START' }, handleResponse);
      if (maybePromise && typeof maybePromise.then === 'function') {
        maybePromise.then(handleResponse).catch((err) => {
          if (settled) return;
          settled = true;
          clearTimeout(timeoutId);
          reject(err);
        });
      }
    } catch (err) {
      if (settled) return;
      settled = true;
      clearTimeout(timeoutId);
      reject(err);
    }
  });
}

function commStartLiveApiRefreshTimer() {
  if (!LIVE_API_STATE_ENABLED) return;
  if (liveApiPeriodicTimer) {
    clearTimeout(liveApiPeriodicTimer);
    liveApiPeriodicTimer = null;
  }
  if (LIVE_API_ALARM_ENABLED && chrome?.runtime?.sendMessage) {
    liveApiAlarmActive = false;
    liveApiAlarmLastStartAt = Date.now();
    console.log('[MW API] Requesting live API alarm schedule');
    commRequestLiveApiAlarmStart()
      .then((ok) => {
        if (ok) {
          liveApiAlarmActive = true;
          console.log('[MW API] Live API alarm scheduled');
          return;
        }
        console.warn('[MW API] Live API alarm start returned no ok, using local timer');
        commStartLiveApiRefreshTimerLocal();
      })
      .catch((err) => {
        console.warn('[MW API] Live API alarm start failed, using local timer:', err?.message || err);
        commStartLiveApiRefreshTimerLocal();
      });
    return;
  }
  commStartLiveApiRefreshTimerLocal();
}

function commIndexLiveApiClans(clans) {
  const next = new Map();
  (Array.isArray(clans) ? clans : []).forEach((entry) => {
    const key = normalizeClanKey(entry);
    if (!key || next.has(key)) return;
    next.set(key, entry);
  });
  liveApiStateLookup = next;
}

function commResolveLiveApiLeagueId() {
  let leagueId = commParseNumber(commSession?.leagueId ?? commSession?.league_id ?? null);
  if (!Number.isFinite(leagueId) && typeof lastLiveSummaryLeague !== 'undefined') {
    leagueId = commParseNumber(lastLiveSummaryLeague);
  }
  if (!Number.isFinite(leagueId) && Number.isFinite(commKorrelCurrentLeagueId)) {
    leagueId = commKorrelCurrentLeagueId;
  }
  return Number.isFinite(leagueId) ? Math.trunc(leagueId) : null;
}

function commFindLiveApiClan(entry) {
  const key = normalizeClanKey(entry);
  if (!key) return null;
  return liveApiStateLookup.get(key) || null;
}

function commMergeWsFieldsIntoApiEntry(wsEntry) {
  if (!wsEntry) return false;
  const apiEntry = commFindLiveApiClan(wsEntry);
  if (!apiEntry) return false;
  const fields = [
    'activeBoostScore',
    'active_boost_score',
    'usedAbilities',
    'used_abilities'
  ];
  let updated = false;
  fields.forEach((field) => {
    const value = wsEntry[field];
    if (value == null || value === '') return;
    if (Array.isArray(value)) {
      if (!value.length) return;
      apiEntry[field] = value.slice();
    } else {
      apiEntry[field] = value;
    }
    updated = true;
  });
  return updated;
}

function commMergeLiveClanFields(baseEntry, apiEntry) {
  if (!apiEntry) return baseEntry;
  const merged = { ...baseEntry };
  const sources = [
    apiEntry,
    apiEntry?.clan,
    apiEntry?.clanData,
    apiEntry?.clanInfo
  ].filter(Boolean);
  const fill = (key) => {
    if (merged[key] != null && merged[key] !== '') return;
    for (const source of sources) {
      const apiValue = source?.[key];
      if (apiValue != null && apiValue !== '') {
        merged[key] = apiValue;
        return;
      }
    }
  };
  LIVE_SCORE_MERGE_KEYS.forEach(fill);
  LIVE_POWER_MERGE_KEYS.forEach(fill);
  LIVE_PPS_MERGE_KEYS.forEach(fill);
  return merged;
}

function commHasBoostSignal(entry) {
  if (!entry) return false;
  const boostScore = commParseNumber(
    entry?.activeBoostScore ??
      entry?.active_boost_score ??
      entry?.boostScore ??
      entry?.boost_score ??
      null
  );
  if (Number.isFinite(boostScore) && boostScore > 0) return true;
  const abilityLists = [
    entry?.usedAbilities,
    entry?.used_abilities,
    entry?.clan?.usedAbilities,
    entry?.clanData?.usedAbilities,
    entry?.clanInfo?.usedAbilities
  ].filter(Boolean);
  for (const abilities of abilityLists) {
    if (!Array.isArray(abilities)) continue;
    if (abilities.some((ability) => Number(ability?.count || 0) > 0)) return true;
  }
  const boostMap = entry?.boosts ?? entry?.boostMap ?? entry?.boost_map ?? entry?.clanBoosts ?? entry?.clan_boosts;
  if (boostMap && typeof boostMap === 'object' && !Array.isArray(boostMap)) {
    return Object.values(boostMap).some((value) => Number(value || 0) > 0);
  }
  return false;
}

async function commRefreshLiveApiState({ force = false } = {}) {
  console.log('[MW API] commRefreshLiveApiState called', {
    force,
    pending: liveApiStatePending,
    LIVE_API_STATE_ENABLED
  });

  if (liveApiStatePending) {
    console.log('[MW API] Already pending, skipping');
    return;
  }
  const now = Date.now();
  if (!force && liveApiStateLastAttemptAt && now - liveApiStateLastAttemptAt < liveApiRefreshIntervalMs) {
    console.log('[MW API] Rate limited, skipping');
    return;
  }
  const tab = typeof commFindMiningWarsTabWithAuth === 'function'
    ? await commFindMiningWarsTabWithAuth()
    : await commFindMiningWarsTab();
  console.log('[MW API] Tab found:', tab?.id);
  if (!tab?.id) {
    console.log('[MW API] No tab, aborting');
    return;
  }
  liveApiStateLastAttemptAt = now;
  liveApiStatePending = true;
  try {
    const endpoint = 'https://api.gomining.com/api/nft-game/round/get-state';

    const leagueId = commResolveLiveApiLeagueId();
    if (!Number.isFinite(leagueId)) {
      console.log('[MW API] No valid leagueId, aborting. commSession:', commSession, 'lastLiveSummaryLeague:', lastLiveSummaryLeague, 'commKorrelCurrentLeagueId:', commKorrelCurrentLeagueId);
      liveApiStatePending = false;
      return;
    }

    console.log('[MW API] Using leagueId:', leagueId);
    liveApiLastLeagueId = Math.trunc(leagueId);
    commRefreshLiveWinsPercent({ force: false }).catch(() => {});

    const roundKeyCandidate = [
      lastCurrentRoundLivePayload?.blockId,
      currentRoundInfo?.blockId,
      lastCurrentRoundLivePayload?.block_id,
      currentRoundInfo?.block_id
    ]
      .map((value) => normalizeBlockId(value))
      .find((value) => value);
    if (roundKeyCandidate && roundKeyCandidate !== '-' && roundKeyCandidate !== '-') {
      if (liveApiRoundKey && roundKeyCandidate !== liveApiRoundKey) {
        liveApiLastStablePpsByClan = liveApiStablePpsByClan;
        liveApiStablePpsByClan = new Map();
        liveApiScoreRateMap = new Map();
        liveApiStoredBasePps = new Map();
        liveApiBasePpsValidCounts = new Map();
        liveApiBasePpsByClan = new Map();
        liveApiTimerSeconds = null;
        liveApiTimerSecondsPrev = null;
        liveApiTimerUpdatedAt = 0;
      }
      liveApiRoundKey = roundKeyCandidate;
    }

    const payload = {
      type: 'clans',
      leagueId: Math.trunc(leagueId),
      pagination: { skip: 0, limit: 50 }
    };

    console.log('[MW API] Fetching clan leaderboard...', { endpoint, payload });
    const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload, { skipLimit: true });
    console.log('[MW API] Response status:', response?.status || 0);
    liveApiLastStatus = response?.status || 0;
    liveApiStateUpdatedAt = now;
    if (!response?.ok) {
      liveApiLastErrorAt = now;
      console.warn('[MW API] Response not ok:', response?.status || 0);
      return;
    }

    liveApiLastOkAt = now;
    const responseData = response?.data;
    console.log('[MW API] Response data:', responseData);
    const data = responseData?.data || responseData;
    const normalizeTimerValue = (value) => {
      if (value == null) return null;
      if (Number.isFinite(value)) return formatRoundTimerSeconds(value);
      const raw = String(value).trim();
      if (!raw) return null;
      if (raw.includes(':')) return raw;
      const numeric = Number(raw);
      if (Number.isFinite(numeric)) return formatRoundTimerSeconds(numeric);
      return raw;
    };
    const normalizeMultiplierValue = (value) => {
      if (value == null) return null;
      const raw = String(value).trim();
      if (!raw) return null;
      return raw;
    };
    const parseBlockId = (value) => {
      const parsed = commParseNumber(value);
      return Number.isFinite(parsed) ? String(Math.trunc(parsed)) : null;
    };
    const roundSources = [
      responseData,
      responseData?.data,
      data,
      data?.data,
      data?.round,
      data?.currentRound,
      data?.roundState,
      data?.state,
      data?.roundInfo,
      data?.currentRoundInfo
    ].filter(Boolean);
    let apiBlockId = null;
    let apiTimer = null;
    let apiMultiplier = null;
    roundSources.forEach((source) => {
      if (!apiBlockId) {
        apiBlockId = parseBlockId(
          source?.blockId ??
          source?.block_id ??
          source?.roundId ??
          source?.round_id ??
          source?.roundNumber ??
          source?.round_number ??
          source?.blockNumber ??
          source?.block_number ??
          source?.currentBlockId ??
          source?.current_block_id ??
          source?.currentRoundId ??
          source?.current_round_id ??
          null
        );
      }
      if (!apiTimer) {
        const timerCandidate =
          source?.timer ??
          source?.roundTime ??
          source?.round_time ??
          source?.timeLeft ??
          source?.time_left ??
          source?.roundTimeLeft ??
          source?.round_time_left ??
          source?.timerSeconds ??
          source?.timer_seconds ??
          source?.roundTimeSeconds ??
          source?.round_time_seconds ??
          source?.timeLeftSeconds ??
          source?.time_left_seconds ??
          source?.roundTimeLeftSeconds ??
          source?.round_time_left_seconds ??
          null;
        apiTimer = normalizeTimerValue(timerCandidate);
      }
      if (!apiMultiplier) {
        const multCandidate =
          source?.multiplier ??
          source?.multiplierValue ??
          source?.multiplier_value ??
          source?.roundMultiplier ??
          source?.round_multiplier ??
          source?.mult ??
          source?.mult_value ??
          null;
        apiMultiplier = normalizeMultiplierValue(multCandidate);
      }
    });
    if (apiBlockId || apiTimer || apiMultiplier) {
      const nextRoundInfo = {
        blockId: apiBlockId ?? currentRoundInfo?.blockId ?? '-',
        timer: apiTimer ?? currentRoundInfo?.timer ?? '-:-',
        multiplier: apiMultiplier ?? currentRoundInfo?.multiplier ?? '-'
      };
      currentRoundInfo = nextRoundInfo;
      if (!lastCurrentRoundLivePayload) {
        lastCurrentRoundLivePayload = nextRoundInfo;
      }
    }
    const parsedApiTimerSeconds = parseRoundTimerSeconds(
      apiTimer ?? currentRoundInfo?.timer ?? lastCurrentRoundLivePayload?.timer
    );
    if (Number.isFinite(parsedApiTimerSeconds)) {
      liveApiTimerSecondsPrev = liveApiTimerSeconds;
      liveApiTimerSeconds = parsedApiTimerSeconds;
      liveApiTimerUpdatedAt = now;
    }
    const allClans = resolveApiArray(data, [
      'data.allClansState',
      'data.all_clans_state',
      'data.participants',
      'data.clans',
      'data.leaderboard',
      'data.leaderboard.clans',
      'data.array',
      'array'
    ]);

    console.log('[MW API] Extracted clans:', Array.isArray(allClans) ? allClans.length : 0);
    console.log('[MW API] First clan sample:', Array.isArray(allClans) ? allClans[0] : null);
    if (Array.isArray(allClans) && allClans.length > 0) {
      liveApiStateRaw = allClans;
      const rateMap = new Map();
      allClans.forEach((item) => {
        const key = commExtractLiveClanKey(item);
        if (!key) return;
        const basePpsCandidate = commParseNumber(item?.base_pps ?? item?.basePps ?? null);
        if (Number.isFinite(basePpsCandidate) && basePpsCandidate > 0) {
          liveApiBasePpsByClan.set(key, basePpsCandidate);
        }
        const score = commExtractLiveScoreValue(item);
        if (!Number.isFinite(score)) return;
        const debugName = item?.clanName ?? item?.clan_name ?? item?.name ?? item?.clan?.name ?? null;
        const shouldDebug = commShouldDebugClan(debugName);
        const previous = liveApiScoreRateMap.get(key);
        const firstScore = Number.isFinite(previous?.lastScore) ? previous.lastScore : score;
        const firstAt = Number.isFinite(previous?.lastAt) ? previous.lastAt : now;
        const lastScore = score;
        const lastAt = now;
        let ratePerSec = previous?.ratePerSec ?? 0;
        const dtSec = (lastAt - firstAt) / 1000;
        const timerDelta = Number.isFinite(liveApiTimerSeconds) && Number.isFinite(liveApiTimerSecondsPrev)
          ? Math.abs(liveApiTimerSeconds - liveApiTimerSecondsPrev)
          : null;
        const useTimerDelta = Number.isFinite(timerDelta)
          && timerDelta >= LIVE_API_TIMER_DELTA_MIN
          && timerDelta <= LIVE_API_TIMER_DELTA_MAX;
        const dtSecUsed = useTimerDelta ? timerDelta : dtSec;
        if (Number.isFinite(dtSecUsed) && dtSecUsed > 0) {
          ratePerSec = (lastScore - firstScore) / dtSecUsed;
        }
        if (shouldDebug) {
          const rawScore = commParseNumber(item?.score ?? item?.currentScore ?? item?.totalScore ?? null);
          const rawAdded = commParseNumber(item?.currentAddedScore ?? item?.current_added_score ?? item?.addedScore ?? item?.added_score ?? null);
          const rawBoost = commParseNumber(item?.activeBoostScore ?? item?.active_boost_score ?? null);
          const baseFromAdded = (Number.isFinite(rawScore) && Number.isFinite(rawAdded))
            ? rawScore - rawAdded
            : null;
          const baseFromBoost = (Number.isFinite(rawScore) && Number.isFinite(rawBoost))
            ? rawScore - rawBoost
            : null;
          const prevRaw = liveApiDebugRawByClan.get(key);
          const prevRawScore = commParseNumber(prevRaw?.rawScore);
          const prevRawAdded = commParseNumber(prevRaw?.rawAdded);
          const prevRawBoost = commParseNumber(prevRaw?.rawBoost);
          const scoreDelta = Number.isFinite(rawScore) && Number.isFinite(prevRawScore) ? rawScore - prevRawScore : null;
          const addedDelta = Number.isFinite(rawAdded) && Number.isFinite(prevRawAdded) ? rawAdded - prevRawAdded : null;
          const boostDelta = Number.isFinite(rawBoost) && Number.isFinite(prevRawBoost) ? rawBoost - prevRawBoost : null;
          const baseRateFromAddedDelta = Number.isFinite(scoreDelta) && Number.isFinite(addedDelta) && Number.isFinite(dtSecUsed) && dtSecUsed > 0
            ? (scoreDelta - addedDelta) / dtSecUsed
            : null;
          const baseRateFromBoostDelta = Number.isFinite(scoreDelta) && Number.isFinite(boostDelta) && Number.isFinite(dtSecUsed) && dtSecUsed > 0
            ? (scoreDelta - boostDelta) / dtSecUsed
            : null;
          console.log('[MW PPS DEBUG] live api', {
            clan: debugName,
            prevScore: firstScore,
            prevAt: firstAt,
            nowScore: lastScore,
            nowAt: lastAt,
            dtSec,
            timerDelta,
            useTimerDelta,
            dtSecUsed,
            ratePerSec,
            baseFromAdded,
            baseFromBoost,
            scoreDelta,
            addedDelta,
            boostDelta,
            baseRateFromAddedDelta,
            baseRateFromBoostDelta,
            raw: {
              basePoints: item?.basePoints ?? item?.base_points ?? null,
              score: item?.score ?? null,
              currentScore: item?.currentScore ?? null,
              totalScore: item?.totalScore ?? null,
              currentAddedScore: item?.currentAddedScore ?? item?.current_added_score ?? null,
              addedScore: item?.addedScore ?? item?.added_score ?? null,
              activeBoostScore: item?.activeBoostScore ?? item?.active_boost_score ?? null
            }
          });
          liveApiDebugRawByClan.set(key, {
            rawScore,
            rawAdded,
            rawBoost,
            at: now
          });
        }
        const ppsRounded = Number.isFinite(ratePerSec) ? Math.round(ratePerSec * 100) / 100 : null;
        const prevRounded = Number.isFinite(previous?.lastPpsRounded) ? previous.lastPpsRounded : null;
        let stableCount = Number.isFinite(previous?.stableCount) ? previous.stableCount : 0;
        if (Number.isFinite(ppsRounded)) {
          stableCount = ppsRounded === prevRounded ? stableCount + 1 : 1;
        }
        const stablePps = stableCount >= 2 && Number.isFinite(ratePerSec) && ratePerSec > 0
          ? ratePerSec
          : (Number.isFinite(previous?.stablePps) ? previous.stablePps : null);
        if (Number.isFinite(stablePps)) {
          liveApiStablePpsByClan.set(key, stablePps);
        }
        rateMap.set(key, {
          firstScore,
          firstAt,
          lastScore,
          lastAt,
          ratePerSec
          , lastPpsRounded: ppsRounded
          , stableCount
          , stablePps
        });
      });
      liveApiScoreRateMap = rateMap;
      const transformedClans = allClans.map((item) => {
        const resolvedClanId = commResolveClanIdFromItem(item, { silent: true });
        return {
          clanId: resolvedClanId,
          clan_id: resolvedClanId,
          clanName: item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null,
          clan_name: item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null,
          score: item.score ?? item.currentScore ?? item.current_added_score ?? item.currentAddedScore ?? item.basePoints ?? item.base_points ?? null,
          power: item.power ?? item.clanPower ?? item.clan_power ?? item.rosterPower ?? item.roster_power ?? null,
          basePoints: item.basePoints ?? item.base_points ?? null,
          activeBoostScore: item.activeBoostScore ?? item.active_boost_score ?? null,
          currentAddedScore: item.currentAddedScore ?? item.current_added_score ?? null,
          usedAbilities: item.usedAbilities ?? item.used_abilities ?? [],
          chance: item.chance ?? item.chanceToWin ?? item.chance_to_win ?? null,
          position: item.position ?? null
        };
      });

      console.log('[MW API] Transformed clans sample:', transformedClans[0]);
      console.log('[MW API] Calling commIndexLiveApiClans with', transformedClans.length, 'clans');
      liveApiStateTransformed = transformedClans;
      transformedClans.forEach((entry) => {
        const id = commParseNumber(entry?.clan_id ?? entry?.clanId ?? null);
        const name = entry?.clan_name ?? entry?.clanName ?? null;
        if (Number.isFinite(id) && name) {
          clanLookup.set(String(Math.trunc(id)), String(name));
        }
      });
      commIndexLiveApiClans(transformedClans);
      const basePpsBlockId = [
        apiBlockId,
        roundKeyCandidate,
        currentRoundInfo?.blockId,
        lastCurrentRoundLivePayload?.blockId,
        lastLiveSummaryBlockId
      ]
        .map((value) => normalizeBlockId(value))
        .find((value) => value);
      const basePpsCycleId = getCycleNumber(new Date());
      if (basePpsBlockId && Number.isFinite(basePpsCycleId)) {
        const blockIdNum = commParseNumber(basePpsBlockId);
        if (Number.isFinite(blockIdNum)) {
          const rows = [];
          const storedKeys = [];
          let skippedNoPps = 0;
          let skippedNoPower = 0;
          let skippedOutOfRange = 0;
          let skippedNoClanId = 0;
          let skippedStored = 0;
          let skippedFirstValid = 0;
          const displayRows = buildLiveRoundRowsFromApi(allClans, Array.isArray(allClans) ? allClans.length : 0);
          displayRows.forEach((row) => {
            const basePpsValue = commParseNumber(row?.base_pps);
            if (!Number.isFinite(basePpsValue) || basePpsValue <= 0) {
              skippedNoPps += 1;
              return;
            }
            const powerValue = commParseNumber(row?.power);
            if (!Number.isFinite(powerValue) || powerValue <= 0) {
              skippedNoPower += 1;
              return;
            }
            const minPps = powerValue * BASE_PPS_MIN_FACTOR;
            const maxPps = powerValue * BASE_PPS_MAX_FACTOR;
            if (basePpsValue < minPps || basePpsValue > maxPps) {
              skippedOutOfRange += 1;
              return;
            }
            const rawClanId = commParseNumber(row?.clan_id ?? row?.clanId ?? row?.id ?? null);
            const clanId = Number.isFinite(rawClanId)
              ? Math.trunc(rawClanId)
              : commResolveClanIdFromItem(row, { silent: true });
            if (!Number.isFinite(clanId)) {
              skippedNoClanId += 1;
              return;
            }
            const storeKey = `${blockIdNum}:${Math.trunc(clanId)}`;
            if (liveApiStoredBasePps.has(storeKey)) {
              skippedStored += 1;
              return;
            }
            const seenCount = liveApiBasePpsValidCounts.get(storeKey) || 0;
            const nextCount = seenCount + 1;
            liveApiBasePpsValidCounts.set(storeKey, nextCount);
            if (nextCount < 2) {
              skippedFirstValid += 1;
              return;
            }
            const clanName = row?.clan_name ?? row?.clanName ?? row?.name ?? null;
            const ppsRounded = Math.round(basePpsValue * 100) / 100;
            rows.push({
              block_id: Math.trunc(blockIdNum),
              cycle_id: Math.trunc(basePpsCycleId),
              league_id: Math.trunc(leagueId),
              clan_id: Math.trunc(clanId),
              clan_name: clanName || null,
              pps_1m: ppsRounded,
              pps_full: ppsRounded,
              snapshots: 1,
              source: 'api'
            });
            storedKeys.push(storeKey);
          });
          if (rows.length) {
            console.log('[MW] Queuing base PPS rows:', {
              count: rows.length,
              cycleId: basePpsCycleId,
              leagueId,
              blockId: blockIdNum
            });
            commUpsertClanBlockPpsRows(rows)
              .then((inserted) => {
                storedKeys.forEach((key) => liveApiStoredBasePps.set(key, true));
                console.log('[MW] Base PPS rows stored:', inserted);
              })
              .catch((err) => {
                console.warn('[MW] Failed to store base PPS rows:', err);
              });
          } else {
            console.log('[MW] No base PPS rows to store:', {
              cycleId: basePpsCycleId,
              leagueId,
              blockId: blockIdNum,
              totalClans: Array.isArray(displayRows) ? displayRows.length : 0,
              skippedNoPps,
              skippedNoPower,
              skippedOutOfRange,
              skippedNoClanId,
              skippedStored,
              skippedFirstValid
            });
          }
        }
      } else {
        console.log('[MW] Skipping base PPS storage (missing block/cycle):', {
          blockId: basePpsBlockId,
          cycleId: basePpsCycleId,
          currentRoundInfo: currentRoundInfo?.blockId ?? null,
          lastLiveSummaryBlockId,
          lastCurrentRoundLivePayload: lastCurrentRoundLivePayload?.blockId ?? null,
          apiBlockId,
          roundKeyCandidate
        });
      }
      liveApiStateUpdatedAt = now;
      if (typeof window.__refreshCurrentRoundCard === 'function') {
        window.__refreshCurrentRoundCard();
      }
    }
  } catch (err) {
    liveApiStateUpdatedAt = Date.now();
    console.error('[MW API] Error:', err);
  } finally {
    liveApiStatePending = false;
  }
}

function getBoostCategoryKey(type) {
  const key = normalizeBoostKey(type);
  if (key.startsWith('powerup')) return 'powerup';
  if (key.startsWith('clanpowerup') || key.startsWith('clan_power_up')) return 'clanpowerup';
  if (key.startsWith('boost_') || key === 'boost' || key.startsWith('powerboost')) return 'powerboost';
  if (key.startsWith('instant')) return 'instant';
  if (key.startsWith('echo')) return 'echo';
  if (key.startsWith('focus')) return 'focus';
  if (key.startsWith('timewrap') || key.startsWith('time_wrap')) return 'focus';
  return key;
}

function getBoostCountWeight(boostType) {
  if (!boostType) return 1;
  const key = normalizeBoostKey(boostType);
  return BOOST_COUNT_MULTIPLIER[key] || 1;
}

function getPowerBoostFactor(entry) {
  if (!entry) return 1;
  let hasPowerUp = false;
  let hasClanPowerUp = false;

  const applyBoostType = (rawType, count) => {
    const normalized = normalizeBoostKey(rawType);
    if (normalized === 'powerup_100x' || normalized === 'powerup' || normalized === 'power_up') {
      if (Number(count || 0) > 0) hasPowerUp = true;
    }
    if (normalized === 'clanpowerup' || normalized === 'clan_power_up') {
      if (Number(count || 0) > 0) hasClanPowerUp = true;
    }
  };

  const boostMap = entry?.boosts;
  if (boostMap && typeof boostMap === 'object' && !Array.isArray(boostMap)) {
    Object.entries(boostMap).forEach(([type, count]) => applyBoostType(type, count));
  }

  const abilities = entry?.used_abilities ?? entry?.usedAbilities ?? [];
  if (Array.isArray(abilities)) {
    abilities.forEach((ability) => {
      const abilityId = ability?.nftGameAbilityId ?? null;
      const boostType = abilityId ? ABILITY_ID_TO_TYPE[abilityId] : null;
      applyBoostType(boostType, ability?.count);
    });
  }

  let factor = 1;
  if (hasPowerUp) factor *= 100;
  if (hasClanPowerUp) factor *= 2;
  return factor;
}

function normalizeBoostKey(raw) {
  if (!raw) return 'unknown';
  return String(raw).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/_+$/,'').replace(/^_+/, '');
}

function isKnownBoostType(raw) {
  const key = normalizeBoostKey(raw);
  if (!key || key === 'unknown') return false;
  if (BOOST_DISPLAY_NAME[key] || BOOST_ICON_MAP[key] || BOOST_COUNT_MULTIPLIER[key]) return true;
  if (
    key.startsWith('boost_')
    || key.startsWith('focus')
    || key.startsWith('echo')
    || key.startsWith('instant')
    || key.startsWith('powerboost')
    || key.startsWith('timewrap')
  ) {
    return true;
  }
  return false;
}

function formatBoostDisplayName(key) {
  if (!key) return '';
  const normalized = normalizeBoostKey(key);
  if (BOOST_DISPLAY_NAME[normalized]) return BOOST_DISPLAY_NAME[normalized];
  return String(normalized).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}

function parseNumericValue(v) {
  if (v == null) return null;
  if (typeof v === 'number') return v;
  try {
    let s = String(v).trim();
    // strip trailing 'x' or leading 'x'
    s = s.replace(/x$/i, '').replace(/^x/i, '');
    // remove non-numeric extras
    s = s.replace(/[^0-9.+-eE]/g, '');
    const n = Number(s);
    return Number.isFinite(n) ? n : null;
  } catch (err) {
    return null;
  }
}

function commGetCurrentBlockNumber() {
  const raw = lastCurrentRoundLivePayload?.blockId ?? currentRoundInfo?.blockId ?? null;
  const normalized = normalizeBlockId(raw);
  const parsed = commParseNumber(normalized);
  return Number.isFinite(parsed) ? parsed : null;
}

function formatCount(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return '-';
  return new Intl.NumberFormat('en-US').format(n);
}

function formatDecimal(value, decimals = 1) {
  const n = Number(value);
  if (!Number.isFinite(n)) return '-';
  return new Intl.NumberFormat('en-US', {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals
  }).format(n);
}

function commGetLeagueName(leagueId) {
  const id = commParseNumber(leagueId);
  if (!Number.isFinite(id)) return '';
  const label = CLAN_SELECTION_LEAGUE_LABELS[Math.trunc(id)];
  if (label) return label;
  if (id >= CLAN_SELECTION_DUNE_START && id <= CLAN_SELECTION_DUNE_END) {
    return `Dune ${Math.trunc(id - CLAN_SELECTION_DUNE_START + 1)}`;
  }
  return `League ${Math.trunc(id)}`;
}

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

function commParseLeagueLabel(value) {
  if (!value) return null;
  const text = String(value).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 raw = duneMatch[1];
    const duneNum = /^\d+$/.test(raw) ? Number(raw) : commRomanToNumber(raw);
    if (Number.isFinite(duneNum) && duneNum > 0) {
      return CLAN_SELECTION_DUNE_START + (Math.trunc(duneNum) - 1);
    }
  }
  return null;
}

function commFormatLeagueDisplay(leagueId) {
  const id = commParseNumber(leagueId);
  if (!Number.isFinite(id)) return '-';
  return commGetLeagueName(id);
}

function getStatsMultiplierFilter() {
  const select = document.getElementById('statsMultiplierFilter');
  const raw =
    (select && select.value) ||
    localStorage.getItem(STATS_MULTIPLIER_FILTER_KEY) ||
    'all';
  if (!raw || raw === 'all') return null;
  const parsed = parseNumericValue(raw);
  return Number.isFinite(parsed) ? parsed : null;
}

function getStatsCycleFilter() {
  const input = document.getElementById('statsCycleFilterInput');
  const allToggle = document.getElementById('statsCycleAllToggle');
  const storedAll = localStorage.getItem(STATS_CYCLE_ALL_KEY) === '1';
  const isAll = allToggle ? allToggle.checked : storedAll;
  if (isAll) return null;
  const raw =
    (input && input.value) ||
    localStorage.getItem(STATS_CYCLE_FILTER_KEY) ||
    '';
  const parsed = parseNumericValue(raw);
  if (Number.isFinite(parsed)) return parsed;
  return getCycleNumber(new Date());
}

function getStatsLeagueFilter() {
  const select = document.getElementById('statsLeagueFilterInput');
  const raw =
    (select && select.value) ||
    localStorage.getItem(STATS_LEAGUE_FILTER_KEY) ||
    '';
  const value = (raw || '').toString().trim();
  if (!value || value === 'all') return null;
  if (value === 'dune-all') return 'dune-all';
  const parsed = parseNumericValue(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function getProfitCycleFilter() {
  const input = document.getElementById('profitCycleFilterInput');
  const allToggle = document.getElementById('profitCycleAllToggle');
  const storedAll = localStorage.getItem(PROFIT_CYCLE_ALL_KEY) === '1';
  const isAll = allToggle ? allToggle.checked : storedAll;
  if (isAll) return null;
  const raw =
    (input && input.value) ||
    localStorage.getItem(PROFIT_CYCLE_FILTER_KEY) ||
    '';
  const parsed = parseNumericValue(raw);
  if (Number.isFinite(parsed)) return parsed;
  return getCycleNumber(new Date());
}

function getProfitLeagueFilter() {
  const select = document.getElementById('profitLeagueFilterInput');
  const raw =
    (select && select.value) ||
    localStorage.getItem(PROFIT_LEAGUE_FILTER_KEY) ||
    '';
  const value = (raw || '').toString().trim();
  if (!value || value === 'all') return null;
  return value;
}

function getRoundHistoryCycleFilter() {
  const input = document.getElementById('statsRoundHistoryCycleInput');
  const raw =
    (input && input.value) ||
    localStorage.getItem(STATS_ROUND_HISTORY_CYCLE_FILTER_KEY) ||
    '';
  const parsed = parseNumericValue(raw);
  return Number.isFinite(parsed) ? parsed : null;
}

function getRoundHistoryLeagueFilter() {
  const input = document.getElementById('statsRoundHistoryLeagueInput');
  const raw =
    (input && input.value) ||
    localStorage.getItem(STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY) ||
    '';
  const value = (raw || '').toString().trim();
  if (!value || value === 'all') return null;
  if (value === 'dune-all') return 'dune-all';
  const parsed = parseNumericValue(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function summarizeNoBoostStats(rows, field) {
  let wins = 0;
  let coeff = 0;
  rows.forEach((row) => {
    const chance = commExtractChanceValue(row?.[field]);
    if (!Number.isFinite(chance)) return;
    const used = commExtractUsedAbilitiesFromWinner(row?.[field]);
    const hasBoost = Array.isArray(used) && used.some((ability) => Number(ability?.count || 0) > 0);
    if (hasBoost) return;
    wins += 1;
    coeff += chance;
  });
  return { wins, avg: wins ? coeff / wins : null };
}

function summarizeUsedAbilities(usedAbilities) {
  if (!Array.isArray(usedAbilities)) return { total: 0, items: [] };
  let total = 0;
  const items = usedAbilities.map((ability) => {
    const count = Number(ability?.count || 0);
    total += Number.isFinite(count) ? count : 0;
    return {
      nftGameAbilityId: ability?.nftGameAbilityId ?? null,
      count: Number.isFinite(count) ? count : 0
    };
  });
  return { total, items };
}

function parseMaybeJson(value) {
  if (value === null || value === undefined) return null;
  if (typeof value === 'object') return value;
  const text = String(value).trim();
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch (err) {
    return null;
  }
}

function commExtractUserWinnerFromRow(row) {
  const direct = parseMaybeJson(row?.user_winner ?? row?.userWinner);
  const raw = parseMaybeJson(row?.raw);
  const fallback = raw?.userWinner ?? raw?.user_winner ?? null;
  const payload = direct || fallback || null;
  if (!payload) return null;
  const user = payload.user ?? payload.userInfo ?? payload.userData ?? null;
  const clan =
    payload.clan ??
    user?.clan ??
    payload.clanInfo ??
    payload.clanData ??
    null;
  return {
    alias: user?.alias ?? user?.name ?? payload?.alias ?? payload?.name ?? null,
    id: user?.id ?? payload?.userId ?? payload?.user_id ?? payload?.id ?? null,
    score: payload?.score ?? user?.score ?? null,
    chance: payload?.chanceToWin ?? payload?.chance_to_win ?? null,
    reward: payload?.gmtReward ?? payload?.reward ?? payload?.btcReward ?? null,
    clanId: clan?.id ?? payload?.clanId ?? payload?.clan_id ?? null,
    clanName: clan?.name ?? payload?.clanName ?? payload?.clan_name ?? null
  };
}

function commExtractClanWinnerFromRow(row) {
  const direct = parseMaybeJson(row?.clan_winner ?? row?.clanWinner);
  const raw = parseMaybeJson(row?.raw);
  const fallback = raw?.clanWinner ?? raw?.clan_winner ?? null;
  const payload = direct || fallback || null;
  if (!payload) return null;
  const clan = payload.clan ?? payload.clanInfo ?? payload.clanData ?? null;
  return {
    name: clan?.name ?? payload?.name ?? null,
    id: clan?.id ?? payload?.clanId ?? payload?.clan_id ?? payload?.id ?? null,
    score: payload?.score ?? clan?.score ?? null,
    chance: payload?.chanceToWin ?? payload?.chance_to_win ?? null,
    reward: payload?.gmtReward ?? payload?.reward ?? payload?.btcReward ?? null
  };
}

function commMatchesUsername(alias, username) {
  if (!alias || !username) return false;
  const left = String(alias).trim().toLowerCase();
  const right = String(username).trim().toLowerCase();
  if (!left || !right) return false;
  return left === right || left.includes(right);
}

function isDataComplete(value) {
  return value === true || value === 'true' || value === 1 || value === '1';
}

function formatDateTimeValue(value) {
  if (!value) return '-';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  return date.toLocaleString();
}

function formatHashShort(value) {
  if (!value) return { text: '-', title: '' };
  const text = String(value);
  if (text.length <= 18) return { text, title: text };
  return { text: `${text.slice(0, 10)}...${text.slice(-6)}`, title: text };
}

const STATS_WIN_BUCKETS = [
  { label: '1 to 2%', min: 1, max: 2 },
  { label: '2 to 3%', min: 2, max: 3 },
  { label: '3 to 4%', min: 3, max: 4 },
  { label: '4 to 5%', min: 4, max: 5 },
  { label: '5 to 6%', min: 5, max: 6 },
  { label: '7 to 8%', min: 7, max: 8 },
  { label: '8 to 9%', min: 8, max: 9 },
  { label: '10 to 11%', min: 10, max: 11 },
  { label: '11 to 12%', min: 11, max: 12 },
  { label: '12 to 13%', min: 12, max: 13 },
  { label: '13 to 14%', min: 13, max: 14 },
  { label: '14 to 15%', min: 14, max: 15 },
  { label: '15 to 16%', min: 15, max: 16 },
  { label: '16 to 17%', min: 16, max: 17 },
  { label: '17 to 18%', min: 17, max: 18 },
  { label: '18 to 19%', min: 18, max: 19 },
  { label: '19 to 20%', min: 19, max: 20 },
  { label: '20%+', min: 20, max: Infinity }
];

const STATS_MULTIPLIER_FILTER_KEY = 'mw_stats_multiplier_filter';
const STATS_CYCLE_FILTER_KEY = 'mw_stats_cycle_filter';
const STATS_CYCLE_ALL_KEY = 'mw_stats_cycle_all';
const STATS_LEAGUE_FILTER_KEY = 'mw_stats_league_filter';
const STATS_ROUND_HISTORY_CYCLE_FILTER_KEY = 'mw_round_history_cycle_filter';
const STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY = 'mw_round_history_league_filter';
const TELEGRAM_BOT_TOKEN_KEY = 'mw_telegram_bot_token';
const TELEGRAM_CHAT_ID_KEY = 'mw_telegram_chat_id';
const TELEGRAM_ENABLED_KEY = 'mw_telegram_enabled';
const TELEGRAM_ALLOWED_MULTS_KEY = 'mw_telegram_allowed_mults';
const TELEGRAM_LAST_SENT_KEY = 'mw_telegram_last_sent';
const TELEGRAM_WINNER_ENABLED_KEY = 'mw_telegram_winner_enabled';
const TELEGRAM_NOTIFY_MULTIPLIER_ENABLED_KEY = 'mw_telegram_notify_multiplier_enabled';
const TELEGRAM_NOTIFY_LOW_BOOST_ENABLED_KEY = 'mw_telegram_notify_low_boost_enabled';
const TELEGRAM_NOTIFY_LOW_BOOST_MAX_KEY = 'mw_telegram_notify_low_boost_max';
const TELEGRAM_NOTIFY_CLAN_POWER_ENABLED_KEY = 'mw_telegram_notify_clan_power_enabled';
const DISCORD_PUSH_TOKEN_KEY = 'mw_discord_push_token';
const DISCORD_ENABLED_KEY = 'mw_discord_enabled';
const DISCORD_ALLOWED_MULTS_KEY = 'mw_discord_allowed_mults';
const DISCORD_LAST_SENT_KEY = 'mw_discord_last_sent';
const DISCORD_WINNER_ENABLED_KEY = 'mw_discord_winner_enabled';
const DISCORD_NOTIFY_MULTIPLIER_ENABLED_KEY = 'mw_discord_notify_multiplier_enabled';
const DISCORD_NOTIFY_LOW_BOOST_ENABLED_KEY = 'mw_discord_notify_low_boost_enabled';
const DISCORD_NOTIFY_LOW_BOOST_MAX_KEY = 'mw_discord_notify_low_boost_max';
const DISCORD_NOTIFY_CLAN_POWER_ENABLED_KEY = 'mw_discord_notify_clan_power_enabled';
const DISCORD_BOT_INVITE_URL = 'https://discord.com/oauth2/authorize?client_id=1468622927783264421&scope=bot%20applications.commands';
const DISCORD_PUSH_API_URL = 'https://www.gominertools.xyz/push';
const DISCORD_PUSH_TIMEOUT_MS = 25000;
const DISCORD_PUSH_MAX_ATTEMPTS = 2;
const DISCORD_PUSH_RETRY_DELAY_MS = 1200;

const COMM_SETTINGS_SYNC_INTERVAL_MS = 5 * 60 * 1000;
const COMM_SETTINGS_SYNC_DEBOUNCE_MS = 2000;
const COMM_SETTINGS_SYNC_KEYS = [
  ORDER_STORAGE_KEY,
  'mw_calc_inputs',
  COMM_ALERT_MUTE_KEY,
  COMM_HIGH_ALERT_KEY,
  COMM_API_DEFAULT_LIMIT_KEY,
  COMM_API_MIN_INTERVAL_KEY,
  COMM_API_BULK_AMOUNT_KEY,
  COMM_API_CYCLE_OVERRIDE_KEY,
  COMM_API_LEAGUE_OVERRIDE_KEY,
  COMM_API_ROSTER_BATCH_KEY,
  COMM_API_ROSTER_MEMBER_LIMIT_KEY,
  COMM_API_ROSTER_VERBOSE_KEY,
  COMM_API_ROSTER_PROGRESS_KEY,
  COMM_API_ROSTER_AUTO_DONE_KEY,
  COMM_API_ROSTER_AUTO_LAST_KEY,
  STATS_MULTIPLIER_FILTER_KEY,
  STATS_CYCLE_FILTER_KEY,
  STATS_CYCLE_ALL_KEY,
  STATS_LEAGUE_FILTER_KEY,
  PROFIT_CYCLE_FILTER_KEY,
  PROFIT_CYCLE_ALL_KEY,
  PROFIT_LEAGUE_FILTER_KEY,
  STATS_ROUND_HISTORY_CYCLE_FILTER_KEY,
  STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY,
  CLAN_SELECTION_FILTERS_KEY,
  CLAN_SELECTION_EXTRA_FILTERS_KEY,
  PERSONAL_ALIAS_KEY,
  PERSONAL_USER_ID_KEY,
  COMM_LICENSE_SENDER_KEY,
  BOOST_HELPER_SCORE_SAMPLE_KEY_V1
];

const BOOST_COST_GMT_ESTIMATE = {
  powerup: 8.66,
  clanpowerup: 7.27,
  powerboost: 1,
  instant: 1,
  echo: 1,
  focus: 1
};

function commExtractChanceValue(value) {
  const parsed = parseMaybeJson(value);
  if (!parsed || typeof parsed !== 'object') return null;
  const raw = parsed.chanceToWin ?? parsed.chance_to_win ?? parsed.chanceToMine ?? parsed.chance_to_mine ?? parsed.percentage ?? parsed.pct ?? null;
  return commParseNumber(raw);
}

function commExtractUsedAbilitiesFromWinner(value) {
  const parsed = parseMaybeJson(value);
  if (!parsed || typeof parsed !== 'object') return [];
  const candidates = [
    parsed?.clan?.usedAbilities,
    parsed?.clanData?.usedAbilities,
    parsed?.clanWinner?.usedAbilities,
    parsed?.user?.usedAbilities,
    parsed?.userData?.usedAbilities,
    parsed?.userWinner?.usedAbilities,
    parsed?.usedAbilities,
    parsed?.used_abilities
  ];
  const selected = candidates.find((list) => Array.isArray(list) && list.length);
  return Array.isArray(selected) ? selected : [];
}

function getBoostCostEstimate(type) {
  const normalized = normalizeBoostKey(type);
  if (normalized.startsWith('powerup')) return BOOST_COST_GMT_ESTIMATE.powerup;
  if (normalized.startsWith('clanpowerup') || normalized.startsWith('clan_power_up')) {
    return BOOST_COST_GMT_ESTIMATE.clanpowerup;
  }
  if (normalized.includes('100x')) return 100;
  if (normalized.includes('10x')) return 10;
  if (normalized.includes('1x')) return 1;
  const category = getBoostCategoryKey(normalized);
  return BOOST_COST_GMT_ESTIMATE[category] || 1;
}

function estimateBoostCost(usedAbilities) {
  if (!Array.isArray(usedAbilities)) return 0;
  return usedAbilities.reduce((sum, ability) => {
    const id = ability?.nftGameAbilityId ?? ability?.nft_game_ability_id ?? ability?.abilityId ?? null;
    const count = Number(ability?.count || 0);
    if (!id || !Number.isFinite(count) || count <= 0) return sum;
    const boostType = ABILITY_ID_TO_TYPE[id] || id;
    const cost = getBoostCostEstimate(boostType);
    return sum + cost * count;
  }, 0);
}

function buildChanceWinBuckets(rows, field) {
  const buckets = STATS_WIN_BUCKETS.map((bucket) => ({
    ...bucket,
    wins: 0,
    coeff: 0,
    cost: 0
  }));
  rows.forEach((row) => {
    const chance = commExtractChanceValue(row?.[field]);
    if (!Number.isFinite(chance)) return;
    const bucket = buckets.find((entry) => {
      if (entry.max === Infinity) return chance >= entry.min;
      return chance >= entry.min && chance < entry.max;
    });
    if (!bucket) return;
    bucket.wins += 1;
    bucket.coeff += chance;
    const usedAbilities = commExtractUsedAbilitiesFromWinner(row?.[field]);
    bucket.cost += estimateBoostCost(usedAbilities);
  });
  return buckets.map((bucket) => ({
    label: bucket.label,
    min: bucket.min,
    max: bucket.max,
    wins: bucket.wins,
    coeff: Math.round(bucket.coeff),
    cost: bucket.cost,
    effectiveness: bucket.cost > 0 ? Math.round(bucket.coeff) / bucket.cost : null
  }));
}

function buildChanceWinTable(rows, firstColumnLabel = 'User percentage', scope = 'user') {
  const body = rows
    .map((row) => (
      `<tr>
        <td>
          <button type="button" class="stats-percent-pill" data-scope="${commEscape(scope)}" data-min="${commEscape(row.min)}" data-max="${commEscape(row.max)}">
            ${commEscape(row.label)}
          </button>
        </td>
        <td class="stats-num">${commEscape(formatCount(row.wins))}</td>
        <td class="stats-num">${commEscape(formatCount(row.coeff))}</td>
        <td class="stats-num">${commEscape(formatDecimal(row.cost, 1))}</td>
        <td class="stats-num">${commEscape(formatDecimal(row.effectiveness, 2))}</td>
      </tr>`
    ))
    .join('');
  return `
    <table class="stats-win-table">
      <thead>
        <tr>
          <th>${commEscape(firstColumnLabel)}</th>
          <th>How much wins</th>
          <th>Coeff (wins*%)</th>
          <th>Boost cost (est.)</th>
          <th>Effectiveness</th>
        </tr>
      </thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

function normalizeTimestampMs(value) {
  const num = Number(value);
  if (!Number.isFinite(num)) return null;
  return num < 1e12 ? num * 1000 : num;
}

function normalizeTimestampList(values) {
  const list = Array.isArray(values) ? values : (values != null ? [values] : []);
  const normalized = list
    .map((value) => normalizeTimestampMs(value))
    .filter((value) => Number.isFinite(value));
  if (!normalized.length) return [];
  const unique = Array.from(new Set(normalized));
  unique.sort((a, b) => a - b);
  return unique;
}

function adjustBoostTimestampMs(ts, roundStartMs) {
  if (!Number.isFinite(ts)) return null;
  if (!Number.isFinite(roundStartMs)) return ts;
  // Treat very small timestamps as elapsed-ms offsets from round start.
  if (ts < 1e11) return roundStartMs + ts;
  return ts;
}

function extractAbilityTimestampsMs(ability) {
  const list = Array.isArray(ability?.usedAtTimestamps) ? ability.usedAtTimestamps : [];
  const fallback = [
    ability?.usedAtTimestamp,
    ability?.used_at,
    ability?.usedAt,
    ability?.timestamp
  ];
  const combined = list.length ? list : fallback;
  return normalizeTimestampList(combined);
}

function extractAbilityTimestampMs(ability) {
  const timestamps = extractAbilityTimestampsMs(ability);
  if (!timestamps.length) return null;
  return timestamps[timestamps.length - 1];
}

function hasAbilityTimestamps(abilities) {
  if (!Array.isArray(abilities)) return false;
  return abilities.some((ability) => extractAbilityTimestampsMs(ability).length > 0);
}

function buildBoostMetaFromAbilities(abilities, source) {
  const meta = {};
  if (!Array.isArray(abilities)) return meta;
  abilities.forEach((ability) => {
    const id = ability?.nftGameAbilityId ?? ability?.nft_game_ability_id ?? null;
    const count = Number(ability?.count || 0);
    if (!id || !Number.isFinite(count) || count <= 0) return;
    const type = normalizeBoostKey(ABILITY_ID_TO_TYPE[id] || `unknown_${id}`);
    const timestamps = extractAbilityTimestampsMs(ability);
    const ts = timestamps.length ? timestamps[timestamps.length - 1] : null;
    meta[type] = { source, ts, timestamps };
  });
  return meta;
}

function buildBoostMetaFromMap(boostMap, source) {
  const meta = {};
  const entries = boostMap instanceof Map ? Array.from(boostMap.entries()) : Object.entries(boostMap || {});
  entries.forEach(([type, rawCount]) => {
    const count = Number(rawCount || 0);
    if (!Number.isFinite(count) || count <= 0) return;
    const key = normalizeBoostKey(type);
    meta[key] = { source, ts: null, timestamps: [] };
  });
  return meta;
}

function extractBoostEventTimestampMs(boostData) {
  const raw = boostData?.data?.usedAtTimestamp
    ?? boostData?.data?.usedAt
    ?? (Array.isArray(boostData?.data?.usedAtTimestamps) ? Math.max(...boostData.data.usedAtTimestamps) : null)
    ?? boostData?.usedAtTimestamp
    ?? boostData?.usedAt
    ?? boostData?.timestamp
    ?? boostData?.createdAt
    ?? boostData?.data?.timestamp
    ?? null;
  const num = Number(raw);
  if (!Number.isFinite(num)) return Date.now();
  return num < 1e12 ? num * 1000 : num;
}

function formatBoostOriginSource(source) {
  const map = {
    'ws:abilityUsage': 'Websocket (ability event)',
    'ws:allUsersStateV2': 'Websocket (allUsersStateV2)',
    'api:summary': 'API live summary',
    'local:pending': 'Local pending boost'
  };
  return map[source] || source || 'Unknown source';
}

function formatBoostOriginTime(ts) {
  const num = Number(ts);
  if (!Number.isFinite(num)) return 'Unknown time';
  const dt = new Date(num);
  if (Number.isNaN(dt.getTime())) return 'Unknown time';
  return dt.toLocaleString([], { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
}

function buildBoostPillsHtml(usedAbilities, options = {}) {
  const abilityList = Array.isArray(usedAbilities) ? usedAbilities : null;
  const boostMap = (!abilityList && usedAbilities && typeof usedAbilities === 'object')
    ? usedAbilities
    : null;
  if ((!abilityList || abilityList.length === 0) && (!boostMap || (boostMap instanceof Map ? boostMap.size === 0 : Object.keys(boostMap).length === 0))) {
    return '<span class="muted">No boosts</span>';
  }
  const counts = {};
  const titles = {};
  if (abilityList && abilityList.length) {
    abilityList.forEach((ability) => {
      const id = ability?.nftGameAbilityId ?? ability?.nft_game_ability_id ?? null;
      const count = Number(ability?.count || 0);
      if (!id || !Number.isFinite(count) || count <= 0) return;
      const type = ABILITY_ID_TO_TYPE[id] || `unknown_${id}`;
      counts[type] = (counts[type] || 0) + count;
      titles[type] = id;
    });
  }
  if (boostMap && Object.keys(counts).length === 0) {
    const entries = boostMap instanceof Map ? Array.from(boostMap.entries()) : Object.entries(boostMap);
    entries.forEach(([type, rawCount]) => {
      const count = Number(rawCount || 0);
      if (!Number.isFinite(count) || count <= 0) return;
      counts[type] = (counts[type] || 0) + count;
    });
  }
  const categoryCounts = {};
  const metaByType = {};
  if (options?.meta) {
    if (options.meta instanceof Map) {
      options.meta.forEach((val, key) => {
        metaByType[normalizeBoostKey(key)] = val;
      });
    } else if (typeof options.meta === 'object') {
      Object.entries(options.meta).forEach(([key, val]) => {
        metaByType[normalizeBoostKey(key)] = val;
      });
    }
  }
  const categoryMeta = {};
  Object.entries(counts).forEach(([type, rawCount]) => {
    const count = Number(rawCount || 0);
    if (!Number.isFinite(count) || count <= 0) return;
    const category = getBoostCategoryKey(type);
    categoryCounts[category] = (categoryCounts[category] || 0) + count;
    const meta = metaByType[normalizeBoostKey(type)] || metaByType[normalizeBoostKey(category)];
    if (meta) {
      const existing = categoryMeta[category];
      const metaTs = normalizeTimestampMs(meta?.ts);
      const existingTs = normalizeTimestampMs(existing?.ts);
      const mergedTimestamps = normalizeTimestampList([
        ...(existing?.timestamps || []),
        ...(meta?.timestamps || []),
        ...(Number.isFinite(metaTs) ? [metaTs] : [])
      ]);
      const mergedMax = mergedTimestamps.length ? mergedTimestamps[mergedTimestamps.length - 1] : null;
      const effectiveMetaTs = Number.isFinite(metaTs) ? metaTs : mergedMax;
      const useMetaSource = !existing || (Number.isFinite(effectiveMetaTs) && Number.isFinite(existingTs) ? effectiveMetaTs >= existingTs : Number.isFinite(effectiveMetaTs));
      categoryMeta[category] = {
        source: useMetaSource ? meta?.source : (existing?.source || meta?.source),
        ts: useMetaSource ? effectiveMetaTs : (existingTs || effectiveMetaTs),
        timestamps: mergedTimestamps,
        debug: meta?.debug || existing?.debug || null
      };
    }
  });
  const entries = Object.entries(categoryCounts);
  if (!entries.length) {
    return '<span class="muted">No boosts</span>';
  }
  const orderIndex = (type) => {
    const idx = BOOST_DISPLAY_ORDER.indexOf(type);
    return idx === -1 ? 999 : idx;
  };
  entries.sort((a, b) => {
    const order = orderIndex(a[0]) - orderIndex(b[0]);
    if (order !== 0) return order;
    return a[0].localeCompare(b[0]);
  });
  return entries
    .map(([type, count]) => {
      const normalized = normalizeBoostKey(type);
      const icon = BOOST_ICON_MAP[normalized] || '?';
      const label = formatBoostDisplayName(normalized) || 'Unknown boost';
      const meta = categoryMeta[normalized] || null;
      const dataAttrs = [
        `data-boost-type="${commEscape(normalized)}"`,
        `data-boost-count="${commEscape(String(count))}"`,
        options?.origin ? `data-boost-origin="${commEscape(options.origin)}"` : '',
        options?.clanName ? `data-clan-name="${commEscape(options.clanName)}"` : '',
        meta?.source ? `data-boost-source="${commEscape(meta.source)}"` : '',
        Number.isFinite(Number(meta?.ts)) ? `data-boost-ts="${commEscape(String(Math.trunc(Number(meta.ts))))}"` : '',
        Array.isArray(meta?.timestamps) && meta.timestamps.length
          ? `data-boost-ts-list="${commEscape(JSON.stringify(meta.timestamps.map((ts) => Math.trunc(Number(ts)) || 0).filter((ts) => Number.isFinite(ts) && ts > 0)))}"`
          : '',
        meta?.debug ? `data-boost-debug="${commEscape(JSON.stringify(meta.debug))}"` : ''
      ].filter(Boolean).join(' ');
      return (
        `<span class="boost-pill compact" title="${commEscape(label)}" ${dataAttrs}>` +
        `<span class="pill-icon">${icon}</span>` +
        `<span class="boost-value">${formatCount(count)}</span>` +
        `</span>`
      );
    })
    .join('');
}

function extractRoundWinnerClan(row) {
  const raw = parseMaybeJson(row?.clan_winner);
  const clan = raw?.clan ?? raw?.clanData ?? raw?.clan_info ?? raw?.clanWinner ?? raw;
  const clanObj = clan && typeof clan === 'object' ? clan : null;
  const usedAbilities = Array.isArray(clanObj?.usedAbilities)
    ? clanObj.usedAbilities
    : (Array.isArray(raw?.usedAbilities) ? raw.usedAbilities : []);
  return {
    raw,
    clan: clanObj,
    name: clanObj?.name ?? raw?.clanName ?? raw?.name ?? null,
    id: clanObj?.id ?? row?.winner_clan_id ?? null,
    score: clanObj?.score ?? raw?.score ?? null,
    chance: raw?.chanceToWin ?? raw?.chance_to_win ?? null,
    usedAbilities
  };
}

function extractRoundWinnerUser(row) {
  const raw = parseMaybeJson(row?.user_winner);
  const user = raw?.user ?? raw?.userInfo ?? raw?.player ?? raw;
  const userObj = user && typeof user === 'object' ? user : null;
  const usedAbilities = Array.isArray(userObj?.usedAbilities)
    ? userObj.usedAbilities
    : (Array.isArray(raw?.usedAbilities) ? raw.usedAbilities : []);
  return {
    raw,
    user: userObj,
    name: userObj?.alias ?? userObj?.name ?? userObj?.username ?? raw?.alias ?? raw?.name ?? null,
    id: userObj?.id ?? row?.winner_user_id ?? null,
    chance: raw?.chanceToWin ?? raw?.chance_to_win ?? null,
    usedAbilities
  };
}

function commExtractGmtReward(winnerRaw, winnerObj) {
  const candidates = [
    winnerRaw?.gmtReward,
    winnerRaw?.gmt_reward,
    winnerRaw?.rewardGmt,
    winnerRaw?.reward_gmt,
    winnerRaw?.reward,
    winnerObj?.gmtReward,
    winnerObj?.gmt_reward,
    winnerObj?.rewardGmt,
    winnerObj?.reward_gmt,
    winnerObj?.reward
  ];
  for (const value of candidates) {
    const parsed = commParseNumber(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return null;
}

function renderRoundHistoryField(label, value, { kind = 'text', title = '' } = {}) {
  let display = value;
  if (kind === 'date') {
    display = formatDateTimeValue(value);
  } else if (kind === 'bool') {
    display = value == null ? '-' : (value ? 'yes' : 'no');
  } else if (kind === 'number') {
    display = value == null ? '-' : formatCount(value);
  } else if (kind === 'json') {
    display = value == null ? '-' : commCompactJson(value, 120);
  } else if (value == null || value === '') {
    display = '-';
  }
  const safeTitle = title ? ` title="${commEscape(title)}"` : '';
  return (
    `<div class="round-history-field">` +
    `<span class="round-history-label">${commEscape(label)}</span>` +
    `<span class="round-history-value"${safeTitle}>${commEscape(display)}</span>` +
    `</div>`
  );
}

function buildRoundHistoryCard(row) {
  const hashMeta = formatHashShort(row?.hash);
  const clanWinner = extractRoundWinnerClan(row);
  const userWinner = extractRoundWinnerUser(row);
  const blockNumber = row?.block_number ?? '-';
  const multiplier = row?.multiplier ?? '-';
  const cycleId = row?.cycle_id ?? '-';
  const leagueLabel = commFormatLeagueDisplay(row?.league_id);

  const fields = [
    renderRoundHistoryField('API round ID', row?.api_round_id, { kind: 'number' }),
    renderRoundHistoryField('Block number', row?.block_number, { kind: 'number' }),
    renderRoundHistoryField('Cycle', row?.cycle_id, { kind: 'number' }),
    renderRoundHistoryField('Multiplier', row?.multiplier, { kind: 'number' }),
    renderRoundHistoryField('League', leagueLabel),
    renderRoundHistoryField('NFT rate', row?.nft_rate, { kind: 'number' }),
    renderRoundHistoryField('Power', row?.power, { kind: 'number' }),
    renderRoundHistoryField('Hash', hashMeta.text, { title: hashMeta.title }),
    renderRoundHistoryField('Winner clan ID', row?.winner_clan_id, { kind: 'number' }),
    renderRoundHistoryField('Winner user ID', row?.winner_user_id, { kind: 'number' }),
    renderRoundHistoryField('Created', row?.created_at, { kind: 'date' }),
    renderRoundHistoryField('Updated', row?.updated_at, { kind: 'date' }),
    renderRoundHistoryField('Started', row?.started_at, { kind: 'date' }),
    renderRoundHistoryField('Ended', row?.ended_at, { kind: 'date' }),
    renderRoundHistoryField('Fetched', row?.fetched_at, { kind: 'date' })
  ];

  const clanTitle = clanWinner.name || 'Unknown clan';
  const clanIdLabel = clanWinner.id != null ? `#${clanWinner.id}` : '-';
  const clanBoosts = buildBoostPillsHtml(clanWinner.usedAbilities);
  const userTitle = userWinner.name || 'Unknown user';
  const userIdLabel = userWinner.id != null ? `#${userWinner.id}` : '-';
  const userBoosts = buildBoostPillsHtml(userWinner.usedAbilities);

  const rawBlock = commCompactJson(row?.raw, 600);
  const rawClanWinner = commCompactJson(clanWinner.raw, 600);
  const rawUserWinner = commCompactJson(userWinner.raw, 600);

  return `
    <div class="round-history-card">
      <div class="round-history-header">
        <div class="round-history-title">Block ${commEscape(blockNumber)}</div>
        <div class="round-history-pills">
          <span class="boost-pill compact"><span class="boost-value">x${commEscape(multiplier)}</span></span>
          <span class="boost-pill compact"><span class="boost-value">Cycle ${commEscape(cycleId)}</span></span>
          <span class="boost-pill compact"><span class="boost-value">${commEscape(leagueLabel)}</span></span>
        </div>
      </div>
      <div class="round-history-grid">
        ${fields.join('')}
      </div>
      <div class="round-history-winner">
        <div class="round-history-winner-title">Winning clan</div>
        <div class="round-history-winner-line">
          <span class="boost-pill compact"><span class="boost-value">${commEscape(clanTitle)}</span></span>
          <span class="boost-pill compact"><span class="boost-value">${commEscape(clanIdLabel)}</span></span>
        </div>
        <div class="round-history-boosts">${clanBoosts}</div>
      </div>
      <div class="round-history-winner">
        <div class="round-history-winner-title">Winning user</div>
        <div class="round-history-winner-line">
          <span class="boost-pill compact"><span class="boost-value">${commEscape(userTitle)}</span></span>
          <span class="boost-pill compact"><span class="boost-value">${commEscape(userIdLabel)}</span></span>
        </div>
        <div class="round-history-boosts">${userBoosts}</div>
      </div>
      <details class="round-history-details">
        <summary>Raw data</summary>
        <pre>${commEscape(rawBlock)}</pre>
        <div class="round-history-raw-sep"></div>
        <pre>${commEscape(rawClanWinner)}</pre>
        <div class="round-history-raw-sep"></div>
        <pre>${commEscape(rawUserWinner)}</pre>
      </details>
    </div>
  `;
}

function buildRoundHistoryTable(rows, finishedLookup = new Map()) {
  const header = `
    <tr>
      <th class="round-history-col-block">Block / X<br>/ Power</th>
      <th class="round-history-col-clan">Winning clan</th>
      <th class="round-history-col-user">Winning user</th>
    </tr>
  `;
  const body = rows
    .map((row) => {
      const clanWinner = extractRoundWinnerClan(row);
      const userWinner = extractRoundWinnerUser(row);
      const blockNumber = row?.block_number ?? '-';
      const blockKey = blockNumber != null ? String(blockNumber) : '';
      const hasFinished = blockKey && finishedLookup && finishedLookup.has(blockKey);
      const multNum = parseNumericValue(row?.multiplier);
      const multiplier = multNum != null ? `${multNum}x` : (row?.multiplier != null ? String(row.multiplier) : '-');
      const powerDisplay = row?.power != null ? formatCount(row.power) : '-';
      const clanName = clanWinner.name || 'Unknown';
      const userName = userWinner.name || 'Unknown';
      const clanBoosts = buildBoostPillsHtml(clanWinner.usedAbilities);
      const userBoosts = buildBoostPillsHtml(userWinner.usedAbilities);
      return `
        <tr>
          <td class="round-history-col-block">
            ${hasFinished
              ? `<button type="button" class="round-history-cell-title round-history-block-pill" data-block-id="${blockKey}">${commEscape(blockNumber)}</button>`
              : `<div class="round-history-cell-title">${commEscape(blockNumber)}</div>`}
            <div class="round-history-cell-sub">${commEscape(multiplier)}</div>
            <div class="round-history-cell-power">${commEscape(powerDisplay)}</div>
          </td>
          <td class="round-history-win-cell round-history-col-clan">
            <div class="round-history-cell-title">${commEscape(clanName)}</div>
            <div class="round-history-cell-sub">Boosts</div>
            <div class="round-history-boosts">${clanBoosts}</div>
          </td>
          <td class="round-history-win-cell round-history-col-user">
            <div class="round-history-cell-title">${commEscape(userName)}</div>
            <div class="round-history-cell-sub">Boosts</div>
            <div class="round-history-boosts">${userBoosts}</div>
          </td>
        </tr>
      `;
    })
    .join('');
  return `
    <div class="round-history-table-wrap">
      <table class="round-history-table">
        <thead>${header}</thead>
        <tbody>${body}</tbody>
      </table>
    </div>
  `;
}

async function loadRoundHistoryFinishedLookup(rows) {
  statsRoundHistoryFinishedLookup = new Map();
  if (!Array.isArray(rows) || rows.length === 0) return statsRoundHistoryFinishedLookup;
  const blockIds = rows
    .map((row) => commParseNumber(row?.block_number ?? row?.blockNumber ?? row?.block_id))
    .filter((value) => Number.isFinite(value));
  const uniqueBlockIds = Array.from(new Set(blockIds));
  if (uniqueBlockIds.length === 0) return statsRoundHistoryFinishedLookup;
  const query = [
    'select=block_id,round_time,multiplier,leading_clans_snapshot,top_10_clans_with_boosts,clan_boosts,clan_name,clan_id,time_round_finished,league,cycle,data_complete',
    `block_id=in.(${uniqueBlockIds.join(',')})`
  ].join('&');
  const rowsFinished = await commFetch(COMM_TABLE_ROUNDS_FINISHED, query);
  if (Array.isArray(rowsFinished)) {
    rowsFinished.forEach((row) => {
      if (!isDataComplete(row?.data_complete)) return;
      const key = row?.block_id != null ? String(row.block_id) : null;
      if (!key) return;
      if (!statsRoundHistoryFinishedLookup.has(key)) {
        statsRoundHistoryFinishedLookup.set(key, []);
      }
      statsRoundHistoryFinishedLookup.get(key).push(row);
    });
  }
  return statsRoundHistoryFinishedLookup;
}

function normalizeFinishedClanEntry(entry) {
  if (!entry || typeof entry !== 'object') return {};
  const usedAbilities = entry.used_abilities ?? entry.usedAbilities ?? null;
  const summary = Array.isArray(usedAbilities) ? summarizeUsedAbilities(usedAbilities) : { total: null, items: [] };
  const boosts = entry?.boosts && typeof entry.boosts === 'object' && !Array.isArray(entry.boosts)
    ? entry.boosts
    : null;
  let usedTotal = entry?.used_abilities_total ?? entry?.usedAbilitiesTotal ?? summary.total ?? null;
  if (usedTotal == null && boosts) {
    usedTotal = Object.values(boosts).reduce((sum, value) => {
      const raw = Number(value || 0);
      return sum + (Number.isFinite(raw) ? raw : 0);
    }, 0);
  }
  return {
    clan_id: entry?.clan_id ?? entry?.clanId ?? entry?.id ?? null,
    clan_name: entry?.clan_name ?? entry?.clanName ?? entry?.name ?? null,
    current_added_score: entry?.current_added_score ?? entry?.currentAddedScore ?? entry?.current_added_score ?? null,
    active_boost_score: entry?.active_boost_score ?? entry?.activeBoostScore ?? null,
    clan_power: entry?.clan_power ?? entry?.clanPower ?? null,
    used_abilities: Array.isArray(usedAbilities) ? usedAbilities : (summary.items.length ? summary.items : null),
    used_abilities_total: usedTotal,
    boosts
  };
}

function normalizeFinishedRoundClans(round) {
  const snapshotRaw = parseMaybeJson(round?.leading_clans_snapshot);
  if (Array.isArray(snapshotRaw) && snapshotRaw.length > 0) {
    const hasProcessedShape = snapshotRaw.some((entry) =>
      entry?.current_added_score != null ||
      entry?.active_boost_score != null ||
      entry?.used_abilities ||
      entry?.usedAbilities ||
      entry?.used_abilities_total != null
    );
    if (hasProcessedShape) {
      return snapshotRaw.map((entry) => normalizeFinishedClanEntry(entry));
    }
    return buildClanSnapshotFromState(snapshotRaw, 10);
  }

  const top10Raw = parseMaybeJson(round?.top_10_clans_with_boosts);
  if (Array.isArray(top10Raw) && top10Raw.length > 0) {
    return top10Raw.map((entry) => {
      const boosts = entry?.boosts && typeof entry.boosts === 'object' && !Array.isArray(entry.boosts)
        ? entry.boosts
        : {};
      const totalBoosts = entry?.total_boosts ?? Object.values(boosts).reduce((sum, value) => {
        const raw = Number(value || 0);
        return sum + (Number.isFinite(raw) ? raw : 0);
      }, 0);
      return {
        clan_id: entry?.clan_id ?? entry?.clanId ?? null,
        clan_name: entry?.name ?? entry?.clan_name ?? entry?.clanName ?? (entry?.clan_id ? `Clan ${entry.clan_id}` : null),
        current_added_score: entry?.current_added_score ?? entry?.currentAddedScore ?? null,
        active_boost_score: entry?.active_boost_score ?? entry?.activeBoostScore ?? null,
        clan_power: entry?.clan_power ?? entry?.clanPower ?? null,
        used_abilities_total: totalBoosts || null,
        boosts
      };
    });
  }

  return [];
}

function buildBoostTotalsFromClans(clans) {
  if (!Array.isArray(clans) || clans.length === 0) return null;
  const totals = {};
  let usedAbilitiesFound = false;
  clans.forEach((entry) => {
    const abilities = entry?.used_abilities ?? entry?.usedAbilities ?? null;
    if (!Array.isArray(abilities) || abilities.length === 0) return;
    usedAbilitiesFound = true;
    abilities.forEach((ability) => {
      const abilityId = ability?.nftGameAbilityId ?? null;
      const boostType = abilityId ? ABILITY_ID_TO_TYPE[abilityId] : null;
      if (!boostType) return;
      const count = Number(ability?.count || 0);
      if (!Number.isFinite(count) || count <= 0) return;
      totals[boostType] = (totals[boostType] || 0) + count;
    });
  });
  if (!usedAbilitiesFound) {
    clans.forEach((entry) => {
      const boostMap = entry?.boosts;
      if (!boostMap || typeof boostMap !== 'object' || Array.isArray(boostMap)) return;
      Object.entries(boostMap).forEach(([type, count]) => {
        const raw = Number(count || 0);
        if (!Number.isFinite(raw) || raw <= 0) return;
        const normalized = normalizeBoostKey(type);
        totals[normalized] = (totals[normalized] || 0) + raw;
      });
    });
  }
  return Object.keys(totals).length > 0 ? totals : null;
}

function commBuildBoostTotalsFromAbilities(abilities) {
  if (!Array.isArray(abilities) || abilities.length === 0) return null;
  const totals = {};
  abilities.forEach((ability) => {
    const id = ability?.nftGameAbilityId ?? ability?.nft_game_ability_id ?? ability?.abilityId ?? ability?.id ?? null;
    const count = Number(ability?.count || 0);
    if (!id || !Number.isFinite(count) || count <= 0) return;
    const boostType = ABILITY_ID_TO_TYPE[id] || id;
    const key = normalizeBoostKey(boostType);
    totals[key] = (totals[key] || 0) + count;
  });
  return Object.keys(totals).length > 0 ? totals : null;
}

function commExtractRoundTimeValue(round) {
  return (
    round?.time_round_finished ??
    round?.timeRoundFinished ??
    round?.finished_at ??
    round?.finishedAt ??
    round?.created_at ??
    round?.createdAt ??
    round?.updated_at ??
    round?.updatedAt ??
    null
  );
}

function pickLatestFinishedRound(rows) {
  if (!Array.isArray(rows) || rows.length === 0) return null;
  return [...rows].sort((a, b) => {
    const aTime = a?.time_round_finished ? new Date(a.time_round_finished).getTime() : 0;
    const bTime = b?.time_round_finished ? new Date(b.time_round_finished).getTime() : 0;
    return bTime - aTime;
  })[0];
}

function ensureRoundHistoryPopup() {
  if (roundHistoryPopupOverlay) return;
  const existing = document.getElementById('roundHistoryPopupOverlay');
  if (existing) {
    roundHistoryPopupOverlay = existing;
    roundHistoryPopupBody = existing.querySelector('.round-history-popup-body');
    roundHistoryPopupTitle = existing.querySelector('.round-history-popup-title');
    return;
  }
  const overlay = document.createElement('div');
  overlay.id = 'roundHistoryPopupOverlay';
  overlay.className = 'round-history-popup-overlay';
  overlay.innerHTML = `
    <div class="round-history-popup" role="dialog" aria-modal="true">
      <div class="round-history-popup-header">
        <div class="round-history-popup-title"></div>
        <button type="button" class="round-history-popup-close" aria-label="Close">x</button>
      </div>
      <div class="round-history-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  roundHistoryPopupOverlay = overlay;
  roundHistoryPopupBody = overlay.querySelector('.round-history-popup-body');
  roundHistoryPopupTitle = overlay.querySelector('.round-history-popup-title');
  const closeBtn = overlay.querySelector('.round-history-popup-close');
  if (closeBtn) {
    closeBtn.addEventListener('click', () => closeRoundHistoryPopup());
  }
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) closeRoundHistoryPopup();
  });
}

function closeRoundHistoryPopup() {
  if (!roundHistoryPopupOverlay) return;
  roundHistoryPopupOverlay.style.display = 'none';
}

function ensureLiveScorePopup() {
  if (liveScorePopupOverlay) return;
  const existing = document.getElementById('liveScorePopupOverlay');
  if (existing) {
    liveScorePopupOverlay = existing;
    liveScorePopupBody = existing.querySelector('.round-history-popup-body');
    liveScorePopupTitle = existing.querySelector('.round-history-popup-title');
    return;
  }
  const overlay = document.createElement('div');
  overlay.id = 'liveScorePopupOverlay';
  overlay.className = 'round-history-popup-overlay live-score-popup-overlay';
  overlay.innerHTML = `
    <div class="round-history-popup live-score-popup" role="dialog" aria-modal="true">
      <div class="round-history-popup-header">
        <div class="round-history-popup-title"></div>
        <button type="button" class="round-history-popup-close" aria-label="Close">x</button>
      </div>
      <div class="round-history-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  liveScorePopupOverlay = overlay;
  liveScorePopupBody = overlay.querySelector('.round-history-popup-body');
  liveScorePopupTitle = overlay.querySelector('.round-history-popup-title');
  const closeBtn = overlay.querySelector('.round-history-popup-close');
  if (closeBtn) {
    closeBtn.addEventListener('click', () => closeLiveScorePopup());
  }
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) closeLiveScorePopup();
  });
}

function closeLiveScorePopup() {
  if (!liveScorePopupOverlay) return;
  liveScorePopupOverlay.style.display = 'none';
  liveScorePopupState = null;
  if (liveScorePopupTimer) {
    clearInterval(liveScorePopupTimer);
    liveScorePopupTimer = null;
  }
}

function resolveLiveScorePopupData(payload) {
  const clanKey = payload?.clanKey || normalizeClanKey({ clan_name: payload?.clanName });
  let apiEntry = clanKey && liveApiStateLookup ? liveApiStateLookup.get(clanKey) : null;
  if (!apiEntry && clanKey && Array.isArray(liveApiStateRaw)) {
    apiEntry = liveApiStateRaw.find((item) => normalizeClanKey(item) === clanKey) || null;
  }
  if (!apiEntry && clanKey && Array.isArray(liveApiStateTransformed)) {
    apiEntry = liveApiStateTransformed.find((item) => normalizeClanKey(item) === clanKey) || null;
  }
  const ppsFromApi = commParseNumber(apiEntry?.base_pps ?? apiEntry?.basePps ?? null);
  const ppsFromCache = clanKey && liveApiBasePpsByClan ? commParseNumber(liveApiBasePpsByClan.get(clanKey)) : null;
  const ppsFromPayload = commParseNumber(payload?.pps);
  const ppsValue = Number.isFinite(ppsFromApi)
    ? ppsFromApi
    : (Number.isFinite(ppsFromCache)
      ? ppsFromCache
      : (Number.isFinite(ppsFromPayload) ? ppsFromPayload : null));
  const wsAdded = clanKey ? commParseNumber(liveWsAddedScoreByClan.get(clanKey)) : null;
  const addedScore = Number.isFinite(wsAdded) ? wsAdded : 0;
  const baseRoundSeconds = Number(payload?.baseRoundSeconds);
  const openedAt = Number(payload?.openedAt);
  const elapsedSec = Number.isFinite(openedAt) ? Math.floor((Date.now() - openedAt) / 1000) : 0;
  let roundSeconds = null;
  if (Number.isFinite(baseRoundSeconds)) {
    roundSeconds = baseRoundSeconds + Math.max(0, elapsedSec);
  }
  if (!Number.isFinite(roundSeconds)) {
    const fallbackLabel = String(
      currentRoundInfo?.timer ??
      lastCurrentRoundLivePayload?.timer ??
      payload?.roundTimeLabel ??
      '-'
    );
    roundSeconds = parseRoundTimerSeconds(fallbackLabel);
  }
  const calcRoundSeconds = applyRoundTimeCalcOffset(roundSeconds);
  const roundTimeLabel = Number.isFinite(roundSeconds)
    ? formatRoundTimerSeconds(roundSeconds)
    : String(payload?.roundTimeLabel ?? currentRoundInfo?.timer ?? '-');
  const totalScore = Number.isFinite(ppsValue) && Number.isFinite(calcRoundSeconds)
    ? (ppsValue * calcRoundSeconds) + (Number.isFinite(addedScore) ? addedScore : 0)
    : null;
  const activation = clanKey ? liveWsBoostActivationByClan.get(clanKey) : null;
  return {
    clanKey,
    clanName: payload?.clanName ? String(payload.clanName) : '',
    pps: Number.isFinite(ppsValue) ? ppsValue : null,
    addedScore: Number.isFinite(addedScore) ? addedScore : null,
    roundTimeLabel,
    roundSeconds: Number.isFinite(calcRoundSeconds) ? calcRoundSeconds : null,
    totalScore: Number.isFinite(totalScore) ? totalScore : null,
    boostActivatedAt: activation?.at ?? null,
    boostRoundTimeLabel: activation?.roundTimeLabel ?? null,
    boostRoundSeconds: activation?.roundSeconds ?? null
  };
}

function getLiveCacheStatus() {
  const formatAge = (ms) => {
    if (!Number.isFinite(ms)) return '-';
    const totalSec = Math.max(0, Math.floor(ms / 1000));
    if (totalSec < 60) return `${totalSec}s`;
    const minutes = Math.floor(totalSec / 60);
    const seconds = totalSec % 60;
    if (minutes < 60) return `${minutes}m ${seconds}s`;
    const hours = Math.floor(minutes / 60);
    const remMin = minutes % 60;
    return `${hours}h ${remMin}m`;
  };
  const cacheReasons = [];
  if (!HAS_CHROME_RUNTIME) cacheReasons.push('no runtime');
  if (!hasSupabaseSession()) cacheReasons.push('no session');
  const cacheOwnerId = getHomeAuthUserId();
  if (!cacheOwnerId) cacheReasons.push('no owner');
  const wsRaw = typeof lastAllUsersStateV2Raw !== 'undefined' ? lastAllUsersStateV2Raw : null;
  const wsFresh = lastAllUsersStateV2At && (Date.now() - lastAllUsersStateV2At < 30000);
  if ((!Array.isArray(wsRaw) || wsRaw.length === 0) && !wsFresh) {
    cacheReasons.push('no WS');
  }
  const cacheCycleId = getCycleNumber(new Date());
  const cacheLeagueId = commResolveLiveLeagueId();
  if (!Number.isFinite(cacheCycleId)) cacheReasons.push('no cycle');
  if (!Number.isFinite(cacheLeagueId)) cacheReasons.push('no league');
  const cacheOk = cacheReasons.length === 0;
  const cacheStatusLabel = cacheOk
    ? 'Cache: ready'
    : `Cache blocked: ${cacheReasons.join(', ')}`;
  const cacheStatusTitle = cacheOk ? 'Live cache ready to sync.' : `Blocked: ${cacheReasons.join(', ')}`;
  const lastSentAt = typeof liveCacheLastSentAt !== 'undefined' ? liveCacheLastSentAt : 0;
  const cacheLastSync = lastSentAt
    ? `Last sync: ${formatAge(Date.now() - lastSentAt)} ago`
    : 'Last sync: -';
  const lastAppliedAt = typeof liveCacheLastAppliedAt !== 'undefined' ? liveCacheLastAppliedAt : 0;
  const cacheLastApplied = lastAppliedAt
    ? `Last applied: ${formatAge(Date.now() - lastAppliedAt)} ago`
    : null;
  return {
    ok: cacheOk,
    label: cacheStatusLabel,
    title: cacheStatusTitle,
    lastSync: cacheLastSync,
    lastApplied: cacheLastApplied
  };
}

function updateLiveScorePopup() {
  if (!liveScorePopupState) return;
  const data = resolveLiveScorePopupData(liveScorePopupState);
  const clanName = data.clanName || '';
  if (liveScorePopupTitle) {
    liveScorePopupTitle.textContent = clanName ? `Score calculation - ${clanName}` : 'Score calculation';
  }
  const formatCalc = (value, decimals = 2) => {
    const num = Number(value);
    if (!Number.isFinite(num)) return '-';
    return formatDecimal(num, decimals);
  };
  const formatTime = (ts) => (ts ? new Date(ts).toLocaleTimeString() : '-');
  const formatAge = (ms) => {
    if (!Number.isFinite(ms)) return '-';
    const totalSec = Math.max(0, Math.floor(ms / 1000));
    if (totalSec < 60) return `${totalSec}s`;
    const minutes = Math.floor(totalSec / 60);
    const seconds = totalSec % 60;
    if (minutes < 60) return `${minutes}m ${seconds}s`;
    const hours = Math.floor(minutes / 60);
    const remMin = minutes % 60;
    return `${hours}h ${remMin}m`;
  };
  const apiConnected = liveApiLastOkAt && (Date.now() - liveApiLastOkAt < LIVE_API_STATUS_WINDOW_MS());
  const summaryConnected = liveSummaryLastAt && (Date.now() - liveSummaryLastAt < LIVE_SUMMARY_STATUS_WINDOW_MS);
  const wsConnected = liveWsLastAt && (Date.now() - liveWsLastAt < LIVE_WS_STATUS_WINDOW_MS);
  const apiStatusText = apiConnected
    ? `API: ok ${formatTime(liveApiLastOkAt)}`
    : (liveApiLastStatus === 200 ? 'API - no sync required' : `API: off ${liveApiLastStatus ? liveApiLastStatus : '-'}`);
  const summaryStatusText = summaryConnected
    ? `Summary: ok ${formatTime(liveSummaryLastAt)}`
    : 'Summary: off';
  const wsStatusText = wsConnected
    ? `WS: ok ${formatTime(liveWsLastAt)}`
    : 'WS: off';
  const openedAtText = `Opened: ${formatTime(Date.now())}`;
  const apiAgeText = liveApiStateLastAttemptAt
    ? `Last API call: ${formatAge(Date.now() - liveApiStateLastAttemptAt)} ago`
    : 'Last API call: -';
  const cacheStatus = getLiveCacheStatus();
  const roundSeconds = Number(data.roundSeconds);
  const roundTimeLabel = data.roundTimeLabel ? String(data.roundTimeLabel) : '-';
  const roundTimeDisplay = Number.isFinite(roundSeconds)
    ? `${roundTimeLabel} (${formatCalc(roundSeconds, 0)}s)`
    : roundTimeLabel;
  const boostStartSeconds = Number.isFinite(data.boostRoundSeconds) ? data.boostRoundSeconds : null;
  const boostStartLabel = data.boostRoundTimeLabel ? String(data.boostRoundTimeLabel) : null;
  const boostStartDisplay = boostStartLabel
    ? (Number.isFinite(boostStartSeconds)
      ? `${boostStartLabel} (${formatCalc(boostStartSeconds, 0)}s)`
      : boostStartLabel)
    : null;
  const boostStatusText = Number.isFinite(data.addedScore) && data.addedScore > 0
    ? (boostStartDisplay ? `Active since ${boostStartDisplay}` : 'Active (time unknown)')
    : 'Not active yet';
  const bodyHtml = `
    <div style="display: flex; flex-direction: column; gap: 10px;">
      <div style="font-size: 0.8rem; opacity: 0.75; text-transform: uppercase; letter-spacing: 0.08em;">Calculation</div>
      <div style="font-weight: 700;">pps * round time + added score</div>
      <div style="font-size: 0.72rem; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.08em;">Live status</div>
      <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 0.8rem;">
        <div style="opacity: 0.8;">${commEscape(apiStatusText)}</div>
        <div style="text-align: right; opacity: 0.8;">${commEscape(summaryStatusText)}</div>
        <div style="opacity: 0.8;">${commEscape(wsStatusText)}</div>
        <div style="text-align: right; opacity: 0.7;">${commEscape(openedAtText)}</div>
        <div style="opacity: 0.8;">${commEscape(apiAgeText)}</div>
        <div></div>
      </div>
      <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 0.85rem;">
        <div style="opacity: 0.8;">PPS</div>
        <div style="text-align: right;">${commEscape(formatCalc(data.pps, 2))}</div>
        <div style="opacity: 0.8;">Round time</div>
        <div style="text-align: right;">${commEscape(roundTimeDisplay)}</div>
        <div style="opacity: 0.8;">PPS * round time</div>
        <div style="text-align: right;">${commEscape(formatCalc(Number.isFinite(data.pps) && Number.isFinite(roundSeconds) ? data.pps * roundSeconds : null, 0))}</div>
        <div style="opacity: 0.8;">Added score</div>
        <div style="text-align: right;">${commEscape(formatCalc(data.addedScore, 0))}</div>
        <div style="opacity: 0.8;">Boost activation</div>
        <div style="text-align: right;">${commEscape(boostStatusText)}</div>
        <div style="opacity: 0.8;">Total</div>
        <div style="text-align: right; font-weight: 600;">${commEscape(formatCalc(data.totalScore, 0))}</div>
      </div>
    </div>
  `;
  if (liveScorePopupBody) {
    liveScorePopupBody.innerHTML = bodyHtml;
  }
}

function openLiveScorePopup(payload) {
  if (!payload) return;
  ensureLiveScorePopup();
  liveScorePopupState = { ...payload };
  if (!Number.isFinite(liveScorePopupState.baseRoundSeconds)) {
    const initialSeconds = Number.isFinite(liveScorePopupState.roundSeconds)
      ? liveScorePopupState.roundSeconds
      : parseRoundTimerSeconds(liveScorePopupState.roundTimeLabel);
    liveScorePopupState.baseRoundSeconds = Number.isFinite(initialSeconds) ? initialSeconds : null;
  }
  liveScorePopupState.openedAt = Date.now();
  updateLiveScorePopup();
  if (liveScorePopupOverlay) {
    liveScorePopupOverlay.style.display = 'flex';
  }
  if (liveScorePopupTimer) clearInterval(liveScorePopupTimer);
  liveScorePopupTimer = setInterval(() => {
    if (!liveScorePopupState) return;
    updateLiveScorePopup();
  }, 1000);
}

function bindLiveScorePills(container) {
  if (!container || container.dataset.liveScorePopupBound) return;
  container.addEventListener('click', (event) => {
    const pill = event.target.closest('.live-score-pill');
    if (!pill) return;
    const payload = {
      clanKey: pill.dataset.clanKey || '',
      clanName: pill.dataset.clanName || '',
      pps: Number(pill.dataset.pps),
      roundSeconds: Number(pill.dataset.roundSeconds),
      roundTimeLabel: pill.dataset.roundTime || '-',
      addedScore: Number(pill.dataset.added),
      totalScore: Number(pill.dataset.total)
    };
    openLiveScorePopup(payload);
  });
  container.dataset.liveScorePopupBound = '1';
}

window.__mwSyncLiveCache = (button) => {
  const target = button || document.getElementById('currentRoundLiveCacheSync');
  if (!target) return;
  const previousSentAt = typeof liveCacheLastSentAt !== 'undefined' ? liveCacheLastSentAt : 0;
  const originalText = target.textContent;
  target.disabled = true;
  target.textContent = 'Syncing...';
  upsertLiveCache('manual', { force: true }).catch(() => {}).finally(() => {
    setTimeout(() => {
      const updated = (typeof liveCacheLastSentAt !== 'undefined' ? liveCacheLastSentAt : 0) > previousSentAt;
      target.textContent = updated ? 'Synced' : 'No data';
      target.disabled = false;
      setTimeout(() => {
        target.textContent = originalText || 'Sync cache';
      }, 1200);
    }, 600);
  });
};

if (typeof document !== 'undefined' && !window.__mwCacheSyncBound) {
  window.__mwCacheSyncBound = true;
  document.addEventListener('click', (event) => {
    const target = event.target;
    const button = target && target.closest ? target.closest('#currentRoundLiveCacheSync') : null;
    if (!button) return;
    if (typeof window.__mwSyncLiveCache === 'function') {
      window.__mwSyncLiveCache(button);
    }
  }, true);
}

function ensureLiveBoostPopup() {
  if (liveBoostPopupOverlay) return;
  const existing = document.getElementById('liveBoostPopupOverlay');
  if (existing) {
    liveBoostPopupOverlay = existing;
    liveBoostPopupBody = existing.querySelector('.round-history-popup-body');
    liveBoostPopupTitle = existing.querySelector('.round-history-popup-title');
    return;
  }
  const overlay = document.createElement('div');
  overlay.id = 'liveBoostPopupOverlay';
  overlay.className = 'round-history-popup-overlay live-boost-popup-overlay';
  overlay.innerHTML = `
    <div class="round-history-popup live-boost-popup" role="dialog" aria-modal="true">
      <div class="round-history-popup-header">
        <div class="round-history-popup-title"></div>
        <button type="button" class="round-history-popup-close" aria-label="Close">x</button>
      </div>
      <div class="round-history-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  liveBoostPopupOverlay = overlay;
  liveBoostPopupBody = overlay.querySelector('.round-history-popup-body');
  liveBoostPopupTitle = overlay.querySelector('.round-history-popup-title');
  const closeBtn = overlay.querySelector('.round-history-popup-close');
  if (closeBtn) closeBtn.addEventListener('click', closeLiveBoostPopup);
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) closeLiveBoostPopup();
  });
}

function closeLiveBoostPopup() {
  if (!liveBoostPopupOverlay) return;
  liveBoostPopupOverlay.style.display = 'none';
}

function getCurrentRoundStartMs() {
  const seconds = Number.isFinite(latestRoundTimerSeconds)
    ? latestRoundTimerSeconds
    : parseRoundTimerSeconds(currentRoundInfo?.timer);
  if (!Number.isFinite(seconds)) return null;
  return Date.now() - (seconds * 1000);
}

function getLiveRoundStartAtMs() {
  return getCurrentRoundStartMs();
}

function formatBoostAbsoluteStamp(ts) {
  if (!Number.isFinite(ts)) return '-';
  return commFormatChatStampShort(ts);
}

function formatBoostRoundStamp(ts) {
  if (!Number.isFinite(ts)) return '-';
  const start = getCurrentRoundStartMs();
  if (!Number.isFinite(start)) return '-';
  const elapsed = Math.round((ts - start) / 1000);
  if (!Number.isFinite(elapsed) || elapsed < 0) return '-';
  return formatRoundTimerSeconds(elapsed);
}

function openLiveBoostPopup(payload) {
  ensureLiveBoostPopup();
  if (!liveBoostPopupOverlay) return;
  const clanName = payload?.clanName || 'Unknown clan';
  const boostType = formatBoostDisplayName(payload?.boostType) || payload?.boostType || 'Boost';
  const sourceLabel = formatBoostOriginSource(payload?.source);
  const whenLabel = formatBoostOriginTime(payload?.timestamp);
  const countLabel = Number.isFinite(payload?.count) ? formatCount(payload.count) : '-';
  const rawList = Array.isArray(payload?.timestamps) ? payload.timestamps : (Number.isFinite(payload?.timestamp) ? [payload.timestamp] : []);
  const timeList = normalizeTimestampList(rawList);
  const timeRows = timeList.length
    ? timeList.map((ts, index) => {
      const absLabel = formatBoostAbsoluteStamp(ts);
      const roundLabel = formatBoostRoundStamp(ts);
      return `<div>Boost ${index + 1}: <strong>${commEscape(absLabel)}</strong> <span class="muted">(${commEscape(roundLabel)})</span></div>`;
    }).join('')
    : '<div class="muted">No timestamps for this boost.</div>';
  const debug = payload?.debug || null;
  const showDebug = Boolean(debug) || commIsBoostDebugActive();
  const debugHtml = showDebug
    ? `<div style="margin-top:6px;padding-top:6px;border-top:1px dashed rgba(255,255,255,0.08);">
        <div><strong>Debug</strong></div>
        <div class="muted" style="font-size:0.75rem;white-space:pre-wrap;">${commEscape(commCompactJson(debug || { note: 'no debug payload' }, 500))}</div>
      </div>`
    : '';
  if (liveBoostPopupTitle) {
    liveBoostPopupTitle.textContent = `Boost info - ${clanName}`;
  }
  if (liveBoostPopupBody) {
    liveBoostPopupBody.innerHTML = `
      <div style="display:grid;gap:8px;font-size:0.85rem;">
        <div><strong>Boost</strong>: ${commEscape(boostType)}</div>
        <div><strong>Count</strong>: ${commEscape(String(countLabel))}</div>
        <div><strong>Source</strong>: ${commEscape(sourceLabel)}</div>
        <div><strong>Last seen</strong>: ${commEscape(whenLabel)}</div>
        <div><strong>Boost timings</strong>:</div>
        <div style="display:grid;gap:4px;">${timeRows}</div>
        ${debugHtml}
      </div>
    `;
  }
  liveBoostPopupOverlay.style.display = 'flex';
}

function bindLiveBoostPills(container) {
  if (!container || container.dataset.liveBoostPopupBound) return;
  container.addEventListener('click', (event) => {
    const pill = event.target.closest('.boost-pill');
    if (!pill || pill.dataset.boostOrigin !== 'live') return;
    const payload = {
      clanName: pill.dataset.clanName || '',
      boostType: pill.dataset.boostType || '',
      source: pill.dataset.boostSource || '',
      timestamp: Number(pill.dataset.boostTs),
      count: Number(pill.dataset.boostCount),
      timestamps: (() => {
        const raw = pill.dataset.boostTsList;
        if (!raw) return [];
        try {
          const parsed = JSON.parse(raw);
          return Array.isArray(parsed) ? parsed : [];
        } catch (_) {
          return [];
        }
      })(),
      debug: (() => {
        const raw = pill.dataset.boostDebug;
        if (!raw) return null;
        try {
          return JSON.parse(raw);
        } catch (_) {
          return null;
        }
      })()
    };
    openLiveBoostPopup(payload);
  });
  container.dataset.liveBoostPopupBound = '1';
}

function buildRoundHistoryDetailCard(round) {
  if (!round) return '<div class="muted">No finished round data found.</div>';
  const blockId = round?.block_id ?? '-';
  const roundTime = round?.round_time ?? '-';
  const multiplier = round?.multiplier ?? '-';
  const leadingClans = normalizeFinishedRoundClans(round);
  const boosts = buildBoostTotalsFromClans(leadingClans);
  const card = buildCurrentRoundLiveCard({
    blockId,
    timer: roundTime,
    multiplier,
    leadingClans,
    boosts,
    clanBoosts: new Map(),
    clanLimit: 10,
    title: `Block ${blockId}`,
    showLimit: false
  });
  return card?.htmlContent || '<div class="muted">No finished round data found.</div>';
}

function openRoundHistoryPopup(blockId) {
  const key = blockId != null ? String(blockId) : '';
  const rows = statsRoundHistoryFinishedLookup.get(key) || [];
  const round = pickLatestFinishedRound(rows);
  if (!round) return;
  ensureRoundHistoryPopup();
  if (roundHistoryPopupTitle) {
    roundHistoryPopupTitle.textContent = `Block ${key}`;
  }
  if (roundHistoryPopupBody) {
    roundHistoryPopupBody.innerHTML = buildRoundHistoryDetailCard(round);
  }
  if (roundHistoryPopupOverlay) {
    roundHistoryPopupOverlay.style.display = 'flex';
  }
}

function bindRoundHistoryBlockPills(container) {
  if (!container || container.dataset.roundHistoryBound) return;
  container.addEventListener('click', (event) => {
    const pill = event.target.closest('.round-history-block-pill');
    if (!pill) return;
    const blockId = pill.dataset.blockId;
    if (!blockId) return;
    openRoundHistoryPopup(blockId);
  });
  container.dataset.roundHistoryBound = '1';
}

async function loadRoundHistory({ force = false, page = statsRoundHistoryPage } = {}) {
  if (statsRoundHistoryLoading) return;
  const safePage = Math.min(Math.max(1, Number(page) || 1), STATS_ROUND_HISTORY_MAX_PAGES);
  if (statsRoundHistoryLoaded && statsRoundHistoryLoadedPage === safePage && !force) return;
  const status = document.getElementById('statsRoundHistoryStatus');
  const list = document.getElementById('statsRoundHistoryList');
  const pageInfo = document.getElementById('statsRoundHistoryPageInfo');
  const pageSelect = document.getElementById('statsRoundHistoryPageSelect');
  if (!list) return;
  statsRoundHistoryPage = safePage;
  statsRoundHistoryLoading = true;
  if (status) status.textContent = 'Loading round history...';
  if (pageInfo) {
    pageInfo.textContent = `Page ${safePage} of ${STATS_ROUND_HISTORY_MAX_PAGES}`;
  }
  if (pageSelect && pageSelect.value !== String(safePage)) {
    pageSelect.value = String(safePage);
  }
  try {
    const offset = (safePage - 1) * STATS_ROUND_HISTORY_PER_PAGE;
    const queryParts = [
      'order=block_number.desc',
      `limit=${STATS_ROUND_HISTORY_PER_PAGE}`,
      `offset=${offset}`
    ];
    const cycleFilter = getRoundHistoryCycleFilter();
    if (Number.isFinite(cycleFilter)) {
      queryParts.unshift(`cycle_id=eq.${cycleFilter}`);
    }
    const leagueFilter = getRoundHistoryLeagueFilter();
    if (leagueFilter === 'dune-all') {
      const duneIds = Array.from({ length: CLAN_SELECTION_DUNE_END - CLAN_SELECTION_DUNE_START + 1 }, (_, idx) =>
        CLAN_SELECTION_DUNE_START + idx
      );
      queryParts.unshift(`league_id=in.(${duneIds.join(',')})`);
    } else if (Number.isFinite(leagueFilter)) {
      queryParts.unshift(`league_id=eq.${leagueFilter}`);
    }
    const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
    const safeRows = Array.isArray(rows) ? rows : [];
    const filteredRows = safeRows.filter((row) => {
      if (row?.active === true || row?.active === 'true' || row?.active === 1) return false;
      return true;
    });
    if (!filteredRows.length) {
      list.innerHTML = '<div class="muted">No round history available yet.</div>';
      if (status) status.textContent = 'No rows found in rounds_cycle_api.';
      statsRoundHistoryLoaded = true;
      statsRoundHistoryLoadedPage = safePage;
      return;
    }
    let finishedLookup = new Map();
    try {
      finishedLookup = await loadRoundHistoryFinishedLookup(filteredRows);
    } catch (err) {
      console.warn('[MW] Failed to fetch finished rounds for history lookup:', err);
      finishedLookup = new Map();
    }
    list.innerHTML = buildRoundHistoryTable(filteredRows, finishedLookup);
    statsRoundHistoryFinishedLookup = finishedLookup;
    bindRoundHistoryBlockPills(list);
    if (status) {
      status.textContent = `Loaded ${filteredRows.length} rounds (page ${safePage}/${STATS_ROUND_HISTORY_MAX_PAGES}).`;
    }
    statsRoundHistoryLoaded = true;
    statsRoundHistoryLoadedPage = safePage;
  } catch (err) {
    list.innerHTML = '<div class="muted">Failed to load round history.</div>';
    if (status) status.textContent = err?.message || 'Failed to load round history.';
  } finally {
    statsRoundHistoryLoading = false;
  }
}

async function fetchAllRoundHistoryRows(onProgress) {
  const rows = [];
  const cycleFilter = getRoundHistoryCycleFilter();
  const leagueFilter = getRoundHistoryLeagueFilter();
  for (let page = 1; page <= STATS_ROUND_HISTORY_MAX_PAGES; page += 1) {
    const offset = (page - 1) * STATS_ROUND_HISTORY_PER_PAGE;
    const queryParts = [
      'order=block_number.desc',
      `limit=${STATS_ROUND_HISTORY_PER_PAGE}`,
      `offset=${offset}`
    ];
    if (Number.isFinite(cycleFilter)) {
      queryParts.unshift(`cycle_id=eq.${cycleFilter}`);
    }
    if (leagueFilter === 'dune-all') {
      const duneIds = Array.from({ length: CLAN_SELECTION_DUNE_END - CLAN_SELECTION_DUNE_START + 1 }, (_, idx) =>
        CLAN_SELECTION_DUNE_START + idx
      );
      queryParts.unshift(`league_id=in.(${duneIds.join(',')})`);
    } else if (Number.isFinite(leagueFilter)) {
      queryParts.unshift(`league_id=eq.${leagueFilter}`);
    }
    const pageRows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
    const safeRows = Array.isArray(pageRows) ? pageRows : [];
    const filteredRows = safeRows.filter((row) => {
      if (row?.active === true || row?.active === 'true' || row?.active === 1) return false;
      return true;
    });
    rows.push(...filteredRows);
    if (typeof onProgress === 'function') {
      onProgress({ page, pageSize: safeRows.length, total: rows.length });
    }
    if (!safeRows.length || safeRows.length < STATS_ROUND_HISTORY_PER_PAGE) break;
  }
  return rows;
}

async function exportRoundHistoryToXlsx() {
  if (statsRoundHistoryExporting) return;
  if (!ensureXlsxLibrary()) return;
  const status = document.getElementById('statsRoundHistoryStatus');
  const exportBtn = document.getElementById('statsRoundHistoryExport');
  statsRoundHistoryExporting = true;
  if (exportBtn) exportBtn.disabled = true;
  try {
    if (status) status.textContent = 'Exporting round history...';
    const rows = await fetchAllRoundHistoryRows((progress) => {
      if (status) {
        status.textContent = `Exporting round history... page ${progress.page} (${progress.total} rows)`;
      }
    });
    if (!rows.length) {
      if (status) status.textContent = 'No round history data available to export.';
      return;
    }
    const filename = `round_history_${getExportTimestamp()}.xlsx`;
    exportRowsToXlsx(rows, filename, 'Round history');
    if (status) status.textContent = `Exported ${rows.length} rounds.`;
  } catch (err) {
    if (status) status.textContent = err?.message || 'Failed to export round history.';
  } finally {
    statsRoundHistoryExporting = false;
    if (exportBtn) exportBtn.disabled = false;
  }
}

function ensureStatsBucketPopup() {
  if (statsBucketPopupOverlay) return;
  const overlay = document.createElement('div');
  overlay.id = 'statsBucketPopupOverlay';
  overlay.className = 'stats-bucket-popup-overlay';
  overlay.innerHTML = `
    <div class="stats-bucket-popup" role="dialog" aria-modal="true">
      <div class="stats-bucket-popup-header">
        <div class="stats-bucket-popup-title"></div>
        <button type="button" class="stats-bucket-popup-close" aria-label="Close">x</button>
      </div>
      <div class="stats-bucket-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  statsBucketPopupOverlay = overlay;
  statsBucketPopupTitle = overlay.querySelector('.stats-bucket-popup-title');
  statsBucketPopupBody = overlay.querySelector('.stats-bucket-popup-body');
  const closeBtn = overlay.querySelector('.stats-bucket-popup-close');
  if (closeBtn) closeBtn.addEventListener('click', closeStatsBucketPopup);
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) closeStatsBucketPopup();
  });
}

function closeStatsBucketPopup() {
  if (statsBucketPopupOverlay) statsBucketPopupOverlay.style.display = 'none';
}

function getStatsBucketRows(scope, minValue, maxValue) {
  const field = scope === 'clan' ? 'clan_winner' : 'user_winner';
  const min = Number(minValue);
  const max = maxValue === Infinity ? Infinity : Number(maxValue);
  return statsWinRowsCache.filter((row) => {
    const chance = commExtractChanceValue(row?.[field]);
    if (!Number.isFinite(chance)) return false;
    if (Number.isFinite(max)) return chance >= min && chance < max;
    return chance >= min;
  });
}

function buildStatsBucketTable(rows, scope) {
  if (!rows.length) {
    return '<div class="muted">No records found for this range.</div>';
  }
  const winnerLabel = scope === 'clan' ? 'Winning clan' : 'Winning user';
  const field = scope === 'clan' ? 'clan_winner' : 'user_winner';
  const body = rows
    .map((row) => {
      const block = row?.block_number ?? '-';
      const multiplier = row?.multiplier ?? '-';
      const winner = scope === 'clan' ? extractRoundWinnerClan(row) : extractRoundWinnerUser(row);
      const name = winner?.name || 'Unknown';
      const chanceValue = commExtractChanceValue(row?.[field]);
      const chanceText = Number.isFinite(chanceValue) ? formatDecimal(chanceValue, 2) : '-';
      const boosts = buildBoostPillsHtml(winner?.usedAbilities);
      return `
        <tr>
          <td>${commEscape(block)}</td>
          <td>${commEscape(multiplier)}x</td>
          <td>${commEscape(name)}</td>
          <td>${commEscape(chanceText)}%</td>
          <td>${boosts}</td>
        </tr>
      `;
    })
    .join('');
  return `
    <table class="stats-bucket-table">
      <thead>
        <tr>
          <th>Block</th>
          <th>X</th>
          <th>${commEscape(winnerLabel)}</th>
          <th>Chance</th>
          <th>Boosts</th>
        </tr>
      </thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

function openStatsBucketPopup(scope, label, minValue, maxValue) {
  ensureStatsBucketPopup();
  const rows = getStatsBucketRows(scope, minValue, maxValue);
  const scopeLabel = scope === 'clan' ? 'Clan' : 'User';
  const multLabel = Number.isFinite(statsWinRowsCacheMultiplier)
    ? `  -  ${statsWinRowsCacheMultiplier}x`
    : '';
  if (statsBucketPopupTitle) {
    statsBucketPopupTitle.textContent = `${scopeLabel} ${label} (${rows.length})${multLabel}`;
  }
  if (statsBucketPopupBody) {
    statsBucketPopupBody.innerHTML = buildStatsBucketTable(rows, scope);
  }
  if (statsBucketPopupOverlay) {
    statsBucketPopupOverlay.style.display = 'flex';
  }
}

function bindStatsPercentPills(container) {
  if (!container || container.dataset.statsPillBound) return;
  container.addEventListener('click', (event) => {
    const pill = event.target.closest('.stats-percent-pill');
    if (!pill) return;
    const scope = pill.dataset.scope || 'user';
    const min = Number(pill.dataset.min);
    const maxRaw = pill.dataset.max;
    const max = maxRaw === 'Infinity' ? Infinity : Number(maxRaw);
    if (!Number.isFinite(min)) return;
    openStatsBucketPopup(scope, pill.textContent.trim(), min, max);
  });
  container.dataset.statsPillBound = '1';
}

async function loadStatsWinTables({ force = false } = {}) {
  if (statsWinTablesLoading) return;
  if (statsWinTablesLoaded && !force) return;
  const userTable = document.getElementById('statsUserWinTable');
  const clanTable = document.getElementById('statsClanWinTable');
  const userStatus = document.getElementById('statsUserWinStatus');
  const clanStatus = document.getElementById('statsClanWinStatus');
  const roundsUsed = document.getElementById('statsWinCount');
  const noBoostSummary = document.getElementById('statsNoBoostSummary');
  if (!userTable || !clanTable) return;
  statsWinTablesLoading = true;
  if (userStatus) userStatus.textContent = 'Loading user win stats...';
  if (clanStatus) clanStatus.textContent = 'Loading clan win stats...';
  if (roundsUsed) roundsUsed.textContent = 'Rounds used: -';
  if (noBoostSummary) noBoostSummary.textContent = 'No-boost wins: -';
  try {
    const queryParts = [
      'select=user_winner,clan_winner,block_number,multiplier',
      'order=block_number.desc',
      'limit=1000'
    ];
    const multiplierFilter = getStatsMultiplierFilter();
    if (Number.isFinite(multiplierFilter)) {
      queryParts.unshift(`multiplier=eq.${multiplierFilter}`);
    }
    const cycleFilter = getStatsCycleFilter();
    if (Number.isFinite(cycleFilter)) {
      queryParts.unshift(`cycle_id=eq.${cycleFilter}`);
    }
    const leagueFilter = getStatsLeagueFilter();
    if (leagueFilter === 'dune-all') {
      const duneIds = [];
      for (let id = CLAN_SELECTION_DUNE_START; id <= CLAN_SELECTION_DUNE_END; id += 1) {
        duneIds.push(id);
      }
      if (duneIds.length) {
        queryParts.unshift(`league_id=in.(${duneIds.join(',')})`);
      }
    } else if (Number.isFinite(leagueFilter)) {
      queryParts.unshift(`league_id=eq.${leagueFilter}`);
    }
    const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
    const safeRows = Array.isArray(rows) ? rows : [];
    if (!safeRows.length) {
      statsWinRowsCache = [];
      statsWinRowsCacheMultiplier = multiplierFilter;
      userTable.innerHTML = '<div class="muted">No API round records found.</div>';
      clanTable.innerHTML = '<div class="muted">No API round records found.</div>';
      if (userStatus) userStatus.textContent = 'No rows found in rounds_cycle_api.';
      if (clanStatus) clanStatus.textContent = 'No rows found in rounds_cycle_api.';
      if (roundsUsed) roundsUsed.textContent = 'Rounds used: 0';
      if (noBoostSummary) noBoostSummary.textContent = 'No-boost wins: User 0  -  Clan 0';
      statsWinTablesLoaded = true;
      return;
    }
    statsWinRowsCache = safeRows;
    statsWinRowsCacheMultiplier = multiplierFilter;
    const userRows = buildChanceWinBuckets(safeRows, 'user_winner');
    const clanRows = buildChanceWinBuckets(safeRows, 'clan_winner');
    const userNoBoost = summarizeNoBoostStats(safeRows, 'user_winner');
    const clanNoBoost = summarizeNoBoostStats(safeRows, 'clan_winner');
    userTable.innerHTML = buildChanceWinTable(userRows, 'User percentage', 'user');
    clanTable.innerHTML = buildChanceWinTable(clanRows, 'Clan percentage', 'clan');
    bindStatsPercentPills(userTable);
    bindStatsPercentPills(clanTable);
    if (userStatus) userStatus.textContent = '';
    if (clanStatus) clanStatus.textContent = '';
    if (roundsUsed) roundsUsed.textContent = `Rounds used: ${safeRows.length}`;
    if (noBoostSummary) {
      const userEff = formatDecimal(userNoBoost.avg, 2);
      const clanEff = formatDecimal(clanNoBoost.avg, 2);
      noBoostSummary.textContent = `No-boost wins: User ${formatCount(userNoBoost.wins)} (eff ${userEff})  -  Clan ${formatCount(clanNoBoost.wins)} (eff ${clanEff})`;
    }
    statsWinTablesLoaded = true;
  } catch (err) {
    statsWinRowsCache = [];
    statsWinRowsCacheMultiplier = multiplierFilter;
    userTable.innerHTML = '<div class="muted">Failed to load win stats.</div>';
    clanTable.innerHTML = '<div class="muted">Failed to load win stats.</div>';
    if (userStatus) userStatus.textContent = err?.message || 'Failed to load win stats.';
    if (clanStatus) clanStatus.textContent = err?.message || 'Failed to load win stats.';
    if (roundsUsed) roundsUsed.textContent = 'Rounds used: -';
    if (noBoostSummary) noBoostSummary.textContent = 'No-boost wins: -';
  } finally {
    statsWinTablesLoading = false;
  }
}

function formatPercentValue(value, decimals = 2) {
  if (!Number.isFinite(value)) return '-';
  return `${formatDecimal(value, decimals)}%`;
}

function commGetLiveWinsPercentForEntry(entry) {
  if (!(liveApiWinsPercentMap instanceof Map) || liveApiWinsPercentMap.size === 0) return null;
  const key = commExtractLiveClanKey(entry);
  if (key && liveApiWinsPercentMap.has(key)) return liveApiWinsPercentMap.get(key);
  const clanName = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? entry?.clan?.name ?? null;
  if (clanName) {
    const nameKey = normalizeClanKey({ clan_name: clanName });
    if (nameKey && liveApiWinsPercentMap.has(nameKey)) return liveApiWinsPercentMap.get(nameKey);
    const resolvedId = findClanIdByName(clanName);
    if (Number.isFinite(resolvedId)) {
      const idKey = normalizeClanKey({ clan_id: resolvedId });
      if (idKey && liveApiWinsPercentMap.has(idKey)) return liveApiWinsPercentMap.get(idKey);
    }
  }
  return null;
}

async function commRefreshLiveWinsPercent({ force = false } = {}) {
  const now = Date.now();
  const cycleId = getCycleNumber(new Date());
  let leagueId = commParseNumber(commSession?.leagueId ?? commSession?.league_id ?? null);
  if (!Number.isFinite(leagueId) && typeof lastLiveSummaryLeague !== 'undefined') {
    leagueId = commParseNumber(lastLiveSummaryLeague);
  }
  if (!Number.isFinite(leagueId) && Number.isFinite(commKorrelCurrentLeagueId)) {
    leagueId = commKorrelCurrentLeagueId;
  }
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return;

  const sameScope = liveApiWinsPercentMeta.cycleId === Math.trunc(cycleId)
    && liveApiWinsPercentMeta.leagueId === Math.trunc(leagueId);
  if (!force && sameScope && (now - liveApiWinsPercentMeta.updatedAt) < LIVE_WINS_PERCENT_REFRESH_MS) return;

  const rows = [];
  let offset = 0;
  while (offset < LIVE_WINS_ROUND_MAX) {
    const query = [
      'select=winner_clan_id,clan_winner,league_id,cycle_id',
      `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
      `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
      'order=block_number.desc',
      `limit=${LIVE_WINS_ROUND_PAGE_SIZE}`,
      `offset=${offset}`
    ].join('&');
    const page = await commFetch(COMM_TABLE_ROUND_CYCLE_API, query);
    if (!Array.isArray(page) || page.length === 0) break;
    rows.push(...page);
    if (page.length < LIVE_WINS_ROUND_PAGE_SIZE) break;
    offset += LIVE_WINS_ROUND_PAGE_SIZE;
  }

  const totalRounds = rows.length;
  const winsByKey = new Map();
  rows.forEach((row) => {
    let clanId = commParseNumber(row?.winner_clan_id);
    let clanName = null;
    if (!Number.isFinite(clanId)) {
      const clanWinner = commExtractClanWinnerFromRow(row);
      clanId = commParseNumber(clanWinner?.id);
      clanName = clanWinner?.name ?? null;
    }
    const key = Number.isFinite(clanId)
      ? normalizeClanKey({ clan_id: clanId })
      : (clanName ? normalizeClanKey({ clan_name: clanName }) : null);
    if (!key) return;
    winsByKey.set(key, (winsByKey.get(key) || 0) + 1);
  });

  const nextMap = new Map();
  if (totalRounds > 0) {
    winsByKey.forEach((wins, key) => {
      const pct = (wins / totalRounds) * 100;
      nextMap.set(key, pct);
    });
  }
  liveApiWinsPercentMap = nextMap;
  liveApiWinsPercentMeta = {
    cycleId: Math.trunc(cycleId),
    leagueId: Math.trunc(leagueId),
    updatedAt: now,
    totalRounds
  };
  if (typeof window.__refreshCurrentRoundCard === 'function') {
    window.__refreshCurrentRoundCard();
  }
}

function buildProfitabilityClanTable(rows) {
  if (!Array.isArray(rows) || !rows.length) {
    return '<div class="muted">No clan profitability data.</div>';
  }
  const header = `
    <tr>
      <th>Clan</th>
      <th>League</th>
      <th>Power</th>
      <th>Users</th>
      <th>GMT/wk</th>
      <th>GMT/TH/wk</th>
      <th>GMT/User/wk</th>
    </tr>
  `;
  const body = rows.map((row) => {
    return `
      <tr>
        <td>${commEscape(row.name || '-')}</td>
        <td>${commEscape(commFormatLeagueDisplay(row.league_id))}</td>
        <td class="stats-num">${row.power != null ? formatDecimal(row.power, 0) : '-'}</td>
        <td class="stats-num">${row.users != null ? formatDecimal(row.users, 0) : '-'}</td>
        <td class="stats-num">${row.gmtIncome != null ? formatDecimal(row.gmtIncome, 0) : '-'}</td>
        <td class="stats-num">${row.gmtPerTh != null ? formatDecimal(row.gmtPerTh, 0) : '-'}</td>
        <td class="stats-num">${row.gmtPerMember != null ? formatDecimal(row.gmtPerMember, 0) : '-'}</td>
      </tr>
    `;
  }).join('');
  return `
    <table class="stats-win-table">
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

function buildProfitabilityRoundsTable(rows) {
  if (!Array.isArray(rows) || !rows.length) {
    return '<div class="muted">No round profitability data.</div>';
  }
  const header = `
    <tr>
      <th>Clan</th>
      <th>League</th>
      <th>Wins/wk</th>
      <th>Win Rate (wk)</th>
      <th>Reward/wk</th>
      <th>Reward/TH/wk</th>
    </tr>
  `;
  const body = rows.map((row) => {
    return `
      <tr>
        <td>${commEscape(row.name || '-')}</td>
        <td>${commEscape(commFormatLeagueDisplay(row.league_id))}</td>
        <td class="stats-num">${row.wins != null ? formatDecimal(row.wins, 0) : '-'}</td>
        <td class="stats-num">${row.winRate != null ? formatPercentValue(row.winRate, 0) : '-'}</td>
        <td class="stats-num">${row.reward != null ? formatDecimal(row.reward, 0) : '-'}</td>
        <td class="stats-num">${row.rewardPerTh != null ? formatDecimal(row.rewardPerTh, 0) : '-'}</td>
      </tr>
    `;
  }).join('');
  return `
    <table class="stats-win-table">
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

async function loadProfitability({ force = false } = {}) {
  if (profitabilityLoading) return;
  if (profitabilityLoaded && !force) return;
  const status = document.getElementById('profitabilityStatus');
  const clanStatus = document.getElementById('profitClanStatus');
  const roundStatus = document.getElementById('profitRoundStatus');
  const clanTable = document.getElementById('profitClanTable');
  const roundTable = document.getElementById('profitRoundTable');
  if (status) status.textContent = 'Loading profitability...';
  if (clanStatus) clanStatus.textContent = 'Loading clan profitability...';
  if (roundStatus) roundStatus.textContent = 'Loading round profitability...';
  profitabilityLoading = true;
  try {
    const cycleFilter = getProfitCycleFilter();
    const leagueFilter = getProfitLeagueFilter();
    const clanQueryParts = [
      'select=cycle_id,league_id,clan_id,clan_name,clan_power,users_count,raw',
      `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`
    ];
    if (Number.isFinite(cycleFilter)) {
      clanQueryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleFilter))}`);
    }
    if (leagueFilter) {
      if (leagueFilter === 'dune-all') {
        clanQueryParts.unshift(`league_id=gte.${CLAN_SELECTION_DUNE_START}`);
        clanQueryParts.unshift(`league_id=lte.${CLAN_SELECTION_DUNE_END}`);
      } else {
        const leagueParsed = commParseNumber(leagueFilter);
        if (Number.isFinite(leagueParsed)) {
          clanQueryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueParsed))}`);
        }
      }
    }
    const clanRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, clanQueryParts.join('&'));
    const safeClanRows = Array.isArray(clanRows) ? clanRows : [];
    profitabilityClanRowsCache = safeClanRows;
    const clanMap = new Map();
    const clanProfitRows = safeClanRows.map((row) => {
      const raw = parseMaybeJson(row?.raw) || {};
      const clanId = commParseNumber(row?.clan_id ?? raw?.id);
      const name = raw?.name ?? row?.clan_name ?? '-';
      const leagueId = commParseNumber(row?.league_id ?? raw?.leagueId);
      const power = commParseNumber(raw?.power ?? row?.clan_power);
      const users = commParseNumber(raw?.usersCount ?? row?.users_count);
      const weeksActive = commGetClanWeeksActive(row, raw);
      const gmtIncomeRaw = commParseNumber(raw?.gmtIncome ?? raw?.gmt_income);
      const gmtIncome = Number.isFinite(gmtIncomeRaw) && Number.isFinite(weeksActive)
        ? gmtIncomeRaw / weeksActive
        : gmtIncomeRaw;
      const gmtPerTh = Number.isFinite(gmtIncome) && Number.isFinite(power) && power > 0
        ? gmtIncome / power
        : null;
      const gmtPerMember = Number.isFinite(gmtIncome) && Number.isFinite(users) && users > 0
        ? gmtIncome / users
        : null;
      if (Number.isFinite(clanId)) {
        clanMap.set(Math.trunc(clanId), {
          name,
          league_id: Number.isFinite(leagueId) ? Math.trunc(leagueId) : null,
          power,
          users,
          weeksActive
        });
      }
      return {
        id: clanId,
        name,
        league_id: Number.isFinite(leagueId) ? Math.trunc(leagueId) : row?.league_id ?? null,
        power: Number.isFinite(power) ? power : null,
        users: Number.isFinite(users) ? users : null,
        gmtIncome: Number.isFinite(gmtIncome) ? gmtIncome : null,
        gmtPerTh,
        gmtPerMember
      };
    });
    clanProfitRows.sort((a, b) => {
      const aValue = Number.isFinite(a.gmtPerTh) ? a.gmtPerTh : -1;
      const bValue = Number.isFinite(b.gmtPerTh) ? b.gmtPerTh : -1;
      return bValue - aValue;
    });
    const clanDisplayLimit = 100;
    const clanDisplayRows = clanProfitRows.slice(0, clanDisplayLimit);
    if (clanTable) clanTable.innerHTML = buildProfitabilityClanTable(clanDisplayRows);
    if (clanStatus) {
      clanStatus.textContent = `Clans used: ${safeClanRows.length}. Showing top ${clanDisplayRows.length} by GMT/TH per week.`;
    }

    const roundQueryParts = [
      'select=winner_clan_id,clan_winner,league_id,block_number,cycle_id',
      'order=block_number.desc',
      `limit=${PROFIT_ROUND_LIMIT}`
    ];
    if (Number.isFinite(cycleFilter)) {
      roundQueryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleFilter))}`);
    }
    if (leagueFilter) {
      if (leagueFilter === 'dune-all') {
        roundQueryParts.unshift(`league_id=gte.${CLAN_SELECTION_DUNE_START}`);
        roundQueryParts.unshift(`league_id=lte.${CLAN_SELECTION_DUNE_END}`);
      } else {
        const leagueParsed = commParseNumber(leagueFilter);
        if (Number.isFinite(leagueParsed)) {
          roundQueryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueParsed))}`);
        }
      }
    }
    const roundRows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, roundQueryParts.join('&'));
    const safeRoundRows = Array.isArray(roundRows) ? roundRows : [];
    profitabilityRoundRowsCache = safeRoundRows;
    const totalRounds = safeRoundRows.length;
    const cycleSet = new Set(
      safeRoundRows
        .map((row) => commParseNumber(row?.cycle_id))
        .filter((value) => Number.isFinite(value))
        .map((value) => Math.trunc(value))
    );
    const roundWeeks = Number.isFinite(cycleFilter) ? 1 : Math.max(1, cycleSet.size || 1);
    const roundsPerWeek = roundWeeks > 0 ? totalRounds / roundWeeks : totalRounds;
    const winMap = new Map();
    safeRoundRows.forEach((row) => {
      const winner = extractRoundWinnerClan(row);
      const clanId = commParseNumber(winner.id);
      if (!Number.isFinite(clanId)) return;
      const key = Math.trunc(clanId);
      const rewardValue = commExtractGmtReward(winner.raw, winner.clan);
      const entry = winMap.get(key) || {
        id: key,
        name: winner.name || '',
        league_id: row?.league_id ?? null,
        wins: 0,
        reward: 0
      };
      entry.wins += 1;
      if (Number.isFinite(rewardValue)) entry.reward += rewardValue;
      if (!entry.name && winner.name) entry.name = winner.name;
      if (!Number.isFinite(entry.league_id)) {
        const leagueFromRow = commParseNumber(row?.league_id);
        if (Number.isFinite(leagueFromRow)) entry.league_id = Math.trunc(leagueFromRow);
      }
      winMap.set(key, entry);
    });
    const roundProfitRows = Array.from(winMap.values()).map((entry) => {
      const clanInfo = clanMap.get(entry.id);
      const power = commParseNumber(clanInfo?.power);
      const weeksActive = commParseNumber(clanInfo?.weeksActive);
      const winsAdjusted = Number.isFinite(weeksActive) && weeksActive > 0
        ? entry.wins / weeksActive
        : entry.wins;
      const rewardTotal = Number.isFinite(entry.reward) && entry.reward > 0 ? entry.reward : null;
      const rewardAdjusted = rewardTotal != null && Number.isFinite(weeksActive) && weeksActive > 0
        ? rewardTotal / weeksActive
        : rewardTotal;
      const rewardPerTh = rewardAdjusted != null && Number.isFinite(power) && power > 0
        ? rewardAdjusted / power
        : null;
      const winRate = roundsPerWeek > 0 && Number.isFinite(winsAdjusted)
        ? (winsAdjusted / roundsPerWeek) * 100
        : null;
      return {
        ...entry,
        name: clanInfo?.name || entry.name || `Clan ${entry.id}`,
        league_id: clanInfo?.league_id ?? entry.league_id ?? null,
        wins: winsAdjusted,
        winRate,
        reward: rewardAdjusted,
        rewardPerTh
      };
    });
    roundProfitRows.sort((a, b) => {
      const aValue = Number.isFinite(a.rewardPerTh) ? a.rewardPerTh : -1;
      const bValue = Number.isFinite(b.rewardPerTh) ? b.rewardPerTh : -1;
      if (bValue !== aValue) return bValue - aValue;
      return b.wins - a.wins;
    });
    const roundDisplayLimit = 100;
    const roundDisplayRows = roundProfitRows.slice(0, roundDisplayLimit);
    if (roundTable) roundTable.innerHTML = buildProfitabilityRoundsTable(roundDisplayRows);
    if (roundStatus) {
      roundStatus.textContent = `Rounds used: ${totalRounds} (limit ${PROFIT_ROUND_LIMIT}). Showing top ${roundDisplayRows.length} by reward/TH per week.`;
    }
    if (status) status.textContent = '';
    profitabilityLoaded = true;
  } catch (err) {
    if (status) status.textContent = err?.message || 'Failed to load profitability.';
    if (clanStatus) clanStatus.textContent = err?.message || 'Failed to load clan profitability.';
    if (roundStatus) roundStatus.textContent = err?.message || 'Failed to load round profitability.';
    if (clanTable) clanTable.innerHTML = '<div class="muted">Failed to load clan profitability.</div>';
    if (roundTable) roundTable.innerHTML = '<div class="muted">Failed to load round profitability.</div>';
  } finally {
    profitabilityLoading = false;
  }
}

function showStatsPanel(tabName, cardOverride = null) {
  const card = cardOverride || document.querySelector('[data-card-id="stats-dashboard-card"]');
  if (!card) return;
  const panels = Array.from(card.querySelectorAll('[data-stats-panel]'));
  if (!panels.length) return;
  const target = tabName || statsActivePanel || 'effectiveness';
  panels.forEach((panel) => {
    panel.classList.toggle('active', panel.dataset.statsPanel === target);
  });
  statsActivePanel = target;
  if (target === 'history') {
    populateClans('statsRoundHistoryGrid');
    loadRoundHistory().catch(() => {});
  }
  if (target === 'clan-selection') {
    populateClanSelection();
  }
  if (target === 'profitability') {
    loadProfitability().catch(() => {});
  }
  if (target === 'effectiveness') {
    loadStatsWinTables({ force: true }).catch(() => {});
  }
}

function showBoostImpactPanel(tabName, cardOverride = null) {
  const card = cardOverride || document.querySelector('[data-card-id="boost-impact-card"]');
  if (!card) return;
  const panels = Array.from(card.querySelectorAll('[data-boost-panel]'));
  if (!panels.length) return;
  const target = tabName || 'ratio';
  panels.forEach((panel) => {
    panel.classList.toggle('active', panel.dataset.boostPanel === target);
  });
  const tabs = Array.from(card.querySelectorAll('[data-boost-tab]'));
  tabs.forEach((tab) => {
    tab.classList.toggle('active', tab.dataset.boostTab === target);
  });
}

function initStatsTabs(card) {
  if (!card || card.dataset.statsBound) return;
  showStatsPanel(statsActivePanel || 'effectiveness', card);
  card.dataset.statsBound = '1';
}

  function buildClanSnapshotFromState(state, limit = 5) {
    // Live table uses API round data only (no WebSocket fallbacks).
    if (!Array.isArray(state)) return [];
    const rows = state.map((clan) => {
      const apiEntry = commFindLiveApiClan(clan);
      const mergedClan = commMergeLiveClanFields(clan, apiEntry);
      const clanId = mergedClan?.clanId ?? mergedClan?.clan_id ?? mergedClan?.id ?? null;
      const clanName = mergedClan?.clanName ?? mergedClan?.clan_name ?? mergedClan?.name ?? (clanId ? (clanLookup.get(String(clanId)) || null) : null);
      const scoreValue = commParseNumber(
        mergedClan?.score ??
        mergedClan?.currentScore ??
        mergedClan?.current_added_score ??
        mergedClan?.currentAddedScore ??
        mergedClan?.basePoints ??
        mergedClan?.base_points
      );
      const powerValue = commParseNumber(
        mergedClan?.power ??
        mergedClan?.clanPower ??
        mergedClan?.clan_power ??
        mergedClan?.rosterPower ??
        mergedClan?.roster_power
      );
      const usedAbilities = Array.isArray(mergedClan?.usedAbilities) ? mergedClan.usedAbilities : [];
      return {
        clan_id: clanId,
        clan_name: clanName,
        score: Number.isFinite(scoreValue) ? scoreValue : null,
        power: Number.isFinite(powerValue) ? powerValue : null,
        chance: commParseNumber(mergedClan?.chance ?? mergedClan?.chanceToWin ?? mergedClan?.chance_to_win),
        active_boost_score: mergedClan?.activeBoostScore ?? mergedClan?.active_boost_score ?? null,
        used_abilities_total: usedAbilities.reduce((sum, ability) => sum + Number(ability?.count || 0), 0),
        used_abilities: usedAbilities
      };
    }).filter(row => row && row.clan_id != null);

  rows.sort((a, b) => {
    const aScore = Number(a.score) || 0;
    const bScore = Number(b.score) || 0;
    return bScore - aScore;
  });
  return rows.slice(0, limit).map((row, idx) => ({ position: idx + 1, ...row }));
}

function updateLiveTotalScoreSeries(roundId, roundSeconds, totalScore) {
  const resetSeries = (nextRoundKey) => {
    liveTotalScoreSeries = [];
    liveTotalScoreSeriesRound = nextRoundKey ?? liveTotalScoreSeriesRound ?? null;
    liveTotalScoreChartState.lastRenderAt = 0;
    liveTotalScoreChartState.lastRenderMinute = null;
    liveTotalScoreChartState.lastBaselineKey = null;
    clearBoostEventLog();
  };
  const normalizedRound = normalizeBlockId(roundId) || String(roundId || '');
  if (normalizedRound && normalizedRound !== liveTotalScoreSeriesRound) {
    resetSeries(normalizedRound);
  }
  if (!Number.isFinite(roundSeconds) || roundSeconds < 0) return;
  if (!Number.isFinite(totalScore)) return;
  const lastBefore = liveTotalScoreSeries[liveTotalScoreSeries.length - 1];
  if (lastBefore && Number.isFinite(lastBefore.t)) {
    const drift = lastBefore.t - roundSeconds;
    if (drift > 60) {
      resetSeries(normalizedRound || liveTotalScoreSeriesRound || String(roundId || ''));
    }
  }
  const minute = Math.max(0, Math.ceil(roundSeconds / 60));
  if (!Number.isFinite(minute) || minute < 0) return;
  const point = { t: minute * 60, v: totalScore };
  const last = liveTotalScoreSeries[liveTotalScoreSeries.length - 1];
  if (last && last.t === point.t) {
    last.v = point.v;
  } else {
    liveTotalScoreSeries.push(point);
  }
  if (liveTotalScoreSeries.length > LIVE_TOTAL_SCORE_MAX_POINTS) {
    liveTotalScoreSeries = liveTotalScoreSeries.slice(-LIVE_TOTAL_SCORE_MAX_POINTS);
  }
}

function getLiveBoostCountsByMinute(clanBoosts) {
  const counts = new Map();
  if (!clanBoosts || typeof clanBoosts.forEach !== 'function') return counts;
  const roundStartAt = getCurrentRoundStartMs();
  if (!Number.isFinite(roundStartAt)) return counts;
  const cutoff = roundStartAt - LIVE_BOOST_TS_SKEW_MS;
  const currentRoundId = normalizeBlockId(
    currentRoundInfo?.blockId ?? lastLiveSummaryBlockId ?? currentRoundInfo?.roundId ?? null
  );
  const seen = new Set();
  const seenKeys = new Set();
  clanBoosts.forEach((clanEntry) => {
    if (!clanEntry) return;
    if (seen.has(clanEntry)) return;
    const clanIdValue = clanEntry.clan_id ?? clanEntry.clanId ?? null;
    const clanNameValue = clanEntry.name ?? clanEntry.clan_name ?? clanEntry.clanName ?? null;
    const idKey = clanIdValue != null ? `id:${String(clanIdValue)}` : null;
    const nameKey = clanNameValue ? `name:${normalizeClanName(clanNameValue)}` : null;
    if ((idKey && seenKeys.has(idKey)) || (nameKey && seenKeys.has(nameKey))) return;
    if (idKey) seenKeys.add(idKey);
    if (nameKey) seenKeys.add(nameKey);
    seen.add(clanEntry);
    if (currentRoundId && clanEntry.roundKey && clanEntry.roundKey !== currentRoundId) return;
    const meta = clanEntry.boostMeta;
    if (!meta || typeof meta !== 'object') return;
    Object.entries(meta).forEach(([typeKey, metaEntry]) => {
      const boostType = normalizeBoostKey(typeKey);
      if (!isKnownBoostType(boostType)) return;
      const timestamps = normalizeTimestampList([
        ...(Array.isArray(metaEntry?.timestamps) ? metaEntry.timestamps : []),
        metaEntry?.ts
      ]);
      if (!timestamps.length) return;
      timestamps.forEach((ts) => {
        if (!Number.isFinite(ts) || ts < cutoff) return;
        const elapsedSec = Math.max(0, Math.round((ts - roundStartAt) / 1000));
        const minute = Math.max(0, Math.ceil(elapsedSec / 60));
        const existing = counts.get(minute) || { total: 0, types: new Map() };
        existing.total += 1;
        existing.types.set(boostType, (existing.types.get(boostType) || 0) + 1);
        counts.set(minute, existing);
      });
    });
  });
  return counts;
}

function formatScoreValueSafe(value) {
  const num = Number(value);
  if (!Number.isFinite(num)) return '-';
  const millions = num / 1_000_000;
  const decimals = Math.abs(millions) >= 100 ? 0 : 2;
  return `${formatDecimal(millions, decimals)}M`;
}

function getLiveTotalScoreWindowConfig(series, mode) {
  const normalizedMode = commNormalizeLiveTotalScoreWindowMode(mode);
  const hasSeries = Array.isArray(series) && series.length > 0;
  const maxSec = hasSeries
    ? Math.max(...series.map((pt) => (Number.isFinite(pt?.t) ? pt.t : 0)))
    : 0;
  const maxMinute = Math.max(0, Math.round(maxSec / 60));
  let startMinute = 0;
  let endMinute = LIVE_TOTAL_SCORE_DEFAULT_MINUTES;
  if (normalizedMode === 'first30') {
    endMinute = 30;
  } else if (normalizedMode === 'start') {
    endMinute = hasSeries ? maxMinute : LIVE_TOTAL_SCORE_DEFAULT_MINUTES;
  } else if (normalizedMode === 'last10') {
    endMinute = hasSeries ? maxMinute : LIVE_TOTAL_SCORE_DEFAULT_MINUTES;
    startMinute = Math.max(0, endMinute - 10);
  } else {
    endMinute = 10;
  }
  if (endMinute < startMinute) endMinute = startMinute;
  const windowMinutes = Math.max(0, endMinute - startMinute);
  const minutes = [];
  for (let m = startMinute; m <= endMinute; m += 1) minutes.push(m);
  return { startMinute, endMinute, windowMinutes, minutes };
}

function buildLiveTotalScoreChart(series) {
  const windowConfig = getLiveTotalScoreWindowConfig(series, liveTotalScoreWindowMode);
  const maxMinutes = windowConfig.endMinute;
  const modeOptions = Object.entries(LIVE_TOTAL_SCORE_WINDOW_MODE_LABELS)
    .map(([value, label]) => `<option value="${value}">${label}</option>`)
    .join('');
  return `
    <div class="live-total-score-chart">
      <div class="live-total-score-title-row">
        <div class="live-total-score-title">Total score (this round)</div>
        <div class="live-total-score-controls">
          <label class="live-total-score-label-text" for="liveTotalScoreWindowMode">View</label>
          <select id="liveTotalScoreWindowMode" class="live-total-score-window-select">
            ${modeOptions}
          </select>
        </div>
      </div>
      <div class="live-total-score-live">Live score: --</div>
      <div class="live-total-score-ct ct-chart"></div>
      <div class="live-total-score-hover-line" aria-hidden="true"></div>
      <div class="live-total-score-tooltip" aria-hidden="true"></div>
      <div class="live-total-score-empty">Waiting for total score history...</div>
      <div class="live-total-score-meta">
        <span>0m</span>
        <span>${maxMinutes}m</span>
      </div>
    </div>
  `;
}

function renderLiveTotalScoreChart(container, series, clanBoosts) {
  if (!container) return;
  const chartEl = container.querySelector('.live-total-score-ct');
  const emptyEl = container.querySelector('.live-total-score-empty');
  const metaEl = container.querySelector('.live-total-score-meta');
  const liveEl = container.querySelector('.live-total-score-live');
  const modeSelect = container.querySelector('#liveTotalScoreWindowMode');
  if (!chartEl) return;
  const ChartistLib = typeof Chartist !== 'undefined' ? Chartist : null;
  if (!ChartistLib || !ChartistLib.LineChart) {
    if (emptyEl) {
      emptyEl.textContent = 'Chart library not loaded.';
      emptyEl.style.display = 'block';
    }
    return;
  }

  if (modeSelect) {
    modeSelect.value = commNormalizeLiveTotalScoreWindowMode(liveTotalScoreWindowMode);
    if (!modeSelect.__mwBound) {
      modeSelect.__mwBound = true;
      modeSelect.addEventListener('pointerdown', () => {
        liveTotalScoreWindowEditing = true;
      });
      modeSelect.addEventListener('focus', () => {
        liveTotalScoreWindowEditing = true;
      });
      modeSelect.addEventListener('blur', () => {
        liveTotalScoreWindowEditing = false;
      });
      modeSelect.addEventListener('change', (event) => {
        const nextValue = commNormalizeLiveTotalScoreWindowMode(event.target.value);
        liveTotalScoreWindowMode = nextValue;
        commSetLiveTotalScoreWindowMode(nextValue);
        liveTotalScoreWindowEditing = false;
        liveTotalScoreChartState.lastRenderAt = 0;
        liveTotalScoreChartState.lastRenderMinute = null;
        liveTotalScoreChartState.lastBaselineKey = null;
        renderLiveTotalScoreChart(container, liveTotalScoreSeries, chartEl.__mwClanBoosts);
      });
    }
  }

  const windowConfig = getLiveTotalScoreWindowConfig(series, liveTotalScoreWindowMode);
  const maxMinutes = windowConfig.endMinute;
  const startMinutes = windowConfig.startMinute;
  if (metaEl) {
    const spans = metaEl.querySelectorAll('span');
    if (spans.length >= 2) {
      spans[0].textContent = `${startMinutes}m`;
      spans[1].textContent = `${maxMinutes}m`;
    }
  }

  const lastPoint = Array.isArray(series) && series.length
    ? series[series.length - 1]
    : null;
  const lastMinute = Number.isFinite(lastPoint?.t) ? Math.round(lastPoint.t / 60) : 0;
  const liveScoreValue = Number.isFinite(liveTotalScoreCurrent)
    ? liveTotalScoreCurrent
    : (Number.isFinite(lastPoint?.v) ? lastPoint.v : null);
  if (liveEl) {
    const label = Number.isFinite(liveScoreValue) ? Math.round(liveScoreValue).toLocaleString() : '--';
    liveEl.textContent = `Live score: ${label}`;
  }

  if (!Array.isArray(series) || series.length === 0) {
    chartEl.innerHTML = '';
    if (emptyEl) emptyEl.style.display = 'block';
    return;
  }

  const byMinute = new Map();
  series.forEach((pt) => {
    const minute = Math.round((Number(pt?.t) || 0) / 60);
    const value = Number(pt?.v);
    if (Number.isFinite(minute) && minute >= 0 && Number.isFinite(value)) {
      byMinute.set(minute, value);
    }
  });

  const lastKnownMinute = byMinute.size ? Math.max(...byMinute.keys()) : -1;
  const totalAllValues = [];
  let lastValue = null;
  for (let m = 0; m <= maxMinutes; m += 1) {
    if (byMinute.has(m)) {
      lastValue = byMinute.get(m);
    }
    if (m > lastKnownMinute) {
      totalAllValues.push(null);
    } else {
      totalAllValues.push(Number.isFinite(lastValue) ? lastValue : null);
    }
  }
  const totalValues = totalAllValues.slice(windowConfig.startMinute, windowConfig.endMinute + 1);
  const availableIndices = [];
  totalValues.forEach((value, idx) => {
    if (Number.isFinite(value)) availableIndices.push(idx);
  });
  if (clanBoosts && typeof clanBoosts.forEach === 'function') {
    chartEl.__mwClanBoosts = clanBoosts;
  }
  const boostCounts = getLiveBoostCountsByMinute(chartEl.__mwClanBoosts);

  let basePpsTotal = computeLiveBasePpsTotal();
  if (!Number.isFinite(basePpsTotal)) {
    basePpsTotal = computeBasePpsTotalFromSnapshot(lastLiveClanSnapshot)
      ?? computeBasePpsTotalFromSnapshot(window.__mwLastLiveClanSnapshot);
  }
  if (!Number.isFinite(basePpsTotal) || basePpsTotal <= 0) {
    basePpsTotal = null;
  }

  const baselineKey = basePpsTotal ? Math.round(basePpsTotal) : 0;
  const now = Date.now();
  const minuteChanged = liveTotalScoreChartState.lastRenderMinute == null
    || liveTotalScoreChartState.lastRenderMinute !== lastMinute;
  const baselineChanged = liveTotalScoreChartState.lastBaselineKey == null
    || liveTotalScoreChartState.lastBaselineKey !== baselineKey;
  const forceRender = now - liveTotalScoreChartState.lastRenderAt > LIVE_TOTAL_SCORE_RENDER_MIN_MS;
  if (!minuteChanged && !baselineChanged && !forceRender) {
    return;
  }
  liveTotalScoreChartState.lastRenderAt = now;
  liveTotalScoreChartState.lastRenderMinute = lastMinute;
  liveTotalScoreChartState.lastBaselineKey = baselineKey;

  const baselineValues = basePpsTotal
    ? windowConfig.minutes.map((minute) => basePpsTotal * 60 * minute)
    : [];

  chartEl.__mwChartData = {
    totalValues,
    baselineValues,
    maxMinutes,
    minutes: windowConfig.minutes,
    startMinute: windowConfig.startMinute,
    availableIndices
  };
  chartEl.__mwBoostCounts = boostCounts;
  chartEl.__mwBoostWindow = windowConfig;

  const data = {
    labels: windowConfig.minutes,
    series: basePpsTotal
      ? [{ name: 'total', data: totalValues }, { name: 'baseline', data: baselineValues }]
      : [{ name: 'total', data: totalValues }]
  };

  const maxLabelMinute = windowConfig.windowMinutes <= 12
    ? 1
    : (windowConfig.windowMinutes <= 30 ? 5 : 10);
  const options = {
    fullWidth: true,
    showPoint: true,
    showLine: true,
    showArea: true,
    lineSmooth: ChartistLib.Interpolation && ChartistLib.Interpolation.monotoneCubic
      ? ChartistLib.Interpolation.monotoneCubic()
      : true,
    low: 0,
    chartPadding: { top: 10, right: 12, bottom: 6, left: 4 },
    axisX: {
      offset: 22,
      showGrid: true,
      labelInterpolationFnc: (value) => {
        const offset = value - windowConfig.startMinute;
        return offset % maxLabelMinute === 0 ? `${value}m` : null;
      }
    },
    axisY: {
      offset: 40,
      showGrid: true,
      onlyInteger: true,
      labelInterpolationFnc: (value) => formatScoreValueSafe(value)
    },
    series: {
      baseline: { showPoint: false }
    }
  };

  if (emptyEl) emptyEl.style.display = 'none';
  const existingChart = chartEl.__mwChartist;
  if (chartEl.__mwHighlightedPoint && chartEl.__mwHighlightedPoint.node) {
    chartEl.__mwHighlightedPoint.node.classList.remove('is-hovered');
    chartEl.__mwHighlightedPoint = null;
  }
  chartEl.__mwPointNodes = [];
  if (existingChart && typeof existingChart.update === 'function') {
    existingChart.update(data, options, true);
    if (existingChart.chartRect) {
      chartEl.__mwChartRect = existingChart.chartRect;
    }
    renderLiveBoostMarkers(container, chartEl);
    bindLiveTotalScoreHover(container, chartEl);
    return;
  }
  chartEl.innerHTML = '';
  const chart = new ChartistLib.LineChart(chartEl, data, options);
  chartEl.__mwChartist = chart;
  chart.on('created', (ctx) => {
    if (ctx && ctx.chartRect) {
      chartEl.__mwChartRect = ctx.chartRect;
    }
    chartEl.__mwPointNodes = [];
    renderLiveBoostMarkers(container, chartEl);
  });
  chart.on('draw', (ctx) => {
    if (ctx.type !== 'point' || ctx.seriesIndex !== 0) return;
    const raw = ctx.value && typeof ctx.value === 'object' ? ctx.value.y ?? ctx.value : ctx.value;
    const label = Number.isFinite(raw) ? Math.round(raw).toLocaleString() : '-';
    const minute = typeof ctx.index === 'number' ? ctx.index : null;
    const titleText = minute != null ? `Minute ${minute} - ${label}` : label;
    const node = ctx.element && (ctx.element._node || (ctx.element.getNode && ctx.element.getNode()));
    if (!node) return;
    if (typeof ctx.index === 'number') {
      chartEl.__mwPointNodes[ctx.index] = node;
    }
    const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
    title.textContent = titleText;
    node.appendChild(title);
  });
  bindLiveTotalScoreHover(container, chartEl);
}

function renderLiveBoostMarkers(container, chartEl) {
  if (!container || !chartEl) return;
  const windowConfig = chartEl.__mwBoostWindow;
  if (!windowConfig || !Array.isArray(windowConfig.minutes)) return;
  const wrapper = chartEl.closest('.live-total-score-chart') || container;
  let markers = wrapper.querySelector('.live-total-score-boost-markers');
  if (!markers) {
    markers = document.createElement('div');
    markers.className = 'live-total-score-boost-markers';
    wrapper.appendChild(markers);
  }
  markers.innerHTML = '';
  const boostCounts = chartEl.__mwBoostCounts;
  if (!boostCounts || boostCounts.size === 0) return;
  const chartRect = chartEl.__mwChartRect;
  if (!chartRect) return;
  const chartBox = chartEl.getBoundingClientRect();
  const containerBox = wrapper.getBoundingClientRect();
  const plotLeft = chartRect.x1;
  const plotRight = chartRect.x2;
  const plotWidth = Math.max(1, plotRight - plotLeft);
  const steps = Math.max(1, windowConfig.minutes.length - 1);
  const top = chartBox.top - containerBox.top + Math.max(4, chartRect.y1 - 40);

  boostCounts.forEach((entry, minute) => {
    const total = Number(entry?.total || 0);
    if (!Number.isFinite(total) || total <= 0) return;
    if (minute < windowConfig.startMinute || minute > windowConfig.endMinute) return;
    const idx = minute - windowConfig.startMinute;
    const xPos = plotLeft + (steps ? (idx / steps) * plotWidth : 0);
    const left = chartBox.left - containerBox.left + xPos;
    const marker = document.createElement('div');
    marker.className = 'live-total-score-boost-marker';
    marker.style.left = `${left}px`;
    marker.style.top = `${top}px`;
    marker.innerHTML = `
      <img src="style/boost-marker.png" alt="Boost" class="live-total-score-boost-icon">
      <div class="live-total-score-boost-count">${formatCount(total)}</div>
    `;
    marker.addEventListener('click', (event) => {
      event.stopPropagation();
      openLiveBoostMinutePopup({
        minute,
        total,
        types: entry?.types instanceof Map ? entry.types : new Map(),
        clanBoosts: chartEl.__mwClanBoosts
      });
    });
    markers.appendChild(marker);
  });
}

function getLiveBoostEventsForMinute(minute, clanBoosts) {
  const events = [];
  if (!Number.isFinite(minute)) return events;
  if (!clanBoosts || typeof clanBoosts.forEach !== 'function') return events;
  const roundStartAt = getCurrentRoundStartMs();
  if (!Number.isFinite(roundStartAt)) return events;
  const cutoff = roundStartAt - LIVE_BOOST_TS_SKEW_MS;
  const currentRoundId = normalizeBlockId(
    currentRoundInfo?.blockId ?? lastLiveSummaryBlockId ?? currentRoundInfo?.roundId ?? null
  );
  const seen = new Set();
  const seenKeys = new Set();

  clanBoosts.forEach((clanEntry) => {
    if (!clanEntry) return;
    if (seen.has(clanEntry)) return;
    const clanIdValue = clanEntry.clan_id ?? clanEntry.clanId ?? null;
    const clanNameValue = clanEntry.name ?? clanEntry.clan_name ?? clanEntry.clanName ?? null;
    const idKey = clanIdValue != null ? `id:${String(clanIdValue)}` : null;
    const nameKey = clanNameValue ? `name:${normalizeClanName(clanNameValue)}` : null;
    if ((idKey && seenKeys.has(idKey)) || (nameKey && seenKeys.has(nameKey))) return;
    if (idKey) seenKeys.add(idKey);
    if (nameKey) seenKeys.add(nameKey);
    seen.add(clanEntry);
    if (currentRoundId && clanEntry.roundKey && clanEntry.roundKey !== currentRoundId) return;
    const clanName = clanEntry.name
      || clanEntry.clan_name
      || clanEntry.clanName
      || (clanEntry.clan_id ? `Clan ${clanEntry.clan_id}` : 'Unknown clan');
    const meta = clanEntry.boostMeta;
    if (!meta || typeof meta !== 'object') return;
    Object.entries(meta).forEach(([typeKey, metaEntry]) => {
      const boostType = normalizeBoostKey(typeKey);
      if (!isKnownBoostType(boostType)) return;
      const timestamps = normalizeTimestampList([
        ...(Array.isArray(metaEntry?.timestamps) ? metaEntry.timestamps : []),
        metaEntry?.ts
      ]);
      timestamps.forEach((ts) => {
        if (!Number.isFinite(ts) || ts < cutoff) return;
        const elapsedSec = Math.max(0, Math.round((ts - roundStartAt) / 1000));
        const eventMinute = Math.max(0, Math.ceil(elapsedSec / 60));
        if (eventMinute !== minute) return;
        events.push({
          ts,
          boostType,
          clanName,
          source: metaEntry?.source || 'unknown',
          userName: null
        });
      });
    });
  });

  const logEvents = BOOST_EVENT_LOG.filter((evt) => {
    const ts = Number(evt?.eventTs ?? evt?.ts);
    if (!Number.isFinite(ts)) return false;
    if (ts < cutoff) return false;
    if (!isKnownBoostType(evt?.boostType)) return false;
    const logRoundId = normalizeBlockId(evt?.roundId);
    if (currentRoundId && logRoundId && logRoundId !== currentRoundId) return false;
    const elapsedSec = Math.max(0, Math.round((ts - roundStartAt) / 1000));
    const eventMinute = Math.max(0, Math.ceil(elapsedSec / 60));
    return eventMinute === minute;
  });

  const unmatched = [...logEvents];
  events.forEach((evt) => {
    const idx = unmatched.findIndex((log) => {
      const logType = normalizeBoostKey(log?.boostType);
      const logClan = log?.clanName || log?.clan_name || null;
      const sameType = logType === evt.boostType;
      const sameClan = !logClan || !evt.clanName || logClan === evt.clanName;
      return sameType && sameClan;
    });
    if (idx >= 0) {
      const match = unmatched.splice(idx, 1)[0];
      const rawUser = match?.playerName || match?.playerKey;
      evt.userName = rawUser === '__local__' ? 'You' : (rawUser || null);
      if (!evt.clanName && match?.clanName) evt.clanName = match.clanName;
      if (!evt.source && match?.source) evt.source = match.source;
    }
  });

  unmatched.forEach((log) => {
    const ts = Number(log?.eventTs ?? log?.ts);
    if (!Number.isFinite(ts)) return;
    const boostType = normalizeBoostKey(log?.boostType);
    if (!isKnownBoostType(boostType)) return;
    const rawUser = log?.playerName || log?.playerKey;
    events.push({
      ts,
      boostType,
      clanName: log?.clanName || log?.clan_name || 'Unknown clan',
      source: log?.source || 'ws:abilityUsage',
      userName: rawUser === '__local__' ? 'You' : (rawUser || null)
    });
  });

  events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
  return events;
}

function openLiveBoostMinutePopup(payload) {
  ensureLiveBoostPopup();
  if (!liveBoostPopupOverlay || !liveBoostPopupBody || !liveBoostPopupTitle) return;
  const minute = Number(payload?.minute);
  const minuteLabel = Number.isFinite(minute) ? `${minute}m` : '-';
  const total = Number(payload?.total || 0);
  const types = payload?.types instanceof Map ? payload.types : new Map();
  const clanBoosts = payload?.clanBoosts;
  const events = getLiveBoostEventsForMinute(minute, clanBoosts);
  const typeRows = Array.from(types.entries())
    .sort((a, b) => (b[1] || 0) - (a[1] || 0))
    .map(([type, count]) => {
      const label = formatBoostDisplayName(type) || type || 'Boost';
      return `
        <div class="boost-minute-row">
          <div class="boost-minute-type">${commEscape(label)}</div>
          <div class="boost-minute-count">${formatCount(count)}</div>
        </div>
      `;
    })
    .join('');
  const eventRows = events.length
    ? events.map((evt) => {
      const timeLabel = formatBoostAbsoluteStamp(evt.ts);
      const roundLabel = formatBoostRoundStamp(evt.ts);
      const boostLabel = formatBoostDisplayName(evt.boostType) || evt.boostType || 'Boost';
      const metaParts = [];
      if (evt.clanName) metaParts.push(evt.clanName);
      if (evt.userName) metaParts.push(evt.userName);
      const metaLabel = metaParts.length ? metaParts.join('  -  ') : 'Unknown';
      return `
        <div class="boost-minute-event">
          <div class="boost-minute-time">${commEscape(timeLabel)} <span class="muted">(${commEscape(roundLabel)})</span></div>
          <div class="boost-minute-event-row">
            <span class="boost-minute-event-type">${commEscape(boostLabel)}</span>
            <span class="boost-minute-event-meta">${commEscape(metaLabel)}</span>
          </div>
        </div>
      `;
    }).join('')
    : '<div class="muted">No boost timestamps available.</div>';
  liveBoostPopupTitle.textContent = `Boosts at ${minuteLabel}`;
  liveBoostPopupBody.innerHTML = `
    <div class="boost-minute-summary">
      <div>Total boosts: <strong>${formatCount(total)}</strong></div>
      <div class="muted">Counts are based on detected boost events for this minute.</div>
    </div>
    <div class="boost-minute-section">
      <div class="boost-minute-section-title">Boost types</div>
      <div class="boost-minute-list">
        ${typeRows || '<div class="muted">No boost details available.</div>'}
      </div>
    </div>
    <div class="boost-minute-section">
      <div class="boost-minute-section-title">Event timeline</div>
      <div class="boost-minute-events">
        ${eventRows}
      </div>
    </div>
  `;
  liveBoostPopupOverlay.style.display = 'flex';
}

function bindLiveTotalScoreHover(container, chartEl) {
  if (!container || !chartEl) return;
  const hoverTarget = container.querySelector('.live-total-score-chart') || chartEl;
  if (hoverTarget.__mwHoverBound) return;
  hoverTarget.__mwHoverBound = true;
  const tooltip = container.querySelector('.live-total-score-tooltip');
  const hoverLine = container.querySelector('.live-total-score-hover-line');
  if (!tooltip || !hoverLine) return;

  const hide = () => {
    if (chartEl.__mwHighlightedPoint && chartEl.__mwHighlightedPoint.node) {
      chartEl.__mwHighlightedPoint.node.classList.remove('is-hovered');
      chartEl.__mwHighlightedPoint = null;
    }
    tooltip.style.opacity = '0';
    tooltip.style.display = 'none';
    tooltip.setAttribute('aria-hidden', 'true');
    hoverLine.style.opacity = '0';
  };

  const show = (event) => {
    const data = chartEl.__mwChartData;
    if (!data || !Array.isArray(data.totalValues) || data.totalValues.length === 0) {
      hide();
      return;
    }
    const chartBox = chartEl.getBoundingClientRect();
    const withinChart = event.clientX >= chartBox.left
      && event.clientX <= chartBox.right
      && event.clientY >= chartBox.top
      && event.clientY <= chartBox.bottom;
    if (!withinChart) {
      hide();
      return;
    }
    const containerBox = container.getBoundingClientRect();
    const plotRect = chartEl.__mwChartRect;
    const plotLeft = plotRect ? plotRect.x1 : 0;
    const plotRight = plotRect ? plotRect.x2 : chartBox.width;
    const plotWidth = Math.max(1, plotRight - plotLeft);
    const xInChart = event.clientX - chartBox.left;
    const clampedX = Math.min(Math.max(xInChart, plotLeft), plotRight);
    const steps = Math.max(0, data.totalValues.length - 1);
    const ratio = plotWidth ? (clampedX - plotLeft) / plotWidth : 0;
    const idx = steps ? Math.round(ratio * steps) : 0;
    const targetIndex = Math.min(Math.max(idx, 0), steps);
    let minuteIndex = targetIndex;
    const pointNodes = Array.isArray(chartEl.__mwPointNodes) ? chartEl.__mwPointNodes : [];
    const nodeIndices = pointNodes.reduce((acc, node, index) => {
      if (node) acc.push(index);
      return acc;
    }, []);
    const candidates = nodeIndices.length
      ? nodeIndices
      : (Array.isArray(data.availableIndices) && data.availableIndices.length ? data.availableIndices : null);
    if (candidates && candidates.length) {
      minuteIndex = candidates.reduce((best, current) => {
        if (best == null) return current;
        return Math.abs(current - targetIndex) < Math.abs(best - targetIndex) ? current : best;
      }, null);
    }
    if (!Number.isFinite(minuteIndex)) minuteIndex = targetIndex;
    const minuteValue = Array.isArray(data.minutes)
      ? data.minutes[Math.min(minuteIndex, data.minutes.length - 1)]
      : minuteIndex;
    const totalValue = data.totalValues[minuteIndex];
    const baselineValue = Array.isArray(data.baselineValues) ? data.baselineValues[minuteIndex] : null;
    const timeLabel = `${minuteValue}m`;
    const totalLabel = Number.isFinite(totalValue) ? Math.round(totalValue).toLocaleString() : '--';
    const baselineLabel = Number.isFinite(baselineValue) ? Math.round(baselineValue).toLocaleString() : '--';

    tooltip.innerHTML = `
      <div class="live-total-score-tooltip-row"><span class="label">Time</span><span class="value">${timeLabel}</span></div>
      <div class="live-total-score-tooltip-row"><span class="label">Score</span><span class="value">${totalLabel}</span></div>
      <div class="live-total-score-tooltip-row"><span class="label">Expected</span><span class="value">${baselineLabel}</span></div>
    `;

    if (chartEl.__mwHighlightedPoint && chartEl.__mwHighlightedPoint.node) {
      chartEl.__mwHighlightedPoint.node.classList.remove('is-hovered');
      chartEl.__mwHighlightedPoint = null;
    }
    const nearestNode = pointNodes[minuteIndex];
    if (nearestNode && nearestNode.classList) {
      nearestNode.classList.add('is-hovered');
      chartEl.__mwHighlightedPoint = { index: minuteIndex, node: nearestNode };
    }

    const xPos = plotLeft + (steps ? (minuteIndex / steps) * plotWidth : 0);
    const left = chartBox.left - containerBox.left + xPos;
    const top = chartBox.top - containerBox.top;

    hoverLine.style.left = `${left}px`;
    hoverLine.style.top = `${top}px`;
    hoverLine.style.height = `${chartBox.height}px`;
    hoverLine.style.opacity = '1';

    tooltip.style.display = 'block';
    tooltip.style.opacity = '1';
    tooltip.style.left = `${left}px`;
    tooltip.style.top = `${top}px`;
    tooltip.setAttribute('aria-hidden', 'false');

    const tooltipWidth = tooltip.offsetWidth || 0;
    if (tooltipWidth) {
      const minLeft = 8 + tooltipWidth / 2;
      const maxLeft = containerBox.width - 8 - tooltipWidth / 2;
      const clampedLeft = Math.min(Math.max(left, minLeft), maxLeft);
      tooltip.style.left = `${clampedLeft}px`;
    }
  };

  hoverTarget.addEventListener('pointermove', show);
  hoverTarget.addEventListener('mousemove', show);
  hoverTarget.addEventListener('pointerdown', show);
  hoverTarget.addEventListener('pointerleave', hide);
  hoverTarget.addEventListener('pointerout', hide);
  hoverTarget.addEventListener('mouseleave', hide);

  if (!chartEl.__mwHoverWindowBound) {
    chartEl.__mwHoverWindowBound = true;
    chartEl.__mwHoverWindowHandler = show;
    window.addEventListener('pointermove', show);
    window.addEventListener('mousemove', show);
  }
}
function buildLiveRoundRowsFromApi(state, limit = 10) {
  const rows = resolveApiArray(state, [
    'data.allClansState',
    'data.all_clans_state',
    'data.participants',
    'data.clans',
    'data.leaderboard',
    'data.leaderboard.clans',
    'data.array',
    'array'
  ]);
  if (!Array.isArray(rows)) return [];
  const now = Date.now();
  const apiConnected = liveApiLastOkAt && (Date.now() - liveApiLastOkAt < LIVE_API_STATUS_WINDOW_MS());
  let roundSeconds = Number.isFinite(latestRoundTimerSeconds) ? latestRoundTimerSeconds : null;
  const timerFallback = parseRoundTimerSeconds(currentRoundInfo?.timer ?? lastCurrentRoundLivePayload?.timer ?? null);
  if (!Number.isFinite(roundSeconds)) {
    roundSeconds = timerFallback;
  }
  const calcRoundSeconds = applyRoundTimeCalcOffset(roundSeconds);
  const mapped = rows.map((item, idx) => {
    const clanId = item.clanId ?? item.clan_id ?? item.id ?? item.clan?.id ?? null;
    const clanName = item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null;
    const power = Number(item.power ?? item.clanPower ?? item.clan_power ?? item.rosterPower ?? item.roster_power ?? item.nftPower ?? 0);
    const basePoints = commParseNumber(item?.basePoints ?? item?.base_points ?? null);
    const boostScore = Number(item.activeBoostScore ?? item.active_boost_score ?? item.addedScore ?? item.boostScore ?? 0);
    const usedAbilities = Array.isArray(item?.usedAbilities)
      ? item.usedAbilities
      : (Array.isArray(item?.used_abilities) ? item.used_abilities : null);
    const apiPosition = Number(item.position ?? item.rank ?? NaN);
    const key = commExtractLiveClanKey(item);
    const rate = key ? liveWsScoreRateMap.get(key) : null;
    const maxWindowSec = 180;
    const lastAt = rate && Number.isFinite(rate.lastAt) ? rate.lastAt : rate?.at;
    const dtSec = lastAt ? Math.min((now - lastAt) / 1000, maxWindowSec) : 0;
    const lastScore = rate && Number.isFinite(rate.lastScore) ? rate.lastScore : rate?.score;
    let projectedScore = rate && Number.isFinite(lastScore) && Number.isFinite(rate.ratePerSec) && rate.ratePerSec > 0
      ? lastScore + (rate.ratePerSec * Math.max(0, dtSec))
      : null;
    if (Number.isFinite(projectedScore) && projectedScore < 0) {
      projectedScore = null;
    }
    const canProject = Number.isFinite(projectedScore) && projectedScore >= 0;
    const basePpsFromApi = commParseNumber(
      item?.base_pps ??
      item?.basePps ??
      null
    );
    let basePps = null;
    if (Number.isFinite(basePpsFromApi) && basePpsFromApi > 0) {
      if (Number.isFinite(power) && power > 0) {
        const minPps = power * BASE_PPS_MIN_FACTOR;
        const maxPps = power * BASE_PPS_MAX_FACTOR;
        if (basePpsFromApi >= minPps && basePpsFromApi <= maxPps) {
          basePps = basePpsFromApi;
        }
      } else {
        basePps = basePpsFromApi;
      }
    }
    if (!Number.isFinite(basePps) && key && liveApiBasePpsByClan.has(key)) {
      const cachedBasePps = liveApiBasePpsByClan.get(key);
      if (Number.isFinite(cachedBasePps) && cachedBasePps > 0) basePps = cachedBasePps;
    }
    if (!Number.isFinite(basePps) || basePps <= 0) {
      basePps = rate && Number.isFinite(rate.stablePps) && rate.stablePps > 0 ? rate.stablePps : null;
    }
    if (!Number.isFinite(basePps) && rate && Number.isFinite(rate.ratePerSec) && rate.ratePerSec > 0) {
      basePps = rate.ratePerSec;
    }
    if ((!Number.isFinite(basePps) || basePps <= 0) && key && liveWsStablePpsByClan.has(key)) {
      const fallbackStable = liveWsStablePpsByClan.get(key);
      if (Number.isFinite(fallbackStable) && fallbackStable > 0) basePps = fallbackStable;
    }
    if (!Number.isFinite(basePps) && Number.isFinite(power) && power > 0) {
      basePps = power * LIVE_PPS_FACTOR;
    }
    const wsAdded = key ? commParseNumber(liveWsAddedScoreByClan.get(key)) : null;
    const nowAdded = Number.isFinite(wsAdded) ? wsAdded : null;
    let baseScoreEstimate = Number.isFinite(basePps) && Number.isFinite(calcRoundSeconds)
      ? basePps * calcRoundSeconds
      : null;
    let baseScoreSource = Number.isFinite(baseScoreEstimate) ? 'pps*seconds' : null;
    const boostedScoreEstimate = Number.isFinite(baseScoreEstimate)
      ? baseScoreEstimate + (Number.isFinite(nowAdded) ? nowAdded : 0)
      : null;
    const useBasePpsProjection = Number.isFinite(baseScoreEstimate);
    const scoreDisplayValue = useBasePpsProjection
      ? baseScoreEstimate
      : (canProject ? projectedScore : null);
    const scoreValue = Number.isFinite(boostedScoreEstimate)
      ? boostedScoreEstimate
      : (Number.isFinite(scoreDisplayValue) ? scoreDisplayValue : null);
    return {
      position: null,
      api_position: Number.isFinite(apiPosition) && apiPosition > 0 ? apiPosition : null,
      clan_id: clanId,
      clan_name: clanName,
      score: Number.isFinite(scoreValue) ? scoreValue : null,
      score_display: Number.isFinite(scoreDisplayValue) ? scoreDisplayValue : null,
      boosted_score_estimate: Number.isFinite(boostedScoreEstimate) ? boostedScoreEstimate : null,
      boost_added_score: Number.isFinite(nowAdded) ? nowAdded : null,
      score_extrapolated: useBasePpsProjection ? true : (canProject && dtSec > 0.5),
      base_pps: Number.isFinite(basePps) ? basePps : null,
      base_score_source: baseScoreSource,
      percentage: item.percentage ?? item.clan_percentage ?? item.share ?? item.pct ?? null,
      chance: item.chance ?? item.chanceToWin ?? item.chance_to_win ?? null,
      wins_pct: commGetLiveWinsPercentForEntry(item),
      power: Number.isFinite(power) ? power : null,
      active_boost_score: Number.isFinite(boostScore) ? boostScore : null,
      used_abilities: usedAbilities
    };
  }).filter(row => row && (row.clan_id != null || row.clan_name));

  const hasPosition = mapped.some((row) => Number.isFinite(row.position));
  if (hasPosition) {
    mapped.sort((a, b) => {
      const aPos = Number(a.position) || 0;
      const bPos = Number(b.position) || 0;
      return aPos - bPos;
    });
  } else {
    mapped.sort((a, b) => {
      const aScore = Number(a.boosted_score_estimate ?? a.score) || 0;
      const bScore = Number(b.boosted_score_estimate ?? b.score) || 0;
      return bScore - aScore;
    });
  }
  return mapped.slice(0, limit).map((row, idx) => ({
    position: Number.isFinite(row.position) ? row.position : (idx + 1),
    ...row
  }));
}

function loadLiveBoostHourlyStats() {
  try {
    const raw = localStorage.getItem(LIVE_BOOST_HOURLY_STORAGE_KEY);
    if (!raw) return { hours: {} };
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return { hours: {} };
    if (!parsed.hours || typeof parsed.hours !== 'object') parsed.hours = {};
    return parsed;
  } catch (err) {
    return { hours: {} };
  }
}

function saveLiveBoostHourlyStats(stats) {
  if (!stats || typeof stats !== 'object') return;
  try {
    localStorage.setItem(LIVE_BOOST_HOURLY_STORAGE_KEY, JSON.stringify(stats));
  } catch (err) {
    // ignore storage errors
  }
}

function buildLiveBoostHourKey(dateInput = new Date()) {
  const d = new Date(dateInput);
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  const hour = String(d.getHours()).padStart(2, '0');
  return `${year}-${month}-${day} ${hour}`;
}

function parseLiveBoostHourKey(key) {
  if (!key) return null;
  const parts = String(key).split(' ');
  if (parts.length !== 2) return null;
  const [datePart, hourPart] = parts;
  const dateBits = datePart.split('-').map((val) => Number(val));
  const hour = Number(hourPart);
  if (dateBits.length !== 3 || !Number.isFinite(hour)) return null;
  const [year, month, day] = dateBits;
  if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
  return new Date(year, month - 1, day, hour, 0, 0, 0);
}

function pruneLiveBoostHourlyStats(stats) {
  if (!stats || typeof stats !== 'object' || !stats.hours) return;
  const now = Date.now();
  const maxAgeMs = LIVE_BOOST_HOURLY_RETENTION_HOURS * 3600 * 1000;
  Object.keys(stats.hours).forEach((key) => {
    const dt = parseLiveBoostHourKey(key);
    if (!dt) {
      delete stats.hours[key];
      return;
    }
    if (now - dt.getTime() > maxAgeMs) {
      delete stats.hours[key];
    }
  });
}

function computeLiveBasePpsTotal() {
  if (!Array.isArray(liveApiStateRaw) || liveApiStateRaw.length === 0) return null;
  const rows = buildLiveRoundRowsFromApi(liveApiStateRaw, liveApiStateRaw.length);
  if (!Array.isArray(rows) || rows.length === 0) return null;
  let total = 0;
  let count = 0;
  rows.forEach((row) => {
    const basePps = commParseNumber(row?.base_pps);
    if (!Number.isFinite(basePps) || basePps <= 0) return;
    total += basePps;
    count += 1;
  });
  return count > 0 ? total : null;
}

function computeBasePpsTotalFromSnapshot(list) {
  if (!Array.isArray(list) || list.length === 0) return null;
  let total = 0;
  let count = 0;
  list.forEach((row) => {
    let basePps = commParseNumber(
      row?.base_pps ??
      row?.basePps ??
      row?.base_pps_total ??
      row?.basePpsTotal ??
      null
    );
    if (!Number.isFinite(basePps) || basePps <= 0) {
      const rosterPower = commParseNumber(row?.roster_power ?? row?.power ?? row?.clan_power ?? null);
      if (Number.isFinite(rosterPower) && rosterPower > 0) {
        basePps = rosterPower * LIVE_PPS_FACTOR;
      }
    }
    if (!Number.isFinite(basePps) || basePps <= 0) return;
    total += basePps;
    count += 1;
  });
  return count > 0 ? total : null;
}

function updateLiveBoostHourlyStats(baseScorePerHour, boostDeltaTotal) {
  if (!liveBoostHourlyStats) liveBoostHourlyStats = loadLiveBoostHourlyStats();
  const stats = liveBoostHourlyStats;
  if (!stats.hours || typeof stats.hours !== 'object') stats.hours = {};
  const key = buildLiveBoostHourKey();
  const entry = stats.hours[key] || { baseSum: 0, baseSamples: 0, boostSum: 0, updatedAt: null };
  if (Number.isFinite(baseScorePerHour) && baseScorePerHour >= 0) {
    entry.baseSum += baseScorePerHour;
    entry.baseSamples += 1;
  }
  if (Number.isFinite(boostDeltaTotal) && boostDeltaTotal > 0) {
    entry.boostSum += boostDeltaTotal;
  }
  entry.updatedAt = new Date().toISOString();
  stats.hours[key] = entry;
  pruneLiveBoostHourlyStats(stats);
  saveLiveBoostHourlyStats(stats);
}

function recordLiveBoostHourlySample(boostDeltaTotal) {
  const now = Date.now();
  if (now - liveBoostHourlyLastSampleAt < LIVE_BOOST_HOURLY_MIN_INTERVAL_MS) return;
  const basePpsTotal = computeLiveBasePpsTotal();
  const baseScorePerHour = Number.isFinite(basePpsTotal) ? basePpsTotal * 3600 : null;
  updateLiveBoostHourlyStats(baseScorePerHour, boostDeltaTotal);
  liveBoostHourlyLastSampleAt = now;
  syncLiveBoostHourlyStatsToDb().catch(() => {});
}

async function syncLiveBoostHourlyStatsToDb(force = false) {
  if (!HAS_CHROME_RUNTIME) return;
  if (!hasSupabaseSession()) return;
  const ownerId = getHomeAuthUserId();
  if (!ownerId) return;
  const now = Date.now();
  if (!force && now - liveBoostHourlySyncLastAt < LIVE_BOOST_HOURLY_SYNC_MIN_MS) return;
  if (!liveBoostHourlyStats) liveBoostHourlyStats = loadLiveBoostHourlyStats();
  const leagueId = commResolveLiveLeagueId();
  if (!Number.isFinite(leagueId)) return;
  const rows = [];
  const hourKeys = Object.keys(liveBoostHourlyStats.hours || {});
  if (!hourKeys.length) return;
  const sortedKeys = hourKeys
    .map((key) => ({ key, ts: parseLiveBoostHourKey(key)?.getTime() || 0 }))
    .filter((entry) => entry.ts > 0)
    .sort((a, b) => a.ts - b.ts)
    .slice(-LIVE_BOOST_HOURLY_MAX_HOURS)
    .map((entry) => entry.key);
  sortedKeys.forEach((key) => {
    const hourStart = parseLiveBoostHourKey(key);
    if (!hourStart) return;
    const entry = liveBoostHourlyStats.hours[key];
    if (!entry) return;
    const baseAvg = entry.baseSamples > 0 ? entry.baseSum / entry.baseSamples : null;
    const boostSum = Number(entry.boostSum || 0);
    if (!Number.isFinite(baseAvg) && !(Number.isFinite(boostSum) && boostSum > 0)) return;
    const cycleId = getCycleNumber(hourStart);
    if (!Number.isFinite(cycleId)) return;
    const baseScorePerHour = Number.isFinite(baseAvg) ? baseAvg : null;
    const basePpsTotal = Number.isFinite(baseScorePerHour) ? baseScorePerHour / 3600 : null;
    rows.push({
      owner_id: ownerId,
      cycle_id: Math.trunc(cycleId),
      league_id: Math.trunc(leagueId),
      hour_start: hourStart.toISOString(),
      base_pps_total: Number.isFinite(basePpsTotal) ? basePpsTotal : null,
      base_score_per_hour: Number.isFinite(baseScorePerHour) ? baseScorePerHour : null,
      base_samples: Number.isFinite(entry.baseSamples) ? Math.trunc(entry.baseSamples) : null,
      boost_added_total: Number.isFinite(boostSum) ? boostSum : null,
      updated_at: new Date().toISOString()
    });
  });
  if (!rows.length) return;
  await commInsert(COMM_TABLE_LIVE_BOOST_HOURLY, rows, {
    upsert: true,
    onConflict: 'owner_id,cycle_id,league_id,hour_start'
  });
  liveBoostHourlySyncLastAt = now;
}

async function hydrateLiveBoostHourlyStatsFromDb() {
  if (!HAS_CHROME_RUNTIME) return;
  if (!hasSupabaseSession()) return;
  const ownerId = getHomeAuthUserId();
  if (!ownerId) return;
  const leagueId = commResolveLiveLeagueId();
  if (!Number.isFinite(leagueId)) return;
  if (!liveBoostHourlyStats) liveBoostHourlyStats = loadLiveBoostHourlyStats();
  const queryParts = [
    'select=hour_start,base_score_per_hour,base_samples,boost_added_total,base_pps_total',
    `owner_id=eq.${encodeURIComponent(ownerId)}`,
    `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
    'order=hour_start.desc',
    `limit=${Math.max(LIVE_BOOST_HOURLY_MAX_HOURS, 72)}`
  ];
  let rows = [];
  try {
    rows = await commFetch(COMM_TABLE_LIVE_BOOST_HOURLY, queryParts.join('&'));
  } catch (err) {
    return;
  }
  if (!Array.isArray(rows) || !rows.length) return;
  rows.forEach((row) => {
    const hourStart = row?.hour_start ? new Date(row.hour_start) : null;
    if (!hourStart || Number.isNaN(hourStart.getTime())) return;
    const key = buildLiveBoostHourKey(hourStart);
    if (liveBoostHourlyStats.hours?.[key]) return;
    const baseSamples = Number(row.base_samples || 0);
    const baseScorePerHour = commParseNumber(row.base_score_per_hour);
    const boostSum = commParseNumber(row.boost_added_total);
    liveBoostHourlyStats.hours[key] = {
      baseSum: Number.isFinite(baseScorePerHour) ? baseScorePerHour * (baseSamples > 0 ? baseSamples : 1) : 0,
      baseSamples: baseSamples > 0 ? baseSamples : (Number.isFinite(baseScorePerHour) ? 1 : 0),
      boostSum: Number.isFinite(boostSum) ? boostSum : 0,
      updatedAt: row.updated_at || null
    };
  });
}

function commCollectBoostHourlyRows() {
  if (!liveBoostHourlyStats) liveBoostHourlyStats = loadLiveBoostHourlyStats();
  const hours = liveBoostHourlyStats?.hours || {};
  const rows = [];
  Object.entries(hours).forEach(([key, entry]) => {
    const hourStart = parseLiveBoostHourKey(key);
    if (!hourStart) return;
    const boostSum = commParseNumber(entry?.boostSum ?? entry?.boost_added_total ?? entry?.boost_added_total_sum);
  const baseSum = commParseNumber(entry?.baseSum);
  const baseSamples = commParseNumber(entry?.baseSamples);
  const baseAvg = Number.isFinite(baseSamples) && baseSamples > 0 && Number.isFinite(baseSum)
    ? baseSum / baseSamples
    : (Number.isFinite(baseSum) ? baseSum : null);
  rows.push({
    hourStart,
    boostSum: Number.isFinite(boostSum) ? boostSum : 0,
    baseSum: Number.isFinite(baseSum) ? baseSum : null,
    baseSamples: Number.isFinite(baseSamples) ? Math.trunc(baseSamples) : null,
    baseAvg: Number.isFinite(baseAvg) ? baseAvg : null
  });
  });
  rows.sort((a, b) => a.hourStart.getTime() - b.hourStart.getTime());
  return rows;
}

function commFormatBoostUseHour(dateValue) {
  const d = new Date(dateValue);
  if (Number.isNaN(d.getTime())) return '-';
  const datePart = d.toLocaleDateString([], { month: 'short', day: '2-digit' });
  const timePart = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
  return `${datePart} ${timePart}`;
}

function commBuildBoostUseBars(rows, { labelFn, valueFn, metaFn } = {}) {
  if (!Array.isArray(rows) || rows.length === 0) {
    return '<div class="boost-use-empty">No boost usage data.</div>';
  }
  const values = rows.map((row) => Number(valueFn ? valueFn(row) : 0)).filter((val) => Number.isFinite(val));
  const maxValue = values.length ? Math.max(...values) : 0;
  return rows.map((row) => {
    const label = labelFn ? labelFn(row) : '';
    const rawValue = valueFn ? valueFn(row) : 0;
    const value = Number.isFinite(rawValue) ? rawValue : 0;
    const width = maxValue > 0 ? Math.max(0, Math.min(100, Math.round((value / maxValue) * 100))) : 0;
    const meta = metaFn ? metaFn(row) : '';
    const displayValue = Number.isFinite(value) ? formatCount(Math.round(value)) : '-';
    return `
      <div class="boost-use-row">
        <div class="boost-use-label">${commEscape(label)}</div>
        <div class="boost-use-bar"><span class="boost-use-bar-fill" style="width:${width}%"></span></div>
        <div class="boost-use-value">${commEscape(displayValue)}${meta ? ` <span class="boost-use-meta">${commEscape(meta)}</span>` : ''}</div>
      </div>
    `;
  }).join('');
}

function commBuildBoostUseHeatmap(matrix, { maxValue = null } = {}) {
  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  const hours = Array.from({ length: 24 }, (_, idx) => String(idx).padStart(2, '0'));
  const values = [];
  matrix.forEach((row) => row.forEach((cell) => values.push(cell?.value || 0)));
  const resolvedMax = Number.isFinite(maxValue) ? maxValue : Math.max(...values, 0);
  const headerCells = hours.map((hour) => (
    `<div class="boost-use-heatmap-cell boost-use-heatmap-hour">${hour}</div>`
  )).join('');
  const bodyRows = days.map((dayLabel, dayIndex) => {
    const rowCells = matrix[dayIndex].map((cell) => {
      const value = Number.isFinite(cell?.value) ? cell.value : 0;
      const count = Number.isFinite(cell?.count) ? cell.count : 0;
      const intensity = resolvedMax > 0 ? Math.max(0, Math.min(1, value / resolvedMax)) : 0;
      const alpha = 0.08 + intensity * 0.72;
      const title = `${dayLabel} ${String(cell?.hour ?? '').padStart(2, '0')}:00  -  ${formatCount(Math.round(value))} boost  -  ${count}h`;
      return `
        <div class="boost-use-heatmap-cell boost-use-heatmap-value" style="background: rgba(56, 189, 248, ${alpha});" title="${commEscape(title)}"></div>
      `;
    }).join('');
    return `
      <div class="boost-use-heatmap-row">
        <div class="boost-use-heatmap-cell boost-use-heatmap-label">${dayLabel}</div>
        ${rowCells}
      </div>
    `;
  }).join('');
  return `
    <div class="boost-use-heatmap">
      <div class="boost-use-heatmap-row boost-use-heatmap-header">
        <div class="boost-use-heatmap-cell boost-use-heatmap-label"></div>
        ${headerCells}
      </div>
      ${bodyRows}
    </div>
  `;
}

async function commRenderBoostUseGraphs({ hydrate = false } = {}) {
  const status = document.getElementById('boostUseStatus');
  const summary = document.getElementById('boostUseSummary');
  const dayList = document.getElementById('boostUseDayList');
  const heatmap = document.getElementById('boostUseHeatmap');
  const topList = document.getElementById('boostUseTopList');
  const cycleList = document.getElementById('boostUseCycleList');
  const cycleSelect = document.getElementById('boostUseCycleSelect');
  if (!dayList || !cycleList) return;
  if (status) status.textContent = 'Loading busiest hours...';
  if (hydrate) {
    await hydrateLiveBoostHourlyStatsFromDb();
  }
  const rows = commCollectBoostHourlyRows();
  if (!rows.length) {
    if (status) {
      status.textContent = hasSupabaseSession()
        ? 'No boost usage samples yet. Open Live to collect data.'
        : 'Sign in to sync boost usage data.';
    }
    if (summary) summary.textContent = '';
    dayList.innerHTML = '<div class="boost-use-empty">No boost usage data yet.</div>';
    if (heatmap) heatmap.innerHTML = '<div class="boost-use-empty">No boost usage data yet.</div>';
    if (topList) topList.innerHTML = '<div class="boost-use-empty">No boost usage data yet.</div>';
    cycleList.innerHTML = '<div class="boost-use-empty">No boost usage data yet.</div>';
    if (cycleSelect) cycleSelect.innerHTML = '';
    return;
  }
  const start = rows[0].hourStart;
  const end = rows[rows.length - 1].hourStart;
  if (summary) {
    summary.textContent = `Tracking ${rows.length} hour(s) from ${formatDateTimeValue(start)} to ${formatDateTimeValue(end)}.`;
  }

  const dayBuckets = Array.from({ length: 24 }, (_, hour) => ({
    hour,
    total: 0,
    count: 0
  }));
  rows.forEach((row) => {
    const hour = row.hourStart.getHours();
    const bucket = dayBuckets[hour];
    if (!bucket) return;
    bucket.total += row.boostSum || 0;
    bucket.count += 1;
  });
  const dayRows = dayBuckets.map((bucket) => ({
    hour: bucket.hour,
    avg: bucket.count ? bucket.total / bucket.count : 0,
    count: bucket.count
  }));
  dayList.innerHTML = commBuildBoostUseBars(dayRows, {
    labelFn: (row) => `${String(row.hour).padStart(2, '0')}:00`,
    valueFn: (row) => row.avg,
    metaFn: (row) => (row.count ? `${row.count}h` : '')
  });

  if (heatmap) {
    const heatmapMatrix = Array.from({ length: 7 }, (_, day) => (
      Array.from({ length: 24 }, (_, hour) => ({ day, hour, total: 0, count: 0, value: 0 }))
    ));
    rows.forEach((row) => {
      const dayIdx = row.hourStart.getDay();
      const hourIdx = row.hourStart.getHours();
      const cell = heatmapMatrix?.[dayIdx]?.[hourIdx];
      if (!cell) return;
      cell.total += row.boostSum || 0;
      cell.count += 1;
    });
    let maxValue = 0;
    heatmapMatrix.forEach((row) => {
      row.forEach((cell) => {
        cell.value = cell.count ? cell.total / cell.count : 0;
        maxValue = Math.max(maxValue, cell.value);
      });
    });
    heatmap.innerHTML = commBuildBoostUseHeatmap(heatmapMatrix, { maxValue });
  }

  if (topList) {
    const topRows = rows
      .slice()
      .sort((a, b) => b.boostSum - a.boostSum)
      .slice(0, 15);
    topList.innerHTML = commBuildBoostUseBars(topRows, {
      labelFn: (row) => commFormatBoostUseHour(row.hourStart),
      valueFn: (row) => row.boostSum,
      metaFn: () => ''
    });
  }

  const cyclesMap = new Map();
  rows.forEach((row) => {
    const cycleId = getCycleNumber(row.hourStart);
    if (!Number.isFinite(cycleId)) return;
    const key = Math.trunc(cycleId);
    if (!cyclesMap.has(key)) cyclesMap.set(key, []);
    cyclesMap.get(key).push(row);
  });
  const cycleIds = Array.from(cyclesMap.keys()).sort((a, b) => b - a);
  if (cycleSelect) {
    cycleSelect.innerHTML = cycleIds.map((cycleId) => (
      `<option value="${cycleId}">Cycle ${cycleId}</option>`
    )).join('');
    const stored = commParseNumber(localStorage.getItem(BOOST_USE_CYCLE_STORAGE_KEY));
    const defaultId = Number.isFinite(stored) && cycleIds.includes(Math.trunc(stored))
      ? Math.trunc(stored)
      : cycleIds[0];
    cycleSelect.value = String(defaultId);
    localStorage.setItem(BOOST_USE_CYCLE_STORAGE_KEY, String(defaultId));
  }
  const selectedCycle = commParseNumber(cycleSelect?.value ?? cycleIds[0]);
  const cycleRows = cyclesMap.get(Number.isFinite(selectedCycle) ? Math.trunc(selectedCycle) : cycleIds[0]) || [];
  const cycleTop = cycleRows
    .slice()
    .sort((a, b) => b.boostSum - a.boostSum)
    .slice(0, 10);
  cycleList.innerHTML = commBuildBoostUseBars(cycleTop, {
    labelFn: (row) => commFormatBoostUseHour(row.hourStart),
    valueFn: (row) => row.boostSum,
    metaFn: () => ''
  });
  if (status) status.textContent = 'Busiest hours updated.';
}

async function commFetchBoostImpactRounds({ force = false, limit = 350 } = {}) {
  if (!hasSupabaseSession()) return [];
  const now = Date.now();
  if (!force && boostImpactRoundsCache && now - boostImpactRoundsFetchedAt < BOOST_IMPACT_ROUNDS_CACHE_MS) {
    return boostImpactRoundsCache;
  }
  const cycleId = getCycleNumber(new Date());
  const leagueId = commResolveLiveLeagueId();
  const selectCandidates = [
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,time_round_finished,created_at,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,created_at,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,created_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id'
  ];
  const filterVariants = [
    { cycleId, leagueId },
    { cycleId: null, leagueId },
    { cycleId, leagueId: null },
    { cycleId: null, leagueId: null }
  ];
  const limitValue = Math.max(50, limit);

  let rows = [];
  let lastError = null;
  for (const select of selectCandidates) {
    for (const variant of filterVariants) {
      const queryParts = [
        `select=${select}`,
        'order=block_number.desc',
        `limit=${limitValue}`
      ];
      if (Number.isFinite(variant.leagueId)) {
        queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(variant.leagueId))}`);
      }
      if (Number.isFinite(variant.cycleId)) {
        queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(variant.cycleId))}`);
      }
      try {
        rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
        if (Array.isArray(rows) && rows.length > 0) {
          boostImpactRoundsCache = rows;
          boostImpactRoundsFetchedAt = now;
          return boostImpactRoundsCache;
        }
      } catch (err) {
        lastError = err;
        rows = [];
      }
    }
  }
  if (lastError) {
    console.warn('[MW] rounds_cycle_api fetch failed:', lastError?.message || lastError);
  }
  boostImpactRoundsCache = Array.isArray(rows) ? rows : [];
  boostImpactRoundsFetchedAt = now;
  return boostImpactRoundsCache;
}

async function commFetchMultiplierTimeRounds({ force = false, limit = 800, leagueFilter = '', cycleFrom = null, cycleTo = null } = {}) {
  if (!hasSupabaseSession()) return [];
  const cacheKey = `league:${leagueFilter || 'all'}|from:${cycleFrom ?? 'any'}|to:${cycleTo ?? 'any'}|limit:${limit}`;
  const cached = multiplierTimeCache.get(cacheKey);
  const now = Date.now();
  if (!force && cached && now - cached.fetchedAt < MULTIPLIER_TIME_CACHE_MS) {
    return cached.rows;
  }
  const selectCandidates = [
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,time_round_finished,created_at,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,created_at,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,created_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id,updated_at',
    'block_number,multiplier,clan_winner,user_winner,cycle_id,league_id'
  ];
  const limitValue = Math.max(50, Math.min(20000, limit));
  const queryFilters = [];
  if (leagueFilter === 'dune-all') {
    const duneIds = Array.from({ length: CLAN_SELECTION_DUNE_END - CLAN_SELECTION_DUNE_START + 1 }, (_, idx) =>
      CLAN_SELECTION_DUNE_START + idx
    );
    queryFilters.push(`league_id=in.(${duneIds.join(',')})`);
  } else {
    const leagueId = commParseNumber(leagueFilter);
    if (Number.isFinite(leagueId)) {
      queryFilters.push(`league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`);
    }
  }
  if (Number.isFinite(cycleFrom)) {
    queryFilters.push(`cycle_id=gte.${encodeURIComponent(Math.trunc(cycleFrom))}`);
  }
  if (Number.isFinite(cycleTo)) {
    queryFilters.push(`cycle_id=lte.${encodeURIComponent(Math.trunc(cycleTo))}`);
  }
  let rows = [];
  let lastError = null;
  const pageSize = Math.max(200, Math.min(1000, limitValue));
  for (const select of selectCandidates) {
    let offset = 0;
    let aggregated = [];
    let success = false;
    while (true) {
      const queryParts = [
        `select=${select}`,
        ...queryFilters,
        'order=block_number.desc',
        `limit=${pageSize}`,
        `offset=${offset}`
      ];
      try {
        const page = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
        const pageRows = Array.isArray(page) ? page : [];
        if (pageRows.length) aggregated.push(...pageRows);
        success = true;
        if (pageRows.length < pageSize) break;
        offset += pageSize;
        if (offset >= limitValue && limitValue > 0) break;
      } catch (err) {
        lastError = err;
        aggregated = [];
        success = false;
        break;
      }
    }
    if (success && aggregated.length) {
      rows = aggregated;
      break;
    }
  }
  if (lastError) {
    console.warn('[MW] multiplier time fetch failed:', lastError?.message || lastError);
  }
  multiplierTimeCache.set(cacheKey, { rows: Array.isArray(rows) ? rows : [], fetchedAt: now });
  return Array.isArray(rows) ? rows : [];
}

function commGetBoostTotalsForRound(round) {
  if (!round) return null;
  if (round?.clan_winner || round?.user_winner) {
    const abilities = [
      ...commExtractUsedAbilitiesFromWinner(round?.clan_winner),
      ...commExtractUsedAbilitiesFromWinner(round?.user_winner)
    ];
    const totals = commBuildBoostTotalsFromAbilities(abilities);
    if (!totals) return null;
    const sum = Object.values(totals).reduce((acc, value) => {
      const raw = Number(value || 0);
      return acc + (Number.isFinite(raw) ? raw : 0);
    }, 0);
    return { totals, total: Number.isFinite(sum) ? sum : null };
  }
  const clans = normalizeFinishedRoundClans(round);
  if (!Array.isArray(clans) || clans.length === 0) return null;
  const totals = buildBoostTotalsFromClans(clans);
  if (!totals) return null;
  const sum = Object.values(totals).reduce((acc, value) => {
    const raw = Number(value || 0);
    return acc + (Number.isFinite(raw) ? raw : 0);
  }, 0);
  return { totals, total: Number.isFinite(sum) ? sum : null };
}

function commBuildBoostImpactRatioRows(rows) {
  const buckets = Array.from({ length: 24 }, (_, hour) => ({
    hour,
    boostTotal: 0,
    baseTotal: 0,
    samples: 0
  }));
  rows.forEach((row) => {
    const hour = row.hourStart.getHours();
    const bucket = buckets[hour];
    if (!bucket) return;
    const boostSum = Number.isFinite(row.boostSum) ? row.boostSum : 0;
    const baseAvg = Number.isFinite(row.baseAvg) ? row.baseAvg : 0;
    bucket.boostTotal += boostSum;
    bucket.baseTotal += baseAvg;
    bucket.samples += 1;
  });
  return buckets.map((bucket) => {
    const denom = bucket.boostTotal + bucket.baseTotal;
    const ratio = denom > 0 ? (bucket.boostTotal / denom) * 100 : 0;
    return {
      hour: bucket.hour,
      ratio,
      samples: bucket.samples
    };
  });
}

function commBuildMultiplierImpactRows(rounds) {
  const map = new Map();
  (Array.isArray(rounds) ? rounds : []).forEach((round) => {
    const multiplier = parseNumericValue(round?.multiplier);
    if (!Number.isFinite(multiplier)) return;
    const boostTotals = commGetBoostTotalsForRound(round);
    if (!boostTotals || !Number.isFinite(boostTotals.total)) return;
    const key = Number(multiplier);
    const entry = map.get(key) || { sum: 0, count: 0 };
    entry.sum += boostTotals.total;
    entry.count += 1;
    map.set(key, entry);
  });
  return Array.from(map.entries())
    .map(([multiplier, entry]) => ({
      multiplier,
      avg: entry.count ? entry.sum / entry.count : 0,
      count: entry.count
    }))
    .sort((a, b) => a.multiplier - b.multiplier);
}

function commBuildBoostTypeRows(rounds) {
  const totals = new Map();
  (Array.isArray(rounds) ? rounds : []).forEach((round) => {
    const boostTotals = commGetBoostTotalsForRound(round);
    if (!boostTotals?.totals) return;
    Object.entries(boostTotals.totals).forEach(([type, value]) => {
      const raw = Number(value || 0);
      if (!Number.isFinite(raw) || raw <= 0) return;
      totals.set(type, (totals.get(type) || 0) + raw);
    });
  });
  return Array.from(totals.entries())
    .map(([type, total]) => ({
      type,
      total
    }))
    .sort((a, b) => b.total - a.total)
    .slice(0, 10);
}

function commFormatMultiplierLabel(value) {
  const num = Number(value);
  if (!Number.isFinite(num)) return '-';
  const isInt = Math.abs(num - Math.round(num)) < 1e-6;
  return `${isInt ? Math.round(num) : formatDecimal(num, 1)}x`;
}

const MULTIPLIER_COLOR_PALETTE = [
  '#38bdf8',
  '#22d3ee',
  '#818cf8',
  '#a855f7',
  '#ec4899',
  '#f97316',
  '#facc15',
  '#4ade80',
  '#14b8a6',
  '#60a5fa',
  '#fb7185',
  '#f59e0b'
];

const MULTIPLIER_COLOR_MAP = {
  '1x': '#0f172a',
  '2x': '#1e3a8a',
  '4x': '#f472b6',
  '8x': '#38bdf8',
  '16x': '#f59e0b',
  '32x': '#f97316',
  '64x': '#a855f7',
  '128x': '#14b8a6',
  '256x': '#eab308',
  '512x': '#f43f5e',
  '1024x': '#8b5cf6'
};

function commGetMultiplierColor(label) {
  if (!label) return 'rgba(148, 163, 184, 0.4)';
  const str = String(label);
  if (MULTIPLIER_COLOR_MAP[str]) return MULTIPLIER_COLOR_MAP[str];
  let hash = 0;
  for (let i = 0; i < str.length; i += 1) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }
  const index = Math.abs(hash) % MULTIPLIER_COLOR_PALETTE.length;
  return MULTIPLIER_COLOR_PALETTE[index];
}

function commBuildMultiplierTimeRows(rounds) {
  const buckets = Array.from({ length: 24 }, (_, hour) => ({
    hour,
    total: 0,
    counts: new Map()
  }));
  (Array.isArray(rounds) ? rounds : []).forEach((round) => {
    const timeValue = commExtractRoundTimeValue(round);
    const date = timeValue ? new Date(timeValue) : null;
    if (!date || Number.isNaN(date.getTime())) return;
    const hour = date.getHours();
    const multiplier = parseNumericValue(round?.multiplier);
    if (!Number.isFinite(multiplier)) return;
    const bucket = buckets[hour];
    if (!bucket) return;
    bucket.total += 1;
    const key = commFormatMultiplierLabel(multiplier);
    bucket.counts.set(key, (bucket.counts.get(key) || 0) + 1);
  });
  return buckets.map((bucket) => {
    const top = Array.from(bucket.counts.entries())
      .map(([multiplier, count]) => ({ multiplier, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 5);
    const topTotal = top.reduce((sum, entry) => sum + entry.count, 0);
    const otherCount = Math.max(0, bucket.total - topTotal);
    const segments = top.map((entry) => {
      const percent = bucket.total > 0 ? (entry.count / bucket.total) * 100 : 0;
      return {
        label: entry.multiplier,
        count: entry.count,
        percent,
        color: commGetMultiplierColor(entry.multiplier)
      };
    });
    if (otherCount > 0) {
      const percent = bucket.total > 0 ? (otherCount / bucket.total) * 100 : 0;
      segments.push({
        label: 'Other',
        count: otherCount,
        percent,
        color: 'rgba(148, 163, 184, 0.35)'
      });
    }
    return {
      hour: bucket.hour,
      total: bucket.total,
      top,
      segments
    };
  });
}

async function commRenderBoostImpactGraphs({ hydrate = false, force = false } = {}) {
  const status = document.getElementById('boostImpactStatus');
  const ratioList = document.getElementById('boostImpactRatioList');
  const multiplierList = document.getElementById('boostImpactMultiplierList');
  const typeList = document.getElementById('boostImpactTypeList');
  if (!ratioList || !multiplierList || !typeList) return;
  if (status) status.textContent = 'Loading boost impact...';
  if (hydrate) {
    await hydrateLiveBoostHourlyStatsFromDb();
  }
  const hourlyRows = commCollectBoostHourlyRows();
  if (!hourlyRows.length) {
    if (ratioList) ratioList.innerHTML = '<div class="boost-use-empty">No boost usage data yet.</div>';
  } else {
    const ratioRows = commBuildBoostImpactRatioRows(hourlyRows);
    ratioList.innerHTML = commBuildBoostUseBars(ratioRows, {
      labelFn: (row) => `${String(row.hour).padStart(2, '0')}:00`,
      valueFn: (row) => row.ratio,
      metaFn: (row) => (row.samples ? `%  -  ${row.samples}h` : '%')
    });
  }

  if (!hasSupabaseSession()) {
    multiplierList.innerHTML = '<div class="boost-use-empty">Sign in to load impact data.</div>';
    typeList.innerHTML = '<div class="boost-use-empty">Sign in to load impact data.</div>';
    if (status) status.textContent = 'Boost impact needs a Supabase session.';
    return;
  }
  const rounds = await commFetchBoostImpactRounds({ force });
  if (!Array.isArray(rounds) || rounds.length === 0) {
    multiplierList.innerHTML = '<div class="boost-use-empty">No rounds_cycle_api rows found. Check cycle/league filters.</div>';
    typeList.innerHTML = '<div class="boost-use-empty">No rounds_cycle_api rows found. Check cycle/league filters.</div>';
    if (status) status.textContent = 'Boost impact updated (no rounds found).';
    return;
  }

  const multiplierRows = commBuildMultiplierImpactRows(rounds);
  multiplierList.innerHTML = multiplierRows.length
    ? commBuildBoostUseBars(multiplierRows, {
      labelFn: (row) => `${formatDecimal(row.multiplier, 0)}x`,
      valueFn: (row) => row.avg,
      metaFn: (row) => (row.count ? `${row.count} rounds` : '')
    })
    : '<div class="boost-use-empty">No multiplier impact data.</div>';

  const typeRows = commBuildBoostTypeRows(rounds);
  typeList.innerHTML = typeRows.length
    ? commBuildBoostUseBars(typeRows, {
      labelFn: (row) => formatBoostDisplayName(row.type),
      valueFn: (row) => row.total,
      metaFn: () => ''
    })
    : '<div class="boost-use-empty">No boost type data.</div>';

  if (status) status.textContent = 'Boost impact updated.';
}

async function commRenderMultiplierTime({ force = false } = {}) {
  const status = document.getElementById('multiplierTimeStatus');
  const summary = document.getElementById('multiplierTimeSummary');
  const list = document.getElementById('multiplierTimeList');
  const leagueSelect = document.getElementById('multiplierTimeLeague');
  const cycleFromInput = document.getElementById('multiplierTimeCycleFrom');
  const cycleToInput = document.getElementById('multiplierTimeCycleTo');
  const hide1xInput = document.getElementById('multiplierTimeHide1x');
  const hide1xRatioInput = document.getElementById('multiplierTimeHide1xRatio');
  if (!status || !list) return;
  if (!hasSupabaseSession()) {
    status.textContent = 'Sign in to load multiplier data.';
    if (summary) summary.textContent = '';
    list.innerHTML = '<div class="boost-use-empty">No data (sign in required).</div>';
    return;
  }
  const leagueFilter = leagueSelect ? String(leagueSelect.value || '') : (localStorage.getItem(MULTIPLIER_TIME_LEAGUE_KEY) || '');
  const cycleFromValue = cycleFromInput ? commParseNumber(cycleFromInput.value) : commParseNumber(localStorage.getItem(MULTIPLIER_TIME_CYCLE_FROM_KEY));
  const cycleToValue = cycleToInput ? commParseNumber(cycleToInput.value) : commParseNumber(localStorage.getItem(MULTIPLIER_TIME_CYCLE_TO_KEY));
  const hide1x = hide1xInput ? hide1xInput.checked : localStorage.getItem(MULTIPLIER_TIME_HIDE_1X_KEY) === '1';
  const hide1xRatio = hide1xRatioInput ? hide1xRatioInput.checked : localStorage.getItem(MULTIPLIER_TIME_HIDE_1X_RATIO_KEY) === '1';
  status.textContent = 'Loading multiplier per time...';
  const rounds = await commFetchMultiplierTimeRounds({
    force,
    limit: 20000,
    leagueFilter,
    cycleFrom: Number.isFinite(cycleFromValue) ? cycleFromValue : null,
    cycleTo: Number.isFinite(cycleToValue) ? cycleToValue : null
  });
  if (!Array.isArray(rounds) || rounds.length === 0) {
    status.textContent = 'No rounds_cycle_api rows with multiplier data.';
    if (summary) summary.textContent = '';
    list.innerHTML = '<div class="boost-use-empty">No multiplier data yet.</div>';
    return;
  }
  const rows = commBuildMultiplierTimeRows(rounds);
  const totalTagged = rows.reduce((sum, row) => sum + row.total, 0);
  if (!totalTagged) {
    status.textContent = 'Rounds found, but no timestamps in rounds_cycle_api (time_round_finished/created_at/updated_at).';
    if (summary) summary.textContent = '';
    list.innerHTML = '<div class="boost-use-empty">No timestamped rounds available.</div>';
    return;
  }
  const maxTotal = Math.max(...rows.map((row) => row.total), 1);
  const maxSegments = Math.max(...rows.map((row) => {
    const baseSegments = Array.isArray(row.segments) ? row.segments : [];
    const visible = hide1x
      ? baseSegments.filter((seg) => {
        const num = parseNumericValue(seg.label);
        return !(Number.isFinite(num) && Math.abs(num - 1) < 1e-6);
      })
      : baseSegments;
    return visible.length;
  }), 1);
  const segmentWidth = 100 / maxSegments;
  list.innerHTML = rows.map((row) => {
    const label = `${String(row.hour).padStart(2, '0')}:00`;
    const allSegments = (Array.isArray(row.segments) ? row.segments : []).filter((seg) => seg.count > 0);
    const segments = hide1x
      ? allSegments.filter((seg) => {
        const num = parseNumericValue(seg.label);
        return !(Number.isFinite(num) && Math.abs(num - 1) < 1e-6);
      })
      : allSegments;
    const emptyCount = Math.max(0, maxSegments - segments.length);
    const sortedSegments = segments
      .slice()
      .sort((a, b) => {
        const aLabel = String(a.label);
        const bLabel = String(b.label);
        if (aLabel.toLowerCase() === 'other') return 1;
        if (bLabel.toLowerCase() === 'other') return -1;
        const aNum = parseNumericValue(aLabel);
        const bNum = parseNumericValue(bLabel);
        if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
        return aLabel.localeCompare(bLabel);
      });
    const barInner = sortedSegments.map((seg) => {
      const percent = Number.isFinite(seg.percent) ? seg.percent : 0;
      const percentInt = Math.max(0, Math.round(percent));
      const width = Math.max(0, Math.min(100, segmentWidth));
      const percentLabel = `${percentInt}%`;
      const labelText = String(seg.label).toUpperCase();
      const textColor = (String(seg.label).toLowerCase() === '1x' || String(seg.label).toLowerCase() === '2x')
        ? '#f8fafc'
        : '#0f172a';
      const text = `${labelText} - (${seg.count}) - ${percentLabel}`;
      const title = `${labelText}: ${seg.count} (${percentLabel})`;
      return `
        <div class="multiplier-time-segment" style="width:${width}%; background:${seg.color}; color:${textColor};" title="${commEscape(title)}">
          <span>${commEscape(text)}</span>
        </div>
      `;
    }).join('');
    const ratioSegments = allSegments
      .slice()
      .sort((a, b) => {
        const aLabel = String(a.label);
        const bLabel = String(b.label);
        if (aLabel.toLowerCase() === 'other') return 1;
        if (bLabel.toLowerCase() === 'other') return -1;
        const aNum = parseNumericValue(aLabel);
        const bNum = parseNumericValue(bLabel);
        if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
        return aLabel.localeCompare(bLabel);
      });
    let hiddenAmount = 0;
    const basePercents = ratioSegments.map((seg) => {
      const num = parseNumericValue(seg.label);
      const percent = Number.isFinite(seg.percent) ? seg.percent : 0;
      if (hide1xRatio && Number.isFinite(num) && Math.abs(num - 1) < 1e-6) {
        const hidden = Math.min(50, percent);
        hiddenAmount += hidden;
        return Math.max(0, percent - hidden);
      }
      return percent;
    });
    const totalPercent = ratioSegments.reduce((sum, seg) => sum + (Number.isFinite(seg.percent) ? seg.percent : 0), 0);
    const remainingTotal = Math.max(0, totalPercent - hiddenAmount);
    const ratioPieces = ratioSegments.map((seg, idx) => {
      const base = basePercents[idx] || 0;
      const width = remainingTotal > 0 ? (base / remainingTotal) * 100 : 0;
      return { width, color: seg.color };
    }).filter((piece) => piece.width > 0);
    const ratioInner = ratioPieces.map((piece) => {
      const width = Math.max(0, Math.min(100, piece.width));
      return `
        <div class="multiplier-time-ratio-segment" style="width:${width}%; background:${piece.color};"></div>
      `;
    }).join('');
    const emptySegments = emptyCount > 0
      ? `<div class="multiplier-time-segment empty" style="width:${segmentWidth * emptyCount}%;"><span>&nbsp;</span></div>`
      : '';
    const fallbackWidth = maxTotal > 0 ? Math.round((row.total / maxTotal) * 100) : 0;
    return `
      <div class="multiplier-time-row">
        <div class="multiplier-time-label">${commEscape(label)}</div>
        <div class="multiplier-time-stack">
          <div class="multiplier-time-bar">
            ${barInner || `<div class="multiplier-time-segment empty" style="width:${fallbackWidth}%;"><span>-</span></div>`}
            ${barInner ? emptySegments : ''}
          </div>
          <div class="multiplier-time-ratio">
            ${ratioInner}
          </div>
        </div>
      </div>
    `;
  }).join('');
  const timeCandidates = rounds
    .map((round) => commExtractRoundTimeValue(round))
    .map((value) => (value ? new Date(value) : null))
    .filter((date) => date && !Number.isNaN(date.getTime()))
    .sort((a, b) => a.getTime() - b.getTime());
  const start = timeCandidates.length ? timeCandidates[0] : null;
  const end = timeCandidates.length ? timeCandidates[timeCandidates.length - 1] : null;
  if (summary) {
    const leagueLabel = leagueFilter === 'dune-all'
      ? 'Dune (all)'
      : (Number.isFinite(commParseNumber(leagueFilter)) ? commGetLeagueName(leagueFilter) : 'All leagues');
    const cycleLabel = Number.isFinite(cycleFromValue) || Number.isFinite(cycleToValue)
      ? `Cycles ${Number.isFinite(cycleFromValue) ? Math.trunc(cycleFromValue) : '?'} -> ${Number.isFinite(cycleToValue) ? Math.trunc(cycleToValue) : '?'}`
      : 'All cycles';
    summary.textContent = `Rounds used: ${formatCount(rounds.length)} | ${leagueLabel} | ${cycleLabel} | ${start ? formatDateTimeValue(start) : '-'} -> ${end ? formatDateTimeValue(end) : '-'}`;
  }
  status.textContent = 'Multiplier per time updated.';
}

function commLogBoostDebug(clanData, sourceLabel = 'allUsersStateV2') {
  const debugName = clanData?.clanName ?? clanData?.clan_name ?? (clanData?.clanId ? (clanLookup.get(String(clanData.clanId)) || null) : null);
  if (!commShouldDebugClan(debugName)) return;
  const key = commExtractLiveClanKey(clanData) || String(clanData?.clanId || debugName || '');
  const now = Date.now();
  const nowAdded = commParseNumber(
    clanData?.currentAddedScore ??
    clanData?.current_added_score ??
    clanData?.addedScore ??
    clanData?.added_score ??
    null
  );
  const nowBoost = commParseNumber(
    clanData?.activeBoostScore ??
    clanData?.active_boost_score ??
    clanData?.boostScore ??
    clanData?.boost_score ??
    null
  );
  const apiClan = typeof commFindLiveApiClan === 'function' ? commFindLiveApiClan(clanData) : null;
  const basePps = commParseNumber(
    apiClan?.basePoints ??
    apiClan?.base_points ??
    apiClan?.base_pps ??
    clanData?.basePoints ??
    clanData?.base_points ??
    null
  );
  const prev = liveWsBoostDebugByClan.get(key);
  const dtSec = prev?.at ? (now - prev.at) / 1000 : null;
  const timerNow = Number.isFinite(latestRoundTimerSeconds) ? latestRoundTimerSeconds : null;
  const timerDelta = Number.isFinite(timerNow) && Number.isFinite(prev?.timer)
    ? Math.abs(timerNow - prev.timer)
    : null;
  const dtSecUsed = Number.isFinite(timerDelta) && timerDelta > 0 ? timerDelta : dtSec;
  const deltaAdded = Number.isFinite(nowAdded) && Number.isFinite(prev?.added) ? nowAdded - prev.added : null;
  const deltaBoost = Number.isFinite(nowBoost) && Number.isFinite(prev?.boost) ? nowBoost - prev.boost : null;
  const baseIncrement = Number.isFinite(basePps) && Number.isFinite(dtSecUsed)
    ? basePps * dtSecUsed
    : null;
  const extraOverBase = Number.isFinite(deltaAdded) && Number.isFinite(baseIncrement)
    ? deltaAdded - baseIncrement
    : null;
  const extraMinusBoost = Number.isFinite(extraOverBase) && Number.isFinite(deltaBoost)
    ? extraOverBase - deltaBoost
    : null;
  console.log('[MW BOOST DEBUG] ' + sourceLabel, {
    clan: debugName,
    basePps,
    nowAdded,
    nowBoost,
    dtSec,
    timerDelta,
    dtSecUsed,
    deltaAdded,
    deltaBoost,
    baseIncrement,
    extraOverBase,
    extraMinusBoost,
    usedAbilities: Array.isArray(clanData?.usedAbilities) ? clanData.usedAbilities : null
  });
  liveWsBoostDebugByClan.set(key, {
    at: now,
    added: nowAdded,
    boost: nowBoost,
    timer: timerNow
  });
}

function updateLiveWsAddedScores(eventData) {
  if (!Array.isArray(eventData)) return;
  const now = Date.now();
  const nextMap = new Map(liveWsAddedScoreByClan);
  let boostDeltaTotal = 0;
  eventData.forEach((item) => {
    if (!item) return;
    const activeBoost = commParseNumber(
      item.activeBoostScore ??
      item.active_boost_score ??
      null
    );
    const addedRaw = commParseNumber(
      item.currentAddedScore ??
      item.current_added_score ??
      item.addedScore ??
      item.added_score ??
      null
    );
    const added = Number.isFinite(addedRaw) ? addedRaw : 0;
    commLogBoostDebug(item, 'allUsersStateV2');
    if (!Number.isFinite(added)) return;
    const idKey = normalizeClanKey(item);
    const name = item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null;
    const nameKey = name ? normalizeClanKey({ clan_name: name }) : '';
    if (!idKey && !nameKey) return;
    const lookupName = clanLookup.get(String(item.clanId ?? item.clan_id ?? '')) || '';
    const lookupKey = lookupName ? normalizeClanKey({ clan_name: lookupName }) : '';
    const baselineKey = idKey || nameKey || lookupKey || '';
    if (baselineKey && !liveWsAddedScoreBaselineByClan.has(baselineKey)) {
      liveWsAddedScoreBaselineByClan.set(baselineKey, added);
    }
    const baselineValue = baselineKey ? commParseNumber(liveWsAddedScoreBaselineByClan.get(baselineKey)) : null;
    const effectiveAdded = Number.isFinite(baselineValue) ? Math.max(0, added - baselineValue) : added;

    if (idKey) nextMap.set(idKey, effectiveAdded);
    if (nameKey) nextMap.set(nameKey, effectiveAdded);
    if (lookupKey) nextMap.set(lookupKey, effectiveAdded);

    const deltaKey = commExtractLiveClanKey(item) || idKey || nameKey || lookupKey;
    if (deltaKey) {
      const prevAddedScore = liveWsAddedScoreByClan.get(deltaKey);
      if ((Number.isFinite(effectiveAdded) && effectiveAdded > 0) || (Number.isFinite(activeBoost) && activeBoost > 0)) {
        const existingActivation = liveWsBoostActivationByClan.get(deltaKey);
        if ((!Number.isFinite(prevAddedScore) || prevAddedScore <= 0) && !existingActivation) {
          const roundLabel = currentRoundInfo?.timer ?? lastCurrentRoundLivePayload?.timer ?? null;
          const roundSeconds = parseRoundTimerSeconds(roundLabel);
          liveWsBoostActivationByClan.set(deltaKey, {
            at: now,
            roundTimeLabel: roundLabel,
            roundSeconds: Number.isFinite(roundSeconds) ? roundSeconds : null
          });
        }
      }
      const prevAdded = liveBoostHourlyPrevAddedByClan.get(deltaKey);
      if (Number.isFinite(prevAdded)) {
        const delta = effectiveAdded - prevAdded;
        if (Number.isFinite(delta) && delta > 0) {
          boostDeltaTotal += delta;
        }
      }
      liveBoostHourlyPrevAddedByClan.set(deltaKey, effectiveAdded);
    }

    const debugName = name || lookupName || null;
    const shouldDebug = commShouldDebugClan(debugName);
    if (shouldDebug) {
      const key = commExtractLiveClanKey(item) || idKey || nameKey || lookupKey;
      const baseScore = commExtractWsScoreValue(item);
      const prev = key ? liveWsDebugRateByClan.get(key) : null;
      const prevScore = Number.isFinite(prev?.score) ? prev.score : baseScore;
      const prevAt = Number.isFinite(prev?.at) ? prev.at : now;
      const dtSec = (now - prevAt) / 1000;
      const ratePerSec = Number.isFinite(baseScore) && Number.isFinite(prevScore) && dtSec > 0
        ? (baseScore - prevScore) / dtSec
        : null;
      const rawScore = commParseNumber(item?.scoreCurrent ?? item?.score_current ?? item?.score ?? item?.currentScore ?? item?.totalScore ?? null);
      const rawAdded = commParseNumber(item?.currentAddedScore ?? item?.current_added_score ?? item?.addedScore ?? item?.added_score ?? null);
      const rawBoost = commParseNumber(item?.activeBoostScore ?? item?.active_boost_score ?? null);
      const baseFromAdded = (Number.isFinite(rawScore) && Number.isFinite(rawAdded))
        ? rawScore - rawAdded
        : null;
      const baseFromBoost = (Number.isFinite(rawScore) && Number.isFinite(rawBoost))
        ? rawScore - rawBoost
        : null;
      const prevRawScore = commParseNumber(prev?.rawScore);
      const prevRawAdded = commParseNumber(prev?.rawAdded);
      const prevRawBoost = commParseNumber(prev?.rawBoost);
      const scoreDelta = Number.isFinite(rawScore) && Number.isFinite(prevRawScore) ? rawScore - prevRawScore : null;
      const addedDelta = Number.isFinite(rawAdded) && Number.isFinite(prevRawAdded) ? rawAdded - prevRawAdded : null;
      const boostDelta = Number.isFinite(rawBoost) && Number.isFinite(prevRawBoost) ? rawBoost - prevRawBoost : null;
      const baseRateFromAddedDelta = Number.isFinite(scoreDelta) && Number.isFinite(addedDelta) && Number.isFinite(dtSec) && dtSec > 0
        ? (scoreDelta - addedDelta) / dtSec
        : null;
      const baseRateFromBoostDelta = Number.isFinite(scoreDelta) && Number.isFinite(boostDelta) && Number.isFinite(dtSec) && dtSec > 0
        ? (scoreDelta - boostDelta) / dtSec
        : null;
      console.log('[MW WS DEBUG] base score', {
        clan: debugName,
        prevScore,
        prevAt,
        nowScore: baseScore,
        nowAt: now,
        dtSec,
        ratePerSec,
        baseFromAdded,
        baseFromBoost,
        scoreDelta,
        addedDelta,
        boostDelta,
        baseRateFromAddedDelta,
        baseRateFromBoostDelta,
        raw: {
          score: item?.score ?? null,
          currentScore: item?.currentScore ?? item?.scoreCurrent ?? item?.score_current ?? null,
          totalScore: item?.totalScore ?? null,
          basePoints: item?.basePoints ?? item?.base_points ?? null,
          currentAddedScore: item?.currentAddedScore ?? item?.current_added_score ?? null,
          addedScore: item?.addedScore ?? item?.added_score ?? null,
          activeBoostScore: item?.activeBoostScore ?? item?.active_boost_score ?? null
        }
      });
      if (key) {
        liveWsDebugRateByClan.set(key, {
          score: baseScore,
          at: now,
          rawScore,
          rawAdded,
          rawBoost
        });
      }
    }
  });
  liveWsAddedScoreByClan = nextMap;
  liveWsAddedScoreUpdatedAt = Date.now();
  recordLiveBoostHourlySample(boostDeltaTotal);
}

function commUpdateLiveXpathScore(payload) {
  if (!payload) return;
  const scoreValue = commParseNumber(payload.score ?? payload.scoreText ?? null);
  const percentRaw = payload.percent ?? payload.percentage ?? payload.percentText ?? null;
  const percentValue = commParseNumber(percentRaw);
  if (!Number.isFinite(scoreValue) || !Number.isFinite(percentValue) || percentValue <= 0) return;
  if (percentValue > 100) return;
  if (typeof percentRaw === 'string' && !percentRaw.includes('%') && percentValue > 1) return;
  liveXpathScoreValue = scoreValue;
  liveXpathPercentValue = percentValue;
  liveXpathTotalScore = scoreValue / (percentValue / 100);
  liveXpathUpdatedAt = Date.now();
}

function commStoreBasePpsFromWs(wsArray, { blockId, leagueId } = {}) {
  if (!Array.isArray(wsArray) || wsArray.length === 0) return;
  const now = Date.now();
  if (now - liveWsBasePpsLastInsertAt < LIVE_WS_BASE_PPS_MIN_INTERVAL_MS) return;
  const basePpsBlockId = normalizeBlockId(blockId);
  const basePpsCycleId = getCycleNumber(new Date());
  const leagueParsed = commParseNumber(leagueId ?? commKorrelCurrentLeagueId ?? lastLiveSummaryLeague);
  if (!basePpsBlockId || !Number.isFinite(basePpsCycleId) || !Number.isFinite(leagueParsed)) return;
  const blockIdNum = commParseNumber(basePpsBlockId);
  if (!Number.isFinite(blockIdNum)) return;

  const rateMap = new Map(liveWsScoreRateMap);
  const rows = [];
  let skippedNoPps = 0;
  let skippedNoClanId = 0;
  wsArray.forEach((item) => {
    const key = commExtractLiveClanKey(item);
    if (!key) return;
    const score = commExtractWsScoreValue(item);
    if (!Number.isFinite(score)) return;
    const debugName = item?.clanName ?? item?.clan_name ?? item?.name ?? item?.clan?.name ?? null;
    const shouldDebug = commShouldDebugClan(debugName);
    const previous = rateMap.get(key);
    const firstScore = Number.isFinite(previous?.lastScore) ? previous.lastScore : score;
    const firstAt = Number.isFinite(previous?.lastAt) ? previous.lastAt : now;
    const lastScore = score;
    const lastAt = now;
    let ratePerSec = previous?.ratePerSec ?? 0;
    const dtSec = (lastAt - firstAt) / 1000;
    if (Number.isFinite(dtSec) && dtSec > 0) {
      ratePerSec = (lastScore - firstScore) / dtSec;
    }
    if (shouldDebug) {
      console.log('[MW PPS DEBUG] ws', {
        clan: debugName,
        prevScore: firstScore,
        prevAt: firstAt,
        nowScore: lastScore,
        nowAt: lastAt,
        dtSec,
        ratePerSec,
        raw: {
          basePoints: item?.basePoints ?? item?.base_points ?? null,
          score: item?.score ?? null,
          currentScore: item?.currentScore ?? item?.scoreCurrent ?? item?.score_current ?? null,
          totalScore: item?.totalScore ?? null,
          currentAddedScore: item?.currentAddedScore ?? item?.current_added_score ?? null,
          addedScore: item?.addedScore ?? item?.added_score ?? null,
          activeBoostScore: item?.activeBoostScore ?? item?.active_boost_score ?? null
        }
      });
    }
    const ppsRounded = Number.isFinite(ratePerSec) ? Math.round(ratePerSec * 100) / 100 : null;
    const prevRounded = Number.isFinite(previous?.lastPpsRounded) ? previous.lastPpsRounded : null;
    let stableCount = Number.isFinite(previous?.stableCount) ? previous.stableCount : 0;
    if (Number.isFinite(ppsRounded)) {
      stableCount = ppsRounded === prevRounded ? stableCount + 1 : 1;
    }
    const stablePps = stableCount >= 2 && Number.isFinite(ratePerSec) && ratePerSec > 0
      ? ratePerSec
      : (Number.isFinite(previous?.stablePps) ? previous.stablePps : null);
    if (Number.isFinite(stablePps)) {
      liveWsStablePpsByClan.set(key, stablePps);
    }
    rateMap.set(key, {
      firstScore,
      firstAt,
      lastScore,
      lastAt,
      ratePerSec,
      lastPpsRounded: ppsRounded,
      stableCount,
      stablePps
    });
    const ppsSample = Number.isFinite(ratePerSec) && ratePerSec > 0
      ? ratePerSec
      : (Number.isFinite(stablePps) && stablePps > 0 ? stablePps : null);
    if (!Number.isFinite(ppsSample) || ppsSample <= 0) {
      skippedNoPps += 1;
      return;
    }
    const clanId = commResolveClanIdFromItem(item, { silent: true });
    if (!Number.isFinite(clanId)) {
      skippedNoClanId += 1;
      return;
    }
    const clanName = item?.clanName ?? item?.clan_name ?? item?.name ?? item?.clan?.name ?? null;
    const ppsSampleRounded = Math.round(ppsSample * 100) / 100;
    rows.push({
      block_id: Math.trunc(blockIdNum),
      cycle_id: Math.trunc(basePpsCycleId),
      league_id: Math.trunc(leagueParsed),
      clan_id: Math.trunc(clanId),
      clan_name: clanName || null,
      pps_sample: ppsSampleRounded
    });
  });
  liveWsScoreRateMap = rateMap;
  if (!rows.length) {
    console.log('[MW] No WS base PPS samples to store:', {
      cycleId: basePpsCycleId,
      leagueId: Math.trunc(leagueParsed),
      blockId: blockIdNum,
      totalClans: wsArray.length,
      skippedNoPps,
      skippedNoClanId
    });
    return;
  }
  liveWsBasePpsLastInsertAt = now;
  console.log('[MW] Queuing WS base PPS samples:', {
    count: rows.length,
    cycleId: basePpsCycleId,
    leagueId: Math.trunc(leagueParsed),
    blockId: blockIdNum
  });
  commInsertClanBlockPpsSamples(rows)
    .then((inserted) => {
      console.log('[MW] WS base PPS samples stored:', inserted);
    })
    .catch((err) => {
      console.warn('[MW] Failed to store WS base PPS rows:', err);
    });
}

function commResolveClanIdFromItem(item, { silent = false } = {}) {
  if (!item) return null;
  const direct = commParseNumber(item.clanId ?? item.clan_id ?? item.id ?? item.clan?.id ?? null);
  if (Number.isFinite(direct)) return Math.trunc(direct);
  const name = item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null;
  if (!name) return null;
  const resolved = findClanIdByName(name, { silent });
  if (Number.isFinite(resolved)) return Math.trunc(resolved);
  const normalized = normalizeClanName(name);
  if (!normalized) return null;
  for (const [id, entry] of currentRoundClanIds.entries()) {
    const candidateName = entry?.clanName ?? entry?.clan_name ?? entry?.name ?? null;
    if (!candidateName) continue;
    if (normalizeClanName(candidateName) === normalized) {
      const parsed = commParseNumber(id ?? entry?.clanId ?? entry?.clan_id);
      if (Number.isFinite(parsed)) return Math.trunc(parsed);
    }
  }
  return null;
}

function commExtractLiveClanKey(item) {
  if (!item) return '';
  const clanId = item.clanId ?? item.clan_id ?? item.id ?? item.clan?.id ?? null;
  const clanName = item.clanName ?? item.clan_name ?? item.name ?? item.clan?.name ?? null;
  const key = normalizeClanKey({ clan_id: clanId, clan_name: clanName });
  return key || '';
}

function commExtractLiveScoreValue(item) {
  const scoreRaw = item?.score ?? item?.currentScore ?? item?.totalScore ?? null;
  const scoreParsed = commParseNumber(scoreRaw);
  const addedRaw = item?.currentAddedScore ?? item?.current_added_score ?? item?.addedScore ?? item?.added_score ?? null;
  const addedParsed = commParseNumber(addedRaw);
  if (Number.isFinite(scoreParsed) && Number.isFinite(addedParsed) && addedParsed > 0) {
    const baseCandidate = scoreParsed - addedParsed;
    if (Number.isFinite(baseCandidate)) return baseCandidate;
  }
  const boostRaw = item?.activeBoostScore ?? item?.active_boost_score ?? item?.boostScore ?? item?.boost_score ?? null;
  const boostParsed = commParseNumber(boostRaw);
  if (Number.isFinite(scoreParsed) && Number.isFinite(boostParsed) && boostParsed > 0) {
    const baseCandidate = scoreParsed - boostParsed;
    if (Number.isFinite(baseCandidate)) return baseCandidate;
  }
  if (Number.isFinite(scoreParsed)) return scoreParsed;
  return null;
}

function commExtractWsScoreValue(item) {
  const scoreRaw = item?.scoreCurrent ?? item?.score_current ?? item?.score ?? item?.currentScore ?? item?.totalScore ?? null;
  const scoreParsed = commParseNumber(scoreRaw);
  const addedRaw = item?.currentAddedScore ?? item?.current_added_score ?? item?.addedScore ?? item?.added_score ?? null;
  const addedParsed = commParseNumber(addedRaw);
  if (Number.isFinite(scoreParsed) && Number.isFinite(addedParsed) && addedParsed > 0) {
    const baseCandidate = scoreParsed - addedParsed;
    if (Number.isFinite(baseCandidate)) return baseCandidate;
  }
  const boostRaw = item?.activeBoostScore ?? item?.active_boost_score ?? item?.boostScore ?? item?.boost_score ?? null;
  const boostParsed = commParseNumber(boostRaw);
  if (Number.isFinite(scoreParsed) && Number.isFinite(boostParsed) && boostParsed > 0) {
    const baseCandidate = scoreParsed - boostParsed;
    if (Number.isFinite(baseCandidate)) return baseCandidate;
  }
  if (Number.isFinite(scoreParsed)) return scoreParsed;
  return null;
}

function buildPlayerSnapshotFromState(state, limit = 10) {
  if (!Array.isArray(state)) return [];
  const rows = state.map((player) => {
    const name = player?.userAlias ?? player?.playerName ?? player?.userName ?? player?.username ?? player?.name ?? null;
    const clanId = player?.clanId ?? player?.clan_id ?? null;
    const addedScore = player?.currentAddedScore ?? player?.current_added_score ?? player?.currentScore ?? player?.score ?? player?.basePoints ?? null;
    const activeBoostScore = player?.activeBoostScore ?? player?.active_boost_score ?? player?.activeBoost ?? null;
    const thPerS = player?.userPower ?? player?.power ?? player?.hashrate ?? null;
    const percentage = player?.chanceToMine ?? player?.userChanceToMine ?? player?.share ?? player?.pct ?? null;
    return {
      player_name: name,
      clan_id: clanId,
      current_added_score: addedScore,
      active_boost_score: activeBoostScore,
      th_per_s: thPerS,
      percentage
    };
  });
  rows.sort((a, b) => {
    const aScore = Number(a.current_added_score) || 0;
    const bScore = Number(b.current_added_score) || 0;
    return bScore - aScore;
  });
  return rows.slice(0, limit).map((row, idx) => ({ rank: idx + 1, ...row }));
}

function buildTopClansWithBoostsFromState(state, limit = 10) {
  if (!Array.isArray(state)) return [];
  const rows = state.map((clan) => {
    const clanId = clan?.clanId ?? clan?.clan_id ?? null;
    const clanName = clan?.clanName ?? clan?.clan_name ?? (clanId ? (clanLookup.get(String(clanId)) || null) : null);
    const addedScore = clan?.currentAddedScore ?? clan?.current_added_score ?? null;
    const activeBoostScore = clan?.activeBoostScore ?? clan?.active_boost_score ?? null;
    const usedAbilities = Array.isArray(clan?.usedAbilities) ? clan.usedAbilities : [];
    const hasAbility = usedAbilities.some((ability) => Number(ability?.count || 0) > 0);
    const boostSignal = (Number(activeBoostScore || 0) > 0) || hasAbility;
    const boosts = {};
    let totalBoosts = 0;
    if (boostSignal && usedAbilities.length) {
      usedAbilities.forEach((ability) => {
        const abilityId = ability?.nftGameAbilityId ?? null;
        const boostType = abilityId ? ABILITY_ID_TO_TYPE[abilityId] : null;
        const count = Number(ability?.count || 0);
        if (boostType && Number.isFinite(count) && count > 0) {
          boosts[boostType] = (boosts[boostType] || 0) + count;
          totalBoosts += count;
        }
      });
    }
    return {
      name: clanName,
      clan_id: clanId,
      current_added_score: addedScore,
      active_boost_score: activeBoostScore,
      clan_power: clan?.clanPower ?? clan?.clan_power ?? null,
      boosts,
      total_boosts: totalBoosts
    };
  });
  rows.sort((a, b) => {
    const aBoosts = Number(a.total_boosts) || 0;
    const bBoosts = Number(b.total_boosts) || 0;
    if (bBoosts !== aBoosts) return bBoosts - aBoosts;
    const aActive = Number(a.active_boost_score) || 0;
    const bActive = Number(b.active_boost_score) || 0;
    if (bActive !== aActive) return bActive - aActive;
    const aScore = Number(a.current_added_score) || 0;
    const bScore = Number(b.current_added_score) || 0;
    return bScore - aScore;
  });
  return rows.slice(0, limit);
}

function buildTopPlayersWithBoostsFromSnapshot(snapshot, limit = 10) {
  if (!Array.isArray(snapshot)) return [];
  return snapshot.slice(0, limit).map((player) => ({
    name: player.player_name ?? player.name ?? null,
    th_per_s: player.th_per_s ?? null,
    percentage: player.percentage ?? null,
    boosts: {},
    total_boosts: 0
  }));
}

function normalizeBlockId(value) {
  if (value == null) return null;
  const raw = String(value).trim();
  const normalized = raw.replace(/[^0-9]/g, '');
  if (!normalized) return null;
  return normalized;
}

function getMostRecentTuesdayStartUTC(fromDate = new Date()) {
  const d = new Date(fromDate.getTime());
  const day = d.getUTCDay(); // 0=Sun,1=Mon,2=Tue
  const diffDays = (day - 2 + 7) % 7;
  d.setUTCDate(d.getUTCDate() - diffDays);
  d.setUTCHours(0, 0, 0, 0);
  return d;
}

function getCurrentCycleRosterStartUTC(fromDate = new Date()) {
  const d = getMostRecentTuesdayStartUTC(fromDate);
  d.setUTCHours(5, 0, 0, 0);
  return d;
}

const BASE_CYCLE_NUMBER = 123;
const BASE_CYCLE_START_UTC = new Date(Date.UTC(2025, 11, 23, 0, 0, 0, 0));

function getCycleNumber(dateInput) {
  const dt = dateInput ? new Date(dateInput) : new Date();
  if (Number.isNaN(dt.getTime())) return null;
  const diffMs = dt.getTime() - BASE_CYCLE_START_UTC.getTime();
  const weekMs = 7 * 24 * 60 * 60 * 1000;
  const offsetWeeks = Math.floor(diffMs / weekMs);
  return BASE_CYCLE_NUMBER + offsetWeeks;
}

function extractBlockIdFromPopupHtml(html) {
  if (!html) return null;
  const str = String(html);
  const withComma = str.match(/\b(\d{3}[,\s]\d{3})\b/);
  if (withComma && withComma[1]) {
    return normalizeBlockId(withComma[1]);
  }
  const plain = str.match(/\b(\d{6})\b/);
  if (plain && plain[1]) {
    return normalizeBlockId(plain[1]);
  }
  return null;
}

function parsePopupHtmlRoundData(html) {
  if (!html) return null;
  try {
    const doc = new DOMParser().parseFromString(String(html), 'text/html');

    const getText = (selector) => doc.querySelector(selector)?.textContent?.trim() || null;
    const readBadgeValue = (badgeTag) => {
      const icon = doc.querySelector(badgeTag);
      if (!icon) return null;
      const sibling = icon.parentElement?.querySelector('span');
      return sibling ? sibling.textContent.trim() : null;
    };

    const roundId = readBadgeValue('icon-circle-block') || getText('a.miner-wars-main__info--left span:nth-child(2)');
    const roundIdNormalized = normalizeBlockId(roundId);
    const multiplier = readBadgeValue('icon-circle-multiplier') || getText('.miner-wars-main__info--left .miner-wars-main__multiplier span');
    const timer = readBadgeValue('icon-circle-time') || getText('.miner-wars-main__timer');

    const clanInfo = doc.querySelector('.modal-body .participant-info');
    const playerInfo = doc.querySelector('.modal-body .participant-info ~ .participant-info');

    const htmlStr = doc.documentElement?.outerHTML || '';
    let clanId = null;
    const clanAttr = doc.querySelector('[data-clan-id],[data-clanid],[data-clan]');
    if (clanAttr) {
      const raw = clanAttr.getAttribute('data-clan-id')
        || clanAttr.getAttribute('data-clanid')
        || clanAttr.getAttribute('data-clan');
      clanId = normalizeBlockId(raw);
    }
    if (!clanId) {
      const patterns = [
        /data-clan-id\s*=\s*["'](\d+)["']/i,
        /data-clanid\s*=\s*["'](\d+)["']/i,
        /"clan_id"\s*:\s*(\d+)/i,
        /"clanId"\s*:\s*(\d+)/i,
        /clanId\s*[:=]\s*["']?(\d+)["']?/i,
        /clan_id\s*[:=]\s*["']?(\d+)["']?/i,
        /\/clan\/(\d+)/i
      ];
      for (const re of patterns) {
        const match = htmlStr.match(re);
        if (match && match[1]) {
          const candidate = normalizeBlockId(match[1]);
          if (candidate && candidate !== roundIdNormalized) {
            clanId = candidate;
            break;
          }
        }
      }
    }

    const data = {
      block_id: roundId,
      multiplier,
      round_time: timer,
      clan_name: clanInfo?.querySelector('.participant-info__main > p')?.textContent?.trim() || null,
      clan_id: clanId,
      winner_player_name: playerInfo?.querySelector('.participant-info__main > p')?.textContent?.trim() || null,
      send_reward_fund: getText('.modal-card-inner .btn-game-green'),
      clan_th_per_s: clanInfo?.querySelector('.participant-info__hashrate')?.textContent?.trim() || null,
      clan_percentage: clanInfo?.querySelector('.participant-info__share')?.textContent?.trim() || null,
      player_th_per_s: playerInfo?.querySelector('.participant-info__hashrate')?.textContent?.trim() || null,
      player_percentage: playerInfo?.querySelector('.participant-info__share')?.textContent?.trim() || null,
      player_minebox: playerInfo?.querySelector('.participant-info__minebox')?.textContent?.trim() || null,
      winner_reward_gmt: playerInfo?.querySelector('.participant-info__reward')?.textContent?.trim() || null
    };

    return data;
  } catch (err) {
    console.warn('[MW] Failed to parse popup HTML:', err);
    return null;
  }
}

// Temporary debug counter: when >0, the next N boost events will be logged verbosely to the console
let TEMP_BOOST_DEBUG_REMAINING = 0;
// Console helper: enable in the browser console like `__enableBoostDebug(100)`
window.__enableBoostDebug = (n = 100) => {
  TEMP_BOOST_DEBUG_REMAINING = Number(n) || 0;
  console.log('[MW] Temporary boost debug enabled for', TEMP_BOOST_DEBUG_REMAINING, 'events');
};

window.__setPpsDebugClan = (name) => {
  commSetBoostDebugClan(name);
  console.log('[MW] PPS debug clan set to', livePpsDebugClan || '(disabled)');
};

window.__showPpsDebugClan = () => livePpsDebugClan;

window.__forceAutoClan = () => commEnsureAutoClan({ force: true });
window.__autoClanStatus = async () => {
  const info = await commResolveCurrentClanInfo();
  const userName = await commResolveClanMemberName();
  return {
    clanInfo: info,
    userName,
    liveMyClanName,
    liveMyClanId,
    sessionClan: commSession?.clanName || null
  };
};

// Diagnostic helpers: collect encountered payload shapes for quick inspection in console.
// Usage:
//   __showBoostShapes() -> shows counts and key lists (console.table)
//   __dumpBoostShapeSamples(i) -> prints raw examples for shape index i
//   __clearBoostShapes() -> clears collected shapes
const BOOST_PAYLOAD_SHAPES = new Map();
function recordBoostShape(data) {
  try {
    if (!data || typeof data !== 'object') return;
    const topKeys = Object.keys(data).sort();
    const nestedKeys = data.data && typeof data.data === 'object' ? Object.keys(data.data).sort() : [];
    const shapeKey = JSON.stringify({ topKeys, nestedKeys });
    const entry = BOOST_PAYLOAD_SHAPES.get(shapeKey) || { count: 0, examples: [] };
    entry.count += 1;
    if (entry.examples.length < 5) entry.examples.push(data);
    BOOST_PAYLOAD_SHAPES.set(shapeKey, entry);
  } catch (err) { /* ignore */ }
}
window.__showBoostShapes = (opts = {}) => {
  try {
    const arr = Array.from(BOOST_PAYLOAD_SHAPES.entries()).map(([k, v], idx) => {
      const parsed = JSON.parse(k);
      return { index: idx, count: v.count, keys: parsed.topKeys.join(', '), nested: parsed.nestedKeys.join(', '), exampleCount: v.examples.length };
    }).sort((a,b) => b.count - a.count);
    console.table(arr.slice(0, opts.limit || 100));
    return arr;
  } catch (e) { console.warn('[MW] __showBoostShapes failed', e); return []; }
};
window.__dumpBoostShapeSamples = (indexOrKey = 0) => {
  try {
    const entries = Array.from(BOOST_PAYLOAD_SHAPES.entries()).map(([k, v]) => ({ key: k, v }));
    if (typeof indexOrKey === 'number') {
      const e = entries[indexOrKey];
      if (e) {
        console.log('Shape', indexOrKey, JSON.parse(e.key), e.v);
        e.v.examples.forEach((ex, i) => console.log('Example', i, ex));
      } else console.warn('No shape at index', indexOrKey);
    } else {
      const e = BOOST_PAYLOAD_SHAPES.get(indexOrKey);
      if (e) {
        console.log('Shape', indexOrKey, JSON.parse(indexOrKey), e);
        e.examples.forEach((ex, i) => console.log('Example', i, ex));
      } else console.warn('No shape for key', indexOrKey);
    }
  } catch (err) { console.warn('[MW] __dumpBoostShapeSamples failed', err); }
};
window.__clearBoostShapes = () => { BOOST_PAYLOAD_SHAPES.clear(); console.log('[MW] Cleared boost shapes'); };

// Quick audit: compare basePoints against computed PPS for top clans.
// Usage: __auditBasePoints(20)
window.__auditBasePoints = (limit = 20) => {
  try {
    const rows = Array.isArray(liveApiStateRaw) ? liveApiStateRaw : [];
    if (!rows.length) {
      console.warn('[MW] __auditBasePoints: no live API rows');
      return [];
    }
    const mapped = rows.map((item) => {
      const key = commExtractLiveClanKey(item);
      const name = item?.clanName ?? item?.clan_name ?? item?.name ?? item?.clan?.name ?? null;
      const basePoints = commParseNumber(item?.basePoints ?? item?.base_points ?? null);
      const rate = key ? liveApiScoreRateMap.get(key) : null;
      const stable = rate && Number.isFinite(rate.stablePps) ? rate.stablePps : null;
      const pps = Number.isFinite(stable) ? stable : (Number.isFinite(rate?.ratePerSec) ? rate.ratePerSec : null);
      const diffPct = Number.isFinite(basePoints) && Number.isFinite(pps) && basePoints !== 0
        ? ((pps - basePoints) / basePoints) * 100
        : null;
      return {
        clan: name,
        basePoints,
        pps,
        diffPct,
        score: commParseNumber(item?.score ?? item?.currentScore ?? item?.totalScore ?? null),
        currentAddedScore: commParseNumber(item?.currentAddedScore ?? item?.current_added_score ?? null)
      };
    }).filter((row) => row.clan);
    const sorted = mapped
      .filter((row) => Number.isFinite(row.basePoints) || Number.isFinite(row.pps))
      .sort((a, b) => {
        const da = Number.isFinite(a.diffPct) ? Math.abs(a.diffPct) : Infinity;
        const db = Number.isFinite(b.diffPct) ? Math.abs(b.diffPct) : Infinity;
        return da - db;
      })
      .slice(0, Math.max(1, Number(limit) || 20));
    console.table(sorted);
    return sorted;
  } catch (err) {
    console.warn('[MW] __auditBasePoints failed', err);
    return [];
  }
};

// Recent processed boost events log (keeps last N entries)
const BOOST_EVENT_LOG = [];
function recordBoostEvent(evt) {
  try {
    BOOST_EVENT_LOG.unshift(evt);
    if (BOOST_EVENT_LOG.length > 500) BOOST_EVENT_LOG.length = 500;
  } catch (e) { /* ignore */ }
}
function clearBoostEventLog() {
  try {
    BOOST_EVENT_LOG.length = 0;
  } catch (_) { /* ignore */ }
}
window.__dumpBoostEventLog = (limit = 20) => { const slice = BOOST_EVENT_LOG.slice(0, limit); console.log('[MW] Boost event log dump:', slice); return slice; };
window.__showBoostEventLog = (limit = 50) => { console.table(BOOST_EVENT_LOG.slice(0, limit).map((e) => ({ ts: new Date(e.ts).toISOString(), dedupeKey: e.dedupeKey, boostType: e.boostType, multKey: e.selectedMultiplierKey, mult: e.multiplier, countKey: e.selectedCountKey, count: e.count, effect: e.effect }))); return BOOST_EVENT_LOG; };

// Map of known clanId -> clanName (populated from incoming events and DB lookups)
const clanLookup = new Map();

function normalizeClanKey(entry) {
  if (!entry) return '';
  const id = commParseNumber(entry?.clan_id ?? entry?.clanId);
  if (Number.isFinite(id)) return `id:${Math.trunc(id)}`;
  const name = entry?.clan_name || entry?.name || entry?.clan || '';
  return name ? `name:${String(name).trim().toLowerCase()}` : '';
}

function isSimClanName(name) {
  if (!name) return false;
  const normalized = String(name).trim().toLowerCase();
  return normalized === 'calculator sim'
    || normalized === 'sim calculator'
    || normalized === 'sim'
    || normalized === 'calculator_sim';
}

function buildRosterEntryFromRow(row) {
  if (!row) return null;
  const rosterPower = commParseNumber(row?.clan_power ?? row?.clanPower);
  return {
    clan_id: row?.clan_id ?? row?.clanId ?? null,
    clan_name: row?.clan_name ?? row?.name ?? null,
    clan_power: Number.isFinite(rosterPower) ? rosterPower : null,
    roster_power: Number.isFinite(rosterPower) ? rosterPower : null,
    current_added_score: 0,
    active_boost_score: 0,
    used_abilities_total: 0,
    boosts: {},
    roster_only: true
  };
}

function sanitizeIlikeValue(value) {
  return String(value || '').replace(/[%_*]/g, '').trim();
}

function commResolveLiveLeagueId() {
  const direct = commParseNumber(commKorrelCurrentLeagueId);
  if (Number.isFinite(direct)) return Math.trunc(direct);
  const candidates = [
    lastLiveSummaryLeague,
    lastCurrentRoundLivePayload?.leagueId,
    lastCurrentRoundLivePayload?.league
  ];
  for (const candidate of candidates) {
    const parsed = commParseNumber(candidate);
    if (Number.isFinite(parsed)) return Math.trunc(parsed);
    if (typeof candidate === 'string') {
      const labelParsed = commParseLeagueLabel(candidate);
      if (Number.isFinite(labelParsed)) return Math.trunc(labelParsed);
    }
  }
  const sessionLeague = commParseNumber(commSession?.leagueId);
  return Number.isFinite(sessionLeague) ? Math.trunc(sessionLeague) : null;
}

async function refreshLiveRosterTotals(cycleId, leagueId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return;
  const now = Date.now();
  if (
    liveRosterTotalsPending ||
    (liveRosterTotals &&
      liveRosterTotals.cycleId === Math.trunc(cycleId) &&
      liveRosterTotals.leagueId === Math.trunc(leagueId) &&
      liveRosterTotals.updatedAt &&
      now - liveRosterTotals.updatedAt < LIVE_ROSTER_TOTALS_TTL_MS)
  ) {
    return;
  }
  liveRosterTotalsPending = true;
  try {
    const pageSize = 1000;
    let offset = 0;
    let totalPower = 0;
    let totalRows = 0;
    while (true) {
      const query = [
        'select=clan_power',
        `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
        `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
        'clan_power=not.is.null',
        `limit=${pageSize}`,
        `offset=${offset}`
      ].join('&');
      const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, query);
      if (!Array.isArray(rows) || rows.length === 0) break;
      rows.forEach((row) => {
        const powerValue = commParseNumber(row?.clan_power);
        if (Number.isFinite(powerValue)) totalPower += powerValue;
      });
      totalRows += rows.length;
      if (rows.length < pageSize) break;
      offset += pageSize;
    }
    liveRosterTotals = {
      cycleId: Math.trunc(cycleId),
      leagueId: Math.trunc(leagueId),
      totalPower,
      totalRows,
      updatedAt: now
    };
  } catch (err) {
    console.warn('[MW] Failed to load roster totals:', err);
  } finally {
    liveRosterTotalsPending = false;
  }
}

async function refreshLiveRosterCache({ force = false, limit = null } = {}) {
  if (liveRosterPending) return;
  let leagueId = commResolveLiveLeagueId();
  const cycleId = getCycleNumber(new Date());
  const now = Date.now();
  const storedProfileName = getStoredLiveProfileClan() || liveProfileClanName || '';
  const storedName = liveMyClanName
    || commSession?.clanName
    || (!isSimClanName(storedProfileName) ? storedProfileName : '')
    || '';
  const storedId = commParseNumber(liveMyClanId ?? commSession?.clanId);
  let inferredClanRow = null;

  if (!Number.isFinite(leagueId) && Number.isFinite(cycleId) && (Number.isFinite(storedId) || storedName)) {
    try {
      if (Number.isFinite(storedId)) {
        const idQuery = [
          'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
          `clan_id=eq.${encodeURIComponent(Math.trunc(storedId))}`,
          `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
          'order=fetched_at.desc',
          'limit=1'
        ];
        const idRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, idQuery.join('&'));
        if (Array.isArray(idRows) && idRows.length) inferredClanRow = idRows[0];
      }
      if (!inferredClanRow && storedName) {
        const cleaned = sanitizeIlikeValue(storedName);
        if (cleaned) {
          const nameQuery = [
            'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
            `clan_name=ilike.*${encodeURIComponent(cleaned)}*`,
            `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
            'order=fetched_at.desc',
            'limit=1'
          ];
          const nameRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, nameQuery.join('&'));
          if (Array.isArray(nameRows) && nameRows.length) inferredClanRow = nameRows[0];
        }
      }
      if (inferredClanRow) {
        const inferredLeague = commParseNumber(inferredClanRow?.league_id ?? inferredClanRow?.leagueId);
        if (Number.isFinite(inferredLeague)) leagueId = Math.trunc(inferredLeague);
      }
    } catch (_) {
      // ignore inference failures
    }
  }

  if (!Number.isFinite(leagueId) || !Number.isFinite(cycleId)) return;
  if (!force && liveRosterFetchedAt && now - liveRosterFetchedAt < 60000 &&
      liveRosterLeagueId === Math.trunc(leagueId) && liveRosterCycleId === Math.trunc(cycleId)) {
    return;
  }
  liveRosterPending = true;
  try {
      const limitValue = Math.max(3, Number(limit) || 5);
      const powerLimit = Math.max(limitValue * 10, 50);
      const queryParts = [
        'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
        `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
        `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
        'clan_power=not.is.null',
        'order=clan_power.desc',
        `limit=${powerLimit}`
      ];
      let rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, queryParts.join('&'));
      liveRosterBaseLeaders = Array.isArray(rows) ? rows : [];

    if (!liveRosterBaseLeaders.length) {
        const fallbackQuery = [
          'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
          `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
          'clan_power=not.is.null',
          'order=cycle_id.desc,fetched_at.desc',
          `limit=${powerLimit}`
        ];
      rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, fallbackQuery.join('&'));
      liveRosterBaseLeaders = Array.isArray(rows) ? rows : [];
    }

    let myClanRow = inferredClanRow;
    if (Number.isFinite(storedId)) {
      const idQuery = [
        'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
        `clan_id=eq.${encodeURIComponent(Math.trunc(storedId))}`,
        `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
        'order=fetched_at.desc',
        'limit=1'
      ];
      const idRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, idQuery.join('&'));
      if (Array.isArray(idRows) && idRows.length) myClanRow = idRows[0];
    }
    if (!myClanRow && Number.isFinite(storedId)) {
      const idFallbackQuery = [
        'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
        `clan_id=eq.${encodeURIComponent(Math.trunc(storedId))}`,
        'order=cycle_id.desc,fetched_at.desc',
        'limit=1'
      ];
      const idRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, idFallbackQuery.join('&'));
      if (Array.isArray(idRows) && idRows.length) myClanRow = idRows[0];
    }
    if (!myClanRow && storedName) {
      const cleaned = sanitizeIlikeValue(storedName);
      if (cleaned) {
        const nameQuery = [
          'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
          `clan_name=ilike.*${encodeURIComponent(cleaned)}*`,
          `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`,
          'order=fetched_at.desc',
          'limit=1'
        ];
        const nameRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, nameQuery.join('&'));
        if (Array.isArray(nameRows) && nameRows.length) myClanRow = nameRows[0];
      }
    }
    if (!myClanRow && storedName) {
      const cleaned = sanitizeIlikeValue(storedName);
      if (cleaned) {
        const nameFallbackQuery = [
          'select=clan_id,clan_name,clan_power,league_id,cycle_id,fetched_at',
          `clan_name=ilike.*${encodeURIComponent(cleaned)}*`,
          'order=cycle_id.desc,fetched_at.desc',
          'limit=1'
        ];
        const nameRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, nameFallbackQuery.join('&'));
        if (Array.isArray(nameRows) && nameRows.length) myClanRow = nameRows[0];
      }
    }
    liveRosterMyClan = myClanRow;
    if (myClanRow && Array.isArray(liveRosterBaseLeaders)) {
      const targetId = commParseNumber(myClanRow?.clan_id ?? myClanRow?.clanId);
      const targetName = String(myClanRow?.clan_name ?? myClanRow?.name ?? '').trim().toLowerCase();
      const alreadyListed = liveRosterBaseLeaders.some((row) => {
        const rowId = commParseNumber(row?.clan_id ?? row?.clanId);
        if (Number.isFinite(targetId) && Number.isFinite(rowId)) {
          return Math.trunc(targetId) === Math.trunc(rowId);
        }
        const rowName = String(row?.clan_name ?? row?.name ?? '').trim().toLowerCase();
        return targetName && rowName === targetName;
      });
      if (!alreadyListed) {
        liveRosterBaseLeaders = [myClanRow, ...liveRosterBaseLeaders];
      }
    }
  } catch (err) {
    // ignore roster cache failures
  } finally {
    liveRosterFetchedAt = now;
    liveRosterPending = false;
    liveRosterLeagueId = Math.trunc(leagueId);
    liveRosterCycleId = Math.trunc(cycleId);
    refreshLiveRosterTotals(cycleId, leagueId).catch(() => {});
    if (typeof window.__refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
      window.__refreshCurrentRoundCard();
    }
  }
}

if (typeof window !== 'undefined') {
  window.refreshLiveRosterCache = refreshLiveRosterCache;
}

function findClanIdByName(clanName, { silent = false } = {}) {
  if (!clanName) return null;

  for (const [id, name] of clanLookup.entries()) {
    if (name === clanName) {
      if (!silent) {
        console.log('[MW] Found clan_id in clanLookup:', id, clanName);
      }
      return parseInt(id);
    }
  }

  const normalizedTarget = normalizeClanName(clanName);
  if (normalizedTarget) {
    for (const [id, name] of clanLookup.entries()) {
      if (normalizeClanName(name) === normalizedTarget) {
        if (!silent) {
          console.log('[MW] Found clan_id by normalized name match:', id, clanName);
        }
        return parseInt(id);
      }
    }
  }

  if (!silent) {
    console.log('[MW] Clan name not found in clanLookup:', clanName);
    console.log('[MW] Current round has', currentRoundClanIds.size, 'clans tracked by ID');
    console.log('[MW] Tracked clan IDs:', Array.from(currentRoundClanIds.keys()));
  }
  return null;
}

if (typeof window !== 'undefined') {
  window.findClanIdByName = findClanIdByName;
}

function parseClanMatch(val) {
  if (val == null) return null;
  // handle object shapes
  if (typeof val === 'object') {
    const clanId = val.clan_id ?? val.clanId ?? val.id ?? null;
    const clanName = val.clan_name ?? val.clanName ?? val.name ?? null;
    return { clanId: clanId != null ? String(clanId) : null, clanName: clanName != null ? String(clanName) : null };
  }
  const s = String(val).trim();
  // Pattern: Name (12345)
  let m = s.match(/^(.*?)\s*\(\s*(\d+)\s*\)\s*$/);
  if (m) return { clanId: m[2], clanName: m[1].trim() };
  // Pattern: id:name or name:id separated by : or -
  const parts = s.split(/[:\-]/).map(p => p.trim()).filter(Boolean);
  if (parts.length === 2) {
    if (/^\d+$/.test(parts[0])) return { clanId: parts[0], clanName: parts[1] };
    if (/^\d+$/.test(parts[1])) return { clanId: parts[1], clanName: parts[0] };
  }
  // Fallback: try to extract a run of digits as id
  m = s.match(/(\d{3,})/);
  if (m) {
    const id = m[1];
    const name = s.replace(id, '').replace(/[():]/g, '').trim();
    return { clanId: id, clanName: name || null };
  }
  // No id found, return string as name
  return { clanId: null, clanName: s };
}

function normalizeClanName(name) {
  if (!name) return '';
  try {
    return String(name)
      .normalize('NFKD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase()
      .replace(/[^\p{L}\p{N}]+/gu, '');
  } catch (err) {
    return String(name).toLowerCase().replace(/[^a-z0-9]+/g, '');
  }
}

function loadOrderStore() {
  try {
    const raw = localStorage.getItem(ORDER_STORAGE_KEY);
    return raw ? JSON.parse(raw) : {};
  } catch (err) {
    console.warn('Failed to load card order store', err);
    return {};
  }
}

function initOrderStore() {
  orderStore = loadOrderStore();
}

function setTheme(name) {
  shell.setAttribute('data-theme', themeMap[name] || 'neon');
}

// Initialize DOM-dependent code after DOM is fully loaded
function initializeApp() {
  console.log('[MW] Initializing app after DOMContentLoaded');

  // Query DOM elements
  tabButtons = document.querySelectorAll('.tab');
  panels = document.querySelectorAll('.tab-panel');
  themeSelect = document.getElementById('themeSelect');
  resizer = document.getElementById('resizer');

  console.log('[MW] Found tab buttons:', tabButtons.length, 'panels:', panels.length);
  updateAdminToolsVisibility();
  updateHomeTabLabel();
  updateMenuUserInfo();
  ensureFloatingWinsContainer();
  refreshWinsPopup();
  scheduleWinsPopupRefresh();
  if (WEBSITE_MODE) {
    const bar = document.getElementById('websiteNoticeBar');
    if (bar) {
      bar.textContent = 'Full features are available only in the Chrome extension. More info on Discord: https://discord.gg/Rybp8RFr';
      bar.style.display = 'block';
    }
  }

  // Set up tab navigation
  tabButtons.forEach((btn) => {
    btn.addEventListener('click', () => {
      if (btn.dataset.tab === 'live') {
        debugPanelRevealClicks += 1;
        if (!debugPanelRevealed && debugPanelRevealClicks >= DEBUG_PANEL_REVEAL_CLICKS) {
          debugPanelRevealed = true;
          if (debugPanel) {
            debugPanel.style.display = '';
          }
          document.body.classList.add('debug-panel-visible');
        }
      }
      activateTab(btn.dataset.tab);
      const statsTarget = btn.dataset.statsTarget;
      if (statsTarget && btn.dataset.tab === 'stats') {
        setTimeout(() => activateStatsSubTab(statsTarget), 0);
      }
      const boostTarget = btn.dataset.boostTarget;
      if (boostTarget && btn.dataset.tab === 'boost-impact') {
        setTimeout(() => activateBoostImpactSubTab(boostTarget), 0);
      }
      if (document.querySelector('.app-shell')?.classList.contains('menu-open')) {
        setHeaderCollapsed(true);
      }
    });
  });
  bindLogoClick();
  bindHeaderToggle();

  // Set up theme
  const initialTheme = themeSelect?.value || shell.getAttribute('data-theme') || 'neon';
  if (themeSelect) {
    themeSelect.addEventListener('change', () => setTheme(themeSelect.value));
  }
  setTheme(initialTheme);
  initOrderStore();
  const initialTab = licenseIsActive() ? 'live' : 'home';
  activateTab(initialTab);

  // Fetch user league data on startup
  if (LIVE_API_STATE_ENABLED && !WEBSITE_MODE) {
    fetchUserLeagueData().catch(err => console.warn('[MW] Failed to fetch user league data:', err));
  }
}

async function fetchUserLeagueData() {
  try {
    console.log('[MW API] Fetching user league data...');
    const tab = await commFindMiningWarsTab();
    if (!tab?.id) {
      console.warn('[MW API] No Mining Wars tab available for user league data.');
      return;
    }
    const endpoint = 'https://api.gomining.com/api/nft-game/league/get-user-positions-data';
    const response = await commFetchGominingApiViaInjection(tab.id, endpoint, {});
    if (!response?.ok) {
      console.warn('[MW API] User league data response not ok:', response?.status || 0);
      return;
    }
    const data = response?.data;
    console.log('[MW API] User league data:', data);

    // Extract league ID from response
    const leagueId = data?.data?.leagueId ?? data?.leagueId ?? data?.data?.currentLeague?.id;
    if (Number.isFinite(commParseNumber(leagueId))) {
      commKorrelCurrentLeagueId = Math.trunc(commParseNumber(leagueId));
      console.log('[MW API] Set league ID from user data:', commKorrelCurrentLeagueId);
      if (LIVE_API_STATE_ENABLED) {
        commRefreshLiveApiState({ force: true }).catch(() => {});
      }
    }
  } catch (err) {
    console.error('[MW API] Error fetching user league data:', err);
  }
}

function bindLogoClick() {
  if (logoClickBound) return;
  document.addEventListener('click', (event) => {
    const target = event.target;
    if (target && target.closest && target.closest('.logo-section')) {
      activateTab('home');
    }
  });
  logoClickBound = true;
}

function setHeaderCollapsed(collapsed, { persist = true } = {}) {
  const shellEl = document.querySelector('.app-shell');
  if (!shellEl) return;
  shellEl.classList.toggle('menu-open', !collapsed);
  const toggleBtn = document.getElementById('menuToggleBtn');
  const drawer = document.getElementById('menuDrawer');
  if (drawer) drawer.setAttribute('aria-hidden', String(!!collapsed));
  if (toggleBtn) {
    const expanded = !collapsed;
    toggleBtn.setAttribute('aria-expanded', String(expanded));
    toggleBtn.setAttribute('aria-label', expanded ? 'Close menu' : 'Open menu');
    toggleBtn.title = expanded ? 'Close menu' : 'Open menu';
  }
  if (persist) {
    localStorage.setItem(HEADER_COLLAPSE_STORAGE_KEY, collapsed ? '1' : '0');
  }
}

function bindHeaderToggle() {
  const toggleBtn = document.getElementById('menuToggleBtn');
  if (!toggleBtn) return;
  const closeBtn = document.getElementById('menuCloseBtn');
  const overlay = document.getElementById('menuOverlay');
  const stored = localStorage.getItem(HEADER_COLLAPSE_STORAGE_KEY);
  const collapsed = stored !== '0';
  setHeaderCollapsed(collapsed, { persist: false });
  toggleBtn.addEventListener('click', () => {
    const isOpen = document.querySelector('.app-shell')?.classList.contains('menu-open');
    setHeaderCollapsed(!!isOpen);
  });
  if (closeBtn) {
    closeBtn.addEventListener('click', () => setHeaderCollapsed(true));
  }
  if (overlay) {
    overlay.addEventListener('click', () => setHeaderCollapsed(true));
  }
}

function updateAdminToolsVisibility() {
  if (!tabButtons || !tabButtons.length) return;
  const adminButton = Array.from(tabButtons).find((btn) => btn.dataset.tab === 'admin-tools');
  const adminPanel = document.querySelector('.tab-panel[data-panel="admin-tools"]');
  const showAdmin = commIsKorrel();
  if (adminButton) {
    adminButton.style.display = showAdmin ? '' : 'none';
  }
  if (adminPanel) {
    adminPanel.style.display = showAdmin ? '' : 'none';
  }
  if (!showAdmin && adminPanel && adminPanel.classList.contains('active')) {
    activateTab('home');
  }
}

function updateMenuUserInfo() {
  const nameEl = document.getElementById('menuUserName');
  const licenseEl = document.getElementById('menuUserLicense');
  const avatarEl = document.getElementById('menuUserAvatar');
  const logoutBtn = document.getElementById('menuLogoutBtn');
  const profile = homeGoogleProfile || loadHomeGoogleProfile();
  const loggedIn = hasSupabaseSession();
  const label = profile?.email || profile?.name || commSession?.memberName || (loggedIn ? 'Signed in' : 'Guest');
  const initials = label ? String(label).trim().charAt(0).toUpperCase() : '?';
  if (nameEl) nameEl.textContent = label;
  if (avatarEl) avatarEl.textContent = initials || '?';
  const expiry = homeLicenseState?.expires_at
    ? `  -  ${new Date(homeLicenseState.expires_at).toLocaleDateString()}`
    : '';
  const licenseText = licenseIsActive()
    ? `License: Active${expiry}`
    : `License: ${homeLicenseState?.reason ? homeLicenseState.reason : 'Inactive'}`;
  if (licenseEl) licenseEl.textContent = licenseText;
  if (logoutBtn) {
    logoutBtn.style.display = loggedIn ? '' : 'none';
    if (!logoutBtn.dataset.bound) {
      logoutBtn.addEventListener('click', async () => {
        await logoutSupabaseSession();
        updateMenuUserInfo();
        updateHomeTabLabel();
        populateHome();
      });
      logoutBtn.dataset.bound = '1';
    }
  }
}

function updateHomeTabLabel() {
  if (!tabButtons || !tabButtons.length) return;
  const homeButton = Array.from(tabButtons).find((btn) => btn.dataset.tab === 'home');
  if (!homeButton) return;
  homeButton.textContent = 'Subscription';
}

function activateStatsSubTab(target) {
  if (!target) return;
  const card = document.querySelector('[data-card-id="stats-dashboard-card"]');
  if (!card) return;
  showStatsPanel(target, card);
}

function activateBoostImpactSubTab(target) {
  if (!target) return;
  const card = document.querySelector('[data-card-id="boost-impact-card"]');
  if (!card) return;
  showBoostImpactPanel(target, card);
}

function activateTab(target) {
  if (!target) return;
  if (!licenseIsActive() && target !== 'home') {
    target = 'home';
  }
  tabButtons.forEach((b) => b.classList.remove('active'));
  panels.forEach((panel) => panel.classList.toggle('active', panel.dataset.panel === target));
  const button = Array.from(tabButtons).find((btn) => btn.dataset.tab === target);
  if (button) button.classList.add('active');
  if (target === 'leaderboard') {
    populateLeaderboard();
  }
  if (target === 'admin-tools') {
    const adminGrid = document.getElementById('adminToolsGrid');
    const adminCard = adminGrid?.querySelector('[data-card-id="admin-tools-card"]');
    if (!adminCard || adminCard.classList.contains('deleted')) {
      populateAdminTools();
    }
  }
  if (target === 'stats') {
    const statsGrid = document.getElementById('statsGrid');
    const statsCard = statsGrid?.querySelector('[data-card-id="stats-dashboard-card"]');
    if (!statsCard || statsCard.classList.contains('deleted')) {
      populateStats();
    }
  }
  if (target === 'personal') {
    const personalGrid = document.getElementById('personalGrid');
    const personalCard = personalGrid?.querySelector('[data-card-id="personal-data-card"]');
    if (!personalCard || personalCard.classList.contains('deleted')) {
      populatePersonalData();
    }
  }
  if (target === 'graphs') {
    populateGraphs();
  }
  if (target === 'multiplier-time') {
    populateMultiplierTime();
  }
  if (target === 'boost-helper') {
    const boostGrid = document.getElementById('boostHelperGrid');
    const boostCard = boostGrid?.querySelector('[data-card-id="boost-helper-card"]');
    if (!boostCard || boostCard.classList.contains('deleted')) {
      populateBoostHelper();
    }
  }
  if (target === 'telegram-bot') {
    const tgGrid = document.getElementById('telegramBotGrid');
    const tgCard = tgGrid?.querySelector('[data-card-id="telegram-bot-card"]');
    if (!tgCard || tgCard.classList.contains('deleted')) {
      populateTelegramBot();
    }
  }
  if (target === 'discord-bot') {
    const dcGrid = document.getElementById('discordBotGrid');
    const dcCard = dcGrid?.querySelector('[data-card-id="discord-bot-card"]');
    if (!dcCard || dcCard.classList.contains('deleted')) {
      populateDiscordBot();
    }
  }
  if (target === 'boost-impact') {
    populateBoostImpact();
  }
  if (target === 'clan-compare') {
    populateClanCompare();
  }
}

function normalizeOrderId(item) {
  return item.id || item.title || '';
}

function applySavedOrder(items, containerId) {
  const saved = orderStore[containerId];
  if (!saved?.length) return items;
  const map = new Map(items.map((item) => [normalizeOrderId(item), item]));
  const ordered = [];
  saved.forEach((cardId) => {
    if (map.has(cardId)) {
      ordered.push(map.get(cardId));
      map.delete(cardId);
    }
  });
  return [...ordered, ...map.values()];
}

function createCard({ title, description, meta, id, htmlContent }) {
  const card = document.createElement('article');
  card.className = 'card';
  card.style.position = 'relative'; // Needed for absolute positioning of delete button
  const cardId = id || title || crypto.randomUUID();
  card.dataset.cardId = cardId;

  // If htmlContent is provided, use it directly; otherwise use title/description/meta
  if (htmlContent) {
    card.innerHTML = htmlContent;
  } else {
    card.innerHTML = `<h3>${title}</h3><p>${description}</p><small>${meta || ''}</small>`;
  }

  // Add delete button to all cards
  const deleteBtn = document.createElement('button');
  deleteBtn.className = 'card-delete-btn';
  deleteBtn.innerHTML = 'Delete';
  deleteBtn.title = 'Delete card';
  deleteBtn.setAttribute('aria-label', 'Delete card');

  deleteBtn.addEventListener('click', (e) => {
    e.stopPropagation(); // Prevent drag events
    e.preventDefault();

    // Mark card as deleted (hide it but keep in DOM for recovery)
    card.classList.add('deleted');
  });

  // Add delete button after content
  card.appendChild(deleteBtn);
  return card;
}

function renderCards(container, items) {
  container.innerHTML = '';
  const orderedItems = applySavedOrder(items, container.id);
  orderedItems.forEach((item) => container.appendChild(createCard(item)));
}

function formatHomeText(value) {
  if (!value) return '';
  const raw = String(value);
  if (raw.includes('<')) {
    return sanitizeHomeHtml(raw);
  }
  return commEscape(raw).replace(/\n/g, '<br>');
}

function sanitizeHomeHtml(input) {
  if (!input) return '';
  const template = document.createElement('template');
  template.innerHTML = String(input);
  template.content.querySelectorAll('script,style,iframe,object,embed,link,meta').forEach((el) => el.remove());
  template.content.querySelectorAll('*').forEach((el) => {
    Array.from(el.attributes).forEach((attr) => {
      const name = attr.name.toLowerCase();
      const value = String(attr.value || '').trim().toLowerCase();
      if (name.startsWith('on')) {
        el.removeAttribute(attr.name);
        return;
      }
      if ((name === 'href' || name === 'src') && value.startsWith('javascript:')) {
        el.removeAttribute(attr.name);
      }
    });
  });
  return template.innerHTML.trim();
}

function getHomeNewsEditorHtml(editor) {
  if (!editor) return '';
  const cleaned = sanitizeHomeHtml(editor.innerHTML || '');
  return cleaned;
}

function buildHomeNewsHtml() {
  const title = homeNewsEntry?.title || 'No news yet';
  const body = homeNewsEntry?.body || 'News updates will appear here.';
  const renderedBody = formatHomeText(body);
  const metaParts = [];
  if (homeNewsEntry?.updated_at) metaParts.push(formatDateTimeValue(homeNewsEntry.updated_at));
  if (homeNewsEntry?.updated_by) metaParts.push(`by ${homeNewsEntry.updated_by}`);
  const meta = metaParts.join(' | ');
  const editor = isAdminLicenseHolder()
    ? `
      <div class="home-news-editor">
        <input id="homeNewsTitle" type="text" placeholder="News title" value="${commEscape(title)}">
        <div class="home-news-toolbar">
          <button class="btn sm ghost" type="button" data-editor-cmd="bold">Bold</button>
          <button class="btn sm ghost" type="button" data-editor-cmd="italic">Italic</button>
          <button class="btn sm ghost" type="button" data-editor-cmd="underline">Underline</button>
          <button class="btn sm ghost" type="button" data-editor-link="true">Link</button>
          <button class="btn sm ghost" type="button" data-editor-cmd="removeFormat">Clear</button>
          <select class="home-news-font" data-editor-font>
            <option value="">Font</option>
            <option value="Space Grotesk">Space Grotesk</option>
            <option value="Georgia">Georgia</option>
            <option value="Courier New">Courier New</option>
          </select>
          <select class="home-news-size" data-editor-size>
            <option value="">Size</option>
            <option value="2">Small</option>
            <option value="3">Normal</option>
            <option value="4">Large</option>
            <option value="5">X-Large</option>
          </select>
        </div>
        <div id="homeNewsBody" class="home-news-body editable" contenteditable="true" spellcheck="true" data-placeholder="Write the update...">${renderedBody}</div>
        <button id="homeNewsSaveBtn" class="btn sm" type="button">Publish update</button>
      </div>
    `
    : '';
  return `
    <div class="home-section">
      <h3 class="home-section-title">News</h3>
      <div class="home-news-list">
        <div class="home-news-item">
          <h4>${commEscape(title)}</h4>
          <div class="home-news-body">${renderedBody}</div>
          <div class="home-news-meta">${commEscape(meta)}</div>
        </div>
      </div>
      ${editor}
    </div>
  `;
}

function commPricingForceZeroPerWeek(row) {
  const text = `${row?.title || ''} ${row?.sku || ''} ${row?.description || ''}`.toLowerCase();
  return text.includes('accepted referral') || text.includes('referral link') || text.includes('lifetime');
}

function commComputePricingPerWeek(row) {
  const price = commParseNumber(row?.price_gmt);
  if (!Number.isFinite(price)) return null;
  if (commPricingForceZeroPerWeek(row)) return 0;
  const durationDays = commParseNumber(row?.duration_days);
  if (!Number.isFinite(durationDays) || durationDays <= 0) return null;
  const weeks = durationDays / 7;
  if (!Number.isFinite(weeks) || weeks <= 0) return null;
  return price / weeks;
}

function buildHomePricingTableHtml() {
  const showWallet = !!homeGoogleProfile?.email;
  const headerCells = ['Plan', 'Price (GMT)'];
  if (showWallet) headerCells.push('Wallet');
  const header = headerCells.map((cell) => `<th>${cell}</th>`).join('');
  if (!homePricingRows.length) {
    return `<div class="muted">No pricing available yet.</div>`;
  }
  const rows = homePricingRows.map((row) => {
    const wallet = row?.payment_wallet || '';
    const perWeek = commComputePricingPerWeek(row);
    const perWeekLabel = perWeek === 0
      ? '0'
      : (Number.isFinite(perWeek) ? formatDecimal(perWeek, 2) : '-');
    const priceLabel = Number.isFinite(commParseNumber(row?.price_gmt))
      ? `${commEscape(formatCount(row?.price_gmt))} <span class="muted">(${commEscape(perWeekLabel)}/wk)</span>`
      : commEscape(formatCount(row?.price_gmt ?? '-'));
    const cells = [
      commEscape(row?.title || row?.sku || 'Plan'),
      priceLabel
    ];
    if (showWallet) {
      if (wallet) {
        const walletShort = formatWalletShort(wallet);
        const walletCell = `
          <div class="home-wallet-cell">
            <span class="home-wallet-short" title="${commEscape(wallet)}">${commEscape(walletShort)}</span>
            <button class="btn sm ghost home-wallet-copy" type="button" data-wallet="${commEscape(wallet)}">Copy</button>
          </div>
        `;
        cells.push(walletCell);
      } else {
        cells.push('<span class="muted">-</span>');
      }
    }
    return `<tr>${cells.map((cell) => `<td>${cell}</td>`).join('')}</tr>`;
  }).join('');
  return `
    <table class="home-pricing-table">
      <thead><tr>${header}</tr></thead>
      <tbody>${rows}</tbody>
    </table>
  `;
}

function formatWalletShort(wallet) {
  const text = String(wallet || '');
  if (text.length <= 10) return text;
  return text.slice(0, 10);
}

function getHomeAuthUserId() {
  const stored = homeGoogleProfile?.user_id || homeGoogleProfile?.id || null;
  return stored ? String(stored) : null;
}

function hasSupabaseSession() {
  loadHomeAuthSession();
  return !!homeAuthSession?.access_token;
}

const FEATURE_OVERVIEW_ID = 'featureOverviewOverlay';
const FEATURE_LIGHTBOX_ID = 'featureOverviewLightbox';
const FEATURE_TABS = [
  {
    id: 'live',
    title: 'Live',
    summary: 'Track the current round in real time with scores, boosts, and cache status.',
    bullets: [
      'Live leaderboard with score, boosts, and power columns.',
      'Click on score for detailed calculation of score.',
      'Total score chart, click on boost buttons for details.',
      'Integrated calculated score matching the current round time.'
    ],
    shotSrc: 'style/features/screen_live.png'
  },
  {
    id: 'calculator',
    title: 'Calculator',
    summary: 'Run quick SIM projections without leaving the app.',
    bullets: [
      'Estimate PPS and boost impact fast.',
      'Compare scenarios across multipliers.',
      'Enable simulated score of calculation on live.'
    ],
    shotSrc: 'style/features/screen_calculator.png'
  },
  {
    id: 'notifications',
    title: 'Notifications',
    summary: 'Configure alerts for boosts, timing windows, and phases, shown on all pages.',
    bullets: [
      'Sound alert when 4x+, when clan member uses clan power-up, or when total score is low.',
      'Rule-based boost windows.',
      'Boost detail popups and history.'
    ],
    shotSrc: 'style/features/screen_notifications.png'
  },
  {
    id: 'discord',
    title: 'Discord Bot',
    summary: 'Use slash commands for live round info and top-10 summaries.',
    bullets: [
      'Top-10 and /round snapshots.',
      'Per-server configuration support.',
      'Fast responses from cached data.'
    ],
    shotSrc: 'style/features/screen_discord.png'
  },
  {
    id: 'telegram',
    title: 'Telegram Bot',
    summary: 'Push alerts and summaries into Telegram chats.',
    bullets: [
      'Instant boost alerts.',
      'Round summaries on demand.',
      'Great for team coordination.'
    ]
  },
  {
    id: 'earnings',
    title: 'Earnings',
    summary: 'Track income per cycle and detailed reward entries.',
    bullets: [
      'Income per cycle overview.',
      'Detailed entries with timestamps.',
      'Fast filters and paging.'
    ],
    shotSrc: 'style/features/screen_earnings.png'
  },
  {
    id: 'leaderboard',
    title: 'Leaderboard',
    summary: 'Top rankings and round performance snapshots.',
    bullets: [
      'Top clans overview.',
      'Quick comparison at a glance.',
      'Useful for scouting.'
    ],
    shotSrc: 'style/features/screen_leaderboard.jpg'
  },
  {
    id: 'round-history',
    title: 'Round History',
    summary: 'Review recent rounds, winners, and outcomes.',
    bullets: [
      'Round results with winners.',
      'Quick lookup by cycle.',
      'Helpful for after-action review.'
    ],
    shotSrc: 'style/features/screen_Roundhistory.png'
  },
  {
    id: 'busiest-hours',
    title: 'Busiest Hours',
    summary: 'Identify the most active hours for boosts.',
    bullets: [
      'Visual busiest-hours overview.',
      'Spot peak activity windows.',
      'Plan boosts with timing context.'
    ],
    shotSrc: 'style/features/screen_busiesthours.png'
  },
  {
    id: 'multiplier-time',
    title: 'Multiplier Timeline',
    summary: 'See multiplier changes over time for better timing decisions.',
    bullets: [
      'Timeline of multiplier shifts.',
      'Spot windows with higher impact.',
      'Useful for planning boosts.'
    ],
    shotSrc: 'style/features/screen_mutiplier_per_time.png'
  },
  {
    id: 'effectiveness',
    title: 'Effectiveness',
    summary: 'Analyze efficiency and performance indicators.',
    bullets: [
      'Effectiveness metrics at a glance.',
      'Compare results between cycles.',
      'Spot drops or spikes quickly.'
    ],
    shotSrc: 'style/features/screen_effectivess.png'
  },
  {
    id: 'clan-profit',
    title: 'Clan Profitability',
    summary: 'Track clan profitability and contribution trends.',
    bullets: [
      'Clan-level profit breakdown.',
      'Identify strong contributors.',
      'Useful for decisions and strategy.'
    ],
    shotSrc: 'style/features/screen_clanprofitability.png'
  },
  {
    id: 'clan-tools',
    title: 'Clan Selection',
    summary: 'Help you to find the clan of your criteria.',
    bullets: [
      'Filter based on cycle, minimum score, maximum score.',
      'Filter only clans that don\'t have a whale.',
      'Compare!'
    ],
    shotSrc: 'style/features/screen_clanselection.png'
  }
];

function buildHomeLicenseHtml() {
  const loggedIn = hasSupabaseSession();
  const statusClass = licenseIsActive() ? 'valid' : 'invalid';
  const expiry = homeLicenseState?.expires_at
    ? ` | Expires ${new Date(homeLicenseState.expires_at).toLocaleDateString()}`
    : '';
  const statusText = licenseIsActive()
    ? `${homeLicenseState?.message || 'License active'}${expiry}`
    : `Locked: ${homeLicenseState?.reason || 'Enter a valid key'}`;
  const authStatus = homeAuthStatus.message
    ? `<div class="home-auth-status ${homeAuthStatus.error ? 'error' : 'ok'}">${commEscape(homeAuthStatus.message)}</div>`
    : '';
  const showKorrelToggle = commIsKorrelIdentity();
  const nonAdminMode = commIsKorrelNonAdminMode();
  const korrelToggle = showKorrelToggle
    ? `
      <div class="home-auth-row" style="margin-top:8px;">
        <label style="display:flex; align-items:center; gap:8px; font-size:0.85rem;">
          <input id="homeKorrelNonAdminToggle" type="checkbox" ${nonAdminMode ? 'checked' : ''}>
          Test as non-admin (Korrel only)
        </label>
      </div>
      <div class="muted" style="margin-top:4px;">Disables Korrel admin mode for testing.</div>
    `
    : '';
  const authRow = loggedIn
    ? `
      <div class="home-auth-row">
        <span class="home-auth-chip">${commEscape(homeGoogleProfile?.email || 'Signed in')}</span>
        <button class="btn sm ghost" id="homeGoogleLogoutBtn" type="button">Log out</button>
      </div>
    `
    : `
      <div class="home-auth-form">
        <input id="homeAuthEmail" type="email" placeholder="Email">
        <input id="homeAuthPassword" type="password" placeholder="Password">
        <div class="home-auth-actions">
          <button class="btn sm" id="homeAuthSignInBtn" type="button">Sign in</button>
          <button class="btn sm ghost" id="homeAuthSignUpBtn" type="button">Create account</button>
        </div>
        <span class="muted">Create an account to receive a license code by email.</span>
      </div>
    `;
  const ownerId = loggedIn ? getHomeAuthUserId() : null;
  const ownerRow = loggedIn
    ? `
      <div class="home-auth-row" style="margin-top:6px;">
        <span class="home-auth-chip" style="max-width:100%; overflow:hidden; text-overflow:ellipsis;">Owner ID: ${commEscape(ownerId || '-')}</span>
        <button class="btn sm ghost" id="homeCopyOwnerIdBtn" type="button"${ownerId ? '' : ' disabled'}>Copy</button>
      </div>
    `
    : '';
  const licenseInputs = loggedIn
    ? `
      <div class="home-license-inputs">
        <input id="homeSenderAddress" type="text" placeholder="Sender address (BNB Smart)" value="${commEscape(localStorage.getItem(COMM_LICENSE_SENDER_KEY) || '')}">
        <input id="homeLicenseKey" type="text" placeholder="License key" value="${commEscape(localStorage.getItem(COMM_LICENSE_STORAGE_KEY) || '')}">
        <button class="btn sm" id="homeLicenseCheckBtn" type="button">Validate license</button>
      </div>
    `
    : '';
  const qrBlock = loggedIn
    ? `
      <div class="home-license-qr">
        <img src="style/gomining_bep20_qr.png" alt="GoMining BEP20 payment QR" loading="lazy" data-wallet="${commEscape(COMM_LICENSE_WALLET_ADDRESS)}" onerror="this.closest('.home-license-qr') && this.closest('.home-license-qr').remove();">
  
      </div>
    `
    : '';
  return `
    <div class="home-license-block">
      <div class="home-license-header" style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
        <h3 class="home-section-title">License</h3>
        <button class="btn sm ghost" id="homeFeaturesBtn" type="button">Features</button>
      </div>
      ${authRow}
      ${ownerRow}
      ${authStatus}
      ${korrelToggle}
      <div class="home-license-status ${statusClass}">${commEscape(statusText)}</div>
      <div>
        <strong>Pricing</strong>
        <div class="home-license-paynote">Pay by BNB Smart in GoMining! License will be enabled ASAP after receiving the correct amount.</div>
        ${buildHomePricingTableHtml()}
      </div>
      ${qrBlock}
      ${licenseInputs}
    </div>
  `;
}

function buildFeatureOverviewHtml() {
  const tabs = FEATURE_TABS.map((tab, index) => `
    <button class="feature-overview-tab ${index === 0 ? 'active' : ''}" data-feature-tab="${commEscape(tab.id)}" type="button">
      ${commEscape(tab.title)}
    </button>
  `).join('');

  const panels = FEATURE_TABS.map((tab, index) => {
    const bullets = (tab.bullets || []).map((item) => `<li>${commEscape(item)}</li>`).join('');
    const screenshot = tab.shotSrc
      ? `
        <div class="feature-overview-screenshot" role="button" tabindex="0" data-feature-shot="${commEscape(tab.id)}" data-feature-src="${commEscape(tab.shotSrc)}" data-feature-title="${commEscape(tab.title)}">
          <img src="${commEscape(tab.shotSrc)}" alt="${commEscape(tab.title)} screenshot" loading="lazy">
          <span class="feature-overview-shot-label">Click to enlarge</span>
        </div>
      `
      : `
        <div class="feature-overview-screenshot feature-overview-screenshot--placeholder">
          <span>Screenshot coming soon</span>
        </div>
      `;

    return `
      <div class="feature-overview-panel ${index === 0 ? 'active' : ''}" data-feature-panel="${commEscape(tab.id)}">
        ${screenshot}
        <div class="feature-overview-details">
          <h4>${commEscape(tab.title)}</h4>
          <p class="muted">${commEscape(tab.summary || '')}</p>
          ${bullets ? `<ul>${bullets}</ul>` : ''}
        </div>
      </div>
    `;
  }).join('');

  return `
    <div class="feature-overview-overlay" id="${FEATURE_OVERVIEW_ID}" aria-hidden="true">
      <div class="feature-overview-card" role="dialog" aria-modal="true" aria-labelledby="featureOverviewTitle">
        <div class="feature-overview-header">
          <div class="feature-overview-title" id="featureOverviewTitle">GoMiner Tools - Feature Overview</div>
          <button class="feature-overview-close" type="button">Close</button>
        </div>
        <div class="feature-overview-body">
          <div class="feature-overview-tabs">${tabs}</div>
          <div class="feature-overview-panels">${panels}</div>
        </div>
      </div>
    </div>
    <div class="feature-overview-lightbox" id="${FEATURE_LIGHTBOX_ID}" aria-hidden="true">
      <div class="feature-overview-lightbox-card" role="dialog" aria-modal="true">
        <div class="feature-overview-lightbox-header">
          <div class="feature-overview-lightbox-title">Screenshot</div>
          <button class="feature-overview-lightbox-close" type="button">Close</button>
        </div>
        <div class="feature-overview-lightbox-body">
          <img class="feature-overview-lightbox-image" alt="Feature screenshot">
          <div class="feature-overview-lightbox-fallback">Image failed to load.</div>
        </div>
      </div>
    </div>
  `;
}

function ensureFeatureOverview() {
  if (document.getElementById(FEATURE_OVERVIEW_ID)) return;
  const wrapper = document.createElement('div');
  wrapper.innerHTML = buildFeatureOverviewHtml();
  while (wrapper.firstChild) {
    document.body.appendChild(wrapper.firstChild);
  }
}

function setActiveFeatureTab(tabId) {
  const overlay = document.getElementById(FEATURE_OVERVIEW_ID);
  if (!overlay) return;
  const tabs = overlay.querySelectorAll('.feature-overview-tab');
  const panels = overlay.querySelectorAll('.feature-overview-panel');
  tabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.featureTab === tabId));
  panels.forEach((panel) => panel.classList.toggle('active', panel.dataset.featurePanel === tabId));
}

function openFeatureOverview() {
  ensureFeatureOverview();
  const overlay = document.getElementById(FEATURE_OVERVIEW_ID);
  if (!overlay) return;
  overlay.style.display = 'flex';
  overlay.setAttribute('aria-hidden', 'false');
  document.body.classList.add('feature-overview-open');
}

function closeFeatureOverview() {
  const overlay = document.getElementById(FEATURE_OVERVIEW_ID);
  if (!overlay) return;
  overlay.style.display = 'none';
  overlay.setAttribute('aria-hidden', 'true');
  document.body.classList.remove('feature-overview-open');
}

function openFeatureLightbox(src, title) {
  const lightbox = document.getElementById(FEATURE_LIGHTBOX_ID);
  if (!lightbox) return;
  const img = lightbox.querySelector('.feature-overview-lightbox-image');
  const titleEl = lightbox.querySelector('.feature-overview-lightbox-title');
  const fallback = lightbox.querySelector('.feature-overview-lightbox-fallback');
  if (img) {
    img.src = src || '';
    img.alt = title ? `${title} screenshot` : 'Feature screenshot';
  }
  if (titleEl) {
    titleEl.textContent = title || 'Screenshot';
  }
  if (fallback) {
    fallback.style.display = 'none';
  }
  lightbox.style.display = 'flex';
  lightbox.setAttribute('aria-hidden', 'false');
}

function closeFeatureLightbox() {
  const lightbox = document.getElementById(FEATURE_LIGHTBOX_ID);
  if (!lightbox) return;
  lightbox.style.display = 'none';
  lightbox.setAttribute('aria-hidden', 'true');
}

function bindFeatureOverviewEvents() {
  const overlay = document.getElementById(FEATURE_OVERVIEW_ID);
  const lightbox = document.getElementById(FEATURE_LIGHTBOX_ID);
  if (!overlay || !lightbox) return;

  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) {
      closeFeatureOverview();
      return;
    }
    const closeBtn = event.target.closest('.feature-overview-close');
    if (closeBtn) {
      closeFeatureOverview();
      return;
    }
    const tabBtn = event.target.closest('[data-feature-tab]');
    if (tabBtn?.dataset?.featureTab) {
      setActiveFeatureTab(tabBtn.dataset.featureTab);
      return;
    }
    const shot = event.target.closest('[data-feature-shot]');
    if (shot) {
      const src = shot.getAttribute('data-feature-src');
      const title = shot.getAttribute('data-feature-title');
      if (src) openFeatureLightbox(src, title || '');
    }
  });

  overlay.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') {
      closeFeatureOverview();
      closeFeatureLightbox();
    }
    if ((event.key === 'Enter' || event.key === ' ') && event.target?.dataset?.featureShot) {
      event.preventDefault();
      const src = event.target.getAttribute('data-feature-src');
      const title = event.target.getAttribute('data-feature-title');
      if (src) openFeatureLightbox(src, title || '');
    }
  });

  lightbox.addEventListener('click', (event) => {
    if (event.target === lightbox) {
      closeFeatureLightbox();
      return;
    }
    if (event.target.closest('.feature-overview-lightbox-close')) {
      closeFeatureLightbox();
    }
  });
}

function initFeatureOverview() {
  ensureFeatureOverview();
  bindFeatureOverviewEvents();
}

function maybeShowFeatureOverviewOnLoad() {
  if (!hasSupabaseSession()) {
    openFeatureOverview();
  }
}

async function populateHome() {
  const container = document.getElementById('homeGrid');
  if (!container) return;
  loadHomeGoogleProfile();
  await ensureHomeAuthUser();
  await Promise.all([
    fetchHomePricing(),
    initHomeLicenseState()
  ]);
  if (licenseIsActive()) {
    await populateSubscription('homeGrid');
    const pricingCard = createCard({ id: 'home-pricing-card', htmlContent: buildHomeLicenseHtml() });
    container.prepend(pricingCard);
    bindHomeHandlers(container);
    return;
  }
  const cards = [
    { id: 'home-license-card', htmlContent: buildHomeLicenseHtml() }
  ];
  renderCards(container, cards);
  bindHomeHandlers(container);
}

function bindHomeHandlers(container) {
  const saveBtn = container.querySelector('#homeNewsSaveBtn');
  if (saveBtn && isAdminLicenseHolder()) {
    saveBtn.addEventListener('click', async () => {
      const titleInput = container.querySelector('#homeNewsTitle');
      const bodyInput = container.querySelector('#homeNewsBody');
      const title = titleInput?.value?.trim() || 'News update';
      const body = getHomeNewsEditorHtml(bodyInput) || '';
      await saveHomeNews({ title, body });
      await populateHome();
    });
  }
  bindHomeNewsEditor(container);

  const signInBtn = container.querySelector('#homeAuthSignInBtn');
  const signUpBtn = container.querySelector('#homeAuthSignUpBtn');
  const emailInput = container.querySelector('#homeAuthEmail');
  const passwordInput = container.querySelector('#homeAuthPassword');
  if (signInBtn || signUpBtn) {
    const runAuth = async (mode) => {
      const email = (emailInput?.value || '').trim();
      const password = passwordInput?.value || '';
      if (!email || !password) {
        setHomeAuthStatus('Enter email and password.', { error: true });
        populateHome();
        return;
      }
      const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
      if (!emailOk) {
        setHomeAuthStatus('Enter a valid email address.', { error: true });
        populateHome();
        return;
      }
      if (password.length < 6) {
        setHomeAuthStatus('Password must be at least 6 characters.', { error: true });
        populateHome();
        return;
      }
      setHomeAuthStatus('');
      try {
        if (mode === 'signup') {
          const data = await signUpWithEmail(email, password);
          const userId = data?.user?.id || data?.session?.user?.id || null;
          let licenseMessage = 'Account created.';
          try {
            await issueLicenseForEmail(email, userId);
            licenseMessage = 'Account created. License code emailed.';
          } catch (licenseErr) {
            licenseMessage = licenseErr?.message || 'Account created, but license email failed.';
          }
          if (data?.session?.access_token) {
            saveHomeAuthSession({
              access_token: data.session.access_token,
              refresh_token: data.session.refresh_token,
              token_type: data.session.token_type,
              expires_at: data.session.expires_at
            });
            setHomeProfileFromUser(data.session.user || data.user);
            await ensureHomeAuthUser();
            await commLoadUserSettingsFromDb();
            commStartUserSettingsSyncTimer();
            commRestoreSessionFromAuth().catch(() => {});
            commLinkMemberToAuth().catch(() => {});
            setHomeAuthStatus(licenseMessage, { error: false });
          } else {
            setHomeAuthStatus(licenseMessage, { error: false });
          }
        } else {
          const data = await signInWithEmail(email, password);
          saveHomeAuthSession({
            access_token: data.access_token,
            refresh_token: data.refresh_token,
            token_type: data.token_type,
            expires_at: data.expires_at
          });
          setHomeProfileFromUser(data.user);
          await ensureHomeAuthUser();
          await commLoadUserSettingsFromDb();
          commStartUserSettingsSyncTimer();
          commRestoreSessionFromAuth().catch(() => {});
          commLinkMemberToAuth().catch(() => {});
          setHomeAuthStatus('Signed in.', { error: false });
        }
      } catch (err) {
        setHomeAuthStatus(err?.message || 'Auth failed.', { error: true });
      }
      populateHome();
    };
    if (signInBtn) signInBtn.addEventListener('click', () => runAuth('signin'));
    if (signUpBtn) signUpBtn.addEventListener('click', () => runAuth('signup'));
  }

  const logoutBtn = container.querySelector('#homeGoogleLogoutBtn');
  if (logoutBtn) {
    logoutBtn.addEventListener('click', async () => {
      await logoutSupabaseSession();
      populateHome();
    });
  }

  const copyOwnerBtn = container.querySelector('#homeCopyOwnerIdBtn');
  if (copyOwnerBtn && !copyOwnerBtn.dataset.bound) {
    copyOwnerBtn.dataset.bound = '1';
    copyOwnerBtn.addEventListener('click', async () => {
      const ownerId = getHomeAuthUserId();
      if (!ownerId) return;
      try {
        await copyTextToClipboard(ownerId);
        setHomeAuthStatus('Owner ID copied.', { error: false });
      } catch (err) {
        setHomeAuthStatus('Copy failed.', { error: true });
      }
      populateHome();
    });
  }

  const korrelToggle = container.querySelector('#homeKorrelNonAdminToggle');
  if (korrelToggle) {
    korrelToggle.addEventListener('change', async (event) => {
      setKorrelNonAdminMode(event.target.checked);
      updateAdminToolsVisibility();
      updateMenuUserInfo();
      await initHomeLicenseState();
      populateHome();
    });
  }

  const walletButtons = container.querySelectorAll('.home-wallet-copy');
  walletButtons.forEach((btn) => {
    btn.addEventListener('click', async () => {
      const wallet = btn.getAttribute('data-wallet') || '';
      if (!wallet) return;
      const ok = await copyTextToClipboard(wallet);
      const originalText = btn.textContent;
      btn.textContent = ok ? 'Copied' : 'Failed';
      btn.disabled = true;
      setTimeout(() => {
        btn.textContent = originalText;
        btn.disabled = false;
      }, 1200);
    });
  });

  const qrImage = container.querySelector('.home-license-qr img');
  if (qrImage) {
    qrImage.addEventListener('click', async () => {
      const wallet = qrImage.getAttribute('data-wallet') || '';
      if (!wallet) return;
      await copyTextToClipboard(wallet);
    });
  }

  const senderInput = container.querySelector('#homeSenderAddress');
  if (senderInput && !senderInput.dataset.bound) {
    senderInput.addEventListener('input', () => {
      const value = senderInput.value || '';
      if (value.trim()) {
        localStorage.setItem(COMM_LICENSE_SENDER_KEY, value.trim());
      } else {
        localStorage.removeItem(COMM_LICENSE_SENDER_KEY);
      }
      commScheduleUserSettingsSync();
    });
    senderInput.dataset.bound = '1';
  }

  const licenseBtn = container.querySelector('#homeLicenseCheckBtn');
  if (licenseBtn) {
    licenseBtn.addEventListener('click', async () => {
      const input = container.querySelector('#homeLicenseKey');
      const key = input?.value?.trim() || '';
      const state = await validateLicenseKey(key);
      if (state?.valid) {
        await commAttachLicenseToUser(key);
      }
      homeLicenseState = state;
      if (key) {
        localStorage.setItem(COMM_LICENSE_STORAGE_KEY, key);
      } else {
        localStorage.removeItem(COMM_LICENSE_STORAGE_KEY);
      }
      if (state.valid && state.token) {
        localStorage.setItem(COMM_LICENSE_TOKEN_KEY, state.token);
      } else {
        localStorage.removeItem(COMM_LICENSE_TOKEN_KEY);
      }
      applyLicenseGate();
      populateHome();
    });
  }

  const featuresBtn = container.querySelector('#homeFeaturesBtn');
  if (featuresBtn) {
    featuresBtn.addEventListener('click', () => {
      openFeatureOverview();
    });
  }
}

function bindHomeNewsEditor(container) {
  const editor = container.querySelector('#homeNewsBody');
  if (!editor) return;
  const toolbarButtons = container.querySelectorAll('[data-editor-cmd]');
  toolbarButtons.forEach((btn) => {
    btn.addEventListener('click', () => {
      const command = btn.getAttribute('data-editor-cmd');
      if (!command) return;
      editor.focus();
      document.execCommand('styleWithCSS', false, true);
      document.execCommand(command, false, null);
    });
  });
  const linkBtn = container.querySelector('[data-editor-link]');
  if (linkBtn) {
    linkBtn.addEventListener('click', () => {
      editor.focus();
      const url = window.prompt('Enter link URL');
      if (!url) return;
      document.execCommand('createLink', false, url);
    });
  }
  const fontSelect = container.querySelector('[data-editor-font]');
  if (fontSelect) {
    fontSelect.addEventListener('change', () => {
      const value = fontSelect.value;
      if (!value) return;
      editor.focus();
      document.execCommand('styleWithCSS', false, true);
      document.execCommand('fontName', false, value);
      fontSelect.value = '';
    });
  }
  const sizeSelect = container.querySelector('[data-editor-size]');
  if (sizeSelect) {
    sizeSelect.addEventListener('change', () => {
      const value = sizeSelect.value;
      if (!value) return;
      editor.focus();
      document.execCommand('styleWithCSS', false, true);
      document.execCommand('fontSize', false, value);
      sizeSelect.value = '';
    });
  }
}

async function copyTextToClipboard(text) {
  try {
    if (navigator?.clipboard?.writeText) {
      await navigator.clipboard.writeText(text);
      return true;
    }
  } catch (err) {
    // Fall through to manual copy.
  }
  try {
    const el = document.createElement('textarea');
    el.value = text;
    el.setAttribute('readonly', 'true');
    el.style.position = 'fixed';
    el.style.top = '-9999px';
    document.body.appendChild(el);
    el.select();
    const ok = document.execCommand('copy');
    document.body.removeChild(el);
    return ok;
  } catch (err) {
    return false;
  }
}

// Build a compact boost summary card (power, multiplier, timer, boosts used + top boosters)
function buildBoostSummaryCard(data = {}) {
  const card = document.createElement('div');
  card.className = 'boost-summary-card';

  // Compact boost list and active boosters; stat pills removed per request
  const boostList = document.createElement('div');
  boostList.className = 'boost-list';

  const controlsBar = document.createElement('div');
  controlsBar.className = 'boost-controls';

  const groupLabel = document.createElement('div');
  groupLabel.className = 'boost-group-label';

  const boosterList = document.createElement('div');
  boosterList.className = 'booster-list';

  card.append(controlsBar, boostList, groupLabel, boosterList);

  card._boostRefs = {
    boostList,
    boosterList
  };

  updateBoostSummaryCard(card, data);
  return card;
}

function updateBoostSummaryCard(card, data = {}) {
  const blockId = data.blockId || data.block_id || '-';
  const timer = data.timer || data.time || data.timeLeft || '--:--';
  const mult = data.multiplier || data.mult || 'x1';
  const boostCounts = data.boostsUsed || data.boosts_used || data.boosts || {};
  const topBoosters = data.topBoosters || [];

  const boostItems = Object.entries(boostCounts).filter(([key, val]) => val > 0 && normalizeBoostKey(key) !== 'service');

  // Query for elements instead of relying on stored references (which are lost after HTML serialization)
  const boostList = card.querySelector('.boost-list');
  const boosterList = card.querySelector('.booster-list');

  if (!boostList || !boosterList) {
    console.error('[MW] updateBoostSummaryCard: Missing required elements', { hasBoostList: !!boostList, hasBoosterList: !!boosterList });
    return;
  }
  // blockId/mult/timer stat pills removed - values are still available in `data` when needed

  boostList.innerHTML = '';
  if (boostItems.length) {
    boostItems.forEach(([key, val]) => {
      const pill = document.createElement('div');
      pill.className = 'boost-pill';
      const icon = BOOST_ICON_MAP[key] || '*';
      const displayName = formatBoostDisplayName(key);
      const displayVal = Number.isFinite(Number(val)) ? `${val}x` : val;
      // Display only icon and multiplier, no boost name (clean gomining style)
      pill.innerHTML = `<span class="pill-icon">${icon}</span><span class="boost-value">${displayVal}</span>`;
      pill.title = `${displayName}: ${displayVal}`; // Show name on hover
      boostList.appendChild(pill);
    });
  } else {
    const placeholder = document.createElement('div');
    placeholder.className = 'boost-pill empty';
    placeholder.textContent = 'No boosts used yet';
    boostList.appendChild(placeholder);
  }

  boosterList.innerHTML = '';
  const boosters = topBoosters.length
    ? topBoosters
        .slice(0, 5)
        .map((booster) => {
          const types = Object.entries(booster.types || {}).filter(([type, v]) => v > 0 && normalizeBoostKey(type) !== 'service');
          types.sort((a, b) => (b[1] - a[1]) || String(a[0]).localeCompare(String(b[0])));
          return { name: booster.name || 'Unknown Clan', types };
        })
    : []; 

  if (boosters.length) {
    boosters.forEach((booster) => {
      const row = document.createElement('div');
      row.className = 'booster-row';

      // Create a single blue player pill that contains the username and all their boosts (multiline)
      const playerPill = document.createElement('div');
      playerPill.className = 'boost-pill player-boost-pill';

      const nameEl = document.createElement('div');
      nameEl.className = 'booster-name';
      nameEl.textContent = booster.name;

      const pills = document.createElement('div');
      pills.className = 'booster-pills';

      if (!booster.types.length) {
        pills.innerHTML = '<span class="boost-type-pill">No boosts</span>';
      } else {
        booster.types.forEach(([type, val]) => {
          const pill = document.createElement('span');
          pill.className = 'boost-type-pill icon-only';
          const tkey = normalizeBoostKey(type);
          const icon = BOOST_ICON_MAP[tkey] || '*';
          const displayName = formatBoostDisplayName(tkey);
          const rawVal = Number(val);
          const displayVal = Number.isFinite(rawVal) ? `${rawVal}x` : val;
          const showBadge = Number.isFinite(rawVal) && rawVal > 1;
          // Icon-only display, name and multiplier available on hover via title and aria-label
          pill.innerHTML = `<span class="pill-icon">${icon}</span>` + (showBadge ? `<span class="boost-badge">${rawVal}</span>` : '');
          pill.title = `${displayName}: ${displayVal}`;
          pill.setAttribute('aria-label', `${displayName}: ${displayVal}`);
          pills.appendChild(pill);
        });
      }

      playerPill.append(nameEl, pills);
      row.appendChild(playerPill);
      boosterList.appendChild(row);
    });
  } else {
    const placeholder = document.createElement('div');
    placeholder.className = 'booster-row empty';
    placeholder.textContent = 'No boosting clans yet';
    boosterList.appendChild(placeholder);
  }
}
function buildRoundCard(round) {
  const summary = describeRoundRecord(round);
  const roundId = round.block_id || '-';
  const multiplier = round.multiplier || '1x';
  const title = `Block ${roundId} x ${multiplier}`;

  const descriptionParts = [];
  descriptionParts.push(`Winner: ${summary.winner || 'Unknown'}`);
  if (summary.winnerTh) descriptionParts.push(summary.winnerTh);
  if (summary.winnerPct) descriptionParts.push(summary.winnerPct);

  const metaParts = [];
  if (summary.player && summary.player !== 'N/A') {
    metaParts.push(`Player: ${summary.player}`);
    if (summary.playerTh) metaParts.push(summary.playerTh);
    if (summary.playerPct) metaParts.push(summary.playerPct);
  }
  if (summary.playerReward) metaParts.push(`Reward: ${summary.playerReward}`);

  // Add time information
  if (round.time_round_finished) {
    const finishedDate = new Date(round.time_round_finished);
    metaParts.push(finishedDate.toLocaleTimeString());
  }

  return {
    id: `block-${roundId}-${isoTimestamp(round.time_round_finished)}`,
    title,
    description: descriptionParts.join('  -  '),
    meta: metaParts.join(' | ')
  };
}

function isoTimestamp(value) {
  if (!value) return '';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return '';
  return date.toISOString();
}

function buildLastFinishedCard(round) {
  if (!round) {
    return {
      id: 'last-finished-waiting',
      htmlContent: `
        <div class="current-round-card" style="border-color: #94a3b8;">
          <div class="round-info-bar">
            <div class="info-pill" style="flex: 1; justify-content: center;">
              <span class="info-value" style="font-size: 0.85rem;">No finished rounds yet</span>
            </div>
          </div>
        </div>
      `
    };
  }

  const summary = describeRoundRecord(round);
  const roundId = round.block_id || '-';
  const multiplier = round.multiplier || '1x';
  const roundTime = round.round_time || '-';
  const winner = summary.winner || round.clan_name || 'Unknown';
  const normalizedWinner = normalizeClanName(winner);

  let leadingClans = [];
  try {
    if (round.leading_clans_snapshot && typeof round.leading_clans_snapshot === 'string') {
      leadingClans = JSON.parse(round.leading_clans_snapshot);
    } else if (Array.isArray(round.leading_clans_snapshot)) {
      leadingClans = round.leading_clans_snapshot;
    }
  } catch (e) {
    console.warn('[MW] Failed to parse leading_clans_snapshot:', e);
  }
  if (!Array.isArray(leadingClans)) leadingClans = [];
  const topClans = leadingClans.slice(0, 5);
  const winnerEntry = winner ? { clan_name: winner } : null;
  const winnerStatsEntry = winnerEntry
    ? leadingClans.find((entry) => {
        const name = entry?.clan_name ?? entry?.name ?? '';
        return normalizeClanName(name) === normalizedWinner;
      }) || null
    : null;

  let finishedTime = '-';
  if (round.time_round_finished) {
    const finishedDate = new Date(round.time_round_finished);
    finishedTime = finishedDate.toLocaleTimeString();
  }

  // Parse boost data if available
  let clanBoosts = null;
  try {
    if (round.clan_boosts && typeof round.clan_boosts === 'string') {
      clanBoosts = JSON.parse(round.clan_boosts);
    } else if (round.clan_boosts && typeof round.clan_boosts === 'object') {
      clanBoosts = round.clan_boosts;
    }
  } catch (e) {
    console.warn('[MW] Failed to parse clan_boosts:', e);
  }

  // Build boost display HTML
  let boostHtml = '';
  if (clanBoosts && Object.keys(clanBoosts).length > 0) {
    const boostItems = Object.entries(clanBoosts)
      .filter(([_, count]) => count > 0)
      .map(([type, count]) => {
        const normalizedType = normalizeBoostKey(type);
        const icon = BOOST_ICON_MAP[normalizedType] || '*';
        return `<span style="font-size: 0.75rem; opacity: 0.9;"><span class="boost-icon">${icon}</span> ${count}</span>`;
      })
      .join(' ');

    if (boostItems) {
      boostHtml = `
        <div class="round-clan-name" style="font-size: 0.8rem; opacity: 0.8; margin-top: 2px;">
          <span style="opacity: 0.7;">Boosts:</span> ${boostItems}
        </div>
      `;
    }
  }

  const headerRow = `
    <div style="display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 6px; font-size: 0.75rem; text-transform: uppercase; opacity: 0.7; margin-top: 6px;">
      <div>Block ID</div>
      <div>Multiplier</div>
      <div>Round time</div>
    </div>
    <div style="display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 6px; font-size: 0.85rem;">
      <div>${roundId}</div>
      <div>${multiplier}</div>
      <div>${roundTime}</div>
    </div>
  `;

  const pill = (text, extraStyle = '', title = '') => `
    <span style="display: inline-flex; align-items: center; gap: 6px; padding: 2px 6px; border-radius: 999px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(148, 163, 184, 0.35); font-size: 0.7rem; line-height: 1.1; min-width: 0; ${extraStyle}"${title ? ` title="${commEscape(title)}"` : ''}>
      ${text}
    </span>
  `;
  const pillCenter = (text, colorStyle = '', title = '') => pill(text, `width: 100%; justify-content: center; ${colorStyle}`, title);
  const pillLeft = (text, colorStyle = '', title = '') => pill(text, `width: 100%; justify-content: flex-start; ${colorStyle}`, title);
  const rankStyle = 'background: rgba(245, 158, 11, 0.18); border-color: rgba(245, 158, 11, 0.45); color: #fbbf24;';
  const clanStyle = 'background: rgba(59, 130, 246, 0.18); border-color: rgba(59, 130, 246, 0.45); color: #93c5fd;';
  const scoreStyle = 'background: rgba(16, 185, 129, 0.18); border-color: rgba(16, 185, 129, 0.45); color: #6ee7b7;';
  const boostStyle = 'background: rgba(239, 68, 68, 0.18); border-color: rgba(239, 68, 68, 0.45); color: #fca5a5;';
  const abilityStyle = 'background: rgba(168, 85, 247, 0.18); border-color: rgba(168, 85, 247, 0.45); color: #d8b4fe;';
  const ppsStyle = 'background: rgba(56, 189, 248, 0.18); border-color: rgba(56, 189, 248, 0.45); color: #7dd3fc;';
  const valueOrDash = (value) => (value == null ? '-' : formatCount(value));

  const gridTemplate = '20px 140px 1fr 1fr 30px 90px';
  const tableHeader = `
    <div style="margin-top: 6px; display: grid; grid-template-columns: ${gridTemplate}; gap: 3px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.7; text-align: center;">
      <div>Rank</div>
      <div>Clan</div>
      <div>Score</div>
      <div>Boost</div>
      <div>Abilities</div>
    </div>
  `;

  const buildBoostsUsedText = (entry) => {
    const abilities = entry?.used_abilities ?? entry?.usedAbilities ?? [];
    const fallbackActiveBoost = () => {
      const activeBoostValue = commParseNumber(entry?.active_boost_score ?? entry?.activeBoostScore);
      if (!Number.isFinite(activeBoostValue) || activeBoostValue <= 0) return '-';
      const icon = BOOST_ICON_MAP.boost || BOOST_ICON_MAP.powerboost || '*';
      return `${icon} ${formatCount(activeBoostValue)}`;
    };
    if (!Array.isArray(abilities) || abilities.length === 0) return fallbackActiveBoost();
    const totals = new Map();
    abilities.forEach((ability) => {
      const abilityId = ability?.nftGameAbilityId ?? null;
      const boostType = abilityId ? ABILITY_ID_TO_TYPE[abilityId] : null;
      if (!boostType) return;
      const count = Number(ability?.count || 0);
      if (count <= 0) return;
      totals.set(boostType, (totals.get(boostType) || 0) + count);
    });
    if (!totals.size) return fallbackActiveBoost();
    const categoryTotals = new Map();
    totals.forEach((count, type) => {
      const category = getBoostCategoryKey(type);
      categoryTotals.set(category, (categoryTotals.get(category) || 0) + count);
    });
    const ordered = BOOST_DISPLAY_ORDER
      .filter((category) => categoryTotals.has(category))
      .map((category) => {
        const icon = BOOST_ICON_MAP[category] || '*';
        const count = categoryTotals.get(category);
        return `<span class="boost-icon">${icon}</span> ${count}`;
      });
    if (!ordered.length) return fallbackActiveBoost();
    return ordered.join(' ');
  };

  const winnerBoostsText = buildBoostsUsedText(winnerStatsEntry);
  const winnerRow = winnerEntry ? `
    <div style="margin-top: 4px; display: grid; grid-template-columns: ${gridTemplate}; gap: 3px; align-items: center;">
      ${pillCenter('&#x1F3C6;', `${rankStyle} padding: 1px 4px;`)}
      ${pillLeft(winnerEntry.clan_name || winner || 'Unknown', `${clanStyle} overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`)}
      ${pillCenter(valueOrDash(winnerStatsEntry?.current_added_score), scoreStyle)}
      ${pillCenter(winnerBoostsText, `${boostStyle} overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`)}
      ${pillCenter(valueOrDash(winnerStatsEntry?.used_abilities_total), `${abilityStyle} padding: 1px 4px;`)}
    </div>
  ` : '';

  const topRows = topClans.map((entry, idx) => {
    const label = `#${idx + 1}`;
    const name = entry.clan_name || entry.name || (entry.clan_id ? `Clan ${entry.clan_id}` : 'Unknown');
    const boostsText = buildBoostsUsedText(entry);
    return `
      <div style="margin-top: 4px; display: grid; grid-template-columns: ${gridTemplate}; gap: 3px; align-items: center;">
        ${pillCenter(label, `${rankStyle} padding: 1px 4px;`)}
        ${pillLeft(name, `${clanStyle} overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`)}
        ${pillCenter(valueOrDash(entry.current_added_score), scoreStyle)}
        ${pillCenter(boostsText, `${boostStyle} overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`)}
        ${pillCenter(valueOrDash(entry.used_abilities_total), `${abilityStyle} padding: 1px 4px;`)}
      </div>
    `;
  }).join('');

  return {
    id: `last-finished-${roundId}`,
    htmlContent: `
      <div class="current-round-card" style="border-color: #94a3b8; background: rgba(15, 23, 42, 0.7);">
        <div style="font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.75; margin-bottom: 4px;">Last round</div>
        ${headerRow}
        ${tableHeader}
        ${winnerRow}
        ${topRows}
        ${boostHtml}
      </div>
    `
  };
}

function parseMultiplierValue(value) {
  if (value == null) return 1;
  const raw = String(value).toLowerCase().replace(/x/g, '').replace(/[^0-9.]/g, '').trim();
  const parsed = Number(raw);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
}

function estimateRoundDurationMinutes(multiplierValue) {
  if (multiplierValue >= 16) return 8;
  if (multiplierValue >= 8) return 10;
  if (multiplierValue >= 4) return 15;
  if (multiplierValue >= 2) return 20;
  return 30;
}

function resolveRoundPhase(roundTimeSeconds, multiplierValue) {
  if (!Number.isFinite(roundTimeSeconds)) return 'unknown';
  const duration = estimateRoundDurationMinutes(multiplierValue);
  const elapsedMin = roundTimeSeconds / 60;
  const ratio = duration > 0 ? elapsedMin / duration : 0;
  if (ratio < 0.33) return 'early';
  if (ratio < 0.7) return 'mid';
  return 'late';
}

function estimateBoostCostFromInputs(inputs) {
  if (!inputs) return 0;
  const powerupCost = Number.isFinite(Number(inputs.powerupCost)) ? Number(inputs.powerupCost) : BOOST_COST_GMT_ESTIMATE.powerup;
  const clanCost = Number.isFinite(Number(inputs.clanCost)) ? Number(inputs.clanCost) : BOOST_COST_GMT_ESTIMATE.clanpowerup;
  const total =
    (Number(inputs.powerupCount) || 0) * powerupCost +
    (Number(inputs.clanActivations) || 0) * clanCost +
    (Number(inputs.powerboost) || 0) * BOOST_COST_GMT_ESTIMATE.powerboost +
    (Number(inputs.instant) || 0) * BOOST_COST_GMT_ESTIMATE.instant +
    (Number(inputs.echo) || 0) * BOOST_COST_GMT_ESTIMATE.echo +
    (Number(inputs.focus) || 0) * BOOST_COST_GMT_ESTIMATE.focus;
  return Number.isFinite(total) ? total : 0;
}

function getBoostEntriesFromInputs(inputs) {
  if (!inputs) return [];
  const entries = [
    { type: 'powerup', count: inputs.powerupCount },
    { type: 'clanpowerup', count: inputs.clanActivations },
    { type: 'powerboost', count: inputs.powerboost },
    { type: 'instant', count: inputs.instant },
    { type: 'echo', count: inputs.echo },
    { type: 'focus', count: inputs.focus }
  ];
  return entries.filter((entry) => Number(entry.count) > 0);
}

function formatBoostEntries(entries) {
  const parts = (Array.isArray(entries) ? entries : [])
    .filter((entry) => Number(entry.count) > 0)
    .map((entry) => `${BOOST_ICON_MAP[entry.type] || '*'} ${entry.count}`);
  return parts.length ? parts.join(' ') : 'None';
}

function buildBoostAdviceText(inputs) {
  const entries = getBoostEntriesFromInputs(inputs);
  return entries.length ? formatBoostEntries(entries) : 'No boosts configured';
}

function buildBoostTimingAdvice(inputs, phase, multiplierValue, signalStatus, strictMode) {
  const entries = getBoostEntriesFromInputs(inputs);
  if (!entries.length) {
    return '<div>Now: No boosts configured</div>';
  }

  const allText = formatBoostEntries(entries);
  if (strictMode && signalStatus !== 'green') {
    return `<div>Now: Hold (strict)</div><div>Later: ${allText}</div>`;
  }
  if (signalStatus === 'red') {
    return `<div>Now: Hold (signal red)</div><div>Later: ${allText}</div>`;
  }

  const now = [];
  const later = [];
  const isEarly = phase === 'early';
  const isMid = phase === 'mid';
  const isLate = phase === 'late';

  entries.forEach((entry) => {
    switch (entry.type) {
      case 'powerup':
      case 'clanpowerup':
        if (isEarly || (isMid && multiplierValue >= 2)) now.push(entry);
        else later.push(entry);
        break;
      case 'echo':
      case 'powerboost':
        if (!isLate) now.push(entry);
        else later.push(entry);
        break;
      case 'focus':
        if (isEarly) later.push(entry);
        else now.push(entry);
        break;
      case 'instant':
        if (isLate) now.push(entry);
        else later.push(entry);
        break;
      default:
        now.push(entry);
        break;
    }
  });

  const phaseLabel = phase ? phase.toUpperCase() : 'UNKNOWN';
  const nowText = now.length ? formatBoostEntries(now) : 'Hold for better timing';
  const laterText = later.length ? formatBoostEntries(later) : 'None';
  return `<div>Now (${phaseLabel}): ${nowText}</div><div>Later: ${laterText}</div>`;
}

function computeLiveBoostSignal({
  leader,
  target,
  multiplierValue,
  roundTimeSeconds,
  profile,
  myPercentValue,
  estimatedCost
}) {
  if (!leader || !target) {
    return {
      status: 'neutral',
      label: 'Waiting for data',
      phase: 'unknown',
      scoreGapPct: null
    };
  }
  const leaderScore = Number(leader?.current_added_score) || 0;
  const targetScore = Number(target?.current_added_score) || 0;
  const leaderBoost = Number(leader?.active_boost_score) || 0;
  const targetBoost = Number(target?.active_boost_score) || 0;
  const scoreRatio = leaderScore > 0 ? targetScore / leaderScore : 1;
  const boostRatio = leaderBoost > 0 ? targetBoost / leaderBoost : 1;
  const phase = resolveRoundPhase(roundTimeSeconds, multiplierValue);

  let score = 0;
  if (scoreRatio >= 0.9) score += 40;
  else if (scoreRatio >= 0.75) score += 25;
  else if (scoreRatio >= 0.6) score += 10;
  else score -= 5;

  if (boostRatio >= 0.9) score += 10;
  else if (boostRatio >= 0.6) score += 5;
  else score -= 5;

  if (multiplierValue >= 4) score += 15;
  else if (multiplierValue >= 2) score += 8;

  if (phase === 'early') score += 10;
  else if (phase === 'mid') score += 5;
  else if (phase === 'late') score -= 5;

  if (Number.isFinite(myPercentValue)) {
    if (myPercentValue >= 2) score += 10;
    else if (myPercentValue >= 1) score += 5;
  }

  const roundCap = Number(profile?.roundCap) || 0;
  if (roundCap > 0 && Number.isFinite(estimatedCost) && estimatedCost > roundCap) {
    score -= 10;
  }

  const thresholds = { conservative: 55, balanced: 45, aggressive: 35 };
  const baseThreshold = thresholds[profile?.mode] || thresholds[CALC_PROFILE_DEFAULT];
  let status = 'red';
  if (score >= baseThreshold + 10) status = 'green';
  else if (score >= baseThreshold) status = 'orange';

  let label = status === 'green' ? 'Boost now' : status === 'orange' ? 'Wait / consider' : 'Skip';
  if (profile?.strict && status !== 'green') {
    label = 'Skip (strict)';
    status = 'red';
  }

  const scoreGapPct = leaderScore > 0 ? Math.max(0, (leaderScore - targetScore) / leaderScore) * 100 : null;

  return {
    status,
    label,
    phase,
    scoreGapPct
  };
}

function buildLiveBoostIndicator({ leadingClans, roundTime, multiplier }) {
  const list = Array.isArray(leadingClans) ? leadingClans : [];
  const profile = readCalculatorProfileSettings();
  const inputs = readCalculatorInputsSafe();
  const estimatedCost = estimateBoostCostFromInputs(inputs);
  const multiplierValue = parseMultiplierValue(multiplier);
  const roundTimeSeconds = parseRoundTimerSeconds(roundTime);
  const storedProfileName = getStoredLiveProfileClan() || liveProfileClanName || '';
  const resolvedClanName = (liveMyClanName || commSession?.clanName || storedProfileName || '').trim();
  const resolvedClanId = commParseNumber(liveMyClanId);
  const rosterOptions = Array.isArray(liveRosterBaseLeaders)
    ? liveRosterBaseLeaders.map((row) => buildRosterEntryFromRow(row)).filter(Boolean)
    : [];
  const leader = list[0] || null;
  const findById = (entries, id) => entries.find((entry) => {
    const entryId = commParseNumber(entry?.clan_id);
    return Number.isFinite(entryId) && Math.trunc(entryId) === Math.trunc(id);
  });
  const findByName = (entries, name) => entries.find((entry) => commMatchesUsername(entry?.clan_name || entry?.name, name));
  let target = null;
  if (Number.isFinite(resolvedClanId)) {
    target = findById(list, resolvedClanId) || findById(rosterOptions, resolvedClanId);
  }
  if (!target && resolvedClanName) {
    target = findByName(list, resolvedClanName) || findByName(rosterOptions, resolvedClanName);
  }
  if (!target && liveRosterMyClan) {
    target = buildRosterEntryFromRow(liveRosterMyClan);
  }
  const displayClanName = target?.clan_name || target?.name || resolvedClanName || 'Unknown';

  const myPercentCandidate = Number.isFinite(liveMyClanPercentValue)
    ? liveMyClanPercentValue
    : commParseNumber(target?.percentage ?? target?.share ?? target?.pct ?? null);

  const signal = computeLiveBoostSignal({
    leader,
    target,
    multiplierValue,
    roundTimeSeconds,
    profile,
    myPercentValue: myPercentCandidate,
    estimatedCost
  });

  const statusClass = signal.status ? `live-boost-indicator ${signal.status}` : 'live-boost-indicator';
  const phaseLabel = signal.phase ? signal.phase.toUpperCase() : 'UNKNOWN';
  const gapText = Number.isFinite(signal.scoreGapPct) ? `${formatDecimal(signal.scoreGapPct, 1)}%` : '-';
  const boostText = buildBoostTimingAdvice(inputs, signal.phase, multiplierValue, signal.status, profile.strict);
  const budgetText = profile.budgetWeek > 0 ? `${formatDecimal(profile.budgetWeek, 1)} GMT/wk` : '-';
  const roundCapText = profile.roundCap > 0 ? `${formatDecimal(profile.roundCap, 1)} / round` : '-';
  const profileLabel = profile.mode ? profile.mode.charAt(0).toUpperCase() + profile.mode.slice(1) : 'Balanced';
  const strictLabel = profile.strict ? ' (strict)' : '';
  const percentText = Number.isFinite(myPercentCandidate) ? `${formatDecimal(myPercentCandidate, 2)}%` : null;

  return `
    <div class="${statusClass}">
      <div class="live-boost-indicator__main">
        <div class="live-boost-indicator__label">${commEscape(signal.label || 'Waiting')}</div>
        <div class="live-boost-indicator__meta">
          Phase ${phaseLabel} | Gap ${gapText} | Mult ${multiplierValue}x${percentText ? ` | My ${percentText}` : ''}
        </div>
        <div class="live-boost-indicator__boosts">${boostText}</div>
        <div class="live-boost-indicator__budget">Budget ${budgetText} | Cap ${roundCapText}</div>
      </div>
      <div class="live-boost-indicator__controls">
        <div class="live-boost-indicator__profile">Clan: ${commEscape(displayClanName)}</div>
        <div class="live-boost-indicator__profile">Profile: ${commEscape(profileLabel)}${commEscape(strictLabel)}</div>
      </div>
    </div>
  `;
}

async function refreshLiveMyClanPercent(force = false) {
  if (liveMyClanPercentPending) return;
  const now = Date.now();
  if (!force && now - liveMyClanPercentUpdatedAt < 30000) return;
  liveMyClanPercentPending = true;
  try {
    const data = await commRequestPage('EXTRACT_MY_CLAN_PERCENT');
    const percentText = data?.percentText ?? null;
    liveMyClanPercent = percentText;
    liveMyClanPercentValue = commParseNumber(percentText);
    if (typeof window.__refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
      window.__refreshCurrentRoundCard();
    }
  } catch (_) {
    // Ignore if extraction fails
  } finally {
    liveMyClanPercentUpdatedAt = now;
    liveMyClanPercentPending = false;
  }
}

async function refreshLiveMyClanName(force = false) {
  if (liveMyClanNamePending) return;
  const now = Date.now();
  if (!force && liveMyClanNameUpdatedAt && now - liveMyClanNameUpdatedAt < 60000) return;
  liveMyClanNamePending = true;
  let nameText = null;
  let clanIdValue = null;
  try {
    try {
      const data = await commRequestPage('EXTRACT_MY_CLAN_NAME');
      nameText = typeof data?.name === 'string' ? data.name : (typeof data === 'string' ? data : null);
      nameText = commCleanClanDisplayName(nameText);
      clanIdValue = commParseNumber(data?.clanId ?? data?.clan_id ?? null);
    } catch (_) {
      // ignore extract errors and fall back to API
    }
    if (Number.isFinite(clanIdValue)) {
      liveMyClanId = Math.trunc(clanIdValue);
    }
    if (nameText && nameText.trim()) {
      liveMyClanName = commCleanClanDisplayName(nameText);
      if (!getStoredLiveProfileClan()) {
        liveProfileClanName = liveMyClanName;
        setStoredLiveProfileClan(liveMyClanName);
      }
    } else if (liveMyClanName && !commCleanClanDisplayName(liveMyClanName)) {
      liveMyClanName = null;
    }
    if (!nameText && !Number.isFinite(clanIdValue)) {
      try {
        const apiData = await commRequestPage('FETCH_CLAN_API', { limit: 1, skip: 0, count: 0 });
        const myClanRaw = apiData?.myClan?.data || apiData?.myClan || {};
        const clanIdApi = commParseNumber(apiData?.clanId ?? myClanRaw?.id ?? myClanRaw?.clanId ?? null);
        const clanNameApi = commCleanClanDisplayName(myClanRaw?.name || myClanRaw?.clanName || null);
        if (Number.isFinite(clanIdApi)) liveMyClanId = Math.trunc(clanIdApi);
        if (clanNameApi && clanNameApi.trim()) {
          liveMyClanName = clanNameApi.trim();
          if (!getStoredLiveProfileClan()) {
            liveProfileClanName = liveMyClanName;
            setStoredLiveProfileClan(liveMyClanName);
          }
        }
      } catch (_) {
        // ignore api fallback errors
      }
    }

    if (
      (nameText && nameText.trim()) ||
      Number.isFinite(clanIdValue) ||
      liveMyClanName ||
      Number.isFinite(liveMyClanId)
    ) {
      if (typeof window.__refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
        window.__refreshCurrentRoundCard();
      }
    }
  } catch (_) {
    // Ignore if extraction fails
  } finally {
    liveMyClanNameUpdatedAt = now;
    liveMyClanNamePending = false;
  }
}

function buildCurrentRoundLiveCard({
  blockId,
  timer,
  multiplier,
  leadingClans,
  boosts,
  clanBoosts,
  clanLimit,
  title = 'Current round',
  showLimit = true
}) {
  const normalizeRoundValue = (value) => {
    if (value == null) return null;
    const raw = String(value).trim();
    if (!raw || raw === '-' || raw === '-:-' || raw.toLowerCase() === 'n/a') return null;
    return raw;
  };
  const roundId = normalizeRoundValue(blockId)
    || normalizeRoundValue(currentRoundInfo?.blockId)
    || '-';
  const roundTime = normalizeRoundValue(timer)
    || normalizeRoundValue(currentRoundInfo?.timer)
    || '-';
  const multValue = normalizeRoundValue(multiplier)
    || normalizeRoundValue(currentRoundInfo?.multiplier)
    || '1x';
  const timingNormalized = commNormalizeRoundTiming(roundTime, multValue);
  let displayRoundTime = timingNormalized.roundTime ?? roundTime;
  let displayMultiplier = timingNormalized.multiplier ?? multValue;

  const normalizeTimerCandidate = (value) => {
    if (value == null) return null;
    const raw = String(value).trim();
    if (!raw || raw === '-' || raw.toLowerCase() === 'n/a') return null;
    return raw.includes(':') ? raw : null;
  };

  const normalizeMultiplierCandidate = (value) => {
    if (value == null) return null;
    const raw = String(value).trim();
    if (!raw || raw === '-' || raw.toLowerCase() === 'n/a') return null;
    if (raw.includes(':')) return null;
    return raw;
  };

  const timerFromRound = normalizeTimerCandidate(displayRoundTime);
  const timerFromMultiplier = normalizeTimerCandidate(displayMultiplier);
  const timerFallback =
    normalizeTimerCandidate(latestRoundTimerValue)
    || normalizeTimerCandidate(currentRoundInfo?.timer)
    || null;
  const multiplierFromRound = normalizeMultiplierCandidate(displayRoundTime);
  const multiplierFromMultiplier = normalizeMultiplierCandidate(displayMultiplier);

  displayRoundTime = timerFromRound || timerFromMultiplier || timerFallback || '-';
  displayMultiplier = multiplierFromMultiplier || multiplierFromRound || currentRoundInfo?.multiplier || '1x';

  const displayMode = commNormalizeLiveDisplayMode(liveDisplayMode);
  const isRawMode = displayMode === 'raw';
  const powerSource = 'roster';
  const powerSourceLabel = LIVE_POWER_SOURCE_LABELS[powerSource] || powerSource;
  const useRosterPower = true;
  const scoreSource = commNormalizeLiveScoreSource(liveScoreSource);
  const scoreSourceLabel = LIVE_SCORE_SOURCE_LABELS[scoreSource] || scoreSource;
  const scoreHeaderLabel = scoreSource ? `Score (${scoreSource})` : 'Score';
  const scoreSourceKind = LIVE_SCORE_SOURCE_KIND[scoreSource] || 'added';

    const safeLimit = Number(clanLimit) || 10;
    const rosterRefresh =
      typeof refreshLiveRosterCache === 'function'
        ? refreshLiveRosterCache
        : (typeof window !== 'undefined' && typeof window.refreshLiveRosterCache === 'function'
          ? window.refreshLiveRosterCache
          : null);
    if (rosterRefresh) {
      rosterRefresh({ limit: safeLimit }).catch(() => {});
    }
    const list = Array.isArray(leadingClans) ? leadingClans : [];
    const simEntries = list.filter((entry) => entry?.simulated);
    const nonSimEntries = list.filter((entry) => !entry?.simulated);
    const simEntry = simEntries.length ? simEntries[0] : null;
    let roundSeconds = parseRoundTimerSeconds(displayRoundTime);
    if (!Number.isFinite(roundSeconds) && Number.isFinite(latestRoundTimerSeconds)) {
      roundSeconds = latestRoundTimerSeconds;
    }
    const calcRoundSeconds = applyRoundTimeCalcOffset(roundSeconds);
    const roundSecondsForScore = Number.isFinite(calcRoundSeconds)
      ? calcRoundSeconds
      : (Number.isFinite(liveLastRoundSeconds) ? liveLastRoundSeconds : null);
    if (Number.isFinite(calcRoundSeconds)) {
      liveLastRoundSeconds = calcRoundSeconds;
    }
    const multiplierValue = parseMultiplierValue(displayMultiplier);
    const resolveRosterPower = (entry) => {
      if (!useRosterPower) {
        const base = commParseNumber(entry?.clan_power);
        return Number.isFinite(base) ? base : null;
      }
      const rosterPower = commParseNumber(entry?.roster_power);
      if (Number.isFinite(rosterPower)) return rosterPower;
      if (entry?.roster_only) {
        const base = commParseNumber(entry?.clan_power);
        return Number.isFinite(base) ? base : null;
      }
      const fallbackPower = commParseNumber(
        entry?.power ??
        entry?.clanPower ??
        entry?.clan_power ??
        entry?.rosterPower ??
        null
      );
      if (Number.isFinite(fallbackPower)) return fallbackPower;
      return null;
    };
    const scoreContextMap = new Map();
    const getScoreContext = (entry) => {
      if (!entry) {
        return {
          candidates: {},
          addedScore: null,
          baseScoreValue: null,
          totalScoreValue: null,
          selectedScoreValue: null
        };
      }
      const cached = scoreContextMap.get(entry);
      if (cached) return cached;
      const baseCandidates = entry?.score_candidates && Object.keys(entry.score_candidates).length
        ? { ...entry.score_candidates }
        : commExtractLiveScoreCandidates(entry);
      const entryKey = normalizeClanKey(entry);
      const wsAdded = entryKey ? commParseNumber(liveWsAddedScoreByClan.get(entryKey)) : null;
      const rawAdded = Number.isFinite(wsAdded) ? wsAdded : null;
      const rawBoostScore = commParseNumber(
        entry?.activeBoostScore ??
        entry?.active_boost_score ??
        null
      );
      if (Number.isFinite(rawAdded)) {
        if (!Number.isFinite(baseCandidates.currentAddedScore)) {
          baseCandidates.currentAddedScore = rawAdded;
        }
        if (!Number.isFinite(baseCandidates.current_added_score)) {
          baseCandidates.current_added_score = rawAdded;
        }
      }
      const basePowerValue = resolveRosterPower(entry);
      const powerFactor = getPowerBoostFactor(entry);
      const effectivePower = Number.isFinite(basePowerValue) ? basePowerValue * powerFactor : basePowerValue;
      const basePpsCandidates = [
        { value: entry?.base_pps, label: 'entry.base_pps' },
        { value: entry?.basePps, label: 'entry.basePps' },
        { value: baseCandidates?.base_pps, label: 'candidates.base_pps' },
        { value: baseCandidates?.basePps, label: 'candidates.basePps' }
      ];
      const minPps = Number.isFinite(basePowerValue) ? basePowerValue * BASE_PPS_MIN_FACTOR : null;
      const maxPps = Number.isFinite(basePowerValue) ? basePowerValue * BASE_PPS_MAX_FACTOR : null;
      let basePpsValue = null;
      let basePpsSource = null;
      for (const candidate of basePpsCandidates) {
        const parsed = commParseNumber(candidate.value);
        if (!Number.isFinite(parsed) || parsed <= 0) continue;
        if (Number.isFinite(minPps) && Number.isFinite(maxPps)) {
          if (parsed < minPps || parsed > maxPps) continue;
        }
        basePpsValue = parsed;
        basePpsSource = candidate.label;
        break;
      }
      if (!Number.isFinite(basePpsValue)) {
        basePpsValue = Number.isFinite(basePowerValue) ? basePowerValue * LIVE_PPS_FACTOR : null;
        basePpsSource = Number.isFinite(basePpsValue) ? 'power*factor' : null;
      }
      const effectivePpsValue = Number.isFinite(effectivePower) ? effectivePower * LIVE_PPS_FACTOR : null;
      // Base score uses base PPS when available (boost comes from rawAdded)
      const baseScoreFromPps = (!entry?.simulated && Number.isFinite(roundSecondsForScore) && Number.isFinite(basePpsValue))
        ? basePpsValue * roundSecondsForScore
        : null;
      let baseScoreValue = Number.isFinite(baseScoreFromPps) ? baseScoreFromPps : null;
      let baseScoreSource = Number.isFinite(baseScoreFromPps) ? 'pps*seconds' : null;
      const totalScoreValue = Number.isFinite(baseScoreValue)
        ? baseScoreValue + (Number.isFinite(rawAdded) ? rawAdded : 0)
        : (Number.isFinite(rawAdded) ? rawAdded : null);

      // Debug logging for score calculation
      const debugName = entry?.clanName ?? entry?.clan_name ?? entry?.name ?? entry?.clan?.name ?? null;
      if (window.__debugLiveScores && debugName && commShouldDebugClan(debugName)) {
        console.log(`[MW SCORE DEBUG] ${debugName}:`, {
          basePower: basePowerValue,
          roundSeconds: roundSecondsForScore,
          pps: basePpsValue,
          ppsSource: basePpsSource,
          baseScore: baseScoreValue,
          baseScoreSource,
          rawAdded: rawAdded,
          rawBoostScore,
          totalScore: totalScoreValue,
          formula: `${basePowerValue} x 1.647 x ${roundSecondsForScore} + ${rawAdded} = ${totalScoreValue}`
        });
      }
      if (Number.isFinite(baseScoreValue) && !Number.isFinite(baseCandidates.basePoints)) {
        baseCandidates.basePoints = baseScoreValue;
      }
      if (Number.isFinite(totalScoreValue)) {
        LIVE_SCORE_TOTAL_FIELDS.forEach((field) => {
          if (!Number.isFinite(baseCandidates[field])) baseCandidates[field] = totalScoreValue;
        });
      }
      const selectedScoreValue = commResolveLiveScoreFromCandidates(baseCandidates, scoreSource);
      const context = {
        candidates: baseCandidates,
        addedScore: Number.isFinite(rawAdded) ? rawAdded : null,
        basePpsValue,
        basePpsSource,
        effectivePpsValue,
        baseScoreValue,
        totalScoreValue,
        selectedScoreValue: Number.isFinite(selectedScoreValue) ? selectedScoreValue : null
      };
      entry.score_candidates = baseCandidates;
      scoreContextMap.set(entry, context);
      return context;
    };
    const hasBoostSignal = (entry) => {
      if (!entry) return false;
      const addedScore = commParseNumber(entry?.current_added_score ?? entry?.currentAddedScore);
      if (Number.isFinite(addedScore) && addedScore > 0) return true;
      const boostScore = commParseNumber(entry?.active_boost_score ?? entry?.activeBoostScore);
      if (Number.isFinite(boostScore) && boostScore > 0) return true;
      const boostMap = entry?.boosts;
      if (boostMap && typeof boostMap === 'object' && !Array.isArray(boostMap)) {
        return Object.values(boostMap).some((value) => Number(value || 0) > 0);
      }
      return false;
    };
    const getTotalScore = (entry) => {
      if (!entry) return 0;
      const context = getScoreContext(entry);
      const added = context.addedScore;
      const selected = context.selectedScoreValue;
      if (isRawMode) return Number.isFinite(selected) ? selected : 0;
      if (entry?.simulated) {
        return Number.isFinite(selected) ? selected : 0;
      }
      if (scoreSourceKind === 'added') {
        if (Number.isFinite(context.baseScoreValue)) {
          return context.baseScoreValue + (Number.isFinite(added) ? added : 0);
        }
        return Number.isFinite(added) ? added : 0;
      }
      if (scoreSourceKind === 'base') {
        if (Number.isFinite(context.baseScoreValue)) return context.baseScoreValue;
        return Number.isFinite(selected) ? selected : 0;
      }
      if (Number.isFinite(selected)) return selected;
      return 0;
    };
    const displayBase = nonSimEntries.slice(0, safeLimit);
    const displayClans = simEntry
      ? (displayBase.some((entry) => entry === simEntry) ? displayBase : [...displayBase, simEntry])
      : displayBase;
    const storedProfileName = getStoredLiveProfileClan() || liveProfileClanName || '';
    const resolvedMyClanId = commParseNumber(liveMyClanId ?? commSession?.clanId);
    const resolvedMyClanName = liveMyClanName
      || commSession?.clanName
      || (!isSimClanName(storedProfileName) ? storedProfileName : '')
      || '';
    if (!storedProfileName && (liveMyClanName || commSession?.clanName)) {
      setStoredLiveProfileClan(liveMyClanName || commSession.clanName);
    }
    let myClanEntry = null;
    if (Number.isFinite(resolvedMyClanId)) {
      myClanEntry = list.find((entry) => {
        const entryId = commParseNumber(entry?.clan_id);
        return Number.isFinite(entryId) && Math.trunc(entryId) === Math.trunc(resolvedMyClanId);
      }) || null;
    }
    if (!myClanEntry && resolvedMyClanName) {
      myClanEntry = list.find((entry) => commMatchesUsername(entry?.clan_name || entry?.name, resolvedMyClanName)) || null;
    }
    if (myClanEntry && !displayClans.some((entry) => normalizeClanKey(entry) === normalizeClanKey(myClanEntry))) {
      myClanEntry = null;
    }
    const indicatorHtml = '';

  const pill = (text, extraStyle = '') => `
    <span style="display: inline-flex; align-items: center; gap: 6px; padding: 2px 6px; border-radius: 999px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(148, 163, 184, 0.35); font-size: 0.7rem; line-height: 1.1; min-width: 0; ${extraStyle}">
      ${text}
    </span>
  `;
  const pillCenter = (text, colorStyle = '') => pill(text, `width: 100%; justify-content: center; ${colorStyle}`);
  const pillLeft = (text, colorStyle = '') => pill(text, `width: 100%; justify-content: flex-start; ${colorStyle}`);
  const rankStyle = 'background: rgba(245, 158, 11, 0.18); border-color: rgba(245, 158, 11, 0.45); color: #fbbf24;';
  const clanStyle = 'background: rgba(59, 130, 246, 0.18); border-color: rgba(59, 130, 246, 0.45); color: #93c5fd;';
  const scoreStyle = 'background: rgba(16, 185, 129, 0.18); border-color: rgba(16, 185, 129, 0.45); color: #6ee7b7;';
  const boostStyle = 'background: rgba(239, 68, 68, 0.18); border-color: rgba(239, 68, 68, 0.45); color: #fca5a5;';
  const abilityStyle = 'background: rgba(168, 85, 247, 0.18); border-color: rgba(168, 85, 247, 0.45); color: #d8b4fe;';
  const ppsStyle = 'background: rgba(56, 189, 248, 0.18); border-color: rgba(56, 189, 248, 0.45); color: #7dd3fc;';
  const okStyle = 'background: rgba(16, 185, 129, 0.18); border-color: rgba(16, 185, 129, 0.45); color: #6ee7b7;';
  const warnStyle = 'background: rgba(239, 68, 68, 0.18); border-color: rgba(239, 68, 68, 0.45); color: #fca5a5;';
  const neutralStyle = 'background: rgba(148, 163, 184, 0.15); border-color: rgba(148, 163, 184, 0.35); color: #cbd5f5;';
  const formatTime = (ts) => (ts ? new Date(ts).toLocaleTimeString() : '-');
  const formatAge = (ms) => {
    if (!Number.isFinite(ms)) return '-';
    const totalSec = Math.max(0, Math.floor(ms / 1000));
    if (totalSec < 60) return `${totalSec}s`;
    const minutes = Math.floor(totalSec / 60);
    const seconds = totalSec % 60;
    if (minutes < 60) return `${minutes}m ${seconds}s`;
    const hours = Math.floor(minutes / 60);
    const remMin = minutes % 60;
    return `${hours}h ${remMin}m`;
  };
  const apiConnected = liveApiLastOkAt && (Date.now() - liveApiLastOkAt < LIVE_API_STATUS_WINDOW_MS());
  const summaryConnected = liveSummaryLastAt && (Date.now() - liveSummaryLastAt < LIVE_SUMMARY_STATUS_WINDOW_MS);
  const apiStatusText = apiConnected
    ? `API: ok ${formatTime(liveApiLastOkAt)}`
    : (liveApiLastStatus === 200 ? 'API - no sync required' : `API: off ${liveApiLastStatus ? liveApiLastStatus : '-'}`);
  const summaryStatusText = summaryConnected
    ? `Summary: ok ${formatTime(liveSummaryLastAt)}`
    : 'Summary: off';
  const apiAgeText = liveApiStateLastAttemptAt
    ? `Last API call: ${formatAge(Date.now() - liveApiStateLastAttemptAt)} ago`
    : 'Last API call: -';
  const cacheStatus = (typeof getLiveCacheStatus === 'function') ? getLiveCacheStatus() : { ok: false, label: 'Cache: -', title: 'Cache status unavailable', lastSync: '-', lastApplied: null };
  const formatScoreValue = (value) => {
    const num = Number(value);
    if (!Number.isFinite(num)) return '-';
    const millions = num / 1_000_000;
    const decimals = Math.abs(millions) >= 100 ? 0 : 2;
    return `${formatDecimal(millions, decimals)}M`;
  };
  const formatPowerValue = (value) => {
    const num = Number(value);
    return Number.isFinite(num) ? formatDecimal(num, 0) : '-';
  };
  const normalizePercentValue = (value) => {
    if (!Number.isFinite(value)) return null;
    if (value > 0 && value <= 1) return value * 100;
    return value;
  };
  const gridTemplate = '60px 1.35fr 1fr 1.4fr 0.9fr';
  const showPowerControl = false;
  const showScoreControl = false;
  const showDisplayModeControl = false;
  const showDebugTables = false;

  const headerRow = `
    <div style="display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 6px; font-size: 0.75rem; text-transform: uppercase; opacity: 0.7; margin-top: 6px;">
      <div>Round ID</div>
      <div>Round time</div>
      <div>Multiplier</div>
    </div>
    <div style="display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 6px; font-size: 0.85rem;">
      <div>${roundId}</div>
      <div>${displayRoundTime}</div>
      <div>${displayMultiplier}</div>
    </div>
  `;

    const tableHeader = `
      <div style="margin-top: 6px; display: grid; grid-template-columns: ${gridTemplate}; gap: 6px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.7; text-align: left;">
        <div>Pos</div>
        <div>Clan Name</div>
        <div>Score</div>
        <div>Boosts</div>
        <div>Power</div>
      </div>
    `;

    const boostedTotalSource = (() => {
      if (Array.isArray(liveApiStateRaw) && liveApiStateRaw.length > 0) {
        return buildLiveRoundRowsFromApi(liveApiStateRaw, liveApiStateRaw.length);
      }
      if (nonSimEntries.length) return nonSimEntries;
      return displayClans;
    })();
    const getHeldScore = (entry) => {
      const resolved = getTotalScore(entry);
      if (Number.isFinite(resolved) && resolved > 0) {
        const key = normalizeClanKey(entry);
        if (key) liveScoreHoldCache.set(key, { value: resolved, ts: Date.now() });
        return resolved;
      }
      const key = normalizeClanKey(entry);
      if (key) {
        const cached = liveScoreHoldCache.get(key);
        if (cached && Date.now() - cached.ts <= LIVE_SCORE_HOLD_MS) {
          return cached.value;
        }
      }
      return 0;
    };
    const boostedTotalSum = boostedTotalSource.reduce((sum, entry) => {
      const value = getHeldScore(entry);
      return sum + value;
    }, 0);
    const boostedTotalCandidate = boostedTotalSum;
    const boostedTotal = Number.isFinite(boostedTotalCandidate) && boostedTotalCandidate > 0
      ? boostedTotalCandidate
      : null;
    liveTotalScoreCurrent = boostedTotal;
    const totalScoreLabel = Number.isFinite(boostedTotalCandidate) && boostedTotalCandidate > 0
      ? formatScoreValue(boostedTotalCandidate)
      : '-';
    updateLiveTotalScoreSeries(roundId, roundSeconds, boostedTotalCandidate);
    const totalScoreChart = buildLiveTotalScoreChart(liveTotalScoreSeries);
    const headerMetaRow = `
      <span style="padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.35); background: rgba(15, 23, 42, 0.55); font-size: 0.75rem; opacity: 0.85;">
        Total score: ${totalScoreLabel}
      </span>
      <span style="padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.35); background: rgba(15, 23, 42, 0.55); font-size: 0.7rem; opacity: 0.75;">
        ${commEscape(apiAgeText)}
      </span>
      <span title="${commEscape(cacheStatus.title)}" style="padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.35); background: rgba(15, 23, 42, 0.55); font-size: 0.7rem; opacity: 0.75; color: ${cacheStatus.ok ? '#86efac' : '#fca5a5'};">
        ${commEscape(cacheStatus.label)}
      </span>
      
    `;
    const sortCandidates = [...nonSimEntries, ...simEntries];
    const entryMap = new Map();
    const orphanEntries = [];
    sortCandidates.forEach((entry) => {
      const key = normalizeClanKey(entry);
      if (!key) {
        orphanEntries.push(entry);
        return;
      }
      entryMap.set(key, entry);
      liveBoostEntryCache.set(key, entry);
    });
    let sortedAll = [];
    const normalizedRoundId = normalizeBlockId(roundId);
    if (normalizedRoundId && normalizedRoundId !== liveBoostSortRoundId) {
      liveBoostSortRoundId = normalizedRoundId;
      liveBoostSortOrder = [];
      liveBoostSortUpdatedAt = 0;
      liveStableSortOrder = [];
      liveStableSortAt = 0;
    }
    const getSortScore = (entry) => {
      const current = getHeldScore(entry);
      const key = normalizeClanKey(entry);
      if (!key) return current;
      if (!Number.isFinite(current) || current <= 0) {
        const cached = liveScoreSortCache.get(key);
        return cached && Number.isFinite(cached.value) ? cached.value : 0;
      }
      const cached = liveScoreSortCache.get(key);
      const prev = cached && Number.isFinite(cached.value) ? cached.value : null;
      const next = Number.isFinite(prev)
        ? (prev * (1 - LIVE_SCORE_SORT_ALPHA) + current * LIVE_SCORE_SORT_ALPHA)
        : current;
      liveScoreSortCache.set(key, { value: next, ts: Date.now() });
      return next;
    };
    const prevOrder = new Map(liveStableSortOrder.map((key, idx) => [key, idx]));
    const nowSortTs = Date.now();
    const shouldResort = !liveStableSortOrder.length || (nowSortTs - liveStableSortAt >= LIVE_SORT_REFRESH_MS);
    let didResort = false;
    if (shouldResort) {
      sortedAll = [...sortCandidates].sort((a, b) => {
        const aScore = getSortScore(a);
        const bScore = getSortScore(b);
        const diff = bScore - aScore;
        if (diff !== 0) {
          const maxScore = Math.max(Math.abs(aScore), Math.abs(bScore));
          const hysteresis = Math.max(LIVE_SCORE_SORT_HYSTERESIS_MIN, maxScore * LIVE_SCORE_SORT_HYSTERESIS_PCT);
          if (Math.abs(diff) > hysteresis) return diff;
        }
        const aKey = normalizeClanKey(a);
        const bKey = normalizeClanKey(b);
        const aIdx = prevOrder.get(aKey);
        const bIdx = prevOrder.get(bKey);
        if (Number.isFinite(aIdx) && Number.isFinite(bIdx)) return aIdx - bIdx;
        if (Number.isFinite(aIdx)) return -1;
        if (Number.isFinite(bIdx)) return 1;
        return 0;
      });
      liveStableSortOrder = sortedAll.map((entry) => normalizeClanKey(entry)).filter((key) => key);
      liveStableSortAt = nowSortTs;
      didResort = true;
    } else {
      const orderSet = new Set(liveStableSortOrder);
      entryMap.forEach((_entry, key) => {
        if (!orderSet.has(key)) {
          liveStableSortOrder.push(key);
          orderSet.add(key);
        }
      });
      sortedAll = liveStableSortOrder
        .map((key) => entryMap.get(key) || liveBoostEntryCache.get(key))
        .filter(Boolean);
    }
    liveBoostSortOrder = liveStableSortOrder.slice();
    if (orphanEntries.length) {
      orphanEntries.sort((a, b) => getSortScore(b) - getSortScore(a));
      sortedAll = [...sortedAll, ...orphanEntries];
    }
    const canonicalClanKey = (entry) => {
      if (!entry) return '';
      const resolvedId = typeof commResolveClanIdFromItem === 'function'
        ? commResolveClanIdFromItem(entry, { silent: true })
        : commParseNumber(entry?.clan_id ?? entry?.clanId ?? entry?.id ?? entry?.clan?.id ?? null);
      if (Number.isFinite(resolvedId)) return `id:${Math.trunc(resolvedId)}`;
      const rawName =
        entry?.clan_name ??
        entry?.clanName ??
        entry?.name ??
        entry?.clan ??
        entry?.clan?.name ??
        '';
      const cleanedName = commCleanClanDisplayName(rawName) || rawName;
      const normalized = normalizeClanName(cleanedName);
      if (normalized) return `name:${normalized}`;
      return rawName ? `name:${String(rawName).trim().toLowerCase()}` : '';
    };
    const preferClanEntry = (a, b) => {
      if (!a) return b;
      if (!b) return a;
      const aId = Number.isFinite(commParseNumber(a?.clan_id ?? a?.clanId ?? a?.id ?? a?.clan?.id));
      const bId = Number.isFinite(commParseNumber(b?.clan_id ?? b?.clanId ?? b?.id ?? b?.clan?.id));
      if (aId !== bId) return aId ? a : b;
      const aScore = getTotalScore(a);
      const bScore = getTotalScore(b);
      if (Number.isFinite(aScore) !== Number.isFinite(bScore)) return Number.isFinite(bScore) ? b : a;
      if (Number.isFinite(aScore) && Number.isFinite(bScore) && bScore !== aScore) {
        return bScore > aScore ? b : a;
      }
      const aPower = commParseNumber(a?.power ?? a?.clan_power ?? a?.clanPower ?? a?.roster_power);
      const bPower = commParseNumber(b?.power ?? b?.clan_power ?? b?.clanPower ?? b?.roster_power);
      if (Number.isFinite(aPower) !== Number.isFinite(bPower)) return Number.isFinite(bPower) ? b : a;
      if (Number.isFinite(aPower) && Number.isFinite(bPower) && bPower !== aPower) {
        return bPower > aPower ? b : a;
      }
      return a;
    };
    const deduped = [];
    const seen = new Map();
    sortedAll.forEach((entry) => {
      const key = canonicalClanKey(entry);
      if (!key) return;
      if (!seen.has(key)) {
        seen.set(key, deduped.length);
        deduped.push(entry);
        return;
      }
      const idx = seen.get(key);
      deduped[idx] = preferClanEntry(deduped[idx], entry);
    });
    sortedAll = deduped;
    const topEntries = sortedAll.filter((entry) => !entry?.simulated).slice(0, safeLimit);
    let displayClansSorted = topEntries;
    if (simEntry) {
      const simKey = normalizeClanKey(simEntry);
      if (!topEntries.some((entry) => normalizeClanKey(entry) === simKey)) {
        displayClansSorted = [...topEntries, simEntry];
      }
    }
    const rankIndex = new Map();
    const nowRankTs = Date.now();
    sortedAll.forEach((entry, idx) => {
      const key = normalizeClanKey(entry);
      if (!key || rankIndex.has(key)) return;
      let rankValue = idx + 1;
      const cached = liveRankHoldCache.get(key);
      if (!didResort && cached && nowRankTs - cached.ts <= LIVE_RANK_HOLD_MS && cached.rank != null) {
        rankValue = cached.rank;
      }
      rankIndex.set(key, rankValue);
      liveRankHoldCache.set(key, { rank: rankValue, ts: nowRankTs });
    });
    const hasBoostCounts = (mapLike) => {
      if (!mapLike) return false;
      if (mapLike instanceof Map) {
        for (const value of mapLike.values()) {
          if (Number(value || 0) > 0) return true;
        }
        return false;
      }
      if (typeof mapLike === 'object') {
        return Object.values(mapLike).some((value) => Number(value || 0) > 0);
      }
      return false;
    };

    if (window.__debugLiveScores && commNormalizeDebugClan(livePpsDebugClan)) {
      const debugTarget = commNormalizeDebugClan(livePpsDebugClan);
      const debugEntry = displayClansSorted.find((entry) => {
        const name = entry?.clanName ?? entry?.clan_name ?? entry?.name ?? entry?.clan?.name ?? '';
        return commNormalizeDebugClan(name).includes(debugTarget);
      });
      if (debugEntry) {
        const debugName = debugEntry?.clanName ?? debugEntry?.clan_name ?? debugEntry?.name ?? debugEntry?.clan?.name ?? 'Unknown';
        const ctx = getScoreContext(debugEntry);
        console.log(`[MW SCORE DEBUG] ${debugName}:`, {
          basePower: resolveRosterPower(debugEntry),
          roundSeconds: roundSecondsForScore,
          pps: ctx?.basePpsValue ?? null,
          ppsSource: ctx?.basePpsSource ?? null,
          baseScore: ctx?.baseScoreValue ?? null,
          rawAdded: ctx?.addedScore ?? null,
          totalScore: ctx?.totalScoreValue ?? null,
          displayRoundTime,
          latestRoundTimerSeconds
        });
      }
    }

    const topRows = displayClansSorted.map((entry, idx) => {
      const rankKey = normalizeClanKey(entry);
      const isMyClan = myClanEntry && rankKey && normalizeClanKey(myClanEntry) === rankKey;
      const isSimulated = entry?.simulated || isSimClanName(entry?.clan_name ?? entry?.clanName ?? entry?.name);
      const rowHighlight = isSimulated
        ? 'padding: 4px 6px; border-radius: 8px; background: rgba(129, 140, 248, 0.2); border: 1px dashed rgba(129, 140, 248, 0.7); box-shadow: 0 0 12px rgba(129, 140, 248, 0.25);'
        : (isMyClan
          ? 'padding: 4px 6px; border-radius: 8px; background: rgba(56, 189, 248, 0.18); border: 1px solid rgba(56, 189, 248, 0.7); box-shadow: 0 0 12px rgba(56, 189, 248, 0.35);'
          : '');
      const positionValue = isSimulated ? 'SIM' : (rankIndex.has(rankKey) ? rankIndex.get(rankKey) : (idx + 1));
      const clanNameValue = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? entry?.clan?.name ?? 'Unknown';
      const scorePercentValue = normalizePercentValue(
        commParseNumber(
          entry?.percentage ??
          entry?.clan_percentage ??
          entry?.share ??
          entry?.pct ??
          entry?.chance ??
          entry?.chanceToWin ??
          entry?.chance_to_win
        )
      );
      const myClanPercentOverride = isMyClan && Number.isFinite(liveMyClanPercentValue)
        ? liveMyClanPercentValue
        : null;
      const computedTotalScore = getTotalScore(entry);
      const scoreKey = rankKey || normalizeClanKey(entry);
      const nowScoreTs = Date.now();
      if (Number.isFinite(computedTotalScore) && computedTotalScore > 0 && scoreKey) {
        liveScoreHoldCache.set(scoreKey, { value: computedTotalScore, ts: nowScoreTs });
      }
      let resolvedTotalScore = Number.isFinite(computedTotalScore) && computedTotalScore > 0
        ? computedTotalScore
        : null;
      if (!Number.isFinite(resolvedTotalScore) && scoreKey) {
        const cached = liveScoreHoldCache.get(scoreKey);
        if (cached && nowScoreTs - cached.ts <= LIVE_SCORE_HOLD_MS) {
          resolvedTotalScore = cached.value;
        }
      }
      const derivedPercentValue = Number.isFinite(resolvedTotalScore)
        && Number.isFinite(boostedTotal)
        && boostedTotal > 0
        ? (resolvedTotalScore / boostedTotal) * 100
        : null;
      const effectivePercentValue = Number.isFinite(derivedPercentValue)
        ? derivedPercentValue
        : (Number.isFinite(myClanPercentOverride)
          ? myClanPercentOverride
          : (Number.isFinite(scorePercentValue)
            ? scorePercentValue
            : null));
      const scorePercentLabel = Number.isFinite(effectivePercentValue)
        ? `${formatDecimal(effectivePercentValue, 2)}%`
        : null;
      const boostPercentLabel = scorePercentLabel;
      const usedAbilities = Array.isArray(entry?.used_abilities)
        ? entry.used_abilities
        : (Array.isArray(entry?.usedAbilities) ? entry.usedAbilities : []);
      const entryBoostMapRaw =
        entry?.boosts ??
        entry?.boostMap ??
        entry?.boost_map ??
        entry?.clanBoosts ??
        entry?.clan_boosts ??
        null;
      const entryBoostMap = hasBoostCounts(entryBoostMapRaw) ? entryBoostMapRaw : null;
      let wsBoostMap = null;
      if (!entryBoostMap && clanBoosts && clanBoosts.size) {
        const entryId = commParseNumber(entry?.clan_id ?? entry?.clanId ?? null);
        const entryName = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? entry?.clan?.name ?? null;
        const idKey = Number.isFinite(entryId) ? `id:${Math.trunc(entryId)}` : null;
        const nameKey = entryName ? `name:${entryName}` : null;
        const cleanedName = entryName ? commCleanClanDisplayName(entryName) : '';
        const cleanedKey = cleanedName ? `name:${cleanedName}` : null;
        const normalizedName = entryName ? normalizeClanName(entryName) : '';
        const normalizedKey = normalizedName ? `name:${normalizedName}` : null;
        const lookupName = Number.isFinite(entryId) ? clanLookup.get(String(Math.trunc(entryId))) : null;
        const lookupKey = lookupName ? `name:${lookupName}` : null;
        const wsEntry =
          (idKey && clanBoosts.get(idKey)) ||
          (nameKey && clanBoosts.get(nameKey)) ||
          (cleanedKey && clanBoosts.get(cleanedKey)) ||
          (normalizedKey && clanBoosts.get(normalizedKey)) ||
          (lookupKey && clanBoosts.get(lookupKey)) ||
          null;
        if (wsEntry) {
          const roundKey = liveBoostRoundKey || normalizeBlockId(roundId) || normalizeBlockId(currentRoundInfo?.blockId) || null;
          const isSameRound = !roundKey || !wsEntry.roundKey || wsEntry.roundKey === roundKey;
          const isFresh = !wsEntry.updatedAt || (Date.now() - wsEntry.updatedAt <= LIVE_BOOST_ICON_TTL_MS);
          wsBoostMap = isSameRound && isFresh ? (wsEntry?.boosts ?? null) : null;
        } else {
          wsBoostMap = null;
        }
      }
      const preferWsOnly = !liveBoostApiSyncEnabled;
      const boostsPreferred = hasBoostCounts(wsBoostMap)
        ? wsBoostMap
        : (preferWsOnly ? null : (usedAbilities.length ? usedAbilities : (entryBoostMap || null)));
      let boostMeta = null;
      if (hasBoostCounts(wsBoostMap) && wsBoostMap && clanBoosts && clanBoosts.size) {
        const entryId = commParseNumber(entry?.clan_id ?? entry?.clanId ?? null);
        const entryName = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? entry?.clan?.name ?? null;
        const idKey = Number.isFinite(entryId) ? `id:${Math.trunc(entryId)}` : null;
        const nameKey = entryName ? `name:${entryName}` : null;
        const cleanedName = entryName ? commCleanClanDisplayName(entryName) : '';
        const cleanedKey = cleanedName ? `name:${cleanedName}` : null;
        const normalizedName = entryName ? normalizeClanName(entryName) : '';
        const normalizedKey = normalizedName ? `name:${normalizedName}` : null;
        const lookupName = Number.isFinite(entryId) ? clanLookup.get(String(Math.trunc(entryId))) : null;
        const lookupKey = lookupName ? `name:${lookupName}` : null;
        const wsEntry =
          (idKey && clanBoosts.get(idKey)) ||
          (nameKey && clanBoosts.get(nameKey)) ||
          (cleanedKey && clanBoosts.get(cleanedKey)) ||
          (normalizedKey && clanBoosts.get(normalizedKey)) ||
          (lookupKey && clanBoosts.get(lookupKey)) ||
          null;
        boostMeta = wsEntry?.boostMeta || null;
      } else if (!preferWsOnly && usedAbilities.length) {
        boostMeta = buildBoostMetaFromAbilities(usedAbilities, 'ws:allUsersStateV2');
      } else if (!preferWsOnly && entryBoostMap) {
        boostMeta = buildBoostMetaFromMap(entryBoostMap, 'api:summary');
      }
      const boostsUsedHtml = buildBoostPillsHtml(boostsPreferred, {
        origin: 'live',
        meta: boostMeta,
        clanName: clanNameValue
      });
      const ctx = getScoreContext(entry);
      const ppsValue = ctx?.basePpsValue;
      const addedValue = ctx?.addedScore;
      const totalValue = Number.isFinite(resolvedTotalScore) ? resolvedTotalScore : null;
      const roundSecondsValue = Number.isFinite(roundSecondsForScore) ? roundSecondsForScore : null;
      const scorePillLabel = Number.isFinite(resolvedTotalScore) ? formatScoreValue(resolvedTotalScore) : '-';
      const scorePill = `
        <button
          type="button"
          class="live-score-pill"
          data-clan-key="${commEscape(normalizeClanKey(entry))}"
          data-clan-name="${commEscape(clanNameValue)}"
          data-pps="${Number.isFinite(ppsValue) ? ppsValue : ''}"
          data-round-seconds="${Number.isFinite(roundSecondsValue) ? roundSecondsValue : ''}"
          data-round-time="${commEscape(displayRoundTime)}"
          data-added="${Number.isFinite(addedValue) ? addedValue : ''}"
          data-total="${Number.isFinite(totalValue) ? totalValue : ''}"
        >
          ${scorePillLabel}${boostPercentLabel ? ` <span style="opacity:0.7;">(${boostPercentLabel})</span>` : ''}
        </button>
      `;
      return `
      <div style="margin-top: 4px; display: grid; grid-template-columns: ${gridTemplate}; gap: 6px; align-items: center; ${rowHighlight}">
        <div style="font-weight: 600;">${positionValue}</div>
        <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${commEscape(clanNameValue)}</div>
        <div>${scorePill}</div>
        <div style="display: flex; flex-wrap: wrap; gap: 4px;">${boostsUsedHtml}</div>
        <div>${formatPowerValue(entry?.power)}</div>
      </div>
    `;
  }).join('');

  // Show message when no baseline data is available (empty clan list)
  const noDataMessage = displayClans.length === 0
    ? `
      <div style="margin-top: 8px; padding: 8px; text-align: center; opacity: 0.7; font-size: 0.85rem;">
        <div style="margin-bottom: 2px;">Waiting for API live data...</div>
        <div style="font-size: 0.75rem; opacity: 0.8;">Live table updates on the selected interval or manual refresh</div>
      </div>
    `
    : '';

  const boostHtml = '';

  const powerSourceOptions = LIVE_POWER_SOURCE_ORDER
    .map((source) => {
      const label = LIVE_POWER_SOURCE_LABELS[source] || source;
      return `<option value="${source}">${commEscape(label)}</option>`;
    })
    .join('');
  const powerSourceControl = showLimit && showPowerControl
    ? `
        <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.75;">
          Power
          <select id="currentRoundPowerSource" style="background: rgba(15, 23, 42, 0.75); border: 1px solid rgba(148, 163, 184, 0.35); color: inherit; border-radius: 999px; padding: 2px 6px; font-size: 0.7rem; max-width: 180px;">
            ${powerSourceOptions}
          </select>
        </label>
      `
    : '';
  const scoreSourceOptions = LIVE_SCORE_SOURCE_ORDER
    .map((source) => {
      const label = LIVE_SCORE_SOURCE_LABELS[source] || source;
      return `<option value="${source}">${commEscape(label)}</option>`;
    })
    .join('');
  const scoreSourceControl = showLimit && showScoreControl
    ? `
        <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.75;">
          Score
          <select id="currentRoundScoreSource" style="background: rgba(15, 23, 42, 0.75); border: 1px solid rgba(148, 163, 184, 0.35); color: inherit; border-radius: 999px; padding: 2px 6px; font-size: 0.7rem; max-width: 200px;">
            ${scoreSourceOptions}
          </select>
        </label>
      `
    : '';
  const displayModeOptions = LIVE_DISPLAY_MODE_ORDER
    .map((mode) => {
      const label = LIVE_DISPLAY_MODE_LABELS[mode] || mode;
      return `<option value="${mode}">${commEscape(label)}</option>`;
    })
    .join('');
  const displayModeControl = showLimit && showDisplayModeControl
    ? `
        <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.75;">
          Mode
          <select id="currentRoundDisplayMode" style="background: rgba(15, 23, 42, 0.75); border: 1px solid rgba(148, 163, 184, 0.35); color: inherit; border-radius: 999px; padding: 2px 6px; font-size: 0.7rem; max-width: 110px;">
            ${displayModeOptions}
          </select>
        </label>
      `
    : '';

  const limitSelected = Number(clanLimit) || 10;
  const limitOptions = [3, 5, 10, 20]
    .map((value) => `<option value="${value}"${value === limitSelected ? ' selected' : ''}>${value}</option>`)
    .join('');
  const limitControl = showLimit
    ? `
        <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 0.7rem; text-transform: uppercase; opacity: 0.75;">
          Clans
        <select id="currentRoundClanLimit" style="background: rgba(15, 23, 42, 0.75); border: 1px solid rgba(148, 163, 184, 0.35); color: inherit; border-radius: 999px; padding: 2px 6px; font-size: 0.7rem;">
          ${limitOptions}
        </select>
      </label>
    `
    : '';

  let powerDebugHtml = '';
  let scoreDebugHtml = '';
  if (showLimit && showDebugTables) {
    const powerDebugFields = ['roster_power', 'clanPower', ...LIVE_POWER_FIELDS];
    const powerDebugSelectedLabel = powerSourceLabel ? `Selected (${powerSourceLabel})` : 'Selected';
    const isSelectedField = (field) => {
      if (powerSource === 'roster') return field === 'roster_power';
      return field === powerSource;
    };
    const powerDebugHeader = powerDebugFields.map((field) => {
      const label = field === 'roster_power' ? 'roster' : field;
      const highlight = isSelectedField(field) ? 'color: #fbbf24; font-weight: 600;' : '';
      return `<th style="padding: 2px 6px; text-align: right; white-space: nowrap; ${highlight}">${commEscape(label)}</th>`;
    }).join('');
    const powerDebugRows = displayClans.map((entry) => {
      const name = entry?.clan_name || entry?.name || (entry?.clan_id ? `Clan ${entry.clan_id}` : 'Unknown');
      const candidates = entry?.power_candidates || {};
      const getFieldValue = (field) => {
        if (field === 'roster_power') return commParseNumber(entry?.roster_power);
        if (field === 'clanPower') return commParseNumber(candidates?.clanPower);
        return commParseNumber(candidates?.[field]);
      };
      const cells = powerDebugFields.map((field) => {
        return `<td style="padding: 2px 6px; text-align: right; white-space: nowrap;">${valueOrDash(getFieldValue(field))}</td>`;
      }).join('');
      return `
        <tr>
          <td style="padding: 2px 6px; text-align: left; white-space: nowrap;">${commEscape(name)}</td>
          <td style="padding: 2px 6px; text-align: right; white-space: nowrap;">${valueOrDash(entry?.clan_power)}</td>
          ${cells}
        </tr>
      `;
    }).join('');
    const powerDebugBody = powerDebugRows || `
      <tr>
        <td colspan="${2 + powerDebugFields.length}" style="padding: 4px 6px; text-align: center; opacity: 0.7;">No live clan data.</td>
      </tr>
    `;
    const powerDebugOpenAttr = currentRoundPowerDebugOpen ? ' open' : '';
    powerDebugHtml = `
        <details id="currentRoundPowerDebug"${powerDebugOpenAttr} style="margin-top: 8px;">
          <summary style="cursor: pointer; font-size: 0.7rem; text-transform: uppercase; opacity: 0.7;">Power fields (source: ${commEscape(powerSourceLabel || 'roster')})</summary>
          <div style="margin-top: 6px; overflow-x: auto;">
            <table style="width: 100%; border-collapse: collapse; font-size: 0.7rem;">
              <thead>
                <tr>
                  <th style="padding: 2px 6px; text-align: left; white-space: nowrap;">Clan</th>
                  <th style="padding: 2px 6px; text-align: right; white-space: nowrap;">${commEscape(powerDebugSelectedLabel)}</th>
                  ${powerDebugHeader}
                </tr>
              </thead>
              <tbody>
                ${powerDebugBody}
              </tbody>
            </table>
          </div>
        </details>
      `;

    const scoreDebugFields = LIVE_SCORE_FIELDS;
    const scoreDebugSelectedLabel = scoreSourceLabel ? `Selected (${scoreSourceLabel})` : 'Selected';
    const scoreDebugHeader = scoreDebugFields.map((field) => {
      const highlight = field === scoreSource ? 'color: #fbbf24; font-weight: 600;' : '';
      return `<th style="padding: 2px 6px; text-align: right; white-space: nowrap; ${highlight}">${commEscape(field)}</th>`;
    }).join('');
    const scoreDebugRows = displayClans.map((entry) => {
      const name = entry?.clan_name || entry?.name || (entry?.clan_id ? `Clan ${entry.clan_id}` : 'Unknown');
      const scoreContext = getScoreContext(entry);
      const candidates = scoreContext.candidates || {};
      const selectedValue = scoreContext.selectedScoreValue;
      const getFieldValue = (field) => commParseNumber(candidates?.[field]);
      const cells = scoreDebugFields.map((field) => {
        return `<td style="padding: 2px 6px; text-align: right; white-space: nowrap;">${valueOrDash(getFieldValue(field))}</td>`;
      }).join('');
      return `
        <tr>
          <td style="padding: 2px 6px; text-align: left; white-space: nowrap;">${commEscape(name)}</td>
          <td style="padding: 2px 6px; text-align: right; white-space: nowrap;">${valueOrDash(selectedValue)}</td>
          ${cells}
        </tr>
      `;
    }).join('');
    const scoreDebugBody = scoreDebugRows || `
      <tr>
        <td colspan="${2 + scoreDebugFields.length}" style="padding: 4px 6px; text-align: center; opacity: 0.7;">No live clan data.</td>
      </tr>
    `;
    const scoreDebugOpenAttr = currentRoundScoreDebugOpen ? ' open' : '';
    scoreDebugHtml = `
      <details id="currentRoundScoreDebug"${scoreDebugOpenAttr} style="margin-top: 8px;">
        <summary style="cursor: pointer; font-size: 0.7rem; text-transform: uppercase; opacity: 0.7;">Score fields (source: ${commEscape(scoreSourceLabel || 'currentAddedScore')})</summary>
        <div style="margin-top: 6px; overflow-x: auto;">
          <table style="width: 100%; border-collapse: collapse; font-size: 0.7rem;">
            <thead>
              <tr>
                <th style="padding: 2px 6px; text-align: left; white-space: nowrap;">Clan</th>
                <th style="padding: 2px 6px; text-align: right; white-space: nowrap;">${commEscape(scoreDebugSelectedLabel)}</th>
                ${scoreDebugHeader}
              </tr>
            </thead>
            <tbody>
              ${scoreDebugBody}
            </tbody>
          </table>
        </div>
      </details>
    `;
  }

  const controlsHtml = (powerSourceControl || scoreSourceControl || displayModeControl)
    ? `<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; max-width: 100%; min-width: 0; flex: 1 1 260px;">${powerSourceControl}${scoreSourceControl}${displayModeControl}</div>`
    : '';
  const cornerControlHtml = limitControl
    ? `<div style="margin-left: auto; display: inline-flex; align-items: center;">${limitControl}</div>`
    : '';

  return {
    id: `current-live-${roundId}`,
    htmlContent: `
        <div class="current-round-card" style="border-color: #38bdf8; background: rgba(12, 18, 34, 0.7);" data-round-id="${commEscape(roundId)}" data-round-time="${commEscape(displayRoundTime)}" data-round-multiplier="${commEscape(displayMultiplier)}">
          <div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; row-gap: 6px; margin-bottom: 6px;">
            <div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0;">
              <div style="font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.75;">${commEscape(title)}</div>
              ${headerMetaRow}
            </div>
            ${cornerControlHtml}
          </div>
        ${controlsHtml}
        ${headerRow}
          ${tableHeader}
          ${topRows || ''}
          ${noDataMessage}
          ${boostHtml}
          ${totalScoreChart}
          ${powerDebugHtml}
          ${scoreDebugHtml}
        </div>
      `
    };
  }

  async function populateLive() {
    const currentRoundContainer = document.getElementById('currentRoundGrid');
    const recentRoundsContainer = document.getElementById('recentRoundsGrid');
    const liveCommContainer = document.getElementById('liveCommGrid');
    const lastFinishedContainer = document.getElementById('lastFinishedGrid');
    const boostSummaryContainer = document.getElementById('boostSummaryGrid');
    const finishedRoundsContainer = document.getElementById('finishedRoundsGrid');
    if (liveCommContainer) {
      const gominingChatHtml = `
        <div class="clan-comm">
          <div class="title">Boost notifications</div>
          <div class="muted" id="gominingChatStatus">${WEBSITE_MODE ? 'Live cache mode (updates when app is open).' : 'Waiting for boost notifications...'}</div>
          <div id="gominingChatList" class="comm-chat gomining-chat-list"></div>
        </div>
      `;
      const gominingChatCard = createCard({ title: 'Boost Notifications', htmlContent: gominingChatHtml, id: 'live-gomining-chat' });
      liveCommContainer.innerHTML = '';
      liveCommContainer.appendChild(gominingChatCard);
      initClanComm();
      if (!WEBSITE_MODE) {
        commStartGominingChatNotifications();
      }
    }
    const legacyMiniChat = document.getElementById('miniCommBar');
    if (legacyMiniChat) legacyMiniChat.remove();
    renderMiniClanComm();
    if (lastFinishedContainer) lastFinishedContainer.style.display = 'none';
    const showFinishedRounds = false;
    const showBoostSummary = false;
    const showRecentRounds = false;
    if (!showRecentRounds && recentRoundsContainer) {
      recentRoundsContainer.style.display = 'none';
    }
    if (WEBSITE_MODE) {
      startLiveCachePolling();
    }
  let lastUpdateData = null;
  let lastFinishedRound = null; // Track the last finished round
  let currentRoundBoosts = {}; // Track boosts for the current round
  currentRoundClanBoosts = currentRoundClanBoosts || new Map(); // Map of clanId/name -> boost counts
  let playerBoosts = {}; // Deprecated (player boosts unreliable)
  let playerMeta = {}; // Deprecated (player boosts unreliable)
  let pendingBoostByPlayer = new Map(); // Pending boosts awaiting clan resolution
  let groupByClan = true; // Force clan-only boosts
  let topBoostersLimit = 5; // How many top boosters to show (default: 5)
  currentRoundInfo = currentRoundInfo || { blockId: '-', timer: '-:-', multiplier: '-' }; // Track current round info
  const _recentBoostKeys = new Map();
  currentRoundClanIds = currentRoundClanIds || new Map(); // Track clan_id for clans in current round (clanId -> clan data)
  const recentRoundEvents = new Map(); // Deduplicate round events by key within a short window
  // lastLiveSummaryBlockId is global (used outside populateLive)
  lastLiveSummaryLeague = null;
  let isSavingFinishedRound = false; // Lock live summary updates while saving a finished round
  let lockedFinishedBlockId = null; // The block id frozen for the finished round save
  const recentClanUpserts = new Map();
  let lastAllUsersStateV2Snapshot = [];
    lastLiveClanSnapshot = null;
    window.lastLiveClanSnapshot = null;
    window.__mwLastLiveClanSnapshot = null;
    let lastAllPlayersStateSnapshot = [];
    let lastWinnerSnapshot = null;
      let currentRoundClanLimit = 10;
      let currentRoundLimitEditing = false;
      let currentRoundLimitLockUntil = 0;
      let currentRoundPowerEditing = false;
      let currentRoundScoreEditing = false;
      let currentRoundDisplayModeEditing = false;
      let currentRoundBoostApiEditing = false;
      let currentRoundBoostApiLockUntil = 0;
    let liveProfileClanEditing = false;
    let liveProfileClanLockUntil = 0;
    lastCurrentRoundLivePayload = null;
    let simTicker = null;
    liveCacheLastSentAt = liveCacheLastSentAt || 0;
    liveCacheLastHash = liveCacheLastHash || '';
    liveCacheTimer = liveCacheTimer || null;
    liveCachePollTimer = liveCachePollTimer || null;
    liveCachePollPending = liveCachePollPending || false;
    liveCacheLastAppliedAt = liveCacheLastAppliedAt || 0;
    liveCacheLastAppliedKey = liveCacheLastAppliedKey || '';

  function buildLiveCachePayload() {
    const cycleId = getCycleNumber(new Date());
    const leagueId = commResolveLiveLeagueId();
    const blockId = normalizeBlockId(
      lastCurrentRoundLivePayload?.blockId ??
        currentRoundInfo?.blockId ??
        lastLiveSummaryBlockId ??
        null
    );
    const roundTime =
      lastCurrentRoundLivePayload?.timer ??
      currentRoundInfo?.timer ??
      lastLiveSummaryTimer ??
      null;
    const roundSeconds = wn(roundTime);
    const multiplierValue = commParseMultiplierValue(
      lastCurrentRoundLivePayload?.multiplier ??
        currentRoundInfo?.multiplier ??
        lastLiveSummaryMultiplier ??
        null
    );
    const payload = {
      liveSummary: {
        blockId,
        roundTime,
        roundSeconds: Number.isFinite(roundSeconds) ? roundSeconds : null,
        multiplier: Number.isFinite(multiplierValue) ? multiplierValue : null,
        leagueId
      },
      allUsersStateV2: Array.isArray(lastAllUsersStateV2Raw) ? lastAllUsersStateV2Raw : null,
      updatedAt: new Date().toISOString()
    };
    return { cycleId, leagueId, blockId, roundTime, roundSeconds, multiplierValue, payload };
  }

  async function upsertLiveCache(reason = '', options = {}) {
    if (!HAS_CHROME_RUNTIME) return;
    if (!hasSupabaseSession()) return;
    const ownerId = getHomeAuthUserId();
    if (!ownerId) return;
    if (!Array.isArray(lastAllUsersStateV2Raw) || lastAllUsersStateV2Raw.length === 0) return;
    const { cycleId, leagueId, blockId, roundTime, roundSeconds, multiplierValue, payload } =
      buildLiveCachePayload();
    if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return;

    const force = options && options.force === true;

    const hashKey = [
      ownerId,
      cycleId,
      leagueId,
      blockId || '-',
      roundTime || '-',
      Number.isFinite(multiplierValue) ? multiplierValue : '-',
      lastAllUsersStateV2Raw.length
    ].join('|');
    const now = Date.now();
    if (!force && hashKey === liveCacheLastHash && now - liveCacheLastSentAt < LIVE_CACHE_FORCE_INTERVAL_MS) {
      return;
    }

    const identity = commGetApiUserIdentity();
    const row = {
      owner_id: ownerId,
      user_id: identity?.userId || null,
      user_label: identity?.userLabel || null,
      license_key: identity?.licenseKey || null,
      cycle_id: Math.trunc(cycleId),
      league_id: Math.trunc(leagueId),
      block_id: Number.isFinite(commParseNumber(blockId)) ? Math.trunc(commParseNumber(blockId)) : null,
      round_time: roundTime || null,
      round_time_seconds: Number.isFinite(roundSeconds) ? Math.trunc(roundSeconds) : null,
      multiplier: Number.isFinite(multiplierValue) ? Math.trunc(multiplierValue) : null,
      my_clan_name: liveMyClanName || commSession?.clanName || null,
      my_clan_percent: Number.isFinite(liveMyClanPercentValue) ? liveMyClanPercentValue : null,
      payload,
      updated_at: new Date().toISOString()
    };

    try {
      await commInsert(COMM_TABLE_LIVE_CACHE, [row], {
        upsert: true,
        onConflict: 'owner_id,cycle_id,league_id'
      });
      liveCacheLastSentAt = now;
      liveCacheLastHash = hashKey;
      if (commIsKorrel()) {
        commSetKorrelLog(
          `Live cache updated${reason ? ` (${reason})` : ''}.`,
          { append: true }
        );
      }
    } catch (err) {
      if (commIsKorrel()) {
        commSetKorrelLog(`Live cache update failed: ${err?.message || err}`, { append: true, error: true });
      }
    }
  }

  function scheduleLiveCacheUpdate(reason = '') {
    if (!HAS_CHROME_RUNTIME) return;
    if (!hasSupabaseSession()) return;
    if (!getHomeAuthUserId()) return;
    if (!Array.isArray(lastAllUsersStateV2Raw) || lastAllUsersStateV2Raw.length === 0) return;
    const now = Date.now();
    const sinceLast = now - liveCacheLastSentAt;
    const delay = sinceLast >= LIVE_CACHE_MIN_INTERVAL_MS ? 0 : LIVE_CACHE_MIN_INTERVAL_MS - sinceLast;
    if (liveCacheTimer) return;
    liveCacheTimer = setTimeout(() => {
      liveCacheTimer = null;
      upsertLiveCache(reason, { force: false }).catch(() => {});
    }, delay);
  }

  async function fetchLiveCacheAndApply() {
    if (liveCachePollPending) return;
    if (!hasSupabaseSession()) return;
    const ownerId = getHomeAuthUserId();
    if (!ownerId) return;
    liveCachePollPending = true;
    try {
      const cycleId = getCycleNumber(new Date());
      const leagueId = commResolveLiveLeagueId();
      const queryParts = [
        'select=owner_id,cycle_id,league_id,block_id,round_time,round_time_seconds,multiplier,my_clan_name,my_clan_percent,payload,updated_at',
        `owner_id=eq.${encodeURIComponent(ownerId)}`,
        Number.isFinite(cycleId) ? `cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}` : null,
        Number.isFinite(leagueId) ? `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}` : null,
        'order=updated_at.desc',
        'limit=1'
      ].filter(Boolean);
      const rows = await commFetch(COMM_TABLE_LIVE_CACHE, queryParts.join('&'));
      const row = Array.isArray(rows) && rows.length ? rows[0] : null;
      if (row) {
        const key = `${row.cycle_id}:${row.league_id}:${row.block_id || ''}:${row.updated_at || ''}`;
        if (key !== liveCacheLastAppliedKey) {
          applyLiveCacheRow(row);
          liveCacheLastAppliedKey = key;
          liveCacheLastAppliedAt = Date.now();
        }
      }
    } catch (err) {
      // ignore polling errors
    } finally {
      liveCachePollPending = false;
    }
  }

  function applyLiveCacheRow(row) {
    const payload = row?.payload ?? parseMaybeJson(row?.payload) ?? {};
    if (row?.my_clan_name) {
      const cleaned = commCleanClanDisplayName(row.my_clan_name);
      if (cleaned) {
        liveMyClanName = cleaned;
      } else if (liveMyClanName && !commCleanClanDisplayName(liveMyClanName)) {
        liveMyClanName = null;
      }
      liveMyClanNameUpdatedAt = Date.now();
    }
    if (Number.isFinite(commParseNumber(row?.my_clan_percent))) {
      liveMyClanPercentValue = commParseNumber(row.my_clan_percent);
      liveMyClanPercent = row.my_clan_percent;
      liveMyClanPercentUpdatedAt = Date.now();
    }
    if (Number.isFinite(commParseNumber(row?.league_id))) {
      lastLiveSummaryLeague = Math.trunc(commParseNumber(row.league_id));
    }
    const summary = payload?.liveSummary || payload?.live_summary || null;
    if (summary && typeof summary === 'object') {
      updateCurrentRound({
        blockId: summary.blockId ?? row?.block_id ?? null,
        timer: summary.roundTime ?? row?.round_time ?? null,
        multiplier: summary.multiplier ?? row?.multiplier ?? null,
        leagueId: summary.leagueId ?? row?.league_id ?? null
      });
    }
    if (Array.isArray(payload?.allUsersStateV2)) {
      const sanitized = payload.allUsersStateV2.map((item) => {
        if (!item || typeof item !== 'object') return item;
        return {
          ...item,
          usedAbilities: null,
          used_abilities: null,
          activeBoostScore: null,
          active_boost_score: null,
          boosts: null,
          boostMap: null,
          boost_map: null,
          clanBoosts: null,
          clan_boosts: null
        };
      });
      liveCacheApplying = true;
      try {
        updateCurrentRound(sanitized);
      } finally {
        liveCacheApplying = false;
      }
    }
  }

  function startLiveCachePolling() {
    if (liveCachePollTimer) return;
    fetchLiveCacheAndApply().catch(() => {});
    liveCachePollTimer = setInterval(() => {
      fetchLiveCacheAndApply().catch(() => {});
    }, 10000);
  }

  function processClanMatchMapping(clanId, clanName, source) {
    if (!clanId || !clanName) return;
    upsertClanMatch(clanId, clanName).then(() => {
      clanLookup.set(String(clanId), clanName);
      console.log('[MW] Upserted clan mapping:', clanId, clanName, source || '');
    }).catch(err => {
      console.warn('[MW] Failed to upsert clan mapping:', err);
    });
  }

  const rosterNameQueue = new Set();
  let rosterNameTimer = null;
  let rosterNameFetching = false;

  function queueRosterNameLookup(ids) {
    const list = Array.isArray(ids) ? ids : [ids];
    let queued = 0;
    list.forEach((rawId) => {
      const parsed = commParseNumber(rawId);
      if (!Number.isFinite(parsed)) return;
      const id = String(Math.trunc(parsed));
      if (clanLookup.has(id)) return;
      if (rosterNameQueue.has(id)) return;
      rosterNameQueue.add(id);
      queued += 1;
    });
    if (!queued) return;
    if (rosterNameTimer || rosterNameFetching) return;
    rosterNameTimer = setTimeout(() => {
      rosterNameTimer = null;
      processRosterNameQueue();
    }, 150);
  }

  async function processRosterNameQueue() {
    if (rosterNameFetching) return;
    const ids = Array.from(rosterNameQueue);
    if (!ids.length) return;
    const batch = ids.slice(0, 50);
    batch.forEach((id) => rosterNameQueue.delete(id));
    rosterNameFetching = true;
    try {
      const limit = Math.min(1000, Math.max(50, batch.length * 5));
      const query = [
        'select=clan_id,clan_name,fetched_at,raw',
        `clan_id=in.(${batch.join(',')})`,
        'order=fetched_at.desc',
        `limit=${limit}`
      ].join('&');
      const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, query);
      const seen = new Set();
      let updated = false;
      (Array.isArray(rows) ? rows : []).forEach((row) => {
        const parsed = commParseNumber(row?.clan_id);
        if (!Number.isFinite(parsed)) return;
        const id = String(Math.trunc(parsed));
        if (seen.has(id)) return;
        seen.add(id);
        if (clanLookup.has(id)) return;
        const raw = parseMaybeJson(row?.raw);
        const name = row?.clan_name ?? raw?.name ?? null;
        if (!name) return;
        clanLookup.set(id, String(name));
        updated = true;
      });
      if (updated) {
        if (currentRoundClanBoosts && currentRoundClanBoosts.size) {
          currentRoundClanBoosts.forEach((entry) => {
            const id = entry?.clan_id;
            if (!id) return;
            const name = clanLookup.get(String(id));
            if (name) entry.name = name;
          });
        }
        if (Array.isArray(lastAllUsersStateV2Raw)) {
          lastAllUsersStateV2Snapshot = buildClanSnapshotFromState(lastAllUsersStateV2Raw, currentRoundClanLimit);
        }
        if (typeof refreshCurrentRoundCard === 'function') {
          refreshCurrentRoundCard(lastCurrentRoundLivePayload || currentRoundInfo);
        }
      }
    } catch (err) {
      console.warn('[MW] Failed to load clan names from roster table:', err);
    } finally {
      rosterNameFetching = false;
      if (rosterNameQueue.size) {
        rosterNameTimer = setTimeout(() => {
          rosterNameTimer = null;
          processRosterNameQueue();
        }, 250);
      }
    }
  }

  // Track if we captured the entire round from start to finish
  let roundTrackingComplete = false; // Set to true when roundOpened is received, false when app starts mid-round

  // Note: roundStateBaseline, currentRoundBlockId, and baselineFetchAttempted are now global variables (declared at top of file)

  // Function to ensure baseline is fetched (called on round start OR when first live data arrives)
  async function ensureRoundStateBaseline(blockId, leagueId) {
    const now = Date.now();
    // Validate blockId - must be a real value, not "-" or null
    if (!blockId || blockId === '-' || blockId === '-' || String(blockId).trim() === '') {
    console.warn('[MW] Cannot ensure baseline: invalid blockId:', blockId);
      return;
    }
    if (baselineNoParticipantsUntil && now < baselineNoParticipantsUntil) {
      console.log('[MW] Baseline fetch paused (no participants cooldown).');
      return;
    }

    // If we already have baseline for this block, skip
    if (currentRoundBlockId === blockId && roundStateBaseline && roundStateBaseline.size > 0) {
      console.log('[MW] Baseline already exists for block', blockId, '- size:', roundStateBaseline.size);
      return;
    }

    // If we already attempted for this block, skip
    if (currentRoundBlockId === blockId && baselineFetchAttempted) {
      console.log('[MW] Baseline fetch already attempted for block', blockId);
      return;
    }

    console.log('[MW] Ensuring baseline for block', blockId);
    currentRoundBlockId = blockId;
    baselineFetchLastAttemptTime = now;

    const baseline = await fetchRoundStateBaseline(blockId, leagueId);
    if (baseline && baseline.size > 0) {
      roundStateBaseline = baseline;
      baselineFetchAttempted = true; // Only mark as attempted if successful
      console.log(`[MW] Round State baseline ready: ${baseline.size} clans`);
      window.debugLogger?.success('Baseline Ready', {
        clans: baseline.size,
        block: blockId
      });

      // Refresh display to show baseline scores
      if (typeof refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }
    } else {
      console.warn('[MW] Baseline fetch failed or returned empty - will retry on next WebSocket event');
      baselineFetchAttempted = false; // Allow retry
      currentRoundBlockId = null; // Reset so it can be attempted again
    }
  }

  // Note: checkBaselineStatus is now defined at the top of the file as a global function

  // Load clan mappings from database on startup
  try {
    console.log('[MW] Loading clan mappings from database...');
    const mappings = await fetchClanMappings();
    if (Array.isArray(mappings)) {
      mappings.forEach(m => {
        if (m.clan_id && m.clan_name) {
          clanLookup.set(String(m.clan_id), m.clan_name);
        }
      });
      console.log('[MW] Loaded', clanLookup.size, 'clan mappings from database');
    }
  } catch (err) {
    console.warn('[MW] Failed to load clan mappings:', err);
  }

  // Initialize current round with placeholder
  renderCards(currentRoundContainer, [{
    id: 'current-placeholder',
    title: 'Current Round',
    description: 'Waiting for live data...',
    meta: 'Live data updates via PPS + WebSocket'
  }]);

  // Initialize last finished round with placeholder
  renderCards(lastFinishedContainer, [buildLastFinishedCard(null)]);

  // Initialize boost summary with placeholder - wrapped in card for drag support
  const initialBoostCardElement = buildBoostSummaryCard({
    blockId: '-',
    timer: '-:-',
    multiplier: '-',
    boostsUsed: {}
  });
  if (!showBoostSummary && boostSummaryContainer) {
    boostSummaryContainer.style.display = 'none';
  } else {
    renderCards(boostSummaryContainer, [{
      id: 'boost-summary',
      htmlContent: initialBoostCardElement.outerHTML
    }]);
  }

    if (!showFinishedRounds && finishedRoundsContainer) {
      finishedRoundsContainer.style.display = 'none';
    } else {
      // Initialize finished rounds with placeholder
      renderFinishedRounds([{
        id: 'finished-placeholder',
        title: 'Loading...',
        description: 'Fetching last 2 finished rounds from database',
        meta: 'Please wait...'
      }]);
    }

    function formatRecentMultiplier(value) {
      if (value == null) return '-';
      const numeric = commParseNumber(value);
      if (Number.isFinite(numeric)) return `${numeric}x`;
      const raw = String(value).trim();
      return raw ? (raw.endsWith('x') ? raw : raw) : '-';
    }

    function formatRecentScore(value) {
      const num = Number(value);
      if (!Number.isFinite(num)) return '-';
      if (Math.abs(num) >= 1_000_000) {
        return `${formatDecimal(num / 1_000_000, 2)}M`;
      }
      return formatCount(num);
    }

    function resolveRecentRoundTime(row) {
      const direct = row?.round_time ?? row?.roundTime ?? null;
      if (direct) return String(direct);
      const raw = parseMaybeJson(row?.raw);
      const rawTime = raw?.round_time ?? raw?.roundTime ?? raw?.timer ?? null;
      if (rawTime) return String(rawTime);
      const startedAt = row?.started_at ?? raw?.started_at ?? raw?.startedAt ?? null;
      const endedAt = row?.ended_at ?? row?.updated_at ?? raw?.ended_at ?? raw?.endedAt ?? null;
      if (startedAt && endedAt) {
        const startMs = new Date(startedAt).getTime();
        const endMs = new Date(endedAt).getTime();
        if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
          const secs = Math.round((endMs - startMs) / 1000);
          return formatRoundTimerSeconds(secs);
        }
      }
      return '-';
    }

    function extractRecentRoundScores(row) {
      const raw = parseMaybeJson(row?.raw);
      const directTotal = commParseNumber(
        raw?.totalScore ??
          raw?.total_score ??
          raw?.scoreTotal ??
          raw?.roundTotalScore ??
          row?.total_score ??
          row?.totalScore ??
          null
      );
      const listCandidates = [
        raw?.clans,
        raw?.participants,
        raw?.leaderboard?.clans,
        raw?.leaderboard?.participants,
        raw?.data?.clans,
        raw?.data?.participants,
        raw?.clanLeaderboard?.clans
      ].filter((value) => Array.isArray(value));
      const scores = [];
      listCandidates.forEach((arr) => {
        arr.forEach((entry) => {
          const score = commParseNumber(
            entry?.score ??
              entry?.currentScore ??
              entry?.totalScore ??
              entry?.currentAddedScore ??
              entry?.basePoints ??
              entry?.scoreCurrent ??
              null
          );
          if (!Number.isFinite(score)) return;
          const name =
            entry?.clan_name ??
            entry?.clanName ??
            entry?.name ??
            entry?.clan?.name ??
            null;
          scores.push({ name, score });
        });
      });
      scores.sort((a, b) => (Number(b.score) || 0) - (Number(a.score) || 0));
      const top = scores[0] || null;
      const summed = scores.length
        ? scores.reduce((sum, entry) => sum + (Number(entry.score) || 0), 0)
        : null;
      const totalScore = Number.isFinite(directTotal)
        ? directTotal
        : (Number.isFinite(summed) ? summed : null);
      return {
        totalScore,
        topClanLabel: top?.name ? `#1 ${top.name}` : null
      };
    }

    function getRoundScoreValue(entry) {
      return commParseNumber(
        entry?.current_added_score ??
          entry?.currentAddedScore ??
          entry?.score ??
          entry?.total_score ??
          entry?.totalScore ??
          entry?.basePoints ??
          entry?.base_points ??
          null
      );
    }

    function buildRecentRoundsCard(rounds = [], meta = null) {
      const recentRounds = Array.isArray(rounds) ? rounds.slice(0, 5) : [];
      const formatAgeShort = (ms) => {
        if (!Number.isFinite(ms)) return '';
        const totalSec = Math.max(0, Math.floor(ms / 1000));
        if (totalSec < 60) return `${totalSec}s`;
        const minutes = Math.floor(totalSec / 60);
        if (minutes < 60) return `${minutes}m`;
        const hours = Math.floor(minutes / 60);
        return `${hours}h`;
      };
      const cycleCandidates = recentRounds
        .map((row) => commParseNumber(row?.cycle_id ?? row?.cycleId ?? row?.cycle))
        .filter((value) => Number.isFinite(value))
        .map((value) => Math.trunc(value));
      const leagueCandidates = recentRounds
        .map((row) => commParseNumber(row?.league_id ?? row?.leagueId ?? row?.league))
        .filter((value) => Number.isFinite(value))
        .map((value) => Math.trunc(value));
      const cycleUnique = Array.from(new Set(cycleCandidates));
      const leagueUnique = Array.from(new Set(leagueCandidates));
      const cycleLabel = cycleUnique.length === 1
        ? `Cycle ${cycleUnique[0]}`
        : (Number.isFinite(commParseNumber(meta?.cycleId)) ? `Cycle ${Math.trunc(commParseNumber(meta.cycleId))}` : 'Cycle -');
      const leagueLabel = leagueUnique.length === 1
        ? `League ${leagueUnique[0]}`
        : (leagueUnique.length > 1 ? 'League mixed' : (Number.isFinite(commParseNumber(meta?.leagueId)) ? `League ${Math.trunc(commParseNumber(meta.leagueId))}` : 'League -'));
      const sourceLabel = meta?.source === 'cache' ? 'Local cache' : (meta?.source === 'rounds_cycle_api' ? 'rounds_cycle_api' : 'No data');
      const cacheAge = meta?.source === 'cache' && Number.isFinite(meta?.cacheAt)
        ? ` (cached ${formatAgeShort(Date.now() - meta.cacheAt)} ago)`
        : '';
      const metaLine = meta
        ? `<div class="muted" style="font-size:0.62rem; margin-bottom:4px;">${commEscape(`${sourceLabel}${cacheAge}  -  ${cycleLabel}  -  ${leagueLabel}`)}</div>`
        : '';
      if (!recentRounds.length) {
        return createCard({
          title: 'Recent rounds',
          htmlContent: `${metaLine}<div class="muted">No rounds found.</div>`,
          id: 'live-recent-rounds'
        });
      }

      const rows = [];
      const addRow = (label, values, { className = '' } = {}) => {
        rows.push({ label, values, className });
      };

      const roundIds = recentRounds.map((round) => round?.block_id ?? round?.block_number ?? '-');
      const multipliers = recentRounds.map((round) => formatRecentMultiplier(round?.multiplier));
      const roundTimes = recentRounds.map((round) => resolveRecentRoundTime(round));
      const totalScores = [];
      const winnerClans = [];
      const winnerUsers = [];
      const topClanLabels = [];

      recentRounds.forEach((round) => {
        const summary = describeRoundRecord(round);
        const clanWinner = commExtractClanWinnerFromRow(round);
        const userWinner = commExtractUserWinnerFromRow(round);
        const winnerClan = clanWinner?.name ?? round?.clan_name ?? summary.winner ?? 'Unknown';
        const winnerUser = userWinner?.alias ?? round?.winner_player_name ?? summary.player ?? '-';
        const { totalScore, topClanLabel } = extractRecentRoundScores(round);

        totalScores.push(formatRecentScore(totalScore));
        winnerClans.push(winnerClan);
        winnerUsers.push(winnerUser);
        topClanLabels.push(topClanLabel || (clanWinner?.name ? `#1 ${clanWinner.name}` : '-'));
      });

      addRow('Multiplier', multipliers);
      addRow('Round time', roundTimes);
      addRow('Total round score', totalScores);
      addRow('Winner clan', winnerClans, { className: 'round-winner' });
      addRow('Winner user', winnerUsers, { className: 'round-user' });
      addRow('Clan highest score', topClanLabels, { className: 'round-top' });

      const header = `
        <tr>
          ${roundIds.map((value) => `<th>${commEscape(value)}</th>`).join('')}
        </tr>
      `;
      const body = rows.map((row) => {
        const cells = row.values
          .map((value) => `<td${row.className ? ` class="${row.className}"` : ''}>${commEscape(value)}</td>`)
          .join('');
        return `<tr>${cells}</tr>`;
      }).join('');

      const tableHtml = `
        ${metaLine}
        <table class="live-recent-rounds-table">
          <tbody>
            ${header}
            ${body}
          </tbody>
        </table>
      `;
      return createCard({ title: 'Recent rounds (last 5)', htmlContent: tableHtml, id: 'live-recent-rounds' });
    }

    function renderRecentRoundsTable(result = []) {
      if (!recentRoundsContainer) return;
      const rows = Array.isArray(result) ? result : (result?.rows || []);
      const meta = Array.isArray(result) ? null : result;
      const card = buildRecentRoundsCard(rows, meta);
      recentRoundsContainer.innerHTML = '';
      recentRoundsContainer.appendChild(card);
    }

    async function fetchRecentRoundsCycleApi() {
      if (!hasSupabaseSession()) {
        const cached = loadRecentRoundsCache();
        return cached ? { rows: cached.rows, source: 'cache', cycleId: cached.cycleId, leagueId: cached.leagueId, cacheAt: cached.cacheAt } : { rows: [], source: 'none' };
      }
      const cycleId = getCycleNumber(new Date());
      const leagueId = commResolveLiveLeagueId();
      const select =
        'block_number,multiplier,round_time,clan_winner,user_winner,winner_clan_id,winner_user_id,cycle_id,league_id,created_at,updated_at,ended_at,started_at,raw,active';
      const filterVariants = [
        { cycleId, leagueId },
        { cycleId, leagueId: null },
        { cycleId: null, leagueId },
        { cycleId: null, leagueId: null }
      ];
      for (const variant of filterVariants) {
        const queryParts = [
          `select=${select}`,
          'order=block_number.desc',
          'limit=5'
        ];
        if (Number.isFinite(variant.leagueId)) {
          queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(variant.leagueId))}`);
        }
        if (Number.isFinite(variant.cycleId)) {
          queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(variant.cycleId))}`);
        }
        try {
          const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
          const safeRows = Array.isArray(rows) ? rows : [];
          const filtered = safeRows.filter((row) => !(row?.active === true || row?.active === 'true' || row?.active === 1));
          if (filtered.length) {
            const trimmed = filtered.slice(0, 5);
            saveRecentRoundsCache(trimmed, { cycleId: variant.cycleId ?? cycleId, leagueId: variant.leagueId ?? leagueId });
            return { rows: trimmed, source: 'rounds_cycle_api', cycleId: variant.cycleId ?? cycleId, leagueId: variant.leagueId ?? leagueId };
          }
        } catch (err) {
          // continue to next filter variant
        }
      }
      const cached = loadRecentRoundsCache();
      return cached ? { rows: cached.rows, source: 'cache', cycleId: cached.cycleId, leagueId: cached.leagueId, cacheAt: cached.cacheAt } : { rows: [], source: 'none' };
    }

    function computeTopBoosters() {
    // Group by clan (use usedAbilities from allUsersStateV2 when available)
    if (currentRoundClanBoosts && currentRoundClanBoosts.size > 0) {
      const arr = Array.from(currentRoundClanBoosts.values()).map((entry) => ({
        name: entry.name,
        clanId: entry.clan_id,
        total: entry.total_boosts,
        types: entry.boosts
      }));
      arr.sort((a, b) => b.total - a.total);
      return arr.slice(0, topBoostersLimit);
    }

    return [];
  }

    function renderFinishedRounds(cards) {
      if (!showFinishedRounds) return;
      renderCards(finishedRoundsContainer, cards);
    }

    if (!simTicker) {
      simTicker = setInterval(() => {
        if (!calculatorSimEnabled) return;
        if (!lastCurrentRoundLivePayload) return;
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }, 1000);
    }

function ensureGroupToggle(card) {
  try {
    return;
    const infoBar = card.querySelector('.boost-controls') || card.querySelector('.boost-info-bar');
    if (!infoBar) return;
    const groupLabel = card.querySelector('.boost-group-label');
    if (infoBar.querySelector('#groupByClanToggle')) {
      infoBar.querySelector('#groupByClanToggle').checked = groupByClan;
      return;
    }

    const label = document.createElement('label');
    label.className = 'view-switch';
    label.innerHTML = `<input type="checkbox" id="groupByClanToggle"><span class="switch-label">Group by clan</span>`;
    const input = label.querySelector('input');
    input.checked = groupByClan;
    input.addEventListener('change', () => {
      groupByClan = input.checked;
      refreshBoostDisplay();
    });
    if (groupLabel) {
      groupLabel.innerHTML = '';
      groupLabel.appendChild(label);
    } else {
      infoBar.appendChild(label);
    }
  } catch (err) {
    console.warn('[MW] Failed to attach group toggle', err);
  }
}

  // Function to refresh boost display with current round info and accumulated boosts
  function refreshBoostDisplay() {
    updateBoostHelperMyClanBoostsFromMap(currentRoundClanBoosts);
    if (!showBoostSummary || !boostSummaryContainer) return;
    const filteredBoosts = Object.fromEntries(Object.entries(currentRoundBoosts || {}).filter(([k,v]) => normalizeBoostKey(k) !== 'service'));
    const displayData = {
      ...currentRoundInfo,
      boostsUsed: filteredBoosts,
      topBoosters: computeTopBoosters()
    };

    console.log('[MW] Refreshing boost display with:', displayData);

    // The boost-summary-card is nested inside the .card wrapper, so we need to find it within the card
    // Use querySelector to get only the FIRST matching card (in case there are duplicates)
    const cardWrapper = boostSummaryContainer.querySelector('.card');
    const existingCard = cardWrapper ? cardWrapper.querySelector('.boost-summary-card') : null;

    console.log('[MW] Boost card check:', {
      hasWrapper: !!cardWrapper,
      hasCard: !!existingCard,
      containerChildren: boostSummaryContainer.children.length
    });

    if (existingCard && cardWrapper) {
      console.log('[MW] Updating existing boost card in-place');
      updateBoostSummaryCard(existingCard, displayData);
      ensureGroupToggle(existingCard);
      // Update wrapper ID to match
      cardWrapper.dataset.cardId = 'boost-summary';
    } else {
      console.log('[MW] Creating new boost card (replacing any placeholders)');
      const boostCard = buildBoostSummaryCard(displayData);
      renderCards(boostSummaryContainer, [{
        id: 'boost-summary',
        htmlContent: boostCard.outerHTML
      }]);
      // Re-query after render to attach toggle
      const renderedCardWrapper = boostSummaryContainer.querySelector('.card');
      const renderedCard = renderedCardWrapper ? renderedCardWrapper.querySelector('.boost-summary-card') : null;
      if (renderedCard) {
        ensureGroupToggle(renderedCard);
      }
    }
  }

  function setClanBoostMeta(clanEntry, boostType, source, ts, timestamps, debug) {
    if (!clanEntry || !boostType) return;
    if (!clanEntry.boostMeta) clanEntry.boostMeta = {};
    const key = normalizeBoostKey(boostType);
    const existing = clanEntry.boostMeta[key];
    const existingTs = Number(existing?.ts || 0);
    const nextTs = normalizeTimestampMs(ts);
    const existingList = normalizeTimestampList([
      ...(existing?.timestamps || []),
      existing?.ts
    ]);
    const incomingList = normalizeTimestampList([
      ...(Array.isArray(timestamps) ? timestamps : (timestamps != null ? [timestamps] : [])),
      ...(Number.isFinite(nextTs) ? [nextTs] : [])
    ]);
    const mergedList = normalizeTimestampList([...existingList, ...incomingList]);
    const listMax = mergedList.length ? mergedList[mergedList.length - 1] : null;
    const effectiveNextTs = Number.isFinite(nextTs) ? nextTs : (Number.isFinite(listMax) ? listMax : null);
    const useNext = !existing || (Number.isFinite(effectiveNextTs) && effectiveNextTs > existingTs);
    clanEntry.boostMeta[key] = {
      source: useNext ? source : (existing?.source || source),
      ts: useNext ? effectiveNextTs : (Number.isFinite(existingTs) && existingTs > 0 ? existingTs : effectiveNextTs),
      timestamps: mergedList,
      debug: debug ?? existing?.debug ?? null
    };
    if (incomingList.length) {
      const existingSet = new Set(existingList);
      const clanLabel = clanEntry?.name || clanEntry?.clan_name || clanEntry?.clanName || (clanEntry?.clan_id ? `Clan ${clanEntry.clan_id}` : null);
      incomingList.forEach((tsValue) => {
        if (existingSet.has(tsValue)) return;
        registerBoostNotification({
          ts: tsValue,
          boostType: key,
          clanName: clanLabel,
          count: 1,
          source
        });
      });
    }
  }

  function applyPendingLocalBoosts() {
    if (!pendingBoostByPlayer || pendingBoostByPlayer.size === 0) return;
    const storedAlias = commGetPersonalUsername();
    const sessionAlias = commSession?.memberName || commSession?.userName || '';
    const localAlias = storedAlias || sessionAlias || '';
    const clanIdRaw = commParseNumber(liveMyClanId ?? commSession?.clanId);
    const resolvedClanId = Number.isFinite(clanIdRaw) ? String(Math.trunc(clanIdRaw)) : null;
    const resolvedClanName = liveMyClanName || commSession?.clanName || null;
    if (!resolvedClanId && !resolvedClanName) return;
    const toApply = [];
    pendingBoostByPlayer.forEach((boostsMap, key) => {
      const isLocal = key === '__local__' || (localAlias && commMatchesUsername(key, localAlias));
      if (!isLocal) return;
      toApply.push({ key, boosts: boostsMap });
    });
    if (!toApply.length) return;
    const ensureClanBoostEntry = (clanId, clanName) => {
      if (!clanId && !clanName) return null;
      const primaryKey = clanId ? `id:${clanId}` : `name:${clanName}`;
      let clanEntry = currentRoundClanBoosts.get(primaryKey);
      if (!clanEntry) {
        const label = clanName || (clanId ? `Clan ${clanId}` : 'Unknown Clan');
        clanEntry = { name: label, clan_id: clanId, boosts: {}, total_boosts: 0 };
      }
      const aliasKeys = new Set();
      if (clanId) aliasKeys.add(`id:${clanId}`);
      if (clanName) aliasKeys.add(`name:${clanName}`);
      const cleanedName = clanName ? commCleanClanDisplayName(clanName) : '';
      if (cleanedName) aliasKeys.add(`name:${cleanedName}`);
      const normalizedName = clanName ? normalizeClanName(clanName) : '';
      if (normalizedName) aliasKeys.add(`name:${normalizedName}`);
      aliasKeys.forEach((key) => {
        currentRoundClanBoosts.set(key, clanEntry);
      });
      return clanEntry;
    };
    const clanEntry = ensureClanBoostEntry(resolvedClanId, resolvedClanName);
    if (!clanEntry) return;
    clanEntry.hasEvent = true;
    clanEntry.updatedAt = Date.now();
    clanEntry.roundKey = clanEntry.roundKey || liveBoostRoundKey || normalizeBlockId(currentRoundInfo?.blockId) || null;
    toApply.forEach(({ key, boosts }) => {
      Object.entries(boosts || {}).forEach(([type, count]) => {
        const raw = Number(count) || 0;
        if (raw <= 0) return;
        clanEntry.boosts[type] = (clanEntry.boosts[type] || 0) + raw;
        clanEntry.total_boosts += raw;
        currentRoundBoosts[type] = (currentRoundBoosts[type] || 0) + raw;
        const pendingTs = Date.now();
        setClanBoostMeta(clanEntry, type, 'local:pending', pendingTs, [pendingTs], { source: 'local:pending', ts: pendingTs });
      });
      pendingBoostByPlayer.delete(key);
    });
  }

  // Function to handle ability/boost usage events
function handleBoostUsage(boostData) {
    // Boost events are noisy; only use them to update clan-level boost counts when possible.
    console.log('[MW] Boost usage event:', boostData);
    try { recordBoostShape(boostData); } catch (_) {}
    try {
      const boostKey = boostData && (boostData.eventId ?? boostData.id ?? boostData.abilityId ?? boostData.timestamp ?? JSON.stringify(boostData).slice(0,200));
      if (boostKey && _recentBoostKeys.has(boostKey)) {
        console.log('[MW] Ignoring duplicate boost event:', boostKey);
        // Do not record duplicates in recent boost panel
        return;
      }
      if (boostKey) {
        _recentBoostKeys.set(boostKey, Date.now());
        setTimeout(() => _recentBoostKeys.delete(boostKey), 5000);
      }
    } catch (err) {
      console.warn('[MW] Failed to compute boost dedupe key', err);
    }

    console.log('[MW] Boost data keys:', boostData ? Object.keys(boostData) : []);

    // Extract boost type/name from the event - try multiple possible field names
    if (!boostData) {
      console.warn('[MW] handleBoostUsage called with empty payload');
      return;
    }

    const tryGet = (obj, ...keys) => {
      for (const k of keys) {
        if (!obj) continue;
        const v = obj[k];
        if (v != null) return v;
      }
      return null;
    };

    // New helper: returns { key, value } for the first matching key on the object
    const getFirstMatch = (obj, ...keys) => {
      for (const k of keys) {
        if (!obj) continue;
        if (Object.prototype.hasOwnProperty.call(obj, k) && obj[k] != null) return { key: k, value: obj[k] };
      }
      return { key: null, value: null };
    };

    // Normalize boost type and parse numeric values (supporting 1x, 10x, plain numbers, etc.)
    let boostTypeRaw = tryGet(boostData, 'abilityType', 'ability_type', 'type', 'name', 'abilityName', 'ability_name', 'ability');
    if (boostTypeRaw && typeof boostTypeRaw === 'object') {
      boostTypeRaw = tryGet(boostTypeRaw, 'name', 'abilityName', 'type') || String(boostTypeRaw);
    }
    const abilityIdRaw = tryGet(boostData, 'nftGameAbilityId', 'nft_game_ability_id', 'abilityId', 'ability_id', 'abilityID');
    const idFallback = tryGet(boostData, 'id');
    const abilityId = abilityIdRaw || (idFallback && ABILITY_ID_TO_TYPE[idFallback] ? idFallback : null);
    const resolvedBoostType = (abilityId && ABILITY_ID_TO_TYPE[abilityId])
      ? ABILITY_ID_TO_TYPE[abilityId]
      : (boostTypeRaw && ABILITY_ID_TO_TYPE[boostTypeRaw]) ? ABILITY_ID_TO_TYPE[boostTypeRaw] : boostTypeRaw;
    const boostType = normalizeBoostKey(resolvedBoostType || '');
    if (!isKnownBoostType(boostType)) {
      console.warn('[MW] Ignoring non-boost ability event:', { boostTypeRaw, abilityId });
      return;
    }
    const boostEventTs = extractBoostEventTimestampMs(boostData);
    if (!Number.isFinite(boostEventTs)) {
      console.warn('[MW] Ignoring boost event without timestamp:', { boostType, boostTypeRaw });
      return;
    }
    const roundStartAt = getCurrentRoundStartMs();
    if (Number.isFinite(roundStartAt)) {
      const now = Date.now();
      if (boostEventTs < roundStartAt - LIVE_BOOST_TS_SKEW_MS || boostEventTs > now + LIVE_BOOST_TS_SKEW_MS) {
        console.warn('[MW] Ignoring boost event outside round window:', { boostType, boostEventTs, roundStartAt, now });
        return;
      }
    }
    const eventRoundId = normalizeBlockId(
      tryGet(boostData, 'roundId', 'round_id', 'blockId', 'block_id')
        || tryGet(boostData?.data, 'roundId', 'round_id', 'blockId', 'block_id')
        || currentRoundInfo?.blockId
        || lastLiveSummaryBlockId
        || null
    );

    let playerName = tryGet(boostData, 'userAlias', 'user_alias', 'player', 'userName', 'username', 'user', 'alias') || 'Unknown';

    // Compute multiplier/effect: multiplier * count (supports various field names and strings like '10x' and nested 'data.multiply')
    let mulMatch = getFirstMatch(boostData, 'multiply', 'mult', 'multiplier', 'multiplier_value', 'multiplierValue');
    if (!mulMatch.key && boostData && typeof boostData === 'object') {
      mulMatch = getFirstMatch(boostData.data, 'multiply', 'mult', 'multiplier', 'multiplier_value', 'multiplierValue');
    }
    const selectedMultiplierKey = mulMatch.key;
    const rawMultiplier = mulMatch.value;

    let cntMatch = getFirstMatch(boostData, 'count', 'times', 'num', 'quantity');
    if (!cntMatch.key && boostData && typeof boostData === 'object') {
      cntMatch = getFirstMatch(boostData.data, 'count', 'times', 'num', 'quantity');
    }
    const selectedCountKey = cntMatch.key;
    const rawCount = cntMatch.value;

    const multiplier = parseNumericValue(rawMultiplier) || 1;
    const count = parseNumericValue(rawCount) || 1;
    const effect = Math.round((multiplier * count) || 0);

    // Temporary verbose logging for diagnosis (controlled by TEMP_BOOST_DEBUG_REMAINING and TEMP_BOOST_DEBUG_THRESHOLD)
    const shouldVerboseLog = (typeof TEMP_BOOST_DEBUG_REMAINING === 'number' && TEMP_BOOST_DEBUG_REMAINING > 0) || (typeof TEMP_BOOST_DEBUG_THRESHOLD === 'number' && effect >= TEMP_BOOST_DEBUG_THRESHOLD);
    if (shouldVerboseLog) {
      if (typeof TEMP_BOOST_DEBUG_REMAINING === 'number' && TEMP_BOOST_DEBUG_REMAINING > 0) TEMP_BOOST_DEBUG_REMAINING -= 1;
      try {
        const nestedMult = boostData?.data && (boostData.data.multiply ?? boostData.data.mult ?? boostData.data.multiplier ?? boostData.data.multiplier_value ?? null);
        const altMultipliers = { top: rawMultiplier == null ? null : rawMultiplier, nested: nestedMult, other: { multiplier_value: boostData.multiplier_value ?? null, multiplierValue: boostData.multiplierValue ?? null } };
        const nestedCount = boostData?.data && (boostData.data.count ?? boostData.data.times ?? boostData.data.num ?? boostData.data.quantity ?? null);
        const altCounts = { top: rawCount == null ? null : rawCount, nested: nestedCount };
        console.groupCollapsed('[MW][TEMP_DEBUG] Parsed boost', { boostKey: boostKey || null, boostType, multiplier, count, effect, playerKey });
        console.log('Selected keys:', { selectedMultiplierKey, selectedCountKey });
        console.log('Raw values:', { rawMultiplier, rawCount });
        console.log('Alt multipliers:', altMultipliers);
        console.log('Alt counts:', altCounts);
        console.log('Player meta:', { clanId: playerMeta[playerKey]?.clanId ?? null, clanName: playerMeta[playerKey]?.clanName ?? null });
        console.log('Raw event:', boostData);
        console.groupEnd();
      } catch (e) { console.log('[MW][TEMP_DEBUG] logging error', e); }
    }

    // Extra debug: if the data object contains multiply/mult that's different, log both for diagnosis
    if (boostData && boostData.data && (boostData.data.multiply || boostData.data.mult)) {
      console.log('[MW] Nested data multiply detected', { nestedMult: boostData.data.multiply ?? boostData.data.mult, parsedMultiplier: multiplier, rawMultiplier });
    }

    // If event came directly from the page and no username is present, treat as the local player's boost
    let playerKey = playerName;
    if ((playerName === 'Unknown' || !playerName) && boostData && boostData.source === 'page') {
      playerKey = '__local__';
      console.log('[MW] Marked boost from page as local (__local__)');
    }

    let eventClanId = null;
    let eventClanName = null;
    // Extract club/clan info if present and store for aggregation (supports clan_matchID with id:name)
    try {
      const rawClanId = tryGet(boostData, 'clanId', 'clan_id', 'clanID') || tryGet(boostData?.data, 'clanId', 'clan_id', 'clanID');
      const rawClanName = tryGet(boostData, 'clanName', 'clan_name', 'clan', 'clanNameText') || tryGet(boostData?.data, 'clanName', 'clan_name', 'clan');

      // Then try the special mapping field clan_matchID which may contain id/name
      const clanMatchRaw = tryGet(boostData, 'clan_matchID', 'clanMatchID', 'clan_match_id', 'clanMatch') || tryGet(boostData?.data, 'clan_matchID', 'clanMatchID', 'clan_match_id', 'clanMatch');
      const parsed = clanMatchRaw ? parseClanMatch(clanMatchRaw) : null;

      let clanId = parsed?.clanId ?? null;
      let clanNameFinal = parsed?.clanName ?? rawClanName ?? null;

      if (!clanId && rawClanId != null) {
        const parsedId = commParseNumber(rawClanId);
        if (Number.isFinite(parsedId)) clanId = String(Math.trunc(parsedId));
      }
      if (!clanId && rawClanName != null) {
        const numericName = commParseNumber(rawClanName);
        if (Number.isFinite(numericName)) clanId = String(Math.trunc(numericName));
      }

      eventClanId = clanId ? String(clanId) : null;
      eventClanName = clanNameFinal != null ? String(clanNameFinal) : null;

      if (eventClanId && !eventClanName) {
        const lookupName = clanLookup.get(String(eventClanId));
        if (lookupName) eventClanName = lookupName;
      }
      if (!eventClanId && eventClanName && currentRoundClanIds && currentRoundClanIds.size) {
        const normalizedName = normalizeClanName(eventClanName);
        for (const entry of currentRoundClanIds.values()) {
          const entryName = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? entry?.clan?.name ?? '';
          if (normalizeClanName(entryName) === normalizedName) {
            const matchId = commParseNumber(entry?.clan_id ?? entry?.clanId);
            if (Number.isFinite(matchId)) {
              eventClanId = String(Math.trunc(matchId));
            }
            if (!eventClanName) eventClanName = entryName;
            break;
          }
        }
      }

      if (eventClanId) clanLookup.set(String(eventClanId), eventClanName || `Clan ${eventClanId}`);

      if (eventClanId || eventClanName) {
        playerMeta[playerKey] = { ...(playerMeta[playerKey] || {}), clanId: eventClanId ? String(eventClanId) : null, clanName: eventClanName };
      }
    } catch (err) {
      // Ignore errors in optional clan extraction
    }

    // Record a compact processed event for later inspection (include clan/user if available)
    try {
      recordBoostEvent({
        ts: Number.isFinite(boostEventTs) ? boostEventTs : Date.now(),
        eventTs: Number.isFinite(boostEventTs) ? boostEventTs : Date.now(),
        dedupeKey: boostKey || null,
        boostType,
        playerKey,
        playerName,
        clanId: eventClanId || null,
        clanName: eventClanName || null,
        roundId: eventRoundId || null,
        selectedMultiplierKey: selectedMultiplierKey || null,
        selectedCountKey: selectedCountKey || null,
        rawMultiplier: rawMultiplier == null ? null : rawMultiplier,
        rawCount: rawCount == null ? null : rawCount,
        multiplier,
        count,
        effect,
        topKeys: boostData ? Object.keys(boostData) : []
      });
    } catch (e) { /* ignore logging errors */ }

    const ensureClanBoostEntry = (resolvedClanId, resolvedClanName) => {
      if (!resolvedClanId && !resolvedClanName) return null;
      const primaryKey = resolvedClanId ? `id:${resolvedClanId}` : `name:${resolvedClanName}`;
      let clanEntry = currentRoundClanBoosts.get(primaryKey);
      if (!clanEntry) {
        const label = resolvedClanName || (resolvedClanId ? `Clan ${resolvedClanId}` : 'Unknown Clan');
        clanEntry = { name: label, clan_id: resolvedClanId, boosts: {}, total_boosts: 0 };
      }
      const aliasKeys = new Set();
      if (resolvedClanId) aliasKeys.add(`id:${resolvedClanId}`);
      if (resolvedClanName) aliasKeys.add(`name:${resolvedClanName}`);
      const cleanedName = resolvedClanName ? commCleanClanDisplayName(resolvedClanName) : '';
      if (cleanedName) aliasKeys.add(`name:${cleanedName}`);
      const normalizedName = resolvedClanName ? normalizeClanName(resolvedClanName) : '';
      if (normalizedName) aliasKeys.add(`name:${normalizedName}`);
      aliasKeys.forEach((key) => {
        currentRoundClanBoosts.set(key, clanEntry);
      });
      return clanEntry;
    };

    if (boostType) {
      console.log('[MW] Parsed boost:', { boostType, multiplier, count, effect, playerKey });
      // Exclude 'service' boosts entirely
      if (boostType === 'service') {
        console.log('[MW] Ignoring service boost:', boostType, boostData);
        return;
      }

      registerBoostNotification({
        ts: boostEventTs,
        boostType,
        clanName: eventClanName || playerMeta[playerKey]?.clanName || null,
        playerName: (playerName && playerName !== 'Unknown' && playerName !== '__local__') ? playerName : null,
        count,
        source: 'ws:abilityUsage'
      });

      // Increment the count for this boost type (number of boosts used, not the effect)
      currentRoundBoosts[boostType] = (currentRoundBoosts[boostType] || 0) + count;

      playerBoosts[playerKey] = { ...(playerBoosts[playerKey] || {}) };
      playerBoosts[playerKey][boostType] = (playerBoosts[playerKey][boostType] || 0) + count;

      console.log('[MW] Updated boost counts:', currentRoundBoosts);

      // Update clan-level boost map when we can resolve a clan from the player metadata
      const meta = playerMeta[playerKey] || {};
      const metaClanId = meta.clanId ? String(meta.clanId) : null;
      const metaClanName = meta.clanName || (metaClanId ? (clanLookup.get(metaClanId) || `Clan ${metaClanId}`) : null);
      let resolvedClanId = metaClanId || eventClanId;
      let resolvedClanName = metaClanName || eventClanName || (resolvedClanId ? (clanLookup.get(resolvedClanId) || `Clan ${resolvedClanId}`) : null);
      if (!resolvedClanId && !resolvedClanName) {
        const storedAlias = commGetPersonalUsername();
        const sessionAlias = commSession?.memberName || commSession?.userName || '';
        const localAlias = storedAlias || sessionAlias || '';
        const isLocal = playerKey === '__local__' || (localAlias && commMatchesUsername(playerName, localAlias));
        if (isLocal) {
          const fallbackId = commParseNumber(liveMyClanId ?? commSession?.clanId);
          resolvedClanId = Number.isFinite(fallbackId) ? String(Math.trunc(fallbackId)) : null;
          resolvedClanName = liveMyClanName || commSession?.clanName || resolvedClanName;
        }
      }
      if (resolvedClanId || resolvedClanName) {
        const clanEntry = ensureClanBoostEntry(resolvedClanId, resolvedClanName);
        if (clanEntry) {
          clanEntry.hasEvent = true;
          clanEntry.updatedAt = Date.now();
          clanEntry.roundKey = clanEntry.roundKey || liveBoostRoundKey || normalizeBlockId(currentRoundInfo?.blockId) || null;
          clanEntry.boosts[boostType] = (clanEntry.boosts[boostType] || 0) + count;
          clanEntry.total_boosts += count;
          setClanBoostMeta(clanEntry, boostType, 'ws:abilityUsage', boostEventTs, boostEventTs ? [boostEventTs] : [], { source: 'ws:abilityUsage', ts: boostEventTs });
          setBoostNotificationCount(clanEntry, boostType, clanEntry.boosts[boostType]);
        }
      } else {
        const pendingKey = playerKey || playerName || '__local__';
        const entry = pendingBoostByPlayer.get(pendingKey) || {};
        entry[boostType] = (entry[boostType] || 0) + count;
        pendingBoostByPlayer.set(pendingKey, entry);
        try {
          const resolvePromise = typeof refreshLiveMyClanName === 'function'
            ? refreshLiveMyClanName(true)
            : Promise.resolve();
          Promise.resolve(resolvePromise)
            .catch(() => {})
            .finally(() => {
              applyPendingLocalBoosts();
              if (typeof refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
                refreshCurrentRoundCard(lastCurrentRoundLivePayload);
              }
            });
        } catch (_) {
          applyPendingLocalBoosts();
        }
      }

      // If we used the __local__ placeholder, attempt to resolve the real username and remap
      if (playerKey === '__local__') {
        try {
          chrome.runtime.sendMessage({ type: 'EXTRACT_MY_USERNAME' }, (res) => {
            if (res && res.ok && res.data) {
              const extracted = typeof res.data === 'string' ? res.data : (res.data && res.data.data) ? res.data.data : null;
              if (extracted && extracted.trim()) {
                const realName = extracted.trim();
                console.log('[MW] Extracted local username from page:', realName);
                // Move counts from __local__ to realName
                playerBoosts[realName] = { ...(playerBoosts[realName] || {}) };
                const types = playerBoosts['__local__'] || {};
                Object.entries(types).forEach(([t, v]) => {
                  playerBoosts[realName][t] = (playerBoosts[realName][t] || 0) + v;
                });
                // Move metadata (clan) as well
                if (playerMeta['__local__']) {
                  playerMeta[realName] = { ...(playerMeta[realName] || {}), ...playerMeta['__local__'] };
                  delete playerMeta['__local__'];
                }
                delete playerBoosts['__local__'];
                refreshBoostDisplay();
              }
            }
          });
        } catch (err) {
          console.warn('[MW] Failed to extract local username', err);
        }
      }

      // Refresh the boost display
      refreshBoostDisplay();
      if (typeof refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }
    } else {
      console.warn('[MW] Could not extract boost type from:', boostData);
    }
  }

  // Function to fetch Round State baseline scores at round start
  async function fetchRoundStateBaseline(blockId, leagueId) {
    console.log('[MW] ===== FETCHING ROUND STATE BASELINE =====');
    console.log('[MW] Block ID:', blockId, 'League ID:', leagueId);
    window.debugLogger?.info('Fetching Round State Baseline', { blockId, leagueId });
    const now = Date.now();
    if (baselineNoParticipantsUntil && now < baselineNoParticipantsUntil) {
      console.log('[MW] Baseline fetch skipped (no participants cooldown).');
      return null;
    }

    try {
      const endpoint = 'https://api.gomining.com/api/nft-game/round/get-state';
      const payload = {
        type: 'clans',
        leagueId: leagueId || commKorrelCurrentLeagueId || 13,
        pagination: { skip: 0, limit: 50 }
      };

      console.log('[MW] Round State baseline payload:', payload);

      // Use content script proxy for authenticated call
      const tab = await commFindMiningWarsTab();
      if (!tab?.id) {
        console.error('[MW] No active gomining.com tab found for Round State baseline');
        window.debugLogger?.error('No Active Tab', { reason: 'Cannot fetch baseline without gomining.com tab' });
        return null;
      }

      const response = await chrome.tabs.sendMessage(tab.id, {
        type: 'API_CALL',
        endpoint,
        method: 'POST',
        payload
      });

      if (!response?.success) {
        console.error('[MW] Round State baseline fetch failed:', response);
        window.debugLogger?.error('Baseline Fetch Failed', { status: response?.status, error: response?.error });
        return null;
      }

      console.log('[MW] Round State baseline response:', response.data);

      // Extract participants and build baseline map
      const participants = response.data?.data?.participants ||
                          response.data?.data ||
                          response.data?.participants ||
                          [];

      if (!Array.isArray(participants) || participants.length === 0) {
        console.warn('[MW] Round State baseline returned no participants');
        baselineNoParticipantsUntil = Date.now() + 120000;
        window.debugLogger?.warning('No Participants', {
          response: response.data,
          retryAfterMs: 120000
        });
        return null;
      }

      const baseline = new Map();
      participants.forEach(item => {
        const clanId = item.clanId ?? item.clan_id ?? item.id;
        const clanName = item.clanName ?? item.clan_name ?? item.name ?? '';
        const score = Number(item.score ?? item.currentScore ?? item.totalScore ?? 0);
        const power = Number(item.rosterPower ?? item.roster_power ?? item.clanPower ?? item.power ?? 0);
        const position = item.position ?? item.rank ?? 0;

        if (clanId != null) {
          baseline.set(String(clanId), {
            clanId: String(clanId),
            clanName,
            baselineScore: score,              // Complete score at baseline moment (from API)
            baselineWebSocketScore: null,      // Will be set on first WebSocket event after baseline
            currentWebSocketScore: 0,          // Current WebSocket score (will be updated)
            totalScore: score,                 // Total = baselineScore + delta (start with baseline)
            power,
            position
          });
          console.log(`[MW] Baseline: Clan ${clanId} (${clanName}) = ${score} score`);
        }
      });

      console.log(`[MW] Round State baseline established: ${baseline.size} clans`);
      window.debugLogger?.success('Baseline Established', {
        clans: baseline.size,
        blockId,
        topClan: Array.from(baseline.values())[0]
      });

      return baseline;

    } catch (err) {
      console.error('[MW] Failed to fetch Round State baseline:', err);
      window.debugLogger?.error('Baseline Fetch Error', { error: err.message });
      return null;
    }
  }

  // Function to reset boosts when a new round starts
function resetBoostScoreCaches() {
  liveWsAddedScoreByClan = new Map();
  liveWsAddedScoreUpdatedAt = 0;
  liveWsAddedScoreBaselineByClan = new Map();
  liveBoostPercentTotal = null;
  liveBoostPercentUpdatedAt = 0;
  liveBoostPercentCache.clear();
  liveBoostSortOrder = [];
  liveBoostSortUpdatedAt = 0;
  liveBoostSortRoundId = null;
  liveBoostEntryCache = new Map();
  liveBoostHourlyPrevAddedByClan = new Map();
  liveWsBoostActivationByClan = new Map();
}

function resetRoundBoosts(trackingComplete = false) {
  console.log('[MW] Resetting boost counts for new round, tracking complete:', trackingComplete);
  currentRoundBoosts = {};
  playerBoosts = {};
  playerMeta = {};
  pendingBoostByPlayer = new Map();
  liveScoreHoldCache = new Map();
  liveRankHoldCache = new Map();
  liveScoreSortCache = new Map();
  liveLastRoundSeconds = null;
  liveStableSortOrder = [];
  liveStableSortAt = 0;
  currentRoundClanIds.clear(); // Clear clan IDs for the new round
  liveClanPresenceCache = new Map();
  resetBoostScoreCaches();
  liveBoostRoundKey = null;
  clearBoostEventLog();
  clearBoostNotificationSeen();
  clearBoostNotificationCounts();
  roundTrackingComplete = trackingComplete;
  refreshBoostDisplay();
}

function resetBoostIconsForRoundChange() {
  currentRoundBoosts = {};
  currentRoundClanBoosts = new Map();
  pendingBoostByPlayer = new Map();
  liveClanPresenceCache = new Map();
  boostHelperMyClanBoosts = null;
  boostHelperActiveRuleIds = new Set();
  boostHelperLastRoundKey = null;
  boostHelperRoundStartAt = null;
  clearBoostEventLog();
  clearBoostNotificationSeen();
  clearBoostNotificationCounts();
  refreshBoostDisplay();
}

  // Function to update current round display
  function applyCalculatorSimToClans(leadingClans, timerValue, blockId) {
    const base = Array.isArray(leadingClans) ? [...leadingClans] : [];
    if (blockId && blockId !== simTimerBlockId) {
      simTimerBlockId = blockId;
      simTimerSeconds = null;
      simTimerLastSync = 0;
      simTimerSource = null;
    }
    const simEntry = buildCalculatorSimEntry(timerValue);
    if (!simEntry) return base;
    base.push(simEntry);
    return base;
  }

function refreshCurrentRoundCard(payload) {
  if (!payload) return;
  const previousTotalScoreChart = currentRoundContainer
    ? currentRoundContainer.querySelector('.live-total-score-chart')
    : null;
  if (liveTotalScoreWindowEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'liveTotalScoreWindowMode') {
      liveTotalScoreWindowEditing = false;
    } else {
      return;
    }
  }
    if (currentRoundLimitLockUntil && Date.now() < currentRoundLimitLockUntil) {
      return;
    }
    if (currentRoundLimitEditing) {
      const active = document.activeElement;
      if (!active || active.id !== 'currentRoundClanLimit') {
        currentRoundLimitEditing = false;
      } else {
        return;
      }
    }
  if (currentRoundPowerEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'currentRoundPowerSource') {
      currentRoundPowerEditing = false;
    } else {
      return;
    }
  }
  if (currentRoundScoreEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'currentRoundScoreSource') {
      currentRoundScoreEditing = false;
    } else {
      return;
    }
  }
  if (currentRoundDisplayModeEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'currentRoundDisplayMode') {
      currentRoundDisplayModeEditing = false;
    } else {
      return;
    }
  }
  if (currentRoundBoostApiLockUntil && Date.now() < currentRoundBoostApiLockUntil) {
    return;
  }
  if (currentRoundBoostApiEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'currentRoundBoostApiSync') {
      currentRoundBoostApiEditing = false;
    } else {
      return;
    }
  }
  if (liveProfileClanEditing) {
    const active = document.activeElement;
    if (!active || active.id !== 'liveProfileClanSelect') {
      liveProfileClanEditing = false;
    } else {
        return;
      }
    }
    if (liveProfileClanLockUntil && Date.now() < liveProfileClanLockUntil) {
      return;
    }
    const storedProfileName = getStoredLiveProfileClan() || liveProfileClanName || '';
    if (!liveMyClanName || isSimClanName(storedProfileName)) {
      refreshLiveMyClanName().catch(() => {});
    }
    applyPendingLocalBoosts();
    const normalized = commNormalizeRoundTiming(payload.timer, payload.multiplier);
    const safeTimer = normalized.roundTime != null ? normalized.roundTime : payload.timer;
    const rawMultiplier = payload.multiplier != null ? String(payload.multiplier).trim() : '';
    const safeMultiplier = normalized.multiplier != null
      ? normalized.multiplier
      : (rawMultiplier.includes(':') ? null : payload.multiplier);
    const simTimerValue = resolveSimRoundTimer(safeTimer, safeMultiplier);
    if (simTimerValue != null) {
      latestRoundTimerValue = simTimerValue;
      const parsedTimer = parseRoundTimerSeconds(simTimerValue);
      if (parsedTimer != null) latestRoundTimerSeconds = parsedTimer;
    }
    if (
      LIVE_API_STATE_ENABLED
      && !liveApiStatePending
      && (!Array.isArray(liveApiStateRaw) || liveApiStateRaw.length === 0)
    ) {
      const now = Date.now();
      if (!liveApiStateLastAttemptAt || now - liveApiStateLastAttemptAt > 15000) {
        commRefreshLiveApiState({ force: true }).catch(() => {});
      }
    }
    const currentLeagueId = commResolveLiveLeagueId();
    const apiMatchesLeague = !Number.isFinite(currentLeagueId)
      || !Number.isFinite(liveApiLastLeagueId)
      || Math.trunc(currentLeagueId) === Math.trunc(liveApiLastLeagueId);
    // Use API data only for live view
    let leadingClans = null;
    let fullClanSnapshot = null;

    // Try API data first (has all clans with roster power)
    if (apiMatchesLeague && Array.isArray(liveApiStateRaw) && liveApiStateRaw.length > 0) {
      fullClanSnapshot = buildLiveRoundRowsFromApi(liveApiStateRaw, currentRoundClanLimit);
      leadingClans = fullClanSnapshot;
    } else if (apiMatchesLeague && Array.isArray(liveApiStateTransformed) && liveApiStateTransformed.length > 0) {
      fullClanSnapshot = buildClanSnapshotFromState(liveApiStateTransformed, currentRoundClanLimit);
      leadingClans = fullClanSnapshot;
    } else if (apiMatchesLeague && liveApiStateLookup && liveApiStateLookup.size > 0) {
      const apiClans = Array.from(liveApiStateLookup.values());
      fullClanSnapshot = buildClanSnapshotFromState(apiClans, currentRoundClanLimit);
      leadingClans = fullClanSnapshot;
    } else if (Array.isArray(lastAllUsersStateV2Raw) && lastAllUsersStateV2Raw.length > 0) {
      fullClanSnapshot = buildClanSnapshotFromState(lastAllUsersStateV2Raw, currentRoundClanLimit);
      leadingClans = fullClanSnapshot;
    }
    if (Array.isArray(leadingClans) && leadingClans.length > 0) {
      lastLiveClanSnapshot = leadingClans;
      window.lastLiveClanSnapshot = leadingClans;
      window.__mwLastLiveClanSnapshot = leadingClans;
    } else if (Array.isArray(lastLiveClanSnapshot) && lastLiveClanSnapshot.length > 0) {
      leadingClans = lastLiveClanSnapshot;
      fullClanSnapshot = lastLiveClanSnapshot;
    }
    const stabilizedSnapshot = stabilizeLiveClanSnapshot(fullClanSnapshot || leadingClans, currentRoundClanLimit);
    if (Array.isArray(stabilizedSnapshot) && stabilizedSnapshot.length > 0) {
      leadingClans = stabilizedSnapshot;
      fullClanSnapshot = stabilizedSnapshot;
    }
    const enrichedClans = applyCalculatorSimToClans(fullClanSnapshot || leadingClans, simTimerValue, payload.blockId);
    const liveCard = buildCurrentRoundLiveCard({
      blockId: payload.blockId,
      timer: safeTimer,
      multiplier: safeMultiplier,
      leadingClans: enrichedClans,
      boosts: currentRoundBoosts,
      clanBoosts: currentRoundClanBoosts,
      clanLimit: currentRoundClanLimit
    });
    renderCards(currentRoundContainer, [liveCard]);
    if (previousTotalScoreChart) {
      const placeholder = currentRoundContainer.querySelector('.live-total-score-chart');
      if (placeholder && placeholder !== previousTotalScoreChart) {
        placeholder.replaceWith(previousTotalScoreChart);
      }
    }
    renderLiveTotalScoreChart(currentRoundContainer, liveTotalScoreSeries, currentRoundClanBoosts);
    boostHelperUpdateScoreSamples();
    bindLiveScorePills(currentRoundContainer);
    bindLiveBoostPills(currentRoundContainer);
    const limitSelect = currentRoundContainer.querySelector('#currentRoundClanLimit');
    if (limitSelect) {
      limitSelect.value = String(currentRoundClanLimit);
      if (!limitSelect.__mwBound) {
        limitSelect.__mwBound = true;
        const applyLimitValue = (rawValue) => {
          const nextValue = Number(rawValue);
          if (!Number.isFinite(nextValue) || nextValue <= 0) return;
          if (nextValue === currentRoundClanLimit) return;
          currentRoundClanLimit = nextValue;
        };
        const armLimitLock = () => {
          currentRoundLimitEditing = true;
          currentRoundLimitLockUntil = Date.now() + 800;
        };
        limitSelect.addEventListener('pointerdown', armLimitLock);
        limitSelect.addEventListener('focus', armLimitLock);
        limitSelect.addEventListener('blur', () => {
          applyLimitValue(limitSelect.value);
          currentRoundLimitEditing = false;
          currentRoundLimitLockUntil = 0;
          if (lastCurrentRoundLivePayload) {
            refreshCurrentRoundCard(lastCurrentRoundLivePayload);
          }
        });
        limitSelect.addEventListener('change', (event) => {
          applyLimitValue(event.target.value);
          currentRoundLimitEditing = false;
          currentRoundLimitLockUntil = 0;
          refreshCurrentRoundCard(lastCurrentRoundLivePayload || payload);
        });
      }
    }
  const powerSelect = currentRoundContainer.querySelector('#currentRoundPowerSource');
  if (powerSelect) {
    powerSelect.value = commNormalizeLivePowerSource(livePowerSource);
    powerSelect.addEventListener('pointerdown', () => {
      currentRoundPowerEditing = true;
    });
      powerSelect.addEventListener('focus', () => {
        currentRoundPowerEditing = true;
      });
      powerSelect.addEventListener('blur', () => {
        currentRoundPowerEditing = false;
        if (lastCurrentRoundLivePayload) {
          refreshCurrentRoundCard(lastCurrentRoundLivePayload);
        }
      });
      powerSelect.addEventListener('change', (event) => {
        const nextValue = commNormalizeLivePowerSource(event.target.value);
        livePowerSource = nextValue;
        commSetLivePowerSource(nextValue);
        currentRoundPowerEditing = false;
        if (Array.isArray(lastAllUsersStateV2Raw)) {
          lastAllUsersStateV2Snapshot = buildClanSnapshotFromState(lastAllUsersStateV2Raw, currentRoundClanLimit);
      }
      refreshCurrentRoundCard(lastCurrentRoundLivePayload || payload);
    });
  }
  const scoreSelect = currentRoundContainer.querySelector('#currentRoundScoreSource');
  if (scoreSelect) {
    scoreSelect.value = commNormalizeLiveScoreSource(liveScoreSource);
    scoreSelect.addEventListener('pointerdown', () => {
      currentRoundScoreEditing = true;
    });
    scoreSelect.addEventListener('focus', () => {
      currentRoundScoreEditing = true;
    });
    scoreSelect.addEventListener('blur', () => {
      currentRoundScoreEditing = false;
      if (lastCurrentRoundLivePayload) {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }
    });
    scoreSelect.addEventListener('change', (event) => {
      const nextValue = commNormalizeLiveScoreSource(event.target.value);
      liveScoreSource = nextValue;
      commSetLiveScoreSource(nextValue);
      currentRoundScoreEditing = false;
      if (Array.isArray(lastAllUsersStateV2Raw)) {
        lastAllUsersStateV2Snapshot = buildClanSnapshotFromState(lastAllUsersStateV2Raw, currentRoundClanLimit);
      }
      refreshCurrentRoundCard(lastCurrentRoundLivePayload || payload);
    });
  }
  const displayModeSelect = currentRoundContainer.querySelector('#currentRoundDisplayMode');
  if (displayModeSelect) {
    displayModeSelect.value = commNormalizeLiveDisplayMode(liveDisplayMode);
    displayModeSelect.addEventListener('pointerdown', () => {
      currentRoundDisplayModeEditing = true;
    });
    displayModeSelect.addEventListener('focus', () => {
      currentRoundDisplayModeEditing = true;
    });
    displayModeSelect.addEventListener('blur', () => {
      currentRoundDisplayModeEditing = false;
      if (lastCurrentRoundLivePayload) {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }
    });
    displayModeSelect.addEventListener('change', (event) => {
      const nextValue = commNormalizeLiveDisplayMode(event.target.value);
      liveDisplayMode = nextValue;
      commSetLiveDisplayMode(nextValue);
      currentRoundDisplayModeEditing = false;
      refreshCurrentRoundCard(lastCurrentRoundLivePayload || payload);
    });
  }
  // Boost API is always enabled; cache sync UI removed.
  const powerDebug = currentRoundContainer.querySelector('#currentRoundPowerDebug');
  if (powerDebug) {
    if (powerDebug.open !== currentRoundPowerDebugOpen) {
      powerDebug.open = currentRoundPowerDebugOpen;
    }
    powerDebug.addEventListener('toggle', () => {
      currentRoundPowerDebugOpen = powerDebug.open;
    });
  }
  const scoreDebug = currentRoundContainer.querySelector('#currentRoundScoreDebug');
  if (scoreDebug) {
    if (scoreDebug.open !== currentRoundScoreDebugOpen) {
      scoreDebug.open = currentRoundScoreDebugOpen;
    }
    scoreDebug.addEventListener('toggle', () => {
      currentRoundScoreDebugOpen = scoreDebug.open;
    });
  }
  const boostDebugToggle = currentRoundContainer.querySelector('#currentRoundBoostDebugToggle');
  if (boostDebugToggle) {
    boostDebugToggle.addEventListener('click', () => {
      if (commIsBoostDebugActive()) {
        commSetBoostDebugClan('');
      } else {
        commSetBoostDebugClan('all');
      }
      if (lastCurrentRoundLivePayload) {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      }
    });
  }
  const profileSelect = currentRoundContainer.querySelector('#liveProfileClanSelect');
  if (profileSelect) {
    profileSelect.addEventListener('pointerdown', () => {
      liveProfileClanEditing = true;
        liveProfileClanLockUntil = Date.now() + 1500;
      });
      profileSelect.addEventListener('focus', () => {
        liveProfileClanEditing = true;
        liveProfileClanLockUntil = Date.now() + 1500;
      });
      profileSelect.addEventListener('blur', () => {
        liveProfileClanEditing = false;
        liveProfileClanLockUntil = Date.now() + 250;
        if (lastCurrentRoundLivePayload) {
          refreshCurrentRoundCard(lastCurrentRoundLivePayload);
        }
      });
      profileSelect.addEventListener('change', (event) => {
        const nextValue = String(event.target.value || '').trim();
        liveProfileClanName = nextValue;
        setStoredLiveProfileClan(nextValue);
        liveProfileClanEditing = false;
        liveProfileClanLockUntil = Date.now() + 250;
        refreshCurrentRoundCard(lastCurrentRoundLivePayload || payload);
      });
    }
  }

  window.__refreshCurrentRoundCard = () => {
    if (lastCurrentRoundLivePayload) {
      refreshCurrentRoundCard(lastCurrentRoundLivePayload);
      return;
    }
    if (currentRoundInfo) {
      refreshCurrentRoundCard(currentRoundInfo);
    }
  };

  function normalizeWsStateArray(data) {
    const extraPaths = [
      'data',
      'data.array',
      'data.items',
      'data.data',
      'data.users',
      'data.usersForClient',
      'data.usersState',
      'data.userState',
      'data.clans',
      'users',
      'usersForClient',
      'usersState',
      'userState',
      'clans',
      'array',
      'participants'
    ];
    if (Array.isArray(data)) {
      if (data.length === 1 && data[0] && typeof data[0] === 'object') {
        const inner = resolveApiArray(data[0], extraPaths);
        if (Array.isArray(inner) && inner.length) {
          return inner;
        }
      }
      return data;
    }
    const fallback = resolveApiArray(data, extraPaths);
    return Array.isArray(fallback) ? fallback : [];
  }

  function mergeWsClanStateIntoMap(wsArray) {
    if (!Array.isArray(wsArray) || wsArray.length === 0) return null;
    let merged = false;
    wsArray.forEach((item) => {
      if (!item || typeof item !== 'object') return;
      const hasClanKey = item.clanId != null
        || item.clan_id != null
        || item.clanName != null
        || item.clan_name != null
        || item.name != null
        || item.usedAbilities;
      if (!hasClanKey) return;
      const key = normalizeClanKey(item);
      if (!key) return;
      const prev = currentRoundClanIds.get(key);
      const next = prev && typeof prev === 'object' ? { ...prev, ...item } : item;
      currentRoundClanIds.set(key, next);
      merged = true;
    });
    if (!merged) return null;
    const values = Array.from(currentRoundClanIds.values());
    if (!values.length) return null;
    if (values.length < wsArray.length) return null;
    return values;
  }

  function filterUsedAbilitiesForCurrentRound(usedAbilities) {
    if (!Array.isArray(usedAbilities) || usedAbilities.length === 0) return [];
  const roundStartAt = getLiveRoundStartAtMs();
  if (!Number.isFinite(roundStartAt)) return usedAbilities;
  const now = Date.now();
  const cutoff = roundStartAt - LIVE_BOOST_TS_SKEW_MS;
  const upper = now + LIVE_BOOST_TS_SKEW_MS;
    const currentRoundId = normalizeBlockId(
      currentRoundInfo?.blockId ?? lastLiveSummaryBlockId ?? currentRoundInfo?.roundId ?? null
    );
    let sawTimestamp = false;
    const filtered = usedAbilities.map((ability) => {
      const rawRoundId = ability?.roundId ?? ability?.round_id ?? ability?.blockId ?? ability?.block_id ?? null;
      const abilityRoundId = normalizeBlockId(rawRoundId);
      if (currentRoundId && abilityRoundId && abilityRoundId !== currentRoundId) {
        return {
          ...ability,
          count: 0,
          usedAtTimestamps: [],
          __mwBoostDebug: {
            reason: 'round-id-mismatch',
            currentRoundId,
            abilityRoundId
          }
        };
      }
      const timestamps = extractAbilityTimestampsMs(ability);
      if (timestamps.length) sawTimestamp = true;
      if (!timestamps.length) {
        return {
          ...ability,
          __mwNoTimestamp: true,
          __mwBoostDebug: {
            reason: 'no-timestamps',
            roundStartAt,
            cutoff,
            upper,
            now
          }
        };
      }
      const valid = timestamps
        .filter((ts) => Number.isFinite(ts) && ts >= cutoff && ts <= upper);
      const count = valid.length;
      return {
        ...ability,
        count,
        usedAtTimestamps: valid,
        __mwBoostDebug: {
          roundStartAt,
          cutoff,
          upper,
          now,
          originalCount: timestamps.length,
          validCount: count
        }
      };
    });
    if (!sawTimestamp) return usedAbilities;
    const normalized = filtered.map((ability) => {
      if (ability?.__mwNoTimestamp) {
        return { ...ability, count: 0, usedAtTimestamps: [] };
      }
      return ability;
    });
    return normalized.filter((ability) => Number(ability?.count || 0) > 0);
  }

  function stabilizeLiveClanSnapshot(entries, limit) {
    if (!Array.isArray(entries)) return entries;
    const now = Date.now();
    const nextKeys = new Set();
    entries.forEach((entry) => {
      const key = normalizeClanKey(entry);
      if (!key) return;
      nextKeys.add(key);
      const cached = liveClanPresenceCache.get(key);
      const fresh = cached && now - cached.lastSeen <= LIVE_CLAN_PRESENCE_TTL_MS;
      const seenCount = fresh ? (Number(cached.seenCount || 0) + 1) : 1;
      const confirmed = Boolean(cached?.confirmed) || seenCount >= 2;
      liveClanPresenceCache.set(key, {
        entry,
        lastSeen: now,
        seenCount,
        confirmed
      });
    });
    liveClanPresenceCache.forEach((data, key) => {
      if (!data || now - data.lastSeen > LIVE_CLAN_PRESENCE_TTL_MS) {
        liveClanPresenceCache.delete(key);
      }
    });
    const merged = [];
    const added = new Set();
    entries.forEach((entry) => {
      const key = normalizeClanKey(entry);
      if (!key || added.has(key)) return;
      const cached = liveClanPresenceCache.get(key);
      if (!cached?.confirmed) return;
      merged.push(entry);
      added.add(key);
    });
    liveClanPresenceCache.forEach((data, key) => {
      if (added.has(key) || !data?.entry || !data.confirmed) return;
      merged.push(data.entry);
      added.add(key);
    });
    const cap = Number.isFinite(Number(limit))
      ? Math.max(Math.trunc(Number(limit)) * 2, merged.length)
      : merged.length;
    return merged.slice(0, cap);
  }

  function updateCurrentRound(data) {
    if (!data) return;

    const normalizedWsArray = normalizeWsStateArray(data);
    if (Array.isArray(data) && Array.isArray(normalizedWsArray) && normalizedWsArray !== data) {
      console.log('[MW] Unwrapped allUsersStateV2 payload:', {
        from: data.length,
        to: normalizedWsArray.length,
        keys: data[0] && typeof data[0] === 'object' ? Object.keys(data[0]) : null
      });
      data = normalizedWsArray;
    }

    if (Array.isArray(data)) {
      const mergedData = mergeWsClanStateIntoMap(data);
      if (mergedData && mergedData.length > data.length) {
        data = mergedData;
      }
    }

    const dataType = Array.isArray(data) ? 'array' : typeof data;
    const dataSize = Array.isArray(data)
      ? data.length
      : (data && typeof data === 'object' ? Object.keys(data).length : 0);
    const sampleKeys = (!Array.isArray(data) && data && typeof data === 'object')
      ? Object.keys(data).slice(0, 8)
      : null;
    console.log('[MW] updateCurrentRound data:', { type: dataType, size: dataSize, keys: sampleKeys });

    // If data is an array (allUsersStateV2), extract clan IDs and sync boost counts
    if (Array.isArray(data)) {
      lastAllUsersStateV2Raw = data;
      lastAllUsersStateV2At = Date.now();
      if (typeof window !== 'undefined') {
        window.__mwLastAllUsersStateV2Raw = data;
        window.__mwLastAllUsersStateV2At = lastAllUsersStateV2At;
      }
      updateLiveWsAddedScores(data);
      if (typeof window.__refreshCurrentRoundCard === 'function') {
        window.__refreshCurrentRoundCard();
      }
      console.log('[MW] ===== INSIDE ARRAY BLOCK =====');
      // Ensure baseline is fetched when we receive first WebSocket data (handles extension loaded mid-round)
      // Do this BEFORE checking if it's player vs clan data, so we always attempt baseline fetch
      console.log('[MW] Checking baseline status: baseline=', !!roundStateBaseline, 'attempted=', baselineFetchAttempted, 'blockId=', currentRoundBlockId);

      const hasClanId = data.some(item => item && (item.clanId != null || item.clan_id != null || item.usedAbilities));
      const hasPlayerId = data.some(item => item && (item.userAlias != null || item.playerName != null || item.userName != null || item.username != null));
      const isPlayerData = !hasClanId && hasPlayerId;

      if (isPlayerData) {
        lastAllPlayersStateSnapshot = buildPlayerSnapshotFromState(data, 10);
        if (Array.isArray(data)) {
          lastAllUsersStateV2Raw = data;
        }
        // Don't return yet - check baseline first, then return
      } else {
        const mergedSnapshot = currentRoundClanIds.size ? Array.from(currentRoundClanIds.values()) : data;
        lastAllUsersStateV2Raw = mergedSnapshot.length >= data.length ? mergedSnapshot : data;
      }

      // Fetch baseline from WebSocket events as fallback if liveSummary/roundOpened don't fire
      // Use aggressive checks to prevent too many attempts:
      // 1. Only if we have NO baseline or baseline is empty
      // 2. Only if we haven't attempted yet OR we have a valid blockId that changed
      // 3. 30 second cooldown between attempts

      // Try to get blockId from multiple sources
      const blockIdCandidate = currentRoundInfo?.blockId
        || lastCurrentRoundLivePayload?.blockId
        || lastLiveSummaryBlockId
        || currentRoundBlockId;
      const hasValidBlockId = blockIdCandidate
        && blockIdCandidate !== '-'
        && blockIdCandidate !== '-'
        && String(blockIdCandidate).trim() !== '';

      if ((!roundStateBaseline || roundStateBaseline.size === 0) && !baselineFetchAttempted && hasValidBlockId) {
        // Cooldown: Don't retry more than once every 30 seconds
        const now = Date.now();
        if (baselineNoParticipantsUntil && now < baselineNoParticipantsUntil) {
          console.log('[MW] Baseline fetch paused (no participants cooldown).');
          return;
        }
        if (now - baselineFetchLastAttemptTime < 30000) {
          console.log('[MW] Baseline fetch on cooldown, skipping...');
          return;
        }

        console.log('[MW] No baseline yet - WebSocket trigger attempting to fetch Round State');
        console.log('[MW] Using blockId:', blockIdCandidate, 'from sources:', {
          currentRoundInfo: currentRoundInfo?.blockId,
          lastLiveSummaryBlockId,
          currentRoundBlockId
        });

        // Use ensureRoundStateBaseline which handles all the logic
        baselineFetchLastAttemptTime = now;
        const blockId = blockIdCandidate;
        const leagueId = commKorrelCurrentLeagueId || lastLiveSummaryLeague || 13;

        ensureRoundStateBaseline(blockId, leagueId).then(() => {
          console.log('[MW] WebSocket trigger: Baseline fetch completed');
          // Refresh display
          if (typeof refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
            refreshCurrentRoundCard(lastCurrentRoundLivePayload);
          }
        }).catch(err => {
          console.error('[MW] WebSocket trigger: Baseline fetch failed:', err);
          baselineFetchAttempted = false; // Allow retry
        });
      }

      // If this was player data, we're done - don't process clan-specific logic below
      if (isPlayerData) {
        return;
      }

      const allowBoostSync = !liveCacheApplying;
      let boostSyncTouched = false;
      const roundStartAt = getLiveRoundStartAtMs();
      const now = Date.now();
      const cutoff = Number.isFinite(roundStartAt) ? roundStartAt - LIVE_BOOST_TS_SKEW_MS : null;
      const upper = Number.isFinite(roundStartAt) ? now + LIVE_BOOST_TS_SKEW_MS : null;
      data.forEach(clanData => {
        const clanIdValue = commParseNumber(clanData?.clanId ?? clanData?.clan_id ?? null);
        const clanKey = normalizeClanKey(clanData);
        if (clanKey) {
          const prev = currentRoundClanIds.get(clanKey);
          currentRoundClanIds.set(clanKey, prev && typeof prev === 'object' ? { ...prev, ...clanData } : clanData);
        }
        const mergedApi = commMergeWsFieldsIntoApiEntry(clanData);
        if (mergedApi && clanIdValue != null) {
          console.log('[MW] Merged WS fields into API state for clan:', clanIdValue);
        }
        if (Number.isFinite(clanIdValue)) {
          console.log('[MW] Tracked clan ID:', clanIdValue);
          const knownName = clanLookup.get(String(clanIdValue));
          if (knownName) {
            const key = String(clanIdValue);
            const last = recentClanUpserts.get(key) || 0;
            const now = Date.now();
            if (now - last > 60000) {
              recentClanUpserts.set(key, now);
              upsertClanMatch(clanIdValue, knownName).then(() => {
                console.log('[MW] Upserted clan mapping from allUsersStateV2:', clanIdValue, knownName);
              }).catch(err => {
                console.warn('[MW] Failed to upsert clan mapping from allUsersStateV2:', err);
              });
            }
          }

          // Calculate delta from WebSocket score changes
          if (roundStateBaseline && roundStateBaseline.has(String(clanIdValue))) {
            const clanId = String(clanIdValue);
            const baseline = roundStateBaseline.get(clanId);
            const wsScore = Number(clanData.currentAddedScore ?? clanData.current_added_score ?? 0);

            // If this is the first WebSocket event after baseline, set the baseline WebSocket score
            if (baseline.baselineWebSocketScore === null) {
              baseline.baselineWebSocketScore = wsScore;
              console.log(`[MW] First WS after baseline: Clan ${clanId} baseline WS score set to ${wsScore}`);
            }

            // Update current WebSocket score
            baseline.currentWebSocketScore = wsScore;

            // Calculate delta: difference between current WebSocket and baseline WebSocket
            const delta = baseline.currentWebSocketScore - baseline.baselineWebSocketScore;

            // Total = baseline score + delta
            baseline.totalScore = baseline.baselineScore + delta;

            console.log(`[MW] Score delta: Clan ${clanId} baseline=${baseline.baselineScore} + delta(${wsScore} - ${baseline.baselineWebSocketScore}) = ${baseline.totalScore}`);
          }
        }

        // Sync boost counts from usedAbilities array
        const activeBoostValue = commParseNumber(clanData?.activeBoostScore ?? clanData?.active_boost_score);
        const rawUsedAbilities = Array.isArray(clanData?.usedAbilities) ? clanData.usedAbilities : [];
        const hasAbilityTs = hasAbilityTimestamps(rawUsedAbilities);
        let usedAbilities = filterUsedAbilitiesForCurrentRound(rawUsedAbilities);
        if (!hasAbilityTs && (!Number.isFinite(activeBoostValue) || activeBoostValue <= 0)) {
          usedAbilities = [];
        }
        const hasAbility = usedAbilities.some((ability) => Number(ability?.count || 0) > 0);
        const boostSignal =
          (Number.isFinite(activeBoostValue) && activeBoostValue > 0) ||
          hasAbility;
        if (!boostSignal && currentRoundClanBoosts && currentRoundClanBoosts.size) {
          const clanId = clanData.clanId != null ? String(clanData.clanId) : null;
          const rawClanName =
            clanData?.clanName ??
            clanData?.clan_name ??
            clanData?.clan ??
            clanData?.name ??
            null;
          const clanName = rawClanName || (clanId ? (clanLookup.get(clanId) || `Clan ${clanId}`) : null);
          const aliasKeys = new Set();
          if (clanId) aliasKeys.add(`id:${clanId}`);
          if (clanName) aliasKeys.add(`name:${clanName}`);
          const cleanedName = clanName ? commCleanClanDisplayName(clanName) : '';
          if (cleanedName) aliasKeys.add(`name:${cleanedName}`);
          const normalizedName = clanName ? normalizeClanName(clanName) : '';
          if (normalizedName) aliasKeys.add(`name:${normalizedName}`);
          let clanEntry = null;
          for (const key of aliasKeys) {
            if (currentRoundClanBoosts.has(key)) {
              clanEntry = currentRoundClanBoosts.get(key);
              break;
            }
          }
          if (clanEntry && !clanEntry.hasEvent) {
            clanEntry.boosts = {};
            clanEntry.total_boosts = 0;
            if (clanEntry.boostMeta) {
              clanEntry.boostMeta = {};
            }
          }
        }
        if (allowBoostSync && boostSignal && usedAbilities.length) {
          boostSyncTouched = true;
          const clanId = clanData.clanId != null ? String(clanData.clanId) : null;
          const rawClanName =
            clanData?.clanName ??
            clanData?.clan_name ??
            clanData?.clan ??
            clanData?.name ??
            null;
          const clanName = rawClanName || (clanId ? (clanLookup.get(clanId) || `Clan ${clanId}`) : 'Unknown Clan');
          const aliasKeys = new Set();
          if (clanId) aliasKeys.add(`id:${clanId}`);
          if (clanName) aliasKeys.add(`name:${clanName}`);
          const cleanedName = clanName ? commCleanClanDisplayName(clanName) : '';
          if (cleanedName) aliasKeys.add(`name:${cleanedName}`);
          const normalizedName = clanName ? normalizeClanName(clanName) : '';
          if (normalizedName) aliasKeys.add(`name:${normalizedName}`);
          let clanEntry = null;
          for (const key of aliasKeys) {
            if (currentRoundClanBoosts.has(key)) {
              clanEntry = currentRoundClanBoosts.get(key);
              break;
            }
          }
          if (!clanEntry) {
            const normalizedTarget = clanName ? normalizeClanName(clanName) : '';
            for (const entry of currentRoundClanBoosts.values()) {
              if (!entry) continue;
              const entryId = entry.clan_id != null ? String(entry.clan_id) : null;
              if (clanId && entryId && entryId === String(clanId)) {
                clanEntry = entry;
                break;
              }
              if (normalizedTarget) {
                const entryName = entry.name || entry.clan_name || '';
                if (normalizeClanName(entryName) === normalizedTarget) {
                  clanEntry = entry;
                  break;
                }
              }
            }
          }
          if (!clanEntry) {
            clanEntry = { name: clanName, clan_id: clanId, boosts: {}, total_boosts: 0 };
          }
          aliasKeys.forEach((key) => {
            currentRoundClanBoosts.set(key, clanEntry);
          });
          clanEntry.updatedAt = Date.now();
          clanEntry.roundKey = liveBoostRoundKey || normalizeBlockId(currentRoundInfo?.blockId) || clanEntry.roundKey || null;
          if (Number.isFinite(roundStartAt) && Array.isArray(rawUsedAbilities) && rawUsedAbilities.length) {
            rawUsedAbilities.forEach((ability) => {
              const abilityId = ability?.nftGameAbilityId;
              const boostType = ABILITY_ID_TO_TYPE[abilityId];
              if (!boostType) return;
              const timestamps = extractAbilityTimestampsMs(ability);
              if (!timestamps.length) return;
              const valid = timestamps.filter((ts) => ts >= cutoff && ts <= upper);
              if (valid.length) return;
              const metaExisting = clanEntry.boostMeta ? clanEntry.boostMeta[normalizeBoostKey(boostType)] : null;
              const metaExistingTs = normalizeTimestampMs(metaExisting?.ts);
              const metaExistingSource = metaExisting?.source || '';
              const hasAbilityUsage = metaExistingSource === 'ws:abilityUsage' && Number.isFinite(metaExistingTs);
              if (hasAbilityUsage && Number.isFinite(metaExistingTs) && metaExistingTs >= roundStartAt) return;
              if (clanEntry.boosts && clanEntry.boosts[boostType]) {
                clanEntry.boosts[boostType] = 0;
              }
              if (clanEntry.boostMeta) {
                delete clanEntry.boostMeta[normalizeBoostKey(boostType)];
              }
            });
          }
          usedAbilities.forEach(ability => {
            const abilityId = ability.nftGameAbilityId;
            const count = Number(ability.count || 0);
            const boostType = ABILITY_ID_TO_TYPE[abilityId];
            if (boostType && count > 0) {
              const existing = Number(clanEntry.boosts[boostType] || 0);
              const allowNew = !clanEntry.hasEvent || existing > 0;
              if (!allowNew) return;
              const abilityList = extractAbilityTimestampsMs(ability);
              const hasTimestamp = abilityList.length > 0;
              const maxAbilityTs = hasTimestamp ? abilityList[abilityList.length - 1] : null;
              const debugInfo = {
                source: 'ws:allUsersStateV2',
                roundStartAt,
                cutoff,
                upper,
                now,
                originalCount: Number.isFinite(ability?.__mwBoostDebug?.originalCount)
                  ? ability.__mwBoostDebug.originalCount
                  : (hasTimestamp ? abilityList.length : null),
                validCount: Number.isFinite(ability?.__mwBoostDebug?.validCount)
                  ? ability.__mwBoostDebug.validCount
                  : (hasTimestamp ? abilityList.length : null),
                reason: ability?.__mwBoostDebug?.reason || null
              };
              const metaExisting = clanEntry.boostMeta ? clanEntry.boostMeta[normalizeBoostKey(boostType)] : null;
              const metaExistingTs = normalizeTimestampMs(metaExisting?.ts);
              const metaExistingSource = metaExisting?.source || '';
              const hasAbilityUsage = metaExistingSource === 'ws:abilityUsage' && Number.isFinite(metaExistingTs);
              if (hasAbilityUsage && Number.isFinite(maxAbilityTs) && maxAbilityTs < metaExistingTs) {
                return;
              }
              if (hasAbilityUsage && !hasTimestamp) {
                return;
              }
              if (hasTimestamp && Number.isFinite(roundStartAt)) {
                const valid = abilityList.filter((ts) => ts >= cutoff && ts <= upper);
                if (!valid.length) {
                  clanEntry.boosts[boostType] = 0;
                  if (clanEntry.boostMeta) delete clanEntry.boostMeta[normalizeBoostKey(boostType)];
                  return;
                }
              }
              clanEntry.boosts[boostType] = Math.max(existing, count);
              const abilityTs = extractAbilityTimestampMs(ability);
              setClanBoostMeta(clanEntry, boostType, 'ws:allUsersStateV2', abilityTs, abilityList, debugInfo);
              notifyBoostCountIncrease(clanEntry, boostType, clanEntry.boosts[boostType], 'ws:allUsersStateV2', abilityTs);
            }
          });
          clanEntry.total_boosts = Object.values(clanEntry.boosts || {}).reduce((sum, value) => sum + (Number(value) || 0), 0);
        }
      });

      if (boostSyncTouched) {
        const aggregate = {};
        currentRoundClanBoosts.forEach((entry) => {
          if (!entry || !entry.boosts) return;
          Object.entries(entry.boosts).forEach(([type, count]) => {
            const raw = Number(count) || 0;
            if (raw <= 0) return;
            aggregate[type] = (aggregate[type] || 0) + raw;
          });
        });
        currentRoundBoosts = aggregate;
      }

      const missingRosterIds = data
        .map((clanData) => commParseNumber(clanData?.clanId ?? clanData?.clan_id))
        .filter((val) => Number.isFinite(val))
        .map((val) => String(Math.trunc(val)))
        .filter((id) => !clanLookup.has(id));
      if (missingRosterIds.length) {
        queueRosterNameLookup(missingRosterIds);
      }

      // Refresh boost display with synced counts
      if (typeof refreshBoostDisplay === 'function') {
        refreshBoostDisplay();
      }
      lastAllUsersStateV2Snapshot = buildClanSnapshotFromState(data, 10);
      if (typeof refreshCurrentRoundCard === 'function') {
        refreshCurrentRoundCard(lastCurrentRoundLivePayload || currentRoundInfo);
      }
      scheduleLiveCacheUpdate('allUsersStateV2');
      return;
    }

    const dataKey = JSON.stringify(data);
    if (dataKey === lastUpdateData) return; // Avoid unnecessary re-renders
    lastUpdateData = dataKey;

    // Try multiple possible field names for the data (preserve previous value when missing)
    const rawBlockId = data.blockId ?? data.block_id ?? data.blockNumber ?? data.block_number ?? data.roundIdValue ?? data.roundIdText;
    const rawRoundTime = data.timer ??
      data.time ??
      data.timeLeft ??
      data.timerText ??
      data.timer_text ??
      (Number.isFinite(data.timerSeconds) ? formatRoundTimerSeconds(data.timerSeconds) : null) ??
      (Number.isFinite(data.timer_seconds) ? formatRoundTimerSeconds(data.timer_seconds) : null);
    const rawMultiplier = data.multiplier ?? data.mult ?? data.multiplierValue ?? data.multiplier_value ?? data.multiplier_text;
    const leadingClan = data.leadingClan ?? data.leading_clan ?? data.clanName ?? data.clan_name ?? null;

    const normalized = commNormalizeRoundTiming(rawRoundTime, rawMultiplier);

    const fallbackTimer = (() => {
      const raw = currentRoundInfo.timer;
      if (raw == null) return null;
      return String(raw).includes(':') ? raw : null;
    })();
    const fallbackMultiplier = (() => {
      const raw = currentRoundInfo.multiplier;
      if (raw == null) return null;
      return String(raw).includes(':') ? null : raw;
    })();

    // Keep the current displayed value when the incoming payload doesn't include that field
    const blockId = rawBlockId != null ? rawBlockId : (currentRoundInfo.blockId ?? '-');
    const round_time = normalized.roundTime != null ? normalized.roundTime : (fallbackTimer ?? '-');
    const multiplier = normalized.multiplier != null ? normalized.multiplier : (fallbackMultiplier ?? '1x');

    const normalizedBlockId = normalizeBlockId(blockId);
    if (normalizedBlockId && normalizedBlockId !== liveBoostRoundKey) {
      const hadKnownRound = Boolean(liveBoostRoundKey);
      resetBoostIconsForRoundChange();
      resetBoostScoreCaches();
      liveBoostRoundKey = normalizedBlockId;
      if (hadKnownRound) {
        commMarkRoundStart(normalizedBlockId);
      } else {
        commSetCurrentRoundWithoutStartMark(normalizedBlockId);
      }
    } else if (normalizedBlockId && commRoundStartDetected.blockId == null) {
      commSetCurrentRoundWithoutStartMark(normalizedBlockId);
    }

    // Update current round info for future updates
    currentRoundInfo = { blockId, timer: round_time, multiplier };
    commCurrentRoundLastAt = Date.now();
    commPostMultiplierToTelegram();
    commPostMultiplierToDiscord();
    const simTimerValue = resolveSimRoundTimer(round_time, multiplier);
    latestRoundTimerValue = simTimerValue;
    const parsedTimer = parseRoundTimerSeconds(simTimerValue);
    if (parsedTimer != null) {
      if (Number.isFinite(lastLiveRoundTimerSeconds) && parsedTimer + 10 < lastLiveRoundTimerSeconds) {
        console.log('[MW] Round timer reset detected (fallback):', {
          prevSeconds: lastLiveRoundTimerSeconds,
          nextSeconds: parsedTimer,
          blockId: normalizedBlockId
        });
        resetBoostIconsForRoundChange();
        resetBoostScoreCaches();
        if (normalizedBlockId) {
          liveBoostRoundKey = normalizedBlockId;
        }
      }
      latestRoundTimerSeconds = parsedTimer;
      lastLiveRoundTimerSeconds = parsedTimer;
    }

    console.log('[MW] Extracted fields:', { blockId, round_time, multiplier, leadingClan });

    commHandleKorrelRoundProgress(blockId);

    lastCurrentRoundLivePayload = { blockId, timer: round_time, multiplier };
    if (LIVE_API_STATE_ENABLED && normalizedBlockId && normalizedBlockId !== liveApiLastRoundFetchId) {
      const leagueId = commResolveLiveApiLeagueId();
      if (!Number.isFinite(leagueId)) {
        console.log('[MW API] Skipping round start fetch: no leagueId yet');
      } else {
        liveApiLastRoundFetchId = normalizedBlockId;
        commRefreshLiveApiState({ force: true }).catch(() => {});
      }
    }
    refreshCurrentRoundCard(lastCurrentRoundLivePayload);
    refreshLiveMyClanPercent();
    commMaybeRingHighAlert(blockId, multiplier);

    // Update boost summary with current round info and accumulated boosts
    refreshBoostDisplay();
    scheduleLiveCacheUpdate('liveSummary');
  }

  try {
    // Fetch last 2 rounds from database
    console.log('[MW] Fetching finished rounds from database...');
    window.debugLogger?.info('App Initializing - Loading Rounds');
    const rounds = await fetchLiveRounds();
    console.log('[MW] Received rounds:', rounds);
    const recentCycleRounds = showRecentRounds ? await fetchRecentRoundsCycleApi() : null;

    if (rounds && rounds.length > 0) {
      // Set the last finished round (most recent one)
      lastFinishedRound = rounds[0];
      renderCards(lastFinishedContainer, [buildLastFinishedCard(lastFinishedRound)]);

      renderFinishedRounds(rounds.map((round) => buildRoundCard(round)));
      if (showRecentRounds) renderRecentRoundsTable(recentCycleRounds);
      window.debugLogger?.success('Initial Rounds Loaded', {
        count: rounds.length,
        latest_block: rounds[0]?.block_id
      });
    } else {
      // Show placeholder
      renderCards(lastFinishedContainer, [buildLastFinishedCard(null)]);

      renderFinishedRounds([
        {
          id: 'placeholder-1',
          title: 'No Data Yet',
          description: 'No finished rounds found in database',
          meta: 'You can still drag these test cards around!'
        },
        {
          id: 'placeholder-2',
          title: 'Test Card 2',
          description: 'Try dragging this card',
          meta: 'Drag and drop to reorder'
        }
      ]);
      if (showRecentRounds) renderRecentRoundsTable(recentCycleRounds);
    }

  const commHandleRuntimeMessage = (message) => {
    if (!message) return false;
    console.log('[MW] Side panel received message:', message.type || message.action);

    if (message.type === 'MW_UI_REFRESH') {
      try {
        commFetchGominingChatNotifications({ force: false }).catch(() => {});
      } catch (_) {}
      try {
        const helperContainer = floatingBoostHelperContainer || document.getElementById('floatingBoostHelper');
        if (helperContainer && helperContainer.style.display !== 'none') {
          refreshBoostHelperPopup();
          if (!boostHelperPopupTimer) {
            boostHelperPopupTimer = setInterval(() => refreshBoostHelperPopup(), 1000);
          }
        }
      } catch (_) {}
      try {
        if (typeof refreshCurrentRoundCard === 'function' && lastCurrentRoundLivePayload) {
          refreshCurrentRoundCard(lastCurrentRoundLivePayload);
        }
      } catch (_) {}
    }

      const isAllowedWsSource = (msg) => {
        const url = msg?.sourceUrl || msg?.data?.sourceUrl || msg?.data?.url || msg?.url || null;
        if (!url) return false;
        return String(url).startsWith('https://app.gomining.com/miner-wars/mining');
      };

      if ((message.type === 'MW_WS_EVENT' || message.action === 'wsEvent') && !isAllowedWsSource(message)) {
        if (commIsBoostDebugActive()) {
          console.log('[MW] Ignoring WS event from non-mining URL:', message?.data?.sourceUrl || message?.sourceUrl || null);
        }
        return false;
      }

      const wsPayload = (() => {
        if (message.type === 'MW_WS_EVENT') {
          return { eventName: message.eventName, eventData: message.eventData };
        }
        if (message.action === 'wsEvent' && Array.isArray(message.data?.rawEvent)) {
          return { eventName: message.data.rawEvent[0], eventData: message.data.rawEvent[1] };
        }
        return null;
      })();

      if (wsPayload) {
        const { eventName, eventData } = wsPayload;
      if (commIsBoostDebugActive()) {
        const label = String(eventName || '').toLowerCase();
        const wsArray = normalizeWsStateArray(eventData);
        console.log('[MW WS EVENT]', {
          eventName,
          size: Array.isArray(wsArray) ? wsArray.length : 0
        });
        if (label.includes('allusersstatev2') || label.includes('allusersstate')) {
            (Array.isArray(wsArray) ? wsArray : []).forEach((item) => {
              commLogBoostDebug(item, 'allUsersStateV2');
            });
          }
        }
        if (!LIVE_WS_ENABLED) {
        const eventLabel = String(eventName || '').toLowerCase();
        const isScoreEvent = eventLabel.includes('allusersstatev2')
          || eventLabel.includes('websocket_score')
          || eventLabel.includes('scorecurrent');
        const wsArray = normalizeWsStateArray(eventData);
        if (isScoreEvent && Array.isArray(wsArray) && wsArray.length > 0) {
          updateLiveWsAddedScores(wsArray);
          if (typeof window.__refreshCurrentRoundCard === 'function') {
            window.__refreshCurrentRoundCard();
            }
          }
          return false;
        }
        liveWsLastAt = Date.now();
        liveWsLastEventName = eventName || null;
        console.log('[MW] WS Event:', eventName, eventData);

        // Update current round with this data (also updates boost summary)
        if (eventData) {
          console.log('[MW] Event data type:', typeof eventData, 'Keys:', Object.keys(eventData || {}));
            updateCurrentRound(eventData);
            // Also handle ability/boost events that arrive as WS events
            try {
              if (typeof eventName === 'string' && eventName.toLowerCase().includes('ability')) {
                handleBoostUsage(eventData);
              }
            } catch (err) {
              console.error('[MW] Failed to process WS ability event:', err);
            }

            // Upsert clan mapping from winner event payloads
            try {
              const nameLower = typeof eventName === 'string' ? eventName.toLowerCase() : '';
              const isWinnerEvent = nameLower.includes('winner') || eventData?.winner === true || eventData?.winner === 'true';
              if (isWinnerEvent && eventData) {
                lastWinnerSnapshot = {
                  round_id: normalizeBlockId(eventData.roundId ?? eventData.blockId ?? eventData.round_id ?? null),
                  ts: Date.now(),
                  winner_player_name: eventData.userAlias ?? eventData.userName ?? eventData.username ?? eventData.playerName ?? null,
                  winner_reward_gmt: eventData.reward ?? eventData.rewardGmt ?? eventData.rewardGMT ?? eventData.playerReward ?? null,
                  player_th_per_s: eventData.userPower ?? eventData.power ?? eventData.hashrate ?? null,
                  player_percentage: eventData.userChanceToMine ?? eventData.chanceToMine ?? eventData.share ?? null,
                  clan_id: eventData.clanId ?? eventData.clan_id ?? null,
                  clan_name: eventData.clanName ?? eventData.clan_name ?? null
                };
              }
              if (nameLower.includes('winner')) {
                const clanId = eventData?.clanId ?? eventData?.clan_id ?? null;
                const clanName = eventData?.clanName ?? eventData?.clan_name ?? null;
                if (clanId && clanName) {
                  upsertClanMatch(clanId, clanName).then(() => {
                    clanLookup.set(String(clanId), clanName);
                    console.log('[MW] Upserted clan mapping from winner event:', clanId, clanName);
                  }).catch(err => {
                    console.warn('[MW] Failed to upsert clan mapping from winner event:', err);
                  });
                } else if (nameLower.includes('winner')) {
                  console.log('[MW] Winner event missing clan data:', { clanId, clanName, eventName });
                }
              } else if (eventData?.clanId && eventData?.clanName) {
                const clanId = eventData.clanId;
                const clanName = eventData.clanName;
                upsertClanMatch(clanId, clanName).then(() => {
                  clanLookup.set(String(clanId), clanName);
                  console.log('[MW] Upserted clan mapping from WS event:', clanId, clanName, eventName);
                }).catch(err => {
                  console.warn('[MW] Failed to upsert clan mapping from WS event:', err);
                });
              }
            } catch (err) {
              console.warn('[MW] Failed to process winner event mapping:', err);
            }
        }
      }

      if (message.type === 'MW_ROUND_EVENT') {
        const { payload } = message;
        console.log('[MW] ===== ROUND EVENT DETECTED =====');
        console.log('[MW] Round event type:', payload.name);
        console.log('[MW] Round event data:', payload.data);
        try {
          const eventId = payload?.data?.block_id ?? payload?.data?.blockId ?? payload?.data?.roundId ?? null;
          if (eventId != null) {
            const eventKey = `${payload.name}:${eventId}`;
            const now = Date.now();
            const lastSeen = recentRoundEvents.get(eventKey);
            if (lastSeen && now - lastSeen < 30000) {
              console.log('[MW] Skipping duplicate round event:', eventKey);
              return false;
            }
            recentRoundEvents.set(eventKey, now);
          }
        } catch (err) {
          console.warn('[MW] Failed to dedupe round event:', err);
        }
        window.debugLogger?.info('Round Event Received', {
          event_name: payload.name,
          has_data: !!payload.data
        });

        // Handle both roundEnded and roundClosed as round finished events
        if (payload.name === 'roundEnded' || payload.name === 'roundClosed') {
          console.log('[MW] ===== ROUND FINISHED EVENT TRIGGERED =====');
          console.log('[MW] Event name:', payload.name);
          console.log('[MW] Payload data:', payload.data);
          console.log('[MW] Payload data keys:', payload.data ? Object.keys(payload.data) : 'null');
          console.log('[MW] Current lastUpdateData:', lastUpdateData);
          window.debugLogger?.info('Round Ended Event Received', {
            block_id: payload.data?.block_id ?? payload.data?.blockId,
            clan: payload.data?.clan_name ?? payload.data?.clanName,
            has_lastUpdateData: !!lastUpdateData
          });

          // Wait for the round finished popup to be fully loaded (continue button appears)
          // Then extract complete data and save to database
          (async () => {
            try {
              console.log('[MW] Starting round finished processing...');
              window.debugLogger?.info('Starting Round Processing', {
                block_id: payload.data?.block_id ?? payload.data?.blockId
              });
              const popupBlockId = extractBlockIdFromPopupHtml(payload?.data?.popupHtml);
              const stableBlockId = normalizeBlockId(lastLiveSummaryBlockId);
              lockedFinishedBlockId = popupBlockId || stableBlockId || null;
              isSavingFinishedRound = true;
              if (stableBlockId) {
                console.log('[MW] Captured stable block_id for finished round:', stableBlockId);
              }
              if (popupBlockId) {
                console.log('[MW] Captured block_id from popup HTML:', popupBlockId);
              }

              // Wait for the continue button to appear (max 10 seconds)
              console.log('[MW] Waiting for round finished popup to complete...');
              const panel = await waitForRoundFinishedComplete(document, 10000);

              if (!panel) {
                console.warn('[MW] Round finished popup did not complete in time, using WebSocket data only');
                window.debugLogger?.warning('Popup Not Found', {
                  message: 'Using WebSocket data only'
                });
              } else {
                console.log('[MW] Round finished popup complete, extracting data...');
                window.debugLogger?.info('Popup Found', {
                  message: 'Extracting complete data from popup'
                });
                // Extract complete data from the popup
                const popupData = extractRoundFinishedData(document);
                if (popupData) {
                  console.log('[MW] Extracted popup data:', popupData);
                  window.debugLogger?.info('Popup Data Extracted', {
                    block_id: popupData.block_id,
                    clan: popupData.clan_name
                  });
                  // Merge popup data with WebSocket data (popup data takes priority)
                  Object.assign(payload.data, popupData);
                } else {
                  console.warn('[MW] Failed to extract popup data');
                  window.debugLogger?.warning('Popup Extraction Failed', {
                    message: 'Could not extract data from popup'
                  });
                }
              }

              // Now save the complete data
              const data = payload.data || {};
              const popupHtmlData = parsePopupHtmlRoundData(data.popupHtml);
              if (popupHtmlData) {
                console.log('[MW] Extracted popup HTML data:', popupHtmlData);
                Object.assign(data, popupHtmlData);
              }

              // Always prefer the six-digit block id from popup/live summary (never use roundId)
              if (lockedFinishedBlockId) {
                data.block_id = lockedFinishedBlockId;
                console.log('[MW] Using locked block_id for finished round:', data.block_id);
              }

              console.log('[MW] Final data for insertion:', data);

            // Ensure we have the required fields
            if (data.block_id || data.blockId) {
              // Normalize the data structure and add current round boosts
              const clanObj = (data.clan && typeof data.clan === 'object') ? data.clan : null;
              let clanName = data.clan_name ?? data.clanName ?? data.winningClan ?? clanObj?.clan_name ?? clanObj?.clanName ?? clanObj?.name ?? null;
              let clanId = data.clan_id ?? data.clanId ?? data.winner_clan_id ?? clanObj?.clan_id ?? clanObj?.clanId ?? clanObj?.id ?? null;
              const winnerFallbackRaw = lastWinnerSnapshot || {};
              const nowTs = Date.now();
              const fallbackFresh = winnerFallbackRaw.ts && (nowTs - winnerFallbackRaw.ts < 120000);
              const fallbackMatchesBlock = winnerFallbackRaw.round_id && lockedFinishedBlockId
                ? String(winnerFallbackRaw.round_id) === String(lockedFinishedBlockId)
                : false;
              const fallbackUnused = winnerFallbackRaw.used_for !== String(lockedFinishedBlockId || '');
              const winnerFallback = (fallbackFresh && fallbackUnused && (fallbackMatchesBlock || !winnerFallbackRaw.round_id))
                ? winnerFallbackRaw
                : {};
              const shouldUseWinnerFallback = winnerFallback.clan_name
                && (!clanName || normalizeClanName(clanName) !== normalizeClanName(winnerFallback.clan_name));
              if (shouldUseWinnerFallback) {
                clanName = winnerFallback.clan_name;
                if (!clanId && winnerFallback.clan_id) {
                  clanId = winnerFallback.clan_id;
                }
                winnerFallbackRaw.used_for = String(lockedFinishedBlockId || '');
              }
              try {
                commPostWinnerToTelegram({
                  ...data,
                  clan_name: clanName,
                  clan_id: clanId
                });
                commPostWinnerToDiscord({
                  ...data,
                  clan_name: clanName,
                  clan_id: clanId
                });
              } catch (err) {
                console.warn('[MW] Winner hook failed:', err?.message || err);
              }

              // If clan_id is not in the event data, try to find it by name
              if (!clanId && clanName) {
                const lookupFn =
                  typeof findClanIdByName === 'function'
                    ? findClanIdByName
                    : (typeof window !== 'undefined' && typeof window.findClanIdByName === 'function'
                      ? window.findClanIdByName
                      : null);
                if (lookupFn) {
                  clanId = lookupFn(clanName);
                  if (clanId) {
                    console.log('[MW] Found clan_id by name lookup:', clanId, 'for', clanName);
                  } else {
                    console.warn('[MW] Could not find clan_id for clan:', clanName);
                  }
                } else {
                  console.warn('[MW] Clan name lookup unavailable for clan:', clanName);
                }
              }
              if (!clanName && clanId) {
                clanName = clanLookup.get(String(clanId)) || null;
                if (clanName) {
                  console.log('[MW] Found clan_name by clanLookup:', clanId, clanName);
                }
              }
              // Always persist clan_id <-> clan_name mapping at round end when both are known
              if (clanId && clanName) {
                upsertClanMatch(clanId, clanName).then(() => {
                  clanLookup.set(String(clanId), clanName);
                  console.log('[MW] Upserted clan mapping from round end:', clanId, clanName);
                }).catch(err => {
                  console.warn('[MW] Failed to upsert clan mapping from round end:', err);
                });
              }

              // Parse leading players/clans snapshots to get TH/s and percentage data
              let leadingPlayers = [];
              let leadingClans = [];

              try {
                const playersSnap = data.leading_players_snapshot ?? data.leadingPlayersSnapshot;
                if (playersSnap) {
                  leadingPlayers = typeof playersSnap === 'string' ? JSON.parse(playersSnap) : playersSnap;
                }
              } catch (e) {
                console.warn('[MW] Failed to parse leading_players_snapshot:', e);
              }

              try {
                const clansSnap = data.leading_clans_snapshot ?? data.leadingClansSnapshot;
                if (clansSnap) {
                  leadingClans = typeof clansSnap === 'string' ? JSON.parse(clansSnap) : clansSnap;
                }
              } catch (e) {
                console.warn('[MW] Failed to parse leading_clans_snapshot:', e);
              }

              if (!clanName && clanId) {
                const match = Array.isArray(leadingClans)
                  ? leadingClans.find(c => String(c.clan_id ?? c.clanId ?? c.id) === String(clanId))
                  : null;
                clanName = match?.clan_name ?? match?.clanName ?? match?.name ?? clanLookup.get(String(clanId)) ?? null;
              }

              // Create lookup maps for player/clan stats
              const playerStatsMap = new Map();
              if (Array.isArray(leadingPlayers)) {
                leadingPlayers.forEach(p => {
                  const name = p.name || p.player_name || p.playerName;
                  if (name) {
                    playerStatsMap.set(name, {
                      th_per_s: p.th_per_s || p.thPerS || p.hashrate || null,
                      percentage: p.percentage || p.share || p.pct || null
                    });
                  }
                });
              }

              const clanStatsMap = new Map();
              if (Array.isArray(leadingClans)) {
                leadingClans.forEach(c => {
                  const name = c.name || c.clan_name || c.clanName;
                  if (name) {
                    clanStatsMap.set(name, {
                      th_per_s: c.th_per_s || c.thPerS || c.hashrate || null,
                      percentage: c.percentage || c.share || c.pct || null
                    });
                  }
                });
              }

              // Build top 10 players with boosts and stats
              let top10PlayersWithBoosts = Object.entries(playerBoosts)
                .map(([name, boosts]) => {
                  const displayName = name === '__local__' ? 'You' : name;
                  const stats = playerStatsMap.get(displayName) || playerStatsMap.get(name) || {};
                  const totalBoosts = Object.values(boosts).reduce((sum, v) => sum + v, 0);

                  return {
                    name: displayName,
                    th_per_s: stats.th_per_s || null,
                    percentage: stats.percentage || null,
                    boosts: boosts,
                    total_boosts: totalBoosts
                  };
                })
                .sort((a, b) => b.total_boosts - a.total_boosts)
                .slice(0, 10);

              if (top10PlayersWithBoosts.length === 0 && lastAllPlayersStateSnapshot.length > 0) {
                top10PlayersWithBoosts = buildTopPlayersWithBoostsFromSnapshot(lastAllPlayersStateSnapshot, 10);
              }

              // Build top 10 clans with boosts and stats (prefer allUsersStateV2 usedAbilities)
              let top10ClansWithBoosts = [];
              if (currentRoundClanBoosts && currentRoundClanBoosts.size > 0) {
                top10ClansWithBoosts = Array.from(currentRoundClanBoosts.values())
                  .map((entry) => {
                    const stats = clanStatsMap.get(entry.name) || {};
                    return {
                      name: entry.name,
                      clan_id: entry.clan_id,
                      th_per_s: stats.th_per_s || null,
                      percentage: stats.percentage || null,
                      boosts: entry.boosts,
                      total_boosts: entry.total_boosts
                    };
                  })
                  .sort((a, b) => b.total_boosts - a.total_boosts)
                  .slice(0, 10);
              } else {
                const clanBoostMap = new Map();
                Object.entries(playerBoosts).forEach(([playerName, boosts]) => {
                  const meta = playerMeta[playerName] || {};
                  const cId = meta.clanId || null;
                  const cName = cId ? (clanLookup.get(cId) || `Clan ${cId}`) : (meta.clanName || 'Unknown Clan');
                  const key = cId ? `id:${cId}` : `name:${cName}`;

                  if (!clanBoostMap.has(key)) {
                    const stats = clanStatsMap.get(cName) || {};
                    clanBoostMap.set(key, {
                      name: cName,
                      clan_id: cId,
                      th_per_s: stats.th_per_s || null,
                      percentage: stats.percentage || null,
                      boosts: {},
                      total_boosts: 0
                    });
                  }
                  const entry = clanBoostMap.get(key);
                  Object.entries(boosts).forEach(([type, count]) => {
                    entry.boosts[type] = (entry.boosts[type] || 0) + count;
                    entry.total_boosts += count;
                  });
                });

                top10ClansWithBoosts = Array.from(clanBoostMap.values())
                  .sort((a, b) => b.total_boosts - a.total_boosts)
                  .slice(0, 10);

                if (top10ClansWithBoosts.length === 0 && Array.isArray(lastAllUsersStateV2Raw)) {
                  const fallbackClans = buildTopClansWithBoostsFromState(lastAllUsersStateV2Raw, 10);
                  if (fallbackClans.length > 0) {
                    top10ClansWithBoosts = fallbackClans;
                  }
                }
              }

              // Collect all unique clan ID/name mappings to upsert to clan_matchID table
              const clanMappingsToUpsert = new Map();

              // Add winning clan
              if (clanId && clanName) {
                clanMappingsToUpsert.set(String(clanId), clanName);
              }

              // Add clans from top 10 with boosts
              top10ClansWithBoosts.forEach(clan => {
                if (clan.clan_id && clan.name) {
                  clanMappingsToUpsert.set(String(clan.clan_id), clan.name);
                }
              });

              // Add clans from leading clans snapshot
              if (Array.isArray(leadingClans)) {
                leadingClans.forEach(clan => {
                  const cId = clan.clan_id || clan.clanId || clan.id;
                  const cName = clan.name || clan.clan_name || clan.clanName;
                  if (cId && cName) {
                    clanMappingsToUpsert.set(String(cId), cName);
                  }
                });
              }

              // Upsert all unique clan mappings to database (fire-and-forget)
              if (clanMappingsToUpsert.size > 0) {
                console.log('[MW] Upserting', clanMappingsToUpsert.size, 'clan mappings to database');
                clanMappingsToUpsert.forEach((name, id) => {
                  upsertClanMatch(id, name).then(() => {
                    clanLookup.set(id, name);
                    console.log('[MW] Upserted clan mapping:', id, '->', name);
                  }).catch(err => {
                    console.warn('[MW] Failed to upsert clan mapping:', id, name, err);
                  });
                });
              }

              const cycleValue = data.cycle ?? getCycleNumber(data.time_round_finished ?? data.timestamp ?? null);
              const snapshotFromState = (lastAllUsersStateV2Snapshot && lastAllUsersStateV2Snapshot.length)
                ? JSON.stringify(lastAllUsersStateV2Snapshot)
                : null;
              const playersSnapshotFromState = (lastAllPlayersStateSnapshot && lastAllPlayersStateSnapshot.length)
                ? JSON.stringify(lastAllPlayersStateSnapshot)
                : null;
              const roundData = {
                block_id: data.block_id ?? data.blockId,
                time_round_finished: data.time_round_finished ?? data.timestamp ?? new Date().toISOString(),
                league: data.league ?? lastLiveSummaryLeague ?? null,
                cycle: cycleValue ?? null,
                round_time: data.round_time ?? data.roundTime ?? data.timer ?? lastLiveSummaryTimer ?? null,
                multiplier: data.multiplier ?? data.mult ?? lastLiveSummaryMultiplier ?? null,
                clan_name: clanName,
                clan_id: clanId,
                clan_th_per_s: data.clan_th_per_s ?? data.clanThPerS ?? data.winnerTh ?? null,
                clan_percentage: data.clan_percentage ?? data.clanPercentage ?? data.winnerPct ?? null,
                clan_boosts: data.clan_boosts ?? data.clanBoosts ?? (Object.keys(currentRoundBoosts).length > 0 ? JSON.stringify(currentRoundBoosts) : null),
                winner_player_name: data.winner_player_name ?? data.winnerPlayerName ?? data.winningPlayer ?? winnerFallback.winner_player_name ?? null,
                player_th_per_s: data.player_th_per_s ?? data.playerThPerS ?? winnerFallback.player_th_per_s ?? null,
                player_minebox: data.player_minebox ?? data.playerMinebox ?? null,
                player_percentage: data.player_percentage ?? data.playerPercentage ?? winnerFallback.player_percentage ?? null,
                player_boosts: data.player_boosts ?? data.playerBoosts ?? (Object.keys(playerBoosts).length > 0 ? JSON.stringify(playerBoosts) : null),
                send_reward_fund: data.send_reward_fund ?? data.rewardFund ?? null,
                leading_clans_snapshot: data.leading_clans_snapshot ?? data.leadingClansSnapshot ?? snapshotFromState ?? null,
                leading_players_snapshot: data.leading_players_snapshot ?? data.leadingPlayersSnapshot ?? playersSnapshotFromState ?? null,
                winner_reward_gmt: data.winner_reward_gmt ?? data.winnerRewardGmt ?? data.playerReward ?? winnerFallback.winner_reward_gmt ?? null,
                top_10_players_with_boosts: top10PlayersWithBoosts.length > 0 ? JSON.stringify(top10PlayersWithBoosts) : null,
                top_10_clans_with_boosts: top10ClansWithBoosts.length > 0 ? JSON.stringify(top10ClansWithBoosts) : null,
                data_complete: roundTrackingComplete // True if we tracked from roundOpened to roundEnded
              };

              try {
                const finishedId = normalizeBlockId(roundData.block_id);
                const winnerClanLabel = roundData.clan_name || winnerFallback.clan_name || 'Unknown';
                const winnerUserLabel = roundData.winner_player_name || winnerFallback.winner_player_name || 'Unknown';
                const parts = [];
                if (finishedId) parts.push(`Round ${finishedId} ended`);
                if (winnerClanLabel) parts.push(`Winner clan: ${winnerClanLabel}`);
                if (winnerUserLabel) parts.push(`Winner user: ${winnerUserLabel}`);
                const text = parts.length ? parts.join('  -  ') : 'Round ended';
                commPushGominingChatMessage({
                  id: finishedId ? `round-ended:${finishedId}` : `round-ended:${Date.now()}`,
                  sender: 'Round finished',
                  text,
                  kind: 'round'
                });
              } catch (err) {
                console.warn('[MW] Failed to push round finished notification', err);
              }

              console.log('[MW] Calling insertFinishedRound with data:', roundData);
              window.debugLogger?.info('Attempting Database Insert', {
                block_id: roundData.block_id,
                clan_name: roundData.clan_name,
                winner: roundData.winner_player_name,
                data_complete: roundData.data_complete
              });

              // Insert into database (fire-and-forget)
              insertFinishedRound(roundData).then((result) => {
                console.log('[MW] Successfully inserted finished round:', result);
                window.debugLogger?.success('Round Data Saved to Database', {
                  block_id: roundData.block_id,
                  clan: roundData.clan_name,
                  data_complete: roundData.data_complete
                });
              }).catch((err) => {
                console.error('[MW] Failed to insert finished round:', err);
                console.error('[MW] Error details:', err.stack);
                window.debugLogger?.error('Database Insert Failed', {
                  block_id: roundData.block_id,
                  error: err.message,
                  stack: err.stack
                });
              });
            } else {
              console.warn('[MW] Missing required block_id, cannot insert finished round');
              window.debugLogger?.warning('Missing Block ID', {
                message: 'Cannot insert round without block_id',
                data: data
              });
            }

            // Reset boosts for the round
            resetRoundBoosts();

            // Wait a moment for data to be saved to database
            setTimeout(async () => {
              // Refresh the finished rounds from database
              try {
                window.debugLogger?.info('Refreshing Rounds from Database');
                const updatedRounds = await fetchLiveRounds();
                const recentCycleRounds = showRecentRounds ? await fetchRecentRoundsCycleApi() : null;
                if (updatedRounds && updatedRounds.length > 0) {
                  // Update last finished round
                  lastFinishedRound = updatedRounds[0];
                  renderCards(lastFinishedContainer, [buildLastFinishedCard(lastFinishedRound)]);

                  renderFinishedRounds(updatedRounds.map((round) => buildRoundCard(round)));
                  if (showRecentRounds) renderRecentRoundsTable(recentCycleRounds);
                  window.debugLogger?.success('UI Updated with Latest Rounds', {
                    count: updatedRounds.length,
                    latest_block: updatedRounds[0]?.block_id
                  });
                }
              } catch (err) {
                console.error('[MW] Failed to refresh rounds:', err);
                window.debugLogger?.error('Failed to Refresh Rounds', {
                  error: err.message
                });
              }

              // Reset current round to waiting state
              lastUpdateData = null;
              renderCards(currentRoundContainer, [{
                id: 'current-waiting',
                title: 'New Round Starting',
                description: 'Waiting for live data...',
                meta: 'Starting soon...'
              }]);
              isSavingFinishedRound = false;
              lockedFinishedBlockId = null;
            }, 2000); // Wait 2 seconds for data to be saved

          } catch (err) {
            console.error('[MW] Error processing roundEnded event:', err);
            window.debugLogger?.error('Round Processing Error', {
              error: err.message,
              stack: err.stack
            });
            isSavingFinishedRound = false;
            lockedFinishedBlockId = null;
          }
          })(); // End of async IIFE
        }

        if (payload.name === 'roundOpened' && payload.data) {
          console.log('[MW] New round opened, updating current round');
          const newBlockId = payload.data?.block_id ?? payload.data?.blockId;
          const leagueId = payload.data?.leagueId ?? payload.data?.league_id ?? lastLiveSummaryLeague;
          const stableId = normalizeBlockId(newBlockId);
          if (stableId) {
            if (lastLiveSummaryBlockId && lastLiveSummaryBlockId !== stableId) {
              lastLiveSummaryTimer = null;
              lastLiveSummaryMultiplier = null;
              lastLiveSummaryMultiplierBlockId = null;
            }
            lastLiveSummaryBlockId = stableId;
          }
          const leagueParsed = commParseNumber(leagueId);
          if (Number.isFinite(leagueParsed)) {
            commKorrelCurrentLeagueId = Math.trunc(leagueParsed);
          }

          window.debugLogger?.info('New Round Opened', {
            block_id: newBlockId,
            league_id: leagueId
          });

          try {
            const openedId = normalizeBlockId(newBlockId);
            const text = openedId ? `Round ${openedId} started` : 'New round started';
            commPushGominingChatMessage({
              id: openedId ? `round-started:${openedId}` : `round-started:${Date.now()}`,
              sender: 'Round started',
              text,
              kind: 'round'
            });
          } catch (err) {
            console.warn('[MW] Failed to push round started notification', err);
          }

          resetRoundBoosts(true); // Reset boosts for new round and mark tracking as complete
          updateCurrentRound(payload.data);

          // Reset baseline tracking for new round
          baselineFetchAttempted = false;
          baselineFetchLastAttemptTime = 0;
          baselineNoParticipantsUntil = 0;

          // Fetch Round State baseline once at round start
          if (newBlockId) {
            ensureRoundStateBaseline(newBlockId, leagueId).catch(err => {
              console.error('[MW] Error ensuring Round State baseline:', err);
            });
          }
        }
      }

      if (message.type === 'MW_LIVE_SCORE_SNAPSHOT' || message.action === 'liveScoreSnapshot') {
        commUpdateLiveXpathScore(message.data || message.payload || message);
        if (typeof window.__refreshCurrentRoundCard === 'function') {
          window.__refreshCurrentRoundCard();
        }
      }

      // Handle live summary (DOM-scraped current round data)
      const liveSummaryData = message.type === 'MW_LIVE_SUMMARY'
        ? message.data
        : (message.action === 'liveSummary' ? message.data : null);
      if (liveSummaryData) {
        console.log('[MW] Live summary received:', liveSummaryData);
        liveSummaryLastAt = Date.now();
        const data = liveSummaryData;
        if (data) {
          const normalizeSummaryTimer = (value) => {
            if (value == null) return null;
            if (Number.isFinite(value)) return formatRoundTimerSeconds(value);
            const raw = String(value).trim();
            if (!raw) return null;
            if (raw.includes(':')) return raw;
            const numeric = Number(raw);
            return Number.isFinite(numeric) ? formatRoundTimerSeconds(numeric) : raw;
          };
          const normalizeSummaryMultiplier = (value) => {
            if (value == null) return null;
            const raw = String(value).trim();
            if (!raw || raw.includes(':')) return null;
            return raw;
          };
          const stableIdRaw = normalizeBlockId(data.blockId ?? data.block_id ?? data.roundIdValue ?? data.roundIdText);
          const stableId = commIsTrustedBlockId(stableIdRaw) ? stableIdRaw : null;
          if (stableId) {
            if (!isSavingFinishedRound) {
              if (lastLiveSummaryBlockId && lastLiveSummaryBlockId !== stableId) {
                lastLiveSummaryTimer = null;
                lastLiveSummaryMultiplier = null;
                lastLiveSummaryMultiplierBlockId = null;
              }
              lastLiveSummaryBlockId = stableId;
            }

            // Fetch Round State baseline if we don't have one yet
            if ((!roundStateBaseline || roundStateBaseline.size === 0) && stableId && !baselineFetchAttempted) {
              const leagueValue = data.leagueId ?? data.league ?? lastLiveSummaryLeague;
              const leagueParsed = commParseNumber(leagueValue);
              const leagueResolved = Number.isFinite(leagueParsed)
                ? leagueParsed
                : commParseLeagueLabel(leagueValue ?? lastLiveSummaryLeague);
              const leagueId = Number.isFinite(leagueResolved) ? Math.trunc(leagueResolved) : null;

              console.log('[MW] Live summary has blockId:', stableId, 'leagueId:', leagueId, '- fetching baseline');
              ensureRoundStateBaseline(stableId, leagueId).catch(err => {
                console.error('[MW] Error ensuring Round State baseline from liveSummary:', err);
              });
            }
          }
            if (!isSavingFinishedRound) {
              const summaryTimer = normalizeSummaryTimer(
                data.timer ??
                data.timerText ??
                data.timer_text ??
                data.round_time ??
                data.roundTime ??
                data.roundTimeLabel ??
                data.round_time_text ??
                data.timerSeconds ??
                data.timer_seconds ??
                data.roundTimeSeconds ??
                data.round_time_seconds ??
                null
              );
              const summaryMultiplier = normalizeSummaryMultiplier(
                data.multiplier ??
                data.multiplierText ??
                data.multiplier_text ??
                data.mult ??
                data.mult_value ??
                data.multiplierValue ??
                data.multiplier_value ??
                null
              );
              if (summaryTimer) lastLiveSummaryTimer = summaryTimer;
              if (summaryMultiplier && stableId) {
                lastLiveSummaryMultiplier = summaryMultiplier;
                lastLiveSummaryMultiplierBlockId = stableId;
              }
              const leagueValue = data.leagueId ?? data.league;
              if (leagueValue != null) {
                lastLiveSummaryLeague = leagueValue;
              }
              const leagueParsed = commParseNumber(leagueValue ?? lastLiveSummaryLeague);
              const leagueResolved = Number.isFinite(leagueParsed)
                ? leagueParsed
                : commParseLeagueLabel(leagueValue ?? lastLiveSummaryLeague);
              if (Number.isFinite(leagueResolved)) {
                commKorrelCurrentLeagueId = Math.trunc(leagueResolved);
              }
            }
          // If the live summary contains a winning clan + id, persist it
          try {
            const clanId = data.clan_id ?? data.clanId ?? data.winner_clan_id ?? null;
            const clanName = data.clan_name ?? data.clanName ?? data.winningClan ?? null;
            if (clanId && clanName) {
              upsertClanMatch(clanId, clanName).then(() => {
                clanLookup.set(String(clanId), clanName);
                console.log('[MW] Upserted clan match from live summary:', clanId, clanName);
              }).catch((err) => {
                console.warn('[MW] Failed to upsert clan match from live summary:', err);
              });
            }
          } catch (err) {
            console.warn('[MW] Error checking live summary for clan match', err);
          }

          updateCurrentRound(data); // Also updates boost summary

          // Ensure baseline is fetched if we have a blockId (handles mid-round extension load)
          if (stableId && !isSavingFinishedRound) {
            ensureRoundStateBaseline(stableId, commKorrelCurrentLeagueId).catch(err => {
              console.warn('[MW] Failed to ensure baseline from live summary:', err);
            });
          }
        }
      }

      if (message.type === 'MW_CLAN_ROSTER') {
        const roster = message.payload;
        if (Array.isArray(roster)) {
          roster.forEach((entry) => {
            const clanId = entry?.clan_id ?? entry?.clanId ?? entry?.id ?? null;
            const clanName = entry?.clan_name ?? entry?.clanName ?? entry?.name ?? null;
            processClanMatchMapping(clanId, clanName, 'roster');
          });
        }
      }

      if (message.type === 'MW_LIVE_API_ALARM' || message.type === 'MW_LIVE_API_ALARM_SCHEDULED') {
        return false;
      }

      if (message.type === 'MW_API_CALL') {
        const details = message.payload || {};
        const endpoint = details.url ? String(details.url).split('?')[0] : '-';
        const method = details.method || 'GET';

        // Track API call frequency
        trackGameApiCall(endpoint, method);

        window.debugLogger?.info('Round API request', {
          endpoint,
          method,
          limit: details.limit ?? null,
          skip: details.skip ?? null,
          count: details.count ?? null,
          cycleId: details.cycleId ?? null,
          leagueId: details.leagueId ?? null,
          multipliers: Array.isArray(details.multipliers) ? details.multipliers.join(',') : null,
          source: details.source || null
        });
      }

      if (message.type === 'MW_WS_DEBUG') {
        lastWsDebugStatus = message;
        const eventName = String(message.eventName || '');
        const shouldLog =
          commIsBoostDebugActive()
          || eventName.startsWith('pageHook:')
          || eventName.startsWith('content')
          || eventName === 'wsRaw'
          || eventName === 'wsMeta'
          || eventName === 'wsMini'
          || eventName === 'wsUrl'
          || eventName.toLowerCase().includes('allusersstate')
          || message.forwarded === true;
        if (shouldLog) {
          console.log(`[MW WS DEBUG] ${eventName || 'unknown'}`, message.payload || {});
        }
        return false;
      }

      if (message.type === 'MW_CLAN_MATCH') {
        const clanId = message.payload?.clanId ?? null;
        const clanName = message.payload?.clanName ?? null;
        processClanMatchMapping(clanId, clanName, message.payload?.source || 'clan-match');
      }

      // Handle ability/boost usage events
      if (message.type === 'MW_ABILITY_USAGE' || message.type === 'MW_BOOST_USAGE') {
        const { payload } = message;
        console.log('[MW] Boost/Ability event received:', payload);
        if (commIsBoostDebugActive()) {
          console.log('[MW BOOST EVENT]', payload);
        }
        if (payload) {
          window.debugLogger?.info('Boost/Ability Used', {
            type: payload.ability_type ?? payload.type ?? 'unknown',
            player: payload.player_name ?? 'unknown',
            clan: payload.clan_name ?? 'unknown'
          });
          handleBoostUsage(payload);
        }
      }

      // Legacy support
      if (message.type === 'MW_LIVE_DATA') {
        updateCurrentRound(message.payload);
      }

      if (message.type === 'MW_ROUND_FINISHED') {
        // Handle legacy round finished event
        setTimeout(async () => {
          try {
            const updatedRounds = await fetchLiveRounds();
            const recentCycleRounds = showRecentRounds ? await fetchRecentRoundsCycleApi() : null;
            if (updatedRounds && updatedRounds.length > 0) {
              // Update last finished round
              lastFinishedRound = updatedRounds[0];
              renderCards(lastFinishedContainer, [buildLastFinishedCard(lastFinishedRound)]);

              renderFinishedRounds(updatedRounds.map((round) => buildRoundCard(round)));
              if (showRecentRounds) renderRecentRoundsTable(recentCycleRounds);
            }
          } catch (err) {
            console.error('[MW] Failed to refresh rounds:', err);
          }
        }, 2000);
      }

      return false;
    };

    // Setup live hooks for real-time updates from chrome.runtime messages
    console.log('[MW] Setting up message listener for WebSocket events...');
    if (HAS_CHROME_RUNTIME) {
      // Listen for messages from background script
      chrome.runtime.onMessage.addListener((message) => commHandleRuntimeMessage(message));

      // Also connect via port so background can push ws events directly
      try {
        const popupPort = chrome.runtime.connect({ name: 'popup' });
        popupPort.onMessage.addListener((msg) => {
          if (!msg) return;
          if (msg.action === 'wsDebug' && msg.data) {
            commHandleRuntimeMessage(msg.data);
            return;
          }
          if (msg.action === 'wsEvent' && msg.data) {
            commHandleRuntimeMessage({ action: 'wsEvent', data: msg.data });
            return;
          }
          if (msg.action === 'liveSummary' && msg.data) {
            commHandleRuntimeMessage({ action: 'liveSummary', data: msg.data });
            return;
          }
          if (msg.action === 'rosterUpdate' && msg.data) {
            commHandleRuntimeMessage({ action: 'rosterUpdate', data: msg.data });
            return;
          }
          if (msg.action === 'tokenExpired' && msg.data) {
            commHandleRuntimeMessage({ action: 'tokenExpired', data: msg.data });
            return;
          }
          commHandleRuntimeMessage(msg);
        });
      } catch (err) {
        console.warn('[MW] Failed to connect popup port', err);
      }
    }

    if (HAS_CHROME_RUNTIME && chrome?.runtime?.sendMessage) {
      try {
        chrome.runtime.sendMessage({ type: 'MW_ENSURE_HOOK' }).catch(() => {});
      } catch (err) {
        console.warn('[MW] Failed to request hook ensure', err);
      }
    }

    // Also check for any stored events when side panel opens
    chrome.storage.local.get(['lastWsEvent', 'lastRoundEvent', 'lastLiveSummary', 'mw_pending_clan_match', 'lastClanMatch'], (result) => {
      console.log('[MW] Checking stored events:', result);

      // Prefer live summary as it's most reliable
      if (result.lastLiveSummary && result.lastLiveSummary.data) {
        console.log('[MW] Found stored live summary, updating display');
        const storedIdRaw = normalizeBlockId(result.lastLiveSummary.data.blockId ?? result.lastLiveSummary.data.block_id);
        const storedId = commIsTrustedBlockId(storedIdRaw) ? storedIdRaw : null;
        if (storedId) {
          lastLiveSummaryBlockId = storedId;
        } else {
          lastLiveSummaryMultiplier = null;
          lastLiveSummaryMultiplierBlockId = null;
        }
        if (result.lastLiveSummary.data.timer) {
          lastLiveSummaryTimer = result.lastLiveSummary.data.timer;
        }
        if (storedId && result.lastLiveSummary.data.multiplier) {
          lastLiveSummaryMultiplier = result.lastLiveSummary.data.multiplier;
          lastLiveSummaryMultiplierBlockId = storedId;
        } else if (!storedId) {
          lastLiveSummaryMultiplier = null;
          lastLiveSummaryMultiplierBlockId = null;
        }
        const storedLeagueValue = result.lastLiveSummary.data.leagueId ?? result.lastLiveSummary.data.league;
        if (storedLeagueValue != null) {
          lastLiveSummaryLeague = storedLeagueValue;
          const leagueParsed = commParseNumber(storedLeagueValue);
          const leagueResolved = Number.isFinite(leagueParsed)
            ? leagueParsed
            : commParseLeagueLabel(storedLeagueValue);
          if (Number.isFinite(leagueResolved)) {
            commKorrelCurrentLeagueId = Math.trunc(leagueResolved);
          }
        }
        updateCurrentRound(result.lastLiveSummary.data);
      } else if (result.lastWsEvent && result.lastWsEvent.eventData) {
        console.log('[MW] Found stored WS event, updating display');
        updateCurrentRound(result.lastWsEvent.eventData);
      }

      if (Array.isArray(result.mw_pending_clan_match)) {
        result.mw_pending_clan_match.forEach((entry) => {
          processClanMatchMapping(entry.clanId, entry.clanName, entry.reason || 'pending');
        });
        chrome.storage.local.remove(['mw_pending_clan_match'], () => {
          if (chrome.runtime.lastError) {
            console.warn('[MW] Failed to clear pending clan match list:', chrome.runtime.lastError);
          }
        });
      }
      if (result.lastClanMatch && result.lastClanMatch.payload) {
        processClanMatchMapping(
          result.lastClanMatch.payload.clanId,
          result.lastClanMatch.payload.clanName,
          result.lastClanMatch.payload.source || 'lastClanMatch'
        );
      }
      // If we have a stored ability/boost event, process it so boosts show up immediately
      if (result.lastAbilityEvent && result.lastAbilityEvent.payload) {
        try {
          console.log('[MW] Found stored ability event, processing boost usage');
          handleBoostUsage(result.lastAbilityEvent.payload);
        } catch (err) {
          console.warn('[MW] Failed to process stored ability event', err);
        }
      }
    });

  } catch (err) {
    console.error('[MW] Error in populateLive:', err);
    window.debugLogger?.error('Failed to Load Initial Data', {
      error: err.message,
      stack: err.stack
    });
    renderFinishedRounds([{
      id: 'error',
      title: 'Database Error',
      description: 'Could not connect to database. Check console for details.',
      meta: err.message
    }]);
  }

}

function resolveLeaderboardLeagueId() {
  const stored = commParseNumber(localStorage.getItem(LEADERBOARD_LEAGUE_STORAGE_KEY));
  if (Number.isFinite(stored)) return Math.trunc(stored);
  const fallback = commParseNumber(commKorrelCurrentLeagueId ?? lastLiveSummaryLeague);
  if (Number.isFinite(fallback)) return Math.trunc(fallback);
  return LEADERBOARD_DEFAULT_LEAGUE_ID;
}

function setLeaderboardLeagueId(value) {
  if (!Number.isFinite(value)) {
    localStorage.removeItem(LEADERBOARD_LEAGUE_STORAGE_KEY);
    return;
  }
  localStorage.setItem(LEADERBOARD_LEAGUE_STORAGE_KEY, String(Math.trunc(value)));
}

function buildLeaderboardLeagueOptions(selected) {
  const options = Array.from({ length: 16 }, (_, idx) => idx + 1);
  return options.map((league) => {
    const isSelected = Number(league) === Number(selected);
    return `<option value="${league}"${isSelected ? ' selected' : ''}>League ${league}</option>`;
  }).join('');
}

function buildLeaderboardPayload() {
  return {
    calculatedAt: new Date().toISOString(),
    leagueId: resolveLeaderboardLeagueId(),
    pagination: {
      skip: LEADERBOARD_DEFAULT_SKIP,
      limit: LEADERBOARD_DEFAULT_LIMIT
    }
  };
}

function formatLeaderboardTimestamp(value) {
  if (!value) return '-';
  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) return String(value);
  return parsed.toLocaleString();
}

function renderLeaderboardCard(container) {
  if (!container) return;
  const payload = leaderboardState.payload;
  const selectedLeague = resolveLeaderboardLeagueId();
  const calculatedAtLabel = payload?.calculatedAt ? formatLeaderboardTimestamp(payload.calculatedAt) : '-';
  const lastUpdatedLabel = leaderboardState.lastFetchedAt ? formatLeaderboardTimestamp(leaderboardState.lastFetchedAt) : '-';
  const metaLine = `League ${selectedLeague} | Limit ${LEADERBOARD_DEFAULT_LIMIT} | Calculated at ${calculatedAtLabel}`;
  let statusText = 'Leaderboard ready.';
  let statusClass = 'leaderboard-status';
  let tableHtml = '';
  const hasData = !!leaderboardState.data;

  if (leaderboardState.loading) {
    statusText = 'Loading clan leaderboard from API...';
    tableHtml = '<div class="leaderboard-empty muted">Loading leaderboard data...</div>';
  } else if (leaderboardState.error) {
    statusText = `Failed to load leaderboard: ${leaderboardState.error.message || leaderboardState.error}`;
    statusClass += ' is-error';
    tableHtml = '<div class="leaderboard-empty muted">No data available.</div>';
  } else if (!leaderboardState.data) {
    statusText = 'Leaderboard will load when this tab opens.';
    tableHtml = '<div class="leaderboard-empty muted">No data loaded yet.</div>';
  } else {
    statusText = 'Leaderboard loaded.';
    tableHtml = buildApiTable(leaderboardState.data, LEADERBOARD_API_KEY);
  }

  const html = `
    <div class="leaderboard-card">
      <div class="leaderboard-header">
        <div class="leaderboard-title-wrap">
          <div class="leaderboard-title">Clan Leaderboard V2</div>
          <div class="leaderboard-meta muted">${metaLine}</div>
        </div>
        <div class="leaderboard-actions">
          <select id="leaderboardLeagueSelect" class="stats-filter-select" aria-label="Select league">
            ${buildLeaderboardLeagueOptions(selectedLeague)}
          </select>
          <button id="leaderboardExportBtn" class="btn sm ghost" type="button" ${hasData && !leaderboardState.loading ? '' : 'disabled'}>Export XLSX</button>
          <button id="leaderboardRefreshBtn" class="btn sm" type="button" ${leaderboardState.loading ? 'disabled' : ''}>Refresh</button>
        </div>
      </div>
      <div class="leaderboard-submeta muted">Last updated: ${lastUpdatedLabel}</div>
      <div class="${statusClass}">${statusText}</div>
      <div class="leaderboard-table-wrap">
        ${tableHtml}
      </div>
    </div>
  `;
  renderCards(container, [{ id: 'leaderboard-card', htmlContent: html }]);

  const refreshBtn = container.querySelector('#leaderboardRefreshBtn');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => populateLeaderboard({ force: true }));
    refreshBtn.dataset.bound = '1';
  }

  const exportBtn = container.querySelector('#leaderboardExportBtn');
  if (exportBtn && !exportBtn.dataset.bound) {
    exportBtn.addEventListener('click', () => exportLeaderboardToXlsx());
    exportBtn.dataset.bound = '1';
  }

  const leagueSelect = container.querySelector('#leaderboardLeagueSelect');
  if (leagueSelect && !leagueSelect.dataset.bound) {
    leagueSelect.addEventListener('change', (event) => {
      const nextLeague = commParseNumber(event.target.value);
      if (!Number.isFinite(nextLeague)) return;
      setLeaderboardLeagueId(nextLeague);
      leaderboardState.loaded = false;
      leaderboardState.data = null;
      populateLeaderboard({ force: true });
    });
    leagueSelect.dataset.bound = '1';
  }
}

function buildLeaderboardRows(data) {
  if (!data) return [];
  const allClans = [
    ...(data?.data?.clansPromoted || []),
    ...(data?.data?.clansRelegated || []),
    ...(data?.data?.clansRemaining || [])
  ];
  return allClans.map(clan => ({
    Position: clan.position,
    'Clan ID': clan.clanId,
    'Clan Name': clan.clan?.name || '',
    'NFT Power': clan.nftPower,
    'Blocks Mined': clan.blocksMined || 0
  }));
}

function columnIndexToLetter(index) {
  let column = '';
  let current = index;
  while (current > 0) {
    const mod = (current - 1) % 26;
    column = String.fromCharCode(65 + mod) + column;
    current = Math.floor((current - 1) / 26);
  }
  return column || 'A';
}

function ensureXlsxLibrary() {
  if (typeof XLSX !== 'undefined') return true;
  console.warn('[MW] XLSX library missing. Unable to export.');
  alert('XLSX export library is not available. Please reload the app and try again.');
  return false;
}

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

function collectExportHeaders(rows) {
  const headers = [];
  const seen = new Set();
  rows.forEach((row) => {
    if (!row || typeof row !== 'object') return;
    Object.keys(row).forEach((key) => {
      if (seen.has(key)) return;
      seen.add(key);
      headers.push(key);
    });
  });
  return headers;
}

function normalizeExportValue(value) {
  if (value == null) return '';
  if (value instanceof Date) return value.toISOString();
  if (typeof value === 'object') {
    try {
      return JSON.stringify(value);
    } catch (err) {
      return String(value);
    }
  }
  return value;
}

function exportRowsToXlsx(rows, filename, sheetName = 'Sheet1') {
  if (!ensureXlsxLibrary()) return false;
  const safeRows = Array.isArray(rows) ? rows : [];
  if (!safeRows.length) return false;
  const headers = collectExportHeaders(safeRows);
  const normalizedRows = safeRows.map((row) => {
    const output = {};
    headers.forEach((key) => {
      output[key] = normalizeExportValue(row?.[key]);
    });
    return output;
  });
  const workbook = XLSX.utils.book_new();
  const worksheet = XLSX.utils.json_to_sheet(normalizedRows, { header: headers });
  const headerCount = Math.max(1, headers.length);
  const endCol = columnIndexToLetter(headerCount);
  const endRow = normalizedRows.length + 1;
  worksheet['!autofilter'] = { ref: `A1:${endCol}${endRow}` };
  worksheet['!cols'] = headers.map((key) => ({
    wch: Math.max(10, Math.min(50, String(key).length + 6))
  }));
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
  XLSX.writeFile(workbook, filename, { compression: true });
  return true;
}

function exportLeaderboardToXlsx() {
  if (!leaderboardState?.data) return;
  const rows = buildLeaderboardRows(leaderboardState.data);
  if (!rows.length) return;
  const filename = `clan_leaderboard_v2_${getExportTimestamp()}.xlsx`;
  exportRowsToXlsx(rows, filename, 'Leaderboard');
}

async function populateLeaderboard({ force = false } = {}) {
  const container = document.getElementById('leaderboardGrid');
  if (!container) return;
  if (leaderboardState.loading) return;

  if (leaderboardState.loaded && !force) {
    renderLeaderboardCard(container);
    return;
  }

  leaderboardState.loading = true;
  leaderboardState.error = null;
  leaderboardState.payload = buildLeaderboardPayload();
  renderLeaderboardCard(container);

  const endpoint = API_DEFINITIONS?.[LEADERBOARD_API_KEY]?.endpoint
    || 'https://api.gomining.com/api/nft-game/clan-leaderboard/index-v2';

  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(leaderboardState.payload)
    });
    const data = await response.json().catch(() => null);
    if (!response.ok) {
      const message = data?.message || data?.error || `Request failed (${response.status})`;
      throw new Error(message);
    }
    leaderboardState.data = data;
    leaderboardState.loaded = true;
    leaderboardState.lastFetchedAt = Date.now();
  } catch (err) {
    leaderboardState.error = err;
  } finally {
    leaderboardState.loading = false;
    renderLeaderboardCard(container);
  }
}

// ============ CALCULATOR LOGIC (adapted from sidepanel.js) ============

// Clan multiplier calculation
function getClanMultiplier(activations) {
  let m = 1;
  for (let i = 1; i <= activations; i++) {
    if (i === 1) m += 1.1;
    else if (i <= 4) m += 1.3;
    else if (i <= 9) m += 1.5;
    else if (i <= 14) m += 1.6;
    else if (i <= 29) m += 1.7;
    else if (i <= 49) m += 2.2;
    else if (i <= 99) m += 2.9;
    else if (i <= 199) m += 3.8;
    else if (i <= 499) m += 4.9;
    else if (i <= 1000) m += 6.4;
  }
  return m;
}

// Get value from input, ensuring non-negative
function getInputValue(id, isFloat = true) {
  const el = document.getElementById(id);
  if (!el) return 0;
  const parsed = isFloat ? parseFloat(el.value) : parseInt(el.value, 10);
  return isNaN(parsed) ? 0 : Math.max(0, parsed);
}

function readCalcStorageSnapshot() {
  try {
    return JSON.parse(localStorage.getItem(CALC_STORAGE_KEY) || '{}') || {};
  } catch (err) {
    return {};
  }
}

function readCalcSettingValue(id, fallback = null) {
  const el = document.getElementById(id);
  if (el) {
    if (el.type === 'checkbox') return Boolean(el.checked);
    return el.value;
  }
  const stored = readCalcStorageSnapshot();
  return stored[id] !== undefined ? stored[id] : fallback;
}

function readCalculatorInputsFromStorage() {
  const getNum = (id, isFloat = true) => {
    const value = readCalcSettingValue(id, null);
    if (value === null || value === undefined || value === '') return 0;
    const parsed = isFloat ? parseFloat(value) : parseInt(value, 10);
    return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
  };
  return {
    basePPS: getNum('calc_pps'),
    globalBoostDelay: getNum('calc_boost_delay', false),
    powerupCount: Math.min(1, Math.max(0, getNum('calc_powerup', false))),
    powerupDelay: getNum('calc_powerup_delay', false),
    powerupCost: getNum('calc_powerup_cost'),
    clanActivations: getNum('calc_clan', false),
    clanDelay: getNum('calc_clan_delay', false),
    clanCost: getNum('calc_clan_cost'),
    powerboost: getNum('calc_powerboost', false),
    powerboostDelay: getNum('calc_powerboost_delay', false),
    instant: getNum('calc_instant', false),
    instantDelay: getNum('calc_instant_delay', false),
    echo: getNum('calc_echo', false),
    echoDelay: getNum('calc_echo_delay', false),
    focus: getNum('calc_focus', false),
    focusDelay: getNum('calc_focus_delay', false)
  };
}

function readCalculatorInputsSafe() {
  if (document.getElementById('calc_pps')) {
    return readCalculatorInputs();
  }
  return readCalculatorInputsFromStorage();
}

function readCalculatorProfileSettings() {
  const rawMode = String(readCalcSettingValue(CALC_PROFILE_MODE_KEY, CALC_PROFILE_DEFAULT) || '').trim().toLowerCase();
  const mode = ['conservative', 'balanced', 'aggressive'].includes(rawMode) ? rawMode : CALC_PROFILE_DEFAULT;
  const strict = Boolean(readCalcSettingValue(CALC_PROFILE_STRICT_KEY, false));
  const budgetWeek = commParseNumber(readCalcSettingValue(CALC_PROFILE_BUDGET_KEY, 0)) || 0;
  const roundCap = commParseNumber(readCalcSettingValue(CALC_PROFILE_ROUND_CAP_KEY, 0)) || 0;
  return {
    mode,
    strict,
    budgetWeek,
    roundCap
  };
}

function getStoredLiveProfileClan() {
  if (typeof localStorage === 'undefined') return '';
  return String(localStorage.getItem(LIVE_PROFILE_CLAN_KEY) || '').trim();
}

function setStoredLiveProfileClan(name) {
  if (typeof localStorage === 'undefined') return;
  const value = String(name || '').trim();
  if (value) {
    localStorage.setItem(LIVE_PROFILE_CLAN_KEY, value);
  } else {
    localStorage.removeItem(LIVE_PROFILE_CLAN_KEY);
  }
}

// Calculate effective seconds after delays
function effectiveSeconds(globalDelay, individualDelay, duration) {
  return Math.max(0, duration - (globalDelay + individualDelay));
}

const ROUND_TIME_CALC_OFFSET_SEC = 2;
function applyRoundTimeCalcOffset(seconds) {
  if (!Number.isFinite(seconds)) return null;
  return Math.max(0, seconds + ROUND_TIME_CALC_OFFSET_SEC);
}

function parseRoundTimerSeconds(timerValue) {
  if (!timerValue) return null;
  const raw = String(timerValue).trim();
  if (!raw) return null;
  const parts = raw
    .split(':')
    .map((part) => String(part).replace(/[^0-9]/g, ''))
    .map((part) => (part === '' ? NaN : Number(part)));
  if (parts.some((part) => !Number.isFinite(part))) return null;
  if (parts.length === 3) {
    return parts[0] * 3600 + parts[1] * 60 + parts[2];
  }
  if (parts.length === 2) {
    return parts[0] * 60 + parts[1];
  }
  if (parts.length === 1) {
    return parts[0];
  }
  return null;
}

function resolveSimRoundTimer(timerValue, multiplierValue) {
  const timerRaw = timerValue != null ? String(timerValue).trim() : '';
  const multRaw = multiplierValue != null ? String(multiplierValue).trim() : '';
  const timerHasColon = timerRaw.includes(':');
  const multHasColon = multRaw.includes(':');
  if (timerHasColon && !multHasColon) return timerValue;
  if (multHasColon && !timerHasColon) return multiplierValue;

  const timerSecs = parseRoundTimerSeconds(timerValue);
  const multSecs = parseRoundTimerSeconds(multiplierValue);
  if (timerSecs == null && multSecs != null) return multiplierValue;
  if (multSecs == null && timerSecs != null) return timerValue;

    const multiplierSet = new Set([1, 2, 3, 4, 5, 8, 10, 16, 32, 64, 128, 256, 100]);
  const timerNum = parseNumericValue(timerValue);
  const multNum = parseNumericValue(multiplierValue);
  const timerLooksLikeMult = timerNum != null && multiplierSet.has(timerNum);
  const multLooksLikeMult = multNum != null && multiplierSet.has(multNum);
  if (timerLooksLikeMult && !multLooksLikeMult) return multiplierValue;
  if (multLooksLikeMult && !timerLooksLikeMult) return timerValue;

  if (timerSecs != null && multSecs != null) {
    return timerSecs >= multSecs ? timerValue : multiplierValue;
  }
  if (timerSecs != null) return timerValue;
  if (multSecs != null) return multiplierValue;
  return timerValue != null ? timerValue : multiplierValue;
}

function commNormalizeRoundTiming(timerValue, multiplierValue) {
  const timerRaw = timerValue != null ? String(timerValue).trim() : '';
  const multRaw = multiplierValue != null ? String(multiplierValue).trim() : '';
  const timerHasDigits = /\d/.test(timerRaw);
  const multHasDigits = /\d/.test(multRaw);
  const invalidTimer = !timerRaw || !timerHasDigits || timerRaw === '-' || timerRaw === '-' || timerRaw.toLowerCase() === 'n/a';
  const invalidMult = !multRaw || !multHasDigits || multRaw === '-' || multRaw === '-' || multRaw.toLowerCase() === 'n/a';
  const timerValueSafe = invalidTimer ? null : timerValue;
  const multiplierValueSafe = invalidMult ? null : multiplierValue;
  const timerHasColon = timerRaw.includes(':');
  const multHasColon = multRaw.includes(':');
  const multiplierSet = new Set([1, 2, 3, 4, 5, 8, 10, 16, 32, 64, 128, 256, 100]);
  const timerNum = parseNumericValue(timerValue);
  const multNum = parseNumericValue(multiplierValue);
  const timerLooksLikeMult = timerNum != null && multiplierSet.has(timerNum);
  const multLooksLikeMult = multNum != null && multiplierSet.has(multNum);

  if ((invalidTimer || timerLooksLikeMult || !timerHasDigits) && multHasColon && !timerHasColon) {
    return { roundTime: multiplierValueSafe, multiplier: timerValueSafe };
  }
  if (timerHasColon && !multHasColon && !invalidTimer) {
    return { roundTime: timerValueSafe, multiplier: multiplierValueSafe };
  }
  if (timerLooksLikeMult && !multLooksLikeMult) {
    return { roundTime: multiplierValueSafe, multiplier: timerValueSafe };
  }
  if (multLooksLikeMult && !timerLooksLikeMult) {
    return { roundTime: timerValueSafe, multiplier: multiplierValueSafe };
  }
  return { roundTime: timerValueSafe, multiplier: multiplierValueSafe };
}

function resetSimTimerFromRoundTime() {
  if (Number.isFinite(latestRoundTimerSeconds)) {
    simTimerSeconds = latestRoundTimerSeconds;
    simTimerLastSync = Date.now();
    simTimerSource = latestRoundTimerValue != null ? String(latestRoundTimerValue) : null;
    return;
  }
  const parsed = parseRoundTimerSeconds(latestRoundTimerValue);
  if (parsed != null) {
    simTimerSeconds = parsed;
    simTimerLastSync = Date.now();
    simTimerSource = latestRoundTimerValue != null ? String(latestRoundTimerValue) : null;
    latestRoundTimerSeconds = parsed;
  }
}

function getSimElapsedSeconds(timerValue) {
  const raw = timerValue != null ? String(timerValue) : null;
  if (raw && raw !== simTimerSource) {
    const parsed = parseRoundTimerSeconds(raw);
    if (parsed != null) {
      simTimerSeconds = parsed;
      simTimerLastSync = Date.now();
      simTimerSource = raw;
      return parsed;
    }

  }
  if (simTimerSeconds == null) {
    const parsed = parseRoundTimerSeconds(timerValue);
    if (parsed != null) {
      simTimerSeconds = parsed;
      simTimerLastSync = Date.now();
      simTimerSource = raw;
      return parsed;
    }
    return 0;
  }
  const elapsed = Math.floor((Date.now() - simTimerLastSync) / 1000);
  return simTimerSeconds + Math.max(0, elapsed);
}

function readCalculatorInputs() {
  return {
    basePPS: getInputValue('calc_pps'),
    globalBoostDelay: getInputValue('calc_boost_delay', false),
    powerupCount: Math.min(1, Math.max(0, getInputValue('calc_powerup', false))),
    powerupDelay: getInputValue('calc_powerup_delay', false),
    powerupCost: getInputValue('calc_powerup_cost'),
    clanActivations: getInputValue('calc_clan', false),
    clanDelay: getInputValue('calc_clan_delay', false),
    clanCost: getInputValue('calc_clan_cost'),
    powerboost: getInputValue('calc_powerboost', false),
    powerboostDelay: getInputValue('calc_powerboost_delay', false),
    instant: getInputValue('calc_instant', false),
    instantDelay: getInputValue('calc_instant_delay', false),
    echo: getInputValue('calc_echo', false),
    echoDelay: getInputValue('calc_echo_delay', false),
    focus: getInputValue('calc_focus', false),
    focusDelay: getInputValue('calc_focus_delay', false)
  };
}

function calculateBoostTotalsForSeconds(durationSeconds, inputs) {
  const {
    basePPS,
    globalBoostDelay,
    powerupCount,
    powerupDelay,
    clanActivations,
    clanDelay,
    powerboost,
    powerboostDelay,
    instant,
    instantDelay,
    echo,
    echoDelay,
    focus,
    focusDelay
  } = inputs;
  const sec = Math.max(0, Number(durationSeconds) || 0);
  const clanMultiplier = getClanMultiplier(clanActivations);

  const base = Math.round(basePPS * sec);

  const activePowerup = effectiveSeconds(globalBoostDelay, powerupDelay, sec);
  const powerup = Math.round((basePPS * ((powerupCount ? 100 : 1) - 1)) * activePowerup);

  const activeClan = effectiveSeconds(globalBoostDelay, clanDelay, sec);
  const clan = Math.round((basePPS * (clanMultiplier - 1)) * activeClan);

  const activePowerBoost = effectiveSeconds(globalBoostDelay, powerboostDelay, sec);
  const powerBoost = Math.round(powerboost * 1800 * activePowerBoost);

  const activeInstant = effectiveSeconds(globalBoostDelay, instantDelay, sec);
  const instantBoost = activeInstant >= 3 ? instant * 400000 : 0;

  const activeEcho = effectiveSeconds(globalBoostDelay, echoDelay, sec);
  const echoTriggers = Math.floor(activeEcho / 120);
  const echoBoost = echo * 70000 * (echoTriggers * (echoTriggers + 1) / 2);

  const activeFocus = effectiveSeconds(globalBoostDelay, focusDelay, sec);
  const focusBoost = (activeFocus >= 3 && activeFocus <= 303) ? (1000000 * focus) : 0;

  const total = base + powerup + clan + powerBoost + instantBoost + echoBoost + focusBoost;

  return {
    total,
    base,
    boostOnly: total - base,
    components: {
      powerup,
      clan,
      powerBoost,
      instant: instantBoost,
      echo: echoBoost,
      focus: focusBoost
    }
  };
}

function buildCalculatorSimEntry(timerValue) {
  if (!calculatorSimEnabled) return null;
  const inputs = readCalculatorInputs();
  const seconds = getSimElapsedSeconds(timerValue);
  const totals = calculateBoostTotalsForSeconds(seconds, inputs);

  const usedAbilities = [];
  if (inputs.powerupCount > 0) {
    usedAbilities.push({ nftGameAbilityId: '5d6f8166-0f20-486f-b920-e898ca94dcc1', count: inputs.powerupCount });
  }
  if (inputs.clanActivations > 0) {
    usedAbilities.push({ nftGameAbilityId: '8a9b57a9-8a17-4647-8b2f-a164dc52b1f4', count: inputs.clanActivations });
  }
  if (inputs.powerboost > 0) {
    usedAbilities.push({ nftGameAbilityId: '3f735c47-cead-4d15-8dad-7db4a7b3f4e7', count: inputs.powerboost });
  }
  if (inputs.instant > 0) {
    usedAbilities.push({ nftGameAbilityId: '370be7b3-d843-4e2f-a23a-eeea37648f99', count: inputs.instant });
  }
  if (inputs.echo > 0) {
    usedAbilities.push({ nftGameAbilityId: '646d3c76-fe06-414f-8d39-eb0aa7da429a', count: inputs.echo });
  }
  if (inputs.focus > 0) {
    usedAbilities.push({ nftGameAbilityId: '592724c5-1f59-43b2-af13-2de8d2c97a9d', count: inputs.focus });
  }

  const usedTotal = usedAbilities.reduce((sum, ability) => sum + (Number(ability.count) || 0), 0);

  return {
    clan_name: 'SIM Calculator',
    current_added_score: totals.total,
    boosted_score_estimate: totals.total,
    active_boost_score: totals.boostOnly,
    score_candidates: { pps_ws: totals.total },
    used_abilities_total: usedTotal,
    used_abilities: usedAbilities,
    clan_power: null,
    simulated: true
  };
}

// Main calculation function
function calculateBoostResults() {
  // Read inputs
  const {
    basePPS,
    globalBoostDelay,
    powerupCount,
    powerupDelay,
    powerupCost,
    clanActivations,
    clanDelay,
    clanCost,
    powerboost,
    powerboostDelay,
    instant,
    instantDelay,
    echo,
    echoDelay,
    focus,
    focusDelay
  } = readCalculatorInputs();

  // Calculate clan multiplier
  const clanMultiplier = getClanMultiplier(clanActivations);

  // Time intervals (seconds)
  const intervals = [300, 600, 1800];
  const labels = ['5 min', '10 min', '30 min'];

  // Calculate base row
  const baseRow = intervals.map(sec => Math.round(basePPS * sec));

  // Calculate powerup row
  const powerupRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, powerupDelay, sec);
    const extraPerSec = basePPS * ((powerupCount ? 100 : 1) - 1);
    return Math.round(extraPerSec * active);
  });

  // Calculate clan row
  const clanRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, clanDelay, sec);
    const extraPerSec = basePPS * (clanMultiplier - 1);
    return Math.round(extraPerSec * active);
  });

  // Calculate power boost row
  const powerBoostRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, powerboostDelay, sec);
    return Math.round(powerboost * 1800 * active);
  });

  // Calculate instant row
  const instantRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, instantDelay, sec);
    return active >= 3 ? instant * 400000 : 0;
  });

  // Calculate echo row
  const echoRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, echoDelay, sec);
    const triggers = Math.floor(active / 120);
    return echo * 70000 * (triggers * (triggers + 1) / 2);
  });

  // Calculate focus row
  const focusRow = intervals.map(sec => {
    const active = effectiveSeconds(globalBoostDelay, focusDelay, sec);
    return (active >= 3 && active <= 303) ? (1000000 * focus) : 0;
  });

  // Calculate totals
  const totals = intervals.map((_, i) =>
    baseRow[i] + powerupRow[i] + clanRow[i] + powerBoostRow[i] +
    instantRow[i] + echoRow[i] + focusRow[i]
  );

  // Calculate total cost
  const totalCost = (powerupCount * powerupCost) +
                    (clanActivations ? clanCost : 0) +
                    powerboost + instant + echo + focus;

  return {
    labels,
    intervals,
    baseRow,
    powerupRow,
    clanRow,
    powerBoostRow,
    instantRow,
    echoRow,
    focusRow,
    totals,
    totalCost,
    clanMultiplier
  };
}

// Render results table
function renderCalculatorResults() {
  const resultsEl = document.getElementById('calc_results');
  if (!resultsEl) return;

  const results = calculateBoostResults();

  const renderRow = (icon, row, intervals) => {
    const cells = row.map((p, i) => {
      const kps = (p / intervals[i] / 1000).toFixed(1);
      return `<td class="cavern-table-cell-number">${p.toLocaleString()}<span class="cavern-table-cell-subtext">${kps}k/s</span></td>`;
    }).join('');
    return `<tr><td class="cavern-table-cell-icon">${icon}</td>${cells}</tr>`;
  };

  let html = '<div style="padding: 8px;">';

  // Summary at top
  html += `<div style="margin-bottom: 12px; padding: 10px; background: rgba(56, 189, 248, 0.1); border-radius: 6px; display: flex; justify-content: space-between; font-size: 1em;">`;
  html += `<span><strong>Cost:</strong> ${results.totalCost.toFixed(2)} GMT</span>`;
  html += `<span><strong>Multi:</strong> ${results.clanMultiplier.toFixed(2)}x</span>`;
  html += `</div>`;

  // Combine all in one compact table
  html += '<table class="cavern-table">';
  html += '<thead><tr><th>Type</th>' +
          results.labels.map(l => `<th>${l}</th>`).join('') + '</tr></thead>';

  html += '<tbody>';
  // Power-ups
  html += `<tr><td colspan="4" class="cavern-section-title" style="background: rgba(56, 189, 248, 0.05);">MULTIPLIERS</td></tr>`;
  html += renderRow(BOOST_ICON_MAP.powerup, results.powerupRow, results.intervals);
  html += renderRow(BOOST_ICON_MAP.clanpowerup, results.clanRow, results.intervals);

  // Spells
  html += `<tr><td colspan="4" class="cavern-section-title" style="background: rgba(251, 113, 133, 0.05); color: var(--accent-2);">SPELLS</td></tr>`;
  html += renderRow(BOOST_ICON_MAP.powerboost, results.powerBoostRow, results.intervals);
  html += renderRow(BOOST_ICON_MAP.instant, results.instantRow, results.intervals);
  html += renderRow(BOOST_ICON_MAP.echo, results.echoRow, results.intervals);
  html += renderRow(BOOST_ICON_MAP.focus, results.focusRow, results.intervals);

  // Totals
  html += `<tr><td colspan="4" class="cavern-section-title" style="background: rgba(255, 255, 255, 0.05);">TOTALS</td></tr>`;
  html += renderRow('*', results.baseRow, results.intervals);
  html += `<tr style="font-weight: 700; background: rgba(56, 189, 248, 0.1);">${renderRow('*', results.totals, results.intervals)}</tr>`;
  html += '</tbody>';

  html += '</table>';
  html += '</div>';

  resultsEl.innerHTML = html;

  const simCheckbox = document.getElementById('calc_sim_enable');
  calculatorSimEnabled = simCheckbox ? simCheckbox.checked : calculatorSimEnabled;
  if (typeof window.__refreshCurrentRoundCard === 'function') {
    window.__refreshCurrentRoundCard();
  }
}

function populateCalculator() {
  const container = document.getElementById('calculatorGrid');
  const calcStorageKey = CALC_STORAGE_KEY;

  // Combined compact inputs card - all in one
  const allInputsHTML = `
    <h3 class="cavern-header">Calculator Calculator</h3>
    <div class="calculator-container">
      <div class="calc-tab-panel active" data-tab="inputs">
      <!-- Base Config -->
      <div class="cavern-grid" style="grid-template-columns: 1fr 1fr; margin-bottom: 10px;">
        <label class="cavern-label-group">
          <span class="cavern-label-text">Base PPS</span>
          <input id="calc_pps" type="number" value="135" min="0" class="cavern-input">
        </label>
        <label class="cavern-label-group">
          <span class="cavern-label-text">Delay (s)</span>
          <input id="calc_boost_delay" type="number" value="30" min="0" class="cavern-input">
        </label>
      </div>
      <label style="display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 0.9em;">
        <input id="calc_sim_enable" type="checkbox" style="margin: 0;">
        <span style="opacity: 0.8;">Show SIM Calculator in live round</span>
      </label>

      <div class="calc-sections">
      <!-- Power-ups Section -->
      <div class="cavern-section">
        <div class="cavern-section-title">POWER-UPS</div>
        <div class="cavern-grid-row" style="margin-bottom: 6px;">
          <span class="cavern-icon-label" title="Power-up">PU</span>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Count</span>
            <input id="calc_powerup" type="number" value="0" min="0" max="1" class="cavern-input">
          </label>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Delay</span>
            <input id="calc_powerup_delay" type="number" value="0" min="0" class="cavern-input">
          </label>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Cost</span>
            <input id="calc_powerup_cost" type="number" value="7.42" step="0.01" class="cavern-input">
          </label>
        </div>
        <div class="cavern-grid-row">
          <span class="cavern-icon-label" title="Clan Power-up">ClanPU</span>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Count</span>
            <input id="calc_clan" type="number" value="0" min="0" class="cavern-input">
          </label>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Delay</span>
            <input id="calc_clan_delay" type="number" value="0" min="0" class="cavern-input">
          </label>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Cost</span>
            <input id="calc_clan_cost" type="number" value="1" step="0.01" class="cavern-input">
          </label>
        </div>
      </div>

      <!-- Spells Section -->
      <div class="cavern-section" style="background: rgba(251, 113, 133, 0.05); border-color: rgba(251, 113, 133, 0.2);">
        <div class="cavern-section-title" style="color: var(--accent-2);">SPELLS</div>
        <div class="cavern-grid" style="grid-template-columns: 1fr 0.6fr 0.6fr;">
          <span class="cavern-icon-label" title="Power Boost">Power</span>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Count</span>
            <input id="calc_powerboost" type="number" value="0" min="0" class="cavern-input">
          </label>
          <label class="cavern-label-group">
            <span class="cavern-label-text">Delay</span>
            <input id="calc_powerboost_delay" type="number" value="0" min="0" class="cavern-input">
          </label>

          <span class="cavern-icon-label" title="Instant">Instant</span>
          <input id="calc_instant" type="number" value="0" min="0" class="cavern-input">
          <input id="calc_instant_delay" type="number" value="0" min="0" class="cavern-input">

          <span class="cavern-icon-label" title="Echo">Echo</span>
          <input id="calc_echo" type="number" value="0" min="0" class="cavern-input">
          <input id="calc_echo_delay" type="number" value="0" min="0" class="cavern-input">

          <span class="cavern-icon-label" title="Focus">Focus</span>
          <input id="calc_focus" type="number" value="0" min="0" class="cavern-input">
          <input id="calc_focus_delay" type="number" value="0" min="0" class="cavern-input">
        </div>
      </div>
      </div>
      </div>
    </div>
  `;

  // Results Card - compact version
  const resultsHTML = `
    <h3 class="cavern-header">Stats Results</h3>
    <div id="calc_results" class="calculator-results">
      <div style="padding: 12px; text-align: center; opacity: 0.7;">
        Click Calculate
      </div>
    </div>
  `;

  const inputCard = createCard({ title: 'Boost Calculator', htmlContent: allInputsHTML, id: 'calc-inputs' });
  inputCard.classList.add('calculator-inputs-card');
  const resultsCard = createCard({ title: 'Results', htmlContent: resultsHTML, id: 'calc-results-card' });

  [inputCard, resultsCard].forEach((card) => {
    const deleteBtn = card.querySelector('.card-delete-btn');
    if (deleteBtn) deleteBtn.remove();
    card.setAttribute('draggable', 'false');
    card.classList.remove('draggable');
  });

  [inputCard, resultsCard].forEach(card => container.appendChild(card));

  const calcInputValues = {
    load() {
      try {
        return JSON.parse(localStorage.getItem(calcStorageKey) || '{}') || {};
      } catch (err) {
        return {};
      }
    },
    save(values) {
      try {
        localStorage.setItem(calcStorageKey, JSON.stringify(values));
        commScheduleUserSettingsSync();
      } catch (err) {
        // Ignore storage errors
      }
    }
  };

  // Add input change listeners for live calculation
  const inputIds = [
    'calc_pps', 'calc_boost_delay',
    'calc_powerup', 'calc_powerup_delay', 'calc_powerup_cost',
    'calc_clan', 'calc_clan_delay', 'calc_clan_cost',
    'calc_powerboost', 'calc_powerboost_delay',
    'calc_instant', 'calc_instant_delay',
    'calc_echo', 'calc_echo_delay',
    'calc_focus', 'calc_focus_delay',
    'calc_sim_enable',
    'calc_profile_budget_week',
    'calc_profile_round_cap',
    'calc_profile_strict',
    'calc_profile_mode'
  ];

  setTimeout(() => {
    const savedValues = calcInputValues.load();
    const profileButtons = Array.from(document.querySelectorAll('.calc-profile-card'));
    const profileInput = document.getElementById('calc_profile_mode');
    const calcTabButtons = Array.from(document.querySelectorAll('.calc-tab-btn'));
    const calcTabPanels = Array.from(document.querySelectorAll('.calc-tab-panel'));

    const applyProfileSelection = (mode) => {
      const raw = String(mode || '').trim().toLowerCase();
      const safeMode = ['conservative', 'balanced', 'aggressive'].includes(raw) ? raw : CALC_PROFILE_DEFAULT;
      if (profileInput) profileInput.value = safeMode;
      profileButtons.forEach((btn) => {
        btn.classList.toggle('active', btn.dataset.profile === safeMode);
      });
    };

    const saveSnapshot = () => {
      const snapshot = {};
      inputIds.forEach((inputId) => {
        const inputEl = document.getElementById(inputId);
        if (inputEl) {
          snapshot[inputId] = inputEl.type === 'checkbox' ? inputEl.checked : inputEl.value;
        }
      });
      calcInputValues.save(snapshot);
    };

    const handleCalcInput = () => {
      saveSnapshot();
      const simCheckbox = document.getElementById('calc_sim_enable');
      calculatorSimEnabled = simCheckbox ? simCheckbox.checked : false;
      resetSimTimerFromRoundTime();
      if (typeof window.__refreshCurrentRoundCard === 'function') {
        window.__refreshCurrentRoundCard();
      }
      renderCalculatorResults();
    };

    inputIds.forEach((id) => {
      const el = document.getElementById(id);
      if (!el) return;
      if (savedValues[id] !== undefined) {
        if (el.type === 'checkbox') {
          el.checked = Boolean(savedValues[id]);
        } else {
          el.value = savedValues[id];
        }
      }
      el.addEventListener('input', handleCalcInput);
      el.addEventListener('change', handleCalcInput);
    });

    const storedProfile = savedValues[CALC_PROFILE_MODE_KEY] || CALC_PROFILE_DEFAULT;
    applyProfileSelection(storedProfile);

    profileButtons.forEach((btn) => {
      btn.addEventListener('click', () => {
        applyProfileSelection(btn.dataset.profile);
        handleCalcInput();
      });
    });

    calcTabButtons.forEach((btn) => {
      btn.addEventListener('click', () => {
        const tab = btn.dataset.tab;
        if (!tab) return;
        calcTabButtons.forEach((button) => button.classList.toggle('active', button.dataset.tab === tab));
        calcTabPanels.forEach((panel) => panel.classList.toggle('active', panel.dataset.tab === tab));
      });
    });

    if (calcTabButtons.length && calcTabPanels.length) {
      calcTabButtons[0].click();
    }

    const simCheckbox = document.getElementById('calc_sim_enable');
    calculatorSimEnabled = simCheckbox ? simCheckbox.checked : false;
    if (typeof window.__refreshCurrentRoundCard === 'function') {
      window.__refreshCurrentRoundCard();
    }
    renderCalculatorResults();
  }, 100);
}



function commEscape(value) {
  return String(value ?? '').replace(/[&<>"']/g, (match) => {
    switch (match) {
      case '&':
        return '&amp;';
      case '<':
        return '&lt;';
      case '>':
        return '&gt;';
      case '"':
        return '&quot;';
      case "'":
        return '&#39;';
      default:
        return match;
    }
  });
}

function commSetStatus(message, { error = false, success = false, privateScope = false } = {}) {
  const el = document.getElementById(privateScope ? 'commPrivateStatus' : 'commStatus');
  if (!el) return;
  el.textContent = message || '';
  if (!message) {
    el.style.color = '';
    return;
  }
  if (error) {
    el.style.color = '#ff6b6b';
  } else if (success) {
    el.style.color = '#7dff8a';
  } else {
    el.style.color = '';
  }
}

function commIsKorrelIdentity() {
  if (typeof loadHomeGoogleProfile === 'function') {
    loadHomeGoogleProfile();
  }
  const name = String(commSession?.memberName || commSession?.userName || '').trim().toLowerCase();
  const email = String(homeGoogleProfile?.email || '').toLowerCase();
  if (name === 'korrel') return true;
  if (email && email.includes('korrel')) return true;
  return false;
}

function commIsKorrelNonAdminMode() {
  return localStorage.getItem(COMM_KORREL_FORCE_NON_ADMIN_KEY) === '1';
}

function setKorrelNonAdminMode(enabled) {
  if (!commIsKorrelIdentity()) return;
  if (enabled) {
    localStorage.setItem(COMM_KORREL_FORCE_NON_ADMIN_KEY, '1');
  } else {
    localStorage.removeItem(COMM_KORREL_FORCE_NON_ADMIN_KEY);
  }
}

function commIsKorrel() {
  if (!commIsKorrelIdentity()) return false;
  if (commIsKorrelNonAdminMode()) return false;
  return true;
}

function commParseNumber(value) {
  if (value === null || value === undefined) return null;
  if (typeof value === 'number') return Number.isFinite(value) ? value : null;
  const cleaned = String(value).replace(/,/g, '').replace(/[^\d.+-]/g, '');
  if (!cleaned) return null;
  const parsed = Number(cleaned);
  return Number.isFinite(parsed) ? parsed : null;
}

function commCleanClanDisplayName(value) {
  if (!value) return '';
  const raw = String(value).trim().replace(/\s+\d+(\.\d+)?%\s*$/, '').replace(/\s+/g, ' ');
  if (/^[0-9.,]+[kKmM]?$/.test(raw)) return '';
  return raw;
}

function loadRecentRoundsCache() {
  try {
    const raw = localStorage.getItem(LIVE_RECENT_ROUNDS_CACHE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return null;
    const rows = Array.isArray(parsed.rows) ? parsed.rows : [];
    return {
      rows,
      cycleId: Number.isFinite(commParseNumber(parsed.cycleId)) ? Math.trunc(commParseNumber(parsed.cycleId)) : null,
      leagueId: Number.isFinite(commParseNumber(parsed.leagueId)) ? Math.trunc(commParseNumber(parsed.leagueId)) : null,
      cacheAt: Number.isFinite(commParseNumber(parsed.cacheAt)) ? Number(parsed.cacheAt) : null
    };
  } catch (err) {
    return null;
  }
}

function saveRecentRoundsCache(rows = [], { cycleId = null, leagueId = null } = {}) {
  try {
    const payload = {
      rows: Array.isArray(rows) ? rows.slice(0, 10) : [],
      cycleId: Number.isFinite(commParseNumber(cycleId)) ? Math.trunc(commParseNumber(cycleId)) : null,
      leagueId: Number.isFinite(commParseNumber(leagueId)) ? Math.trunc(commParseNumber(leagueId)) : null,
      cacheAt: Date.now()
    };
    localStorage.setItem(LIVE_RECENT_ROUNDS_CACHE_KEY, JSON.stringify(payload));
  } catch (err) {
    // ignore cache write errors
  }
}

function loadHomeGoogleProfile() {
  if (homeGoogleProfile) return homeGoogleProfile;
  try {
    const raw = localStorage.getItem(COMM_GOOGLE_PROFILE_KEY);
    if (!raw) return null;
    homeGoogleProfile = JSON.parse(raw);
    return homeGoogleProfile;
  } catch (err) {
    homeGoogleProfile = null;
    return null;
  }
}

function saveHomeGoogleProfile(profile) {
  homeGoogleProfile = profile;
  try {
    if (profile) {
      localStorage.setItem(COMM_GOOGLE_PROFILE_KEY, JSON.stringify(profile));
    } else {
      localStorage.removeItem(COMM_GOOGLE_PROFILE_KEY);
    }
  } catch (err) {
    /* ignore */
  }
}

function loadHomeAuthSession() {
  if (homeAuthSession) return homeAuthSession;
  try {
    const raw = localStorage.getItem(COMM_AUTH_SESSION_KEY);
    if (!raw) return null;
    homeAuthSession = JSON.parse(raw);
    return homeAuthSession;
  } catch (err) {
    homeAuthSession = null;
    return null;
  }
}

function saveHomeAuthSession(session) {
  homeAuthSession = session;
  try {
    if (session) {
      localStorage.setItem(COMM_AUTH_SESSION_KEY, JSON.stringify(session));
    } else {
      localStorage.removeItem(COMM_AUTH_SESSION_KEY);
    }
  } catch (err) {
    /* ignore */
  }
}

function clearHomeAuthSession() {
  saveHomeAuthSession(null);
  saveHomeGoogleProfile(null);
}

async function captureAuthFromUrl() {
  const hash = window.location?.hash || '';
  if (!hash || !hash.includes('access_token=')) return false;
  const params = new URLSearchParams(hash.replace(/^#/, ''));
  const accessToken = params.get('access_token');
  if (!accessToken) return false;
  const refreshToken = params.get('refresh_token');
  const expiresAt = Number(params.get('expires_at')) || null;
  const tokenType = params.get('token_type') || 'bearer';
  saveHomeAuthSession({
    access_token: accessToken,
    refresh_token: refreshToken,
    token_type: tokenType,
    expires_at: expiresAt
  });
  try {
    const user = await fetchSupabaseUser(accessToken);
    if (user?.email) setHomeProfileFromUser(user);
  } catch (err) {
    /* ignore */
  }
  setHomeAuthStatus('Email confirmed. You can close this tab.', { error: false });
  try {
    window.history.replaceState({}, document.title, window.location.pathname);
  } catch (err) {
    /* ignore */
  }
  return true;
}

function isHomeAuthSessionExpired(session) {
  if (!session?.expires_at) return true;
  const expiresAtMs = Number(session.expires_at) * 1000;
  return !Number.isFinite(expiresAtMs) || Date.now() >= expiresAtMs;
}

function setHomeAuthStatus(message, { error = false } = {}) {
  homeAuthStatus = { message: message || '', error: !!error };
}

async function fetchSupabaseUser(accessToken) {
  if (!accessToken) return null;
  const res = await fetch(`${COMM_SUPABASE_AUTH_URL}/user`, {
    method: 'GET',
    headers: {
      apikey: COMM_SUPABASE_KEY,
      Authorization: `Bearer ${accessToken}`
    }
  });
  if (!res.ok) return null;
  const data = await res.json().catch(() => null);
  return data;
}

async function refreshSupabaseSession(refreshToken) {
  if (!refreshToken) return null;
  const res = await fetch(`${COMM_SUPABASE_AUTH_URL}/token?grant_type=refresh_token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      apikey: COMM_SUPABASE_KEY,
      Authorization: `Bearer ${COMM_SUPABASE_KEY}`
    },
    body: JSON.stringify({ refresh_token: refreshToken })
  });
  if (!res.ok) return null;
  return res.json().catch(() => null);
}

async function ensureHomeAuthUser() {
  loadHomeAuthSession();
  if (homeGoogleProfile?.email) return;
  if (!homeAuthSession) return;
  let session = homeAuthSession;
  if (isHomeAuthSessionExpired(session) && session.refresh_token) {
    const refreshed = await refreshSupabaseSession(session.refresh_token);
    if (refreshed?.access_token) {
      session = {
        access_token: refreshed.access_token,
        refresh_token: refreshed.refresh_token || session.refresh_token,
        token_type: refreshed.token_type,
        expires_at: refreshed.expires_at || Math.floor(Date.now() / 1000) + Number(refreshed.expires_in || 0)
      };
      saveHomeAuthSession(session);
    }
  }
  const user = await fetchSupabaseUser(session.access_token);
  if (!user?.email) return;
  saveHomeGoogleProfile({
    email: user.email,
    name: user.user_metadata?.full_name || user.user_metadata?.name || user.email,
    logged_in_at: new Date().toISOString(),
    user_id: user.id || null
  });
}

async function signInWithSupabaseIdToken(idToken) {
  const res = await fetch(`${COMM_SUPABASE_AUTH_URL}/token?grant_type=id_token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      apikey: COMM_SUPABASE_KEY,
      Authorization: `Bearer ${COMM_SUPABASE_KEY}`
    },
    body: JSON.stringify({ provider: 'google', id_token: idToken })
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const message = data?.error_description || data?.error || `Login failed (${res.status})`;
    throw new Error(message);
  }
  return data;
}

function setHomeProfileFromUser(user) {
  if (!user?.email) return;
  saveHomeGoogleProfile({
    email: user.email,
    name: user.user_metadata?.full_name || user.user_metadata?.name || user.email,
    logged_in_at: new Date().toISOString(),
    user_id: user.id || null
  });
}

async function signUpWithEmail(email, password) {
  const res = await fetch(`${COMM_SUPABASE_AUTH_URL}/signup`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      apikey: COMM_SUPABASE_KEY,
      Authorization: `Bearer ${COMM_SUPABASE_KEY}`
    },
    body: JSON.stringify({ email, password })
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const message =
      data?.error_description ||
      data?.error ||
      data?.message ||
      data?.msg ||
      (Array.isArray(data?.errors)
        ? data.errors.map((err) => err?.message || err?.msg).filter(Boolean).join(', ')
        : null) ||
      (res.status === 422
        ? 'Sign up failed (422). Check email format, password length, or signups disabled.'
        : `Sign up failed (${res.status})`);
    throw new Error(message);
  }
  return data;
}

async function issueLicenseForEmail(email, userId = null, groupName = null) {
  const payload = { email, group_name: groupName };
  if (userId) payload.user_id = userId;
  const endpoints = [COMM_LICENSE_ISSUE_ENDPOINT, COMM_LICENSE_ISSUE_ENDPOINT_ALT]
    .filter((endpoint, index, list) => endpoint && list.indexOf(endpoint) === index);
  let lastError = null;
  for (const endpoint of endpoints) {
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        apikey: COMM_SUPABASE_KEY,
        Authorization: `Bearer ${COMM_SUPABASE_KEY}`
      },
      body: JSON.stringify(payload)
    });
    const data = await res.json().catch(() => ({}));
    if (res.ok && data?.ok) {
      return data;
    }
    lastError = {
      status: res.status,
      message: data?.error || data?.message || `License email failed (${res.status})`
    };
    if (res.status !== 404) break;
  }
  throw new Error(lastError?.message || `License email failed (${lastError?.status || 'unknown'})`);
}

function formatRoundTimerSeconds(secondsValue) {
  const total = Math.max(0, Math.floor(Number(secondsValue) || 0));
  const hours = Math.floor(total / 3600);
  const minutes = Math.floor((total % 3600) / 60);
  const secs = total % 60;
  if (hours > 0) {
    return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  }
  return `${minutes}:${String(secs).padStart(2, '0')}`;
}

async function signInWithEmail(email, password) {
  const res = await fetch(`${COMM_SUPABASE_AUTH_URL}/token?grant_type=password`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      apikey: COMM_SUPABASE_KEY,
      Authorization: `Bearer ${COMM_SUPABASE_KEY}`
    },
    body: JSON.stringify({ email, password })
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const message =
      data?.error_description ||
      data?.error ||
      data?.message ||
      data?.msg ||
      (Array.isArray(data?.errors)
        ? data.errors.map((err) => err?.message || err?.msg).filter(Boolean).join(', ')
        : null) ||
      `Sign in failed (${res.status})`;
    throw new Error(message);
  }
  return data;
}

async function launchGoogleAuthFlow(authUrl) {
  if (chrome?.identity?.launchWebAuthFlow) {
    return new Promise((resolve, reject) => {
      chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }, (redirectedTo) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }
        resolve(redirectedTo);
      });
    });
  }
  if (!chrome?.runtime?.sendMessage) {
    throw new Error('Chrome identity API not available.');
  }
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type: 'MW_AUTH_GOOGLE', url: authUrl }, (response) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      if (!response?.ok) {
        reject(new Error(response?.error || 'Login failed.'));
        return;
      }
      resolve(response.redirectUrl);
    });
  });
}

async function startSupabaseGoogleLogin() {
  const manifest = chrome.runtime.getManifest();
  const clientId = manifest?.oauth2?.client_id;
  const scopes = Array.isArray(manifest?.oauth2?.scopes)
    ? manifest.oauth2.scopes
    : ['openid', 'email', 'profile'];
  if (!clientId) {
    setHomeAuthStatus('Missing oauth2.client_id in manifest.json.', { error: true });
    return false;
  }
  const redirectUri = `https://${chrome.runtime.id}.chromiumapp.org`;
  const authUrl = new URL('https://accounts.google.com/o/oauth2/auth');
  authUrl.searchParams.set('client_id', clientId);
  authUrl.searchParams.set('response_type', 'id_token');
  authUrl.searchParams.set('access_type', 'offline');
  authUrl.searchParams.set('prompt', 'consent');
  authUrl.searchParams.set('redirect_uri', redirectUri);
  authUrl.searchParams.set('scope', scopes.join(' '));
  const responseUrl = await launchGoogleAuthFlow(authUrl.toString()).catch((err) => {
    setHomeAuthStatus(err?.message || 'Login failed.', { error: true });
    return null;
  });
  if (!responseUrl) return false;
  const url = new URL(responseUrl);
  const params = new URLSearchParams((url.hash || '').replace(/^#/, '') || url.search.replace(/^\?/, ''));
  const idToken = params.get('id_token');
  if (!idToken) {
    const errMsg = params.get('error_description') || params.get('error') || 'Login failed.';
    setHomeAuthStatus(errMsg, { error: true });
    return false;
  }
  try {
    const session = await signInWithSupabaseIdToken(idToken);
    const expiresIn = Number(session.expires_in || 0);
    const expiresAt = Number(session.expires_at) || (expiresIn ? Math.floor(Date.now() / 1000) + expiresIn : null);
    saveHomeAuthSession({
      access_token: session.access_token,
      refresh_token: session.refresh_token,
      token_type: session.token_type,
      expires_at: expiresAt
    });
    if (session?.user?.email) {
      saveHomeGoogleProfile({
        email: session.user.email,
        name: session.user.user_metadata?.full_name || session.user.user_metadata?.name || session.user.email,
        logged_in_at: new Date().toISOString()
      });
    } else {
      await ensureHomeAuthUser();
    }
    setHomeAuthStatus('Logged in with Google.', { error: false });
    updateMenuUserInfo();
    return true;
  } catch (err) {
    setHomeAuthStatus(err?.message || 'Login failed.', { error: true });
    return false;
  }
}

async function logoutSupabaseSession() {
  if (homeAuthSession?.access_token) {
    try {
      await fetch(`${COMM_SUPABASE_AUTH_URL}/logout?scope=global`, {
        method: 'POST',
        headers: {
          apikey: COMM_SUPABASE_KEY,
          Authorization: `Bearer ${homeAuthSession.access_token}`
        }
      });
    } catch (err) {
      /* ignore */
    }
  }
  clearHomeAuthSession();
  setHomeAuthStatus('Logged out.', { error: false });
  updateMenuUserInfo();
}

function isKorrelUser() {
  return commIsKorrel();
}

function isAdminLicenseHolder() {
  if (!homeLicenseState?.valid) return false;
  const group = String(homeLicenseState?.group_name || '').trim().toLowerCase();
  return group === 'admin';
}

function licenseIsActive() {
  return !!homeLicenseState?.valid && hasSupabaseSession();
}

async function fetchLicenseRow(key) {
  const trimmed = String(key || '').trim();
  if (!trimmed) return null;
  const query = [
    'select=license_key,expires_at,active,note,reason,group_name',
    `license_key=eq.${encodeURIComponent(trimmed)}`
  ].join('&');
  const rows = await commFetch(COMM_TABLE_LICENSE_KEYS, query);
  return Array.isArray(rows) && rows.length ? rows[0] : null;
}

async function validateLicenseKey(key) {
  const trimmed = String(key || '').trim();
  if (!trimmed) {
    return { valid: false, reason: 'Enter a license key' };
  }
  try {
    const edgeState = await validateLicenseViaEdge(trimmed);
    if (edgeState) return enforceAuthForLicenseState(enforceKorrelAdminLicense(edgeState));
    const record = await fetchLicenseRow(trimmed);
    if (!record) return { valid: false, reason: 'Unknown license key' };
    if (record.active === false) {
      return { valid: false, reason: record.reason || 'License not active' };
    }
    const expiry = record.expires_at ? new Date(record.expires_at) : null;
    if (expiry && new Date() > expiry) {
      return { valid: false, reason: 'License expired', expires_at: record.expires_at };
    }
    return enforceAuthForLicenseState(enforceKorrelAdminLicense({
      valid: true,
      reason: record.note || 'Active',
      message: record.note || 'Active',
      expires_at: record.expires_at || null,
      group_name: record.group_name || null
    }));
  } catch (err) {
    return { valid: false, reason: 'License check error' };
  }
}

function enforceKorrelAdminLicense(state) {
  if (!state || !state.valid) return state;
  if (!isKorrelUser()) return state;
  const group = String(state.group_name || '').trim().toLowerCase();
  if (group !== 'admin') {
    return { valid: false, reason: 'Korrel requires Admin license' };
  }
  return state;
}

function enforceAuthForLicenseState(state) {
  if (!state || !state.valid) return state;
  if (hasSupabaseSession()) return state;
  return { ...state, reason: 'Login required to activate license' };
}

async function validateLicenseViaEdge(key) {
  try {
    const res = await fetch(COMM_LICENSE_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        apikey: COMM_SUPABASE_KEY,
        Authorization: `Bearer ${COMM_SUPABASE_KEY}`
      },
      body: JSON.stringify({ key })
    });
    const data = await res.json().catch(() => ({}));
    if (res.ok && data?.valid) {
      return {
        valid: true,
        reason: data.message || 'Active',
        message: data.message || 'Active',
        expires_at: data.expires_at || null,
        token: data.token || null,
        group_name: data.group_name || null
      };
    }
    const reason = data?.reason || data?.message || '';
    if (res.status === 404) return { valid: false, reason: 'Unknown license key' };
    if (res.status === 403 && reason) {
      return { valid: false, reason: reason === 'Inactive' ? 'License not active' : reason };
    }
    if (reason) return { valid: false, reason };
    if (!res.ok) return { valid: false, reason: `License check failed (${res.status})` };
    return { valid: false, reason: 'Unknown license key' };
  } catch (err) {
    return null;
  }
}

async function commAttachLicenseToUser(licenseKey) {
  const trimmed = String(licenseKey || '').trim();
  if (!trimmed) return false;
  try {
    await ensureHomeAuthUser();
    const userId = getHomeAuthUserId();
    if (!userId) return false;
    await commInsert(
      COMM_TABLE_LICENSE_ASSIGNMENTS,
      [{ user_id: userId, license_key: trimmed }],
      { upsert: true, onConflict: 'user_id' }
    );
    return true;
  } catch (err) {
    console.warn('[MW] Failed to attach license assignment:', err);
    return false;
  }
}

async function initHomeLicenseState() {
  const stored = localStorage.getItem(COMM_LICENSE_STORAGE_KEY) || '';
  if (!stored) {
    homeLicenseState = { valid: false, reason: 'No license key', expires_at: null, message: '' };
    applyLicenseGate();
    return false;
  }
  const state = await validateLicenseKey(stored);
  if (state?.valid) {
    await commAttachLicenseToUser(stored);
  }
  homeLicenseState = state;
  if (state?.token) {
    try {
      localStorage.setItem(COMM_LICENSE_TOKEN_KEY, state.token);
    } catch (err) {
      /* ignore */
    }
  }
  applyLicenseGate();
  return !!state.valid;
}

function applyLicenseGate() {
  if (!tabButtons || !tabButtons.length) return;
  tabButtons.forEach((btn) => {
    const isHome = btn.dataset.tab === 'home';
    btn.disabled = !licenseIsActive() && !isHome;
  });
  updateHomeTabLabel();
  updateMenuUserInfo();
  if (licenseIsActive()) {
    if (!autoDefaultTabApplied) {
      autoDefaultTabApplied = true;
      activateTab('live');
    }
  } else {
    autoDefaultTabApplied = false;
    activateTab('home');
  }
  updateFloatingChatVisibility();
}

async function fetchHomeNews() {
  try {
    const rows = await commFetch(
      COMM_TABLE_HOME_NEWS,
      'select=id,title,body,updated_at,updated_by&order=updated_at.desc&limit=1'
    );
    homeNewsEntry = Array.isArray(rows) && rows.length ? rows[0] : null;
  } catch (err) {
    homeNewsEntry = null;
  }
}

async function fetchAppNotice() {
  try {
    const rows = await commFetch(
      COMM_TABLE_APP_NOTICE,
      'select=id,message,enabled,updated_at,updated_by&order=updated_at.desc&limit=1'
    );
    appNoticeEntry = Array.isArray(rows) && rows.length ? rows[0] : null;
  } catch (err) {
    appNoticeEntry = null;
  }
}

function renderAppNotice() {
  const bar = document.getElementById('appNoticeBar');
  if (!bar) return;
  const message = appNoticeEntry?.message ? String(appNoticeEntry.message).trim() : '';
  const enabled = appNoticeEntry?.enabled === true;
  if (enabled && message) {
    bar.textContent = message;
    bar.style.display = 'block';
  } else {
    bar.textContent = '';
    bar.style.display = 'none';
  }
}

async function saveHomeNews({ title, body }) {
  const payload = [{
    id: homeNewsEntry?.id || 1,
    title,
    body,
    updated_by: commSession?.memberName || homeGoogleProfile?.email || null
  }];
  await commInsert(COMM_TABLE_HOME_NEWS, payload, { upsert: true, onConflict: 'id' });
  await fetchHomeNews();
}

async function fetchHomePricing() {
  try {
    const rows = await commFetch(
      COMM_TABLE_LICENSE_PRICING,
      'select=id,sku,title,description,price_gmt,duration_days,payment_wallet,active,sort_order&order=sort_order.asc'
    );
    homePricingRows = Array.isArray(rows) ? rows.filter((row) => row?.active !== false) : [];
  } catch (err) {
    homePricingRows = [];
  }
}

function commGetKorrelLogContextPrefix(context) {
  const cycleId = Number.isFinite(context?.cycleId)
    ? Math.trunc(context.cycleId)
    : getCycleNumber(new Date());
  const leagueId = Number.isFinite(context?.leagueId)
    ? Math.trunc(context.leagueId)
    : commGetKorrelActiveLeagueId();
  const cycleLabel = Number.isFinite(cycleId) ? `C${cycleId}` : 'C?';
  let leagueLabel = 'L?';
  if (Number.isFinite(leagueId)) {
    const formatted = commFormatLeagueDisplay(leagueId);
    leagueLabel = formatted && formatted !== '-' ? formatted : `L${Math.trunc(leagueId)}`;
  }
  return `[${cycleLabel} ${leagueLabel}] `;
}

function commInferKorrelLogContext(message) {
  if (!message) return null;
  const text = String(message);
  const cycleMatch = text.match(/\bcycle\s+(\d+)\b/i);
  let leagueMatch = text.match(/\bleague\s+(\d+)\b/i);
  if (!leagueMatch) {
    leagueMatch = text.match(/\bleagues?\s*[:\-]?\s*(\d+)\b/i);
  }
  if (!leagueMatch) {
    leagueMatch = text.match(/\bleagueId\b[^0-9]*(\d+)/i);
  }
  let cycleMatchAlt = null;
  if (!cycleMatch) {
    cycleMatchAlt = text.match(/\bcycleId\b[^0-9]*(\d+)/i);
  }
  const context = {};
  if (cycleMatch) {
    const cycleId = commParseNumber(cycleMatch[1]);
    if (Number.isFinite(cycleId)) context.cycleId = Math.trunc(cycleId);
  } else if (cycleMatchAlt) {
    const cycleIdAlt = commParseNumber(cycleMatchAlt[1]);
    if (Number.isFinite(cycleIdAlt)) context.cycleId = Math.trunc(cycleIdAlt);
  }
  if (leagueMatch) {
    const leagueId = commParseNumber(leagueMatch[1]);
    if (Number.isFinite(leagueId)) context.leagueId = Math.trunc(leagueId);
  }
  return Object.keys(context).length ? context : null;
}

function commSetKorrelLog(
  message,
  { error = false, success = false, append = true, persist = true, source = 'local', context = null, showContext = true } = {}
) {
  const hasExistingPrefix = /\[C\?|\[C\d+/i.test(message || '');
  const inferred = !context && showContext && !hasExistingPrefix ? commInferKorrelLogContext(message) : null;
  const resolvedContext = context || inferred || (showContext && !hasExistingPrefix ? commKorrelLogContextOverride : null);
  const prefix = message && showContext && !hasExistingPrefix ? commGetKorrelLogContextPrefix(resolvedContext) : '';
  const outputMessage = message ? `${prefix}${message}` : message;
  if (persist && outputMessage) {
    commPersistKorrelLog(outputMessage, { error, success, source }).catch(() => {});
  }
  const log = document.getElementById('commKorrelLog');
  if (!log) return;
  if (!outputMessage) {
    log.innerHTML = '';
    return;
  }
  const stamp = new Date().toLocaleTimeString([], { hour12: false });
  const klass = error ? 'error' : success ? 'success' : '';
  const line = `<div class="comm-log-line ${klass}"><span class="comm-log-time">${stamp}</span>${commEscape(outputMessage)}</div>`;
  log.innerHTML = append ? line + log.innerHTML : line;
}

function commGetKorrelLogIdentity() {
  const profile = homeGoogleProfile || loadHomeGoogleProfile();
  if (profile?.email) {
    const email = String(profile.email).trim().toLowerCase();
    return { userId: `email:${email}`, label: profile.email };
  }
  if (commSession?.memberName) {
    const memberName = String(commSession.memberName).trim();
    return { userId: `member:${memberName.toLowerCase()}`, label: memberName };
  }
  let anonId = '';
  try {
    anonId = localStorage.getItem(COMM_KORREL_LOG_USER_KEY) || '';
    if (!anonId) {
      const generated = (typeof crypto !== 'undefined' && crypto.randomUUID)
        ? crypto.randomUUID()
        : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
      anonId = `anon:${generated}`;
      localStorage.setItem(COMM_KORREL_LOG_USER_KEY, anonId);
    }
  } catch (err) {
    anonId = anonId || `anon:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  }
  const label = anonId.replace(/^anon:/, 'anon-').slice(-12);
  return { userId: anonId, label };
}

function commGetApiUserIdentity() {
  const profile = homeGoogleProfile || loadHomeGoogleProfile();
  const licenseKey = String(localStorage.getItem(COMM_LICENSE_STORAGE_KEY) || '').trim() || null;
  if (profile?.user_id) {
    return {
      userId: `auth:${String(profile.user_id).trim()}`,
      userLabel: profile.email || profile.name || profile.user_id,
      licenseKey
    };
  }
  if (profile?.email) {
    const email = String(profile.email).trim().toLowerCase();
    return { userId: `email:${email}`, userLabel: profile.email, licenseKey };
  }
  const fallback = commGetKorrelLogIdentity();
  return { userId: fallback.userId, userLabel: fallback.label, licenseKey };
}

function commGetRoundLockOwnerKey() {
  const identity = commGetKorrelLogIdentity();
  if (!identity?.userId) return null;
  return identity.userId;
}

async function commRecordApiCall({ endpoint, status, ok, callCount = 1, cycleId, leagueId } = {}) {
  if (!endpoint) return;
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) return;
  const payload = [{
    user_id: identity.userId,
    user_label: identity.userLabel || null,
    license_key: identity.licenseKey || null,
    endpoint: String(endpoint),
    status: Number.isFinite(commParseNumber(status)) ? Math.trunc(commParseNumber(status)) : null,
    ok: typeof ok === 'boolean' ? ok : null,
    call_count: Math.max(1, Math.trunc(commParseNumber(callCount) || 1)),
    cycle_id: Number.isFinite(commParseNumber(cycleId)) ? Math.trunc(commParseNumber(cycleId)) : null,
    league_id: Number.isFinite(commParseNumber(leagueId)) ? Math.trunc(commParseNumber(leagueId)) : null,
    created_at: new Date().toISOString()
  }];
  try {
    await commInsert(COMM_TABLE_API_CALL_LOG, payload);
  } catch (err) {
    commApiLogInsertFailedAt = Date.now();
    commApiLogInsertError = err?.message || String(err);
    if (commIsKorrel()) {
      const now = Date.now();
      if (now - commApiLogInsertWarnedAt > 60000) {
        commApiLogInsertWarnedAt = now;
        commSetKorrelLog(`API usage logging failed: ${commApiLogInsertError}`, { append: true, error: true });
      }
    }
  }
}

async function commPersistKorrelLog(message, { error = false, success = false, source = 'local' } = {}) {
  const identity = commGetKorrelLogIdentity();
  if (!identity?.userId) return;
  const level = error ? 'error' : success ? 'success' : 'info';
  const payload = [{
    user_id: identity.userId,
    user_label: identity.label || null,
    level,
    source: source || null,
    message,
    created_at: new Date().toISOString()
  }];
  try {
    await commInsert(COMM_TABLE_KORREL_LOGS, payload);
  } catch (err) {
    return;
  }
  commTrimKorrelLogs(identity.userId).catch(() => {});
}

async function commTrimKorrelLogs(userId) {
  if (!userId) return;
  const lastTrim = commKorrelLogTrimAt.get(userId) || 0;
  if (Date.now() - lastTrim < 60000) return;
  commKorrelLogTrimAt.set(userId, Date.now());
  const query = [
    `select=id`,
    `user_id=eq.${encodeURIComponent(userId)}`,
    'order=created_at.desc',
    `offset=${COMM_KORREL_LOG_MAX_PER_USER}`,
    'limit=200'
  ].join('&');
  const rows = await commFetch(COMM_TABLE_KORREL_LOGS, query);
  const ids = Array.isArray(rows) ? rows.map((row) => row?.id).filter(Boolean) : [];
  if (!ids.length) return;
  await commDelete(COMM_TABLE_KORREL_LOGS, `id=in.(${ids.join(',')})`);
}

async function commFetchKorrelRemoteLogs() {
  if (!commIsKorrel()) return;
  if (commKorrelRemoteLogFetching) return;
  commKorrelRemoteLogFetching = true;
  try {
    const identity = commGetKorrelLogIdentity();
    const localId = identity?.userId || null;
    const queryParts = [
      'select=id,user_id,user_label,level,source,message,created_at',
      'order=id.asc',
      `limit=${COMM_KORREL_LOG_FETCH_LIMIT}`
    ];
    if (commKorrelRemoteLogLastId) {
      queryParts.unshift(`id=gt.${commKorrelRemoteLogLastId}`);
    }
    const rows = await commFetch(COMM_TABLE_KORREL_LOGS, queryParts.join('&'));
    if (!Array.isArray(rows) || !rows.length) return;
    rows.forEach((row) => {
      if (localId && row.user_id === localId) {
        commKorrelRemoteLogLastId = Math.max(commKorrelRemoteLogLastId, row.id || 0);
        return;
      }
      const label = row.user_label || row.user_id || 'user';
      const level = String(row.level || '').toLowerCase();
      const prefix = `[${label}] `;
      commSetKorrelLog(`${prefix}${row.message}`, {
        error: level === 'error',
        success: level === 'success',
        append: true,
        persist: false,
        source: 'remote'
      });
      commKorrelRemoteLogLastId = Math.max(commKorrelRemoteLogLastId, row.id || 0);
    });
  } catch (err) {
    // ignore
  } finally {
    commKorrelRemoteLogFetching = false;
  }
}

function commGetKorrelActiveLeagueId() {
  const override = Number.isFinite(commKorrelApiLeagueOverride)
    ? commKorrelApiLeagueOverride
    : commKorrelCurrentLeagueId;
  return Number.isFinite(override) ? Math.trunc(override) : null;
}

async function commLoadApiLimitsFromDb({ force = false } = {}) {
  const now = Date.now();
  if (!force && commApiLimitFetching) return commApiLimitConfig;
  if (!force && commApiLimitLastFetchAt && now - commApiLimitLastFetchAt < COMM_API_LIMIT_REFRESH_MS) {
    return commApiLimitConfig;
  }
  commApiLimitFetching = true;
  try {
    const rows = await commFetch(
      COMM_TABLE_API_LIMITS,
      'select=min_interval_ms,max_interval_ms,max_calls_per_10m,updated_at&order=updated_at.desc&limit=1'
    );
    const row = Array.isArray(rows) ? rows[0] : null;
    if (row) {
      const minRaw = commParseNumber(row.min_interval_ms);
      const maxRaw = commParseNumber(row.max_interval_ms);
      const maxCallsRaw = commParseNumber(row.max_calls_per_10m);
      const minIntervalMs = Number.isFinite(minRaw) ? Math.trunc(minRaw) : null;
      const maxIntervalMs = Number.isFinite(maxRaw) ? Math.trunc(maxRaw) : null;
      const maxCallsPer10Min = Number.isFinite(maxCallsRaw) ? Math.trunc(maxCallsRaw) : null;
      commApiLimitConfig = {
        minIntervalMs,
        maxIntervalMs: Number.isFinite(maxIntervalMs) ? Math.max(maxIntervalMs, minIntervalMs || 0) : maxIntervalMs,
        maxCallsPer10Min
      };
    }
  } catch (err) {
    // ignore
  } finally {
    commApiLimitFetching = false;
    commApiLimitLastFetchAt = now;
  }
  return commApiLimitConfig;
}

function commInferApiCallContext(endpoint, payload = {}, options = {}) {
  const cycleFromPayload = commParseNumber(payload?.cycleId ?? payload?.cycle_id);
  const leagueFromPayload = commParseNumber(payload?.leagueId ?? payload?.league_id);
  const cycleFallback = commParseNumber(options?.cycleId)
    ?? commParseNumber(commKorrelApiCycleOverride)
    ?? commParseNumber(getCycleNumber(new Date()));
  const leagueFallback = commParseNumber(options?.leagueId)
    ?? commParseNumber(commKorrelApiLeagueOverride)
    ?? commParseNumber(commKorrelCurrentLeagueId);
  return {
    cycleId: Number.isFinite(cycleFromPayload) ? Math.trunc(cycleFromPayload) : (Number.isFinite(cycleFallback) ? Math.trunc(cycleFallback) : null),
    leagueId: Number.isFinite(leagueFromPayload) ? Math.trunc(leagueFromPayload) : (Number.isFinite(leagueFallback) ? Math.trunc(leagueFallback) : null),
    endpoint
  };
}

function commSetApiLimitWait(waitMs, endpoint) {
  if (!Number.isFinite(waitMs) || waitMs <= 0) return;
  commApiLimitWaitUntil = Date.now() + waitMs;
  commApiLimitWaitEndpoint = endpoint || '';
  if (commApiLimitCountdownTimer) return;
  commApiLimitCountdownTimer = setInterval(() => {
    if (!commApiLimitWaitUntil || Date.now() >= commApiLimitWaitUntil) {
      clearInterval(commApiLimitCountdownTimer);
      commApiLimitCountdownTimer = null;
      commApiLimitWaitUntil = 0;
      commApiLimitWaitEndpoint = '';
      if (typeof window.__refreshCurrentRoundCard === 'function') {
        window.__refreshCurrentRoundCard();
      }
      return;
    }
    if (typeof window.__refreshCurrentRoundCard === 'function') {
      window.__refreshCurrentRoundCard();
    }
  }, 1000);
}

async function commEnforceApiCallLimit({ endpoint, cost = 1, context } = {}) {
  await commLoadApiLimitsFromDb();
  const limitRaw = commParseNumber(commApiLimitConfig?.maxCallsPer10Min);
  if (!Number.isFinite(limitRaw) || limitRaw <= 0) return;
  const identity = commGetApiUserIdentity();
  const userId = identity?.userId || 'anon';
  if (commApiCallWindowUser !== userId) {
    commApiCallWindowUser = userId;
    commApiCallWindow = [];
  }
  const now = Date.now();
  commApiCallWindow = commApiCallWindow.filter((ts) => now - ts < COMM_API_CALL_WINDOW_MS);
  const calls = Math.max(1, Math.trunc(commParseNumber(cost) || 1));
  if (commApiCallWindow.length + calls <= limitRaw) {
    for (let i = 0; i < calls; i += 1) {
      commApiCallWindow.push(now);
    }
    return;
  }
  const oldest = commApiCallWindow[0] || now;
  const waitMs = Math.max(0, COMM_API_CALL_WINDOW_MS - (now - oldest));
  if (waitMs > 0) {
    const seconds = Math.ceil(waitMs / 1000);
    if (commIsKorrel()) {
      commSetKorrelLog(`API rate limit: waiting ${seconds}s (${endpoint || 'api'}).`, {
        append: true,
        context
      });
    }
    commSetApiLimitWait(waitMs, endpoint);
    await commDelay(waitMs);
  }
  const newNow = Date.now();
  commApiCallWindow = commApiCallWindow.filter((ts) => newNow - ts < COMM_API_CALL_WINDOW_MS);
  for (let i = 0; i < calls; i += 1) {
    commApiCallWindow.push(newNow);
  }
}

function commPickRandomIntervalMs() {
  const configMin = commParseNumber(commApiLimitConfig?.minIntervalMs);
  const configMax = commParseNumber(commApiLimitConfig?.maxIntervalMs);
  const minMs = Math.max(
    0,
    Number.isFinite(configMin) ? Math.trunc(configMin) : COMM_KORREL_AUTO_MIN_INTERVAL_MS
  );
  const maxMs = Math.max(
    minMs,
    Number.isFinite(configMax) ? Math.trunc(configMax) : COMM_KORREL_AUTO_MAX_INTERVAL_MS
  );
  const span = maxMs - minMs;
  return minMs + Math.floor(Math.random() * (span + 1));
}

async function commRefreshKorrelStoredBlockNumber({ force = false } = {}) {
  const cycleId = getCycleNumber(new Date());
  if (!Number.isFinite(cycleId)) return null;
  const now = Date.now();
  const leagueId = commGetKorrelActiveLeagueId();
  if (!Number.isFinite(leagueId)) {
    return commKorrelLastStoredBlockNumber;
  }
  if (
    !force &&
    commKorrelLastStoredCycleId === cycleId &&
    now - commKorrelLastStoredCheckAt < 60000
  ) {
    return commKorrelLastStoredBlockNumber;
  }
  try {
    const queryParts = [
      'select=block_number,cycle_id',
      `cycle_id=eq.${encodeURIComponent(cycleId)}`,
      'order=block_number.desc',
      'limit=1'
    ];
    queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`);
    const query = queryParts.join('&');
    const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, query);
    const lastBlock = commParseNumber(rows?.[0]?.block_number);
    commKorrelLastStoredBlockNumber = Number.isFinite(lastBlock) ? lastBlock : null;
    commKorrelLastStoredCycleId = cycleId;
    commKorrelLastStoredCheckAt = now;
    return commKorrelLastStoredBlockNumber;
  } catch (err) {
    return commKorrelLastStoredBlockNumber;
  }
}

function commComputeKorrelRoundsLeft() {
  const currentBlock = commGetCurrentBlockNumber();
  const cycleId = getCycleNumber(new Date());
  if (
    Number.isFinite(currentBlock) &&
    Number.isFinite(commKorrelLastStoredBlockNumber) &&
    commKorrelLastStoredCycleId === cycleId &&
    currentBlock >= commKorrelLastStoredBlockNumber
  ) {
    const diff = currentBlock - commKorrelLastStoredBlockNumber;
    return {
      remaining: Math.max(0, COMM_KORREL_ROUND_SNAPSHOT_INTERVAL - diff),
      diff,
      currentBlock,
      storedBlock: commKorrelLastStoredBlockNumber,
      usingDb: true
    };
  }
  return {
    remaining: Math.max(0, COMM_KORREL_ROUND_SNAPSHOT_INTERVAL - commKorrelRoundCount),
    diff: null,
    currentBlock,
    storedBlock: commKorrelLastStoredBlockNumber,
    usingDb: false
  };
}

function commUpdateKorrelBackfillCountdown() {
  const elements = [
    document.getElementById('commKorrelBackfillCall'),
    document.getElementById('commKorrelOtherLeagueCall')
  ].filter(Boolean);
  if (!elements.length) return;
  const setText = (text) => {
    elements.forEach((el) => { el.textContent = text; });
  };
  if (!commSession || !commIsKorrel()) {
    setText('');
    return;
  }
  if (commKorrelBackfillRunning || commKorrelOtherLeagueRunning) {
    setText('Backfill update: running...');
    return;
  }
  const nextAt = commKorrelBackfillNextAt || commKorrelOtherLeagueNextAt;
  if (nextAt) {
    const remainingMs = nextAt - Date.now();
    const remainingSec = Math.max(0, Math.ceil(remainingMs / 1000));
    if (remainingSec > 0) {
      setText(`Backfill update in ${commFormatDurationShort(remainingMs)}`);
      return;
    }
  }
  setText('Backfill update: ready');
}

function commUpdateKorrelOtherLeagueCountdown() {
  commUpdateKorrelBackfillCountdown();
}

function commUpdateKorrelCountdown() {
  const el = document.getElementById('commKorrelNextCall');
  if (!el) return;
  if (!commSession || !commIsKorrel()) {
    el.textContent = '';
    commUpdateKorrelBackfillCountdown();
    return;
  }
  if (commKorrelRoundSnapshotPending) {
    el.textContent = 'Next auto snapshot: running...';
    commUpdateKorrelBackfillCountdown();
    return;
  }
  if (commKorrelNextSnapshotAt) {
    const remainingMs = commKorrelNextSnapshotAt - Date.now();
    const remainingSec = Math.max(0, Math.ceil(remainingMs / 1000));
    el.textContent = remainingSec > 0
      ? `Next auto snapshot in ${commFormatDurationShort(remainingMs)}`
      : 'Next auto snapshot: starting...';
    commUpdateKorrelBackfillCountdown();
    return;
  }
  const { remaining: remainingRounds } = commComputeKorrelRoundsLeft();
  if (remainingRounds > 0 && remainingRounds < COMM_KORREL_ROUND_SNAPSHOT_INTERVAL) {
    el.textContent = `Next auto snapshot after ${remainingRounds} round${remainingRounds === 1 ? '' : 's'}`;
    commUpdateKorrelBackfillCountdown();
    return;
  }
  el.textContent = `Next auto snapshot after ${COMM_KORREL_ROUND_SNAPSHOT_INTERVAL} rounds`;
  commUpdateKorrelBackfillCountdown();
}

function commChunkArray(list, size) {
  const result = [];
  if (!Array.isArray(list) || list.length === 0) return result;
  const chunkSize = Math.max(1, size || 1);
  for (let i = 0; i < list.length; i += chunkSize) {
    result.push(list.slice(i, i + chunkSize));
  }
  return result;
}

async function commUpsertRoundCycleRows(rows) {
  const batches = commChunkArray(rows, 10);
  let total = 0;
  for (const batch of batches) {
    try {
      await commInsert(COMM_TABLE_ROUND_CYCLE_API, batch, { upsert: true, onConflict: 'api_round_id' });
      total += batch.length;
    } catch (err) {
      const msg = String(err?.message || err);
      if (msg.includes('42P10') || msg.toLowerCase().includes('on conflict')) {
        commSetKorrelLog('Upsert unavailable, retrying insert only...', { error: true });
        await commInsert(COMM_TABLE_ROUND_CYCLE_API, batch);
        total += batch.length;
      } else if (msg.includes('23505') || msg.toLowerCase().includes('duplicate key')) {
        commSetKorrelLog('Duplicate key detected, patching existing rows...', { error: true });
        for (const row of batch) {
          if (!row.api_round_id) continue;
          const query = `api_round_id=eq.${encodeURIComponent(row.api_round_id)}`;
          await commPatch(COMM_TABLE_ROUND_CYCLE_API, query, row);
        }
        total += batch.length;
      } else {
        throw err;
      }
    }
  }
  return total;
}

function commComputeReliablePps(samples, tolerance = 0.05, minCount = 2) {
  const values = (Array.isArray(samples) ? samples : [])
    .map((value) => Number(value))
    .filter((value) => Number.isFinite(value) && value > 0);
  if (!values.length) return { value: null, count: 0 };
  let bestCount = 0;
  let bestCluster = [];
  values.forEach((anchor) => {
    const cluster = values.filter((value) => Math.abs(value - anchor) / anchor <= tolerance);
    if (cluster.length > bestCount) {
      bestCount = cluster.length;
      bestCluster = cluster;
    }
  });
  if (bestCount < minCount) return { value: null, count: bestCount };
  const sum = bestCluster.reduce((acc, val) => acc + val, 0);
  return { value: bestCluster.length ? sum / bestCluster.length : null, count: bestCount };
}

async function commUpsertClanBlockPpsRows(rows) {
  const validRows = Array.isArray(rows)
    ? rows.filter((row) =>
      Number.isFinite(commParseNumber(row?.block_id))
      && Number.isFinite(commParseNumber(row?.clan_id))
      && Number.isFinite(commParseNumber(row?.cycle_id))
      && Number.isFinite(commParseNumber(row?.league_id))
      && Number.isFinite(commParseNumber(row?.pps_1m ?? row?.pps_full)))
    : [];
  if (!validRows.length) return 0;

  const nowIso = new Date().toISOString();
  const payload = validRows.map((row) => {
    const ppsValue = Number.isFinite(commParseNumber(row?.pps_1m))
      ? commParseNumber(row.pps_1m)
      : commParseNumber(row?.pps_full);
    const ppsRounded = Math.round(ppsValue * 100) / 100;
    return {
      cycle_id: Math.trunc(commParseNumber(row.cycle_id)),
      league_id: Math.trunc(commParseNumber(row.league_id)),
      block_id: Math.trunc(commParseNumber(row.block_id)),
      clan_id: Math.trunc(commParseNumber(row.clan_id)),
      clan_name: row.clan_name || null,
      pps_1m: ppsRounded,
      pps_full: Number.isFinite(commParseNumber(row?.pps_full)) ? commParseNumber(row.pps_full) : ppsRounded,
      snapshots: Number.isFinite(commParseNumber(row?.snapshots)) ? Math.trunc(commParseNumber(row.snapshots)) : 1,
      last_seen_at: row.last_seen_at || nowIso,
      source: row.source || 'api'
    };
  });

  const batches = commChunkArray(payload, 50);
  let total = 0;
  for (const batch of batches) {
    await commInsert(COMM_TABLE_CLAN_BLOCK_PPS, batch, {
      upsert: true,
      onConflict: 'cycle_id,league_id,block_id,clan_id'
    });
    total += batch.length;
  }
  return total;
}

async function commInsertClanBlockPpsSamples(rows) {
  const validRows = Array.isArray(rows)
    ? rows.filter((row) =>
      Number.isFinite(commParseNumber(row?.block_id))
      && Number.isFinite(commParseNumber(row?.clan_id))
      && Number.isFinite(commParseNumber(row?.cycle_id))
      && Number.isFinite(commParseNumber(row?.league_id))
      && Number.isFinite(commParseNumber(row?.pps_sample)))
    : [];
  if (!validRows.length) return 0;

  const nowIso = new Date().toISOString();
  const grouped = new Map();
  validRows.forEach((row) => {
    const cycleId = Math.trunc(commParseNumber(row.cycle_id));
    const leagueId = Math.trunc(commParseNumber(row.league_id));
    const blockId = Math.trunc(commParseNumber(row.block_id));
    const key = `${cycleId}:${leagueId}:${blockId}`;
    if (!grouped.has(key)) {
      grouped.set(key, {
        cycleId,
        leagueId,
        blockId,
        rows: []
      });
    }
    grouped.get(key).rows.push(row);
  });

  const upsertRows = [];
  for (const group of grouped.values()) {
    const clanIds = Array.from(new Set(group.rows.map((row) => Math.trunc(commParseNumber(row.clan_id)))));
    if (!clanIds.length) continue;
    const selectFields = [
      'clan_id',
      'clan_name',
      'pps_1m',
      'pps_full',
      'snapshots',
      'first_seen_at',
      'last_seen_at'
    ];
    if (commClanBlockPpsSampleColumnsAvailable !== false) {
      selectFields.push('pps_samples', 'pps_samples_at');
    }
    const select = selectFields.join(',');
    const query = [
      `select=${select}`,
      `cycle_id=eq.${encodeURIComponent(group.cycleId)}`,
      `league_id=eq.${encodeURIComponent(group.leagueId)}`,
      `block_id=eq.${encodeURIComponent(group.blockId)}`,
      `clan_id=in.(${clanIds.join(',')})`
    ].join('&');
    let existingRows = [];
    try {
      existingRows = await commFetch(COMM_TABLE_CLAN_BLOCK_PPS, query);
    } catch (err) {
      const msg = String(err?.message || err);
      if (
        commClanBlockPpsSampleColumnsAvailable !== false &&
        (msg.includes('pps_samples') || msg.includes('pps_samples_at') || msg.includes('42703'))
      ) {
        commClanBlockPpsSampleColumnsAvailable = false;
        const fallbackSelect = [
          'clan_id',
          'clan_name',
          'pps_1m',
          'pps_full',
          'snapshots',
          'first_seen_at',
          'last_seen_at'
        ].join(',');
        const fallbackQuery = [
          `select=${fallbackSelect}`,
          `cycle_id=eq.${encodeURIComponent(group.cycleId)}`,
          `league_id=eq.${encodeURIComponent(group.leagueId)}`,
          `block_id=eq.${encodeURIComponent(group.blockId)}`,
          `clan_id=in.(${clanIds.join(',')})`
        ].join('&');
        existingRows = await commFetch(COMM_TABLE_CLAN_BLOCK_PPS, fallbackQuery).catch(() => []);
      } else {
        throw err;
      }
    }
    const existingMap = new Map();
    (Array.isArray(existingRows) ? existingRows : []).forEach((row) => {
      const id = commParseNumber(row?.clan_id);
      if (!Number.isFinite(id)) return;
      existingMap.set(Math.trunc(id), row);
    });

    group.rows.forEach((row) => {
      const clanId = Math.trunc(commParseNumber(row.clan_id));
      const sampleValue = Number(row.pps_sample);
      const existing = existingMap.get(clanId);
      let samples = Array.isArray(existing?.pps_samples) ? [...existing.pps_samples] : [];
      let sampleTimes = Array.isArray(existing?.pps_samples_at) ? [...existing.pps_samples_at] : [];
      const minLen = Math.min(samples.length, sampleTimes.length);
      if (minLen > 0) {
        samples = samples.slice(-minLen);
        sampleTimes = sampleTimes.slice(-minLen);
      }
      samples.push(sampleValue);
      sampleTimes.push(nowIso);
      if (samples.length > CLAN_BLOCK_PPS_SAMPLE_LIMIT) {
        samples = samples.slice(-CLAN_BLOCK_PPS_SAMPLE_LIMIT);
        sampleTimes = sampleTimes.slice(-CLAN_BLOCK_PPS_SAMPLE_LIMIT);
      }
      const reliable = commComputeReliablePps(samples, 0.05, 2);
      const reliableValue = Number.isFinite(reliable.value)
        ? Math.round(reliable.value * 100) / 100
        : null;
      const clanName = row.clan_name || existing?.clan_name || null;
      const upsertRow = {
        cycle_id: group.cycleId,
        league_id: group.leagueId,
        block_id: group.blockId,
        clan_id: clanId,
        clan_name: clanName,
        pps_1m: sampleValue,
        pps_full: reliableValue ?? (Number.isFinite(commParseNumber(existing?.pps_full)) ? existing.pps_full : null),
        snapshots: samples.length,
        first_seen_at: existing?.first_seen_at || nowIso,
        last_seen_at: nowIso,
        source: existing?.source || 'api'
      };
      if (commClanBlockPpsSampleColumnsAvailable !== false) {
        upsertRow.pps_samples = samples;
        upsertRow.pps_samples_at = sampleTimes;
      }
      upsertRows.push(upsertRow);
    });
  }

  const batches = commChunkArray(upsertRows, 50);
  let total = 0;
  for (const batch of batches) {
    try {
      await commInsert(COMM_TABLE_CLAN_BLOCK_PPS, batch, {
        upsert: true,
        onConflict: 'cycle_id,league_id,block_id,clan_id'
      });
      total += batch.length;
    } catch (err) {
      console.warn('[MW] clan_block_pps upsert failed:', err?.message || err, {
        sample: batch[0]
      });
      throw err;
    }
  }
  return total;
}

function commStartKorrelCountdownTimer() {
  if (commKorrelCountdownTimer) {
    clearInterval(commKorrelCountdownTimer);
    commKorrelCountdownTimer = null;
  }
  if (commKorrelStatusTimer) {
    clearInterval(commKorrelStatusTimer);
    commKorrelStatusTimer = null;
  }
  if (!commSession || !commIsKorrel()) return;
  commRefreshKorrelStoredBlockNumber()
    .then(() => commUpdateKorrelCountdown())
    .catch(() => commUpdateKorrelCountdown());
  commKorrelCountdownTimer = setInterval(() => {
    commUpdateKorrelCountdown();
  }, 1000);
}

function commFormatDurationShort(ms) {
  if (!Number.isFinite(ms) || ms <= 0) return '0s';
  const totalSeconds = Math.ceil(ms / 1000);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  if (hours > 0) {
    return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
  }
  if (minutes > 0) {
    return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
  }
  return `${seconds}s`;
}

function commGetRosterCompletionSelectFields() {
  const base = 'cycle_id,league_id,user_id,checked_at,total_targets,stored_clans,stored_members,missing_count,api_calls';
  if (commRosterCompletionColumnsAvailable === false) return base;
  return `${base},full_run_at,last_clan_at`;
}

function commNormalizeRosterCompletionRow(row) {
  if (!row) return null;
  const cycleId = commParseNumber(row.cycle_id ?? row.cycleId);
  const leagueId = commParseNumber(row.league_id ?? row.leagueId);
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return null;
  const checkedRaw = row.checked_at ?? row.checkedAt ?? null;
  const checkedAt = checkedRaw ? new Date(checkedRaw).getTime() : 0;
  const fullRaw = row.full_run_at ?? row.fullRunAt ?? null;
  const fullRunAt = fullRaw ? new Date(fullRaw).getTime() : 0;
  const clanRaw = row.last_clan_at ?? row.lastClanAt ?? null;
  const lastClanAt = clanRaw ? new Date(clanRaw).getTime() : 0;
  return {
    cycleId: Math.trunc(cycleId),
    leagueId: Math.trunc(leagueId),
    userId: row.user_id ?? row.userId ?? null,
    checkedAt: Number.isFinite(checkedAt) ? checkedAt : 0,
    fullRunAt: Number.isFinite(fullRunAt) ? fullRunAt : 0,
    lastClanAt: Number.isFinite(lastClanAt) ? lastClanAt : 0
  };
}

async function commFetchRosterCompletionState(cycleId, leagueId, { forceBasic = false } = {}) {
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) return null;
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return null;
  const selectFields = forceBasic
    ? 'cycle_id,league_id,user_id,checked_at,total_targets,stored_clans,stored_members,missing_count,api_calls'
    : commGetRosterCompletionSelectFields();
  const query = [
    `select=${selectFields}`,
    `cycle_id=eq.${Math.trunc(cycleId)}`,
    `league_id=eq.${Math.trunc(leagueId)}`,
    `user_id=eq.${encodeURIComponent(identity.userId)}`,
    'limit=1'
  ].join('&');
  try {
    const rows = await commFetch(COMM_TABLE_ROSTER_COMPLETION, query);
    const row = Array.isArray(rows) ? rows[0] : null;
    if (!forceBasic && commRosterCompletionColumnsAvailable === null) {
      commRosterCompletionColumnsAvailable = true;
    }
    return commNormalizeRosterCompletionRow(row);
  } catch (err) {
    const msg = String(err?.message || '');
    if (!forceBasic && (msg.toLowerCase().includes('full_run_at') || msg.toLowerCase().includes('last_clan_at'))) {
      commRosterCompletionColumnsAvailable = false;
      return commFetchRosterCompletionState(cycleId, leagueId, { forceBasic: true });
    }
    return null;
  }
}

function commGetRosterFullWaitMs(state, now = Date.now()) {
  if (!state) return 0;
  const fullAt = state.fullRunAt || 0;
  const clanAt = state.lastClanAt || 0;
  const fullWait = fullAt > 0 ? COMM_KORREL_ROSTER_FULL_COOLDOWN_MS - (now - fullAt) : 0;
  const clanWait = clanAt > 0 ? COMM_KORREL_ROSTER_FULL_MIN_INTERVAL_MS - (now - clanAt) : 0;
  return Math.max(0, fullWait, clanWait);
}

function commFormatShortDateTime(value) {
  if (!value) return '-';
  const date = value instanceof Date ? value : new Date(value);
  if (!Number.isFinite(date.getTime())) return '-';
  return date.toLocaleString([], { hour12: false });
}

function commBuildKorrelCycleProgressTable(rows, cycleId) {
  if (!Array.isArray(rows) || rows.length === 0) {
    return `<div class="muted">No progress data yet for cycle ${commEscape(cycleId)}.</div>`;
  }
  const header = `
    <tr>
      <th>Cycle</th>
      <th>League</th>
      <th>RND</th>
      <th>% Cycle</th>
      <th>API %</th>
      <th>miss</th>
      <th>Rounds Updated</th>
      <th>clan</th>
      <th>Roster Updated</th>
      <th>API Calls</th>
    </tr>
  `;
  const body = rows.map((row) => `
    <tr>
      <td>${commEscape(row.cycleLabel)}</td>
      <td>${commEscape(row.leagueLabel)}</td>
      <td>${commEscape(formatCount(row.roundCount))}</td>
      <td>${commEscape(row.roundPercentLabel || `${formatDecimal(row.roundPercent, 1)}%`)}</td>
      <td>${commEscape(row.apiPercentLabel || (Number.isFinite(row.apiPercent) ? `${formatDecimal(row.apiPercent, 1)}%` : '-'))}</td>
      <td>${commEscape(formatCount(row.missingCount))}</td>
      <td>${commEscape(row.roundUpdatedAt)}</td>
      <td>${commEscape(formatCount(row.rosterCount))}</td>
      <td>${commEscape(row.rosterUpdatedAt)}</td>
      <td>${commEscape(formatCount(row.apiCalls))}</td>
    </tr>
  `).join('');
  return `
    <table>
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

function commBuildKorrelApiUsageTable(rows) {
  if (!Array.isArray(rows) || rows.length === 0) {
    if (commApiLogInsertFailedAt) {
      const detail = commApiLogInsertError ? ` (${commApiLogInsertError})` : '';
      return `<div class="muted">API usage not recorded: insert failed${commEscape(detail)}.</div>`;
    }
    return '<div class="muted">No API usage recorded for this cycle.</div>';
  }
  const header = `
    <tr>
      <th>User</th>
      <th>License</th>
      <th>Total Calls</th>
      <th>Max / hr</th>
      <th>Max / hr (today)</th>
      <th>% Total</th>
    </tr>
  `;
  const body = rows.map((row) => `
    <tr>
      <td>${commEscape(row.label)}</td>
      <td>${commEscape(row.licenseKey || '-')}</td>
      <td>${commEscape(formatCount(row.totalCalls))}</td>
      <td>${commEscape(formatCount(row.maxPerHour))}</td>
      <td>${commEscape(formatCount(row.maxPerHourToday))}</td>
      <td>${commEscape(formatDecimal(row.percentOfTotal, 1))}%</td>
    </tr>
  `).join('');
  return `
    <table>
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

async function commLoadKorrelOverview() {
  if (!commIsKorrel()) return;
  if (commKorrelOverviewLoading) return;
  const progressEl = document.getElementById('commKorrelCycleProgress');
  const usageEl = document.getElementById('commKorrelApiUsage');
  const limitsEl = document.getElementById('commKorrelApiLimits');
  if (!progressEl || !usageEl) return;
  commKorrelOverviewLoading = true;
  try {
    await commLoadApiLimitsFromDb();
    const cycleId = Number.isFinite(commKorrelApiCycleOverride)
      ? Math.trunc(commKorrelApiCycleOverride)
      : getCycleNumber(new Date());
    if (!Number.isFinite(cycleId)) {
      progressEl.innerHTML = '<div class="muted">Cycle unavailable.</div>';
      usageEl.innerHTML = '<div class="muted">Cycle unavailable.</div>';
      return;
    }
    let roundRows = [];
    let rosterRows = [];
    let apiRows = [];
    try {
      const roundQuery = [
        'select=league_id,block_number,updated_at',
        `cycle_id=eq.${encodeURIComponent(cycleId)}`,
        `limit=${COMM_KORREL_PROGRESS_LIMIT}`
      ].join('&');
      roundRows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, roundQuery);
    } catch (err) {
      roundRows = [];
    }
    try {
      const rosterQuery = [
        'select=league_id,clan_id,fetched_at',
        `cycle_id=eq.${encodeURIComponent(cycleId)}`,
        `limit=${COMM_KORREL_PROGRESS_LIMIT}`
      ].join('&');
      rosterRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, rosterQuery);
    } catch (err) {
      rosterRows = [];
    }
    try {
      const apiQuery = [
        'select=user_id,user_label,license_key,call_count,league_id,cycle_id,created_at',
        `cycle_id=eq.${encodeURIComponent(cycleId)}`,
        `limit=${COMM_KORREL_PROGRESS_LIMIT}`
      ].join('&');
      apiRows = await commFetch(COMM_TABLE_API_CALL_LOG, apiQuery);
    } catch (err) {
      apiRows = [];
    }
    const completionMap = await commFetchRoundCompletionStatesByCycle(cycleId);
    const roundCounts = new Map();
    const roundUpdated = new Map();
    const rosterCounts = new Map();
    const rosterUpdated = new Map();
    const apiCallsByLeague = new Map();
    const leagueSet = new Set();

    (Array.isArray(roundRows) ? roundRows : []).forEach((row) => {
      const leagueId = commParseNumber(row?.league_id);
      if (!Number.isFinite(leagueId)) return;
      const leagueKey = Math.trunc(leagueId);
      leagueSet.add(leagueKey);
      const count = (roundCounts.get(leagueKey) || 0) + 1;
      roundCounts.set(leagueKey, count);
      const updatedAtRaw = row?.updated_at || row?.updatedAt || null;
      const updatedAtMs = updatedAtRaw ? new Date(updatedAtRaw).getTime() : 0;
      if (Number.isFinite(updatedAtMs)) {
        const prev = roundUpdated.get(leagueKey) || 0;
        if (updatedAtMs > prev) roundUpdated.set(leagueKey, updatedAtMs);
      }
    });

    (Array.isArray(rosterRows) ? rosterRows : []).forEach((row) => {
      const leagueId = commParseNumber(row?.league_id);
      if (!Number.isFinite(leagueId)) return;
      const leagueKey = Math.trunc(leagueId);
      leagueSet.add(leagueKey);
      const count = (rosterCounts.get(leagueKey) || 0) + 1;
      rosterCounts.set(leagueKey, count);
      const fetchedRaw = row?.fetched_at || row?.fetchedAt || null;
      const fetchedMs = fetchedRaw ? new Date(fetchedRaw).getTime() : 0;
      if (Number.isFinite(fetchedMs)) {
        const prev = rosterUpdated.get(leagueKey) || 0;
        if (fetchedMs > prev) rosterUpdated.set(leagueKey, fetchedMs);
      }
    });

    (Array.isArray(apiRows) ? apiRows : []).forEach((row) => {
      const leagueId = commParseNumber(row?.league_id);
      if (!Number.isFinite(leagueId)) return;
      const leagueKey = Math.trunc(leagueId);
      leagueSet.add(leagueKey);
      const calls = Math.max(1, Math.trunc(commParseNumber(row?.call_count) || 1));
      apiCallsByLeague.set(leagueKey, (apiCallsByLeague.get(leagueKey) || 0) + calls);
    });

    completionMap.forEach((_, leagueId) => {
      leagueSet.add(leagueId);
    });
    COMM_KNOWN_LEAGUE_IDS.forEach((leagueId) => leagueSet.add(leagueId));

    const leagueList = Array.from(leagueSet).filter((id) => Number.isFinite(id)).sort((a, b) => a - b);
    const progressRows = leagueList.map((leagueId) => {
      const roundCount = roundCounts.get(leagueId) || 0;
      const completion = completionMap.get(leagueId) || null;
      const missingCount = Math.max(0, COMM_KORREL_TOTAL_ROUNDS_PER_CYCLE - roundCount);
      const roundUpdatedAt = completion?.checkedAt
        ? commFormatShortDateTime(new Date(completion.checkedAt))
        : commFormatShortDateTime(roundUpdated.get(leagueId));
      const rosterUpdatedAt = commFormatShortDateTime(rosterUpdated.get(leagueId));
      const roundPercent = COMM_KORREL_TOTAL_ROUNDS_PER_CYCLE
        ? (roundCount / COMM_KORREL_TOTAL_ROUNDS_PER_CYCLE) * 100
        : 0;
      const apiPercent = commComputeRoundPercent(completion?.totalCount, completion?.missingCount);
      const roundPercentLabel = Number.isFinite(roundPercent) ? `${formatDecimal(roundPercent, 1)}%` : '-';
      const apiPercentLabel = Number.isFinite(apiPercent) ? `${formatDecimal(apiPercent, 1)}%` : '-';
      return {
        cycleLabel: `C${cycleId}`,
        leagueLabel: commFormatLeagueDisplay(leagueId),
        roundCount,
        roundPercent,
        apiPercent,
        roundPercentLabel,
        apiPercentLabel,
        missingCount,
        roundUpdatedAt,
        rosterCount: rosterCounts.get(leagueId) || 0,
        rosterUpdatedAt,
        apiCalls: apiCallsByLeague.get(leagueId) || 0
      };
    });
    progressEl.innerHTML = commBuildKorrelCycleProgressTable(progressRows, cycleId);

    const userStats = new Map();
    let totalCalls = 0;
    const todayKey = new Date().toDateString();
    (Array.isArray(apiRows) ? apiRows : []).forEach((row) => {
      const userId = row?.user_id || 'unknown';
      const calls = Math.max(1, Math.trunc(commParseNumber(row?.call_count) || 1));
      totalCalls += calls;
      if (!userStats.has(userId)) {
        userStats.set(userId, {
          label: row?.user_label || userId,
          licenseKey: row?.license_key || null,
          totalCalls: 0,
          perHour: new Map(),
          perHourToday: new Map()
        });
      }
      const stat = userStats.get(userId);
      stat.totalCalls += calls;
      const createdAt = row?.created_at ? new Date(row.created_at) : null;
      if (createdAt && Number.isFinite(createdAt.getTime())) {
        const hourKey = `${createdAt.getFullYear()}-${createdAt.getMonth()}-${createdAt.getDate()} ${createdAt.getHours()}`;
        stat.perHour.set(hourKey, (stat.perHour.get(hourKey) || 0) + calls);
        if (createdAt.toDateString() === todayKey) {
          stat.perHourToday.set(hourKey, (stat.perHourToday.get(hourKey) || 0) + calls);
        }
      }
    });
    const usageRows = Array.from(userStats.values())
      .map((stat) => {
        const maxPerHour = Math.max(0, ...Array.from(stat.perHour.values()), 0);
        const maxPerHourToday = Math.max(0, ...Array.from(stat.perHourToday.values()), 0);
        const percentOfTotal = totalCalls ? (stat.totalCalls / totalCalls) * 100 : 0;
        return {
          label: stat.label,
          licenseKey: stat.licenseKey,
          totalCalls: stat.totalCalls,
          maxPerHour,
          maxPerHourToday,
          percentOfTotal
        };
      })
      .sort((a, b) => b.totalCalls - a.totalCalls);
    usageEl.innerHTML = commBuildKorrelApiUsageTable(usageRows);

    if (limitsEl) {
      const min = commParseNumber(commApiLimitConfig?.minIntervalMs);
      const max = commParseNumber(commApiLimitConfig?.maxIntervalMs);
      const maxCalls = commParseNumber(commApiLimitConfig?.maxCallsPer10Min);
      const minLabel = Number.isFinite(min) ? `${Math.round(min / 1000)}s` : '-';
      const maxLabel = Number.isFinite(max) ? `${Math.round(max / 1000)}s` : '-';
      const callsLabel = Number.isFinite(maxCalls) ? `${maxCalls}` : '-';
      limitsEl.innerHTML = `<div class="muted" style="font-size:11px;">Limits: min interval ${minLabel} / max interval ${maxLabel} / max calls per 10m ${callsLabel}</div>`;
    }
  } finally {
    commKorrelOverviewLoading = false;
  }
}

function commStartKorrelOverviewTimer() {
  if (commKorrelOverviewTimer) {
    clearInterval(commKorrelOverviewTimer);
    commKorrelOverviewTimer = null;
  }
  if (!commIsKorrel()) return;
  commLoadKorrelOverview().catch(() => {});
  commKorrelOverviewTimer = setInterval(() => {
    commLoadKorrelOverview().catch(() => {});
  }, COMM_KORREL_PROGRESS_REFRESH_MS);
}

async function commLogKorrelRoundCountdownStatus() {
  if (!commSession || !commIsKorrel()) return;
  if (!COMM_KORREL_AUTO_SNAPSHOT_ENABLED) return;
  await commRefreshKorrelStoredBlockNumber({ force: true });
  if (commKorrelRoundSnapshotPending) {
    commSetKorrelLog('Round snapshot status: running now...', { append: true });
    return;
  }
  const { remaining, usingDb } = commComputeKorrelRoundsLeft();
  if (remaining <= 0) {
    commSetKorrelLog('Round snapshot status: due on next round.', { append: true });
    return;
  }
  const suffix = usingDb ? ' (from DB)' : '';
  commSetKorrelLog(
    `Round snapshot status: ${remaining} round${remaining === 1 ? '' : 's'} left${suffix}.`,
    { append: true }
  );
}

function commFormatKorrelValue(value) {
  if (value === null || value === undefined || value === '') return '-';
  if (typeof value === 'number') return Number.isFinite(value) ? formatCount(value) : '-';
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
  if (Array.isArray(value)) return value.length ? value.join(', ') : '-';
  if (typeof value === 'object') {
    try {
      return JSON.stringify(value);
    } catch (err) {
      return '[object]';
    }
  }
  const text = String(value).trim();
  if (!text) return '-';
  const numericText = text.replace(/,/g, '');
  if (/^-?\d+(\.\d+)?$/.test(numericText)) {
    const parsed = commParseNumber(numericText);
    return parsed === null ? text : formatCount(parsed);
  }
  return text;
}

function commSetKorrelTable(fields) {
  const container = document.getElementById('commKorrelTable');
  if (!container) return;
  if (!Array.isArray(fields) || fields.length === 0) {
    container.innerHTML = '';
    return;
  }
  const rows = fields
    .map(([label, value]) => {
      const formatted = commFormatKorrelValue(value);
      return `<tr><th scope="row">${commEscape(label)}</th><td>${commEscape(formatted)}</td></tr>`;
    })
    .join('');
  container.innerHTML = `
    <table>
      <thead>
        <tr><th>Field</th><th>Value</th></tr>
      </thead>
      <tbody>${rows}</tbody>
    </table>
  `;
}

function commCompactJson(value, limit = 600) {
  if (value === null || value === undefined) return '-';
  let text = '';
  if (typeof value === 'string') {
    text = value;
  } else {
    try {
      text = JSON.stringify(value);
    } catch (err) {
      text = String(value);
    }
  }
  if (!text) return '-';
  if (text.length <= limit) return text;
  return `${text.slice(0, Math.max(0, limit - 3))}...`;
}

function commStoreKorrelRawPayload(label, payload) {
  commKorrelApiLastRawLabel = label || 'RAW payload';
  commKorrelApiLastRawPayload = payload;
}

function commGetApiInputDefault(endpoint, field) {
  const isRoundFindByCycle = endpoint === COMM_KORREL_ROUND_SNAPSHOT_ENDPOINT;
  switch (field) {
    case 'clanId': {
      const id = commGetKorrelClanId();
      return id ? String(id) : '';
    }
    case 'pagination.limit':
      return isRoundFindByCycle ? '10' : String(commKorrelApiDefaultLimit);
    case 'pagination.skip':
      return '0';
    case 'pagination.count':
      return '0';
    case 'multipliers':
      return COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS_TEXT;
    case 'filters.filterType':
      return 'none';
    case 'filters.type':
      return endpoint === 'https://api.gomining.com/api/nft-game/rewards-by-user' ? 'all' : 'none';
    case 'sort.sortType':
      return 'none';
    case 'cycleId': {
      if (commKorrelApiCycleOverride) return String(commKorrelApiCycleOverride);
      const cycle = getCycleNumber(new Date());
      return cycle ? String(cycle) : '';
    }
    case 'leagueId':
      return commKorrelApiLeagueOverride ? String(commKorrelApiLeagueOverride) : '';
    case 'type':
      return 'current';
    default:
      return '';
  }
}

function commSetKorrelApiInputValue(endpoint, field, value) {
  if (!endpoint || !field) return;
  const key = String(endpoint);
  if (!commKorrelApiInputValues[key]) commKorrelApiInputValues[key] = {};
  commKorrelApiInputValues[key][field] = value;
}

function commGetKorrelApiInputValue(endpoint, field) {
  if (!endpoint || !field) return '';
  const key = String(endpoint);
  if (commKorrelApiInputValues[key] && commKorrelApiInputValues[key][field] !== undefined) {
    return commKorrelApiInputValues[key][field];
  }
  return commGetApiInputDefault(endpoint, field);
}

function commReadApiNumber(endpoint, field, fallback) {
  const raw = commGetKorrelApiInputValue(endpoint, field);
  const parsed = commParseNumber(raw);
  if (Number.isFinite(parsed)) return parsed;
  return fallback;
}

function commReadApiText(endpoint, field, fallback) {
  const raw = commGetKorrelApiInputValue(endpoint, field);
  const text = String(raw ?? '').trim();
  return text || fallback;
}

function commReadApiNumberList(endpoint, field, fallback = []) {
  const raw = commGetKorrelApiInputValue(endpoint, field);
  if (Array.isArray(raw)) {
    const filtered = raw.map((v) => commParseNumber(v)).filter((v) => Number.isFinite(v));
    return filtered.length ? filtered : fallback;
  }
  const text = String(raw ?? '').trim();
  if (!text) return fallback;
  const parts = text.split(/[^0-9.+-]+/).filter(Boolean);
  const nums = parts.map((part) => commParseNumber(part)).filter((v) => Number.isFinite(v));
  return nums.length ? nums : fallback;
}

function commLoadKorrelRosterAutoState() {
  try {
    const doneRaw = localStorage.getItem(COMM_API_ROSTER_AUTO_DONE_KEY);
    const doneParsed = commParseNumber(doneRaw);
    if (Number.isFinite(doneParsed)) {
      commKorrelRosterAutoDoneCycleId = Math.trunc(doneParsed);
    }
    const lastRaw = localStorage.getItem(COMM_API_ROSTER_AUTO_LAST_KEY);
    const lastParsed = commParseNumber(lastRaw);
    if (Number.isFinite(lastParsed)) {
      commKorrelRosterAutoLastAttemptAt = Math.max(0, Math.trunc(lastParsed));
    }
  } catch (err) {
    // ignore
  }
}

function commSaveKorrelRosterAutoState() {
  try {
    if (Number.isFinite(commKorrelRosterAutoDoneCycleId)) {
      localStorage.setItem(COMM_API_ROSTER_AUTO_DONE_KEY, String(commKorrelRosterAutoDoneCycleId));
    } else {
      localStorage.removeItem(COMM_API_ROSTER_AUTO_DONE_KEY);
    }
    if (Number.isFinite(commKorrelRosterAutoLastAttemptAt)) {
      localStorage.setItem(COMM_API_ROSTER_AUTO_LAST_KEY, String(commKorrelRosterAutoLastAttemptAt));
    }
  } catch (err) {
    // ignore
  }
}

function commMarkRosterAutoDone(cycleId) {
  if (!Number.isFinite(cycleId)) return;
  commKorrelRosterAutoDoneCycleId = Math.trunc(cycleId);
  commSaveKorrelRosterAutoState();
}

function commLogRosterAutoSkip(reason, minIntervalMs = 60000) {
  if (!commIsKorrel()) return;
  const now = Date.now();
  if (
    reason === commKorrelRosterAutoSkipReason &&
    now - commKorrelRosterAutoSkipAt < minIntervalMs
  ) {
    return;
  }
  commKorrelRosterAutoSkipReason = reason;
  commKorrelRosterAutoSkipAt = now;
  commSetKorrelLog(`Roster auto-run skipped: ${reason}.`, { append: true });
}

async function commGetRosterTargetsSummary(cycleId) {
  if (!Number.isFinite(cycleId)) {
    return {
      needsMore: false,
      totalTargets: 0,
      missingCount: 0,
      leagueCount: 0,
      missingLeagues: [],
      presentLeagues: []
    };
  }
  const targets = await commFetchRosterTargetsFromDb({
    cycleIdOverride: cycleId,
    leagueIdOverride: null,
    ignoreOverride: true
  });
  const clans = Array.isArray(targets?.clans) ? targets.clans : [];
  if (!clans.length) {
    const presentLeagues = await commFetchLeagueIdsFromDb(cycleId);
    const presentSet = new Set((presentLeagues || []).map((id) => Math.trunc(id)));
    const missingLeagues = COMM_KNOWN_LEAGUE_IDS.filter((id) => !presentSet.has(id));
    return {
      needsMore: false,
      totalTargets: 0,
      missingCount: 0,
      leagueCount: presentSet.size,
      missingLeagues,
      presentLeagues: Array.from(presentSet)
    };
  }
  const existing = await commFetchExistingRosterClanKeys(cycleId, null);
  let missingCount = 0;
  const leagueSet = new Set();
  const missingLeagueSet = new Set();
  clans.forEach((clan) => {
    const clanId = commParseNumber(clan?.clanId);
    if (!Number.isFinite(clanId)) return;
    const leagueId = commParseNumber(clan?.leagueId);
    if (Number.isFinite(leagueId)) leagueSet.add(Math.trunc(leagueId));
    const leagueKey = Number.isFinite(leagueId) ? Math.trunc(leagueId) : 'x';
    const key = `${leagueKey}:${Math.trunc(clanId)}`;
    if (!existing.has(key)) {
      missingCount += 1;
      if (Number.isFinite(leagueId)) missingLeagueSet.add(Math.trunc(leagueId));
    }
  });
  const presentLeagues = await commFetchLeagueIdsFromDb(cycleId);
  const presentSet = new Set(
    (presentLeagues || [])
      .map((id) => commParseNumber(id))
      .filter((val) => Number.isFinite(val))
      .map((val) => Math.trunc(val))
  );
  const missingLeagues = COMM_KNOWN_LEAGUE_IDS.filter((id) => !presentSet.has(id));
  return {
    needsMore: missingCount > 0,
    totalTargets: clans.length,
    missingCount,
    leagueCount: presentSet.size || leagueSet.size,
    missingLeagues,
    presentLeagues: Array.from(presentSet)
  };
}

function commBuildAutoLeagueCandidates(currentLeague, dbLeagues) {
  const ordered = [];
  const addLeague = (value) => {
    const parsed = commParseNumber(value);
    if (!Number.isFinite(parsed)) return;
    const id = Math.trunc(parsed);
    if (!ordered.includes(id)) ordered.push(id);
  };
  if (Number.isFinite(currentLeague)) addLeague(currentLeague);
  (Array.isArray(dbLeagues) ? dbLeagues : []).forEach(addLeague);
  COMM_KNOWN_LEAGUE_IDS.forEach(addLeague);
  if (!ordered.length) return [];
  const current = Number.isFinite(currentLeague) ? Math.trunc(currentLeague) : null;
  const rest = ordered.filter((id) => id !== current).sort((a, b) => a - b);
  return current !== null ? [current, ...rest] : rest;
}

function commBuildAutoRoundQueue(cycleId, completionMap, dbLeagues) {
  const now = Date.now();
  const ownerKey = commGetRoundLockOwnerKey();
  const leagueIds = Array.from(new Set([...(dbLeagues || []), ...COMM_KNOWN_LEAGUE_IDS]))
    .map((val) => commParseNumber(val))
    .filter((val) => Number.isFinite(val))
    .map((val) => Math.trunc(val));
  const entries = [];
  leagueIds.forEach((leagueId) => {
    const state = completionMap.get(leagueId) || null;
    const missingCount = state?.missingCount;
    const totalCount = state?.totalCount;
    const checkedAt = state?.checkedAt || 0;
    const stale = commIsRoundCompletionStale(state, now);
    const lockOwner = state?.lockOwner || null;
    const lockExpiresAt = state?.lockExpiresAt || 0;
    const lockedByOther = lockOwner && lockOwner !== ownerKey && Number.isFinite(lockExpiresAt) && lockExpiresAt > now;
    if (lockedByOther) {
      return;
    }
    const needsWork = missingCount == null || missingCount > 0 || stale;
    if (!needsWork) return;
    const priority = missingCount == null ? 3 : missingCount > 0 ? 2 : 1;
    const reason = missingCount == null
      ? 'missing unknown'
      : missingCount > 0
        ? `missing ${missingCount}`
        : 'stale completion';
    entries.push({
      leagueId,
      missingCount,
      totalCount,
      checkedAt,
      stale,
      priority,
      reason
    });
  });
  entries.sort((a, b) => {
    if (b.priority !== a.priority) return b.priority - a.priority;
    const aMissing = Number.isFinite(a.missingCount) ? a.missingCount : 0;
    const bMissing = Number.isFinite(b.missingCount) ? b.missingCount : 0;
    if (bMissing !== aMissing) return bMissing - aMissing;
    const aChecked = Number.isFinite(a.checkedAt) ? a.checkedAt : 0;
    const bChecked = Number.isFinite(b.checkedAt) ? b.checkedAt : 0;
    if (aChecked !== bChecked) return aChecked - bChecked;
    return a.leagueId - b.leagueId;
  });
  const queue = entries.map((entry) => entry.leagueId);
  const entryMap = new Map(entries.map((entry) => [entry.leagueId, entry]));
  return { queue, entryMap };
}

async function commFetchRoundCompletionStatesByCycle(cycleId, { forceBasic = false } = {}) {
  if (!Number.isFinite(cycleId)) return new Map();
  const selectFields = forceBasic
    ? 'cycle_id,league_id,missing_count,total_count,latest_block,checked_at,lock_owner,lock_expires_at'
    : commGetRoundCompletionSelectFields();
  const query = [
    `select=${selectFields}`,
    `cycle_id=eq.${Math.trunc(cycleId)}`,
    'order=league_id.asc',
    'limit=200'
  ].join('&');
  try {
    const rows = await commFetch(COMM_TABLE_ROUND_COMPLETION, query);
    if (!forceBasic && commRoundRefreshColumnAvailable === null) {
      commRoundRefreshColumnAvailable = true;
    }
    const map = new Map();
    (Array.isArray(rows) ? rows : []).forEach((row) => {
      const state = commNormalizeRoundCompletionRow(row);
      if (state) map.set(state.leagueId, state);
    });
    return map;
  } catch (err) {
    const msg = String(err?.message || '');
    if (!forceBasic && msg.toLowerCase().includes('last_refresh_at')) {
      commRoundRefreshColumnAvailable = false;
      return commFetchRoundCompletionStatesByCycle(cycleId, { forceBasic: true });
    }
    return new Map();
  }
}

function commIsRoundCompletionFresh(state, now) {
  if (!state) return false;
  if (state.missingCount !== 0) return false;
  const checkedAt = state.checkedAt || 0;
  if (!Number.isFinite(checkedAt) || checkedAt <= 0) return false;
  return now - checkedAt < COMM_KORREL_ROUND_COMPLETION_STALE_MS;
}

function commIsRoundCompletionStale(state, now) {
  if (!state) return true;
  if (commRoundRefreshColumnAvailable === false) {
    if (state.missingCount !== 0) return false;
    const checkedAt = state.checkedAt || 0;
    if (!Number.isFinite(checkedAt) || checkedAt <= 0) return true;
    return now - checkedAt >= COMM_KORREL_ROUND_COMPLETION_STALE_MS;
  }
  const refreshAt = state.lastRefreshAt || 0;
  if (!Number.isFinite(refreshAt) || refreshAt <= 0) return true;
  return now - refreshAt >= COMM_KORREL_ROUND_REFRESH_MS;
}

async function commPickNextAutoRoundTarget(cycleId, currentLeague) {
  const dbLeagues = await commFetchLeagueIdsFromDb(cycleId);
  const completionMap = await commFetchRoundCompletionStatesByCycle(cycleId);
  const { queue, entryMap } = commBuildAutoRoundQueue(cycleId, completionMap, dbLeagues);
  if (!queue.length) {
    return { leagueId: null, stale: false, reason: 'complete', candidates: [] };
  }
  const queueKey = queue.join(',');
  if (commKorrelRoundQueueCycleId !== cycleId || commKorrelRoundQueueKey !== queueKey) {
    commKorrelRoundQueue = queue;
    commKorrelRoundQueueCycleId = cycleId;
    commKorrelRoundQueueKey = queueKey;
    commKorrelRoundQueueIndex = commKorrelRoundQueueIndex % queue.length;
  }
  let attempts = 0;
  while (attempts < commKorrelRoundQueue.length) {
    const idx = commKorrelRoundQueueIndex % commKorrelRoundQueue.length;
    const leagueId = commKorrelRoundQueue[idx];
    commKorrelRoundQueueIndex = (idx + 1) % commKorrelRoundQueue.length;
    const entry = entryMap.get(leagueId);
    if (entry) {
      return {
        leagueId,
        stale: entry.stale,
        reason: entry.reason,
        missingCount: entry.missingCount,
        totalCount: entry.totalCount,
        candidates: commKorrelRoundQueue
      };
    }
    attempts += 1;
  }
  return { leagueId: null, stale: false, reason: 'complete', candidates: commKorrelRoundQueue };
}

async function commGetRosterMissingSummaryForLeague(cycleId, leagueId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) {
    return { totalTargets: 0, missingCount: 0, userCount: 0 };
  }
  const targets = await commFetchRosterTargetsFromDb({
    cycleIdOverride: cycleId,
    leagueIdOverride: Math.trunc(leagueId),
    ignoreOverride: true
  });
  const clans = Array.isArray(targets?.clans) ? targets.clans : [];
  if (!clans.length) {
    return { totalTargets: 0, missingCount: 0, userCount: targets?.userCount ?? 0 };
  }
  const existing = await commFetchExistingRosterClanKeys(cycleId, leagueId);
  let missingCount = 0;
  clans.forEach((clan) => {
    const clanId = commParseNumber(clan?.clanId);
    if (!Number.isFinite(clanId)) return;
    const key = `${Math.trunc(leagueId)}:${Math.trunc(clanId)}`;
    if (!existing.has(key)) missingCount += 1;
  });
  return {
    totalTargets: clans.length,
    missingCount,
    userCount: Number.isFinite(targets?.userCount) ? targets.userCount : 0
  };
}

async function commPickNextAutoRosterTarget(cycleId, currentLeague, candidates) {
  const list = Array.isArray(candidates) && candidates.length
    ? candidates
    : commBuildAutoLeagueCandidates(currentLeague, await commFetchLeagueIdsFromDb(cycleId));
  let minWaitMs = null;
  for (const leagueId of list) {
    const completion = await commGetRoundCompletionStateShared(cycleId, leagueId);
    const pct = commComputeRoundPercent(completion?.totalCount, completion?.missingCount);
    const roundsReady = completion?.missingCount === 0 || (Number.isFinite(pct) && pct >= COMM_KORREL_ROUND_ROSTER_READY_PCT);
    const fullRosterEligible = Number.isFinite(pct) && pct >= COMM_KORREL_ROSTER_FULL_PCT;
    const summary = await commGetRosterMissingSummaryForLeague(cycleId, leagueId);
    if (!summary.totalTargets) continue;
    if (fullRosterEligible) {
      const rosterState = await commFetchRosterCompletionState(cycleId, leagueId);
      const waitMs = commGetRosterFullWaitMs(rosterState, Date.now());
      if (waitMs <= 0) {
        return { leagueId, summary, fullRoster: true, waitMs };
      }
      if (roundsReady && summary.missingCount > 0) {
        return { leagueId, summary, fullRoster: false, waitMs: 0 };
      }
      minWaitMs = minWaitMs == null ? waitMs : Math.min(minWaitMs, waitMs);
      continue;
    }
    if (roundsReady && summary.missingCount > 0) {
      return { leagueId, summary, fullRoster: false, waitMs: 0 };
    }
  }
  if (Number.isFinite(minWaitMs) && minWaitMs > 0) {
    return { leagueId: null, summary: null, fullRoster: true, waitMs: minWaitMs };
  }
  return null;
}

async function commMaybeRunAutoRosterBatch() {
  if (commKorrelRosterRunning || commKorrelRosterPatchRunning || commKorrelRosterResumeTimer) {
    commLogRosterAutoSkip('roster already running or paused');
    return false;
  }
  const cycleId = getCycleNumber(new Date());
  if (!Number.isFinite(cycleId)) {
    commLogRosterAutoSkip('cycle id unavailable');
    return false;
  }
  const startAt = getCurrentCycleRosterStartUTC(new Date());
  if (Date.now() < startAt.getTime()) {
    commLogRosterAutoSkip(`waiting until roster window opens (${startAt.toLocaleTimeString([], { hour12: false })} UTC)`);
    return false;
  }
  if (commKorrelRosterAutoDoneCycleId === cycleId) {
    try {
      const summary = await commGetRosterTargetsSummary(cycleId);
      if (!summary.needsMore) {
        const leagueInfo = summary.leagueCount
          ? ` across ${summary.leagueCount} leagues`
          : '';
        if (summary.missingLeagues.length) {
          commLogRosterAutoSkip(
            `cycle ${cycleId} roster complete (${summary.totalTargets} clans${leagueInfo}, missing 0). Waiting for rounds in leagues: ${summary.missingLeagues.join(', ')}`
          );
        } else {
          commLogRosterAutoSkip(
            `cycle ${cycleId} already completed (${summary.totalTargets} clans${leagueInfo}, missing 0)`
          );
        }
        return false;
      }
      const missingLeagues = summary.missingLeagues.length
        ? ` (leagues ${summary.missingLeagues.join(', ')})`
        : '';
      commKorrelRosterAutoDoneCycleId = null;
      commSaveKorrelRosterAutoState();
      commSetKorrelLog(
        `Roster auto-run: cycle ${cycleId} has ${summary.missingCount}/${summary.totalTargets} clans missing${missingLeagues}, resuming.`,
        { append: true }
      );
    } catch (err) {
      commLogRosterAutoSkip(`cycle ${cycleId} completion check failed`);
      return false;
    }
  }
  const now = Date.now();
  if (!commKorrelRosterAutoNextAt) {
    const base = commKorrelRosterAutoLastAttemptAt || now;
    commKorrelRosterAutoNextAt = base + commPickRandomIntervalMs();
    const waitSec = Math.max(0, Math.round((commKorrelRosterAutoNextAt - now) / 1000));
    commLogRosterAutoSkip(`waiting ${waitSec}s until next roster run`);
    return false;
  }
  if (now < commKorrelRosterAutoNextAt) {
    const waitSec = Math.max(0, Math.round((commKorrelRosterAutoNextAt - now) / 1000));
    commLogRosterAutoSkip(`waiting ${waitSec}s until next roster run`);
    return false;
  }
  const plan = await commPickNextAutoRosterTarget(cycleId, commGetKorrelActiveLeagueId(), []);
  if (plan?.waitMs && plan.waitMs > 0) {
    commKorrelRosterAutoNextAt = now + plan.waitMs;
    commSaveKorrelRosterAutoState();
    commLogRosterAutoSkip(`full roster cooldown: ${commFormatDurationShort(plan.waitMs)}`);
    return false;
  }
  const fullRoster = Boolean(plan?.fullRoster);
  const leagueOverride = Number.isFinite(plan?.leagueId) ? Math.trunc(plan.leagueId) : null;
  commKorrelRosterAutoLastAttemptAt = now;
  const nextDelay = Math.max(commPickRandomIntervalMs(), fullRoster ? COMM_KORREL_ROSTER_FULL_MIN_INTERVAL_MS : 0);
  commKorrelRosterAutoNextAt = now + nextDelay;
  commSaveKorrelRosterAutoState();
  commSetKorrelLog(
    `Roster auto-run: cycle ${cycleId} starting${fullRoster ? ' (full)' : ''}.`,
    { append: true, context: leagueOverride ? { cycleId, leagueId: leagueOverride } : undefined }
  );
  if (fullRoster && Number.isFinite(leagueOverride)) {
    commPersistRosterCompletionState({
      cycleId,
      leagueId: leagueOverride,
      fullRunAt: now,
      lastClanAt: null
    }).catch(() => {});
  }
  return commRunKorrelClanRosterBatch({
    auto: true,
    cycleIdOverride: cycleId,
    leagueIdOverride: leagueOverride,
    fullRoster
  });
}

async function commMaybeRunKorrelAutoWork() {
  if (commKorrelBackfillRunning) return;
  if (commKorrelRoundSnapshotPending || commKorrelRosterRunning || commKorrelRosterPatchRunning) {
    return;
  }
  await commLoadApiLimitsFromDb();
  const now = Date.now();
  const maxIntervalMs = commParseNumber(commApiLimitConfig?.maxIntervalMs);
  if (
    Number.isFinite(maxIntervalMs) &&
    maxIntervalMs > 0 &&
    commKorrelBackfillNextAt &&
    commKorrelBackfillNextAt - now > maxIntervalMs
  ) {
    commKorrelBackfillNextAt = now + commPickRandomIntervalMs();
    commUpdateKorrelOtherLeagueCountdown();
    if (commIsKorrel()) {
      commSetKorrelLog(
        `Auto update rescheduled to ${commFormatDurationShort(commKorrelBackfillNextAt - now)} (limit change).`,
        { append: true }
      );
    }
  }
  if (!commKorrelBackfillNextAt) {
    commKorrelBackfillNextAt = now + commPickRandomIntervalMs();
    commUpdateKorrelOtherLeagueCountdown();
    if (commIsKorrel()) {
      const waitMs = commKorrelBackfillNextAt - now;
      commSetKorrelLog(`Auto update scheduled in ${commFormatDurationShort(waitMs)}.`, { append: true });
    }
    return;
  }
  if (now < commKorrelBackfillNextAt) return;
  const cycleId = Number.isFinite(commKorrelApiCycleOverride)
    ? Math.trunc(commKorrelApiCycleOverride)
    : getCycleNumber(new Date());
  if (!Number.isFinite(cycleId)) {
    commSetKorrelLog('Auto update skipped: cycle id unavailable.', { error: true });
    commKorrelBackfillNextAt = now + commPickRandomIntervalMs();
    commUpdateKorrelOtherLeagueCountdown();
    return;
  }
  commKorrelBackfillRunning = true;
  commKorrelBackfillLastAt = now;
  commKorrelBackfillNextAt = now + commPickRandomIntervalMs();
  commUpdateKorrelOtherLeagueCountdown();
  const currentLeague = commGetKorrelActiveLeagueId();
  try {
    const roundPlan = await commPickNextAutoRoundTarget(cycleId, currentLeague);
    let leagueId = roundPlan?.leagueId ?? null;
    if (!Number.isFinite(leagueId)) {
      if (commKorrelRosterResumeTimer && commKorrelRosterResumeAt) {
        const remainingMs = Math.max(0, commKorrelRosterResumeAt - now);
        commLogRosterAutoSkip(`roster resume scheduled (${commFormatDurationShort(remainingMs)})`);
        return;
      }
      const rosterPlan = await commPickNextAutoRosterTarget(cycleId, currentLeague, []);
      if (rosterPlan?.waitMs && rosterPlan.waitMs > 0) {
        const waitMs = rosterPlan.waitMs;
        commSetKorrelLog(`Auto roster delayed: ${commFormatDurationShort(waitMs)} (rounds continue).`, {
          append: true,
          context: { cycleId }
        });
        return;
      }
      if (!rosterPlan?.leagueId) {
        commSetKorrelLog(`Auto update: cycle ${cycleId} is complete for all leagues.`, { append: true });
        return;
      }
      leagueId = rosterPlan.leagueId;
      const leagueLabel = commGetLeagueName(leagueId);
      const fullRoster = Boolean(rosterPlan.fullRoster);
      commSetKorrelLog(
        `Auto roster: ${rosterPlan.summary.missingCount}/${rosterPlan.summary.totalTargets} clans missing in ${leagueLabel}${fullRoster ? ' (full)' : ''}.`,
        { append: true, context: { cycleId, leagueId } }
      );
      if (fullRoster) {
        const minDelay = COMM_KORREL_ROSTER_FULL_MIN_INTERVAL_MS;
        commKorrelBackfillNextAt = now + Math.max(commPickRandomIntervalMs(), minDelay);
        commUpdateKorrelOtherLeagueCountdown();
      }
      if (fullRoster) {
        commPersistRosterCompletionState({
          cycleId,
          leagueId,
          fullRunAt: Date.now()
        }).catch(() => {});
      }
      await commRunKorrelClanRosterBatch({
        auto: true,
        cycleIdOverride: cycleId,
        leagueIdOverride: leagueId,
        fullRoster
      });
      return;
    }
    commKorrelBackfillLastLeagueId = leagueId;
    commKorrelBackfillLastCycleId = cycleId;
    const leagueLabel = commGetLeagueName(leagueId);
    const reason = roundPlan?.reason ? ` (${roundPlan.reason})` : '';
    const refreshNote = roundPlan?.stale ? ' (refresh)' : '';
    commSetKorrelLog(`Auto update: round tick for ${leagueLabel}${reason}${refreshNote}.`, {
      append: true,
      context: { cycleId, leagueId }
    });
    const roundOk = await commRunKorrelRoundCyclePage({
      cycleId,
      leagueId,
      reason: roundPlan?.reason || ''
    });
    if (!roundOk) {
      commSetKorrelLog(`Auto update: round tick failed for ${leagueLabel}.`, {
        append: true,
        error: true,
        context: { cycleId, leagueId }
      });
    }
    return;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Auto update failed.', { error: true });
  } finally {
    commKorrelBackfillRunning = false;
    commUpdateKorrelOtherLeagueCountdown();
  }
}

function commLoadKorrelApiSettings() {
  try {
    const limitRaw = localStorage.getItem(COMM_API_DEFAULT_LIMIT_KEY);
    const intervalRaw = localStorage.getItem(COMM_API_MIN_INTERVAL_KEY);
    const bulkRaw = localStorage.getItem(COMM_API_BULK_AMOUNT_KEY);
    const cycleRaw = localStorage.getItem(COMM_API_CYCLE_OVERRIDE_KEY);
    const leagueRaw = localStorage.getItem(COMM_API_LEAGUE_OVERRIDE_KEY);
    const rosterBatchRaw = localStorage.getItem(COMM_API_ROSTER_BATCH_KEY);
    const rosterLimitRaw = localStorage.getItem(COMM_API_ROSTER_MEMBER_LIMIT_KEY);
    const rosterVerboseRaw = localStorage.getItem(COMM_API_ROSTER_VERBOSE_KEY);
    const limitParsed = commParseNumber(limitRaw);
    if (Number.isFinite(limitParsed) && limitParsed > 0) {
      commKorrelApiDefaultLimit = Math.max(1, Math.floor(limitParsed));
    }
    const intervalParsed = commParseNumber(intervalRaw);
    if (Number.isFinite(intervalParsed) && intervalParsed >= 0) {
      commKorrelApiMinIntervalMs = Math.max(0, Math.floor(intervalParsed));
    }
    const bulkParsed = commParseNumber(bulkRaw);
    if (Number.isFinite(bulkParsed) && bulkParsed > 0) {
      commKorrelApiBulkAmount = Math.max(COMM_KORREL_ROUND_SNAPSHOT_LIMIT, Math.min(500, Math.floor(bulkParsed)));
    }
    const cycleParsed = commParseNumber(cycleRaw);
    if (Number.isFinite(cycleParsed) && cycleParsed > 0) {
      commKorrelApiCycleOverride = Math.floor(cycleParsed);
    }
    const leagueParsed = commParseNumber(leagueRaw);
    if (Number.isFinite(leagueParsed) && leagueParsed > 0) {
      commKorrelApiLeagueOverride = Math.floor(leagueParsed);
    }
    const rosterBatchParsed = commParseNumber(rosterBatchRaw);
    if (Number.isFinite(rosterBatchParsed) && rosterBatchParsed > 0) {
      commKorrelRosterBatchSize = Math.max(1, Math.min(200, Math.floor(rosterBatchParsed)));
    }
    const rosterLimitParsed = commParseNumber(rosterLimitRaw);
    if (Number.isFinite(rosterLimitParsed) && rosterLimitParsed > 0) {
      commKorrelRosterMemberLimit = Math.max(10, Math.min(25, Math.floor(rosterLimitParsed)));
    }
    if (rosterVerboseRaw !== null && rosterVerboseRaw !== undefined && rosterVerboseRaw !== '') {
      commKorrelRosterVerbose = rosterVerboseRaw !== '0' && rosterVerboseRaw !== 'false';
    }
    commLoadKorrelRosterAutoState();
  } catch (err) {
    // ignore
  }
}

function commSaveKorrelApiSettings() {
  try {
    localStorage.setItem(COMM_API_DEFAULT_LIMIT_KEY, String(commKorrelApiDefaultLimit));
    localStorage.setItem(COMM_API_MIN_INTERVAL_KEY, String(commKorrelApiMinIntervalMs));
    localStorage.setItem(COMM_API_BULK_AMOUNT_KEY, String(commKorrelApiBulkAmount));
    localStorage.setItem(COMM_API_CYCLE_OVERRIDE_KEY, commKorrelApiCycleOverride ? String(commKorrelApiCycleOverride) : '');
    localStorage.setItem(COMM_API_LEAGUE_OVERRIDE_KEY, commKorrelApiLeagueOverride ? String(commKorrelApiLeagueOverride) : '');
    localStorage.setItem(COMM_API_ROSTER_BATCH_KEY, String(commKorrelRosterBatchSize));
    localStorage.setItem(COMM_API_ROSTER_MEMBER_LIMIT_KEY, String(commKorrelRosterMemberLimit));
    localStorage.setItem(COMM_API_ROSTER_VERBOSE_KEY, commKorrelRosterVerbose ? '1' : '0');
    commScheduleUserSettingsSync();
  } catch (err) {
    // ignore
  }
}

function commUpdateKorrelApiSettingsUI() {
  const limitInput = document.getElementById('commApiDefaultLimit');
  const limitValue = document.getElementById('commApiDefaultLimitValue');
  if (limitInput) limitInput.value = String(commKorrelApiDefaultLimit);
  if (limitValue) limitValue.textContent = `${commKorrelApiDefaultLimit}`;
  const intervalInput = document.getElementById('commApiMinInterval');
  const intervalValue = document.getElementById('commApiMinIntervalValue');
  const seconds = Math.max(0, Math.round(commKorrelApiMinIntervalMs / 1000));
  if (intervalInput) intervalInput.value = String(seconds);
  if (intervalValue) intervalValue.textContent = `${seconds}s`;
  const bulkInput = document.getElementById('commApiBulkAmount');
  const bulkValue = document.getElementById('commApiBulkAmountValue');
  const bulkAmount = commGetKorrelBulkAmount();
  if (bulkInput) bulkInput.value = String(bulkAmount);
  if (bulkValue) bulkValue.textContent = `${bulkAmount}`;
  const cycleInput = document.getElementById('commApiCycleOverride');
  if (cycleInput) {
    cycleInput.value = commKorrelApiCycleOverride ? String(commKorrelApiCycleOverride) : '';
  }
  const leagueInput = document.getElementById('commApiLeagueOverride');
  if (leagueInput) {
    leagueInput.value = commKorrelApiLeagueOverride ? String(commKorrelApiLeagueOverride) : '';
  }
  const rosterBatchInput = document.getElementById('commRosterBatch');
  const rosterBatchValue = document.getElementById('commRosterBatchValue');
  if (rosterBatchInput) rosterBatchInput.value = String(commKorrelRosterBatchSize);
  if (rosterBatchValue) rosterBatchValue.textContent = `${commKorrelRosterBatchSize}`;
  const rosterLimitInput = document.getElementById('commRosterMemberLimit');
  const rosterLimitValue = document.getElementById('commRosterMemberLimitValue');
  if (rosterLimitInput) rosterLimitInput.value = String(commKorrelRosterMemberLimit);
  if (rosterLimitValue) rosterLimitValue.textContent = `${commKorrelRosterMemberLimit}`;
  const rosterVerboseInput = document.getElementById('commRosterVerbose');
  if (rosterVerboseInput) rosterVerboseInput.checked = Boolean(commKorrelRosterVerbose);
  const bulkBtn = document.getElementById('commApiRoundBulkBtn');
  if (bulkBtn) bulkBtn.textContent = `API: round x${bulkAmount}`;
}

function commGetKorrelBulkAmount() {
  const input = document.getElementById('commApiBulkAmount');
  const raw = input ? input.value : commKorrelApiBulkAmount;
  const parsed = commParseNumber(raw);
  if (!Number.isFinite(parsed)) return commKorrelApiBulkAmount;
  return Math.max(COMM_KORREL_ROUND_SNAPSHOT_LIMIT, Math.min(500, Math.floor(parsed)));
}

function commGetKorrelRosterBatchSize() {
  const input = document.getElementById('commRosterBatch');
  const raw = input ? input.value : commKorrelRosterBatchSize;
  const parsed = commParseNumber(raw);
  if (!Number.isFinite(parsed)) return commKorrelRosterBatchSize;
  return Math.max(1, Math.min(200, Math.floor(parsed)));
}

function commGetKorrelRosterMemberLimit() {
  const input = document.getElementById('commRosterMemberLimit');
  const raw = input ? input.value : commKorrelRosterMemberLimit;
  const parsed = commParseNumber(raw);
  if (!Number.isFinite(parsed)) return commKorrelRosterMemberLimit;
  return Math.max(10, Math.min(25, Math.floor(parsed)));
}

function commLoadKorrelRosterProgress() {
  try {
    const raw = localStorage.getItem(COMM_API_ROSTER_PROGRESS_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return null;
    return parsed;
  } catch (err) {
    return null;
  }
}

function commSaveKorrelRosterProgress(progress) {
  try {
    if (!progress) {
      localStorage.removeItem(COMM_API_ROSTER_PROGRESS_KEY);
      commScheduleUserSettingsSync();
      return;
    }
    localStorage.setItem(COMM_API_ROSTER_PROGRESS_KEY, JSON.stringify(progress));
    commScheduleUserSettingsSync();
  } catch (err) {
    // ignore
  }
}

function commResetKorrelRosterProgress({ log = true } = {}) {
  commKorrelRosterProgress = null;
  commSaveKorrelRosterProgress(null);
  if (log) commSetKorrelLog('Roster progress reset.', { append: true });
}

function commResetKorrelAutoSchedule({ log = true } = {}) {
  commKorrelBackfillNextAt = 0;
  commKorrelOtherLeagueNextAt = 0;
  commUpdateKorrelOtherLeagueCountdown();
  commUpdateKorrelCountdown();
  if (log) {
    commSetKorrelLog('Auto update schedule reset (using current limits).', { append: true });
  }
  commMaybeRunKorrelAutoWork().catch(() => {});
}

function commShouldThrottleKorrelApi(endpoint) {
  const configMin = commParseNumber(commApiLimitConfig?.minIntervalMs);
  const localMin = commParseNumber(commKorrelApiMinIntervalMs);
  const interval = Math.max(
    0,
    Number.isFinite(configMin) ? configMin : (Number.isFinite(localMin) ? localMin : 0)
  );
  if (!interval) return null;
  const last = commKorrelApiLastCallAt.get(endpoint) || 0;
  const now = Date.now();
  const delta = now - last;
  if (delta >= interval) return null;
  return Math.max(0, interval - delta);
}

function commShouldThrottleRosterApi(endpoint) {
  const configMin = commParseNumber(commApiLimitConfig?.minIntervalMs);
  const localMin = commParseNumber(COMM_KORREL_ROSTER_MIN_INTERVAL_MS);
  const interval = Math.max(
    0,
    Number.isFinite(configMin) ? configMin : (Number.isFinite(localMin) ? localMin : 0)
  );
  if (!interval) return null;
  const last = commKorrelApiLastCallAt.get(endpoint) || 0;
  const now = Date.now();
  const delta = now - last;
  if (delta >= interval) return null;
  return Math.max(0, interval - delta);
}

function commSetKorrelApiInputs(endpoints) {
  const container = document.getElementById('commKorrelApiInputs');
  if (!container) return;
  const list = Array.isArray(endpoints) ? endpoints : [endpoints];
  const blocks = list
    .map((endpoint) => {
      const schema = COMM_API_INPUT_SCHEMAS[endpoint];
      if (!schema) {
        return `
          <table class="comm-korrel-schema-table">
            <thead>
              <tr><th colspan="3">${commEscape(endpoint || 'Unknown endpoint')}</th></tr>
              <tr><th>Field</th><th>Accepted values</th><th>Notes</th></tr>
            </thead>
            <tbody>
              <tr><td>-</td><td>-</td><td>No schema available.</td></tr>
            </tbody>
          </table>
        `;
      }
      const rows = schema.rows
        .map((row) => {
          const hasInput = row.field && row.field !== '(none)';
          const inputType = row.values.includes('array') ? 'text' : (row.values.includes('number') ? 'number' : 'text');
          const inputValue = hasInput ? commGetKorrelApiInputValue(endpoint, row.field) : '';
          const inputCell = hasInput
            ? `<input class="comm-korrel-schema-input" type="${inputType}" value="${commEscape(inputValue)}" data-endpoint="${commEscape(endpoint)}" data-field="${commEscape(row.field)}">`
            : '-';
          return `<tr><td>${commEscape(row.field)}</td><td>${commEscape(row.values)}</td><td>${commEscape(row.notes)}</td><td>${inputCell}</td></tr>`;
        })
        .join('');
      return `
        <table class="comm-korrel-schema-table">
          <thead>
            <tr><th colspan="4">${commEscape(schema.title)} (${commEscape(schema.method)})</th></tr>
            <tr><th>Field</th><th>Accepted values</th><th>Notes</th><th>Input</th></tr>
          </thead>
          <tbody>${rows}</tbody>
        </table>
      `;
    })
    .join('');
  container.innerHTML = blocks;
  if (!container.dataset.bound) {
    container.addEventListener('input', (event) => {
      const input = event.target.closest('.comm-korrel-schema-input');
      if (!input) return;
      const endpoint = input.getAttribute('data-endpoint');
      const field = input.getAttribute('data-field');
      if (!endpoint || !field) return;
      input.dataset.userEdited = '1';
      commSetKorrelApiInputValue(endpoint, field, input.value);
    });
    container.dataset.bound = '1';
  }
}

function commSummarizeValue(value, limit = 120) {
  if (value === null || value === undefined) return '-';
  if (typeof value === 'number') return Number.isFinite(value) ? formatCount(value) : '-';
  if (typeof value === 'boolean') return value ? 'true' : 'false';
  if (typeof value === 'string') {
    const text = value.trim();
    if (!text) return '-';
    return text.length > limit ? `${text.slice(0, Math.max(0, limit - 3))}...` : text;
  }
  if (Array.isArray(value)) return `array (${value.length})`;
  if (typeof value === 'object') {
    const keys = Object.keys(value);
    if (!keys.length) return 'object {}';
    const preview = keys.slice(0, 6).join(', ');
    return keys.length > 6 ? `object {${preview}, ...}` : `object {${preview}}`;
  }
  const text = String(value);
  return text.length > limit ? `${text.slice(0, Math.max(0, limit - 3))}...` : text;
}

function commSummarizeObjectValues(value, keyLimit = 6, valueLimit = 60) {
  if (value === null || value === undefined) return '-';
  if (Array.isArray(value)) return `array (${value.length})`;
  if (typeof value !== 'object') return commSummarizeValue(value, valueLimit);
  const keys = Object.keys(value);
  if (!keys.length) return 'object {}';
  const parts = keys.slice(0, keyLimit).map((key) => {
    const rendered = commSummarizeValue(value[key], valueLimit);
    return `${key}:${rendered}`;
  });
  return keys.length > keyLimit ? `${parts.join(', ')} ...` : parts.join(', ');
}

function commExtractApiArrayDetails(response) {
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  const details = [];
  const pushArray = (label, items) => {
    if (Array.isArray(items)) {
      details.push({ label, items });
    }
  };
  if (Array.isArray(inner)) {
    pushArray('Data', inner);
  } else if (inner && typeof inner === 'object') {
    Object.entries(inner).forEach(([key, value]) => {
      if (Array.isArray(value)) pushArray(`Data.${key}`, value);
    });
  }
  return details;
}

function commSetKorrelDetailActions(details) {
  const container = document.getElementById('commKorrelDetailActions');
  if (!container) return;
  commKorrelApiDetailData = Array.isArray(details) ? details : [];
  if (!commKorrelApiDetailData.length) {
    container.innerHTML = '';
    return;
  }
  const buttons = commKorrelApiDetailData
    .map((detail, index) => {
      const label = detail?.label || 'Data';
      const count = Array.isArray(detail?.items) ? detail.items.length : 0;
      return `<button class="btn sm ghost" type="button" data-detail-index="${index}">View ${commEscape(label)} (${count})</button>`;
    })
    .join('');
  container.innerHTML = buttons;
}

function commBuildKorrelDetailTable(items) {
  if (!Array.isArray(items) || items.length === 0) {
    return '<div class="muted">No data available.</div>';
  }
  const objectItems = items.filter((item) => item && typeof item === 'object' && !Array.isArray(item));
  if (!objectItems.length) {
    const rows = items
      .map((item, index) => (
        `<tr><td>${index}</td><td>${commEscape(commCompactJson(item, 500))}</td></tr>`
      ))
      .join('');
    return `
      <table class="comm-korrel-detail-table">
        <thead><tr><th>#</th><th>Value</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    `;
  }
  const keySet = new Set();
  objectItems.forEach((item) => {
    Object.keys(item).forEach((key) => keySet.add(key));
  });
  const keys = Array.from(keySet);
  const header = `<tr><th>#</th>${keys.map((key) => `<th>${commEscape(key)}</th>`).join('')}</tr>`;
  const rows = items
    .map((item, index) => {
      const row = keys
        .map((key) => {
          const value = item && typeof item === 'object' && !Array.isArray(item) ? item[key] : null;
          return `<td>${commEscape(commCompactJson(value, 500))}</td>`;
        })
        .join('');
      return `<tr><td>${index}</td>${row}</tr>`;
    })
    .join('');
  return `
    <table class="comm-korrel-detail-table">
      <thead>${header}</thead>
      <tbody>${rows}</tbody>
    </table>
  `;
}

function commRenderKorrelRawValue(value) {
  if (value === null || value === undefined) return '<span class="muted">-</span>';
  if (typeof value === 'object') {
    let text = '';
    try {
      text = JSON.stringify(value, null, 2);
    } catch (err) {
      text = String(value);
    }
    return `<pre class="comm-korrel-raw-pre">${commEscape(text)}</pre>`;
  }
  return commEscape(String(value));
}

function commBuildKorrelRawTable(raw) {
  if (raw === null || raw === undefined) {
    return '<div class="muted">No raw payload captured yet.</div>';
  }
  if (Array.isArray(raw)) {
    const rows = raw
      .map((item, index) => (
        `<tr><td>${index}</td><td>${commRenderKorrelRawValue(item)}</td></tr>`
      ))
      .join('');
    return `
      <table class="comm-korrel-detail-table comm-korrel-raw-table">
        <thead><tr><th>#</th><th>Value</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    `;
  }
  if (typeof raw === 'object') {
    const rows = Object.entries(raw)
      .map(([key, value]) => (
        `<tr><th scope="row">${commEscape(key)}</th><td>${commRenderKorrelRawValue(value)}</td></tr>`
      ))
      .join('');
    return `
      <table class="comm-korrel-detail-table comm-korrel-raw-table">
        <thead><tr><th>Field</th><th>Value</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    `;
  }
  return `
    <table class="comm-korrel-detail-table comm-korrel-raw-table">
      <thead><tr><th>Value</th></tr></thead>
      <tbody><tr><td>${commRenderKorrelRawValue(raw)}</td></tr></tbody>
    </table>
  `;
}

function commShowKorrelApiDetails(index) {
  const modal = document.getElementById('commKorrelDetailModal');
  const title = document.getElementById('commKorrelDetailTitle');
  const body = document.getElementById('commKorrelDetailTable');
  if (!modal || !title || !body) return;
  const detail = commKorrelApiDetailData[index];
  if (!detail) return;
  const label = detail.label || 'Data';
  title.textContent = `${label} details`;
  body.innerHTML = commBuildKorrelDetailTable(detail.items || []);
  modal.style.display = 'flex';
}

function commShowKorrelRawPayload() {
  const modal = document.getElementById('commKorrelDetailModal');
  const title = document.getElementById('commKorrelDetailTitle');
  const body = document.getElementById('commKorrelDetailTable');
  if (!modal || !title || !body) return;
  const label = commKorrelApiLastRawLabel || 'RAW payload';
  title.textContent = label;
  body.innerHTML = commBuildKorrelRawTable(commKorrelApiLastRawPayload);
  modal.style.display = 'flex';
}

function commHideKorrelApiDetails() {
  const modal = document.getElementById('commKorrelDetailModal');
  if (modal) modal.style.display = 'none';
}

function commBuildApiFields(endpoint, payload, response) {
  const status = response?.status ?? '-';
  const ok = response?.ok ? 'yes' : 'no';
  const method = response?.method ? String(response.method).toUpperCase() : 'POST';
  const meta = response?.meta || {};
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  const outerKeys = raw && typeof raw === 'object' && !Array.isArray(raw) ? Object.keys(raw).join(', ') : '-';
  let innerKeys = '-';
  if (Array.isArray(inner)) {
    innerKeys = `array (${inner.length})`;
  } else if (inner && typeof inner === 'object') {
    innerKeys = Object.keys(inner).join(', ') || '-';
  }
  const message = raw && typeof raw === 'object' ? (raw.message || raw.error || null) : null;
  const rows = [
    ['Method', method],
    ['Endpoint', endpoint],
    ['Status', status],
    ['OK', ok],
    ['Payload', commCompactJson(payload ?? {}, 300)],
    ['Response keys', outerKeys || '-'],
    ['Data keys', innerKeys],
    ['Message', message || '-'],
    ['Auth header used', meta?.usedAuthHeader ? 'yes' : 'no'],
    ['Token cookie', meta?.tokenFound ? 'found' : 'not found']
  ];
  if (Array.isArray(inner)) {
    rows.push(['Data', `array (${inner.length})`]);
    const maxItems = 8;
    inner.slice(0, maxItems).forEach((item, index) => {
      rows.push([`Data[${index}]`, commSummarizeObjectValues(item)]);
    });
    if (inner.length > maxItems) {
      rows.push(['Data (more)', `${inner.length - maxItems} more items`]);
    }
  } else if (inner && typeof inner === 'object') {
    const keys = Object.keys(inner);
    if (!keys.length) {
      rows.push(['Data', 'object {}']);
    } else {
      const limit = 18;
      keys.slice(0, limit).forEach((key) => {
        rows.push([`Data.${key}`, commSummarizeValue(inner[key])]);
      });
      if (keys.length > limit) {
        rows.push(['Data (more)', `${keys.length - limit} more keys`]);
      }
      const arrayEntries = Object.entries(inner).filter(([, value]) => Array.isArray(value));
      const maxArrays = 2;
      const maxItems = 6;
      arrayEntries.slice(0, maxArrays).forEach(([key, value]) => {
        rows.push([`Data.${key} length`, value.length]);
        value.slice(0, maxItems).forEach((item, index) => {
          rows.push([`Data.${key}[${index}]`, commSummarizeObjectValues(item)]);
        });
        if (value.length > maxItems) {
          rows.push([`Data.${key} (more)`, `${value.length - maxItems} more items`]);
        }
      });
    }
  } else if (inner !== undefined) {
    rows.push(['Data', commSummarizeValue(inner)]);
  }
  return rows;
}

function commSetNoticeStatus(message, { error = false, success = false } = {}) {
  const el = document.getElementById('commNoticeStatus');
  if (!el) return;
  el.textContent = message || '';
  if (!message) {
    el.style.color = '';
    return;
  }
  if (error) {
    el.style.color = '#ff6b6b';
  } else if (success) {
    el.style.color = '#7dff8a';
  } else {
    el.style.color = '';
  }
}

function commSetSnapshotStatus(message, { error = false } = {}) {
  const el = document.getElementById('commSnapshotStatus');
  if (!el) return;
  el.textContent = message || '';
  el.style.color = error ? '#ff6b6b' : '';
}

function commLoadSession() {
  try {
    const raw = localStorage.getItem(COMM_SESSION_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (parsed && parsed.clanId && parsed.memberName) return parsed;
  } catch (err) {
    return null;
  }
  return null;
}

function commSaveSession(session) {
  try {
    if (session) {
      localStorage.setItem(COMM_SESSION_KEY, JSON.stringify(session));
    } else {
      localStorage.removeItem(COMM_SESSION_KEY);
    }
  } catch (err) {
    // ignore
  }
}

function commCollectUserSettings() {
  const settings = {};
  COMM_SETTINGS_SYNC_KEYS.forEach((key) => {
    if (!key) return;
    try {
      const value = localStorage.getItem(key);
      if (value !== null && value !== undefined) {
        settings[key] = value;
      }
    } catch (err) {
      // ignore storage errors
    }
  });
  return settings;
}

async function commSaveUserSettingsToDb() {
  if (commSettingsSyncRunning) return;
  if (!hasSupabaseSession()) return;
  commSettingsSyncRunning = true;
  try {
    await ensureHomeAuthUser();
    const userId = getHomeAuthUserId();
    if (!userId) return;
    const settings = commCollectUserSettings();
    const senderAddress = localStorage.getItem(COMM_LICENSE_SENDER_KEY) || null;
    const payload = [{
      user_id: userId,
      user_email: homeGoogleProfile?.email || null,
      settings,
      sender_address: senderAddress,
      updated_at: new Date().toISOString()
    }];
    await commInsert(COMM_TABLE_USER_SETTINGS, payload, { upsert: true, onConflict: 'user_id' });
  } catch (err) {
    // ignore sync errors
  } finally {
    commSettingsSyncRunning = false;
  }
}

function commScheduleUserSettingsSync() {
  if (commSettingsSyncQueued) return;
  commSettingsSyncQueued = true;
  setTimeout(() => {
    commSettingsSyncQueued = false;
    commSaveUserSettingsToDb().catch(() => {});
  }, COMM_SETTINGS_SYNC_DEBOUNCE_MS);
}

async function commLoadUserSettingsFromDb() {
  if (!hasSupabaseSession()) return false;
  try {
    await ensureHomeAuthUser();
    const userId = getHomeAuthUserId();
    if (!userId) return false;
    const rows = await commFetch(
      COMM_TABLE_USER_SETTINGS,
      `select=settings,sender_address&user_id=eq.${encodeURIComponent(userId)}&limit=1`
    );
    const settingsRow = Array.isArray(rows) && rows.length ? rows[0] : null;
    const settings = settingsRow?.settings || null;
    if (!settings || typeof settings !== 'object') return false;
    COMM_SETTINGS_SYNC_KEYS.forEach((key) => {
      if (!Object.prototype.hasOwnProperty.call(settings, key)) return;
      const value = settings[key];
      try {
        if (value === null || value === undefined || value === '') {
          localStorage.removeItem(key);
        } else {
          localStorage.setItem(key, String(value));
        }
      } catch (err) {
        // ignore storage errors
      }
    });
    const senderAddress = settingsRow?.sender_address;
    if (senderAddress) {
      try {
        localStorage.setItem(COMM_LICENSE_SENDER_KEY, String(senderAddress));
      } catch (err) {
        // ignore storage errors
      }
    }
    return true;
  } catch (err) {
    return false;
  }
}

function commStartUserSettingsSyncTimer() {
  if (commSettingsSyncTimer) return;
  commSettingsSyncTimer = setInterval(() => {
    commSaveUserSettingsToDb().catch(() => {});
  }, COMM_SETTINGS_SYNC_INTERVAL_MS);
}

async function commRestoreSessionFromAuth() {
  if (commSession) return commSession;
  if (!hasSupabaseSession()) return null;
  try {
    await ensureHomeAuthUser();
    const userId = getHomeAuthUserId();
    const email = homeGoogleProfile?.email || '';
    if (!userId && !email) return null;
    let query = userId ? `user_id=eq.${encodeURIComponent(userId)}` : `user_email=eq.${encodeURIComponent(email)}`;
    let members = await commFetch(
      COMM_TABLE_MEMBERS,
      `select=id,clan_id,clan_name,user_name,is_owner&${query}&limit=1`
    );
    if ((!Array.isArray(members) || !members.length) && email && userId) {
      query = `user_email=eq.${encodeURIComponent(email)}`;
      members = await commFetch(
        COMM_TABLE_MEMBERS,
        `select=id,clan_id,clan_name,user_name,is_owner&${query}&limit=1`
      );
    }
    if (!Array.isArray(members) || !members.length) return null;
    const member = members[0];
    if (!member?.clan_id || !member?.user_name) return null;
    const clanRows = await commFetch(
      COMM_TABLE_CLANS,
      `select=id,clan_name,created_by,code_hash,${COMM_NOTICE_FIELD}&id=eq.${encodeURIComponent(member.clan_id)}&limit=1`
    );
    const clan = Array.isArray(clanRows) && clanRows.length ? clanRows[0] : null;
    const creator = clan?.created_by || '';
    const memberName = member.user_name;
    const isOwner = member.is_owner === true || (creator && memberName && creator.toLowerCase() === memberName.toLowerCase());
    commSession = {
      clanId: member.clan_id,
      clanName: member.clan_name || clan?.clan_name || '',
      memberName,
      clanCreator: creator,
      isOwner,
      lastCodeWord: '',
      memberId: member.id || null,
      codeHash: clan?.code_hash || null
    };
    commNoticeText = clan?.[COMM_NOTICE_FIELD] || '';
    commSaveSession(commSession);
    commToggleView();
    await commLoadMessages(true);
    await commLoadMembers(true);
    await commLoadNotice();
    commStartTimers();
    return commSession;
  } catch (err) {
    return null;
  }
}

async function commLinkMemberToAuth() {
  if (!commSession || !hasSupabaseSession()) return false;
  try {
    await ensureHomeAuthUser();
    const userId = getHomeAuthUserId();
    if (!userId) return false;
    const email = homeGoogleProfile?.email || null;
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&user_name=eq.${encodeURIComponent(commSession.memberName)}`;
    await commPatch(COMM_TABLE_MEMBERS, query, {
      user_id: userId,
      user_email: email
    });
    return true;
  } catch (err) {
    return false;
  }
}

function commLoadAlertMute() {
  try {
    commAlertMuted = localStorage.getItem(COMM_ALERT_MUTE_KEY) === '1';
  } catch (err) {
    commAlertMuted = false;
  }
}

function commUpdateAlertMuteButtons() {
  const label = commAlertMuted ? 'Unmute alert' : 'Mute alert';
  const liveBtn = document.getElementById('miniCommMuteAlertBtn');
  if (liveBtn) liveBtn.textContent = label;
  const toolsBtn = document.getElementById('commMuteAlertBtn');
  if (toolsBtn) toolsBtn.textContent = label;
  const floatingBtn = document.getElementById('floatingCommMuteBtn');
  if (floatingBtn) floatingBtn.textContent = commAlertMuted ? 'Unmute' : 'Mute';
}

function commToggleAlertMute() {
  commAlertMuted = !commAlertMuted;
  try {
    localStorage.setItem(COMM_ALERT_MUTE_KEY, commAlertMuted ? '1' : '0');
    commScheduleUserSettingsSync();
  } catch (err) {
    // ignore
  }
  commUpdateAlertMuteButtons();
}

function commLoadHighAlert() {
  try {
    const stored = localStorage.getItem(COMM_HIGH_ALERT_KEY);
    if (stored == null) {
      localStorage.setItem(COMM_HIGH_ALERT_KEY, '0');
      commHighAlertEnabled = false;
      return;
    }
    commHighAlertEnabled = stored === '1';
  } catch (err) {
    commHighAlertEnabled = false;
  }
}

function commUpdateHighAlertButtons() {
  const label = commHighAlertEnabled ? 'Disable 4+ alerts' : 'Enable 4+ alerts';
  const liveBtn = document.getElementById('miniCommHighAlertBtn');
  if (liveBtn) liveBtn.textContent = label;
  const toolsBtn = document.getElementById('commHighAlertBtn');
  if (toolsBtn) toolsBtn.textContent = label;
}

function commToggleHighAlert() {
  commHighAlertEnabled = !commHighAlertEnabled;
  commHighAlertPending = commHighAlertEnabled;
  try {
    localStorage.setItem(COMM_HIGH_ALERT_KEY, commHighAlertEnabled ? '1' : '0');
    commScheduleUserSettingsSync();
  } catch (err) {
    // ignore
  }
  commUpdateHighAlertButtons();
}

function commParseMultiplierValue(raw) {
  const value = parseNumericValue(raw);
  return Number.isFinite(value) ? value : null;
}

let commHighAlertAudioContext = null;
function commPlayHighAlertSound() {
  try {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) return;
    if (!commHighAlertAudioContext) commHighAlertAudioContext = new AudioCtx();
    if (commHighAlertAudioContext.state === 'suspended') {
      commHighAlertAudioContext.resume().catch(() => {});
    }
    const ctx = commHighAlertAudioContext;
    const now = ctx.currentTime;
    const duration = 3;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(880, now);
    gain.gain.setValueAtTime(0.0001, now);
    gain.gain.exponentialRampToValueAtTime(0.35, now + 0.05);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
    osc.connect(gain).connect(ctx.destination);
    osc.start(now);
    osc.stop(now + duration);
  } catch (err) {
    // ignore
  }
}

function commMaybeRingHighAlert(blockId, multiplier) {
  if (!commHighAlertEnabled) return;
  const value = commParseMultiplierValue(multiplier);
  if (!value || value < 4) return;
  const normalizedBlockId = normalizeBlockId(blockId);
  const now = Date.now();
  if (normalizedBlockId) {
    if (commHighAlertLastBlockId === normalizedBlockId && !commHighAlertPending) return;
    commHighAlertLastBlockId = normalizedBlockId;
    commHighAlertPending = false;
  } else {
    if (!commHighAlertPending && now - commHighAlertLastFiredAt < 60000) return;
    commHighAlertPending = false;
    commHighAlertLastFiredAt = now;
  }
  commPlayHighAlertSound();
}

commLoadAlertMute();
commLoadHighAlert();

async function commResolveClanMemberName() {
  const storedAlias = commGetPersonalUsername();
  if (storedAlias) return storedAlias;
  try {
    await commResolveGominingUserId({ force: true });
  } catch (err) {
    // ignore lookup errors
  }
  const alias = commGetPersonalUsername();
  if (alias) return alias;
  return '';
}

async function commHashCode(word) {
  const value = (word || '').trim();
  if (!value) return '';
  const data = new TextEncoder().encode(value);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, '0')).join('');
}

async function commGetHeaders() {
  const headers = { ...COMM_HEADERS };
  try {
    loadHomeAuthSession();
    let session = homeAuthSession;
    if (session && isHomeAuthSessionExpired(session) && session.refresh_token) {
      const refreshed = await refreshSupabaseSession(session.refresh_token);
      if (refreshed?.access_token) {
        session = {
          access_token: refreshed.access_token,
          refresh_token: refreshed.refresh_token || session.refresh_token,
          token_type: refreshed.token_type,
          expires_at: refreshed.expires_at || Math.floor(Date.now() / 1000) + Number(refreshed.expires_in || 0)
        };
        saveHomeAuthSession(session);
      }
    }
    const token = session?.access_token;
    if (token) {
      headers.Authorization = `Bearer ${token}`;
    }
  } catch (err) {
    /* ignore */
  }
  headers.apikey = COMM_SUPABASE_KEY;
  if (!headers.Authorization) {
    headers.Authorization = `Bearer ${COMM_SUPABASE_KEY}`;
  }
  return headers;
}

async function commInsert(table, rows, { upsert = false, onConflict = null } = {}) {
  if (!Array.isArray(rows) || rows.length === 0) return { inserted: 0 };
  const headers = await commGetHeaders();
  if (upsert || onConflict) {
    const prefer = (headers.Prefer || '').split(',').map((s) => s.trim()).filter(Boolean);
    if (!prefer.includes('resolution=merge-duplicates')) {
      prefer.push('resolution=merge-duplicates');
    }
    headers.Prefer = prefer.join(',');
  }
  let url = `${COMM_SUPABASE_URL}/${table}`;
  if (onConflict) {
    const param = `on_conflict=${encodeURIComponent(onConflict)}`;
    url += url.includes('?') ? `&${param}` : `?${param}`;
  }
  url = commWithApiKey(url);
  const res = await fetch(url, {
    method: 'POST',
    headers,
    body: JSON.stringify(rows)
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `Supabase insert failed (${res.status})`);
  }
  const payload = await res.json().catch(() => null);
  return { inserted: rows.length, payload };
}

async function commFetch(table, query = '') {
  const qs = query ? (query.startsWith('?') ? query : `?${query}`) : '';
  const headers = await commGetHeaders();
  const res = await fetch(commWithApiKey(`${COMM_SUPABASE_URL}/${table}${qs}`), {
    method: 'GET',
    headers
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `Supabase fetch failed (${res.status})`);
  }
  return res.json().catch(() => []);
}

async function commFetchWithCount(table, query = '') {
  const qs = query ? (query.startsWith('?') ? query : `?${query}`) : '';
  const headers = { ...(await commGetHeaders()), Prefer: 'count=exact' };
  const res = await fetch(commWithApiKey(`${COMM_SUPABASE_URL}/${table}${qs}`), {
    method: 'GET',
    headers
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `Supabase fetch failed (${res.status})`);
  }
  const rows = await res.json().catch(() => []);
  const range = res.headers.get('content-range') || res.headers.get('Content-Range') || '';
  let count = null;
  if (range && range.includes('/')) {
    const total = Number(range.split('/').pop());
    count = Number.isFinite(total) ? total : null;
  }
  return { rows: Array.isArray(rows) ? rows : [], count };
}

async function commDelete(table, query = '') {
  const qs = query ? (query.startsWith('?') ? query : `?${query}`) : '';
  const headers = { ...(await commGetHeaders()), Prefer: 'return=minimal' };
  const res = await fetch(commWithApiKey(`${COMM_SUPABASE_URL}/${table}${qs}`), {
    method: 'DELETE',
    headers
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `Supabase delete failed (${res.status})`);
  }
  return true;
}

async function commPatch(table, query, payload) {
  const qs = query ? (query.startsWith('?') ? query : `?${query}`) : '';
  const headers = await commGetHeaders();
  const res = await fetch(commWithApiKey(`${COMM_SUPABASE_URL}/${table}${qs}`), {
    method: 'PATCH',
    headers,
    body: JSON.stringify(payload)
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(text || `Supabase update failed (${res.status})`);
  }
  return res.json().catch(() => null);
}

function commToggleView() {
  const publicPanel = document.getElementById('clanCommPublic');
  const privatePanel = document.getElementById('clanCommPrivate');
  const intro = document.getElementById('commIntro');
  const label = document.getElementById('commClanLabel');
  const codeControls = document.getElementById('commCodeControls');
  const codeInput = document.getElementById('commCodeInput');
  const snapshotCard = document.getElementById('commSnapshotCard');
  const noticeCard = document.getElementById('commNoticeCard');
  const noticeInput = document.getElementById('commNoticeInput');
  const noticeSave = document.getElementById('commNoticeSaveBtn');
  const korrelTools = document.getElementById('commKorrelTools');

  if (!publicPanel || !privatePanel) return;

  if (commSession) {
    publicPanel.style.display = 'none';
    privatePanel.style.display = 'block';
    if (intro) intro.style.display = 'none';
    if (label) label.textContent = `Clan: ${commSession.clanName}  -  You: ${commSession.memberName}`;
    const isOwner = !!commSession.isOwner;
    if (codeControls) codeControls.style.display = isOwner ? 'grid' : 'none';
    if (codeInput) codeInput.value = isOwner ? (commSession.lastCodeWord || '') : '';
    if (snapshotCard) snapshotCard.style.display = isOwner ? 'block' : 'none';
    if (korrelTools) korrelTools.style.display = commIsKorrel() ? 'block' : 'none';
    if (commIsKorrel()) {
      commStartKorrelOverviewTimer();
    } else if (commKorrelOverviewTimer) {
      clearInterval(commKorrelOverviewTimer);
      commKorrelOverviewTimer = null;
    }

    if (noticeCard) noticeCard.style.display = commNoticeAvailable ? 'block' : 'none';
    if (noticeInput) {
      noticeInput.value = commNoticeText || '';
      noticeInput.disabled = !commNoticeAvailable;
      noticeInput.readOnly = !commNoticeAvailable || !isOwner;
      noticeInput.placeholder = commNoticeAvailable
        ? 'Only owners can edit this notice...'
        : 'Clan notices unavailable.';
    }
    if (noticeSave) {
      noticeSave.style.display = commNoticeAvailable && isOwner ? 'inline-flex' : 'none';
      noticeSave.disabled = !commNoticeAvailable || !isOwner;
    }

    commSetNoticeStatus('');
    commSetSnapshotStatus('');
  } else {
    publicPanel.style.display = 'block';
    privatePanel.style.display = 'none';
    if (intro) intro.style.display = '';
    if (codeControls) codeControls.style.display = 'none';
    if (codeInput) codeInput.value = '';
    if (noticeCard) noticeCard.style.display = 'none';
    if (noticeInput) {
      noticeInput.value = '';
      noticeInput.disabled = true;
      noticeInput.readOnly = true;
      noticeInput.placeholder = commNoticeAvailable
        ? 'Only owners can edit this notice...'
        : 'Clan notices unavailable.';
    }
    if (noticeSave) {
      noticeSave.style.display = 'none';
      noticeSave.disabled = true;
    }
  if (snapshotCard) snapshotCard.style.display = 'none';
  if (korrelTools) korrelTools.style.display = 'none';
  commSetNoticeStatus('');
  commSetSnapshotStatus('');
  commSetKorrelLog('');
  }

  updateAdminToolsVisibility();
  commRenderChat();
  commRenderMembers();
  commRenderSnapshots();
}

function commResetSession() {
  commSession = null;
  commMessages = [];
  commMessageIds = new Set();
  commMembers = [];
  commSnapshots = [];
  commNoticeText = '';
  commSaveSession(null);
  if (commChatTimer) {
    clearInterval(commChatTimer);
    commChatTimer = null;
  }
  if (commMetaTimer) {
    clearInterval(commMetaTimer);
    commMetaTimer = null;
  }
  if (commKorrelCountdownTimer) {
    clearInterval(commKorrelCountdownTimer);
    commKorrelCountdownTimer = null;
  }
  if (commKorrelRemoteLogTimer) {
    clearInterval(commKorrelRemoteLogTimer);
    commKorrelRemoteLogTimer = null;
  }
  if (commKorrelAutoTimer) {
    clearInterval(commKorrelAutoTimer);
    commKorrelAutoTimer = null;
  }
  if (commKorrelStartupSnapshotTimer) {
    clearTimeout(commKorrelStartupSnapshotTimer);
    commKorrelStartupSnapshotTimer = null;
  }
  if (commKorrelRosterResumeTimer) {
    clearTimeout(commKorrelRosterResumeTimer);
    commKorrelRosterResumeTimer = null;
  }
  commKorrelRosterResumeAt = 0;
  commKorrelStartupSnapshotDone = false;
  commKorrelNextSnapshotAt = null;
  commKorrelRoundCount = 0;
  commKorrelLastRoundBlockId = null;
  commKorrelRoundSnapshotPending = false;
  commKorrelCurrentLeagueId = null;
  commKorrelOtherLeagueLastAt = 0;
  commKorrelOtherLeagueIndex = 0;
  commKorrelOtherLeagueRunning = false;
  commKorrelOtherLeagueNextAt = 0;
  commKorrelRoundQueue = [];
  commKorrelRoundQueueIndex = 0;
  commKorrelRoundQueueCycleId = null;
  commKorrelRoundQueueKey = '';
  commKorrelRosterAutoNextAt = 0;
  commKorrelRemoteLogLastId = 0;
  commKorrelRemoteLogFetching = false;
  commToggleView();
}

async function commLoadNotice() {
  if (!commSession) {
    commNoticeText = '';
    commToggleView();
    return;
  }
  try {
    const query = `select=id,${COMM_NOTICE_FIELD}&id=eq.${encodeURIComponent(commSession.clanId)}`;
    const rows = await commFetch(COMM_TABLE_CLANS, query);
    commNoticeText = (rows && rows[0] ? rows[0][COMM_NOTICE_FIELD] : '') || '';
    commToggleView();
  } catch (err) {
    if (String(err).includes(COMM_NOTICE_FIELD)) {
      commNoticeAvailable = false;
      commToggleView();
      commSetNoticeStatus('Clan notices unavailable (missing notice_text column).', { error: true });
    }
  }
}

async function commLoadSnapshots(force = false) {
  if (!commSession) {
    commSnapshots = [];
    commRenderSnapshots();
    return;
  }
  if (commSnapshotsLoading && !force) return;
  commSnapshotsLoading = true;
  commRenderSnapshots();
  try {
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&order=captured_at.desc&limit=3`;
    const snapshots = await commFetch(COMM_TABLE_SNAPSHOTS, query);
    const compiled = [];
    for (const snapshot of snapshots || []) {
      const members = await commFetch(
        COMM_TABLE_SNAPSHOT_MEMBERS,
        `snapshot_id=eq.${encodeURIComponent(snapshot.id)}&order=user_name.asc`
      );
      compiled.push({ ...snapshot, members: members || [] });
    }
    commSnapshots = compiled;
    commRenderSnapshots();
  } catch (err) {
    commSetSnapshotStatus('Unable to load snapshots.', { error: true });
  } finally {
    commSnapshotsLoading = false;
    commRenderSnapshots();
  }
}

const COMM_PING_DISPLAY_COOLDOWN_MS = 10 * 60 * 1000;
const COMM_PING_DISPLAY_TTL_MS = 10 * 60 * 1000;
const COMM_MINI_CHAT_LIMIT = 5;
const GOMINING_CHAT_POLL_MS = 20000;
const GOMINING_CHAT_LIMIT = 20;
const GOMINING_CHAT_ENDPOINT = 'https://api.gomining.com/api/nft-game/chat-message/find-by-destination';

function commFilterVisibleMessages(messages) {
  if (!Array.isArray(messages)) return [];
  const lastPingBySender = new Map();
  const now = Date.now();
  return messages.filter((msg) => {
    if (!msg || msg.kind !== 'ping') return true;
    const senderKey = msg.sender || msg.content || 'ping';
    const time = msg.created_at ? new Date(msg.created_at).getTime() : 0;
    const stamp = Number.isFinite(time) && time > 0 ? time : now;
    if ((now - stamp) > COMM_PING_DISPLAY_TTL_MS) {
      return false;
    }
    const last = lastPingBySender.get(senderKey);
    if (last && (stamp - last) < COMM_PING_DISPLAY_COOLDOWN_MS) {
      return false;
    }
    lastPingBySender.set(senderKey, stamp);
    return true;
  });
}

function commRenderChat() {
  const chat = document.getElementById('commChat');
  if (!chat) {
    renderMiniClanComm();
    commRenderFloatingChat();
    return;
  }
  if (miniCommContainer && miniCommContainer.isConnected) {
    miniCommContainer.style.display = 'none';
  }
  if (!commSession) {
    chat.innerHTML = '<div class="muted">Join a clan to unlock chat.</div>';
    return;
  }
  const visibleMessages = commFilterVisibleMessages(commMessages);
  if (!visibleMessages.length) {
    chat.innerHTML = '<div class="muted">No clan messages yet.</div>';
    return;
  }
    const html = visibleMessages
      .map((msg) => {
        const stamp = commFormatChatStampShort(msg.created_at);
        const sender = commEscape(msg.sender || 'Unknown');
        const content = msg.content ? commEscape(msg.content) : (msg.kind === 'ping' ? 'Clan ping' : '');
        const pingClass = msg.kind === 'ping' ? ' ping' : '';
        return `<div class="comm-message${pingClass}"><span class="muted">[${stamp}]</span> <strong>${sender}</strong>: ${content}</div>`;
      })
      .join('');
  chat.innerHTML = html;
  chat.scrollTop = chat.scrollHeight;
  commRenderFloatingChat();
}

function commFormatChatTime(value) {
  if (!value) return '';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return '';
  return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' });
}

function commFormatChatStampShort(value) {
  if (!value) return '';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return '';
  const day = String(date.getDate()).padStart(2, '0');
  const month = date.toLocaleString('en-US', { month: 'short' }).toLowerCase();
  const time = date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit' });
  return `${day}${month} ${time}`;
}

function commFormatChatAge(value) {
  const ts = value ? new Date(value).getTime() : NaN;
  if (!Number.isFinite(ts)) return '';
  const delta = Math.max(0, Date.now() - ts);
  const mins = Math.floor(delta / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ago`;
  const days = Math.floor(hrs / 24);
  return `${days}d ago`;
}

function commExtractGominingChatItems(payload) {
  if (!payload) return [];
  const root = payload?.data ?? payload?.result ?? payload;
  const candidate =
    root?.items ||
    root?.messages ||
    root?.rows ||
    root?.data ||
    root;
  return Array.isArray(candidate) ? candidate : [];
}

function commNormalizeGominingChatRow(item) {
  if (!item || typeof item !== 'object') return null;
  const id =
    item?.id ??
    item?._id ??
    item?.messageId ??
    item?.message_id ??
    item?.uuid ??
    null;
  const createdAt =
    item?.createdAt ??
    item?.created_at ??
    item?.timestamp ??
    item?.time ??
    item?.date ??
    null;
  const sender =
    item?.author?.alias ??
    item?.author?.name ??
    item?.user?.alias ??
    item?.user?.name ??
    item?.sender?.alias ??
    item?.sender?.name ??
    item?.sender ??
    item?.userName ??
    item?.username ??
    'Unknown';
  const text =
    item?.text ??
    item?.message ??
    item?.content ??
    item?.body ??
    item?.msg ??
    '';
  const row = {
    id: id != null ? String(id) : null,
    createdAt,
    sender,
    text,
    raw: item,
    kind: 'chat'
  };
  row.id = commChatKey(row);
  return row;
}

function commChatKey(row) {
  if (!row) return '';
  if (row.id != null && String(row.id).trim()) return String(row.id);
  const createdAt = row.createdAt || row.created_at || Date.now();
  const sender = row.sender || '';
  const text = row.text || '';
  return `local:${createdAt}:${sender}:${text}`;
}

function clearBoostNotificationSeen() {
  boostNotificationSeen = new Set();
}

function clearBoostNotificationCounts() {
  boostNotificationCounts = new Map();
}

function buildBoostNotificationKey(clanEntry, boostType) {
  if (!clanEntry) return '';
  const key = normalizeBoostKey(boostType);
  if (!key || key === 'unknown') return '';
  const clanKey = normalizeClanKey(clanEntry);
  if (!clanKey) return '';
  return `${clanKey}:${key}`;
}

function setBoostNotificationCount(clanEntry, boostType, nextCount) {
  const mapKey = buildBoostNotificationKey(clanEntry, boostType);
  if (!mapKey) return;
  const safeNext = Number(nextCount) || 0;
  const prev = boostNotificationCounts.get(mapKey) || 0;
  if (safeNext > prev) {
    boostNotificationCounts.set(mapKey, safeNext);
  }
}

function notifyBoostCountIncrease(clanEntry, boostType, nextCount, source, ts) {
  if (!clanEntry || !isKnownBoostType(boostType)) return;
  const mapKey = buildBoostNotificationKey(clanEntry, boostType);
  if (!mapKey) return;
  const safeNext = Number(nextCount) || 0;
  const prev = boostNotificationCounts.get(mapKey) || 0;
  if (safeNext <= prev) return;
  boostNotificationCounts.set(mapKey, safeNext);
  const delta = safeNext - prev;
  const eventTs = normalizeTimestampMs(ts) || Date.now();
  const clanLabel =
    clanEntry?.name
    || clanEntry?.clan_name
    || clanEntry?.clanName
    || (clanEntry?.clan_id ? `Clan ${clanEntry.clan_id}` : null);
  registerBoostNotification({
    ts: eventTs,
    boostType,
    clanName: clanLabel,
    count: delta,
    source: source || 'state:boosts'
  });
}

function registerBoostNotification({ ts, boostType, clanName, playerName, count, source } = {}) {
  if (!isKnownBoostType(boostType)) return;
  const normalizedTs = normalizeTimestampMs(ts);
  const roundStartAt = getCurrentRoundStartMs();
  if (Number.isFinite(roundStartAt) && Number.isFinite(normalizedTs) && normalizedTs < roundStartAt - LIVE_BOOST_TS_SKEW_MS) {
    return;
  }
  const cleanClan = clanName ? commCleanClanDisplayName(clanName) : null;
  const normalizedName = cleanClan || clanName || null;
  const safePlayer = playerName && playerName !== 'Unknown' && playerName !== '__local__' ? playerName : null;
  const dedupeKey = [
    normalizeBoostKey(boostType),
    Number.isFinite(normalizedTs) ? Math.trunc(normalizedTs) : 'na',
    normalizedName || '',
    safePlayer || ''
  ].join('|');
  if (boostNotificationSeen.has(dedupeKey)) return;
  boostNotificationSeen.add(dedupeKey);
  const boostLabel = formatBoostDisplayName(boostType) || boostType || 'Boost';
  const countLabel = Number.isFinite(count) && count > 1 ? ` x${count}` : '';
  const sender = safePlayer || normalizedName || 'Unknown';
  const createdAt = Number.isFinite(normalizedTs) ? new Date(normalizedTs).toISOString() : new Date().toISOString();
  commPushGominingChatMessage({
    createdAt,
    sender,
    text: `activated ${boostLabel}${countLabel}`.trim(),
    kind: 'boost',
    source: source || null
  });
}

function commPushGominingChatMessage(message) {
  if (!message) return;
  const createdAt = message.createdAt || message.created_at || new Date().toISOString();
  const sender = message.sender || 'Unknown';
  const text = message.text || '';
  if (!text && !sender) return;
  const row = {
    id: message.id != null ? String(message.id) : null,
    createdAt,
    sender,
    text,
    kind: message.kind || 'boost'
  };
  row.id = commChatKey(row);
  if (gominingChatMessageIds.has(row.id)) return;
  gominingChatMessageIds.add(row.id);
  gominingChatMessages = [...gominingChatMessages, row];
  gominingChatMessages.sort((a, b) => {
    const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
    const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
    return ta - tb;
  });
  gominingChatMessages = gominingChatMessages.slice(-GOMINING_CHAT_LIMIT);
  gominingChatLastFetchAt = Date.now();
  gominingChatLastError = null;
  commRenderGominingChatNotifications();
}

function commRenderGominingChatNotifications() {
  const list = document.getElementById('gominingChatList');
  const status = document.getElementById('gominingChatStatus');
  if (!list) return;
  if (status) {
    if (gominingChatLastError) {
      status.textContent = gominingChatLastError;
    } else if (gominingChatLastFetchAt) {
      const age = commFormatChatAge(gominingChatLastFetchAt);
      status.textContent = age ? `Last update: ${age}` : 'Boosts synced.';
    } else {
      status.textContent = 'Waiting for boost notifications...';
    }
  }
  if (!gominingChatMessages.length) {
    list.innerHTML = '<div class="muted">No notifications yet.</div>';
    return;
  }
  const html = gominingChatMessages
    .map((msg) => {
      const timeLabel = commFormatChatTime(msg.createdAt);
      const ageLabel = commFormatChatAge(msg.createdAt);
      const timeBlock = timeLabel ? `[${timeLabel}${ageLabel ? `  -  ${ageLabel}` : ''}]` : '';
      const sender = commEscape(msg.sender || 'Unknown');
      const text = msg.text ? commEscape(msg.text) : '';
      if (msg.kind === 'boost') {
        return `<div class="comm-message boost-msg"><span class="muted">${timeBlock}</span> <strong>${sender}</strong> ${text}</div>`;
      }
      if (msg.kind === 'round') {
        return `<div class="comm-message round-msg"><span class="muted">${timeBlock}</span> <strong>${sender}</strong> ${text}</div>`;
      }
      return `<div class="comm-message"><span class="muted">${timeBlock}</span> <strong>${sender}</strong>: ${text}</div>`;
    })
    .join('');
  list.innerHTML = html;
  list.scrollTop = list.scrollHeight;
}

async function commFetchGominingChatNotifications({ force = false } = {}) {
  if (gominingChatPollPending) return;
  const now = Date.now();
  if (!force && gominingChatLastFetchAt && now - gominingChatLastFetchAt < GOMINING_CHAT_POLL_MS) {
    return;
  }
  gominingChatPollPending = true;
  gominingChatLastError = null;
  try {
    const tab = typeof commFindMiningWarsTabWithAuth === 'function'
      ? await commFindMiningWarsTabWithAuth()
      : await commFindMiningWarsTab();
    if (!tab?.id) {
      gominingChatLastError = 'Open Mining Wars to sync boosts.';
      commRenderGominingChatNotifications();
      return;
    }
    const payload = { limit: GOMINING_CHAT_LIMIT, destination: 'clan' };
    const response = await commFetchGominingApiViaInjection(tab.id, GOMINING_CHAT_ENDPOINT, payload, { method: 'POST' });
    if (!response?.ok) {
      const status = Number.isFinite(commParseNumber(response?.status)) ? Math.trunc(commParseNumber(response?.status)) : 0;
      gominingChatLastError = status ? `Boost sync failed (${status}).` : 'Boost sync failed.';
      commRenderGominingChatNotifications();
      return;
    }
    const items = commExtractGominingChatItems(response?.data ?? response);
    const normalized = items
      .map(commNormalizeGominingChatRow)
      .filter((row) => row && (row.text || row.sender));
    const localNotifs = gominingChatMessages.filter((row) => row && (row.kind === 'boost' || row.kind === 'round'));
    const merged = [...localNotifs, ...normalized];
    const deduped = [];
    const seen = new Set();
    merged.forEach((row) => {
      const key = commChatKey(row);
      if (!key || seen.has(key)) return;
      seen.add(key);
      deduped.push({ ...row, id: key });
    });
    deduped.sort((a, b) => {
      const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
      const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
      return ta - tb;
    });
    gominingChatMessages = deduped.slice(-GOMINING_CHAT_LIMIT);
    gominingChatMessageIds = new Set(gominingChatMessages.map((row) => row.id).filter(Boolean));
    gominingChatLastFetchAt = Date.now();
    commRenderGominingChatNotifications();
  } catch (err) {
    gominingChatLastError = err?.message || 'Boost sync failed.';
    commRenderGominingChatNotifications();
  } finally {
    gominingChatPollPending = false;
  }
}

function commStartGominingChatNotifications() {
  if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) return;
  if (gominingChatTimer) clearInterval(gominingChatTimer);
  commFetchGominingChatNotifications({ force: true }).catch(() => {});
  gominingChatTimer = setInterval(() => {
    commFetchGominingChatNotifications().catch(() => {});
  }, GOMINING_CHAT_POLL_MS);
}

function ensureMiniCommContainer() {
  if (miniCommContainer && miniCommContainer.isConnected) return miniCommContainer;
  const currentRoundGrid = document.getElementById('currentRoundGrid');
  if (!currentRoundGrid || !currentRoundGrid.parentElement) return null;
  const container = document.createElement('div');
  container.id = 'miniCommBar';
  container.className = 'mini-comm-bar';
  currentRoundGrid.parentElement.insertBefore(container, currentRoundGrid);
  miniCommContainer = container;
  return container;
}

function renderMiniClanComm() {
  if (miniCommContainer && miniCommContainer.isConnected) {
    miniCommContainer.remove();
  } else {
    const existing = document.getElementById('miniCommBar');
    if (existing) existing.remove();
  }
  return;
}

function loadFloatingChatState() {
  if (floatingChatState) return floatingChatState;
  const fallback = {
    x: Math.max(24, window.innerWidth - 140),
    y: Math.max(24, window.innerHeight - 140),
    width: 320,
    height: 360,
    collapsed: true
  };
  try {
    const raw = localStorage.getItem(FLOATING_CHAT_STATE_KEY);
    if (!raw) {
      floatingChatState = fallback;
      return floatingChatState;
    }
    const parsed = JSON.parse(raw);
    floatingChatState = {
      ...fallback,
      ...parsed
    };
  } catch (err) {
    floatingChatState = fallback;
  }
  return floatingChatState;
}

function saveFloatingChatState() {
  if (!floatingChatState) return;
  try {
    localStorage.setItem(FLOATING_CHAT_STATE_KEY, JSON.stringify(floatingChatState));
  } catch (err) {
    // ignore
  }
}

function getBoostHelperDefaults() {
  return {
    phases: {
      early: 50,
      afterEarly: 120,
      mid: 240
    },
    soundEnabled: false,
    soundVolume: 0.6,
    rules: []
  };
}

function loadBoostHelperConfig() {
  const fallback = getBoostHelperDefaults();
  try {
    const raw = localStorage.getItem(BOOST_HELPER_CONFIG_KEY);
    if (!raw) return fallback;
    const parsed = JSON.parse(raw);
    const rawRules = Array.isArray(parsed?.rules) ? parsed.rules : fallback.rules;
    return {
      ...fallback,
      ...parsed,
      phases: { ...fallback.phases, ...(parsed?.phases || {}) },
      rules: rawRules.filter((rule) => boostHelperIsSupportedRule(rule))
    };
  } catch (err) {
    return fallback;
  }
}

function saveBoostHelperConfig(config) {
  try {
    localStorage.setItem(BOOST_HELPER_CONFIG_KEY, JSON.stringify(config));
  } catch (err) {
    // ignore
  }
}

function loadFloatingBoostHelperState() {
  const fallback = {
    x: Math.max(24, window.innerWidth - 180),
    y: Math.max(24, window.innerHeight - 220),
    width: 320,
    height: 220,
    visible: false
  };
  try {
    const raw = localStorage.getItem(FLOATING_BOOST_HELPER_STATE_KEY);
    if (!raw) return fallback;
    const parsed = JSON.parse(raw);
    return {
      ...fallback,
      ...parsed
    };
  } catch (err) {
    return fallback;
  }
}

function saveFloatingBoostHelperState(state) {
  try {
    localStorage.setItem(FLOATING_BOOST_HELPER_STATE_KEY, JSON.stringify(state));
  } catch (err) {
    // ignore
  }
}

function ensureFloatingChatStyles() {
  if (document.getElementById('floatingClanChatStyles')) return;
  const style = document.createElement('style');
  style.id = 'floatingClanChatStyles';
  style.textContent = `
    .floating-chat {
      position: fixed;
      z-index: 2147483647;
      background: rgba(11, 18, 32, 0.92);
      border: 1px solid rgba(148, 163, 184, 0.4);
      border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.45);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      backdrop-filter: blur(10px);
    }
    .floating-chat.hidden {
      display: none;
    }
    .floating-chat-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      padding: 8px 10px;
      background: rgba(30, 41, 59, 0.6);
      cursor: move;
    }
    .floating-chat-title {
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: #e2e8f0;
      white-space: nowrap;
    }
    .floating-chat-actions {
      display: flex;
      gap: 6px;
      align-items: center;
    }
    .floating-chat-actions button {
      font-size: 0.7rem;
      padding: 2px 8px;
      border-radius: 999px;
      border: 1px solid rgba(148, 163, 184, 0.4);
      background: rgba(15, 23, 42, 0.6);
      color: #e2e8f0;
    }
    .floating-chat-body {
      flex: 1;
      overflow-y: auto;
      padding: 8px 10px;
      font-size: 0.75rem;
    }
    .floating-chat-input {
      display: flex;
      gap: 6px;
      padding: 8px 10px;
      background: rgba(15, 23, 42, 0.65);
    }
    .floating-chat-input input {
      flex: 1;
      padding: 6px 8px;
      border-radius: 10px;
      border: 1px solid rgba(148, 163, 184, 0.3);
      background: rgba(15, 23, 42, 0.5);
      color: #e2e8f0;
      font-size: 0.75rem;
    }
    .floating-chat-input button {
      padding: 6px 10px;
      border-radius: 10px;
      font-size: 0.72rem;
    }
    .floating-chat-resize {
      position: absolute;
      width: 14px;
      height: 14px;
      right: 6px;
      bottom: 6px;
      cursor: nwse-resize;
      border-radius: 4px;
      background: rgba(148, 163, 184, 0.4);
    }
    .floating-wins {
      position: fixed;
      z-index: 2147483647;
      background: rgba(11, 18, 32, 0.95);
      border: 1px solid rgba(148, 163, 184, 0.4);
      border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.45);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      backdrop-filter: blur(10px);
    }
    .floating-wins-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      padding: 8px 10px;
      background: rgba(30, 41, 59, 0.6);
      cursor: move;
    }
    .floating-wins-title {
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: #e2e8f0;
      white-space: nowrap;
    }
    .floating-wins-body {
      padding: 10px;
      font-size: 0.8rem;
      display: grid;
      gap: 8px;
    }
    .floating-wins-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
    }
    .floating-wins-resize {
      position: absolute;
      width: 14px;
      height: 14px;
      right: 6px;
      bottom: 6px;
      cursor: nwse-resize;
      border-radius: 4px;
      background: rgba(148, 163, 184, 0.4);
    }
    .floating-boost-helper {
      position: fixed;
      z-index: 2147483647;
      background: rgba(11, 18, 32, 0.96);
      border: 1px solid rgba(148, 163, 184, 0.45);
      border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.45);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      backdrop-filter: blur(10px);
      min-width: 260px;
      min-height: 180px;
    }
    .floating-boost-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      padding: 8px 10px;
      background: rgba(30, 41, 59, 0.6);
      cursor: move;
    }
    .floating-boost-title {
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: #e2e8f0;
    }
    .floating-boost-body {
      padding: 10px;
      display: grid;
      gap: 8px;
      font-size: 0.8rem;
    }
    .floating-boost-meta {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      font-size: 0.75rem;
      color: rgba(226, 232, 240, 0.8);
    }
    .floating-boost-list {
      display: grid;
      gap: 6px;
    }
    .floating-boost-group {
      display: grid;
      gap: 6px;
      padding: 6px;
      border-radius: 10px;
      background: rgba(15, 23, 42, 0.45);
      border: 1px solid rgba(148, 163, 184, 0.25);
    }
    .floating-boost-group-title {
      font-size: 0.68rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: rgba(226, 232, 240, 0.7);
    }
    .floating-boost-pill {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 6px 10px;
      border-radius: 999px;
      background: rgba(15, 23, 42, 0.8);
      border: 1px solid rgba(94, 234, 212, 0.45);
      color: #e2fdf9;
      font-size: 0.75rem;
    }
    .floating-boost-pill.active {
      background: rgba(34, 197, 94, 0.55);
      border-color: rgba(34, 197, 94, 1);
      color: #ffffff;
      box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.35), 0 6px 18px rgba(34, 197, 94, 0.25);
    }
    .floating-boost-empty {
      color: rgba(148, 163, 184, 0.8);
      font-size: 0.75rem;
    }
    .floating-boost-resize {
      position: absolute;
      width: 14px;
      height: 14px;
      right: 6px;
      bottom: 6px;
      cursor: nwse-resize;
      border-radius: 4px;
      background: rgba(148, 163, 184, 0.4);
    }
    .floating-chat-launcher {
      position: fixed;
      right: 18px;
      bottom: 18px;
      display: flex;
      gap: 8px;
      z-index: 2147483647;
    }
    .floating-chat-toggle {
      position: static;
      padding: 8px 12px;
      border-radius: 999px;
      background: rgba(30, 41, 59, 0.85);
      border: 1px solid rgba(148, 163, 184, 0.45);
      color: #e2e8f0;
      font-size: 0.75rem;
      box-shadow: 0 12px 24px rgba(0,0,0,0.35);
    }
    .floating-chat-toggle.wins {
      background: rgba(15, 23, 42, 0.85);
      border-color: rgba(34, 211, 238, 0.6);
      color: #cffafe;
    }
    .floating-chat-toggle.helper {
      background: rgba(30, 64, 175, 0.85);
      border-color: rgba(96, 165, 250, 0.8);
      color: #dbeafe;
    }
    .floating-chat-toggle.unread {
      background: rgba(14, 116, 144, 0.9);
      border-color: rgba(56, 189, 248, 0.8);
      color: #e0f2fe;
    }
  `;
  document.head.appendChild(style);
}

function ensureFloatingChatContainer() {
  if (floatingChatContainer && floatingChatContainer.isConnected) return floatingChatContainer;
  ensureFloatingChatStyles();
  const state = loadFloatingChatState();
  const host = document.body || document.documentElement;
  const container = document.createElement('div');
  container.id = 'floatingClanChat';
  container.className = 'floating-chat';
  const launcher = document.createElement('div');
  launcher.id = 'floatingChatLauncher';
  launcher.className = 'floating-chat-launcher';
  const toggle = document.createElement('button');
  toggle.id = 'floatingClanChatToggle';
  toggle.className = 'floating-chat-toggle';
  toggle.type = 'button';
  toggle.textContent = 'Clan chat';
  const winsBtn = document.createElement('button');
  winsBtn.id = 'floatingClanWinsBtn';
  winsBtn.className = 'floating-chat-toggle wins';
  winsBtn.type = 'button';
  winsBtn.textContent = 'Wins';
  const helperBtn = document.createElement('button');
  helperBtn.id = 'floatingBoostHelperBtn';
  helperBtn.className = 'floating-chat-toggle helper';
  helperBtn.type = 'button';
  helperBtn.textContent = 'Notifications';
  launcher.appendChild(toggle);
  launcher.appendChild(winsBtn);
  launcher.appendChild(helperBtn);
  container.style.position = 'fixed';
  host.appendChild(container);
  host.appendChild(launcher);
  floatingChatContainer = container;
  floatingChatToggle = toggle;
  floatingChatLauncher = launcher;
  floatingChatWinsBtn = winsBtn;
  floatingChatBoostHelperBtn = helperBtn;

  const applyState = () => {
    const maxX = Math.max(12, window.innerWidth - 120);
    const maxY = Math.max(12, window.innerHeight - 60);
    if (!Number.isFinite(floatingChatState.x) || floatingChatState.x > maxX + 50) {
      floatingChatState.x = maxX;
    }
    if (!Number.isFinite(floatingChatState.y) || floatingChatState.y > maxY + 50) {
      floatingChatState.y = maxY;
    }
    floatingChatState.x = Math.min(Math.max(12, floatingChatState.x), maxX);
    floatingChatState.y = Math.min(Math.max(12, floatingChatState.y), maxY);
    const { x, y, width, height, collapsed } = floatingChatState || state;
    container.style.width = `${Math.max(260, width)}px`;
    container.style.height = `${Math.max(220, height)}px`;
    container.style.left = `${Math.max(12, x)}px`;
    container.style.top = `${Math.max(12, y)}px`;
    toggle.style.left = 'auto';
    toggle.style.top = 'auto';
    container.classList.toggle('hidden', !!collapsed);
    toggle.style.display = collapsed ? 'block' : 'none';
    if (floatingChatUnread) toggle.classList.add('unread');
    else toggle.classList.remove('unread');
  };

  const clampToViewport = () => {
    const maxX = Math.max(12, window.innerWidth - 120);
    const maxY = Math.max(12, window.innerHeight - 60);
    floatingChatState.x = Math.min(Math.max(12, floatingChatState.x), maxX);
    floatingChatState.y = Math.min(Math.max(12, floatingChatState.y), maxY);
  };

  const setCollapsed = (value) => {
    floatingChatState.collapsed = value;
    if (!value) {
      const last = commMessages.length ? commMessages[commMessages.length - 1] : null;
      floatingChatLastSeenId = last?.id ?? null;
      floatingChatUnread = false;
    }
    saveFloatingChatState();
    applyState();
  };

  toggle.addEventListener('click', () => setCollapsed(false));
  winsBtn.addEventListener('click', () => toggleWinsPopup());
  helperBtn.addEventListener('click', () => toggleBoostHelperPopup());

  const handleDrag = (event) => {
    if (event.button !== 0) return;
    const startX = event.clientX;
    const startY = event.clientY;
    const startLeft = floatingChatState.x;
    const startTop = floatingChatState.y;
    const onMove = (moveEvt) => {
      floatingChatState.x = startLeft + (moveEvt.clientX - startX);
      floatingChatState.y = startTop + (moveEvt.clientY - startY);
      clampToViewport();
      applyState();
    };
    const onUp = () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      saveFloatingChatState();
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  };

  const handleResize = (event) => {
    if (event.button !== 0) return;
    event.preventDefault();
    const startX = event.clientX;
    const startY = event.clientY;
    const startW = floatingChatState.width;
    const startH = floatingChatState.height;
    const onMove = (moveEvt) => {
      const nextW = Math.max(260, startW + (moveEvt.clientX - startX));
      const nextH = Math.max(220, startH + (moveEvt.clientY - startY));
      floatingChatState.width = Math.min(nextW, window.innerWidth - 24);
      floatingChatState.height = Math.min(nextH, window.innerHeight - 24);
      applyState();
    };
    const onUp = () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      saveFloatingChatState();
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  };

  container.addEventListener('pointerdown', (event) => {
    if (event.target && event.target.closest('.floating-chat-header')) {
      handleDrag(event);
    }
  });

  window.addEventListener('resize', () => {
    clampToViewport();
    applyState();
  });

  applyState();

  container.innerHTML = `
    <div class="floating-chat-header">
      <div class="floating-chat-title">Clan chat</div>
      <div class="floating-chat-actions">
        <button type="button" id="floatingCommAlertBtn">Boost</button>
        <button type="button" id="floatingCommMuteBtn">Mute</button>
        <button type="button" id="floatingCommCollapseBtn">^</button>
      </div>
    </div>
    <div class="floating-chat-body" id="floatingCommMessages"></div>
    <div class="floating-chat-input">
      <input id="floatingCommMessageInput" type="text" placeholder="Type a clan message..." autocomplete="off" />
      <button type="button" id="floatingCommSendBtn">Send</button>
    </div>
    <div class="floating-chat-resize" id="floatingCommResize"></div>
  `;

  const collapseBtn = container.querySelector('#floatingCommCollapseBtn');
  if (collapseBtn) collapseBtn.addEventListener('click', () => setCollapsed(true));
  const resizeHandle = container.querySelector('#floatingCommResize');
  if (resizeHandle) resizeHandle.addEventListener('pointerdown', handleResize);

  const alertBtn = container.querySelector('#floatingCommAlertBtn');
  if (alertBtn) alertBtn.addEventListener('click', () => commSendPing());
  const muteBtn = container.querySelector('#floatingCommMuteBtn');
  if (muteBtn) muteBtn.addEventListener('click', () => commToggleAlertMute());

  const input = container.querySelector('#floatingCommMessageInput');
  const sendBtn = container.querySelector('#floatingCommSendBtn');
  const sendMessage = async () => {
    if (!input) return;
    const text = (input.value || '').trim();
    if (!text) return;
    try {
      await commSendMessage('chat', text);
      input.value = '';
      await commLoadMessages(true);
    } catch (err) {
      commSetStatus('Failed to send message.', { error: true, privateScope: true });
    }
  };
  if (input) {
    input.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        sendMessage();
      }
    });
    input.addEventListener('focus', () => {
      floatingChatUnread = false;
      const last = commMessages.length ? commMessages[commMessages.length - 1] : null;
      floatingChatLastSeenId = last?.id ?? null;
      applyState();
    });
  }
  if (sendBtn) sendBtn.addEventListener('click', sendMessage);

  return container;
}

function updateFloatingChatVisibility() {
  const chatAllowed = licenseIsActive() || !!homeLicenseState?.valid;
  if (!chatAllowed) {
    if (floatingChatContainer) floatingChatContainer.classList.add('hidden');
    if (floatingChatLauncher) floatingChatLauncher.style.display = 'none';
    if (floatingChatToggle) floatingChatToggle.style.display = 'none';
    if (floatingChatWinsBtn) floatingChatWinsBtn.style.display = 'none';
    if (floatingChatBoostHelperBtn) floatingChatBoostHelperBtn.style.display = 'none';
    if (floatingWinsContainer) floatingWinsContainer.style.display = 'none';
    if (floatingBoostHelperContainer) floatingBoostHelperContainer.style.display = 'none';
    return;
  }
  ensureFloatingChatContainer();
  ensureFloatingWinsContainer();
  ensureFloatingBoostHelperContainer();
  const state = loadFloatingChatState();
  if (state.collapsed) {
    floatingChatContainer.classList.add('hidden');
    if (floatingChatLauncher) floatingChatLauncher.style.display = 'flex';
    if (floatingChatToggle) floatingChatToggle.style.display = 'block';
    if (floatingChatWinsBtn) floatingChatWinsBtn.style.display = 'block';
    if (floatingChatBoostHelperBtn) floatingChatBoostHelperBtn.style.display = 'block';
  } else {
    floatingChatContainer.classList.remove('hidden');
    if (floatingChatLauncher) floatingChatLauncher.style.display = 'flex';
    if (floatingChatToggle) floatingChatToggle.style.display = 'none';
    if (floatingChatWinsBtn) floatingChatWinsBtn.style.display = 'block';
    if (floatingChatBoostHelperBtn) floatingChatBoostHelperBtn.style.display = 'block';
  }
}

function commRenderFloatingChat() {
  const chatAllowed = licenseIsActive() || !!homeLicenseState?.valid;
  if (!chatAllowed) return;
  const container = ensureFloatingChatContainer();
  if (!container) return;
  updateFloatingChatVisibility();
  const messagesEl = container.querySelector('#floatingCommMessages');
  if (!messagesEl) return;
  if (!commSession) {
    messagesEl.innerHTML = '<div class="muted">Join a clan to unlock chat.</div>';
    return;
  }
  const visibleMessages = commFilterVisibleMessages(commMessages);
  if (!visibleMessages.length) {
    messagesEl.innerHTML = '<div class="muted">No clan messages yet.</div>';
  } else {
    const html = visibleMessages
      .map((msg) => {
        const stamp = commFormatChatStampShort(msg.created_at);
        const sender = commEscape(msg.sender || 'Unknown');
        const content = msg.content ? commEscape(msg.content) : (msg.kind === 'ping' ? 'Clan ping' : '');
        const pingClass = msg.kind === 'ping' ? ' ping' : '';
        return `<div class="comm-message${pingClass}"><span class="muted">[${stamp}]</span> <strong>${sender}</strong>: ${content}</div>`;
      })
      .join('');
    messagesEl.innerHTML = html;
    messagesEl.scrollTop = messagesEl.scrollHeight;
  }
  const last = commMessages.length ? commMessages[commMessages.length - 1] : null;
  const lastId = last?.id ?? null;
  if (floatingChatState?.collapsed) {
    if (lastId && lastId !== floatingChatLastSeenId) {
      floatingChatUnread = true;
    }
  } else {
    floatingChatLastSeenId = lastId;
    floatingChatUnread = false;
  }
  if (floatingChatToggle) {
    floatingChatToggle.classList.toggle('unread', floatingChatUnread);
  }
  if (floatingChatWinsBtn) {
    floatingChatWinsBtn.classList.toggle('unread', floatingChatUnread);
  }
  commUpdateAlertMuteButtons();
  commUpdateHighAlertButtons();
}

function loadFloatingWinsState() {
  if (floatingWinsState) return floatingWinsState;
  const fallback = {
    x: Math.max(24, window.innerWidth - 380),
    y: Math.max(24, window.innerHeight - 260),
    width: 320,
    height: 180,
    visible: false
  };
  try {
    const raw = localStorage.getItem(FLOATING_WINS_STATE_KEY);
    floatingWinsState = raw ? { ...fallback, ...JSON.parse(raw) } : fallback;
  } catch (err) {
    floatingWinsState = fallback;
  }
  return floatingWinsState;
}

function saveFloatingWinsState() {
  if (!floatingWinsState) return;
  try {
    localStorage.setItem(FLOATING_WINS_STATE_KEY, JSON.stringify(floatingWinsState));
  } catch (err) {
    // ignore
  }
}

function ensureFloatingWinsContainer() {
  if (floatingWinsContainer && floatingWinsContainer.isConnected) return floatingWinsContainer;
  ensureFloatingChatStyles();
  const state = loadFloatingWinsState();
  const container = document.createElement('div');
  container.id = 'floatingWins';
  container.className = 'floating-wins';
  container.style.width = `${Math.max(260, state.width)}px`;
  container.style.height = `${Math.max(160, state.height)}px`;
  container.style.left = `${Math.max(12, state.x)}px`;
  container.style.top = `${Math.max(12, state.y)}px`;
  container.innerHTML = `
    <div class="floating-wins-header">
      <div class="floating-wins-title">Wins this cycle</div>
      <button id="winsPopupClose" class="btn sm ghost" type="button">Close</button>
    </div>
    <div class="floating-wins-body">
      <div class="muted" id="winsPopupStatus">Loading wins...</div>
      <div class="floating-wins-row">
        <span>Clan wins (cycle)</span>
        <strong id="winsPopupClanValue">-</strong>
      </div>
      <div class="floating-wins-row">
        <span>User wins (cycle)</span>
        <strong id="winsPopupUserValue">-</strong>
      </div>
    </div>
    <div class="floating-wins-resize" id="winsPopupResize"></div>
  `;
  document.body.appendChild(container);
  floatingWinsContainer = container;

  const closeBtn = container.querySelector('#winsPopupClose');
  closeBtn?.addEventListener('click', () => toggleWinsPopup(false));

  const header = container.querySelector('.floating-wins-header');
  header?.addEventListener('pointerdown', (event) => {
    if (event.button !== 0) return;
    const startX = event.clientX;
    const startY = event.clientY;
    const startLeft = floatingWinsState.x;
    const startTop = floatingWinsState.y;
    const onMove = (moveEvt) => {
      floatingWinsState.x = startLeft + (moveEvt.clientX - startX);
      floatingWinsState.y = startTop + (moveEvt.clientY - startY);
      container.style.left = `${Math.max(12, floatingWinsState.x)}px`;
      container.style.top = `${Math.max(12, floatingWinsState.y)}px`;
    };
    const onUp = () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      saveFloatingWinsState();
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  });

  const resizeHandle = container.querySelector('#winsPopupResize');
  resizeHandle?.addEventListener('pointerdown', (event) => {
    if (event.button !== 0) return;
    event.preventDefault();
    const startX = event.clientX;
    const startY = event.clientY;
    const startW = floatingWinsState.width;
    const startH = floatingWinsState.height;
    const onMove = (moveEvt) => {
      floatingWinsState.width = Math.max(260, startW + (moveEvt.clientX - startX));
      floatingWinsState.height = Math.max(160, startH + (moveEvt.clientY - startY));
      container.style.width = `${floatingWinsState.width}px`;
      container.style.height = `${floatingWinsState.height}px`;
    };
    const onUp = () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      saveFloatingWinsState();
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  });

  return container;
}

let winsPopupLoading = false;
const WINS_POPUP_REFRESH_MS = 10 * 60 * 1000;
let winsPopupRefreshTimer = null;
function toggleWinsPopup(forceState = null) {
  const state = loadFloatingWinsState();
  const next = forceState == null ? !state.visible : !!forceState;
  state.visible = next;
  saveFloatingWinsState();
  const container = ensureFloatingWinsContainer();
  if (!container) return;
  container.style.display = next ? 'flex' : 'none';
  if (next) {
    refreshWinsPopup();
  }
}

function scheduleWinsPopupRefresh() {
  if (winsPopupRefreshTimer) return;
  winsPopupRefreshTimer = setInterval(() => {
    if (!document.getElementById('winsPopupStatus')) {
      ensureFloatingWinsContainer();
    }
    refreshWinsPopup();
  }, WINS_POPUP_REFRESH_MS);
}

async function refreshWinsPopup() {
  if (winsPopupLoading) return;
  const statusEl = document.getElementById('winsPopupStatus');
  const clanEl = document.getElementById('winsPopupClanValue');
  const userEl = document.getElementById('winsPopupUserValue');
  if (!statusEl || !clanEl || !userEl) return;
  winsPopupLoading = true;
  statusEl.textContent = 'Loading wins...';
  try {
    const data = await fetchWinsSummaryCurrentCycle();
    clanEl.textContent = data.clanWins != null ? formatCount(data.clanWins) : '-';
    userEl.textContent = data.userWins != null ? formatCount(data.userWins) : '-';
    statusEl.textContent = data.label || 'Updated just now';
  } catch (err) {
    statusEl.textContent = err?.message || 'Failed to load wins.';
    clanEl.textContent = '-';
    userEl.textContent = '-';
  } finally {
    winsPopupLoading = false;
  }
}

async function fetchWinsSummaryCurrentCycle() {
  const cycleId = getCycleNumber(new Date());
  if (!Number.isFinite(cycleId)) {
    throw new Error('Current cycle unavailable.');
  }
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) {
    throw new Error('Sign in to load wins.');
  }
  const ensureRewardsLoaded = async () => {
    if (!personalRewardsCache.length) {
      try {
        personalRewardsCache = await commFetchRewardsEntriesFromDb(COMM_TABLE_USER_REWARDS, identity.userId);
        commSyncPersonalIdentityFromRewards(personalRewardsCache);
      } catch (err) {
        // ignore
      }
    }
    if (!personalClanRewardsCache.length) {
      try {
        personalClanRewardsCache = await commFetchRewardsEntriesFromDb(COMM_TABLE_USER_REWARDS_CLAN, identity.userId);
        commSyncPersonalIdentityFromRewards(personalClanRewardsCache);
      } catch (err) {
        // ignore
      }
    }
    if (!personalRewardsCache.length && !personalClanRewardsCache.length) {
      await Promise.all([
        commLoadPersonalRewardsFromDb().catch(() => {}),
        commLoadPersonalClanRewardsFromDb().catch(() => {})
      ]);
    }
  };
  await ensureRewardsLoaded();

  const countWinsFromRewards = (entries) => {
    if (!Array.isArray(entries) || entries.length === 0) return 0;
    const set = new Set();
    let count = 0;
    entries.forEach((entry) => {
      const entryCycle = commExtractRewardsCycleId(entry);
      if (!Number.isFinite(entryCycle) || Math.trunc(entryCycle) !== Math.trunc(cycleId)) return;
      const roundId = commExtractRewardsRoundId(entry);
      if (Number.isFinite(roundId)) {
        const key = `r:${Math.trunc(roundId)}`;
        if (!set.has(key)) {
          set.add(key);
          count += 1;
        }
      } else {
        count += 1;
      }
    });
    return count;
  };

  const clanWins = countWinsFromRewards(personalClanRewardsCache);
  const userWins = countWinsFromRewards(personalRewardsCache);
  const totalChecked = Math.max(personalClanRewardsCache.length, personalRewardsCache.length);
  if (!totalChecked) {
    throw new Error('No rewards cached yet. Open Earnings once to cache rewards.');
  }
  const label = `Cycle ${Math.trunc(cycleId)}  -  ${totalChecked} rewards checked`;
  return { clanWins, userWins, label };
}

const BOOST_HELPER_TYPES = [
  { key: 'powerboost', label: 'Power boost' },
  { key: 'focus', label: 'Focus boost' },
  { key: 'clanpowerup', label: 'Clan power-up' },
  { key: 'powerup', label: 'Power-up' },
  { key: 'instant', label: 'Instant boost' },
  { key: 'echo', label: 'Echo boost' }
];

const BOOST_HELPER_PHASE_LABELS = {
  any: 'Any phase',
  early: 'Early',
  afterEarly: 'After early',
  mid: 'Mid',
  late: 'Late'
};

function isBoostHelperNoBoostRule(rule) {
  return rule?.boostType === 'no_boosts';
}

function boostHelperGetNoBoostSeconds(rule) {
  const raw = Number(rule?.maxSeconds ?? rule?.seconds ?? rule?.untilSeconds ?? rule?.noBoostSeconds ?? null);
  return Number.isFinite(raw) ? raw : null;
}

function boostHelperGetNoBoostWindow(rule) {
  const fromRaw = Number(rule?.fromSeconds ?? rule?.minSeconds ?? rule?.startSeconds ?? null);
  const from = Number.isFinite(fromRaw) ? Math.max(0, fromRaw) : 0;
  const to = boostHelperGetNoBoostSeconds(rule);
  return { from, to };
}

function boostHelperGetNoBoostMaxCount(rule) {
  const raw = Number(rule?.maxBoostCount ?? rule?.maxBoosts ?? rule?.maxCount ?? 0);
  return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 0;
}

function isBoostHelperClanBoostRule(rule) {
  return rule?.boostType === 'my_clan_boost';
}

function isBoostHelperTotalScoreRule(rule) {
  return rule?.boostType === 'total_score';
}

function boostHelperGetTotalScoreOp(rule) {
  const raw = String(rule?.operator ?? rule?.op ?? 'gte').toLowerCase();
  return raw === 'lte' || raw === '<=' ? 'lte' : 'gte';
}

function boostHelperGetTotalScoreValue(rule) {
  const raw = Number(rule?.scoreValue ?? rule?.totalScore ?? rule?.value ?? null);
  return Number.isFinite(raw) ? raw : null;
}

function boostHelperGetTotalScoreWindow(rule) {
  const fromMin = Number(rule?.fromMinutes ?? rule?.minMinutes ?? rule?.fromMin ?? rule?.minMin ?? null);
  const toMin = Number(rule?.toMinutes ?? rule?.maxMinutes ?? rule?.toMin ?? rule?.maxMin ?? null);
  const from = Number.isFinite(fromMin) ? Math.max(0, fromMin) : null;
  const to = Number.isFinite(toMin) ? Math.max(0, toMin) : null;
  return { from, to };
}

function boostHelperGetCurrentTotalScore() {
  return Number.isFinite(liveTotalScoreCurrent) ? liveTotalScoreCurrent : null;
}

function boostHelperGetScoreSampleScope() {
  const leagueId = commResolveLiveLeagueId();
  const league = Number.isFinite(leagueId) ? String(Math.trunc(leagueId)) : 'unknown';
  const cycleId = commParseNumber(liveCycleId ?? liveCycleNumber ?? commKorrelCurrentCycle);
  const cycle = Number.isFinite(cycleId) ? String(Math.trunc(cycleId)) : 'unknown';
  return `${league}:${cycle}`;
}

function boostHelperLoadScoreSamples(scope = null) {
  try {
    const raw = localStorage.getItem(BOOST_HELPER_SCORE_SAMPLE_KEY);
    if (!raw) {
      const legacy = localStorage.getItem(BOOST_HELPER_SCORE_SAMPLE_KEY_V1);
      if (!legacy) return { oneMin: [], twoMin: [] };
      const legacyParsed = JSON.parse(legacy);
      return {
        oneMin: Array.isArray(legacyParsed?.oneMin) ? legacyParsed.oneMin : [],
        twoMin: Array.isArray(legacyParsed?.twoMin) ? legacyParsed.twoMin : []
      };
    }
    const parsed = JSON.parse(raw);
    const scoped = scope ? parsed?.[scope] : parsed;
    return {
      oneMin: Array.isArray(scoped?.oneMin) ? scoped.oneMin : [],
      twoMin: Array.isArray(scoped?.twoMin) ? scoped.twoMin : []
    };
  } catch {
    return { oneMin: [], twoMin: [] };
  }
}

function boostHelperSaveScoreSamples(scope, state) {
  try {
    const raw = localStorage.getItem(BOOST_HELPER_SCORE_SAMPLE_KEY);
    const parsed = raw ? JSON.parse(raw) : {};
    parsed[scope] = state;
    localStorage.setItem(BOOST_HELPER_SCORE_SAMPLE_KEY, JSON.stringify(parsed));
    localStorage.setItem(BOOST_HELPER_SCORE_SAMPLE_KEY_V1, JSON.stringify(state));
  } catch {}
}

function boostHelperRecordScoreSample(roundId, bucket, score, elapsedSec) {
  if (!roundId || !bucket) return;
  const scope = boostHelperGetScoreSampleScope();
  const next = boostHelperLoadScoreSamples(scope);
  const list = bucket === 'oneMin' ? next.oneMin : next.twoMin;
  if (list.some((row) => String(row.roundId) === String(roundId))) return;
  list.push({
    roundId: String(roundId),
    score: Number.isFinite(score) ? score : null,
    elapsedSec: Number.isFinite(elapsedSec) ? elapsedSec : null,
    ts: Date.now()
  });
  const trimmed = list.slice(-10);
  if (bucket === 'oneMin') {
    next.oneMin = trimmed;
  } else {
    next.twoMin = trimmed;
  }
  boostHelperSaveScoreSamples(scope, next);
}

function boostHelperUpdateScoreSamples() {
  let roundId = normalizeBlockId(
    currentRoundInfo?.blockId
    ?? liveBoostRoundKey
    ?? liveApiRoundKey
    ?? lastLiveSummaryBlockId
    ?? currentRoundInfo?.roundId
    ?? null
  );
  if (!roundId && Number.isFinite(boostHelperRoundStartAt)) {
    roundId = `t:${new Date(boostHelperRoundStartAt).toISOString()}`;
  }
  let basePpsTotal = computeLiveBasePpsTotal();
  if (!Number.isFinite(basePpsTotal)) {
    basePpsTotal = computeBasePpsTotalFromSnapshot(lastLiveClanSnapshot)
      ?? computeBasePpsTotalFromSnapshot(window.__mwLastLiveClanSnapshot);
  }
  if (!roundId || !Number.isFinite(basePpsTotal)) {
    const panel = document.getElementById('boostHelperScoreHistoryBody');
    if (panel && panel.dataset.visible === '1') {
      panel.innerHTML = '<div class="muted">Waiting for base PPS + round id...</div>';
    }
    return;
  }
  const score1 = basePpsTotal * 60;
  const score2 = basePpsTotal * 120;
  boostHelperRecordScoreSample(roundId, 'oneMin', score1, 60);
  boostHelperRecordScoreSample(roundId, 'twoMin', score2, 120);
  const panel = document.getElementById('boostHelperScoreHistoryBody');
  if (panel && panel.dataset.visible === '1') {
    boostHelperRenderScoreHistory(panel);
  }
}

function boostHelperRenderScoreHistory(panel) {
  if (!panel) return;
  const scope = boostHelperGetScoreSampleScope();
  const data = boostHelperLoadScoreSamples(scope);
  const scopeLabel = scope ? scope.replace(':', '  -  Cycle ') : 'unknown';
  const renderList = (list) => {
    if (!list.length) return '<div class="muted">No samples yet.</div>';
    return list
      .slice()
      .reverse()
      .map((row) => {
        const scoreLabel = Number.isFinite(row.score) ? formatScoreValue(row.score) : '-';
        const timeLabel = row.ts ? commFormatChatStampShort(row.ts) : '-';
        const elapsedLabel = Number.isFinite(row.elapsedSec) ? formatRoundTimerSeconds(row.elapsedSec) : '-';
        return `<div style="display:flex;justify-content:space-between;gap:8px;">
          <span>${commEscape(row.roundId)}</span>
          <span>${commEscape(scoreLabel)}</span>
          <span class="muted">${commEscape(elapsedLabel)}</span>
          <span class="muted">${commEscape(timeLabel)}</span>
        </div>`;
      })
      .join('');
  };
  panel.innerHTML = `
    <div class="muted" style="font-size:0.72rem;margin-bottom:6px;">Scope: League ${commEscape(scopeLabel)}  -  Estimated from base PPS</div>
    <div style="font-weight:600;margin-bottom:4px;">1 minute scores</div>
    <div style="display:grid;gap:4px;margin-bottom:10px;">${renderList(data.oneMin)}</div>
    <div style="font-weight:600;margin-bottom:4px;">2 minute scores</div>
    <div style="display:grid;gap:4px;">${renderList(data.twoMin)}</div>
  `;
}

function boostHelperGetClanBoostType(rule) {
  const raw = rule?.clanBoostType ?? rule?.triggerBoostType ?? rule?.boostUsedType ?? 'any';
  if (!raw) return 'any';
  if (String(raw).toLowerCase() === 'any') return 'any';
  return normalizeBoostKey(raw);
}

function boostHelperGetClanBoostCount(rule) {
  const raw = Number(rule?.minCount ?? rule?.count ?? 1);
  return Number.isFinite(raw) && raw > 0 ? raw : 1;
}

function boostHelperGetMyClanBoostCount(type) {
  if (!boostHelperMyClanBoosts) return 0;
  if (type === 'any') return Number(boostHelperMyClanBoosts.total_boosts || 0) || 0;
  const boosts = boostHelperMyClanBoosts.boosts || {};
  return Number(boosts[type] || 0) || 0;
}

function updateBoostHelperMyClanBoostsFromMap(map) {
  if (!map || map.size === 0) {
    boostHelperMyClanBoosts = null;
    return;
  }
  const clanIdRaw = commParseNumber(liveMyClanId ?? commSession?.clanId);
  const clanId = Number.isFinite(clanIdRaw) ? String(Math.trunc(clanIdRaw)) : null;
  const clanNameRaw = (liveMyClanName || commSession?.clanName || '').trim();
  const cleanedName = clanNameRaw ? commCleanClanDisplayName(clanNameRaw) : '';
  const normalizedName = clanNameRaw ? normalizeClanName(clanNameRaw) : '';
  const candidates = [];
  if (clanId) {
    candidates.push(map.get(`id:${clanId}`));
  }
  if (clanNameRaw) {
    candidates.push(map.get(`name:${clanNameRaw}`));
  }
  if (cleanedName) {
    candidates.push(map.get(`name:${cleanedName}`));
  }
  if (normalizedName) {
    candidates.push(map.get(`name:${normalizedName}`));
  }
  const entry = candidates.find(Boolean);
  if (!entry) {
    boostHelperMyClanBoosts = null;
    return;
  }
  boostHelperMyClanBoosts = {
    boosts: entry.boosts || {},
    total_boosts: Number(entry.total_boosts || 0) || 0,
    boostMeta: entry.boostMeta || {},
    roundKey: entry.roundKey || null,
    clan_id: entry.clan_id || clanId || null,
    name: entry.name || clanNameRaw || null
  };
}

function boostHelperGetNoBoostStatus(windowCfg) {
  if (!boostHelperMyClanBoosts) return 'unknown';
  const startAt = Number.isFinite(boostHelperRoundStartAt)
    ? boostHelperRoundStartAt
    : getCurrentRoundStartMs();
  if (!Number.isFinite(startAt)) return 'unknown';
  const fromSec = Number.isFinite(windowCfg?.from) ? windowCfg.from : 0;
  const toSec = Number.isFinite(windowCfg?.to) ? windowCfg.to : null;
  if (!Number.isFinite(toSec)) return 'unknown';
  const windowStart = startAt + fromSec * 1000;
  const windowEnd = startAt + toSec * 1000;
  const boostsMap = boostHelperMyClanBoosts.boosts || {};
  const totalBoosts = Number(boostHelperMyClanBoosts.total_boosts || 0)
    || Object.values(boostsMap).reduce((sum, value) => sum + (Number(value) || 0), 0);
  if (totalBoosts <= 0) return 'noBoost';
  const meta = boostHelperMyClanBoosts.boostMeta || {};
  const timestamps = [];
  Object.values(meta).forEach((entry) => {
    const list = normalizeTimestampList([
      ...(Array.isArray(entry?.timestamps) ? entry.timestamps : []),
      entry?.ts
    ]);
    list.forEach((ts) => {
      const adjusted = adjustBoostTimestampMs(ts, startAt);
      if (Number.isFinite(adjusted)) timestamps.push(adjusted);
    });
  });
  if (!timestamps.length) return 'unknown';
  const hasInWindow = timestamps.some((ts) => ts >= windowStart && ts <= windowEnd);
  if (hasInWindow) return 'hasBoost';
  if (totalBoosts > timestamps.length) return 'unknown';
  return 'noBoost';
}

function boostHelperGetBoostCountInWindow(windowCfg) {
  if (!boostHelperMyClanBoosts) return null;
  const startAt = Number.isFinite(boostHelperRoundStartAt)
    ? boostHelperRoundStartAt
    : getCurrentRoundStartMs();
  if (!Number.isFinite(startAt)) return null;
  const fromSec = Number.isFinite(windowCfg?.from) ? windowCfg.from : 0;
  const toSec = Number.isFinite(windowCfg?.to) ? windowCfg.to : null;
  if (!Number.isFinite(toSec)) return null;
  const windowStart = startAt + fromSec * 1000;
  const windowEnd = startAt + toSec * 1000;
  const boostsMap = boostHelperMyClanBoosts.boosts || {};
  const totalBoosts = Number(boostHelperMyClanBoosts.total_boosts || 0)
    || Object.values(boostsMap).reduce((sum, value) => sum + (Number(value) || 0), 0);
  const meta = boostHelperMyClanBoosts.boostMeta || {};
  const timestamps = [];
  Object.values(meta).forEach((entry) => {
    const list = normalizeTimestampList([
      ...(Array.isArray(entry?.timestamps) ? entry.timestamps : []),
      entry?.ts
    ]);
    list.forEach((ts) => {
      const adjusted = adjustBoostTimestampMs(ts, startAt);
      if (Number.isFinite(adjusted)) timestamps.push(adjusted);
    });
  });
  if (!timestamps.length) return totalBoosts;
  return timestamps.filter((ts) => ts >= windowStart && ts <= windowEnd).length;
}

function getBoostHelperPhase(roundSeconds, phases) {
  if (!Number.isFinite(roundSeconds)) return null;
  const early = Number(phases?.early ?? 50);
  const afterEarly = Number(phases?.afterEarly ?? 120);
  const mid = Number(phases?.mid ?? 240);
  if (roundSeconds <= early) return 'early';
  if (roundSeconds <= afterEarly) return 'afterEarly';
  if (roundSeconds <= mid) return 'mid';
  return 'late';
}

function getBoostHelperPhaseLabel(phase) {
  return BOOST_HELPER_PHASE_LABELS[phase] || 'Unknown';
}

function boostHelperNormalizeRoundLabel(value) {
  if (value == null) return null;
  const raw = String(value).trim();
  if (!raw || raw === '-' || raw === '-:-' || raw.toLowerCase() === 'n/a') return null;
  return raw;
}

function boostHelperNormalizeMultiplierValue(value) {
  if (value == null) return null;
  if (value === '64+') return '64+';
  const num = Number(value);
  return Number.isFinite(num) ? num : null;
}

function isBoostHelperMultiplierRule(rule) {
  return rule?.boostType === 'multiplier_alert';
}

function boostHelperIsSupportedRule(rule) {
  if (!rule || typeof rule !== 'object') return false;
  return (
    isBoostHelperNoBoostRule(rule)
    || isBoostHelperTotalScoreRule(rule)
    || isBoostHelperClanBoostRule(rule)
    || isBoostHelperMultiplierRule(rule)
  );
}

function boostHelperGetMultiplierList(rule) {
  if (!rule) return [];
  const raw = Array.isArray(rule.multipliers) ? rule.multipliers : [];
  return raw
    .map((value) => boostHelperNormalizeMultiplierValue(value))
    .filter((value) => value != null);
}

function boostHelperIsMultiplierMatch(multiplier, rule) {
  const list = boostHelperGetMultiplierList(rule);
  if (!list.length) return false;
  const numeric = Number(multiplier);
  if (!Number.isFinite(numeric)) return false;
  if (list.includes(numeric)) return true;
  if (numeric >= 64 && list.includes('64+')) return true;
  return false;
}

function boostHelperReadRoundFromDom() {
  const card = document.querySelector('.current-round-card');
  if (!card) return null;
  let roundId = boostHelperNormalizeRoundLabel(card.dataset.roundId);
  let roundTime = boostHelperNormalizeRoundLabel(card.dataset.roundTime);
  let multiplier = boostHelperNormalizeRoundLabel(card.dataset.roundMultiplier);
  if (!roundId || !roundTime || !multiplier) {
    const grids = Array.from(card.querySelectorAll('div[style*="grid-template-columns: 1.2fr 1fr 1fr"]'));
    if (grids.length >= 2) {
      const values = Array.from(grids[1].children).map((el) => boostHelperNormalizeRoundLabel(el.textContent));
      if (!roundId) roundId = values[0] || roundId;
      if (!roundTime) roundTime = values[1] || roundTime;
      if (!multiplier) multiplier = values[2] || multiplier;
    }
  }
  if (!roundId && !roundTime && !multiplier) return null;
  return { roundId, roundTime, multiplier };
}

function getBoostHelperContext() {
  const timerLabel =
    currentRoundInfo?.timer ??
    lastCurrentRoundLivePayload?.timer ??
    lastLiveSummaryTimer ??
    null;
  const multiplierLabel =
    currentRoundInfo?.multiplier ??
    lastCurrentRoundLivePayload?.multiplier ??
    lastLiveSummaryMultiplier ??
    null;
  const domRound = boostHelperReadRoundFromDom();
  const resolvedTimerLabel = boostHelperNormalizeRoundLabel(timerLabel) || domRound?.roundTime || '-';
  const resolvedMultiplierLabel = boostHelperNormalizeRoundLabel(multiplierLabel) || domRound?.multiplier || '1x';
  const roundSeconds = parseRoundTimerSeconds(resolvedTimerLabel);
  const multiplier = parseNumericValue(resolvedMultiplierLabel ?? null);
  const elapsedSeconds = Number.isFinite(boostHelperRoundStartAt)
    ? Math.max(0, Math.floor((Date.now() - boostHelperRoundStartAt) / 1000))
    : (Number.isFinite(roundSeconds) ? roundSeconds : null);
  const roundIdLabel = boostHelperNormalizeRoundLabel(
    currentRoundInfo?.blockId ??
    lastCurrentRoundLivePayload?.blockId ??
    lastLiveSummaryBlockId ??
    domRound?.roundId ??
    '-'
  ) || '-';
  return {
    timerLabel: resolvedTimerLabel,
    multiplierLabel: resolvedMultiplierLabel,
    roundSeconds,
    multiplier: Number.isFinite(multiplier) ? multiplier : null,
    elapsedSeconds,
    roundIdLabel
  };
}

function isBoostHelperRuleActive(rule, context, phases) {
  if (!rule) return false;
  if (isBoostHelperNoBoostRule(rule)) {
    const windowCfg = boostHelperGetNoBoostWindow(rule);
    const maxSeconds = windowCfg.to;
    const elapsed = Number.isFinite(context?.elapsedSeconds) ? context.elapsedSeconds : context.roundSeconds;
    if (!Number.isFinite(maxSeconds) || !Number.isFinite(elapsed)) return false;
    if (elapsed < maxSeconds) return false;
    const maxBoostCount = boostHelperGetNoBoostMaxCount(rule);
    if (maxBoostCount > 0) {
      const usedCount = boostHelperGetBoostCountInWindow(windowCfg);
      if (!Number.isFinite(usedCount)) return false;
      return usedCount <= maxBoostCount;
    }
    const status = boostHelperGetNoBoostStatus(windowCfg);
    return status === 'noBoost';
  }
  if (isBoostHelperTotalScoreRule(rule)) {
    const op = boostHelperGetTotalScoreOp(rule);
    const target = boostHelperGetTotalScoreValue(rule);
    const current = boostHelperGetCurrentTotalScore();
    const windowCfg = boostHelperGetTotalScoreWindow(rule);
    const elapsed = Number.isFinite(context?.elapsedSeconds) ? context.elapsedSeconds : context.roundSeconds;
    if (Number.isFinite(windowCfg.from) || Number.isFinite(windowCfg.to)) {
      if (!Number.isFinite(elapsed)) return false;
      const elapsedMin = elapsed / 60;
      if (Number.isFinite(windowCfg.from) && elapsedMin < windowCfg.from) return false;
      if (Number.isFinite(windowCfg.to) && elapsedMin > windowCfg.to) return false;
    }
    if (!Number.isFinite(target) || !Number.isFinite(current)) return false;
    return op === 'lte' ? current <= target : current >= target;
  }
  if (isBoostHelperMultiplierRule(rule)) {
    return boostHelperIsMultiplierMatch(context.multiplier, rule);
  }
  if (isBoostHelperClanBoostRule(rule)) {
    const type = boostHelperGetClanBoostType(rule);
    const count = boostHelperGetClanBoostCount(rule);
    const used = boostHelperGetMyClanBoostCount(type);
    return used >= count;
  }
  const phase = getBoostHelperPhase(context.roundSeconds, phases);
  const rulePhase = rule?.phase || 'any';
  const phaseMatch = rulePhase === 'any' ? true : (phase ? rulePhase === phase : false);
  const multMatch = Number.isFinite(rule?.minMultiplier) && Number.isFinite(context.multiplier)
    ? context.multiplier >= rule.minMultiplier
    : true;
  return phaseMatch && multMatch;
}

function evaluateBoostHelperRecommendations(config, context) {
  const rules = Array.isArray(config?.rules) ? config.rules : [];
  const phase = getBoostHelperPhase(context.roundSeconds, config?.phases);
  return rules
    .filter((rule) => {
      if (!rule) return false;
      if (rule.phase && rule.phase !== 'any' && phase && rule.phase !== phase) return false;
      if (Number.isFinite(rule.minMultiplier) && Number.isFinite(context.multiplier)) {
        if (context.multiplier < rule.minMultiplier) return false;
      }
      return true;
    })
    .map((rule) => {
      const typeLabel = formatBoostDisplayName(rule.boostType) || 'Boost';
      const minMult = Number.isFinite(rule.minMultiplier) ? `${rule.minMultiplier}x+` : 'any multiplier';
      const note = rule.note ? ` -  ${rule.note}` : '';
      return `Use ${typeLabel} (${minMult}) ${note}`.trim();
    });
}

let boostHelperSteamAudio = null;
let boostHelperAudioUnlocked = false;
let boostHelperUnlockCtx = null;
let boostHelperSoundBlobUrl = null;
let boostHelperSoundBlobType = null;
let boostHelperDebugLastLogAt = 0;

function getBoostHelperSoundSources() {
  return [
    { type: 'audio/mpeg', src: 'redesign-app/sound/840325__robo9418__long-steamboat-whistle.mp3' },
    { type: 'audio/ogg', src: 'redesign-app/sound/840325__robo9418__long-steamboat-whistle.ogg' }
  ];
}

async function loadBoostHelperSoundBlob() {
  if (boostHelperSoundBlobUrl) return { url: boostHelperSoundBlobUrl, type: boostHelperSoundBlobType };
  const sources = getBoostHelperSoundSources();
  for (const item of sources) {
    const candidateUrls = [];
    if (HAS_CHROME_RUNTIME && chrome?.runtime?.getURL) {
      candidateUrls.push(chrome.runtime.getURL(item.src));
    }
    candidateUrls.push(item.src);
    candidateUrls.push(item.src.replace(/^redesign-app\//, ''));
    for (const url of candidateUrls) {
      try {
        const res = await fetch(url);
        if (!res.ok) continue;
        const blob = await res.blob();
        boostHelperSoundBlobUrl = URL.createObjectURL(blob);
        boostHelperSoundBlobType = item.type;
        return { url: boostHelperSoundBlobUrl, type: boostHelperSoundBlobType };
      } catch (err) {
        // try next url
      }
    }
  }
  return null;
}

async function boostHelperUnlockAudio() {
  if (boostHelperAudioUnlocked) return true;
  try {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) return false;
    if (!boostHelperUnlockCtx) boostHelperUnlockCtx = new AudioCtx();
    const ctx = boostHelperUnlockCtx;
    if (ctx.state === 'suspended') {
      await ctx.resume();
    }
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    gain.gain.value = 0.0001;
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + 0.05);
    boostHelperAudioUnlocked = true;
    return true;
  } catch (err) {
    return false;
  }
}

async function playBoostHelperSteamSound(statusEl = null) {
  try {
    const cfg = loadBoostHelperConfig();
    const volume = Number(cfg?.soundVolume);
    const vol = Number.isFinite(volume) ? Math.min(1, Math.max(0, volume)) : 0.6;
    const audio = boostHelperSteamAudio || new Audio();
    boostHelperSteamAudio = audio;
    audio.muted = false;
    audio.volume = vol;
    const blobInfo = await loadBoostHelperSoundBlob();
    if (!blobInfo) {
      if (statusEl) statusEl.textContent = 'Sound file not found.';
      return;
    }
    audio.src = blobInfo.url;
    try { audio.pause(); } catch (_) {}
    try { audio.load(); } catch (_) {}
    audio.currentTime = 0;
    await audio.play();
    if (statusEl) statusEl.textContent = '';
  } catch (err) {
    if (statusEl) {
      statusEl.textContent = err?.name === 'NotAllowedError'
        ? 'Sound blocked by browser. Click the page once and retry.'
        : err?.name === 'NotSupportedError'
          ? 'Audio format not supported.'
          : 'Sound failed.';
    }
  }
}

function ensureFloatingBoostHelperContainer() {
  if (floatingBoostHelperContainer && floatingBoostHelperContainer.isConnected) return floatingBoostHelperContainer;
  ensureFloatingChatStyles();
  const host = document.body || document.documentElement;
  const state = loadFloatingBoostHelperState();
  const container = document.createElement('div');
  container.id = 'floatingBoostHelper';
  container.className = 'floating-boost-helper';
  container.style.left = `${state.x}px`;
  container.style.top = `${state.y}px`;
  container.style.width = `${state.width}px`;
  container.style.height = `${state.height}px`;
  container.style.display = state.visible ? 'flex' : 'none';
  container.innerHTML = `
    <div class="floating-boost-header">
      <div class="floating-boost-title">Notifications</div>
      <button id="boostHelperCloseBtn" class="btn sm ghost" type="button">Close</button>
    </div>
    <div class="floating-boost-body">
      <div class="floating-boost-meta">
        <span id="boostHelperRound">Round: -</span>
      </div>
      <div class="floating-boost-meta">
        <span id="boostHelperMultiplier">Multiplier: -</span>
        <span id="boostHelperTime">Time: -</span>
      </div>
      <div class="floating-boost-list" id="boostHelperList"></div>
    </div>
    <div class="floating-boost-resize" id="boostHelperResize"></div>
  `;
  host.appendChild(container);
  floatingBoostHelperContainer = container;

  const header = container.querySelector('.floating-boost-header');
  const closeBtn = container.querySelector('#boostHelperCloseBtn');
  const resizeHandle = container.querySelector('#boostHelperResize');

  if (closeBtn) closeBtn.addEventListener('click', () => toggleBoostHelperPopup(false));
  if (header) {
    header.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      const startX = event.clientX;
      const startY = event.clientY;
      const startLeft = state.x;
      const startTop = state.y;
      const onMove = (moveEvt) => {
        state.x = startLeft + (moveEvt.clientX - startX);
        state.y = startTop + (moveEvt.clientY - startY);
        container.style.left = `${state.x}px`;
        container.style.top = `${state.y}px`;
      };
      const onUp = () => {
        window.removeEventListener('pointermove', onMove);
        window.removeEventListener('pointerup', onUp);
        saveFloatingBoostHelperState(state);
      };
      window.addEventListener('pointermove', onMove);
      window.addEventListener('pointerup', onUp);
    });
  }

  if (resizeHandle) {
    resizeHandle.addEventListener('pointerdown', (event) => {
      if (event.button !== 0) return;
      event.preventDefault();
      const startX = event.clientX;
      const startY = event.clientY;
      const startW = state.width;
      const startH = state.height;
      const onMove = (moveEvt) => {
        state.width = Math.max(260, startW + (moveEvt.clientX - startX));
        state.height = Math.max(180, startH + (moveEvt.clientY - startY));
        container.style.width = `${state.width}px`;
        container.style.height = `${state.height}px`;
      };
      const onUp = () => {
        window.removeEventListener('pointermove', onMove);
        window.removeEventListener('pointerup', onUp);
        saveFloatingBoostHelperState(state);
      };
      window.addEventListener('pointermove', onMove);
      window.addEventListener('pointerup', onUp);
    });
  }

  if (state.visible) {
    refreshBoostHelperPopup();
    if (!boostHelperPopupTimer) {
      boostHelperPopupTimer = setInterval(() => refreshBoostHelperPopup(), 1000);
    }
  }

  return container;
}

function refreshBoostHelperPopup() {
  const container = ensureFloatingBoostHelperContainer();
  if (!container || container.style.display === 'none') return;
  updateBoostHelperMyClanBoostsFromMap(currentRoundClanBoosts);
  const config = loadBoostHelperConfig();
  const context = getBoostHelperContext();
  const now = Date.now();
  if (now - boostHelperDebugLastLogAt > 5000) {
    boostHelperDebugLastLogAt = now;
    originalConsole.log('[MW][BoostHelper] popup context', {
      roundIdLabel: context.roundIdLabel,
      timerLabel: context.timerLabel,
      multiplierLabel: context.multiplierLabel,
      roundSeconds: context.roundSeconds,
      hasCurrentRoundInfo: !!currentRoundInfo,
      currentRoundInfo,
      lastCurrentRoundLivePayload,
      lastLiveSummaryBlockId,
      lastLiveSummaryTimer,
      lastLiveSummaryMultiplier
    });
  }
  const phase = getBoostHelperPhase(context.roundSeconds, config?.phases);
  const roundEl = container.querySelector('#boostHelperRound');
  const multEl = container.querySelector('#boostHelperMultiplier');
  const timeEl = container.querySelector('#boostHelperTime');
  const listEl = container.querySelector('#boostHelperList');
  if (roundEl) roundEl.textContent = `Round: ${context.roundIdLabel || '-'}`;
  if (multEl) multEl.textContent = `Multiplier: ${context.multiplierLabel || '-'}`;
  if (timeEl) timeEl.textContent = `Time: ${context.timerLabel || '-'}`;

  const rules = Array.isArray(config?.rules) ? config.rules.filter((rule) => boostHelperIsSupportedRule(rule)) : [];
  if (!listEl) return;
  if (!rules.length) {
    listEl.innerHTML = '<div class="floating-boost-empty">No boost recommendations yet.</div>';
    return;
  }
  const roundKey = normalizeBlockId(context.roundIdLabel ?? null);
  if (roundKey && boostHelperLastRoundKey !== roundKey) {
    boostHelperActiveRuleIds = new Set();
    boostHelperLastRoundKey = roundKey;
    boostHelperRoundStartAt = Number.isFinite(context.roundSeconds)
      ? Date.now() - (context.roundSeconds * 1000)
      : Date.now();
  } else if (!roundKey && boostHelperLastRoundKey) {
    boostHelperLastRoundKey = null;
    boostHelperRoundStartAt = null;
  }
  const newlyActive = [];
  const groups = {
    noBoost: [],
    totalScore: [],
    clanBoost: [],
    multiplier: []
  };
  rules.forEach((rule) => {
    if (isBoostHelperNoBoostRule(rule)) groups.noBoost.push(rule);
    else if (isBoostHelperTotalScoreRule(rule)) groups.totalScore.push(rule);
    else if (isBoostHelperClanBoostRule(rule)) groups.clanBoost.push(rule);
    else if (isBoostHelperMultiplierRule(rule)) groups.multiplier.push(rule);
  });
  const renderRule = (rule, groupTitle) => {
    const isActive = isBoostHelperRuleActive(rule, context, config?.phases);
    const note = rule?.note ? `  -  ${rule.note}` : '';
    let label = '';
    if (isBoostHelperNoBoostRule(rule)) {
      const windowCfg = boostHelperGetNoBoostWindow(rule);
      const seconds = windowCfg.to;
      const fromLabel = Number.isFinite(windowCfg.from) ? `${windowCfg.from}s` : '0s';
      const elapsed = Number.isFinite(context?.elapsedSeconds) ? context.elapsedSeconds : context.roundSeconds;
      const remaining = Number.isFinite(seconds) && Number.isFinite(elapsed) ? Math.max(0, Math.ceil(seconds - elapsed)) : null;
      const status = remaining != null && remaining <= 0 ? boostHelperGetNoBoostStatus(windowCfg) : null;
      const maxBoostCount = boostHelperGetNoBoostMaxCount(rule);
      if (Number.isFinite(seconds)) {
        if (maxBoostCount > 0) {
          const usedCount = remaining != null && remaining <= 0
            ? boostHelperGetBoostCountInWindow(windowCfg)
            : null;
          const usedLabel = Number.isFinite(usedCount) ? `${usedCount}` : '?';
          label = remaining != null && remaining > 0
            ? `In first ${seconds}s, my clan used <= ${maxBoostCount} boosts (wait ${remaining}s)${note}`
            : `In first ${seconds}s, my clan used <= ${maxBoostCount} boosts (used: ${usedLabel})${note}`;
        } else {
          label = remaining != null && remaining > 0
            ? `When there are no boosts between ${fromLabel} and ${seconds}s (wait ${remaining}s)${note}`
            : `When there are no boosts between ${fromLabel} and ${seconds}s (${status === 'unknown' ? 'waiting for boost data' : 'window passed'})${note}`;
        }
      } else {
        label = `No boosts (time not set)${note}`;
      }
    } else if (isBoostHelperTotalScoreRule(rule)) {
      const op = boostHelperGetTotalScoreOp(rule);
      const target = boostHelperGetTotalScoreValue(rule);
      const current = boostHelperGetCurrentTotalScore();
      const opLabel = op === 'lte' ? '<=' : '>=';
      const targetLabel = Number.isFinite(target) ? formatScoreValue(target) : '-';
      const currentLabel = Number.isFinite(current) ? formatScoreValue(current) : '-';
      const windowCfg = boostHelperGetTotalScoreWindow(rule);
      const elapsed = Number.isFinite(context?.elapsedSeconds) ? context.elapsedSeconds : context.roundSeconds;
      const elapsedLabel = Number.isFinite(elapsed) ? formatRoundTimerSeconds(elapsed) : '-';
      const fromLabel = Number.isFinite(windowCfg.from) ? `${windowCfg.from}m` : null;
      const toLabel = Number.isFinite(windowCfg.to) ? `${windowCfg.to}m` : null;
      const windowLabel = (fromLabel || toLabel)
        ? ` between ${fromLabel || '0m'}-${toLabel || 'inf'}`
        : '';
      label = `Total score ${opLabel} ${targetLabel}${windowLabel} (now ${currentLabel}, t=${elapsedLabel})${note}`;
    } else if (isBoostHelperClanBoostRule(rule)) {
      const type = boostHelperGetClanBoostType(rule);
      const count = boostHelperGetClanBoostCount(rule);
      const typeLabel = type === 'any' ? 'any boost' : formatBoostDisplayName(type);
      label = `My clan used ${count}x ${typeLabel}${note}`;
    } else if (isBoostHelperMultiplierRule(rule)) {
      const list = boostHelperGetMultiplierList(rule);
      const labelList = list.map((value) => (value === '64+' ? '64+' : `${value}x`)).join(', ');
      label = `When multiplier is ${labelList || '-' }${note}`;
    } else {
      const typeLabel = formatBoostDisplayName(rule?.boostType) || 'Boost';
      const minMult = Number.isFinite(rule?.minMultiplier) ? `${rule.minMultiplier}x+` : 'any multiplier';
      label = `Use ${typeLabel} (${minMult})${note}`;
    }
    if (isActive && rule?.id && !boostHelperActiveRuleIds.has(rule.id)) {
      newlyActive.push({
        id: rule.id,
        groupTitle,
        label
      });
    }
    return `<div class="floating-boost-pill${isActive ? ' active' : ''}">${commEscape(label)}</div>`;
  };
  const renderGroup = (title, items) => {
    if (!items.length) return '';
    return `
      <div class="floating-boost-group">
        <div class="floating-boost-group-title">${commEscape(title)}</div>
        ${items.map((rule) => renderRule(rule, title)).join('')}
      </div>
    `;
  };
  listEl.innerHTML = [
    renderGroup('No-boost windows', groups.noBoost),
    renderGroup('Total score rules', groups.totalScore),
    renderGroup('Clan boost rules', groups.clanBoost),
    renderGroup('Multiplier notifications', groups.multiplier)
  ].filter(Boolean).join('');
  commPostConfiguredBotOptionNotifications(context);
  const newlyActiveIds = newlyActive
    .map((entry) => entry?.id)
    .filter((id) => !!id);
  if (newlyActive.length && config?.soundEnabled) {
    newlyActiveIds.forEach((id) => boostHelperActiveRuleIds.add(id));
    playBoostHelperSteamSound();
  } else {
    newlyActiveIds.forEach((id) => boostHelperActiveRuleIds.add(id));
  }
}

function toggleBoostHelperPopup(forceState = null) {
  const state = loadFloatingBoostHelperState();
  const next = forceState == null ? !state.visible : !!forceState;
  state.visible = next;
  saveFloatingBoostHelperState(state);
  const container = ensureFloatingBoostHelperContainer();
  if (!container) return;
  container.style.display = next ? 'flex' : 'none';
  if (next) {
    refreshBoostHelperPopup();
    if (!boostHelperPopupTimer) {
      boostHelperPopupTimer = setInterval(() => refreshBoostHelperPopup(), 1000);
    }
  } else if (boostHelperPopupTimer) {
    clearInterval(boostHelperPopupTimer);
    boostHelperPopupTimer = null;
  }
}

function commRenderMembers() {
  const list = document.getElementById('commMembersList');
  if (!list) return;
  if (!commSession) {
    list.innerHTML = '<h5>Clan members online</h5><div class="muted">Join a clan to view members.</div>';
    return;
  }
  if (!commMembers.length) {
    list.innerHTML = '<h5>Clan members online</h5><div class="muted">No members yet.</div>';
    return;
  }
  const now = Date.now();
  const isOwner = !!commSession?.isOwner;
  const rows = commMembers
    .map((member) => {
      const lastSeen = member.last_seen ? new Date(member.last_seen) : null;
      const online = lastSeen && !Number.isNaN(lastSeen.getTime()) && (now - lastSeen.getTime() < 120000);
      const statusText = online ? 'Online' : `Last seen ${lastSeen ? lastSeen.toLocaleTimeString([], { hour12: false }) : 'unknown'}`;
      const statusClass = online ? 'online' : 'offline';
      const ownerLabel = member.is_owner ? '<span class="status" style="color:#ffd166;margin-left:6px;font-weight:600;">Owner</span>' : '';
      const showActions = isOwner && member.user_name && commSession.memberName && member.user_name !== commSession.memberName;
      const kickBtn = showActions
        ? `<button class="btn ghost comm-kick-btn" data-member="${commEscape(member.user_name)}">Kick</button>`
        : '';
      const ownerBtn = showActions && !member.is_owner
        ? `<button class="btn ghost comm-owner-btn" data-member="${commEscape(member.user_name)}">Make owner</button>`
        : '';
      return `
        <div class="comm-member-row ${statusClass}">
          <div>
            ${commEscape(member.user_name || 'Member')}
            ${ownerLabel}
          </div>
          <div class="status">${commEscape(statusText)}</div>
          ${kickBtn || ownerBtn ? `<div class="comm-member-actions">${kickBtn}${ownerBtn}</div>` : ''}
        </div>
      `;
    })
    .join('');
  list.innerHTML = `<h5>Clan members online</h5>${rows}`;
}

function commRenderSnapshots() {
  const list = document.getElementById('commSnapshotList');
  if (!list) return;
  if (!commSession) {
    list.innerHTML = '<div class="muted">Join a clan to capture snapshots.</div>';
    return;
  }
  if (commSnapshotsLoading) {
    list.innerHTML = '<div class="muted">Loading snapshots...</div>';
    return;
  }
  if (!commSnapshots.length) {
    list.innerHTML = '<div class="muted">No snapshots yet.</div>';
    return;
  }
  list.innerHTML = commSnapshots
    .map((snapshot, index) => {
      const stamp = snapshot.captured_at ? new Date(snapshot.captured_at).toLocaleString() : '';
      const members = Array.isArray(snapshot.members) ? snapshot.members : [];
      const rows = members
        .map((member) => {
          return `<tr><td>${commEscape(member.user_name || '')}</td><td>${commEscape(member.user_th || '')}</td><td>${commEscape(member.boosts || '')}</td></tr>`;
        })
        .join('');
      return `
        <div class="comm-snapshot">
          <h5>Snapshot ${index + 1}  -  ${commEscape(stamp)}</h5>
          <table>
            <thead><tr><th>Member</th><th>TH/s</th><th>Boosts</th></tr></thead>
            <tbody>${rows || '<tr><td colspan="3" class="muted">No members captured.</td></tr>'}</tbody>
          </table>
        </div>
      `;
    })
    .join('');
}

async function commLoadMembers(force = false) {
  if (!commSession) return;
  if (!force && Date.now() - commMembersLastFetch < 15000) return;
  try {
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&order=joined_at.asc`;
    const rows = await commFetch(COMM_TABLE_MEMBERS, query);
    commMembers = Array.isArray(rows) ? rows : [];
    commMembersLastFetch = Date.now();
    commRenderMembers();
  } catch (err) {
    commRenderMembers();
  }
}

async function commHeartbeat() {
  if (!commSession) return;
  try {
    const payload = [{
      clan_id: commSession.clanId,
      clan_name: commSession.clanName,
      user_name: commSession.memberName,
      last_seen: new Date().toISOString()
    }];
    await commInsert(COMM_TABLE_MEMBERS, payload, { upsert: true, onConflict: 'clan_id,user_name' });
  } catch (err) {
    // ignore
  }
}

async function commSendMessage(kind, text) {
  if (!commSession) return;
  const content = (text || '').trim();
  if (kind === 'chat' && !content) return;
  const payload = [{
    clan_id: commSession.clanId,
    clan_name: commSession.clanName,
    sender: commSession.memberName,
    content,
    kind,
    created_at: new Date().toISOString()
  }];
  await commInsert(COMM_TABLE_MESSAGES, payload);
  await commHeartbeat();
}

let commMessageLoading = false;
async function commLoadMessages(force = false) {
  if (!commSession || (commMessageLoading && !force)) return;
  commMessageLoading = true;
  try {
    await commHeartbeat();
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&order=created_at.desc&limit=60`;
    const rows = await commFetch(COMM_TABLE_MESSAGES, query);
    const ordered = Array.isArray(rows) ? rows.slice().reverse() : [];
    const existing = new Set(commMessages.map((msg) => msg.id));
    ordered.forEach((msg) => {
      if (msg?.id) {
        commMessageIds.add(msg.id);
        if (!existing.has(msg.id) && msg.kind === 'ping' && msg.sender !== commSession.memberName) {
          commPlayPingSound();
        }
      }
    });
    commMessages = ordered;
    commRenderChat();
    await commLoadMembers();
  } catch (err) {
    commSetStatus('Unable to load clan chat.', { error: true, privateScope: true });
  } finally {
    commMessageLoading = false;
  }
}

function commStartTimers() {
  if (commChatTimer) clearInterval(commChatTimer);
  if (commMetaTimer) clearInterval(commMetaTimer);
  if (commKorrelStatusTimer) clearInterval(commKorrelStatusTimer);
  if (commKorrelRemoteLogTimer) clearInterval(commKorrelRemoteLogTimer);
  if (commKorrelAutoTimer) clearInterval(commKorrelAutoTimer);
  if (commApiLimitTimer) clearInterval(commApiLimitTimer);
  if (commKorrelOverviewTimer) clearInterval(commKorrelOverviewTimer);
  if (commSession) {
    commChatTimer = setInterval(() => {
      commLoadMessages().catch(() => {});
    }, 5000);
    commMetaTimer = setInterval(() => {
      commLoadNotice().catch(() => {});
      commLoadSnapshots().catch(() => {});
    }, 30000);
  }
  commLoadApiLimitsFromDb().catch(() => {});
  commApiLimitTimer = setInterval(() => {
    commLoadApiLimitsFromDb().catch(() => {});
  }, COMM_API_LIMIT_REFRESH_MS);
  commStartKorrelCountdownTimer();
  commMaybeRunKorrelAutoWork().catch(() => {});
  commKorrelAutoTimer = setInterval(() => {
    commMaybeRunKorrelAutoWork().catch(() => {});
  }, COMM_KORREL_AUTO_CHECK_INTERVAL_MS);
  commKorrelStatusTimer = setInterval(() => {
    if (commIsKorrel()) commLogKorrelRoundCountdownStatus().catch(() => {});
  }, COMM_KORREL_STATUS_INTERVAL_MS);
  if (commIsKorrel()) {
    commFetchKorrelRemoteLogs().catch(() => {});
    commKorrelRemoteLogTimer = setInterval(() => {
      commFetchKorrelRemoteLogs().catch(() => {});
    }, COMM_KORREL_REMOTE_LOG_INTERVAL_MS);
  }
  commStartKorrelOverviewTimer();
}

let commAudioContext = null;
function commPlayPingSound() {
  try {
    if (commAlertMuted) return;
    if (window.speechSynthesis && window.SpeechSynthesisUtterance) {
      const synth = window.speechSynthesis;
      if (synth.speaking || synth.pending) synth.cancel();
      const utter = new SpeechSynthesisUtterance('Time to boost');
      utter.volume = 1;
      utter.rate = 0.9;
      utter.pitch = 1;
      utter.lang = 'en-US';
      const voices = synth.getVoices ? synth.getVoices() : [];
      const englishVoice = voices.find((voice) => voice.lang && voice.lang.toLowerCase().startsWith('en'));
      if (englishVoice) utter.voice = englishVoice;
      synth.speak(utter);
      return;
    }
  } catch (err) {
    // fallback to beep below
  }

  try {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) return;
    if (!commAudioContext) commAudioContext = new AudioCtx();
    if (commAudioContext.state === 'suspended') {
      commAudioContext.resume().catch(() => {});
    }
    const ctx = commAudioContext;
    const now = ctx.currentTime;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(880, now);
    gain.gain.setValueAtTime(0.35, now);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.6);
    osc.connect(gain).connect(ctx.destination);
    osc.start(now);
    osc.stop(now + 0.6);
  } catch (err) {
    // ignore
  }
}

async function commCreateClan() {
  const clanName = (document.getElementById('commCreateName')?.value || '').trim();
  const codeWord = (document.getElementById('commCreateCode')?.value || '').trim();
  if (!clanName || !codeWord) {
    commSetStatus('Provide a clan name and code word.', { error: true });
    return;
  }
  const userName = await commResolveClanMemberName();
  if (!userName) {
    commSetStatus('Unable to read your GoMining username. Open the Mining Wars page and try again.', { error: true });
    return;
  }
  try {
    commSetStatus('Creating clan...');
    const codeHash = await commHashCode(codeWord);
    const payload = [{
      clan_name: clanName,
      code_hash: codeHash,
      created_by: userName,
      created_at: new Date().toISOString()
    }];
    await commInsert(COMM_TABLE_CLANS, payload);
    commSetStatus(`Clan ${clanName} created. Joining...`, { success: true });
    await commJoinClanInternal(clanName, codeWord, userName);
  } catch (err) {
    commSetStatus(err?.message || 'Failed to create clan', { error: true });
  }
}

async function commResolveCurrentClanInfo() {
  let clanName = typeof liveMyClanName === 'string' ? commCleanClanDisplayName(liveMyClanName) : '';
  let clanIdValue = commParseNumber(liveMyClanId);
  try {
    const data = await commRequestPage('EXTRACT_MY_CLAN_NAME');
    if (data) {
      if (typeof data.name === 'string' && data.name.trim()) {
        const cleaned = commCleanClanDisplayName(data.name);
        if (cleaned) clanName = cleaned;
      }
      const parsedId = commParseNumber(data.clanId ?? data.clan_id ?? null);
      if (Number.isFinite(parsedId)) clanIdValue = parsedId;
    }
  } catch (_) {
    // ignore, fallback to cached values
  }
  if (!clanName || !Number.isFinite(clanIdValue)) {
    try {
      const tab = await commFindMiningWarsTabWithAuth();
      if (tab?.id) {
        const apiData = await commFetchClanApiViaInjection(tab.id, { limit: 1, skip: 0, count: 0 });
        const myClanRaw = apiData?.myClan?.data || apiData?.myClan || {};
        const clanRaw = apiData?.clanData?.data || apiData?.clanData || {};
        const apiClanName = commCleanClanDisplayName(
          myClanRaw?.name || myClanRaw?.clanName || clanRaw?.name || clanRaw?.clanName || null
        );
        const apiClanId = commParseNumber(
          apiData?.clanId
          ?? myClanRaw?.id
          ?? myClanRaw?.clanId
          ?? clanRaw?.id
          ?? clanRaw?.clanId
          ?? null
        );
        if (!clanName && apiClanName && String(apiClanName).trim()) {
          clanName = String(apiClanName).trim();
        }
        if (!Number.isFinite(clanIdValue) && Number.isFinite(apiClanId)) {
          clanIdValue = apiClanId;
        }
      }
    } catch (_) {
      // ignore api fallback errors
    }
  }
  if (!clanName && typeof commSession?.clanName === 'string') {
    clanName = commSession.clanName.trim();
  }
  const clanId = Number.isFinite(clanIdValue) ? Math.trunc(clanIdValue) : null;
  if (!clanName && Number.isFinite(clanId)) {
    clanName = `Clan ${clanId}`;
  }
  return {
    clanId,
    clanName: clanName || ''
  };
}

async function commEnsureAutoClan({ force = false, silentStatus = false } = {}) {
  if (commAutoClanPending) return;
  const now = Date.now();
  if (!force && (now - commAutoClanLastAttempt) < COMM_AUTO_CLAN_COOLDOWN_MS) return;
  commAutoClanLastAttempt = now;
  commAutoClanPending = true;
  try {
    const { clanId, clanName } = await commResolveCurrentClanInfo();
    const cleanedClanName = commCleanClanDisplayName(clanName);
    if (!Number.isFinite(clanId)) {
      if (!silentStatus) {
        commSetStatus('Open Mining Wars so we can read your clan.', { error: true });
      }
      return;
    }
    const userName = await commResolveClanMemberName();
    if (!userName) {
      if (!silentStatus) {
        commSetStatus('Unable to read your GoMining username. Open the Mining Wars page and try again.', { error: true });
      }
      return;
    }
    const codeWord = `mw-${clanId}`;
    const codeHash = await commHashCode(codeWord);
    if (!force && commSession?.codeHash && commSession.codeHash === codeHash) {
      commToggleView();
      return;
    }
    if (!silentStatus) {
      commSetStatus('Syncing clan chat...', { privateScope: true });
    }
    let clanRow = null;
    try {
      const queryParts = [
        `select=id,clan_name,created_by,code_hash,${COMM_NOTICE_FIELD}`,
        `code_hash=eq.${codeHash}`,
        cleanedClanName ? `clan_name=eq.${encodeURIComponent(cleanedClanName)}` : null,
        'limit=1'
      ].filter(Boolean);
      const rows = await commFetch(COMM_TABLE_CLANS, queryParts.join('&'));
      if (Array.isArray(rows) && rows.length) clanRow = rows[0];
    } catch (_) {
      // ignore
    }
    if (!clanRow) {
      try {
        await commInsert(COMM_TABLE_CLANS, [{
          clan_name: cleanedClanName || `Clan ${clanId}`,
          code_hash: codeHash,
          created_by: userName,
          created_at: new Date().toISOString()
        }]);
      } catch (err) {
        const msg = err?.message || String(err);
        const lowerMsg = msg.toLowerCase();
        if (!msg.includes('23505') && !lowerMsg.includes('duplicate')) {
          throw err;
        }
      }
      try {
        const queryParts = [
          `select=id,clan_name,created_by,code_hash,${COMM_NOTICE_FIELD}`,
          `code_hash=eq.${codeHash}`,
          'limit=1'
        ];
        const rows = await commFetch(COMM_TABLE_CLANS, queryParts.join('&'));
        if (Array.isArray(rows) && rows.length) clanRow = rows[0];
      } catch (_) {
        // ignore
      }
    }
    const joinName = clanRow?.clan_name || cleanedClanName || `Clan ${clanId}`;
    await commJoinClanInternal(joinName, codeWord, userName, { silentStatus: true });
    if (!silentStatus) {
      commSetStatus(`Connected to ${joinName}.`, { success: true, privateScope: true });
    }
  } catch (err) {
    if (!silentStatus) {
      commSetStatus(err?.message || 'Failed to sync clan chat.', { error: true, privateScope: true });
    }
  } finally {
    commAutoClanPending = false;
  }
}

async function commJoinClanInternal(clanName, codeWord, userName, { silentStatus = false } = {}) {
  if (!clanName || !codeWord || !userName) {
    if (!silentStatus) commSetStatus('Provide clan and code word. Username is read from GoMining.', { error: true });
    return;
  }
  try {
    if (!silentStatus) commSetStatus('Joining clan...');
    await ensureHomeAuthUser();
    const codeHash = await commHashCode(codeWord);
    const primaryQuery = `clan_name=eq.${encodeURIComponent(clanName)}&code_hash=eq.${codeHash}`;
    let clans = await commFetch(COMM_TABLE_CLANS, primaryQuery);
    if (!Array.isArray(clans) || !clans.length) {
      const fallbackQuery = `code_hash=eq.${codeHash}&limit=1`;
      clans = await commFetch(COMM_TABLE_CLANS, fallbackQuery);
    }
    if (!Array.isArray(clans) || !clans.length) throw new Error('Clan not found or code word invalid.');
    const clan = clans[0];
    const authUserId = getHomeAuthUserId();
    const authEmail = homeGoogleProfile?.email || null;
    const memberPayload = [{
      clan_id: clan.id,
      clan_name: clan.clan_name,
      user_name: userName,
      user_id: authUserId || null,
      user_email: authEmail,
      joined_at: new Date().toISOString()
    }];
    let memberRow = memberPayload[0];
    const fallbackConflict = 'clan_id,user_name';
    try {
      const memberRes = await commInsert(
        COMM_TABLE_MEMBERS,
        memberPayload,
        { upsert: true, onConflict: authUserId ? 'user_id' : fallbackConflict }
      );
      memberRow = memberRes?.payload?.[0] || memberPayload[0];
    } catch (err) {
      const msg = err?.message || String(err);
      const lowerMsg = msg.toLowerCase();
      if (msg.includes('42P10') || msg.includes('ON CONFLICT') || lowerMsg.includes('duplicate key') || msg.includes('23505')) {
        if (authUserId) {
          try {
            const patchQuery = `user_id=eq.${encodeURIComponent(authUserId)}`;
            await commPatch(COMM_TABLE_MEMBERS, patchQuery, {
              clan_id: clan.id,
              clan_name: clan.clan_name,
              user_name: userName,
              user_email: authEmail
            });
            const patched = await commFetch(
              COMM_TABLE_MEMBERS,
              `select=id,clan_id,clan_name,user_name,is_owner&${patchQuery}&limit=1`
            );
            if (Array.isArray(patched) && patched.length) {
              memberRow = patched[0];
            }
          } catch (patchErr) {
            // ignore and fallback to conflict on clan/name
          }
        }
        try {
          const fallbackRes = await commInsert(
            COMM_TABLE_MEMBERS,
            memberPayload,
            { upsert: true, onConflict: fallbackConflict }
          );
          memberRow = fallbackRes?.payload?.[0] || memberPayload[0];
        } catch (fallbackErr) {
          throw fallbackErr;
        }
      } else {
        throw err;
      }
    }
    const creator = clan.created_by || '';
    const isOwner = creator && creator.toLowerCase() === userName.toLowerCase();
    commSession = {
      clanId: clan.id,
      clanName: clan.clan_name,
      memberName: userName,
      clanCreator: creator,
      isOwner,
      lastCodeWord: isOwner ? codeWord : '',
      memberId: memberRow?.id || null,
      codeHash
    };
    commNoticeText = clan[COMM_NOTICE_FIELD] || '';
    commSaveSession(commSession);
    if (!silentStatus) commSetStatus(`Joined ${clan.clan_name}.`, { success: true });
    commToggleView();
    await commHeartbeat();
    await commLoadMessages(true);
    await commLoadMembers(true);
    await commLoadNotice();
    commStartTimers();
  } catch (err) {
    if (!silentStatus) commSetStatus(err?.message || 'Failed to join clan.', { error: true });
  }
}

async function commJoinClan() {
  const clanName = (document.getElementById('commJoinName')?.value || '').trim();
  const codeWord = (document.getElementById('commJoinCode')?.value || '').trim();
  if (!clanName || !codeWord) {
    commSetStatus('Provide clan and code word.', { error: true });
    return;
  }
  const userName = await commResolveClanMemberName();
  if (!userName) {
    commSetStatus('Unable to read your GoMining username. Open the Mining Wars page and try again.', { error: true });
    return;
  }
  await commJoinClanInternal(clanName, codeWord, userName);
}

async function commLeaveClan() {
  if (!commSession) {
    commSetStatus('Not currently in a clan.', { error: true });
    return;
  }
  try {
    commSetStatus('Leaving clan...', { privateScope: true });
    const authUserId = getHomeAuthUserId();
    const query = authUserId
      ? `user_id=eq.${encodeURIComponent(authUserId)}`
      : `clan_id=eq.${encodeURIComponent(commSession.clanId)}&user_name=eq.${encodeURIComponent(commSession.memberName)}`;
    await commDelete(COMM_TABLE_MEMBERS, query);
    commSetStatus('Left the clan.', { success: true, privateScope: true });
  } catch (err) {
    commSetStatus(err?.message || 'Unable to leave clan.', { error: true, privateScope: true });
  } finally {
    commResetSession();
    commSetStatus('', {});
  }
}

async function commSendChat() {
  const input = document.getElementById('commMessageInput');
  if (!input) return;
  const message = input.value || '';
  if (!message.trim()) return;
  try {
    await commSendMessage('chat', message);
    input.value = '';
    await commLoadMessages(true);
  } catch (err) {
    commSetStatus('Failed to send message.', { error: true, privateScope: true });
  }
}

async function commSendPing() {
  try {
    await commSendMessage('ping', `${commSession?.memberName || 'Member'} wants to boost`);
    commSetStatus('Ping dispatched.', { success: true, privateScope: true });
    commPlayPingSound();
    try {
      if (window.Notification && Notification.permission === 'granted') {
        new Notification('Clan ping sent', { body: 'Your clan has been alerted.' });
      } else if (window.Notification && Notification.permission === 'default') {
        Notification.requestPermission();
      }
    } catch (err) {
      // ignore
    }
    await commLoadMessages(true);
    await commLoadMembers(true);
  } catch (err) {
    commSetStatus('Failed to alert clan.', { error: true, privateScope: true });
  }
}

async function commKickMember(memberName) {
  if (!commSession || !memberName) return;
  if (!commSession.isOwner) {
    commSetStatus('Only clan creator can kick members.', { error: true, privateScope: true });
    return;
  }
  if (memberName === commSession.memberName) {
    commSetStatus('Cannot kick yourself.', { error: true, privateScope: true });
    return;
  }
  try {
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&user_name=eq.${encodeURIComponent(memberName)}`;
    await commDelete(COMM_TABLE_MEMBERS, query);
    await commSendMessage('chat', `${commSession.memberName} removed ${memberName} from the clan`);
    commSetStatus(`Removed ${memberName}.`, { success: true, privateScope: true });
    await commLoadMembers(true);
  } catch (err) {
    commSetStatus(err?.message || 'Failed to remove member.', { error: true, privateScope: true });
  }
}

async function commPromoteMember(memberName) {
  if (!commSession || !memberName) return;
  if (!commSession.isOwner) {
    commSetStatus('Only clan owner can assign new owners.', { error: true, privateScope: true });
    return;
  }
  if (memberName === commSession.memberName) {
    commSetStatus('You are already the owner.', { error: true, privateScope: true });
    return;
  }
  try {
    const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&user_name=eq.${encodeURIComponent(memberName)}`;
    await commPatch(COMM_TABLE_MEMBERS, query, { is_owner: true });
    await commSendMessage('chat', `${commSession.memberName} granted owner rights to ${memberName}`);
    commSetStatus(`Granted owner rights to ${memberName}.`, { success: true, privateScope: true });
    await commLoadMembers(true);
  } catch (err) {
    commSetStatus(err?.message || 'Unable to assign owner rights.', { error: true, privateScope: true });
  }
}

async function commUpdateCodeWord() {
  if (!commSession) return;
  if (!commSession.isOwner) {
    commSetStatus('Only clan creator can change the code word.', { error: true, privateScope: true });
    return;
  }
  const input = document.getElementById('commCodeInput');
  const value = (input?.value || '').trim();
  if (!value || value.length < 4) {
    commSetStatus('Enter a code word (min 4 characters).', { error: true, privateScope: true });
    return;
  }
  try {
    commSetStatus('Updating code word...', { privateScope: true });
    const hash = await commHashCode(value);
    const query = `id=eq.${encodeURIComponent(commSession.clanId)}`;
    await commPatch(COMM_TABLE_CLANS, query, { code_hash: hash });
    if (input) input.value = value;
    commSession.lastCodeWord = value;
    commSession.codeHash = hash;
    commSaveSession(commSession);
    commSetStatus('Code word updated. Share it with trusted members.', { success: true, privateScope: true });
  } catch (err) {
    commSetStatus(err?.message || 'Unable to update code word.', { error: true, privateScope: true });
  }
}

async function commSaveNotice() {
  if (!commSession) return;
  if (!commNoticeAvailable) {
    commSetNoticeStatus('Clan notices unavailable. Ask admin to add notice_text column.', { error: true });
    return;
  }
  if (!commSession.isOwner) {
    commSetNoticeStatus('Only an owner can edit the notice.', { error: true });
    return;
  }
  const input = document.getElementById('commNoticeInput');
  if (!input) return;
  const text = input.value || '';
  try {
    commSetNoticeStatus('Saving...');
    const query = `id=eq.${encodeURIComponent(commSession.clanId)}`;
    await commPatch(COMM_TABLE_CLANS, query, { [COMM_NOTICE_FIELD]: text });
    commNoticeText = text;
    commSetNoticeStatus('Notice updated.', { success: true });
  } catch (err) {
    if (String(err).includes(COMM_NOTICE_FIELD)) {
      commNoticeAvailable = false;
      commToggleView();
      commSetNoticeStatus('Clan notices unavailable (missing notice_text column).', { error: true });
      return;
    }
    commSetNoticeStatus(err?.message || 'Unable to save notice.', { error: true });
  }
}

const COMM_MINING_WARS_URL_PATTERNS = ['*://gomining.com/*miner-wars*', '*://*.gomining.com/*miner-wars*'];
const COMM_GOMINING_URL_PATTERNS = ['*://gomining.com/*', '*://*.gomining.com/*'];
function commIsMiningWarsHost(hostname) {
  return /(^|\.)gomining\.com$/i.test(hostname || '');
}

function commIsMiningWarsUrl(url) {
  try {
    const parsed = new URL(url);
    return commIsMiningWarsHost(parsed.hostname) && /\/miner-wars(\/|$)/i.test(parsed.pathname || '');
  } catch (_) {
    return false;
  }
}

function commFindMiningWarsTab() {
  return new Promise((resolve) => {
    if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) {
      resolve(null);
      return;
    }
    if (!chrome?.tabs?.query) {
      resolve(null);
      return;
    }
    chrome.tabs.query({ active: true, currentWindow: true }, (activeTabs) => {
      const active = Array.isArray(activeTabs) && activeTabs.length ? activeTabs[0] : null;
      const activeUrl = active?.url || '';
      if (active && commIsMiningWarsUrl(activeUrl)) {
        resolve(active);
        return;
      }
      chrome.tabs.query({ url: COMM_MINING_WARS_URL_PATTERNS }, (tabs) => {
        if (!Array.isArray(tabs) || !tabs.length) {
          resolve(null);
          return;
        }
        const poolTab = tabs.find((tab) => /\/miner-wars\/pool\//i.test(tab.url || ''));
        resolve(poolTab || tabs[0]);
      });
    });
  });
}

function commTabHasAccessToken(tabId) {
  return new Promise((resolve) => {
    if (!chrome?.scripting?.executeScript) {
      resolve(false);
      return;
    }
    chrome.scripting.executeScript(
      {
        target: { tabId },
        func: () => {
          try {
            return /(?:^|; )access_token=/.test(document.cookie || '');
          } catch (err) {
            return false;
          }
        }
      },
      (results) => {
        if (chrome.runtime.lastError) {
          resolve(false);
          return;
        }
        resolve(Boolean(results && results[0] && results[0].result));
      }
    );
  });
}

function commFindMiningWarsTabWithAuth() {
  return new Promise((resolve) => {
    if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) {
      resolve(null);
      return;
    }
    if (!chrome?.tabs?.query) {
      resolve(null);
      return;
    }
    chrome.tabs.query({ active: true, currentWindow: true }, async (activeTabs) => {
      const active = Array.isArray(activeTabs) && activeTabs.length ? activeTabs[0] : null;
      const activeUrl = active?.url || '';
      if (active && commIsMiningWarsUrl(activeUrl)) {
        const activeHasToken = await commTabHasAccessToken(active.id);
        if (activeHasToken) {
          resolve(active);
          return;
        }
      }
      chrome.tabs.query({ url: COMM_MINING_WARS_URL_PATTERNS }, async (tabs) => {
        if (!Array.isArray(tabs) || !tabs.length) {
          resolve(null);
          return;
        }
        for (const tab of tabs) {
          if (!tab?.id) continue;
          const hasToken = await commTabHasAccessToken(tab.id);
          if (hasToken) {
            resolve(tab);
            return;
          }
        }
        resolve(null);
      });
    });
  });
}

function commInjectContentScript(tabId) {
  return new Promise((resolve, reject) => {
    if (!chrome?.scripting?.executeScript) {
      reject(new Error('Cannot inject content script.'));
      return;
    }
    chrome.scripting.executeScript({ target: { tabId }, files: ['content-script.js'] }, () => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message || 'Failed to inject content script.'));
        return;
      }
      resolve();
    });
  });
}

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

async function commEnsureContentScript(tabId) {
  if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) {
    return false;
  }
  const ready = await commProbeTabForContent(tabId);
  if (ready) return true;
  try {
    await commInjectContentScript(tabId);
  } catch (err) {
    return false;
  }
  for (let attempt = 0; attempt < 5; attempt += 1) {
    await commDelay(200);
    const probe = await commProbeTabForContent(tabId);
    if (probe) return true;
  }
  return false;
}

async function commFetchClanApiViaInjection(tabId, payload) {
  const endpointMy = 'https://api.gomining.com/api/nft-game/clan/get-my';
  const endpointById = 'https://api.gomining.com/api/nft-game/clan/get-by-id';
  const context = commInferApiCallContext(endpointById, payload);
  await commEnforceApiCallLimit({ endpoint: endpointById, cost: 2, context });
  return new Promise((resolve, reject) => {
    if (!chrome?.scripting?.executeScript) {
      reject(new Error('Cannot execute script.'));
      return;
    }
    chrome.scripting.executeScript({
      target: { tabId },
      func: async (request) => {
        const readToken = () => {
          const match = document.cookie.match(/(?:^|; )access_token=([^;]+)/);
          return match ? decodeURIComponent(match[1]) : '';
        };
        const fetchJson = async (url, token, body) => {
        const headers = {
          'Content-Type': 'application/json',
          Accept: 'application/json,text/plain,*/*',
          'Accept-Language': navigator.language || 'en-US',
          'Cache-Control': 'no-cache',
          Pragma: 'no-cache',
          Priority: 'u=1,i',
          'Ngsw-Bypass': 'true',
          Referer: `${location.origin}/`,
          '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) : '{}'
          });
          const text = await res.text();
          if (!res.ok) {
            throw new Error(text || `Clan API failed (${res.status})`);
          }
          return text ? JSON.parse(text) : null;
        };
        try {
          const token = readToken();
          const meta = { tokenFound: Boolean(token), usedAuthHeader: Boolean(token) };
          const myClan = await fetchJson('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(request?.limit)) ? Number(request.limit) : 10;
          const skip = Number.isFinite(Number(request?.skip)) ? Number(request.skip) : 0;
          const count = Number.isFinite(Number(request?.count)) ? Number(request.count) : 0;
          const pagination = { limit, skip };
          if (count > 0) pagination.count = count;
          const body = {
            clanId,
            pagination,
            filters: { filterType: 'none' },
            sort: { sortType: 'none' }
          };
          const clanData = await fetchJson('https://api.gomining.com/api/nft-game/clan/get-by-id', token, body);
          meta.clanStatus = 200;
          return { ok: true, data: { clanId, myClan, clanData, meta } };
        } catch (err) {
          return { ok: false, error: err?.message || String(err) };
        }
      },
      args: [payload]
    }, (results) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message || 'Failed to run script.'));
        return;
      }
      const response = results && results[0] && results[0].result;
      if (!response) {
        commRecordApiCall({
          endpoint: endpointById,
          status: 0,
          ok: false,
          callCount: 2,
          cycleId: context?.cycleId,
          leagueId: context?.leagueId
        }).catch(() => {});
        reject(new Error('No response from page.'));
        return;
      }
      if (!response.ok) {
        commRecordApiCall({
          endpoint: endpointById,
          status: 0,
          ok: false,
          callCount: 2,
          cycleId: context?.cycleId,
          leagueId: context?.leagueId
        }).catch(() => {});
        reject(new Error(response.error || 'Clan API failed.'));
        return;
      }
      const meta = response?.data?.meta || {};
      const myStatus = Number.isFinite(commParseNumber(meta.myClanStatus)) ? Math.trunc(commParseNumber(meta.myClanStatus)) : 200;
      const byIdStatus = Number.isFinite(commParseNumber(meta.clanStatus)) ? Math.trunc(commParseNumber(meta.clanStatus)) : 200;
      commRecordApiCall({
        endpoint: endpointMy,
        status: myStatus,
        ok: myStatus >= 200 && myStatus < 300,
        callCount: 1,
        cycleId: context?.cycleId,
        leagueId: context?.leagueId
      }).catch(() => {});
      commRecordApiCall({
        endpoint: endpointById,
        status: byIdStatus,
        ok: byIdStatus >= 200 && byIdStatus < 300,
        callCount: 1,
        cycleId: context?.cycleId,
        leagueId: context?.leagueId
      }).catch(() => {});
      resolve(response.data);
    });
  });
}

async function commFetchGominingApiViaInjection(tabId, endpoint, payload, options = {}) {
  if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) {
    throw new Error('Website mode: GoMining connection disabled.');
  }
  const callCost = Math.max(1, Math.trunc(commParseNumber(options?.cost) || 1));
  const context = commInferApiCallContext(endpoint, payload, options);
  if (!options?.skipLimit) {
    await commEnforceApiCallLimit({ endpoint, cost: callCost, context });
  }
  return new Promise((resolve, reject) => {
    if (!chrome?.scripting?.executeScript) {
      reject(new Error('Cannot execute script.'));
      return;
    }
    chrome.scripting.executeScript({
      target: { tabId },
      func: async (url, body, method) => {
        const isJwtLike = (value) => typeof value === 'string' && value.split('.').length === 3;
        const findJwtInObject = (value, depth = 0, seen = new Set()) => {
          if (depth > 5 || value == null) return '';
          if (typeof value === 'string') {
            return isJwtLike(value) ? value : '';
          }
          if (typeof value !== 'object') return '';
          if (seen.has(value)) return '';
          seen.add(value);
          for (const key in value) {
            if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
            const entry = value[key];
            if (typeof entry === 'string') {
              if (isJwtLike(entry)) return entry;
            } else if (entry && typeof entry === 'object') {
              const nested = findJwtInObject(entry, depth + 1, seen);
              if (nested) return nested;
            }
          }
          return '';
        };
        const readCookieValue = (key) => {
          const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]+)`));
          return match ? decodeURIComponent(match[1]) : '';
        };
        const readToken = () => {
          const cookieToken = readCookieValue('access_token');
          if (cookieToken) return cookieToken;
          const directKeys = ['access_token', 'auth_token', 'gomining_access_token', 'accessToken', 'authToken', 'token', 'jwt', 'idToken'];
          for (const key of directKeys) {
            const raw = localStorage.getItem(key) || sessionStorage.getItem(key);
            if (isJwtLike(raw)) return raw;
          }
          const scanStorage = (store) => {
            for (let i = 0; i < store.length; i += 1) {
              const key = store.key(i);
              if (!key) continue;
              const raw = store.getItem(key);
              if (!raw) continue;
              if (isJwtLike(raw)) return raw;
              if (raw.startsWith('{') || raw.startsWith('[')) {
                try {
                  const parsed = JSON.parse(raw);
                  const token =
                    parsed?.access_token ||
                    parsed?.accessToken ||
                    parsed?.auth_token ||
                    parsed?.authToken ||
                    parsed?.token ||
                    parsed?.jwt ||
                    parsed?.idToken ||
                    parsed?.currentSession?.access_token ||
                    parsed?.session?.access_token ||
                    parsed?.data?.access_token ||
                    parsed?.data?.accessToken;
                  if (isJwtLike(token)) return token;
                  const nested = findJwtInObject(parsed);
                  if (nested) return nested;
                } catch (_) {}
              }
            }
            return '';
          };
          const storageToken = scanStorage(localStorage) || scanStorage(sessionStorage) || '';
          if (storageToken) return storageToken;
          const windowToken = findJwtInObject(window.__NUXT__ || window.__INITIAL_STATE__ || window.__NGXS__ || window.__STORE__ || null);
          return windowToken || '';
        };
        const toJson = (text) => {
          if (!text) return null;
          try {
            return JSON.parse(text);
          } catch (_) {
            return text;
          }
        };
        try {
          const endpointUrl = String(url || '').trim();
          if (!endpointUrl) {
            return { ok: false, status: 0, error: 'Missing endpoint.', method: method || 'POST' };
          }
          const requestMethod = (method || 'POST').toUpperCase();
          const token = readToken();
          const randomNumber = readCookieValue('randomNumber');
          const entryUrl = readCookieValue('entryUrl');
          const meta = { tokenFound: Boolean(token), usedAuthHeader: Boolean(token) };
          const buildHeaders = (includeAuth) => {
            const headers = {
              'Content-Type': 'application/json',
              accept: 'application/json, text/plain, */*',
              'x-device-type': 'desktop'
            };
            if (includeAuth && token) headers.Authorization = `Bearer ${token}`;
            if (randomNumber) headers['x-random-number'] = randomNumber;
            if (entryUrl) headers['x-entry-url'] = entryUrl;
            return headers;
          };
          const fetchOptions = {
            method: requestMethod,
            credentials: 'include'
          };
          if (requestMethod !== 'GET') {
            fetchOptions.body = JSON.stringify(body || {});
          }
          let headers = buildHeaders(true);
          let res = await fetch(endpointUrl, { ...fetchOptions, headers });
          let text = await res.text();
          let data = toJson(text);
          let usedAuthHeader = Boolean(headers.Authorization);
          if (usedAuthHeader && (res.status === 401 || res.status === 403)) {
            const retryHeaders = buildHeaders(false);
            res = await fetch(endpointUrl, { ...fetchOptions, headers: retryHeaders });
            text = await res.text();
            data = toJson(text);
            headers = retryHeaders;
            usedAuthHeader = false;
          }
          return {
            ok: res.ok,
            status: res.status,
            data,
            meta: {
              ...meta,
              usedAuthHeader,
              requestHeaders: headers
            },
            error: res.ok ? null : (typeof data === 'string' ? data : (data?.message || data?.error || null)),
            method: requestMethod
          };
        } catch (err) {
          return { ok: false, status: 0, error: err?.message || String(err), method: method || 'POST' };
        }
      },
      args: [endpoint, payload, options.method || 'POST']
    }, (results) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message || 'Failed to run script.'));
        return;
      }
      const response = results && results[0] && results[0].result;
      if (!response) {
        commRecordApiCall({
          endpoint,
          status: 0,
          ok: false,
          callCount: callCost,
          cycleId: context?.cycleId,
          leagueId: context?.leagueId
        }).catch(() => {});
        reject(new Error('No response from page.'));
        return;
      }
      commRecordApiCall({
        endpoint,
        status: response?.status,
        ok: response?.ok,
        callCount: callCost,
        cycleId: context?.cycleId,
        leagueId: context?.leagueId
      }).catch(() => {});
      resolve(response);
    });
  });
}

function commProbeTabForContent(tabId) {
  return new Promise((resolve) => {
    if (!chrome?.scripting?.executeScript) {
      resolve(false);
      return;
    }
    chrome.scripting.executeScript(
      { target: { tabId }, func: () => Boolean(window.__MW_CONTENT_SCRIPT_LOADED__) },
      (results) => {
        if (chrome.runtime.lastError) {
          resolve(false);
          return;
        }
        resolve(Boolean(results && results[0] && results[0].result));
      }
    );
  });
}

function commSendToTab(tabId, message) {
  return new Promise((resolve, reject) => {
    if (!chrome?.tabs?.sendMessage) {
      reject(new Error('Tab messaging unavailable.'));
      return;
    }
    chrome.tabs.sendMessage(tabId, message, (response) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message || 'Tab message failed.'));
        return;
      }
      if (!response) {
        reject(new Error('No response from tab.'));
        return;
      }
      if (!response.ok) {
        reject(new Error(response.error || 'Page extraction failed.'));
        return;
      }
      resolve(response.data);
    });
  });
}

async function commRequestPageViaTab(type, payload = {}) {
  const tab = await commFindMiningWarsTab();
  if (!tab?.id) {
    throw new Error('No Mining Wars tab found.');
  }
  const message = { type, ...payload };
  try {
    return await commSendToTab(tab.id, message);
  } catch (err) {
    const msg = err?.message || '';
    if (
      msg.includes('Receiving end does not exist') ||
      msg.includes('Could not establish connection') ||
      msg.includes('No response from tab') ||
      msg.includes('Tab message failed')
    ) {
      const retryReady = await commEnsureContentScript(tab.id);
      if (retryReady) return await commSendToTab(tab.id, message);
      throw new Error('Content script not ready in that tab. Reload the Mining Wars tab and try again.');
    }
    throw err;
  }
}

function commRequestPage(type, payload = {}) {
  return new Promise((resolve, reject) => {
    if (!chrome?.runtime?.sendMessage) {
      commRequestPageViaTab(type, payload).then(resolve).catch(reject);
      return;
    }
    chrome.runtime.sendMessage({ type, ...payload }, (response) => {
      if (chrome.runtime.lastError) {
        const msg = chrome.runtime.lastError.message || 'Request failed';
        if (msg.includes('message port closed') || msg.includes('port closed')) {
          commRequestPageViaTab(type, payload).then(resolve).catch(() => reject(new Error(msg)));
          return;
        }
        reject(new Error(msg));
        return;
      }
      if (!response) {
        commRequestPageViaTab(type, payload).then(resolve).catch(() => reject(new Error('No response from page.')));
        return;
      }
      if (!response.ok) {
        const msg = response.error || 'Page extraction failed.';
        if (msg.includes('No content script tab') || msg.includes('Failed to reach content script')) {
          commRequestPageViaTab(type, payload).then(resolve).catch(() => reject(new Error(msg)));
          return;
        }
        reject(new Error(msg));
        return;
      }
      resolve(response.data);
    });
  });
}

async function commCaptureSnapshot() {
  if (!commSession) return;
  if (!commSession.isOwner) {
    commSetSnapshotStatus('Only a clan owner can capture snapshots.', { error: true });
    return;
  }
  try {
    commSetSnapshotStatus('Requesting clan data...');
    const data = await commRequestPage('EXTRACT_CLAN_PAGE');
    const summary = data?.summary || {};
    if (!summary?.clanId) {
      throw new Error('No clan data detected on the current page. Open your clan page in Mining Wars first.');
    }
    const members = Array.isArray(data?.members) ? data.members : [];
    const snapshotPayload = [{
      clan_id: commSession.clanId,
      clan_name: summary.clanName || commSession.clanName,
      captured_at: new Date().toISOString(),
      metadata: summary
    }];
    const snapshotRes = await commInsert(COMM_TABLE_SNAPSHOTS, snapshotPayload);
    const snapshotId = snapshotRes?.payload?.[0]?.id || snapshotRes?.payload?.[0]?.snapshot_id;
    if (snapshotId && members.length) {
      const rows = members
        .map((member) => ({
          snapshot_id: snapshotId,
          clan_id: commSession.clanId,
          user_name: member.userName || member.user_name,
          user_th: member.userTh || member.user_th,
          boosts: Array.isArray(member.boosts) ? member.boosts.join(', ') : (member.boosts || '')
        }))
        .filter((row) => row.user_name);
      if (rows.length) await commInsert(COMM_TABLE_SNAPSHOT_MEMBERS, rows);
    }
    commSetSnapshotStatus(`Captured snapshot with ${members.length} members.`);
    await commLoadSnapshots(true);
  } catch (err) {
    commSetSnapshotStatus(err?.message || 'Failed to capture snapshot.', { error: true });
  }
}

async function commStoreClanStats() {
  if (!commSession) return;
  if (!commIsKorrel()) {
    commSetKorrelLog('Only korrel can run this action.', { error: true });
    return;
  }
  try {
    commSetKorrelLog('Requesting clan data from the active Mining Wars page...');
    const tab = await commFindMiningWarsTab();
    if (tab?.id) {
      commSetKorrelLog(`Using tab ${tab.id}: ${tab.url || 'unknown url'}`);
      if (tab.url && !/\/miner-wars\/pool\//i.test(tab.url)) {
        commSetKorrelLog('Tip: open your clan page (/miner-wars/pool/...) for best results.');
      }
    }
    const data = await commRequestPageViaTab('EXTRACT_CLAN_PAGE', { skipMembers: true, skipReadyWait: true });
    const summary = data?.summary || {};
    const rawClanId = summary.clanId ?? commSession.clanId;
    const parsedClanId = commParseNumber(rawClanId);
    const clanId = Number.isFinite(parsedClanId) ? Math.trunc(parsedClanId) : null;
    if (!clanId) {
      throw new Error('No clan id detected. Open your clan page in Mining Wars first.');
    }
    const payload = {
      time_scrape: summary.time_scrape || new Date().toISOString(),
      clan_id: clanId,
      clan_name: summary.clanName || commSession.clanName || null,
      clan_th: commParseNumber(summary.clanTh),
      users_count: commParseNumber(summary.users),
      current_blocks: commParseNumber(summary.currentBlocks),
      league: summary.league || null,
      description: summary.description || null,
      earned_gmt: commParseNumber(summary.earnedGmt),
      btc: commParseNumber(summary.btc),
      position: commParseNumber(summary.position),
      free_to_join: typeof summary.freeToJoin === 'boolean' ? summary.freeToJoin : null,
      url: summary.url || null
    };
    commSetKorrelLog(`Saving clan ${payload.clan_name || payload.clan_id}...`);
    try {
      await commInsert(COMM_TABLE_CLAN_STATS, [payload], { upsert: true });
    } catch (err) {
      const msg = err?.message || String(err);
      if (msg.includes('42P10') || msg.includes('ON CONFLICT')) {
        commSetKorrelLog('Upsert unavailable, retrying insert only...');
        await commInsert(COMM_TABLE_CLAN_STATS, [payload]);
      } else {
        throw err;
      }
    }
    commSetKorrelLog(
      `Saved clan ${payload.clan_name || payload.clan_id} (TH ${payload.clan_th ?? 'n/a'}, users ${payload.users_count ?? 'n/a'}, blocks ${payload.current_blocks ?? 'n/a'}, league ${payload.league ?? 'n/a'}).`,
      { success: true }
    );
  } catch (err) {
    commSetKorrelLog(err?.message || 'Failed to store clan stats.', { error: true });
  }
}

async function commFetchClanApiData() {
  if (!commSession) return;
  if (!commIsKorrel()) {
    commSetKorrelLog('Only korrel can run this action.', { error: true });
    return;
  }
  try {
    commSetKorrelLog('Requesting clan data from GoMining API...');
    commSetKorrelApiInputs([
      'https://api.gomining.com/api/nft-game/clan/get-my',
      'https://api.gomining.com/api/nft-game/clan/get-by-id'
    ]);
    const tab = await commFindMiningWarsTab();
    if (tab?.id) {
      commSetKorrelLog(`Using tab ${tab.id}: ${tab.url || 'unknown url'}`);
    }
    let data;
    try {
      data = await commRequestPageViaTab('FETCH_CLAN_API', { limit: 25, skip: 0, count: 0 });
    } catch (err) {
      const msg = err?.message || '';
      if (tab?.id && msg.includes('Content script not ready')) {
        commSetKorrelLog('Content script missing, using direct API injection...');
        data = await commFetchClanApiViaInjection(tab.id, { limit: 25, skip: 0, count: 0 });
      } else {
        throw err;
      }
    }
    commStoreKorrelRawPayload('clan/get-my + clan/get-by-id', data);
    const meta = data?.meta || {};
    const myClanRaw = data?.myClan?.data || data?.myClan || {};
    const clanRaw = data?.clanData?.data || data?.clanData || {};
    const clanIdValue = data?.clanId ?? myClanRaw?.id ?? myClanRaw?.clanId ?? clanRaw?.id ?? clanRaw?.clanId ?? commSession?.clanId;
    const parsedClanId = commParseNumber(clanIdValue);
    const clanId = Number.isFinite(parsedClanId) ? Math.trunc(parsedClanId) : null;
    if (!clanId) {
      throw new Error('No clan id returned from API. Make sure you are logged in.');
    }
    commKorrelApiLastClanId = clanId;
    const clanName = clanRaw?.name ?? myClanRaw?.name ?? commSession?.clanName ?? null;
    const leagueId = clanRaw?.leagueId ?? clanRaw?.league?.id ?? myClanRaw?.leagueId ?? myClanRaw?.league?.id ?? null;
    const description = clanRaw?.description ?? myClanRaw?.description ?? null;
    const usersCount = clanRaw?.usersCount ?? clanRaw?.membersCount ?? myClanRaw?.usersCount ?? myClanRaw?.membersCount ?? null;
    const power = clanRaw?.power ?? clanRaw?.clanPower ?? myClanRaw?.power ?? myClanRaw?.clanPower ?? null;
    const blocksMined = clanRaw?.blocksMined ?? clanRaw?.blocks ?? myClanRaw?.blocksMined ?? myClanRaw?.blocks ?? null;
    const blocksThisCycle = clanRaw?.blocksThisCycle ?? clanRaw?.blocksShare ?? clanRaw?.cycleBlocks ?? null;
    const clanRank = clanRaw?.clanRank ?? clanRaw?.rank ?? null;
    const btcIncome = clanRaw?.btcIncome ?? clanRaw?.btc ?? null;
    const gmtIncome = clanRaw?.gmtIncome ?? clanRaw?.gmt ?? null;
    commSetKorrelTable([
      ['Clan ID', clanId],
      ['Clan name', clanName],
      ['League ID', leagueId],
      ['Users', usersCount],
      ['Power TH/s', power],
      ['Blocks total', blocksMined],
      ['Blocks this cycle', blocksThisCycle],
      ['Clan rank', clanRank],
      ['Description', description],
      ['BTC income', btcIncome],
      ['GMT income', gmtIncome],
      ['Auth header used', meta?.usedAuthHeader ? 'yes' : 'no'],
      ['Token cookie', meta?.tokenFound ? 'found' : 'not found']
    ]);
    commSetKorrelDetailActions([]);
    commSetKorrelLog(`Fetched clan API data for ${clanName || clanId}.`, { success: true });
  } catch (err) {
    const msg = err?.message || 'Failed to fetch clan API data.';
    commSetKorrelLog(msg, { error: true });
    commSetKorrelTable([
      ['Endpoint', 'clan/get-my + clan/get-by-id'],
      ['Status', '-'],
      ['OK', 'no'],
      ['Message', msg]
    ]);
    commSetKorrelDetailActions([]);
  }
}

async function commRunKorrelApiTest(label, endpoint, payload, options = {}) {
  if (!commSession) return;
  if (!commIsKorrel()) {
    commSetKorrelLog('Only korrel can run this action.', { error: true });
    return;
  }
  const throttleMs = commShouldThrottleKorrelApi(endpoint);
  if (throttleMs !== null) {
    const seconds = Math.ceil(throttleMs / 1000);
    const msg = `Rate limit active. Try again in ${seconds}s.`;
    commSetKorrelLog(msg, { error: true });
    commSetKorrelTable([
      ['Endpoint', endpoint],
      ['Status', '-'],
      ['OK', 'no'],
      ['Message', msg]
    ]);
    commSetKorrelApiInputs(endpoint);
    commSetKorrelDetailActions([]);
    return;
  }
  const tab = await commFindMiningWarsTab();
  if (!tab?.id) {
    const msg = 'No Mining Wars tab found.';
    commSetKorrelLog(msg, { error: true });
    commSetKorrelTable([
      ['Endpoint', endpoint],
      ['Status', '-'],
      ['OK', 'no'],
      ['Message', msg]
    ]);
    commSetKorrelApiInputs(endpoint);
    return;
  }
  commSetKorrelLog(`Requesting ${label}...`);
  commSetKorrelLog(`Using tab ${tab.id}: ${tab.url || 'unknown url'}`);
  commSetKorrelApiInputs(endpoint);
  try {
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload || {}, options);
    commStoreKorrelRawPayload(label, response?.data ?? response);
    const fields = commBuildApiFields(endpoint, payload, response);
    commSetKorrelTable(fields);
    commSetKorrelDetailActions(commExtractApiArrayDetails(response));
    if (response?.ok) {
      const raw = response?.data;
      const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
      const candidateId = inner?.id ?? inner?.clanId ?? raw?.data?.id ?? null;
      const parsed = commParseNumber(candidateId);
      if (Number.isFinite(parsed)) commKorrelApiLastClanId = Math.trunc(parsed);
      commSetKorrelLog(`${label} OK (status ${response.status}).`, { success: true });
    } else {
      commSetKorrelLog(`${label} failed: ${response?.error || 'API error'}`, { error: true });
    }
  } catch (err) {
    commSetKorrelLog(err?.message || `${label} failed.`, { error: true });
    commSetKorrelDetailActions([]);
  }
}

function commGetKorrelClanId() {
  const parsed = commParseNumber(commKorrelApiLastClanId ?? commSession?.clanId);
  return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
}

async function commRunApiGetMy() {
  await commRunKorrelApiTest('clan/get-my', 'https://api.gomining.com/api/nft-game/clan/get-my', {});
}

async function commRunApiGetById() {
  const endpoint = 'https://api.gomining.com/api/nft-game/clan/get-by-id';
  const clanId = commReadApiNumber(endpoint, 'clanId', commGetKorrelClanId());
  if (!clanId) {
    commSetKorrelLog('No clan id available. Run "clan/get-my" first.', { error: true });
    return;
  }
  const limit = commReadApiNumber(endpoint, 'pagination.limit', 25);
  const skip = commReadApiNumber(endpoint, 'pagination.skip', 0);
  const count = commReadApiNumber(endpoint, 'pagination.count', 0);
  const filterType = commReadApiText(endpoint, 'filters.filterType', 'none');
  const sortType = commReadApiText(endpoint, 'sort.sortType', 'none');
  const pagination = { limit, skip };
  if (Number.isFinite(count) && count > 0) pagination.count = count;
  const payload = {
    clanId,
    pagination,
    filters: { filterType },
    sort: { sortType }
  };
  await commRunKorrelApiTest('clan/get-by-id', endpoint, payload);
}

async function commRunApiRewardsByUser() {
  const endpoint = 'https://api.gomining.com/api/nft-game/rewards-by-user';
  const filterType = commReadApiText(endpoint, 'filters.type', commReadApiText(endpoint, 'filters.filterType', 'all'));
  const filterCycleId = commReadApiNumber(endpoint, 'filters.cycleId', null);
  const filterLeagueId = commReadApiNumber(endpoint, 'filters.leagueId', null);
  const filterRoundId = commReadApiNumber(endpoint, 'filters.roundId', null);
  const filterUserId = commReadApiNumber(endpoint, 'filters.userId', null);
  const limit = commReadApiNumber(endpoint, 'pagination.limit', 25);
  const skip = commReadApiNumber(endpoint, 'pagination.skip', 0);
  const count = commReadApiNumber(endpoint, 'pagination.count', 0);
  const pagination = { limit, skip };
  if (Number.isFinite(count) && count > 0) pagination.count = count;
  const filters = { type: filterType };
  if (Number.isFinite(filterCycleId)) filters.cycleId = Math.trunc(filterCycleId);
  if (Number.isFinite(filterLeagueId)) filters.leagueId = Math.trunc(filterLeagueId);
  if (Number.isFinite(filterRoundId)) filters.roundId = Math.trunc(filterRoundId);
  if (Number.isFinite(filterUserId)) filters.userId = Math.trunc(filterUserId);
  const payload = { filters, pagination };
  await commRunKorrelApiTest('rewards-by-user', endpoint, payload);
}

async function commRunApiRoundFindByCycle() {
  const endpoint = 'https://api.gomining.com/api/nft-game/round/find-by-cycleId';
  const cycleId = commReadApiNumber(endpoint, 'cycleId', getCycleNumber(new Date()));
  const leagueId = commReadApiNumber(endpoint, 'leagueId', null);
  const multipliers = commReadApiNumberList(endpoint, 'multipliers', COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS);
  const limit = commReadApiNumber(endpoint, 'pagination.limit', 10);
  const skip = commReadApiNumber(endpoint, 'pagination.skip', 0);
  const count = commReadApiNumber(endpoint, 'pagination.count', 0);
  const payload = {
    cycleId,
    multipliers,
    pagination: { limit, skip, count }
  };
  if (Number.isFinite(leagueId)) {
    payload.leagueId = leagueId;
  }
  await commRunKorrelApiTest('round/find-by-cycleId', endpoint, payload);
}

async function commRunApiRoundGetLast() {
  await commRunKorrelApiTest(
    'round/get-last',
    'https://api.gomining.com/api/nft-game/round/get-last',
    {},
    { method: 'GET' }
  );
}

async function commRunApiRoundGetState() {
  const endpoint = 'https://api.gomining.com/api/nft-game/round/get-state';
  const type = commReadApiText(endpoint, 'type', 'current');
  const leagueId = commReadApiNumber(endpoint, 'leagueId', null);
  const payload = { type };
  if (Number.isFinite(leagueId)) {
    payload.leagueId = leagueId;
  }
  await commRunKorrelApiTest('round/get-state', endpoint, payload);
}

function commExtractAllClansState(response) {
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  const clans =
    inner?.allClansState ||
    inner?.all_clans_state ||
    inner?.allClans ||
    inner?.clans ||
    inner?.data?.allClansState ||
    inner?.data?.all_clans_state ||
    [];
  return Array.isArray(clans) ? clans : [];
}

function commExtractClanRosterResponse(response) {
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  if (!inner || typeof inner !== 'object') {
    return { clan: null, users: [], totalCount: null };
  }
  const clan =
    inner?.clan ||
    inner?.clanData ||
    inner?.clanInfo ||
    inner?.data?.clan ||
    inner?.data?.clanData ||
    inner?.data?.clanInfo ||
    null;
  const users =
    inner?.usersForClient ||
    inner?.users ||
    inner?.members ||
    inner?.clanUsers ||
    inner?.clanMembers ||
    inner?.data?.usersForClient ||
    inner?.data?.users ||
    inner?.data?.members ||
    [];
  const pagination = inner?.pagination || inner?.data?.pagination || null;
  const looksLikeClan =
    inner &&
    (inner?.id ||
      inner?.clanId ||
      inner?.name ||
      inner?.clanName ||
      inner?.power ||
      inner?.clanPower ||
      inner?.usersCount ||
      inner?.membersCount ||
      inner?.blocksMined ||
      inner?.clanRank);
  const finalClan = clan || (looksLikeClan ? inner : null);
  const totalCount = commParseNumber(
    pagination?.count ??
    pagination?.total ??
    pagination?.totalCount ??
    finalClan?.usersCount ??
    finalClan?.membersCount
  );
  return {
    clan: finalClan,
    users: Array.isArray(users) ? users : [],
    totalCount: Number.isFinite(totalCount) ? totalCount : null
  };
}

function commBuildRosterClanRow({
  cycleId,
  leagueId,
  clanId,
  clanData,
  clanInfo,
  usersCountOverride,
  fetchedAt
}) {
  const clanName =
    clanData?.name ??
    clanData?.clanName ??
    clanData?.title ??
    clanInfo?.clanName ??
    clanInfo?.name ??
    null;
  const clanPower = commParseNumber(
    clanData?.power ??
    clanData?.clanPower ??
    clanInfo?.clanPower ??
    clanInfo?.clan_power ??
    clanInfo?.power
  );
  const usersCount = commParseNumber(
    usersCountOverride ??
    clanData?.usersCount ??
    clanData?.membersCount ??
    clanInfo?.usersCount ??
    clanInfo?.membersCount
  );
  return {
    cycle_id: cycleId,
    league_id: leagueId,
    clan_id: clanId,
    clan_name: clanName,
    clan_power: Number.isFinite(clanPower) ? clanPower : null,
    users_count: Number.isFinite(usersCount) ? usersCount : null,
    fetched_at: fetchedAt || new Date().toISOString(),
    raw: clanData || null
  };
}

function commFormatRosterUser(member) {
  if (!member || typeof member !== 'object') return 'unknown';
  const idRaw = member?.id ?? member?.userId ?? member?.user?.id ?? member?.user_id;
  const idParsed = commParseNumber(idRaw);
  const id = Number.isFinite(idParsed) ? Math.trunc(idParsed) : '?';
  const alias =
    member?.alias ??
    member?.userAlias ??
    member?.user_name ??
    member?.user?.alias ??
    member?.user?.name ??
    member?.name ??
    '-';
  const powerRaw = member?.power ?? member?.userPower ?? member?.user?.power ?? member?.user_power;
  const powerParsed = commParseNumber(powerRaw);
  const power = Number.isFinite(powerParsed) ? powerParsed : null;
  const abilities = Array.isArray(member?.usedNftGameAbilities)
    ? member.usedNftGameAbilities.length
    : Array.isArray(member?.usedAbilities)
      ? member.usedAbilities.length
      : 0;
  const owner = member?.isOwner ? 'owner' : '';
  const parts = [`${id}`, alias];
  if (Number.isFinite(power)) parts.push(`p:${power}`);
  if (abilities) parts.push(`boosts:${abilities}`);
  if (owner) parts.push(owner);
  return parts.join(' ');
}

function commDeduplicateRosterMembers(rows) {
  if (!Array.isArray(rows) || rows.length === 0) return [];
  const seen = new Set();
  const unique = [];
  for (const row of rows) {
    if (!row) continue;
    const key = `${row.cycle_id}:${row.league_id}:${row.clan_id}:${row.user_id}`;
    if (seen.has(key)) continue;
    seen.add(key);
    unique.push(row);
  }
  return unique;
}

async function commFetchExistingRosterClanKeys(cycleId, leagueId) {
  if (!Number.isFinite(cycleId)) return new Set();
  const queryParts = [
    'select=clan_id,league_id',
    `cycle_id=eq.${encodeURIComponent(cycleId)}`,
    `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`
  ];
  if (Number.isFinite(leagueId)) {
    queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`);
  }
  const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, queryParts.join('&'));
  const set = new Set();
  (Array.isArray(rows) ? rows : []).forEach((row) => {
    const clanIdParsed = commParseNumber(row?.clan_id);
    if (!Number.isFinite(clanIdParsed)) return;
    const leagueParsed = commParseNumber(row?.league_id);
    const leagueKey = Number.isFinite(leagueParsed) ? Math.trunc(leagueParsed) : 'x';
    set.add(`${leagueKey}:${Math.trunc(clanIdParsed)}`);
  });
  return set;
}

async function commFetchMissingRosterClans(cycleId, limit) {
  if (!Number.isFinite(cycleId)) return [];
  const queryParts = [
    'select=clan_id,league_id,clan_name,clan_power,users_count',
    `cycle_id=eq.${encodeURIComponent(cycleId)}`,
    'or=(clan_name.is.null,clan_power.is.null)',
    `limit=${encodeURIComponent(limit)}`
  ];
  if (Number.isFinite(commKorrelApiLeagueOverride)) {
    queryParts.unshift(`league_id=eq.${encodeURIComponent(commKorrelApiLeagueOverride)}`);
  }
  const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, queryParts.join('&'));
  return Array.isArray(rows) ? rows : [];
}

function commBuildRosterSignature(clans) {
  if (!Array.isArray(clans) || clans.length === 0) return '0';
  const first = clans[0] || {};
  const last = clans[clans.length - 1] || {};
  const firstKey = `${first?.leagueId ?? 'x'}-${first?.clanId ?? 'x'}`;
  const lastKey = `${last?.leagueId ?? 'x'}-${last?.clanId ?? 'x'}`;
  return `${clans.length}:${firstKey}:${lastKey}`;
}

async function commFetchRosterTargetsFromDb(options = {}) {
  const cycleOverride = Number.isFinite(options?.cycleIdOverride)
    ? Math.trunc(options.cycleIdOverride)
    : null;
  const leagueOverride = Number.isFinite(options?.leagueIdOverride)
    ? Math.trunc(options.leagueIdOverride)
    : null;
  const ignoreOverride = !!options?.ignoreOverride;
  const queryParts = [
    'select=winner_clan_id,winner_user_id,clan_winner,user_winner,league_id,block_number,cycle_id',
    'order=block_number.desc',
    `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`
  ];
  if (Number.isFinite(cycleOverride)) {
    queryParts.unshift(`cycle_id=eq.${cycleOverride}`);
  } else if (!ignoreOverride && Number.isFinite(commKorrelApiCycleOverride)) {
    queryParts.unshift(`cycle_id=eq.${commKorrelApiCycleOverride}`);
  }
  if (Number.isFinite(leagueOverride)) {
    queryParts.unshift(`league_id=eq.${leagueOverride}`);
  } else if (!ignoreOverride && Number.isFinite(commKorrelApiLeagueOverride)) {
    queryParts.unshift(`league_id=eq.${commKorrelApiLeagueOverride}`);
  }
  const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
  const clanMap = new Map();
  const userSet = new Set();
  const leagueSet = new Set();
  (Array.isArray(rows) ? rows : []).forEach((row) => {
    const leagueRaw = commParseNumber(row?.league_id ?? commKorrelApiLeagueOverride);
    const leagueId = Number.isFinite(leagueRaw) ? Math.trunc(leagueRaw) : null;
    if (Number.isFinite(leagueId)) leagueSet.add(leagueId);
    let clanId = commParseNumber(row?.winner_clan_id);
    if (!Number.isFinite(clanId)) {
      const clanInfo = extractRoundWinnerClan(row);
      clanId = commParseNumber(clanInfo?.id);
    }
    if (Number.isFinite(clanId)) {
      const key = `${Number.isFinite(leagueId) ? leagueId : 'x'}:${Math.trunc(clanId)}`;
      if (!clanMap.has(key)) {
        clanMap.set(key, {
          clanId: Math.trunc(clanId),
          leagueId: Number.isFinite(leagueId) ? Math.trunc(leagueId) : null
        });
      }
    }
    let userId = commParseNumber(row?.winner_user_id);
    if (!Number.isFinite(userId)) {
      const userInfo = extractRoundWinnerUser(row);
      userId = commParseNumber(userInfo?.id);
    }
    if (Number.isFinite(userId)) {
      userSet.add(Math.trunc(userId));
    }
  });
  return {
    clans: Array.from(clanMap.values()),
    userCount: userSet.size,
    leagueCount: leagueSet.size
  };
}

async function commFetchLeagueIdsFromDb(cycleId) {
  try {
    const queryParts = [
      'select=league_id',
      'order=league_id.asc',
      `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`
    ];
    if (Number.isFinite(cycleId)) {
      queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`);
    }
    const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
    const ids = Array.isArray(rows)
      ? rows.map((row) => commParseNumber(row?.league_id)).filter((val) => Number.isFinite(val))
      : [];
    const unique = Array.from(new Set(ids)).sort((a, b) => a - b);
    return unique;
  } catch (err) {
    return [];
  }
}

async function commGetNextRosterLeagueId(currentLeague, cycleId) {
  const ids = await commFetchLeagueIdsFromDb(cycleId);
  const merged = Array.from(new Set([...(ids || []), ...COMM_KNOWN_LEAGUE_IDS]));
  if (!merged.length) return null;
  const sorted = merged.slice().sort((a, b) => a - b);
  const current = commParseNumber(currentLeague);
  if (!Number.isFinite(current)) return sorted[0];
  const currentIdx = sorted.indexOf(Math.trunc(current));
  if (currentIdx === -1) return sorted[0];
  if (currentIdx < sorted.length - 1) return sorted[currentIdx + 1];
  return null;
}

async function commFetchLeagueClans(tabId, leagueId) {
  const endpoint = 'https://api.gomining.com/api/nft-game/round/get-state';
  const throttleMs = commShouldThrottleKorrelApi(endpoint);
  if (throttleMs) {
    await commDelay(throttleMs);
  }
  const payload = { type: 'current' };
  if (Number.isFinite(leagueId)) {
    payload.leagueId = leagueId;
  }
  const response = await commFetchGominingApiViaInjection(tabId, endpoint, payload);
  commKorrelApiLastCallAt.set(endpoint, Date.now());
  if (!response?.ok) {
    const status = response?.status ?? '?';
    const errorMsg = response?.error || (typeof response?.data === 'string' ? response.data : response?.data?.message || response?.data?.error);
    const authNote = response?.meta?.tokenFound ? 'token ok' : 'token missing';
    const authHeader = response?.meta?.usedAuthHeader ? 'auth header' : 'no auth header';
    commSetKorrelLog(
      `Roster: league ${leagueId} get-state failed (status ${status}). ${errorMsg || 'API error'}. ${authHeader}, ${authNote}.`,
      { error: true }
    );
    return [];
  }
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  const stateLeagueId = commParseNumber(inner?.leagueId ?? inner?.league_id);
  if (
    Number.isFinite(leagueId) &&
    Number.isFinite(stateLeagueId) &&
    stateLeagueId !== leagueId
  ) {
    commSetKorrelLog(`Roster: league ${leagueId} mismatch (state league ${stateLeagueId}).`, { error: true });
    return [];
  }
  const clans = commExtractAllClansState(response);
  if (!clans.length) {
    let keySummary = '-';
    if (Array.isArray(inner)) {
      keySummary = `array (${inner.length})`;
    } else if (inner && typeof inner === 'object') {
      const keys = Object.keys(inner);
      keySummary = keys.length ? keys.join(', ') : 'object {}';
    }
    commSetKorrelLog(`Roster: league ${leagueId} get-state returned 0 clans. Data keys: ${keySummary}.`, { error: true });
  }
  return clans;
}

async function commFetchClanRoster(tabId, leagueId, clanInfo, cycleId) {
  const endpoint = 'https://api.gomining.com/api/nft-game/clan/get-by-id';
  const clanIdRaw = clanInfo?.clanId ?? clanInfo?.clan_id ?? clanInfo?.id;
  const clanIdParsed = commParseNumber(clanIdRaw);
  const clanId = Number.isFinite(clanIdParsed) ? Math.trunc(clanIdParsed) : null;
  if (!clanId) {
    throw new Error('Missing clan id.');
  }
  const limitRaw = commGetKorrelRosterMemberLimit();
  const limit = Math.min(25, Math.max(1, Math.floor(limitRaw)));
  let skip = 0;
  let totalCount = null;
  let clanData = null;
  const members = [];
  const buildErrorMessage = (response) => {
    const status = response?.status ?? '?';
    const errorMsg = response?.error || (typeof response?.data === 'string'
      ? response.data
      : response?.data?.message || response?.data?.error);
    const detailText = response?.data && response?.data !== errorMsg
      ? commCompactJson(response.data, 200)
      : '';
    const authNote = response?.meta?.tokenFound ? 'token ok' : 'token missing';
    const authHeader = response?.meta?.usedAuthHeader ? 'auth header' : 'no auth header';
    const detailSuffix = detailText && detailText !== errorMsg ? ` | ${detailText}` : '';
    return `Status ${status}: ${errorMsg || 'API error'}${detailSuffix} (${authHeader}, ${authNote})`;
  };
  const shouldRetry = (status) => [429, 500, 502, 503, 504].includes(Number(status));
  for (let page = 0; page < 200; page += 1) {
    const throttleMs = commShouldThrottleRosterApi(endpoint);
    if (throttleMs) {
      const seconds = Math.ceil(throttleMs / 1000);
      const status = commKorrelRosterStatus;
      const progressNote = status
        ? ` Progress ${status.currentIndex ?? status.processed}/${status.total}, stored ${status.storedClans} clans, ${status.storedMembers} members.`
        : '';
      commSetKorrelLog(
        `Roster batch: waiting ${seconds}s (roster rate limit).${progressNote}`,
        { append: true }
      );
      await commDelay(throttleMs);
    }
    const payload = {
      clanId,
      pagination: { limit, skip },
      filters: { filterType: 'none' },
      sort: { sortType: 'none' }
    };
    if (commKorrelRosterVerbose) {
      commSetKorrelLog(
        `Roster API request: clan ${clanId} page ${page + 1} ${commCompactJson(payload, 220)}`,
        { append: true }
      );
    }
    let response = await commFetchGominingApiViaInjection(tabId, endpoint, payload, { cycleId, leagueId });
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    if (!response?.ok && shouldRetry(response?.status)) {
      const delayMs = 5000 + Math.floor(Math.random() * 4000);
      await commDelay(delayMs);
      response = await commFetchGominingApiViaInjection(tabId, endpoint, payload, { cycleId, leagueId });
      commKorrelApiLastCallAt.set(endpoint, Date.now());
    }
    if (!response?.ok && Number(response?.status) === 400) {
      const fallbackLimit = Math.min(limit, 25);
      commSetKorrelLog(
        `Roster batch: clan ${clanId} got 400. Retrying without filters/sort (limit ${fallbackLimit}).`,
        { append: true }
      );
      await commDelay(800 + Math.floor(Math.random() * 700));
      const fallbackPayload = {
        clanId,
        pagination: { limit: fallbackLimit, skip }
      };
      response = await commFetchGominingApiViaInjection(tabId, endpoint, fallbackPayload, { cycleId, leagueId });
      commKorrelApiLastCallAt.set(endpoint, Date.now());
      if (!response?.ok && Number(response?.status) === 400 && fallbackLimit > 10) {
        commSetKorrelLog(
          `Roster batch: clan ${clanId} still 400. Retrying with limit 10.`,
          { append: true }
        );
        await commDelay(800 + Math.floor(Math.random() * 700));
        const strictPayload = {
          clanId,
          pagination: { limit: 10, skip }
        };
        response = await commFetchGominingApiViaInjection(tabId, endpoint, strictPayload, { cycleId, leagueId });
        commKorrelApiLastCallAt.set(endpoint, Date.now());
      }
      if (!response?.ok && Number(response?.status) === 400 && Number.isFinite(leagueId)) {
        commSetKorrelLog(
          `Roster batch: clan ${clanId} still 400. Retrying with leagueId ${leagueId}.`,
          { append: true }
        );
        await commDelay(800 + Math.floor(Math.random() * 700));
        const leaguePayload = {
          clanId,
          leagueId,
          pagination: { limit: 10, skip }
        };
        response = await commFetchGominingApiViaInjection(tabId, endpoint, leaguePayload, { cycleId, leagueId });
        commKorrelApiLastCallAt.set(endpoint, Date.now());
      }
    }
    if (!response?.ok) {
      throw new Error(buildErrorMessage(response));
    }
    const parsed = commExtractClanRosterResponse(response);
    if (commKorrelRosterVerbose) {
      const raw = response?.data;
      const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
      const keySummary =
        inner && typeof inner === 'object' && !Array.isArray(inner)
          ? Object.keys(inner).join(', ')
          : Array.isArray(inner)
            ? `array (${inner.length})`
            : '-';
      const clanName =
        parsed?.clan?.name ??
        parsed?.clan?.clanName ??
        parsed?.clan?.title ??
        '-';
      commSetKorrelLog(
        `Roster API response: clan ${clanId} page ${page + 1} users ${parsed?.users?.length ?? 0}, total ${parsed?.totalCount ?? 'n/a'}, name ${clanName}. Keys: ${keySummary}`,
        { append: true }
      );
      const users = Array.isArray(parsed?.users) ? parsed.users : [];
      const chunkSize = 6;
      for (let i = 0; i < users.length; i += chunkSize) {
        const slice = users.slice(i, i + chunkSize).map(commFormatRosterUser).join(' | ');
        commSetKorrelLog(
          `Roster API users clan ${clanId} page ${page + 1}: ${slice}`,
          { append: true }
        );
      }
    }
    if (!clanData && parsed?.clan) clanData = parsed.clan;
    if (Number.isFinite(parsed?.totalCount)) totalCount = parsed.totalCount;
    const users = Array.isArray(parsed?.users) ? parsed.users : [];
    if (users.length) members.push(...users);
    if (!users.length || users.length < limit) break;
    skip += limit;
    if (Number.isFinite(totalCount) && skip >= totalCount) break;
    const delayMs = 1000 + Math.floor(Math.random() * 1500);
    await commDelay(delayMs);
  }
  const fetchedAt = new Date().toISOString();
  const usersCount = commParseNumber(totalCount ?? members.length);
  const clanRow = commBuildRosterClanRow({
    cycleId,
    leagueId,
    clanId,
    clanData,
    clanInfo,
    usersCountOverride: usersCount,
    fetchedAt
  });
  const memberRows = members
    .map((member) => {
      const userIdRaw = member?.userId ?? member?.id ?? member?.user?.id;
      const userIdParsed = commParseNumber(userIdRaw);
      const userId = Number.isFinite(userIdParsed) ? Math.trunc(userIdParsed) : null;
      if (!userId) return null;
      const userName =
        member?.userAlias ??
        member?.alias ??
        member?.userName ??
        member?.username ??
        member?.name ??
        member?.user?.alias ??
        member?.user?.name ??
        null;
      const userPower = commParseNumber(
        member?.userPower ??
        member?.power ??
        member?.user?.power ??
        member?.user_power
      );
      return {
        cycle_id: cycleId,
        league_id: leagueId,
        clan_id: clanId,
        user_id: userId,
        user_name: userName,
        user_power: Number.isFinite(userPower) ? userPower : null,
        fetched_at: fetchedAt,
        raw: member || null
      };
    })
    .filter(Boolean);
  const dedupedMemberRows = commDeduplicateRosterMembers(memberRows);
  if (commKorrelRosterVerbose && dedupedMemberRows.length !== memberRows.length) {
    commSetKorrelLog(
      `Roster batch: clan ${clanId} deduped members ${memberRows.length} -> ${dedupedMemberRows.length}.`,
      { append: true }
    );
  }
  return { clanRow, memberRows: dedupedMemberRows, clanId, usersCount: usersCount ?? null };
}

async function commUpsertClanRosterRows(clanRow, memberRows) {
  if (clanRow) {
    try {
      await commInsert(COMM_TABLE_CLAN_ROSTER_CLANS, [clanRow], {
        upsert: true,
        onConflict: 'cycle_id,league_id,clan_id'
      });
    } catch (err) {
      const msg = err?.message || String(err);
      if (msg.includes('42P10') || msg.includes('ON CONFLICT')) {
        await commInsert(COMM_TABLE_CLAN_ROSTER_CLANS, [clanRow]);
      } else {
        throw err;
      }
    }
  }
  if (!Array.isArray(memberRows) || !memberRows.length) return;
  const uniqueRows = commDeduplicateRosterMembers(memberRows);
  if (commKorrelRosterVerbose && uniqueRows.length !== memberRows.length) {
    commSetKorrelLog(
      `Roster batch: deduped member rows ${memberRows.length} -> ${uniqueRows.length} before insert.`,
      { append: true }
    );
  }
  const batches = commChunkArray(uniqueRows, 250);
  for (const batch of batches) {
    try {
      await commInsert(COMM_TABLE_CLAN_ROSTER_MEMBERS, batch, {
        upsert: true,
        onConflict: 'cycle_id,league_id,clan_id,user_id'
      });
    } catch (err) {
      const msg = err?.message || String(err);
      if (msg.includes('42P10') || msg.includes('ON CONFLICT')) {
        await commInsert(COMM_TABLE_CLAN_ROSTER_MEMBERS, batch);
      } else {
        throw err;
      }
    }
  }
}

async function commRunKorrelClanRosterBatch(options = {}) {
  const { auto = false, cycleIdOverride = null, leagueIdOverride = null, fullRoster = false } = options;
  if (commKorrelRosterRunning) {
    commSetKorrelLog('Roster batch already running.', { error: true });
    return false;
  }
  const tab = await commFindMiningWarsTabWithAuth();
  if (!tab?.id) {
    commSetKorrelLog('Roster batch skipped: no Mining Wars tab with auth token found.', { error: true });
    return false;
  }
  commKorrelRosterRunning = true;
  const currentCycle = getCycleNumber(new Date());
  const cycleOverride = Number.isFinite(cycleIdOverride)
    ? Math.trunc(cycleIdOverride)
    : null;
  const manualCycleOverride = Number.isFinite(commKorrelApiCycleOverride)
    ? Math.trunc(commKorrelApiCycleOverride)
    : null;
  const cycleId = Number.isFinite(cycleOverride)
    ? cycleOverride
    : (auto
      ? (Number.isFinite(manualCycleOverride) && manualCycleOverride === currentCycle ? manualCycleOverride : currentCycle)
      : (Number.isFinite(manualCycleOverride) ? manualCycleOverride : currentCycle));
  const activeLeagueOverride = Number.isFinite(leagueIdOverride)
    ? Math.trunc(leagueIdOverride)
    : (auto
      ? commGetKorrelActiveLeagueId()
      : (Number.isFinite(commKorrelApiLeagueOverride) ? Math.trunc(commKorrelApiLeagueOverride) : null));
  const batchSize = fullRoster ? 1 : commGetKorrelRosterBatchSize();
  let leagueOverride = activeLeagueOverride;
  let autoNextLeagueId = null;
  const prevLogContext = commKorrelLogContextOverride;
  try {
    let hadErrors = false;
    let storedClans = 0;
    let storedMembers = 0;
    let processed = 0;
    let scanned = 0;
    let existingRosterKeys = new Set();
    let lastClanAt = null;
    let clans = [];
    const advanceToNextLeague = async (note, { error = false } = {}) => {
      if (!Number.isFinite(leagueOverride)) return false;
      const currentLeague = leagueOverride;
      const nextLeague = await commGetNextRosterLeagueId(currentLeague, cycleId);
      const summary = storedClans
        ? `Stored ${storedClans} clans.`
        : 'No clans stored.';
      if (!Number.isFinite(nextLeague)) {
        commSetKorrelLog(
          `Roster batch complete for all leagues. ${summary}`.trim(),
          { append: true, error: !storedClans || hadErrors }
        );
        if (!auto && Number.isFinite(commKorrelApiLeagueOverride)) {
          commKorrelApiLeagueOverride = null;
          commSaveKorrelApiSettings();
          commUpdateKorrelApiSettingsUI();
        }
        leagueOverride = null;
        return false;
      }
      if (clans.length) {
        const storedTotal = Math.min(clans.length, (existingRosterKeys.size || 0) + storedClans);
        const missingCount = Math.max(0, clans.length - storedTotal);
        commPersistRosterCompletionState({
          cycleId,
          leagueId: currentLeague,
          totalTargets: clans.length,
          storedClans: storedTotal,
          storedMembers,
          missingCount,
          apiCalls: processed,
          fullRunAt: fullRoster ? Date.now() : undefined,
          lastClanAt
        }).catch(() => {});
      }
      const reasonText = note ? ` ${note}` : '';
      commSetKorrelLog(
        `Roster batch: league ${currentLeague}${reasonText}. ${summary} Continuing with league ${nextLeague}.`,
        { append: true, error: error || !storedClans || hadErrors }
      );
      if (!auto && Number.isFinite(commKorrelApiLeagueOverride)) {
        commKorrelApiLeagueOverride = nextLeague;
        commSaveKorrelApiSettings();
        commUpdateKorrelApiSettingsUI();
      }
      leagueOverride = nextLeague;
      commKorrelLogContextOverride = { cycleId, leagueId: leagueOverride };
      autoNextLeagueId = nextLeague;
      return true;
    };
    if (auto && !Number.isFinite(leagueOverride)) {
      const fallbackLeague = await commGetNextRosterLeagueId(null, cycleId);
      if (Number.isFinite(fallbackLeague)) {
        leagueOverride = fallbackLeague;
      }
    }
    commKorrelLogContextOverride = { cycleId, leagueId: Number.isFinite(leagueOverride) ? leagueOverride : commGetKorrelActiveLeagueId() };
    const targets = await commFetchRosterTargetsFromDb({
      cycleIdOverride: cycleId,
      leagueIdOverride: leagueOverride
    });
    clans = Array.isArray(targets?.clans) ? targets.clans : [];
    const rosterSignature = commBuildRosterSignature(clans);
    if (!clans.length) {
      commResetKorrelRosterProgress({ log: false });
      const hadLeagueOverride = !auto && Number.isFinite(leagueOverride);
      const advanced = await advanceToNextLeague('has no clans in rounds_cycle_api', { error: true });
      if (advanced) return true;
      if (hadLeagueOverride) return false;
      commSetKorrelLog('Roster batch failed: no clans found in rounds_cycle_api.', { error: true });
      return false;
    }
    let progress = commLoadKorrelRosterProgress();
    if (!progress || progress.cycleId !== cycleId || progress.signature !== rosterSignature) {
      progress = { cycleId, signature: rosterSignature, index: 0 };
    }
    if (!Number.isFinite(progress.index) || progress.index < 0 || progress.index > clans.length) {
      progress.index = 0;
    }
    if (Number.isFinite(leagueOverride) && !auto) {
      progress = { cycleId, signature: rosterSignature, index: 0 };
      commSetKorrelLog(`Roster batch: using league override ${leagueOverride}.`, { append: true });
    }
    commKorrelRosterStatus = {
      processed,
      total: clans.length,
      storedClans,
      storedMembers,
      currentIndex: null,
      currentClanId: null
    };
    try {
      existingRosterKeys = await commFetchExistingRosterClanKeys(cycleId, leagueOverride);
      commSetKorrelLog(`Roster batch: existing clans in DB ${existingRosterKeys.size}.`, { append: true });
    } catch (err) {
      commSetKorrelLog('Roster batch: could not read existing roster clans (skip disabled).', { error: true, append: true });
    }
    const startIndex = progress.index;
    let stoppedEarly = false;
    commSetKorrelLog(
      `Roster batch start: cycle ${cycleId}, target ${batchSize} clans${fullRoster ? ' (full)' : ''}.`,
      { append: true }
    );
    commSetKorrelLog(`Roster batch: using tab ${tab.id}: ${tab.url || 'unknown url'}.`, { append: true });
    commSetKorrelLog(`Roster source: rounds_cycle_api (limit ${COMM_KORREL_ROSTER_DB_LIMIT}).`, { append: true });
    commSetKorrelLog(
      `Roster batch resume at clan ${progress.index + 1}/${clans.length}.`,
      { append: true }
    );
    if (Number.isFinite(targets?.leagueCount)) {
      commSetKorrelLog(
        `Roster targets from DB: ${clans.length} clans, ${targets.userCount} users, ${targets.leagueCount} leagues.`,
        { append: true }
      );
    } else {
      commSetKorrelLog(
        `Roster targets from DB: ${clans.length} clans, ${targets.userCount} users.`,
        { append: true }
      );
    }
    if (commKorrelRosterVerbose) {
      const ids = clans
        .map((clan) => clan?.clanId)
        .filter((id) => id !== null && id !== undefined)
        .map((id) => String(id));
      const chunkSize = 12;
      for (let i = 0; i < ids.length; i += chunkSize) {
        commSetKorrelLog(
          `Roster target IDs: ${ids.slice(i, i + chunkSize).join(', ')}`,
          { append: true }
        );
      }
    }
    for (let ci = progress.index; ci < clans.length; ci += 1) {
      if (processed >= batchSize) {
        progress.index = ci;
        stoppedEarly = true;
        break;
      }
      scanned += 1;
      const clanInfo = clans[ci];
      const clanIdRaw = clanInfo?.clanId ?? '?';
      const clanIdParsed = commParseNumber(clanIdRaw);
      const clanId = Number.isFinite(clanIdParsed) ? Math.trunc(clanIdParsed) : clanIdRaw;
      const leagueId = clanInfo?.leagueId ?? null;
      const leagueKey = Number.isFinite(commParseNumber(leagueId)) ? Math.trunc(commParseNumber(leagueId)) : 'x';
      const rosterKey = Number.isFinite(clanIdParsed) ? `${leagueKey}:${clanId}` : null;
      if (commKorrelRosterVerbose) {
        commSetKorrelLog(
          `Roster target ${ci + 1}/${clans.length}: ${commCompactJson(clanInfo, 200)}`,
          { append: true }
        );
      }
      if (!fullRoster && rosterKey && existingRosterKeys.has(rosterKey)) {
        if (commKorrelRosterVerbose) {
          commSetKorrelLog(
            `Roster batch: clan ${clanId} (league ${leagueId ?? 'n/a'}) skipped (already stored).`,
            { append: true }
          );
        }
        continue;
      }
      if (commKorrelRosterStatus) {
        commKorrelRosterStatus.currentIndex = ci + 1;
        commKorrelRosterStatus.currentClanId = clanId;
        commKorrelRosterStatus.processed = processed;
      }
      commSetKorrelLog(
        `Roster batch: clan ${clanId} (league ${leagueId ?? 'n/a'}) ${ci + 1}/${clans.length}...`,
        { append: true }
      );
      try {
        const { clanRow, memberRows, usersCount } = await commFetchClanRoster(
          tab.id,
          leagueId,
          clanInfo,
          cycleId
        );
        await commUpsertClanRosterRows(clanRow, memberRows);
        storedClans += 1;
        storedMembers += memberRows.length;
        if (commKorrelRosterStatus) {
          commKorrelRosterStatus.storedClans = storedClans;
          commKorrelRosterStatus.storedMembers = storedMembers;
        }
        lastClanAt = Date.now();
        commSetKorrelLog(
          `Roster batch: stored clan ${clanRow?.clan_id} (${memberRows.length} members, users ${usersCount ?? 'n/a'}).`,
          { append: true, success: true }
        );
      } catch (err) {
        hadErrors = true;
        commSetKorrelLog(
          `Roster batch: clan ${clanId} failed: ${err?.message || 'API error'}.`,
          { error: true, append: true }
        );
      }
      processed += 1;
      const delayMs = fullRoster
        ? 0
        : (2000 + Math.floor(Math.random() * 4000));
      await commDelay(delayMs);
    }
    if (!stoppedEarly) {
      progress.index = Math.min(startIndex + scanned, clans.length);
    }
    if (progress.index >= clans.length || processed < batchSize) {
      commResetKorrelRosterProgress({ log: false });
      const hadLeagueOverride = !auto && Number.isFinite(leagueOverride);
      const advanced = await advanceToNextLeague('complete');
      if (!advanced && !hadLeagueOverride) {
        if (!storedClans) {
          commSetKorrelLog(
            'Roster batch finished: no clans stored. Check your API session or rounds_cycle_api data.',
            { append: true, error: true }
          );
        } else if (hadErrors) {
          commSetKorrelLog(
            `Roster batch complete with errors. Stored ${storedClans} clans.`,
            { append: true, error: true }
          );
        } else {
          commSetKorrelLog('Roster batch complete for all clans in rounds_cycle_api.', { append: true, success: true });
        }
        commMarkRosterAutoDone(cycleId);
      }
    } else {
      commKorrelRosterProgress = progress;
      commSaveKorrelRosterProgress(progress);
      commSetKorrelLog(
        `Roster batch paused at clan ${progress.index + 1}/${clans.length}.`,
        { append: true }
      );
      if (clans.length && Number.isFinite(leagueOverride)) {
        const storedTotal = Math.min(clans.length, (existingRosterKeys.size || 0) + storedClans);
        const missingCount = Math.max(0, clans.length - storedTotal);
        commPersistRosterCompletionState({
          cycleId,
          leagueId: leagueOverride,
          totalTargets: clans.length,
          storedClans: storedTotal,
          storedMembers,
          missingCount,
          apiCalls: processed,
          fullRunAt: fullRoster ? Date.now() : undefined,
          lastClanAt
        }).catch(() => {});
      }
      if (commKorrelRosterResumeTimer) {
        clearTimeout(commKorrelRosterResumeTimer);
      }
      commKorrelRosterResumeAt = 0;
      const resumeDelayMs = 15 * 60 * 1000;
      const resumeAt = new Date(Date.now() + resumeDelayMs);
      commKorrelRosterResumeAt = resumeAt.getTime();
      commSetKorrelLog(
        `Roster batch will resume in 15 minutes (${resumeAt.toLocaleTimeString([], { hour12: false })}).`,
        { append: true }
      );
      const resumeOptions = {
        auto,
        cycleIdOverride: cycleId,
        leagueIdOverride: Number.isFinite(leagueOverride) ? leagueOverride : null
      };
      commKorrelRosterResumeTimer = setTimeout(() => {
        commKorrelRosterResumeTimer = null;
        if (commKorrelRosterRunning || commKorrelRosterPatchRunning) return;
        commRunKorrelClanRosterBatch(resumeOptions);
      }, resumeDelayMs);
    }
    return true;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Roster batch failed.', { error: true });
    return false;
  } finally {
    commKorrelRosterRunning = false;
    commKorrelRosterStatus = null;
    commKorrelLogContextOverride = prevLogContext;
    if (autoNextLeagueId) {
      if (commKorrelRosterResumeTimer) {
        clearTimeout(commKorrelRosterResumeTimer);
        commKorrelRosterResumeTimer = null;
      }
      commKorrelRosterResumeAt = 0;
      const resumeOptions = {
        auto,
        cycleIdOverride: cycleId,
        leagueIdOverride: autoNextLeagueId
      };
      commKorrelRosterResumeAt = Date.now() + 2000;
      commKorrelRosterResumeTimer = setTimeout(() => {
        commKorrelRosterResumeTimer = null;
        if (commKorrelRosterRunning || commKorrelRosterPatchRunning) return;
        commRunKorrelClanRosterBatch(resumeOptions);
      }, 2000);
    }
  }
}

async function commRunKorrelClanRosterPatch() {
  if (!commSession || !commIsKorrel()) {
    commSetKorrelLog('Only korrel can run this action.', { error: true });
    return false;
  }
  if (commKorrelRosterPatchRunning) {
    commSetKorrelLog('Roster patch already running.', { error: true });
    return false;
  }
  if (commKorrelRosterRunning) {
    commSetKorrelLog('Roster batch is running. Wait for it to finish before patching.', { error: true });
    return false;
  }
  const tab = await commFindMiningWarsTabWithAuth();
  if (!tab?.id) {
    commSetKorrelLog('Roster patch skipped: no Mining Wars tab with auth token found.', { error: true });
    return false;
  }
  commKorrelRosterPatchRunning = true;
  const cycleId = Number.isFinite(commKorrelApiCycleOverride)
    ? commKorrelApiCycleOverride
    : getCycleNumber(new Date());
  const patchLimit = commGetKorrelRosterBatchSize();
  try {
    commSetKorrelLog(`Roster patch start: cycle ${cycleId}, limit ${patchLimit}.`, { append: true });
    commSetKorrelLog(`Roster patch: using tab ${tab.id}: ${tab.url || 'unknown url'}.`, { append: true });
    const missing = await commFetchMissingRosterClans(cycleId, patchLimit);
    if (!missing.length) {
      commSetKorrelLog('Roster patch: no clans missing name/power.', { append: true, success: true });
      return true;
    }
    commSetKorrelLog(`Roster patch: ${missing.length} clans missing fields.`, { append: true });
    for (let i = 0; i < missing.length; i += 1) {
      const row = missing[i];
      const clanIdRaw = row?.clan_id;
      const clanIdParsed = commParseNumber(clanIdRaw);
      const clanId = Number.isFinite(clanIdParsed) ? Math.trunc(clanIdParsed) : clanIdRaw;
      const leagueId = row?.league_id ?? null;
      const throttleMs = commShouldThrottleRosterApi('https://api.gomining.com/api/nft-game/clan/get-by-id');
      if (throttleMs) {
        const seconds = Math.ceil(throttleMs / 1000);
        commSetKorrelLog(`Roster patch: waiting ${seconds}s (roster rate limit).`, { append: true });
        await commDelay(throttleMs);
      }
      const payload = {
        clanId,
        pagination: { limit: 1, skip: 0 },
        filters: { filterType: 'none' },
        sort: { sortType: 'none' }
      };
      if (commKorrelRosterVerbose) {
        commSetKorrelLog(`Roster patch request: clan ${clanId} ${commCompactJson(payload, 200)}`, { append: true });
      }
      const response = await commFetchGominingApiViaInjection(
        tab.id,
        'https://api.gomining.com/api/nft-game/clan/get-by-id',
        payload
      );
      commKorrelApiLastCallAt.set('https://api.gomining.com/api/nft-game/clan/get-by-id', Date.now());
      if (!response?.ok) {
        commSetKorrelLog(`Roster patch: clan ${clanId} failed: ${response?.error || 'API error'}.`, { error: true, append: true });
        continue;
      }
      const parsed = commExtractClanRosterResponse(response);
      const clanData = parsed?.clan;
      const clanRow = commBuildRosterClanRow({
        cycleId,
        leagueId,
        clanId: Number.isFinite(clanIdParsed) ? clanId : null,
        clanData,
        clanInfo: { clanId, leagueId },
        usersCountOverride: parsed?.totalCount,
        fetchedAt: new Date().toISOString()
      });
      await commUpsertClanRosterRows(clanRow, []);
      commSetKorrelLog(
        `Roster patch: updated clan ${clanRow?.clan_id} (${clanRow?.clan_name || 'n/a'}, power ${clanRow?.clan_power ?? 'n/a'}).`,
        { append: true, success: true }
      );
      const delayMs = 2000 + Math.floor(Math.random() * 4000);
      await commDelay(delayMs);
    }
    return true;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Roster patch failed.', { error: true });
    return false;
  } finally {
    commKorrelRosterPatchRunning = false;
  }
}

function commExtractRoundCycleItems(response) {
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  if (Array.isArray(inner)) return inner;
  if (inner && typeof inner === 'object') {
    if (Array.isArray(inner.array)) return inner.array;
    if (Array.isArray(inner.data)) return inner.data;
  }
  return [];
}

function commExtractRoundCycleCount(response) {
  const raw = response?.data;
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  const countValue =
    (inner && typeof inner === 'object' ? inner.count : null) ??
    (raw && typeof raw === 'object' ? raw.count : null);
  const parsed = commParseNumber(countValue);
  return Number.isFinite(parsed) ? parsed : null;
}

async function commFetchStoredRoundBlocks(cycleId, leagueId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) {
    return { blocks: [], blockSet: new Set(), count: null };
  }
  const queryParts = [
    `cycle_id=eq.${encodeURIComponent(cycleId)}`,
    `league_id=eq.${encodeURIComponent(leagueId)}`,
    'select=block_number',
    'order=block_number.desc',
    `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`
  ];
  const { rows, count } = await commFetchWithCount(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
  const blocks = Array.isArray(rows)
    ? rows
        .map((row) => commParseNumber(row?.block_number))
        .filter((val) => Number.isFinite(val))
        .map((val) => Math.trunc(val))
    : [];
  return {
    blocks,
    blockSet: new Set(blocks),
    count: Number.isFinite(count) ? count : blocks.length
  };
}

function commComputeHeadCoverage(blockSet, latestBlock) {
  if (!blockSet || !blockSet.size || !Number.isFinite(latestBlock)) return 0;
  let coverage = 0;
  while (blockSet.has(latestBlock - coverage)) {
    coverage += 1;
  }
  return coverage;
}

function commComputeRoundMissing(blockSet, latestBlock, totalCount) {
  if (!blockSet || !Number.isFinite(latestBlock) || !Number.isFinite(totalCount) || totalCount <= 0) {
    return { missingCount: null, oldestMissing: null, minBlock: null };
  }
  const minBlock = latestBlock - (totalCount - 1);
  let missingCount = 0;
  let oldestMissing = null;
  for (let block = latestBlock; block >= minBlock; block -= 1) {
    if (!blockSet.has(block)) {
      missingCount += 1;
      oldestMissing = block;
    }
  }
  return { missingCount, oldestMissing, minBlock };
}

function commComputeRoundPercent(totalCount, missingCount) {
  if (!Number.isFinite(totalCount) || totalCount <= 0) return null;
  if (!Number.isFinite(missingCount) || missingCount < 0) return null;
  return ((totalCount - missingCount) / totalCount) * 100;
}

function commComputeDynamicRoundOverlap(checkedAt, now = Date.now()) {
  const base = COMM_KORREL_ROUND_OVERLAP_BASE;
  const max = COMM_KORREL_ROUND_OVERLAP_MAX;
  if (!Number.isFinite(checkedAt) || checkedAt <= 0) return base;
  const hoursAway = Math.max(0, (now - checkedAt) / 3600000);
  const expectedNew = Math.ceil(hoursAway * COMM_KORREL_ROUND_OVERLAP_PER_HOUR);
  const overlap = base + expectedNew;
  return Math.min(max, Math.max(base, overlap));
}

function commIsRoundRefreshDue(state, now = Date.now()) {
  if (commRoundRefreshColumnAvailable === false) return false;
  const refreshAt = state?.lastRefreshAt;
  if (!Number.isFinite(refreshAt) || refreshAt <= 0) return true;
  return now - refreshAt >= COMM_KORREL_ROUND_REFRESH_MS;
}

function commGetRoundCompletionSelectFields() {
  const base = 'cycle_id,league_id,missing_count,total_count,latest_block,checked_at,lock_owner,lock_expires_at';
  if (commRoundRefreshColumnAvailable === false) return base;
  return `${base},last_refresh_at`;
}

function commNormalizeRoundCompletionRow(row) {
  if (!row) return null;
  const cycleId = commParseNumber(row.cycle_id ?? row.cycleId);
  const leagueId = commParseNumber(row.league_id ?? row.leagueId);
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return null;
  const checkedRaw = row.checked_at ?? row.checkedAt ?? null;
  const checkedAt = checkedRaw ? new Date(checkedRaw).getTime() : 0;
  const refreshRaw = row.last_refresh_at ?? row.lastRefreshAt ?? null;
  const lastRefreshAt = refreshRaw ? new Date(refreshRaw).getTime() : 0;
  const lockRaw = row.lock_expires_at ?? row.lockExpiresAt ?? null;
  const lockExpiresAt = lockRaw ? new Date(lockRaw).getTime() : null;
  return {
    cycleId: Math.trunc(cycleId),
    leagueId: Math.trunc(leagueId),
    missingCount: Number.isFinite(commParseNumber(row.missing_count ?? row.missingCount))
      ? commParseNumber(row.missing_count ?? row.missingCount)
      : null,
    totalCount: Number.isFinite(commParseNumber(row.total_count ?? row.totalCount))
      ? commParseNumber(row.total_count ?? row.totalCount)
      : null,
    latestBlock: Number.isFinite(commParseNumber(row.latest_block ?? row.latestBlock))
      ? commParseNumber(row.latest_block ?? row.latestBlock)
      : null,
    checkedAt: Number.isFinite(checkedAt) ? checkedAt : 0,
    lastRefreshAt: Number.isFinite(lastRefreshAt) ? lastRefreshAt : 0,
    lockOwner: row.lock_owner ?? row.lockOwner ?? null,
    lockExpiresAt: Number.isFinite(lockExpiresAt) ? lockExpiresAt : null
  };
}

async function commFetchRoundCompletionStateFromDb(cycleId, leagueId, { forceBasic = false } = {}) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return null;
  const selectFields = forceBasic
    ? 'cycle_id,league_id,missing_count,total_count,latest_block,checked_at,lock_owner,lock_expires_at'
    : commGetRoundCompletionSelectFields();
  const query = [
    `select=${selectFields}`,
    `cycle_id=eq.${Math.trunc(cycleId)}`,
    `league_id=eq.${Math.trunc(leagueId)}`,
    'limit=1'
  ].join('&');
  try {
    const rows = await commFetch(COMM_TABLE_ROUND_COMPLETION, query);
    const row = Array.isArray(rows) ? rows[0] : null;
    if (!forceBasic && commRoundRefreshColumnAvailable === null) {
      commRoundRefreshColumnAvailable = true;
    }
    return commNormalizeRoundCompletionRow(row);
  } catch (err) {
    const msg = String(err?.message || '');
    if (!forceBasic && msg.toLowerCase().includes('last_refresh_at')) {
      commRoundRefreshColumnAvailable = false;
      return commFetchRoundCompletionStateFromDb(cycleId, leagueId, { forceBasic: true });
    }
    return null;
  }
}

async function commPersistRoundCompletionState(state, { lockOwner, lockExpiresAt } = {}) {
  if (!state || !Number.isFinite(state.cycleId) || !Number.isFinite(state.leagueId)) return;
  const payload = {
    cycle_id: Math.trunc(state.cycleId),
    league_id: Math.trunc(state.leagueId),
    checked_at: new Date(state.checkedAt || Date.now()).toISOString()
  };
  if (state.missingCount !== undefined) {
    payload.missing_count = Number.isFinite(state.missingCount) ? Math.trunc(state.missingCount) : null;
  }
  if (state.totalCount !== undefined) {
    payload.total_count = Number.isFinite(state.totalCount) ? Math.trunc(state.totalCount) : null;
  }
  if (state.latestBlock !== undefined) {
    payload.latest_block = Number.isFinite(state.latestBlock) ? Math.trunc(state.latestBlock) : null;
  }
  if (state.lastRefreshAt !== undefined && commRoundRefreshColumnAvailable !== false) {
    payload.last_refresh_at = new Date(state.lastRefreshAt || Date.now()).toISOString();
  }
  if (lockOwner !== undefined) payload.lock_owner = lockOwner;
  if (lockExpiresAt !== undefined) payload.lock_expires_at = lockExpiresAt;
  try {
    await commInsert(COMM_TABLE_ROUND_COMPLETION, [payload], {
      upsert: true,
      onConflict: 'cycle_id,league_id'
    });
    if (payload.last_refresh_at && commRoundRefreshColumnAvailable === null) {
      commRoundRefreshColumnAvailable = true;
    }
  } catch (err) {
    const msg = String(err?.message || '');
    if (payload.last_refresh_at && msg.toLowerCase().includes('last_refresh_at')) {
      commRoundRefreshColumnAvailable = false;
      delete payload.last_refresh_at;
      await commInsert(COMM_TABLE_ROUND_COMPLETION, [payload], {
        upsert: true,
        onConflict: 'cycle_id,league_id'
      });
      return;
    }
    throw err;
  }
}

async function commPersistRosterCompletionState(state) {
  if (!state || !Number.isFinite(state.cycleId) || !Number.isFinite(state.leagueId)) return;
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) return;
  const payload = {
    cycle_id: Math.trunc(state.cycleId),
    league_id: Math.trunc(state.leagueId),
    user_id: identity.userId,
    user_label: identity.userLabel || null,
    total_targets: Number.isFinite(commParseNumber(state.totalTargets)) ? Math.trunc(commParseNumber(state.totalTargets)) : null,
    stored_clans: Number.isFinite(commParseNumber(state.storedClans)) ? Math.trunc(commParseNumber(state.storedClans)) : null,
    stored_members: Number.isFinite(commParseNumber(state.storedMembers)) ? Math.trunc(commParseNumber(state.storedMembers)) : null,
    missing_count: Number.isFinite(commParseNumber(state.missingCount)) ? Math.trunc(commParseNumber(state.missingCount)) : null,
    api_calls: Number.isFinite(commParseNumber(state.apiCalls)) ? Math.trunc(commParseNumber(state.apiCalls)) : null,
    checked_at: new Date().toISOString()
  };
  if (state.fullRunAt !== undefined && commRosterCompletionColumnsAvailable !== false) {
    payload.full_run_at = new Date(state.fullRunAt || Date.now()).toISOString();
  }
  if (state.lastClanAt !== undefined && commRosterCompletionColumnsAvailable !== false) {
    payload.last_clan_at = new Date(state.lastClanAt || Date.now()).toISOString();
  }
  try {
    await commInsert(COMM_TABLE_ROSTER_COMPLETION, [payload], {
      upsert: true,
      onConflict: 'cycle_id,league_id,user_id'
    });
    if ((payload.full_run_at || payload.last_clan_at) && commRosterCompletionColumnsAvailable === null) {
      commRosterCompletionColumnsAvailable = true;
    }
  } catch (err) {
    const msg = String(err?.message || '');
    if ((payload.full_run_at || payload.last_clan_at) && (msg.toLowerCase().includes('full_run_at') || msg.toLowerCase().includes('last_clan_at'))) {
      commRosterCompletionColumnsAvailable = false;
      delete payload.full_run_at;
      delete payload.last_clan_at;
      try {
        await commInsert(COMM_TABLE_ROSTER_COMPLETION, [payload], {
          upsert: true,
          onConflict: 'cycle_id,league_id,user_id'
        });
      } catch (_) {
        // ignore
      }
      return;
    }
    // ignore
  }
}

function commSetRoundCompletionState({ cycleId, leagueId, missingCount, totalCount, latestBlock, lastRefreshAt }) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return;
  commKorrelRoundCompletionState = {
    cycleId: Math.trunc(cycleId),
    leagueId: Math.trunc(leagueId),
    missingCount: Number.isFinite(missingCount) ? missingCount : null,
    totalCount: Number.isFinite(totalCount) ? totalCount : null,
    latestBlock: Number.isFinite(latestBlock) ? latestBlock : null,
    checkedAt: Date.now(),
    lastRefreshAt: Number.isFinite(lastRefreshAt) ? lastRefreshAt : 0
  };
  commPersistRoundCompletionState(commKorrelRoundCompletionState).catch(() => {});
}

function commGetRoundCompletionState(cycleId, leagueId) {
  const state = commKorrelRoundCompletionState;
  if (!state) return null;
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return null;
  if (state.cycleId !== Math.trunc(cycleId) || state.leagueId !== Math.trunc(leagueId)) {
    return null;
  }
  if (Date.now() - state.checkedAt > COMM_KORREL_ROUND_COMPLETION_TTL_MS) {
    return null;
  }
  return state;
}

async function commGetRoundCompletionStateShared(cycleId, leagueId) {
  const local = commGetRoundCompletionState(cycleId, leagueId);
  if (local) return local;
  const remote = await commFetchRoundCompletionStateFromDb(cycleId, leagueId);
  if (!remote) return null;
  if (Date.now() - remote.checkedAt > COMM_KORREL_ROUND_COMPLETION_TTL_MS) return null;
  commKorrelRoundCompletionState = remote;
  return remote;
}

async function commAcquireRoundCompletionLock(cycleId, leagueId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return { ok: true };
  try {
    const owner = commGetRoundLockOwnerKey() || 'anon';
    const now = Date.now();
    const existing = await commFetchRoundCompletionStateFromDb(cycleId, leagueId);
    if (existing?.lockExpiresAt && existing.lockExpiresAt > now && existing.lockOwner && existing.lockOwner !== owner) {
      return { ok: false, state: existing };
    }
    const lockExpiresAt = new Date(now + COMM_KORREL_ROUND_COMPLETION_LOCK_MS).toISOString();
    const payload = {
      cycleId: Math.trunc(cycleId),
      leagueId: Math.trunc(leagueId),
      checkedAt: now,
      missingCount: existing?.missingCount ?? null,
      totalCount: existing?.totalCount ?? null,
      latestBlock: existing?.latestBlock ?? null
    };
    await commPersistRoundCompletionState(payload, { lockOwner: owner, lockExpiresAt });
    return { ok: true, owner, lockExpiresAt };
  } catch (err) {
    return { ok: true };
  }
}

async function commReleaseRoundCompletionLock(cycleId, leagueId) {
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) return;
  const payload = {
    cycleId: Math.trunc(cycleId),
    leagueId: Math.trunc(leagueId),
    checkedAt: Date.now()
  };
  await commPersistRoundCompletionState(payload, { lockOwner: null, lockExpiresAt: null });
}

async function commRunKorrelRoundCyclePage({ cycleId, leagueId, reason = '' } = {}) {
  const endpoint = COMM_KORREL_ROUND_SNAPSHOT_ENDPOINT;
  const throttleMs = commShouldThrottleKorrelApi(endpoint);
  if (throttleMs !== null) {
    const seconds = Math.ceil(throttleMs / 1000);
    commSetKorrelLog(`Round tick skipped (rate limit: ${seconds}s).`, { error: true });
    return false;
  }
  const tab = await commFindMiningWarsTab();
  if (!tab?.id) {
    commSetKorrelLog('Round tick skipped: no Mining Wars tab found.', { error: true });
    return false;
  }
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) {
    commSetKorrelLog('Round tick skipped: cycle/league id unavailable.', { error: true });
    return false;
  }
  const prevLogContext = commKorrelLogContextOverride;
  commKorrelLogContextOverride = { cycleId, leagueId };
  try {
    const localCompletion = commGetRoundCompletionState(cycleId, leagueId);
    const completion = localCompletion || await commFetchRoundCompletionStateFromDb(cycleId, leagueId);
    const now = Date.now();
    const refreshDue = commIsRoundCompletionStale(completion, now) || commIsRoundRefreshDue(completion, now);
    const missingCount = completion?.missingCount;
    const totalCountKnown = completion?.totalCount;
    const latestBlockKnown = completion?.latestBlock;
    if (missingCount === 0 && !refreshDue) {
      commSetKorrelLog(`Round tick skipped: cycle ${cycleId} league ${leagueId} already complete.`, { append: true });
      return true;
    }
    const lock = await commAcquireRoundCompletionLock(cycleId, leagueId);
    if (!lock.ok) {
      const lockOwner = lock?.state?.lockOwner || 'another user';
      commSetKorrelLog(`Round tick skipped: cycle ${cycleId} league ${leagueId} locked by ${lockOwner}.`, { append: true });
      return true;
    }
    const multipliers = commReadApiNumberList(endpoint, 'multipliers', COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS);
    const storedInfo = await commFetchStoredRoundBlocks(cycleId, leagueId);
    const storedSet = storedInfo.blockSet || new Set();
    const storedCount = Number.isFinite(storedInfo.count) ? storedInfo.count : storedSet.size;
    const overlap = commComputeDynamicRoundOverlap(completion?.checkedAt || 0, now);
    const currentBlock = commGetCurrentBlockNumber();
    const liveBlock = Number.isFinite(currentBlock) ? Math.trunc(currentBlock) : null;
    const headCoverage = Number.isFinite(liveBlock) ? commComputeHeadCoverage(storedSet, liveBlock) : null;
    const mode = refreshDue || missingCount == null || !Number.isFinite(latestBlockKnown) || !Number.isFinite(totalCountKnown)
      ? 'refresh'
      : 'backfill';
    const skipBase = Number.isFinite(headCoverage) ? headCoverage : storedCount;
    const rawSkip = mode === 'backfill' ? Math.max(0, skipBase - overlap) : 0;
    const minStoredBlock = storedInfo.blocks && storedInfo.blocks.length
      ? Math.min(...storedInfo.blocks)
      : null;
    const estimatedTotal = Number.isFinite(liveBlock) && Number.isFinite(minStoredBlock)
      ? Math.max(storedCount, Math.max(0, liveBlock - minStoredBlock + 1))
      : null;
    const maxSkipEstimated = Number.isFinite(estimatedTotal)
      ? Math.max(0, estimatedTotal - COMM_KORREL_ROUND_SNAPSHOT_LIMIT)
      : null;
    const maxSkip = Number.isFinite(totalCountKnown)
      ? Math.max(0, totalCountKnown - COMM_KORREL_ROUND_SNAPSHOT_LIMIT)
      : maxSkipEstimated;
    const skip = Number.isFinite(maxSkip) ? Math.min(rawSkip, maxSkip) : rawSkip;
    const skipNote = Number.isFinite(maxSkip) && rawSkip !== skip
      ? ` cap ${maxSkip}${Number.isFinite(totalCountKnown) ? '' : ' est'}`
      : '';
    const perRequest = COMM_KORREL_ROUND_SNAPSHOT_LIMIT;
    const payload = {
      cycleId,
      multipliers,
      pagination: { limit: perRequest, skip, count: 0 },
      leagueId
    };
    const liveNote = Number.isFinite(liveBlock)
      ? ` live ${liveBlock}${Number.isFinite(headCoverage) ? ` head ${headCoverage}` : ''}${Number.isFinite(estimatedTotal) && !Number.isFinite(totalCountKnown) ? ` est ${estimatedTotal}` : ''}`
      : '';
    commSetKorrelLog(
      `Round tick: ${mode} ${reason ? `(${reason}) ` : ''}skip ${skip}${skipNote}, overlap ${overlap}.${liveNote}`,
      { append: true }
    );
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload);
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    if (!response?.ok) {
      const statusLabel = Number.isFinite(commParseNumber(response?.status))
        ? Math.trunc(commParseNumber(response?.status))
        : 0;
      commSetKorrelLog(`Round tick failed: ${statusLabel} ${response?.error || 'API error'}.`, { error: true });
      return false;
    }
    const items = commExtractRoundCycleItems(response);
    if (!items.length) {
      commSetKorrelLog('Round tick returned no rows.', { error: true });
      return false;
    }
    const totalCount = commExtractRoundCycleCount(response) ?? totalCountKnown ?? null;
    const rows = items.map((item) => commBuildRoundCycleRow(item, cycleId));
    const cleanRows = rows.filter((row) => row.api_round_id || row.block_number);
    const storedCountNow = await commUpsertRoundCycleRows(cleanRows);
    const maxBlock = cleanRows
      .map((row) => commParseNumber(row.block_number))
      .filter((val) => Number.isFinite(val))
      .reduce((acc, val) => (acc == null || val > acc ? val : acc), null);
    const latestBlock = Number.isFinite(liveBlock)
      ? liveBlock
      : (mode === 'refresh'
        ? maxBlock
        : (Number.isFinite(latestBlockKnown) ? latestBlockKnown : (storedInfo.blocks?.[0] ?? maxBlock)));
    cleanRows
      .map((row) => commParseNumber(row.block_number))
      .filter((val) => Number.isFinite(val))
      .forEach((val) => storedSet.add(Math.trunc(val)));
    const missingInfo = Number.isFinite(latestBlock) && Number.isFinite(totalCount)
      ? commComputeRoundMissing(storedSet, latestBlock, totalCount)
      : { missingCount: missingCount ?? null };
    const newMissing = Number.isFinite(missingInfo.missingCount)
      ? missingInfo.missingCount
      : (missingCount ?? null);
    const refreshAt = mode === 'refresh' ? Date.now() : completion?.lastRefreshAt;
    await commPersistRoundCompletionState({
      cycleId,
      leagueId,
      missingCount: newMissing,
      totalCount,
      latestBlock,
      checkedAt: Date.now(),
      lastRefreshAt: refreshAt
    }, {
      lockOwner: lock.owner,
      lockExpiresAt: lock.lockExpiresAt
    });
    commKorrelRoundCompletionState = {
      cycleId: Math.trunc(cycleId),
      leagueId: Math.trunc(leagueId),
      missingCount: Number.isFinite(newMissing) ? newMissing : null,
      totalCount: Number.isFinite(totalCount) ? totalCount : null,
      latestBlock: Number.isFinite(latestBlock) ? latestBlock : null,
      checkedAt: Date.now(),
      lastRefreshAt: Number.isFinite(refreshAt) ? refreshAt : 0
    };
    const pct = commComputeRoundPercent(totalCount, newMissing);
    const pctLabel = Number.isFinite(pct) ? `${pct.toFixed(1)}%` : 'n/a';
    commSetKorrelLog(
      `Round tick stored ${storedCountNow || cleanRows.length} rows. Missing ${newMissing ?? 'n/a'} (${pctLabel}).`,
      { append: true, success: true }
    );
    return true;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Round tick failed.', { error: true });
    return false;
  } finally {
    commKorrelLogContextOverride = prevLogContext;
  }
}

function commBuildRoundCycleRow(item, fallbackCycleId) {
  const cycleId = item?.cycleId ?? item?.cycle_id ?? fallbackCycleId ?? null;
  const apiRoundId = commParseNumber(item?.id ?? item?.roundId ?? item?.round_id);
  const blockNumber = commParseNumber(item?.blockNumber ?? item?.block_number);
  const multiplier = commParseNumber(item?.multiplier);
  const leagueId = commParseNumber(item?.leagueId ?? item?.league_id);
  const nftRate = commParseNumber(item?.nftRate ?? item?.nft_rate);
  const power = commParseNumber(item?.power);
  const winnerClanId = commParseNumber(item?.winnerClanId ?? item?.winner_clan_id);
  const winnerUserId = commParseNumber(item?.winnerUserId ?? item?.winner_user_id);
  return {
    cycle_id: cycleId,
    block_number: blockNumber,
    active: item?.active ?? null,
    multiplier,
    league_id: leagueId,
    nft_rate: nftRate,
    power,
    hash: item?.hash ?? null,
    custom_multipliers: item?.customMultipliers ?? item?.custom_multipliers ?? null,
    api_round_id: apiRoundId,
    clan_winner: item?.clanWinner ?? item?.clan_winner ?? null,
    user_winner: item?.userWinner ?? item?.user_winner ?? null,
    winner_clan_id: winnerClanId,
    winner_user_id: winnerUserId,
    created_at: item?.createdAt ?? item?.created_at ?? null,
    updated_at: item?.updatedAt ?? item?.updated_at ?? null,
    started_at: item?.startedAt ?? item?.started_at ?? null,
    ended_at: item?.endedAt ?? item?.ended_at ?? null,
    raw: item,
    fetched_at: new Date().toISOString()
  };
}

async function commRunKorrelRoundCycleSnapshot(triggerBlockId, options = {}) {
  const { leagueIdOverride = null, logLabel = null, updateProgress = true, force = false } = options;
  const endpoint = COMM_KORREL_ROUND_SNAPSHOT_ENDPOINT;
  const throttleMs = commShouldThrottleKorrelApi(endpoint);
  if (throttleMs !== null) {
    const seconds = Math.ceil(throttleMs / 1000);
    commSetKorrelLog(`Round snapshot skipped (rate limit: ${seconds}s).`, { error: true });
    return false;
  }
  const tab = await commFindMiningWarsTab();
  if (!tab?.id) {
    commSetKorrelLog('Round snapshot skipped: no Mining Wars tab found.', { error: true });
    return false;
  }
  const cycleId = commReadApiNumber(endpoint, 'cycleId', getCycleNumber(new Date()));
  if (!Number.isFinite(cycleId)) {
    commSetKorrelLog('Round snapshot skipped: cycle id unavailable.', { error: true });
    return false;
  }
  const leagueIdInput = commReadApiNumber(endpoint, 'leagueId', null);
  const leagueId = Number.isFinite(leagueIdOverride)
    ? Math.trunc(leagueIdOverride)
    : (Number.isFinite(leagueIdInput) ? Math.trunc(leagueIdInput) : commGetKorrelActiveLeagueId());
  const prevLogContext = commKorrelLogContextOverride;
  commKorrelLogContextOverride = { cycleId, leagueId };
  if (Number.isFinite(leagueId)) {
    const completion = await commGetRoundCompletionStateShared(cycleId, leagueId);
    if (completion && completion.missingCount === 0 && !force) {
      const leagueText = ` league ${leagueId}`;
      commSetKorrelLog(`Round snapshot skipped: cycle ${cycleId}${leagueText} already complete.`, { append: true });
      return true;
    }
    const lock = await commAcquireRoundCompletionLock(cycleId, leagueId);
    if (!lock.ok) {
      const lockOwner = lock?.state?.lockOwner || 'another user';
      commSetKorrelLog(`Round snapshot skipped: cycle ${cycleId} league ${leagueId} locked by ${lockOwner}.`, { append: true });
      return true;
    }
  }
  const multipliers = commReadApiNumberList(endpoint, 'multipliers', COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS);
  const perRequest = COMM_KORREL_ROUND_SNAPSHOT_LIMIT;
  const blockLabel = triggerBlockId ? `block ${triggerBlockId}` : 'unknown block';
  if (logLabel) {
    commSetKorrelLog(`${logLabel} (${blockLabel}).`, { append: true });
  } else {
    const leagueSuffix = Number.isFinite(leagueId) ? ` league ${leagueId}` : '';
    commSetKorrelLog(
      `Round snapshot after ${COMM_KORREL_ROUND_SNAPSHOT_INTERVAL} rounds${leagueSuffix} (${blockLabel}).`,
      { append: true }
    );
  }
  try {
    const firstPayload = {
      cycleId,
      multipliers,
      pagination: { limit: perRequest, skip: 0, count: 0 }
    };
    if (Number.isFinite(leagueId)) {
      firstPayload.leagueId = leagueId;
    }
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    const firstResponse = await commFetchGominingApiViaInjection(tab.id, endpoint, firstPayload);
    commKorrelApiLastCallAt.set(endpoint, Date.now());
    if (!firstResponse?.ok) {
      const statusLabel = Number.isFinite(commParseNumber(firstResponse?.status))
        ? Math.trunc(commParseNumber(firstResponse?.status))
        : 0;
      commSetKorrelLog(`Round snapshot failed: ${statusLabel} ${firstResponse?.error || 'API error'}.`, { error: true });
      return false;
    }
    const rawLabel = logLabel ? `${logLabel} payload` : 'round/find-by-cycleId snapshot';
    commStoreKorrelRawPayload(rawLabel, firstResponse?.data ?? firstResponse);
    const firstItems = commExtractRoundCycleItems(firstResponse);
    if (!firstItems.length) {
      commSetKorrelLog('Round snapshot returned no rows.', { error: true });
      return false;
    }
    const totalCount = commExtractRoundCycleCount(firstResponse);
    const latestBlock = firstItems
      .map((item) => commParseNumber(item?.blockNumber ?? item?.block_number))
      .filter((val) => Number.isFinite(val))
      .reduce((acc, val) => (acc == null || val > acc ? val : acc), null);
    let storedInfo = { blockSet: new Set(), count: null };
    if (Number.isFinite(leagueId)) {
      storedInfo = await commFetchStoredRoundBlocks(cycleId, leagueId);
    }
    const storedSet = storedInfo.blockSet || new Set();
    const headCoverage = commComputeHeadCoverage(storedSet, latestBlock);
    const missingInfo = commComputeRoundMissing(storedSet, latestBlock, totalCount);
    const missingCount = missingInfo.missingCount;
    const oldestMissing = missingInfo.oldestMissing;
    if (Number.isFinite(leagueId)) {
      commSetRoundCompletionState({
        cycleId,
        leagueId,
        missingCount,
        totalCount,
        latestBlock,
        lastRefreshAt: Date.now()
      });
    }
    if (Number.isFinite(missingCount) && missingCount === 0) {
      const leagueText = Number.isFinite(leagueId) ? ` league ${leagueId}` : '';
      commSetKorrelLog(
        `Round snapshot: cycle ${cycleId}${leagueText} is complete (${totalCount || firstItems.length} rows).`,
        { success: true }
      );
      return true;
    }
    if (headCoverage > 0) {
      commSetKorrelLog(`Round snapshot: skipping ${headCoverage} newest rounds already stored.`, { append: true });
    }
    if (Number.isFinite(totalCount)) {
      const storedCount = Number.isFinite(storedInfo.count) ? storedInfo.count : storedSet.size;
      const missingLabel = Number.isFinite(missingCount) ? missingCount : 'unknown';
      commSetKorrelLog(
        `Round snapshot: cycle ${cycleId} has ${totalCount} rounds, stored ${storedCount}, missing ${missingLabel}.`,
        { append: true }
      );
    }
    const maxOffset = Number.isFinite(oldestMissing) && Number.isFinite(latestBlock)
      ? latestBlock - oldestMissing
      : (Number.isFinite(totalCount) ? totalCount - 1 : null);
    const items = [];
    let nextSkip = Math.max(0, headCoverage);
    let usedFirst = false;
    while (true) {
      if (maxOffset !== null && nextSkip > maxOffset) break;
      let batchItems = [];
      if (nextSkip === 0 && !usedFirst) {
        batchItems = firstItems;
        usedFirst = true;
      } else {
        const payload = {
          cycleId,
          multipliers,
          pagination: { limit: perRequest, skip: nextSkip, count: 0 }
        };
        if (Number.isFinite(leagueId)) {
          payload.leagueId = leagueId;
        }
        commSetKorrelLog(`Round snapshot: requesting ${perRequest} rounds (skip ${nextSkip})...`, { append: true });
        const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload);
        commKorrelApiLastCallAt.set(endpoint, Date.now());
        if (!response?.ok) {
          const statusLabel = Number.isFinite(commParseNumber(response?.status))
            ? Math.trunc(commParseNumber(response?.status))
            : 0;
          commSetKorrelLog(`Round snapshot failed: ${statusLabel} ${response?.error || 'API error'}.`, { error: true });
          break;
        }
        batchItems = commExtractRoundCycleItems(response);
      }
      if (!batchItems.length) {
        commSetKorrelLog('Round snapshot returned no rows.', { error: true });
        break;
      }
      items.push(...batchItems);
      if (maxOffset !== null && nextSkip + perRequest > maxOffset) break;
      nextSkip += perRequest;
      const delayMs = 2000 + Math.floor(Math.random() * 4000);
      commSetKorrelLog(`Round snapshot: waiting ${Math.round(delayMs / 1000)}s...`, { append: true });
      await commDelay(delayMs);
    }
    if (!items.length) return false;
    const rows = items.map((item) => commBuildRoundCycleRow(item, cycleId));
    const cleanRows = rows.filter((row) => row.api_round_id || row.block_number);
    if (!cleanRows.length) {
      commSetKorrelLog('Round snapshot rows were empty.', { error: true });
      return false;
    }
    const storedCount = await commUpsertRoundCycleRows(cleanRows);
    if (!Number.isFinite(leagueIdOverride)) {
      const leagueFromRows = Number.isFinite(leagueId)
        ? leagueId
        : commParseNumber(cleanRows[0]?.league_id);
      if (Number.isFinite(leagueFromRows)) {
        commKorrelCurrentLeagueId = Math.trunc(leagueFromRows);
      }
    }
    const maxBlock = cleanRows
      .map((row) => commParseNumber(row.block_number))
      .filter((val) => Number.isFinite(val))
      .reduce((acc, val) => (acc == null || val > acc ? val : acc), null);
    if (updateProgress && Number.isFinite(maxBlock)) {
      commKorrelLastStoredBlockNumber = maxBlock;
      commKorrelLastStoredCycleId = cycleId;
      commKorrelLastStoredCheckAt = Date.now();
    }
    if (Number.isFinite(leagueId)) {
      cleanRows
        .map((row) => commParseNumber(row.block_number))
        .filter((val) => Number.isFinite(val))
        .forEach((val) => storedSet.add(Math.trunc(val)));
      const finalMissing = commComputeRoundMissing(storedSet, latestBlock, totalCount);
      commSetRoundCompletionState({
        cycleId,
        leagueId,
        missingCount: finalMissing.missingCount,
        totalCount,
        latestBlock,
        lastRefreshAt: Date.now()
      });
    }
    const leagueText = Number.isFinite(leagueId) ? ` league ${leagueId}` : '';
    commSetKorrelLog(
      `Stored ${storedCount || cleanRows.length} rounds for cycle ${cycleId}${leagueText}.`,
      { success: true }
    );
    return true;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Round snapshot failed.', { error: true });
    return false;
  } finally {
    commKorrelLogContextOverride = prevLogContext;
  }
}

async function commRunKorrelRoundBulkSnapshot() {
  const endpoint = COMM_KORREL_ROUND_SNAPSHOT_ENDPOINT;
  const throttleMs = commShouldThrottleKorrelApi(endpoint);
  if (throttleMs !== null) {
    const seconds = Math.ceil(throttleMs / 1000);
    commSetKorrelLog(`Bulk snapshot skipped (rate limit: ${seconds}s).`, { error: true });
    return false;
  }
  const tab = await commFindMiningWarsTab();
  if (!tab?.id) {
    commSetKorrelLog('Bulk snapshot skipped: no Mining Wars tab found.', { error: true });
    return false;
  }
  const cycleId = commReadApiNumber(endpoint, 'cycleId', getCycleNumber(new Date()));
  if (!Number.isFinite(cycleId)) {
    commSetKorrelLog('Bulk snapshot skipped: cycle id unavailable.', { error: true });
    return false;
  }
  const leagueId = commReadApiNumber(endpoint, 'leagueId', null);
  const prevLogContext = commKorrelLogContextOverride;
  commKorrelLogContextOverride = { cycleId, leagueId };
  const multipliers = commReadApiNumberList(endpoint, 'multipliers', COMM_KORREL_ROUND_DEFAULT_MULTIPLIERS);
  const targetAmount = commGetKorrelBulkAmount();
  const perRequest = COMM_KORREL_ROUND_SNAPSHOT_LIMIT;
  const batches = Math.ceil(targetAmount / perRequest);
  commKorrelApiLastCallAt.set(endpoint, Date.now());
  commSetKorrelLog(`Bulk snapshot: requesting ${targetAmount} rounds from API...`, { append: true });
  commSetKorrelLog(`Using tab ${tab.id}: ${tab.url || 'unknown url'}`, { append: true });
  try {
    const items = [];
    for (let batch = 0; batch < batches; batch += 1) {
      const skip = batch * perRequest;
      const payload = {
        cycleId,
        multipliers,
        pagination: { limit: perRequest, skip, count: 0 }
      };
      if (Number.isFinite(leagueId)) {
        payload.leagueId = leagueId;
      }
      commSetKorrelLog(`Bulk snapshot: requesting ${perRequest} rounds (skip ${skip})...`, { append: true });
      const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload);
      commKorrelApiLastCallAt.set(endpoint, Date.now());
      if (!response?.ok) {
        commSetKorrelLog(`Bulk snapshot failed: ${response?.error || 'API error'}.`, { error: true });
        return false;
      }
      if (batch === 0) {
        commStoreKorrelRawPayload('round/find-by-cycleId bulk', response?.data ?? response);
      }
      const batchItems = commExtractRoundCycleItems(response);
      if (!batchItems.length) {
        commSetKorrelLog('Bulk snapshot returned no rows.', { error: true });
        return false;
      }
      items.push(...batchItems);
      if (batch < batches - 1) {
        const delayMs = 2000 + Math.floor(Math.random() * 4000);
        commSetKorrelLog(`Bulk snapshot: waiting ${Math.round(delayMs / 1000)}s...`, { append: true });
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      }
    }
    const trimmedItems = items.slice(0, targetAmount);
    commSetKorrelLog(`Bulk snapshot fetched ${trimmedItems.length} rows. Upserting...`, { append: true });
    const rows = trimmedItems.map((item) => commBuildRoundCycleRow(item, cycleId));
    const cleanRows = rows.filter((row) => row.api_round_id || row.block_number);
    if (!cleanRows.length) {
      commSetKorrelLog('Bulk snapshot rows were empty.', { error: true });
      return false;
    }
    const storedCount = await commUpsertRoundCycleRows(cleanRows);
    const maxBlock = cleanRows
      .map((row) => commParseNumber(row.block_number))
      .filter((val) => Number.isFinite(val))
      .reduce((acc, val) => (acc == null || val > acc ? val : acc), null);
    if (Number.isFinite(maxBlock)) {
      commKorrelLastStoredBlockNumber = maxBlock;
      commKorrelLastStoredCycleId = cycleId;
      commKorrelLastStoredCheckAt = Date.now();
    }
    commSetKorrelLog(`Bulk snapshot stored ${storedCount || cleanRows.length} rounds for cycle ${cycleId}.`, { success: true });
    commUpdateKorrelCountdown();
    return true;
  } catch (err) {
    commSetKorrelLog(err?.message || 'Bulk snapshot failed.', { error: true });
    return false;
  } finally {
    commKorrelLogContextOverride = prevLogContext;
  }
}

async function commMaybeRunKorrelOtherLeagueSnapshot() {
  if (!COMM_KORREL_AUTO_SNAPSHOT_ENABLED) return false;
  if (commKorrelRoundSnapshotPending || commKorrelOtherLeagueRunning) return false;
  await commRefreshKorrelStoredBlockNumber({ force: true });
  const cycleId = getCycleNumber(new Date());
  const currentLeague = commGetKorrelActiveLeagueId();
  const now = Date.now();
  if (!commKorrelOtherLeagueNextAt) {
    commKorrelOtherLeagueNextAt = now + commPickRandomIntervalMs();
    commUpdateKorrelOtherLeagueCountdown();
    return false;
  }
  if (now < commKorrelOtherLeagueNextAt) return false;
  const dbLeagues = await commFetchLeagueIdsFromDb(cycleId);
  const merged = Array.from(new Set([...(dbLeagues || []), ...COMM_KNOWN_LEAGUE_IDS]))
    .map((val) => commParseNumber(val))
    .filter((val) => Number.isFinite(val))
    .map((val) => Math.trunc(val));
  if (!merged.length) return false;
  const leagues = merged.slice().sort((a, b) => a - b);
  let candidateLeagues = leagues;
  if (Number.isFinite(currentLeague) && leagues.length > 1) {
    candidateLeagues = leagues.filter((league) => league !== Math.trunc(currentLeague));
  }
  if (!candidateLeagues.length) candidateLeagues = leagues;
  const idx = commKorrelOtherLeagueIndex % candidateLeagues.length;
  const nextLeague = candidateLeagues[idx];
  commKorrelOtherLeagueIndex = (idx + 1) % candidateLeagues.length;
  if (!Number.isFinite(nextLeague)) return false;
  commKorrelOtherLeagueRunning = true;
  commKorrelOtherLeagueLastAt = now;
  commKorrelOtherLeagueNextAt = now + commPickRandomIntervalMs();
  const label = `Other league snapshot: league ${nextLeague}`;
  try {
    const ok = await commRunKorrelRoundCycleSnapshot(null, {
      leagueIdOverride: nextLeague,
      logLabel: label,
      updateProgress: false
    });
    return ok;
  } finally {
    commKorrelOtherLeagueRunning = false;
    commUpdateKorrelCountdown();
  }
}

function commHandleKorrelRoundProgress(blockId) {
  const normalized = normalizeBlockId(blockId);
  if (!normalized) return;
  if (commKorrelLastRoundBlockId === normalized) return;
  commKorrelLastRoundBlockId = normalized;
  if (!COMM_KORREL_AUTO_SNAPSHOT_ENABLED) return;
  if (commKorrelRoundSnapshotPending) return;
  commRefreshKorrelStoredBlockNumber().catch(() => {});
  const currentBlock = commParseNumber(normalized);
  const cycleId = getCycleNumber(new Date());
  if (
    Number.isFinite(currentBlock) &&
    Number.isFinite(commKorrelLastStoredBlockNumber) &&
    commKorrelLastStoredCycleId === cycleId &&
    currentBlock >= commKorrelLastStoredBlockNumber
  ) {
    commKorrelRoundCount = currentBlock - commKorrelLastStoredBlockNumber;
  } else {
    commKorrelRoundCount += 1;
  }
  commUpdateKorrelCountdown();
  if (commKorrelRoundCount < COMM_KORREL_ROUND_SNAPSHOT_INTERVAL) return;
  commKorrelRoundSnapshotPending = true;
  commKorrelNextSnapshotAt = null;
  commUpdateKorrelCountdown();
  commRunKorrelRoundCycleSnapshot(normalized)
    .then((ok) => {
      if (ok) {
        commKorrelRoundCount = 0;
      } else {
        commKorrelRoundCount = Math.max(0, COMM_KORREL_ROUND_SNAPSHOT_INTERVAL - 1);
      }
      commUpdateKorrelCountdown();
    })
    .catch((err) => {
      commKorrelRoundCount = Math.max(0, COMM_KORREL_ROUND_SNAPSHOT_INTERVAL - 1);
      commSetKorrelLog(err?.message || 'Round snapshot failed.', { error: true });
      commUpdateKorrelCountdown();
    })
    .finally(() => {
      commKorrelRoundSnapshotPending = false;
      commUpdateKorrelCountdown();
    });
}

function commScheduleKorrelStartupSnapshot() {
  if (!COMM_KORREL_AUTO_SNAPSHOT_ENABLED) return;
  if (commKorrelStartupSnapshotTimer || commKorrelStartupSnapshotDone) return;
  const delay = 30000 + Math.floor(Math.random() * 15000);
  commKorrelNextSnapshotAt = Date.now() + delay;
  commKorrelStartupSnapshotTimer = setTimeout(() => {
    commKorrelStartupSnapshotTimer = null;
    commMaybeRunKorrelStartupSnapshot();
  }, delay);
  commSetKorrelLog(`Startup snapshot check scheduled in ${Math.round(delay / 1000)}s.`, { append: true });
  commUpdateKorrelCountdown();
}

async function commMaybeRunKorrelStartupSnapshot() {
  if (!COMM_KORREL_AUTO_SNAPSHOT_ENABLED) return;
  commKorrelStartupSnapshotDone = true;
  commKorrelNextSnapshotAt = null;
  commUpdateKorrelCountdown();
  if (commKorrelRoundSnapshotPending) {
    commSetKorrelLog('Startup snapshot skipped: another snapshot is running.', { error: true });
    return;
  }
  let rawBlockId = lastCurrentRoundLivePayload?.blockId ?? currentRoundInfo?.blockId ?? null;
  let normalized = normalizeBlockId(rawBlockId);
  let currentBlock = commParseNumber(normalized);
  let leagueId = commGetKorrelActiveLeagueId();
  if (!Number.isFinite(currentBlock)) {
    const tab = await commFindMiningWarsTab();
    if (tab?.id) {
      const endpoint = 'https://api.gomining.com/api/nft-game/round/get-state';
      const payload = { type: 'current' };
      if (Number.isFinite(leagueId)) payload.leagueId = leagueId;
      try {
        const response = await commFetchGominingApiViaInjection(tab.id, endpoint, payload);
        const raw = response?.data;
        const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
        const apiBlock = commParseNumber(
          inner?.blockNumber ?? inner?.block_number ?? inner?.blockId ?? inner?.block_id
        );
        const apiLeague = commParseNumber(inner?.leagueId ?? inner?.league_id);
        if (Number.isFinite(apiLeague)) {
          leagueId = Math.trunc(apiLeague);
          commKorrelCurrentLeagueId = leagueId;
        }
        if (Number.isFinite(apiBlock)) {
          rawBlockId = apiBlock;
          normalized = normalizeBlockId(apiBlock);
          currentBlock = commParseNumber(normalized);
        }
      } catch (err) {
        commSetKorrelLog('Startup snapshot: failed to read current round from API.', { error: true });
      }
    }
  }
  const hasCurrentBlock = Number.isFinite(currentBlock);
  if (!hasCurrentBlock) {
    commSetKorrelLog('Startup snapshot: current block id missing, running snapshot anyway.', { append: true });
  }
  const cycleId = getCycleNumber(new Date());
  if (!Number.isFinite(cycleId)) {
    commSetKorrelLog('Startup snapshot skipped: cycle id unavailable.', { error: true });
    commUpdateKorrelCountdown();
    return;
  }
  let lastBlockNumber = null;
  try {
    if (Number.isFinite(leagueId)) {
      const queryParts = [
        `league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`,
        'select=block_number',
        `cycle_id=eq.${encodeURIComponent(cycleId)}`,
        'order=block_number.desc',
        'limit=1'
      ];
      const query = queryParts.join('&');
      const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, query);
      lastBlockNumber = commParseNumber(rows?.[0]?.block_number);
      commKorrelLastStoredBlockNumber = Number.isFinite(lastBlockNumber) ? lastBlockNumber : null;
      commKorrelLastStoredCycleId = cycleId;
      commKorrelLastStoredCheckAt = Date.now();
    }
  } catch (err) {
    commSetKorrelLog('Could not verify last snapshot, running anyway.', { error: true });
  }
  if (Number.isFinite(lastBlockNumber) && hasCurrentBlock) {
    const diff = currentBlock - lastBlockNumber;
    if (diff < 0) {
      commSetKorrelLog('Startup snapshot: stored block ahead, fetching now...', { append: true });
    } else if (diff < COMM_KORREL_ROUND_SNAPSHOT_INTERVAL) {
      commSetKorrelLog(`Startup snapshot skipped: last insert is ${diff} rounds behind.`, { append: true });
      commUpdateKorrelCountdown();
      return;
    }
    commSetKorrelLog(`Startup snapshot: ${diff} rounds behind, fetching now...`, { append: true });
  } else {
    commSetKorrelLog('Startup snapshot: no recent insert found, fetching now...', { append: true });
  }
  commKorrelRoundSnapshotPending = true;
  try {
    const ok = await commRunKorrelRoundCycleSnapshot(hasCurrentBlock ? normalized : null);
    if (ok) commKorrelRoundCount = 0;
  } finally {
    commKorrelRoundSnapshotPending = false;
    commUpdateKorrelCountdown();
  }
}

function initClanComm() {
  const autoJoinBtn = document.getElementById('commAutoJoinBtn');
  if (autoJoinBtn && !autoJoinBtn.dataset.bound) {
    autoJoinBtn.addEventListener('click', () => {
      commEnsureAutoClan({ force: true }).catch(() => {});
    });
    autoJoinBtn.dataset.bound = '1';
  }
  const leaveBtn = document.getElementById('commLeaveBtn');
  if (leaveBtn && !leaveBtn.dataset.bound) {
    leaveBtn.addEventListener('click', commLeaveClan);
    leaveBtn.dataset.bound = '1';
  }
  const sendBtn = document.getElementById('commSendBtn');
  if (sendBtn && !sendBtn.dataset.bound) {
    sendBtn.addEventListener('click', commSendChat);
    sendBtn.dataset.bound = '1';
  }
  const messageInput = document.getElementById('commMessageInput');
  if (messageInput && !messageInput.dataset.bound) {
    messageInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        commSendChat();
      }
    });
    messageInput.dataset.bound = '1';
  }
  const pingBtn = document.getElementById('commPingBtn');
  if (pingBtn && !pingBtn.dataset.bound) {
    pingBtn.addEventListener('click', commSendPing);
    pingBtn.dataset.bound = '1';
  }
  const muteAlertBtn = document.getElementById('commMuteAlertBtn');
  if (muteAlertBtn && !muteAlertBtn.dataset.bound) {
    muteAlertBtn.addEventListener('click', commToggleAlertMute);
    muteAlertBtn.dataset.bound = '1';
  }
  commUpdateAlertMuteButtons();
  if (WEBSITE_MODE) {
    const statusEl = document.getElementById('commStatus');
    if (statusEl) {
      statusEl.textContent = 'Website mode: open the app to sync clan chat.';
    }
    ['commAutoJoinBtn', 'commSendBtn', 'commMessageInput', 'commPingBtn', 'commMuteAlertBtn', 'commLeaveBtn'].forEach((id) => {
      const el = document.getElementById(id);
      if (el) {
        el.disabled = true;
        el.setAttribute('aria-disabled', 'true');
      }
    });
  }
  commUpdateHighAlertButtons();
  const membersList = document.getElementById('commMembersList');
  if (membersList && !membersList.dataset.bound) {
    membersList.addEventListener('click', (event) => {
      const kickBtn = event.target.closest('.comm-kick-btn');
      if (kickBtn && kickBtn.dataset.member) {
        commKickMember(kickBtn.dataset.member);
        return;
      }
      const ownerBtn = event.target.closest('.comm-owner-btn');
      if (ownerBtn && ownerBtn.dataset.member) {
        commPromoteMember(ownerBtn.dataset.member);
      }
    });
    membersList.dataset.bound = '1';
  }
  const codeUpdateBtn = document.getElementById('commCodeUpdateBtn');
  if (codeUpdateBtn && !codeUpdateBtn.dataset.bound) {
    codeUpdateBtn.addEventListener('click', commUpdateCodeWord);
    codeUpdateBtn.dataset.bound = '1';
  }
  const noticeSaveBtn = document.getElementById('commNoticeSaveBtn');
  if (noticeSaveBtn && !noticeSaveBtn.dataset.bound) {
    noticeSaveBtn.addEventListener('click', commSaveNotice);
    noticeSaveBtn.dataset.bound = '1';
  }
  const snapshotBtn = document.getElementById('commSnapshotBtn');
  if (snapshotBtn && !snapshotBtn.dataset.bound) {
    snapshotBtn.addEventListener('click', commCaptureSnapshot);
    snapshotBtn.dataset.bound = '1';
  }
  const clanStoreBtn = document.getElementById('commClanStoreBtn');
  if (clanStoreBtn && !clanStoreBtn.dataset.bound) {
    clanStoreBtn.addEventListener('click', commStoreClanStats);
    clanStoreBtn.dataset.bound = '1';
  }
  const clanApiBtn = document.getElementById('commClanApiBtn');
  if (clanApiBtn && !clanApiBtn.dataset.bound) {
    clanApiBtn.addEventListener('click', commFetchClanApiData);
    clanApiBtn.dataset.bound = '1';
  }
  const detailActions = document.getElementById('commKorrelDetailActions');
  if (detailActions && !detailActions.dataset.bound) {
    detailActions.addEventListener('click', (event) => {
      const btn = event.target.closest('[data-detail-index]');
      if (!btn) return;
      const index = Number(btn.getAttribute('data-detail-index'));
      if (Number.isFinite(index)) commShowKorrelApiDetails(index);
    });
    detailActions.dataset.bound = '1';
  }
  const rawBtn = document.getElementById('commKorrelRawBtn');
  if (rawBtn && !rawBtn.dataset.bound) {
    rawBtn.addEventListener('click', () => {
      commShowKorrelRawPayload();
    });
    rawBtn.dataset.bound = '1';
  }
  const detailClose = document.getElementById('commKorrelDetailClose');
  if (detailClose && !detailClose.dataset.bound) {
    detailClose.addEventListener('click', commHideKorrelApiDetails);
    detailClose.dataset.bound = '1';
  }
  const detailModal = document.getElementById('commKorrelDetailModal');
  if (detailModal && !detailModal.dataset.bound) {
    detailModal.addEventListener('click', (event) => {
      if (event.target === detailModal) commHideKorrelApiDetails();
    });
    detailModal.dataset.bound = '1';
  }
  const minIntervalInput = document.getElementById('commApiMinInterval');
  if (minIntervalInput && !minIntervalInput.dataset.bound) {
    minIntervalInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) return;
      commKorrelApiMinIntervalMs = Math.max(0, Math.floor(value * 1000));
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    minIntervalInput.dataset.bound = '1';
  }
  const defaultLimitInput = document.getElementById('commApiDefaultLimit');
  if (defaultLimitInput && !defaultLimitInput.dataset.bound) {
    defaultLimitInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) return;
      commKorrelApiDefaultLimit = Math.max(1, Math.floor(value));
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    defaultLimitInput.dataset.bound = '1';
  }
  const bulkAmountInput = document.getElementById('commApiBulkAmount');
  if (bulkAmountInput && !bulkAmountInput.dataset.bound) {
    bulkAmountInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) return;
      commKorrelApiBulkAmount = Math.max(COMM_KORREL_ROUND_SNAPSHOT_LIMIT, Math.min(500, Math.floor(value)));
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    bulkAmountInput.dataset.bound = '1';
  }
  const rosterBatchInput = document.getElementById('commRosterBatch');
  if (rosterBatchInput && !rosterBatchInput.dataset.bound) {
    rosterBatchInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) return;
      commKorrelRosterBatchSize = Math.max(1, Math.min(200, Math.floor(value)));
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    rosterBatchInput.dataset.bound = '1';
  }
  const rosterLimitInput = document.getElementById('commRosterMemberLimit');
  if (rosterLimitInput && !rosterLimitInput.dataset.bound) {
    rosterLimitInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) return;
      commKorrelRosterMemberLimit = Math.max(10, Math.min(25, Math.floor(value)));
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    rosterLimitInput.dataset.bound = '1';
  }
  const rosterVerboseInput = document.getElementById('commRosterVerbose');
  if (rosterVerboseInput && !rosterVerboseInput.dataset.bound) {
    rosterVerboseInput.addEventListener('change', (event) => {
      commKorrelRosterVerbose = Boolean(event.target.checked);
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    rosterVerboseInput.dataset.bound = '1';
  }
  const cycleOverrideInput = document.getElementById('commApiCycleOverride');
  if (cycleOverrideInput && !cycleOverrideInput.dataset.bound) {
    cycleOverrideInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) {
        commKorrelApiCycleOverride = null;
      } else {
        commKorrelApiCycleOverride = Math.max(1, Math.floor(value));
      }
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    cycleOverrideInput.dataset.bound = '1';
  }
  const leagueOverrideInput = document.getElementById('commApiLeagueOverride');
  if (leagueOverrideInput && !leagueOverrideInput.dataset.bound) {
    leagueOverrideInput.addEventListener('input', (event) => {
      const value = commParseNumber(event.target.value);
      if (!Number.isFinite(value)) {
        commKorrelApiLeagueOverride = null;
      } else {
        commKorrelApiLeagueOverride = Math.max(1, Math.floor(value));
      }
      commSaveKorrelApiSettings();
      commUpdateKorrelApiSettingsUI();
    });
    leagueOverrideInput.dataset.bound = '1';
  }
  const apiGetMyBtn = document.getElementById('commApiGetMyBtn');
  if (apiGetMyBtn && !apiGetMyBtn.dataset.bound) {
    apiGetMyBtn.addEventListener('click', commRunApiGetMy);
    apiGetMyBtn.dataset.bound = '1';
  }
  const apiGetByIdBtn = document.getElementById('commApiGetByIdBtn');
  if (apiGetByIdBtn && !apiGetByIdBtn.dataset.bound) {
    apiGetByIdBtn.addEventListener('click', commRunApiGetById);
    apiGetByIdBtn.dataset.bound = '1';
  }
  const apiRewardsBtn = document.getElementById('commApiRewardsBtn');
  if (apiRewardsBtn && !apiRewardsBtn.dataset.bound) {
    apiRewardsBtn.addEventListener('click', commRunApiRewardsByUser);
    apiRewardsBtn.dataset.bound = '1';
  }
  const apiRoundCycleBtn = document.getElementById('commApiRoundCycleBtn');
  if (apiRoundCycleBtn && !apiRoundCycleBtn.dataset.bound) {
    apiRoundCycleBtn.addEventListener('click', commRunApiRoundFindByCycle);
    apiRoundCycleBtn.dataset.bound = '1';
  }
  const apiRoundLastBtn = document.getElementById('commApiRoundLastBtn');
  if (apiRoundLastBtn && !apiRoundLastBtn.dataset.bound) {
    apiRoundLastBtn.addEventListener('click', commRunApiRoundGetLast);
    apiRoundLastBtn.dataset.bound = '1';
  }
  const apiRoundStateBtn = document.getElementById('commApiRoundStateBtn');
  if (apiRoundStateBtn && !apiRoundStateBtn.dataset.bound) {
    apiRoundStateBtn.addEventListener('click', commRunApiRoundGetState);
    apiRoundStateBtn.dataset.bound = '1';
  }
  const apiRoundBulkBtn = document.getElementById('commApiRoundBulkBtn');
  if (apiRoundBulkBtn && !apiRoundBulkBtn.dataset.bound) {
    apiRoundBulkBtn.addEventListener('click', commRunKorrelRoundBulkSnapshot);
    apiRoundBulkBtn.dataset.bound = '1';
  }
  const apiRosterBtn = document.getElementById('commApiRosterBtn');
  if (apiRosterBtn && !apiRosterBtn.dataset.bound) {
    apiRosterBtn.addEventListener('click', commRunKorrelClanRosterBatch);
    apiRosterBtn.dataset.bound = '1';
  }
  const apiRosterPatchBtn = document.getElementById('commApiRosterPatchBtn');
  if (apiRosterPatchBtn && !apiRosterPatchBtn.dataset.bound) {
    apiRosterPatchBtn.addEventListener('click', commRunKorrelClanRosterPatch);
    apiRosterPatchBtn.dataset.bound = '1';
  }
  const apiRosterResetBtn = document.getElementById('commApiRosterResetBtn');
  if (apiRosterResetBtn && !apiRosterResetBtn.dataset.bound) {
    apiRosterResetBtn.addEventListener('click', () => {
      commResetKorrelRosterProgress({ log: true });
    });
    apiRosterResetBtn.dataset.bound = '1';
  }
  const scheduleResetBtn = document.getElementById('commApiScheduleResetBtn');
  if (scheduleResetBtn && !scheduleResetBtn.dataset.bound) {
    scheduleResetBtn.addEventListener('click', () => {
      commResetKorrelAutoSchedule({ log: true });
    });
    scheduleResetBtn.dataset.bound = '1';
  }

  if (!commSession) {
    commSession = commLoadSession();
  }
  commLoadKorrelApiSettings();
  commUpdateKorrelApiSettingsUI();
  commToggleView();
  if (commSession) {
    commLinkMemberToAuth().catch(() => {});
    commLoadMessages(true);
    commLoadMembers(true);
    commLoadNotice();
    commScheduleKorrelStartupSnapshot();
  }
  commStartTimers();
  if (commSession) {
    commEnsureAutoClan({ silentStatus: true }).catch(() => {});
  } else {
    commRestoreSessionFromAuth()
      .then(() => commEnsureAutoClan({ silentStatus: true }))
      .catch(() => commEnsureAutoClan({ silentStatus: true }));
  }
}

function buildKorrelToolsMarkup() {
  return `
        <div class="comm-card comm-korrel-tools" id="commKorrelTools" style="margin-top:10px;">
          <h4>Korrel tools</h4>
          <p class="muted" style="margin-bottom:6px;font-size:11px;">Store clan stats from the active Mining Wars clan page.</p>
          <div class="comm-korrel-actions">
            <button id="commClanStoreBtn" class="btn sm" type="button">Store clan stats</button>
            <button id="commClanApiBtn" class="btn sm ghost" type="button">Fetch clan API</button>
          </div>
          <div class="comm-korrel-api-settings">
            <label>Min interval (sec)</label>
            <div class="comm-korrel-slider-row">
              <input id="commApiMinInterval" class="comm-korrel-slider" type="range" min="0" max="600" step="5">
              <span id="commApiMinIntervalValue" class="muted">60s</span>
            </div>
            <label>Default limit</label>
            <div class="comm-korrel-slider-row">
              <input id="commApiDefaultLimit" class="comm-korrel-slider" type="range" min="5" max="200" step="5">
              <span id="commApiDefaultLimitValue" class="muted">25</span>
            </div>
          <label>Bulk amount</label>
          <div class="comm-korrel-slider-row">
            <input id="commApiBulkAmount" class="comm-korrel-input" type="number" min="10" max="500" step="1" value="50">
            <span id="commApiBulkAmountValue" class="muted">50</span>
          </div>
          <label>Roster batch (clans)</label>
          <div class="comm-korrel-slider-row">
            <input id="commRosterBatch" class="comm-korrel-input" type="number" min="1" max="200" step="1" value="25">
            <span id="commRosterBatchValue" class="muted">25</span>
          </div>
          <label>Roster member limit</label>
          <div class="comm-korrel-slider-row">
            <input id="commRosterMemberLimit" class="comm-korrel-input" type="number" min="10" max="25" step="5" value="25">
            <span id="commRosterMemberLimitValue" class="muted">25</span>
          </div>
          <label>Verbose roster log</label>
          <div class="comm-korrel-slider-row">
            <input id="commRosterVerbose" type="checkbox">
            <span class="muted">Log every clan + member</span>
          </div>
          <label>Cycle override</label>
          <div class="comm-korrel-slider-row">
            <input id="commApiCycleOverride" class="comm-korrel-input" type="number" min="1" step="1" placeholder="Current">
            <span class="muted">Optional</span>
          </div>
            <label>League override</label>
            <div class="comm-korrel-slider-row">
              <input id="commApiLeagueOverride" class="comm-korrel-input" type="number" min="1" step="1" placeholder="All">
              <span class="muted">Optional</span>
            </div>
          </div>
          <div id="commKorrelNextCall" class="muted" style="margin:6px 0 4px;font-size:11px;">Next auto snapshot: -</div>
          <div id="commKorrelBackfillCall" class="muted" style="margin:0 0 6px;font-size:11px;">Backfill update: -</div>
          <div class="comm-korrel-actions" style="margin-bottom:6px;">
            <button id="commApiScheduleResetBtn" class="btn sm ghost" type="button">Reset auto schedule</button>
          </div>
          <div class="comm-korrel-actions comm-korrel-api-actions">
            <button id="commApiGetMyBtn" class="btn sm ghost" type="button">API: get-my</button>
            <button id="commApiGetByIdBtn" class="btn sm ghost" type="button">API: get-by-id</button>
            <button id="commApiRewardsBtn" class="btn sm ghost" type="button">API: rewards-by-user</button>
            <button id="commApiRoundCycleBtn" class="btn sm ghost" type="button">API: round/cycle</button>
            <button id="commApiRoundLastBtn" class="btn sm ghost" type="button">API: round/last</button>
            <button id="commApiRoundStateBtn" class="btn sm ghost" type="button">API: round/state</button>
            <button id="commApiRoundBulkBtn" class="btn sm" type="button">API: round x${commKorrelApiBulkAmount}</button>
            <button id="commApiRosterBtn" class="btn sm" type="button">API: roster batch</button>
            <button id="commApiRosterPatchBtn" class="btn sm ghost" type="button">API: patch clans</button>
            <button id="commApiRosterResetBtn" class="btn sm ghost" type="button">Reset roster</button>
          </div>
          <div id="commKorrelTable" class="comm-korrel-table"></div>
          <div id="commKorrelDetailActions" class="comm-korrel-actions comm-korrel-detail-actions"></div>
          <div class="comm-korrel-actions">
            <button id="commKorrelRawBtn" class="btn sm ghost" type="button">View RAW payload</button>
          </div>
          <div id="commKorrelApiInputs" class="comm-korrel-table comm-korrel-schema"></div>
          <div class="comm-korrel-table">
            <h5 style="margin:10px 0 4px;">Cycle progress</h5>
            <div id="commKorrelCycleProgress"></div>
          </div>
          <div class="comm-korrel-table">
            <h5 style="margin:10px 0 4px;">API usage</h5>
            <div id="commKorrelApiUsage"></div>
            <div id="commKorrelApiLimits" style="margin-top:4px;"></div>
          </div>
          <div id="commKorrelLog" class="comm-log"></div>
          <div id="commKorrelDetailModal" class="comm-korrel-modal" style="display:none;">
            <div class="comm-korrel-modal-card">
              <div class="comm-korrel-modal-header">
                <span id="commKorrelDetailTitle">API details</span>
                <button id="commKorrelDetailClose" class="btn sm ghost" type="button">Close</button>
              </div>
              <div id="commKorrelDetailTable" class="comm-korrel-detail-body"></div>
            </div>
          </div>
        </div>
  `;
}

function populateTools() {
  const container = document.getElementById('toolsGrid');
  if (!container) return;
  commLoadKorrelApiSettings();
  commUpdateKorrelApiSettingsUI();
  const html = `
    <div class="clan-comm">
      <div class="title">Clan communication</div>
      <div class="muted" id="commIntro">Clan chat automatically syncs with your current Mining Wars clan.</div>
      <div class="comm-card" id="commNoticeCard" style="margin-top:8px;">
        <h4>Clan instructions</h4>
        <textarea id="commNoticeInput" placeholder="Only owners can edit this notice..." readonly></textarea>
        <button id="commNoticeSaveBtn" class="btn sm" type="button" style="margin-top:8px;">Save notice</button>
        <div id="commNoticeStatus" class="muted" style="font-size:11px;margin-top:6px;"></div>
      </div>

      <div id="clanCommPublic">
        <div class="comm-grid">
          <div class="comm-card">
            <h4>Auto sync</h4>
            <p class="muted" style="margin-bottom:8px;">We auto-connect you to your current Mining Wars clan chat.</p>
            <button id="commAutoJoinBtn" class="btn" type="button">Sync clan now</button>
          </div>
        </div>
        <div id="commStatus" class="comm-status muted"></div>
      </div>

      <div id="clanCommPrivate" class="comm-private" style="display:none">
        <div class="comm-meta">
          <span id="commClanLabel"></span>
          <button id="commLeaveBtn" class="btn sm ghost" type="button" style="float:right;">Leave clan</button>
        </div>
        <div id="commMembersList" class="comm-members">
          <h5>Clan members online</h5>
          <div class="muted">Loading clan members...</div>
        </div>
        <div id="commChat" class="comm-chat"></div>
        <div class="comm-chat-input">
          <input type="text" id="commMessageInput" placeholder="Type a message to your clan..." autocomplete="off" />
          <button id="commSendBtn" class="btn" type="button">Send</button>
        </div>
        <div class="comm-actions">
          <button id="commPingBtn" class="btn ghost" type="button">Time to boost alert</button>
          <button id="commMuteAlertBtn" class="btn ghost" type="button">Mute alert</button>
          <span class="muted" style="font-size:11px;">Ping plays a short tone for everyone logged in.</span>
        </div>
        <div id="commCodeControls" class="comm-grid" style="display:none;margin-top:10px;">
          <div class="comm-card">
            <h4>Update clan code word</h4>
            <label>New code word</label>
            <input type="text" id="commCodeInput" placeholder="New code word" autocomplete="off" />
            <button id="commCodeUpdateBtn" class="btn ghost" type="button" style="margin-top:8px;">Update code word</button>
            <div class="muted" style="font-size:11px;margin-top:6px;">Share the new code with clan members securely.</div>
          </div>
        </div>
        <div id="commPrivateStatus" class="comm-status muted"></div>
      </div>
    </div>
  `;
  const card = createCard({ title: 'Clan Communication', htmlContent: html, id: 'clan-tools-comm' });
  card.classList.add('clan-tools-card');
  container.innerHTML = '';
  container.appendChild(card);
  initClanComm();
}

function commNormalizeNotificationOptionMultipliers(values, fallback = [4, 8, 16, 32, 64, '64+']) {
  const allowed = Array.isArray(values) ? values : [];
  const normalized = allowed
    .map((value) => (value === '64+' ? '64+' : Number(value)))
    .filter((value) => value === '64+' || Number.isFinite(value))
    .filter((value) => BOOST_HELPER_MULTIPLIERS.includes(value));
  if (normalized.length) return normalized;
  return Array.isArray(fallback) ? fallback.slice() : [4, 8, 16, 32, 64, '64+'];
}

function buildNotificationOptionsUi({
  idPrefix,
  dataAttr,
  state = {},
  showReset = true
}) {
  const lowBoostEnabled = !!state.lowBoostEnabled;
  const lowBoostMaxCount = Math.max(0, Math.floor(Number(state.lowBoostMaxCount || 0) || 0));
  const clanPowerEnabled = !!state.clanPowerEnabled;
  const multiplierEnabled = !!state.multiplierEnabled;
  const selectedMultipliers = new Set(
    commNormalizeNotificationOptionMultipliers(
      state.selectedMultipliers,
      [4, 8, 16, 32, 64, '64+']
    ).map((value) => String(value))
  );
  const multiplierOptions = BOOST_HELPER_MULTIPLIERS
    .map((value) => {
      const label = value === '64+' ? '64+' : `x${value}`;
      const checked = selectedMultipliers.has(String(value)) ? 'checked' : '';
      return `
        <label class="stats-filter-toggle" style="gap:6px;">
          <input type="checkbox" data-${dataAttr}="${commEscape(String(value))}" ${checked}>
          ${commEscape(label)}
        </label>
      `;
    })
    .join('');
  return `
    <div style="margin-top:12px;">
      <div class="title" style="font-size:0.9rem;display:flex;align-items:center;justify-content:space-between;gap:8px;">
        <span>Notification options</span>
        ${showReset ? `<button id="${idPrefix}ResetOptions" class="btn sm ghost" type="button">Reset options</button>` : ''}
      </div>
      <div class="boost-helper-options">
        <div class="boost-helper-option boost-helper-option-low">
          <label class="stats-filter-toggle">
            <input id="${idPrefix}OptLowBoostEnabled" type="checkbox" ${lowBoostEnabled ? 'checked' : ''}>
            Few or no boosts in the first 90 seconds
          </label>
          <div class="stats-filter-bar" style="margin-top:8px;gap:8px;align-items:center;">
            <span class="muted">Max boosts by all clans</span>
            <input id="${idPrefix}OptLowBoostMaxCount" class="stats-filter-input" style="max-width:110px;" type="number" min="0" step="1" value="${commEscape(String(lowBoostMaxCount))}">
          </div>
        </div>
        <div class="boost-helper-option boost-helper-option-clan">
          <label class="stats-filter-toggle">
            <input id="${idPrefix}OptClanPowerEnabled" type="checkbox" ${clanPowerEnabled ? 'checked' : ''}>
            My clan used a Clan power-up
          </label>
        </div>
        <div class="boost-helper-option boost-helper-option-multiplier">
          <label class="stats-filter-toggle">
            <input id="${idPrefix}OptMultiplierEnabled" type="checkbox" ${multiplierEnabled ? 'checked' : ''}>
            All multipliers
          </label>
          <div style="margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
            ${multiplierOptions}
            <button id="${idPrefix}SelectAllMultiplier" class="btn sm ghost" type="button">Select all</button>
            <button id="${idPrefix}ClearMultiplier" class="btn sm ghost" type="button">Clear</button>
          </div>
        </div>
      </div>
    </div>
  `;
}

function bindNotificationOptionsUi(root, {
  idPrefix,
  dataAttr,
  onChange = null,
  getResetState = null
} = {}) {
  const lowBoostEnabledInput = root.querySelector(`#${idPrefix}OptLowBoostEnabled`);
  const lowBoostMaxCountInput = root.querySelector(`#${idPrefix}OptLowBoostMaxCount`);
  const clanPowerEnabledInput = root.querySelector(`#${idPrefix}OptClanPowerEnabled`);
  const multiplierEnabledInput = root.querySelector(`#${idPrefix}OptMultiplierEnabled`);
  const multiplierBoxes = Array.from(root.querySelectorAll(`[data-${dataAttr}]`));
  const selectAllMultBtn = root.querySelector(`#${idPrefix}SelectAllMultiplier`);
  const clearMultBtn = root.querySelector(`#${idPrefix}ClearMultiplier`);
  const resetOptionsBtn = root.querySelector(`#${idPrefix}ResetOptions`);

  const readSelectedMultipliers = () => multiplierBoxes
    .filter((box) => box.checked)
    .map((box) => {
      const raw = box.getAttribute(`data-${dataAttr}`);
      return raw === '64+' ? '64+' : Number(raw);
    })
    .filter((value) => value === '64+' || Number.isFinite(value));

  const readState = () => ({
    lowBoostEnabled: !!lowBoostEnabledInput?.checked,
    lowBoostMaxCount: Math.max(0, Math.floor(Number(lowBoostMaxCountInput?.value || 0) || 0)),
    clanPowerEnabled: !!clanPowerEnabledInput?.checked,
    multiplierEnabled: !!multiplierEnabledInput?.checked,
    selectedMultipliers: readSelectedMultipliers()
  });

  const applyState = (state = {}) => {
    if (lowBoostEnabledInput) lowBoostEnabledInput.checked = !!state.lowBoostEnabled;
    if (lowBoostMaxCountInput) lowBoostMaxCountInput.value = String(Math.max(0, Math.floor(Number(state.lowBoostMaxCount || 0) || 0)));
    if (clanPowerEnabledInput) clanPowerEnabledInput.checked = !!state.clanPowerEnabled;
    if (multiplierEnabledInput) multiplierEnabledInput.checked = !!state.multiplierEnabled;
    if (Array.isArray(state.selectedMultipliers)) {
      const selectedSet = new Set(state.selectedMultipliers.map((value) => String(value)));
      multiplierBoxes.forEach((box) => {
        const raw = box.getAttribute(`data-${dataAttr}`);
        box.checked = selectedSet.has(String(raw));
      });
    }
    updateMultiplierOptionState();
  };

  const updateMultiplierOptionState = () => {
    const enabled = !!multiplierEnabledInput?.checked;
    multiplierBoxes.forEach((box) => { box.disabled = !enabled; });
    if (selectAllMultBtn) selectAllMultBtn.disabled = !enabled;
    if (clearMultBtn) clearMultBtn.disabled = !enabled;
  };

  const emitChange = () => {
    if (typeof onChange === 'function') onChange(readState());
  };

  if (lowBoostEnabledInput) lowBoostEnabledInput.addEventListener('change', emitChange);
  if (lowBoostMaxCountInput) lowBoostMaxCountInput.addEventListener('input', emitChange);
  if (clanPowerEnabledInput) clanPowerEnabledInput.addEventListener('change', emitChange);
  if (multiplierEnabledInput) {
    multiplierEnabledInput.addEventListener('change', () => {
      updateMultiplierOptionState();
      emitChange();
    });
  }
  multiplierBoxes.forEach((box) => box.addEventListener('change', emitChange));
  if (selectAllMultBtn) {
    selectAllMultBtn.addEventListener('click', () => {
      multiplierBoxes.forEach((box) => { box.checked = true; });
      emitChange();
    });
  }
  if (clearMultBtn) {
    clearMultBtn.addEventListener('click', () => {
      multiplierBoxes.forEach((box) => { box.checked = false; });
      emitChange();
    });
  }
  if (resetOptionsBtn) {
    resetOptionsBtn.addEventListener('click', () => {
      const resetState = typeof getResetState === 'function'
        ? getResetState(readState())
        : {
            lowBoostEnabled: false,
            lowBoostMaxCount: 1,
            clanPowerEnabled: false,
            multiplierEnabled: false,
            selectedMultipliers: []
          };
      applyState(resetState);
      emitChange();
    });
  }

  updateMultiplierOptionState();
  return {
    readState,
    applyState,
    updateMultiplierOptionState
  };
}

function populateTelegramBot() {
  const container = document.getElementById('telegramBotGrid');
  if (!container) return;
  const cfg = commLoadTelegramConfig();
  let telegramWasEnabled = !!cfg.enabled;
  const html = `
    <div class="comm-card">
      <h4>Telegram bot</h4>
      <p class="muted" style="margin-bottom:6px;">
        Post multipliers to a Telegram channel automatically. Stored locally in your browser.
        <br>Note: It only works while someone in the clan is online with this tool open.
      </p>
      <ol style="margin:6px 0 10px 18px;line-height:1.5;">
        <li>Create a bot with <strong>@BotFather</strong> and copy the <strong>Bot Token</strong>.</li>
        <li>Add the bot as <strong>admin</strong> to your channel.</li>
        <li>Get your <strong>Channel ID</strong> (usually starts with <code>-100</code>).</li>
      </ol>
      <div class="stats-filter-bar" style="gap:10px;">
        <label class="stats-filter-toggle">
          <input id="tgEnabled" type="checkbox" ${cfg.enabled ? 'checked' : ''}>
          Enable Telegram posting
        </label>
        <label class="stats-filter-toggle">
          <input id="tgWinnerEnabled" type="checkbox" ${cfg.winnerEnabled ? 'checked' : ''}>
          Post when my clan wins a round
        </label>
      </div>
      <div class="comm-card" style="margin-top:8px;">
        <h5>Example winner message</h5>
        <div class="muted" style="font-size:0.8rem;line-height:1.4;white-space:pre-wrap;">
[WIN] Clan won the round!
Clan: Kicking Blocks
Player: Korrel
Round: 934588
Multiplier: 2x
Round time: 00:17:04
Reward: 0.012345
        </div>
      </div>
      <div class="stats-filter-bar" style="gap:10px;margin-top:8px;">
        <div class="stats-filter-group" style="flex:1;">
          <span class="muted">Bot token</span>
          <input id="tgToken" class="stats-filter-input" type="password" placeholder="123456:ABC..." value="${commEscape(cfg.token)}">
        </div>
        <div class="stats-filter-group" style="flex:1;">
          <span class="muted">Channel ID</span>
          <input id="tgChatId" class="stats-filter-input" type="text" placeholder="-100xxxxxxxxxx" value="${commEscape(cfg.chatId)}">
        </div>
      </div>
      ${buildNotificationOptionsUi({
        idPrefix: 'tgNotify',
        dataAttr: 'tg-multiplier',
        state: {
          lowBoostEnabled: cfg.notifyLowBoostEnabled,
          lowBoostMaxCount: cfg.notifyLowBoostMaxCount,
          clanPowerEnabled: cfg.notifyClanPowerEnabled,
          multiplierEnabled: cfg.notifyMultiplierEnabled !== false,
          selectedMultipliers: Array.isArray(cfg.allowed) ? cfg.allowed : [1, 2, 4, 8, 16, 32, 64, '64+']
        },
        showReset: true
      })}
      <div class="comm-actions" style="margin-top:10px;">
        <button id="tgTestBtn" class="btn sm ghost" type="button">Send test message</button>
        <span id="tgStatus" class="muted" style="font-size:0.78rem;"></span>
      </div>
    </div>
  `;
  renderCards(container, [{ id: 'telegram-bot-card', htmlContent: html }]);

  const card = container.querySelector('[data-card-id="telegram-bot-card"]')?.parentElement || container;
  const tokenInput = container.querySelector('#tgToken');
  const chatInput = container.querySelector('#tgChatId');
  const enabledInput = container.querySelector('#tgEnabled');
  const winnerInput = container.querySelector('#tgWinnerEnabled');
  const statusEl = container.querySelector('#tgStatus');
  let notificationOptionsBinding = null;
  const updateConfig = () => {
    const next = commLoadTelegramConfig();
    next.token = tokenInput?.value || '';
    next.chatId = chatInput?.value || '';
    next.enabled = !!enabledInput?.checked;
    next.winnerEnabled = !!winnerInput?.checked;
    const optionsState = notificationOptionsBinding
      ? notificationOptionsBinding.readState()
      : {
          lowBoostEnabled: false,
          lowBoostMaxCount: 1,
          clanPowerEnabled: false,
          multiplierEnabled: true,
          selectedMultipliers: next.allowed
        };
    next.notifyLowBoostEnabled = !!optionsState.lowBoostEnabled;
    next.notifyLowBoostMaxCount = Math.max(0, Math.floor(Number(optionsState.lowBoostMaxCount || 0) || 0));
    next.notifyClanPowerEnabled = !!optionsState.clanPowerEnabled;
    next.notifyMultiplierEnabled = !!optionsState.multiplierEnabled;
    next.allowed = commNormalizeNotificationOptionMultipliers(
      optionsState.selectedMultipliers,
      [1, 2, 4, 8, 16, 32, 64, '64+']
    );
    const becameEnabled = !telegramWasEnabled && !!next.enabled;
    telegramWasEnabled = !!next.enabled;
    commSaveTelegramConfig(next);
    if (becameEnabled) {
      boostHelperActiveRuleIds = new Set();
      refreshBoostHelperPopup();
    }
  };
  tokenInput?.addEventListener('input', updateConfig);
  chatInput?.addEventListener('input', updateConfig);
  enabledInput?.addEventListener('change', updateConfig);
  winnerInput?.addEventListener('change', updateConfig);
  notificationOptionsBinding = bindNotificationOptionsUi(container, {
    idPrefix: 'tgNotify',
    dataAttr: 'tg-multiplier',
    onChange: updateConfig,
    getResetState: () => ({
      lowBoostEnabled: false,
      lowBoostMaxCount: 1,
      clanPowerEnabled: false,
      multiplierEnabled: false,
      selectedMultipliers: [1, 2, 4, 8, 16, 32, 64, '64+']
    })
  });

  const testBtn = container.querySelector('#tgTestBtn');
  if (testBtn) {
    testBtn.addEventListener('click', async () => {
      updateConfig();
      if (statusEl) statusEl.textContent = 'Sending...';
      try {
        await commSendTelegramMessage('Test message from Miner Wars Simulator.');
        if (statusEl) statusEl.textContent = 'Sent.';
      } catch (err) {
        if (statusEl) statusEl.textContent = err?.message || 'Failed.';
      } finally {
        setTimeout(() => {
          if (statusEl) statusEl.textContent = '';
        }, 2000);
      }
    });
  }
}

function populateDiscordBot() {
  const container = document.getElementById('discordBotGrid');
  if (!container) return;
  const cfg = commLoadDiscordConfig();
  let discordWasEnabled = !!cfg.enabled;
  const ownerIdKnown = String(getHomeAuthUserId() || '').trim();
  const html = `
    <div class="comm-card">
      <h4>Discord bot</h4>
      <p class="muted" style="margin-bottom:6px;">
        Send multiplier and winner notifications to Discord while this tool is running.
      </p>
      <div class="stats-filter-bar" style="gap:10px;">
        <label class="stats-filter-toggle">
          <input id="dcEnabled" type="checkbox" ${cfg.enabled ? 'checked' : ''}>
          Enable Discord posting
        </label>
        <label class="stats-filter-toggle">
          <input id="dcWinnerEnabled" type="checkbox" ${cfg.winnerEnabled ? 'checked' : ''}>
          Post when my clan wins a round
        </label>
      </div>
      <div class="comm-card" style="margin-top:8px;">
        <h5>Setup Discord Bot Push</h5>
        <ol style="margin:6px 0 10px 18px;line-height:1.5;">
          <li>Add the bot to your server: <a href="${DISCORD_BOT_INVITE_URL}" target="_blank" rel="noopener noreferrer">Add Discord Bot</a></li>
          <li>Run <code>/link owner_id:&lt;your owner id&gt;</code> in Discord.</li>
          <li>Run <code>/setpushchannel</code> in the target channel.</li>
          <li>Run <code>/pushtoken</code> and copy the token.</li>
          <li>Paste the Push token below.</li>
        </ol>
      </div>
      <div class="stats-filter-bar" style="gap:10px;margin-top:8px;flex-wrap:wrap;">
        <div class="stats-filter-group" style="flex:1;min-width:220px;">
          <span class="muted">Owner ID (auto)</span>
          <input id="dcOwnerId" class="stats-filter-input" type="text" readonly value="${commEscape(ownerIdKnown || '-')}">
        </div>
        <div class="stats-filter-group" style="flex:1;min-width:220px;">
          <span class="muted">Push token (from /pushtoken)</span>
          <input id="dcPushToken" class="stats-filter-input" type="password" placeholder="Paste push token" value="${commEscape(cfg.pushToken)}">
        </div>
      </div>
      ${buildNotificationOptionsUi({
        idPrefix: 'dcNotify',
        dataAttr: 'dc-multiplier',
        state: {
          lowBoostEnabled: cfg.notifyLowBoostEnabled,
          lowBoostMaxCount: cfg.notifyLowBoostMaxCount,
          clanPowerEnabled: cfg.notifyClanPowerEnabled,
          multiplierEnabled: cfg.notifyMultiplierEnabled !== false,
          selectedMultipliers: Array.isArray(cfg.allowed) ? cfg.allowed : [1, 2, 4, 8, 16, 32, 64, '64+']
        },
        showReset: true
      })}
      <div class="comm-card" style="margin-top:10px;">
        <h5>Bot status</h5>
        <div class="muted" style="font-size:0.8rem;line-height:1.45;">
          <div>Endpoint: <span id="dcEndpointStatus">Checking...</span></div>
          <div>Messages sent for this owner: <span id="dcSentCount">0</span></div>
          <div>Last sent: <span id="dcLastSentAt">-</span></div>
          <div>Last error: <span id="dcLastError">-</span></div>
        </div>
        <div style="margin-top:8px;">
          <div class="muted" style="font-size:0.78rem;margin-bottom:4px;">Last 5 pushed messages</div>
          <div id="dcLastMessages" class="muted" style="font-size:0.76rem;line-height:1.4;max-height:120px;overflow:auto;border:1px solid rgba(109,65,255,.35);border-radius:8px;padding:6px;">
            Loading...
          </div>
        </div>
        <div class="comm-actions" style="margin-top:8px;">
          <button id="dcRefreshStatusBtn" class="btn sm ghost" type="button">Refresh status</button>
        </div>
      </div>
      <div class="comm-actions" style="margin-top:10px;">
        <button id="dcTestBtn" class="btn sm ghost" type="button">Send test message</button>
        <span id="dcStatus" class="muted" style="font-size:0.78rem;"></span>
      </div>
    </div>
  `;
  renderCards(container, [{ id: 'discord-bot-card', htmlContent: html }]);

  const pushTokenInput = container.querySelector('#dcPushToken');
  const enabledInput = container.querySelector('#dcEnabled');
  const winnerInput = container.querySelector('#dcWinnerEnabled');
  const statusEl = container.querySelector('#dcStatus');
  const endpointStatusEl = container.querySelector('#dcEndpointStatus');
  const sentCountEl = container.querySelector('#dcSentCount');
  const lastSentEl = container.querySelector('#dcLastSentAt');
  const lastErrorEl = container.querySelector('#dcLastError');
  const lastMessagesEl = container.querySelector('#dcLastMessages');
  const refreshStatusBtn = container.querySelector('#dcRefreshStatusBtn');
  let notificationOptionsBinding = null;
  const formatStamp = (value) => {
    if (!value) return '-';
    const parsed = Date.parse(String(value));
    return Number.isFinite(parsed) ? new Date(parsed).toLocaleString() : String(value);
  };
  const refreshDiscordStatus = async () => {
    const ownerId = String(getHomeAuthUserId() || '').trim();
    const currentCfg = commLoadDiscordConfig();
    if (!ownerId) {
      if (endpointStatusEl) endpointStatusEl.textContent = 'No owner ID (login required)';
      if (sentCountEl) sentCountEl.textContent = '0';
      if (lastSentEl) lastSentEl.textContent = '-';
      if (lastErrorEl) lastErrorEl.textContent = '-';
      if (lastMessagesEl) lastMessagesEl.textContent = 'No owner ID (login required).';
      return;
    }
    if (!currentCfg.pushToken) {
      if (endpointStatusEl) endpointStatusEl.textContent = 'Push token missing';
      if (sentCountEl) sentCountEl.textContent = '0';
      if (lastSentEl) lastSentEl.textContent = '-';
      if (lastErrorEl) lastErrorEl.textContent = '-';
      if (lastMessagesEl) lastMessagesEl.textContent = 'Set your push token first.';
      return;
    }
    if (endpointStatusEl) endpointStatusEl.textContent = 'Checking...';
    if (lastMessagesEl) lastMessagesEl.textContent = 'Loading...';
    try {
      const status = await commFetchDiscordPushStatus(ownerId, currentCfg.pushToken);
      if (endpointStatusEl) endpointStatusEl.textContent = `Online (HTTP ${status.statusCode || 200})`;
      if (sentCountEl) sentCountEl.textContent = String(status.sentCount || 0);
      if (lastSentEl) lastSentEl.textContent = formatStamp(status.lastSentAt);
      if (lastErrorEl) {
        const errorLabel = status.lastError
          ? `${status.lastError} (${formatStamp(status.lastErrorAt)})`
          : '-';
        lastErrorEl.textContent = errorLabel;
      }
      if (lastMessagesEl) {
        const rows = Array.isArray(status.lastMessages) ? status.lastMessages : [];
        if (!rows.length) {
          lastMessagesEl.textContent = 'No pushed messages yet.';
        } else {
          lastMessagesEl.innerHTML = rows.map((row, index) => {
            const stamp = formatStamp(row.sentAt);
            const line = row.message || '(empty)';
            return `<div>${index + 1}. ${commEscape(stamp)} - ${commEscape(line)}</div>`;
          }).join('');
        }
      }
    } catch (err) {
      const statusCode = Number(err?.status);
      const statusLabel = Number.isFinite(statusCode) && statusCode > 0
        ? `HTTP ${statusCode}`
        : (err?.name === 'AbortError' ? 'Timeout' : (err?.message || 'Offline'));
      if (endpointStatusEl) endpointStatusEl.textContent = `Offline (${statusLabel})`;
      if (lastMessagesEl) {
        const errorText = err?.message ? String(err.message) : 'Could not load server status.';
        lastMessagesEl.textContent = errorText;
      }
    }
  };
  const updateConfig = () => {
    const next = commLoadDiscordConfig();
    next.pushToken = pushTokenInput?.value || '';
    next.enabled = !!enabledInput?.checked;
    next.winnerEnabled = !!winnerInput?.checked;
    const optionsState = notificationOptionsBinding
      ? notificationOptionsBinding.readState()
      : {
          lowBoostEnabled: false,
          lowBoostMaxCount: 1,
          clanPowerEnabled: false,
          multiplierEnabled: true,
          selectedMultipliers: next.allowed
        };
    next.notifyLowBoostEnabled = !!optionsState.lowBoostEnabled;
    next.notifyLowBoostMaxCount = Math.max(0, Math.floor(Number(optionsState.lowBoostMaxCount || 0) || 0));
    next.notifyClanPowerEnabled = !!optionsState.clanPowerEnabled;
    next.notifyMultiplierEnabled = !!optionsState.multiplierEnabled;
    next.allowed = commNormalizeNotificationOptionMultipliers(
      optionsState.selectedMultipliers,
      [1, 2, 4, 8, 16, 32, 64, '64+']
    );
    const becameEnabled = !discordWasEnabled && !!next.enabled;
    discordWasEnabled = !!next.enabled;
    commSaveDiscordConfig(next);
    if (becameEnabled) {
      boostHelperActiveRuleIds = new Set();
      refreshBoostHelperPopup();
    }
  };
  pushTokenInput?.addEventListener('input', updateConfig);
  enabledInput?.addEventListener('change', updateConfig);
  winnerInput?.addEventListener('change', updateConfig);
  notificationOptionsBinding = bindNotificationOptionsUi(container, {
    idPrefix: 'dcNotify',
    dataAttr: 'dc-multiplier',
    onChange: updateConfig,
    getResetState: () => ({
      lowBoostEnabled: false,
      lowBoostMaxCount: 1,
      clanPowerEnabled: false,
      multiplierEnabled: false,
      selectedMultipliers: [1, 2, 4, 8, 16, 32, 64, '64+']
    })
  });

  const testBtn = container.querySelector('#dcTestBtn');
  if (testBtn) {
    testBtn.addEventListener('click', async () => {
      updateConfig();
      if (statusEl) statusEl.textContent = 'Sending...';
      try {
        await commSendDiscordMessage('Test message from Miner Wars Simulator.');
        if (statusEl) statusEl.textContent = 'Sent.';
        await refreshDiscordStatus();
      } catch (err) {
        if (statusEl) statusEl.textContent = err?.message || 'Failed.';
        await refreshDiscordStatus();
      } finally {
        setTimeout(() => {
          if (statusEl) statusEl.textContent = '';
        }, 2000);
      }
    });
  }
  if (refreshStatusBtn) {
    refreshStatusBtn.addEventListener('click', () => {
      refreshDiscordStatus().catch(() => {});
    });
  }
  refreshDiscordStatus().catch(() => {});
}

function boostHelperBuildRuleSummary(rule) {
  if (isBoostHelperNoBoostRule(rule)) {
    const windowCfg = boostHelperGetNoBoostWindow(rule);
    const seconds = windowCfg.to;
    const fromLabel = Number.isFinite(windowCfg.from) ? `${windowCfg.from}s` : '0s';
    const maxBoostCount = boostHelperGetNoBoostMaxCount(rule);
    const label = Number.isFinite(seconds)
      ? (maxBoostCount > 0
        ? `In first ${seconds}s, my clan used <= ${maxBoostCount} boosts`
        : `When there are no boosts between ${fromLabel} and ${seconds}s`)
      : 'No boosts (time not set)';
    return label;
  }
  if (isBoostHelperTotalScoreRule(rule)) {
    const op = boostHelperGetTotalScoreOp(rule);
    const target = boostHelperGetTotalScoreValue(rule);
    const current = boostHelperGetCurrentTotalScore();
    const opLabel = op === 'lte' ? '<=' : '>=';
    const targetLabel = Number.isFinite(target) ? formatScoreValue(target) : '-';
    const currentLabel = Number.isFinite(current) ? formatScoreValue(current) : '-';
    const windowCfg = boostHelperGetTotalScoreWindow(rule);
    const fromLabel = Number.isFinite(windowCfg.from) ? `${windowCfg.from}m` : null;
    const toLabel = Number.isFinite(windowCfg.to) ? `${windowCfg.to}m` : null;
    const windowLabel = (fromLabel || toLabel)
      ? ` between ${fromLabel || '0m'}-${toLabel || 'inf'}`
      : '';
    return `Total score ${opLabel} ${targetLabel}${windowLabel} (now ${currentLabel})`;
  }
  if (isBoostHelperClanBoostRule(rule)) {
    const type = boostHelperGetClanBoostType(rule);
    const count = boostHelperGetClanBoostCount(rule);
    const typeLabel = type === 'any' ? 'any boost' : formatBoostDisplayName(type);
    return `When my clan uses ${count}x ${typeLabel}`;
  }
  if (isBoostHelperMultiplierRule(rule)) {
    const list = boostHelperGetMultiplierList(rule);
    const labelList = list.map((value) => (value === '64+' ? '64+' : `${value}x`)).join(', ');
    return `When multiplier is ${labelList || '-'}`;
  }
  const typeLabel = formatBoostDisplayName(rule?.boostType) || 'Boost';
  const minMult = Number.isFinite(rule?.minMultiplier) ? `${rule.minMultiplier}x+` : 'any multiplier';
  return `${typeLabel} when ${minMult}`;
}

function boostHelperGetRuleBuilderMode(rule) {
  if (isBoostHelperNoBoostRule(rule)) return 'no_boost';
  if (isBoostHelperTotalScoreRule(rule)) return 'score';
  if (isBoostHelperClanBoostRule(rule)) return 'clan_boost';
  if (isBoostHelperMultiplierRule(rule)) return 'multiplier';
  return 'no_boost';
}

function boostHelperGetRuleBadgeLabel(rule) {
  if (isBoostHelperNoBoostRule(rule)) return 'No boost';
  if (isBoostHelperTotalScoreRule(rule)) return 'Score';
  if (isBoostHelperClanBoostRule(rule)) return 'Clan boost';
  if (isBoostHelperMultiplierRule(rule)) return 'Multiplier';
  return 'Rule';
}

function boostHelperRenderRules(listEl, config, handlers = {}) {
  if (!listEl) return;
  const sourceRules = Array.isArray(config?.rules) ? config.rules : [];
  const rules = sourceRules.filter((rule) => boostHelperIsSupportedRule(rule));
  let mutated = false;
  if (sourceRules.length !== rules.length) {
    config.rules = rules;
    mutated = true;
  }
  rules.forEach((rule) => {
    if (!rule.id) {
      rule.id = crypto.randomUUID();
      mutated = true;
    }
  });
  if (mutated) saveBoostHelperConfig(config);
  if (!rules.length) {
    listEl.innerHTML = '<div class="muted">No rules yet. Add one below.</div>';
    return;
  }
  const canEdit = typeof handlers.onEdit === 'function';
  const readOnly = !!handlers.readOnly;
  const rows = rules.map((rule, index) => `
    <div class="boost-helper-rule" data-rule-id="${commEscape(rule.id || '')}" style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
      <div class="boost-helper-rule-text" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
        <span class="muted" style="font-size:0.72rem;border:1px solid rgba(109,65,255,.55);border-radius:999px;padding:2px 8px;">${commEscape(boostHelperGetRuleBadgeLabel(rule))}</span>
        <span style="opacity:.8;">${index + 1}.</span>
        <span>${commEscape(boostHelperBuildRuleSummary(rule))}</span>
      </div>
      ${readOnly ? '' : `<div style="display:flex;gap:6px;flex-wrap:wrap;">
        ${canEdit ? `<button class="btn sm ghost boost-helper-edit" type="button" data-edit-id="${commEscape(rule.id || '')}">Edit</button>` : ''}
        <button class="btn sm ghost boost-helper-duplicate" type="button" data-duplicate-id="${commEscape(rule.id || '')}">Duplicate</button>
        <button class="btn sm ghost boost-helper-remove" type="button" data-remove-id="${commEscape(rule.id || '')}">Delete</button>
      </div>`}
    </div>
  `).join('');
  listEl.innerHTML = `<div class="boost-helper-rule-group"><div class="boost-helper-rule-group-title">Active rules</div><div class="boost-helper-rule-group-body">${rows}</div></div>`;
  if (readOnly) return;
  listEl.querySelectorAll('.boost-helper-remove').forEach((btn) => {
    btn.addEventListener('click', () => {
      const id = btn.dataset.removeId;
      const next = loadBoostHelperConfig();
      next.rules = (next.rules || []).filter((r) => r.id !== id);
      saveBoostHelperConfig(next);
      boostHelperRenderRules(listEl, next, handlers);
      refreshBoostHelperPopup();
    });
  });
  listEl.querySelectorAll('.boost-helper-duplicate').forEach((btn) => {
    btn.addEventListener('click', () => {
      const id = btn.dataset.duplicateId;
      const source = rules.find((r) => r.id === id);
      if (!source) return;
      if (typeof handlers.onDuplicate === 'function') {
        handlers.onDuplicate(source);
        return;
      }
      const next = loadBoostHelperConfig();
      next.rules = Array.isArray(next.rules) ? next.rules : [];
      next.rules.push({ ...source, id: crypto.randomUUID() });
      saveBoostHelperConfig(next);
      boostHelperRenderRules(listEl, next, handlers);
      refreshBoostHelperPopup();
    });
  });
  listEl.querySelectorAll('.boost-helper-edit').forEach((btn) => {
    btn.addEventListener('click', () => {
      const id = btn.dataset.editId;
      const source = rules.find((r) => r.id === id);
      if (!source) return;
      if (typeof handlers.onEdit === 'function') {
        handlers.onEdit(source);
      }
    });
  });
}

function commFindGominingTabWithAuth() {
  return new Promise((resolve) => {
    if (WEBSITE_MODE || !HAS_CHROME_RUNTIME) {
      resolve(null);
      return;
    }
    if (!chrome?.tabs?.query) {
      resolve(null);
      return;
    }
    chrome.tabs.query({ url: COMM_GOMINING_URL_PATTERNS }, async (tabs) => {
      if (!Array.isArray(tabs) || !tabs.length) {
        resolve(null);
        return;
      }
      for (const tab of tabs) {
        if (!tab?.id) continue;
        const hasToken = await commTabHasAccessToken(tab.id);
        if (hasToken) {
          resolve(tab);
          return;
        }
      }
      resolve(null);
    });
  });
}

async function commLoadBoostHelperTemplate() {
  if (!commSession?.clanId) {
    throw new Error('Join a clan to load the template.');
  }
  const query = `select=id,${COMM_BOOST_HELPER_FIELD}&id=eq.${encodeURIComponent(commSession.clanId)}&limit=1`;
  let rows = null;
  try {
    rows = await commFetch(COMM_TABLE_CLANS, query);
  } catch (err) {
    if (String(err).includes(COMM_BOOST_HELPER_FIELD)) {
      throw new Error('Clan templates unavailable. Ask admin to add boost_helper_template column.');
    }
    throw err;
  }
  const raw = rows && rows[0] ? rows[0][COMM_BOOST_HELPER_FIELD] : null;
  if (!raw) {
    throw new Error('No clan template saved yet.');
  }
  let parsed = null;
  try {
    parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
  } catch (err) {
    throw new Error('Saved template is not valid JSON.');
  }
  if (!parsed || typeof parsed !== 'object') {
    throw new Error('Saved template is empty.');
  }
  return parsed;
}

async function commSaveBoostHelperTemplate(config) {
  if (!commSession?.clanId) {
    throw new Error('Join a clan to save the template.');
  }
  if (!commSession?.isOwner) {
    throw new Error('Only clan owners can save the template.');
  }
  const payload = JSON.stringify(config || {});
  const query = `id=eq.${encodeURIComponent(commSession.clanId)}`;
  try {
    await commPatch(COMM_TABLE_CLANS, query, { [COMM_BOOST_HELPER_FIELD]: payload });
  } catch (err) {
    if (String(err).includes(COMM_BOOST_HELPER_FIELD)) {
      throw new Error('Clan templates unavailable. Ask admin to add boost_helper_template column.');
    }
    throw err;
  }
}

async function commFetchBoostHelperTemplates() {
  if (!commSession?.clanId) {
    throw new Error('Join a clan to load templates.');
  }
  const query = `select=id,name,updated_at,template&clan_id=eq.${encodeURIComponent(commSession.clanId)}&order=updated_at.desc&limit=100`;
  try {
    const rows = await commFetch(COMM_TABLE_BOOST_HELPER_TEMPLATES, query);
    return Array.isArray(rows) ? rows : [];
  } catch (err) {
    if (String(err).includes(COMM_TABLE_BOOST_HELPER_TEMPLATES)) {
      throw new Error('Clan templates unavailable. Ask admin to add clan_boost_helper_templates table.');
    }
    throw err;
  }
}

async function commSaveBoostHelperTemplateByName(name, config) {
  if (!commSession?.clanId) {
    throw new Error('Join a clan to save the template.');
  }
  if (!commSession?.isOwner) {
    throw new Error('Only clan owners can save templates.');
  }
  const cleanName = String(name || '').trim();
  if (!cleanName) {
    throw new Error('Template name required.');
  }
  const payload = {
    clan_id: commSession.clanId,
    name: cleanName,
    template: config || {},
    updated_at: new Date().toISOString()
  };
  try {
    await commInsert(COMM_TABLE_BOOST_HELPER_TEMPLATES, [payload], {
      upsert: true,
      onConflict: 'clan_id,name'
    });
  } catch (err) {
    if (String(err).includes(COMM_TABLE_BOOST_HELPER_TEMPLATES)) {
      throw new Error('Clan templates unavailable. Ask admin to add clan_boost_helper_templates table.');
    }
    throw err;
  }
}

async function commDeleteBoostHelperTemplate(name) {
  if (!commSession?.clanId) {
    throw new Error('Join a clan to delete templates.');
  }
  if (!commSession?.isOwner) {
    throw new Error('Only clan owners can delete templates.');
  }
  const cleanName = String(name || '').trim();
  if (!cleanName) {
    throw new Error('Template name required.');
  }
  const query = `clan_id=eq.${encodeURIComponent(commSession.clanId)}&name=eq.${encodeURIComponent(cleanName)}`;
  await commDelete(COMM_TABLE_BOOST_HELPER_TEMPLATES, query);
}

function populateBoostHelper() {
  const container = document.getElementById('boostHelperGrid');
  if (!container) return;
  const config = loadBoostHelperConfig();
  const existingRules = Array.isArray(config.rules) ? config.rules.filter((rule) => boostHelperIsSupportedRule(rule)) : [];
  const lowBoostRule = existingRules.find((rule) => isBoostHelperNoBoostRule(rule)) || null;
  const clanPowerRule = existingRules.find((rule) =>
    isBoostHelperClanBoostRule(rule) && boostHelperGetClanBoostType(rule) === 'clanpowerup'
  ) || null;
  const multiplierRule = existingRules.find((rule) => isBoostHelperMultiplierRule(rule)) || null;
  const lowBoostEnabled = !!lowBoostRule;
  const clanPowerEnabled = !!clanPowerRule;
  const lowBoostMaxCount = lowBoostRule ? boostHelperGetNoBoostMaxCount(lowBoostRule) : 1;
  const existingMultipliers = boostHelperGetMultiplierList(multiplierRule);
  const multiplierEnabled = existingMultipliers.length > 0;
  const html = `
    <div class="boost-helper-panel">
      <div class="title">Notifications</div>
      <p class="muted">Build a boost script for the popup. The helper will show which boost to use based on multiplier.</p>

      <div class="stats-filter-bar" style="gap:12px;margin-top:8px;">
        <label class="stats-filter-toggle">
          <input id="boostHelperSoundToggle" type="checkbox" ${config.soundEnabled ? 'checked' : ''}>
          Play sound when rule activates
        </label>
        <label class="stats-filter-toggle" style="gap:6px;">
          <span class="muted">Volume</span>
          <input id="boostHelperSoundVolume" type="range" min="0" max="100" value="${Math.round((Number.isFinite(config.soundVolume) ? config.soundVolume : 0.6) * 100)}">
        </label>
        <button id="boostHelperSoundTest" class="btn sm ghost" type="button">Test sound</button>
        <span id="boostHelperSoundStatus" class="muted" style="font-size:0.75rem;"></span>
      </div>

      ${buildNotificationOptionsUi({
        idPrefix: 'boostHelper',
        dataAttr: 'mw-multiplier',
        state: {
          lowBoostEnabled,
          lowBoostMaxCount,
          clanPowerEnabled,
          multiplierEnabled,
          selectedMultipliers: existingMultipliers.length ? existingMultipliers : [4, 8, 16, 32, 64, '64+']
        },
        showReset: true
      })}
        <div id="boostHelperRulesList" class="boost-helper-rules"></div>

      <div class="comm-card" style="margin-top:12px;">
        <h4>Clan templates</h4>
        <div id="boostHelperTemplateStatus" class="muted" style="font-size:0.78rem;">Templates not loaded.</div>
        <div class="stats-filter-bar" style="gap:10px;margin-top:8px;">
          <div class="stats-filter-group" style="min-width:180px;">
            <span class="muted">Template</span>
            <select id="boostHelperTemplateSelect" class="stats-filter-select">
              <option value="">Select template...</option>
            </select>
          </div>
          <div class="stats-filter-group" style="flex:1;min-width:160px;">
            <span class="muted">Template name</span>
            <input id="boostHelperTemplateName" class="stats-filter-input" type="text" placeholder="e.g. My setup">
          </div>
        </div>
        <div class="comm-actions" style="margin-top:8px;">
          <button id="boostHelperSaveTemplate" class="btn sm" type="button">Save template</button>
          <button id="boostHelperLoadTemplate" class="btn sm ghost" type="button">Load template</button>
          <button id="boostHelperDeleteTemplate" class="btn sm ghost" type="button">Delete template</button>
        </div>
      </div>
    </div>
  `;
  const card = createCard({ title: 'Notifications', htmlContent: html, id: 'boost-helper-card' });
  container.innerHTML = '';
  container.appendChild(card);

  const rulesList = card.querySelector('#boostHelperRulesList');
  boostHelperRenderRules(rulesList, config, { readOnly: true });

  const soundToggle = card.querySelector('#boostHelperSoundToggle');
  if (soundToggle) {
    soundToggle.addEventListener('change', () => {
      const next = loadBoostHelperConfig();
      next.soundEnabled = !!soundToggle.checked;
      saveBoostHelperConfig(next);
      if (soundToggle.checked) {
        boostHelperUnlockAudio().catch(() => {});
      }
    });
  }
  const soundVolumeInput = card.querySelector('#boostHelperSoundVolume');
  if (soundVolumeInput) {
    soundVolumeInput.addEventListener('input', () => {
      const next = loadBoostHelperConfig();
      const value = Number(soundVolumeInput.value);
      next.soundVolume = Number.isFinite(value) ? Math.min(1, Math.max(0, value / 100)) : 0.6;
      saveBoostHelperConfig(next);
    });
  }
  const soundTestBtn = card.querySelector('#boostHelperSoundTest');
  if (soundTestBtn) {
    soundTestBtn.addEventListener('click', async () => {
      const status = card.querySelector('#boostHelperSoundStatus');
      if (status) status.textContent = 'Playing...';
      try {
        await boostHelperUnlockAudio();
        await playBoostHelperSteamSound(status);
      } catch (err) {
        if (status) status.textContent = 'Sound failed.';
      } finally {
        if (status) {
          setTimeout(() => {
            status.textContent = '';
          }, 1500);
        }
      }
    });
  }

  let notificationOptionsBinding = null;
  const saveSimpleNotificationRules = () => {
    const next = loadBoostHelperConfig();
    const rules = [];
    const optionsState = notificationOptionsBinding
      ? notificationOptionsBinding.readState()
      : {
          lowBoostEnabled: false,
          lowBoostMaxCount: 1,
          clanPowerEnabled: false,
          multiplierEnabled: false,
          selectedMultipliers: []
        };
    if (optionsState.lowBoostEnabled) {
      rules.push({
        id: crypto.randomUUID(),
        boostType: 'no_boosts',
        fromSeconds: 0,
        maxSeconds: 90,
        maxBoostCount: optionsState.lowBoostMaxCount,
        note: ''
      });
    }
    if (optionsState.clanPowerEnabled) {
      rules.push({
        id: crypto.randomUUID(),
        boostType: 'my_clan_boost',
        clanBoostType: 'clanpowerup',
        minCount: 1,
        note: ''
      });
    }
    if (optionsState.multiplierEnabled) {
      const selected = optionsState.selectedMultipliers;
      if (selected.length) {
        rules.push({
          id: crypto.randomUUID(),
          boostType: 'multiplier_alert',
          multipliers: selected,
          note: ''
        });
      }
    }
    next.rules = rules;
    saveBoostHelperConfig(next);
    boostHelperRenderRules(rulesList, next, { readOnly: true });
    refreshBoostHelperPopup();
  };
  notificationOptionsBinding = bindNotificationOptionsUi(card, {
    idPrefix: 'boostHelper',
    dataAttr: 'mw-multiplier',
    onChange: saveSimpleNotificationRules,
    getResetState: () => ({
      lowBoostEnabled: false,
      lowBoostMaxCount: 1,
      clanPowerEnabled: false,
      multiplierEnabled: false,
      selectedMultipliers: []
    })
  });
  saveSimpleNotificationRules();

  const templateStatus = card.querySelector('#boostHelperTemplateStatus');
  const templateSelect = card.querySelector('#boostHelperTemplateSelect');
  const templateNameInput = card.querySelector('#boostHelperTemplateName');
  const saveTemplateBtn = card.querySelector('#boostHelperSaveTemplate');
  const loadTemplateBtn = card.querySelector('#boostHelperLoadTemplate');
  const deleteTemplateBtn = card.querySelector('#boostHelperDeleteTemplate');
  let templateCache = [];

  const refreshTemplateList = async () => {
    if (!templateSelect) return;
    if (!commSession?.clanId) {
      if (templateStatus) templateStatus.textContent = 'Join a clan to load templates.';
      templateSelect.innerHTML = '<option value="">Select template...</option>';
      return;
    }
    try {
      if (templateStatus) templateStatus.textContent = 'Loading templates...';
      templateCache = await commFetchBoostHelperTemplates();
      const options = templateCache
        .map((row) => `<option value="${commEscape(row.name || '')}">${commEscape(row.name || '')}</option>`)
        .join('');
      templateSelect.innerHTML = `<option value="">Select template...</option>${options}`;
      if (templateStatus) templateStatus.textContent = templateCache.length ? 'Templates loaded.' : 'No clan templates yet.';
    } catch (err) {
      if (templateStatus) templateStatus.textContent = err?.message || 'Failed to load templates.';
      templateSelect.innerHTML = '<option value="">Select template...</option>';
    }
  };

  if (saveTemplateBtn) {
    saveTemplateBtn.addEventListener('click', async () => {
      try {
        saveTemplateBtn.disabled = true;
        if (templateStatus) templateStatus.textContent = 'Saving template...';
        const current = loadBoostHelperConfig();
        const name = templateNameInput?.value || templateSelect?.value;
        await commSaveBoostHelperTemplateByName(name, current);
        if (templateStatus) templateStatus.textContent = 'Template saved.';
        await refreshTemplateList();
        if (templateSelect && name) templateSelect.value = name;
      } catch (err) {
        if (templateStatus) templateStatus.textContent = err?.message || 'Failed to save template.';
      } finally {
        saveTemplateBtn.disabled = false;
      }
    });
  }
  if (loadTemplateBtn) {
    loadTemplateBtn.addEventListener('click', async () => {
      try {
        loadTemplateBtn.disabled = true;
        if (templateStatus) templateStatus.textContent = 'Loading template...';
        const selected = templateSelect?.value || templateNameInput?.value;
        if (!selected) throw new Error('Select a template first.');
        const row = templateCache.find((t) => t.name === selected);
        if (!row) throw new Error('Template not found.');
        const template = row.template || {};
        const defaults = getBoostHelperDefaults();
        const next = {
          ...defaults,
          ...template,
          phases: {
            ...defaults.phases,
            ...(template?.phases || {})
          },
          rules: Array.isArray(template?.rules) ? template.rules : []
        };
        saveBoostHelperConfig(next);
        refreshBoostHelperPopup();
        populateBoostHelper();
        if (templateStatus) templateStatus.textContent = 'Template loaded.';
        if (templateNameInput && selected) templateNameInput.value = selected;
      } catch (err) {
        if (templateStatus) templateStatus.textContent = err?.message || 'Failed to load template.';
      } finally {
        loadTemplateBtn.disabled = false;
      }
    });
  }
  if (deleteTemplateBtn) {
    deleteTemplateBtn.addEventListener('click', async () => {
      try {
        deleteTemplateBtn.disabled = true;
        if (templateStatus) templateStatus.textContent = 'Deleting template...';
        const selected = templateSelect?.value || templateNameInput?.value;
        if (!selected) throw new Error('Select a template first.');
        await commDeleteBoostHelperTemplate(selected);
        if (templateStatus) templateStatus.textContent = 'Template deleted.';
        if (templateNameInput) templateNameInput.value = '';
        await refreshTemplateList();
      } catch (err) {
        if (templateStatus) templateStatus.textContent = err?.message || 'Failed to delete template.';
      } finally {
        deleteTemplateBtn.disabled = false;
      }
    });
  }
  if (saveTemplateBtn && !commSession?.isOwner) {
    saveTemplateBtn.disabled = true;
    if (templateStatus) {
      templateStatus.textContent = commSession?.clanId
        ? 'Only clan owners can save templates.'
        : 'Join a clan to access templates.';
    }
  }
  if (deleteTemplateBtn && !commSession?.isOwner) {
    deleteTemplateBtn.disabled = true;
  }
  if (templateSelect && templateNameInput) {
    templateSelect.addEventListener('change', () => {
      if (templateSelect.value) templateNameInput.value = templateSelect.value;
    });
  }
  refreshTemplateList().catch(() => {});
}

function populateStats() {
  const container = document.getElementById('statsGrid');
  if (!container) return;
  const storedLeague = localStorage.getItem(STATS_LEAGUE_FILTER_KEY) || '';
  const currentLeague = commGetKorrelActiveLeagueId();
  const defaultLeague = storedLeague || (Number.isFinite(currentLeague) ? String(Math.trunc(currentLeague)) : '');
  const leagueOptions = buildClanSelectionLeagueFilterOptions(defaultLeague);
  const effectivenessHtml = `
    <div class="stats-filter-bar">
      <div id="statsWinCount" class="muted">Rounds used: -</div>
      <div class="stats-filter-group">
        <span class="muted">Multiplier</span>
        <select id="statsMultiplierFilter" class="stats-filter-select">
          <option value="all">All</option>
          <option value="1">1x</option>
          <option value="2">2x</option>
          <option value="4">4x</option>
          <option value="8">8x</option>
          <option value="16">16x</option>
          <option value="32">32x</option>
          <option value="64">64x</option>
          <option value="128">128x</option>
          <option value="256">256x</option>
        </select>
      </div>
      <div class="stats-filter-group">
        <span class="muted">Cycle</span>
        <input id="statsCycleFilterInput" class="stats-filter-input" type="number" min="1" placeholder="Current">
        <label class="stats-filter-toggle">
          <input id="statsCycleAllToggle" type="checkbox">
          All
        </label>
      </div>
      <div class="stats-filter-group">
        <span class="muted">League</span>
        <select id="statsLeagueFilterInput" class="stats-filter-select">
          ${leagueOptions}
        </select>
      </div>
    </div>
    <div id="statsNoBoostSummary" class="muted" style="margin-bottom:6px;">No-boost wins: -</div>
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">User percentage vs. wins</div>
      </div>
      <div id="statsUserWinStatus" class="muted">Loading user win stats...</div>
      <div id="statsUserWinTable" class="stats-win-table-wrap"></div>
    </div>
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Clan percentage vs. wins</div>
      </div>
      <div id="statsClanWinStatus" class="muted">Loading clan win stats...</div>
      <div id="statsClanWinTable" class="stats-win-table-wrap"></div>
    </div>
  `;
  const html = `
    <div class="stats-tab-panel" data-stats-panel="effectiveness">
      ${effectivenessHtml}
    </div>
    <div class="stats-tab-panel" data-stats-panel="profitability">
      ${buildProfitabilityPanelHtml()}
    </div>
    <div class="stats-tab-panel" data-stats-panel="history">
      <div class="card-grid" id="statsRoundHistoryGrid"></div>
    </div>
    <div class="stats-tab-panel" data-stats-panel="clan-selection">
      <div class="card-grid" id="clanSelectionGrid"></div>
    </div>
  `;
  renderCards(container, [{ title: 'Statistics', htmlContent: html, id: 'stats-dashboard-card' }]);
  const card = container.querySelector('[data-card-id="stats-dashboard-card"]');
  if (card) card.classList.add('stats-tabs-card');
  const filterSelect = container.querySelector('#statsMultiplierFilter');
  const cycleInput = container.querySelector('#statsCycleFilterInput');
  const cycleAllToggle = container.querySelector('#statsCycleAllToggle');
  const leagueInput = container.querySelector('#statsLeagueFilterInput');
  if (filterSelect && !filterSelect.dataset.bound) {
    const stored = localStorage.getItem(STATS_MULTIPLIER_FILTER_KEY);
    if (stored) filterSelect.value = stored;
    filterSelect.addEventListener('change', () => {
      localStorage.setItem(STATS_MULTIPLIER_FILTER_KEY, filterSelect.value);
      statsWinTablesLoaded = false;
      loadStatsWinTables({ force: true }).catch(() => {});
    });
    filterSelect.dataset.bound = '1';
  }
  if (cycleInput && !cycleInput.dataset.bound) {
    const storedCycle = localStorage.getItem(STATS_CYCLE_FILTER_KEY) || '';
    if (storedCycle) cycleInput.value = storedCycle;
    cycleInput.addEventListener('change', () => {
      localStorage.setItem(STATS_CYCLE_FILTER_KEY, cycleInput.value);
      statsWinTablesLoaded = false;
      loadStatsWinTables({ force: true }).catch(() => {});
    });
    cycleInput.dataset.bound = '1';
  }
  if (cycleAllToggle && !cycleAllToggle.dataset.bound) {
    const storedAll = localStorage.getItem(STATS_CYCLE_ALL_KEY) === '1';
    cycleAllToggle.checked = storedAll;
    cycleAllToggle.addEventListener('change', () => {
      localStorage.setItem(STATS_CYCLE_ALL_KEY, cycleAllToggle.checked ? '1' : '0');
      statsWinTablesLoaded = false;
      loadStatsWinTables({ force: true }).catch(() => {});
    });
    cycleAllToggle.dataset.bound = '1';
  }
  if (leagueInput && !leagueInput.dataset.bound) {
    if (storedLeague) {
      leagueInput.value = storedLeague;
    } else if (Number.isFinite(currentLeague)) {
      leagueInput.value = String(Math.trunc(currentLeague));
    }
    leagueInput.addEventListener('change', () => {
      localStorage.setItem(STATS_LEAGUE_FILTER_KEY, leagueInput.value);
      statsWinTablesLoaded = false;
      loadStatsWinTables({ force: true }).catch(() => {});
    });
    leagueInput.dataset.bound = '1';
  }
  statsWinTablesLoaded = false;
  bindProfitabilityPanel(container);
  if (card) initStatsTabs(card);
}

function buildProfitabilityPanelHtml() {
  const storedLeague = localStorage.getItem(PROFIT_LEAGUE_FILTER_KEY) || '';
  const leagueOptions = buildClanSelectionLeagueFilterOptions(storedLeague);
  return `
    <div class="stats-filter-bar">
      <div id="profitabilityStatus" class="muted">Loading profitability...</div>
      <div class="stats-filter-group">
        <span class="muted">Cycle</span>
        <input id="profitCycleFilterInput" class="stats-filter-input" type="number" min="1" placeholder="Current">
        <label class="stats-filter-toggle">
          <input id="profitCycleAllToggle" type="checkbox">
          All
        </label>
      </div>
      <div class="stats-filter-group">
        <span class="muted">League</span>
        <select id="profitLeagueFilterInput" class="stats-filter-select">
          ${leagueOptions}
        </select>
      </div>
      <button id="profitabilityRefresh" class="btn sm ghost" type="button">Refresh</button>
    </div>
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Clan profitability</div>
      </div>
      <div id="profitClanStatus" class="muted">Loading clan profitability...</div>
      <div id="profitClanTable" class="stats-win-table-wrap"></div>
    </div>
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Round win profitability</div>
      </div>
      <div id="profitRoundStatus" class="muted">Loading round profitability...</div>
      <div id="profitRoundTable" class="stats-win-table-wrap"></div>
    </div>
  `;
}

function bindProfitabilityPanel(container) {
  if (!container) return;
  const cycleInput = container.querySelector('#profitCycleFilterInput');
  const cycleAllToggle = container.querySelector('#profitCycleAllToggle');
  const leagueSelect = container.querySelector('#profitLeagueFilterInput');
  const refreshBtn = container.querySelector('#profitabilityRefresh');
  const storedCycle = localStorage.getItem(PROFIT_CYCLE_FILTER_KEY) || '';
  const storedAll = localStorage.getItem(PROFIT_CYCLE_ALL_KEY) === '1';
  const storedLeague = localStorage.getItem(PROFIT_LEAGUE_FILTER_KEY) || '';
  if (cycleInput) cycleInput.value = storedCycle;
  if (cycleAllToggle) cycleAllToggle.checked = storedAll;
  if (leagueSelect) leagueSelect.value = storedLeague;
  const triggerReload = () => {
    profitabilityLoaded = false;
    loadProfitability({ force: true }).catch(() => {});
  };
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', triggerReload);
    refreshBtn.dataset.bound = '1';
  }
  if (cycleInput && !cycleInput.dataset.bound) {
    cycleInput.addEventListener('change', () => {
      localStorage.setItem(PROFIT_CYCLE_FILTER_KEY, cycleInput.value);
      triggerReload();
    });
    cycleInput.dataset.bound = '1';
  }
  if (cycleAllToggle && !cycleAllToggle.dataset.bound) {
    cycleAllToggle.addEventListener('change', () => {
      localStorage.setItem(PROFIT_CYCLE_ALL_KEY, cycleAllToggle.checked ? '1' : '0');
      triggerReload();
    });
    cycleAllToggle.dataset.bound = '1';
  }
  if (leagueSelect && !leagueSelect.dataset.bound) {
    leagueSelect.addEventListener('change', () => {
      localStorage.setItem(PROFIT_LEAGUE_FILTER_KEY, leagueSelect.value);
      triggerReload();
    });
    leagueSelect.dataset.bound = '1';
  }
}

function populateProfitability() {
  const container = document.getElementById('profitabilityGrid');
  if (!container) return;
  const html = buildProfitabilityPanelHtml();
  renderCards(container, [{ title: 'Profitability', htmlContent: html, id: 'profitability-card' }]);
  const card = container.querySelector('[data-card-id="profitability-card"]');
  if (card) card.classList.add('stats-tabs-card');
  bindProfitabilityPanel(container);
  loadProfitability({ force: true }).catch(() => {});
}

function commGetPersonalUsername() {
  const raw = localStorage.getItem(PERSONAL_ALIAS_KEY);
  const value = String(raw || '').trim();
  if (!value) return '';
  if (value.includes('@')) return '';
  return value;
}

function commGetStoredPersonalUserId() {
  const raw = localStorage.getItem(PERSONAL_USER_ID_KEY);
  const parsed = commParseNumber(raw);
  return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
}

function commStorePersonalIdentity({ userId = null, alias = null } = {}) {
  if (Number.isFinite(commParseNumber(userId))) {
    localStorage.setItem(PERSONAL_USER_ID_KEY, String(Math.trunc(commParseNumber(userId))));
  }
  if (alias) {
    localStorage.setItem(PERSONAL_ALIAS_KEY, String(alias).trim());
  }
}

async function commResolveGominingUserId({ force = false } = {}) {
  const existing = commGetStoredPersonalUserId();
  if (Number.isFinite(existing) && !force) return existing;
  let lastAt = 0;
  try {
    lastAt = commParseNumber(localStorage.getItem(PERSONAL_USER_RESOLVE_KEY));
  } catch (err) {
    lastAt = 0;
  }
  const now = Date.now();
  if (!force && lastAt && now - lastAt < PERSONAL_USER_RESOLVE_TTL_MS) {
    return commGetStoredPersonalUserId();
  }
  try {
    localStorage.setItem(PERSONAL_USER_RESOLVE_KEY, String(now));
  } catch (err) {
    // ignore storage errors
  }
  try {
    const tab = await commFindMiningWarsTabWithAuth();
    if (!tab?.id) return commGetStoredPersonalUserId();
    const data = await commFetchClanApiViaInjection(tab.id, { limit: 1, skip: 0, count: 0 });
    const myClanRaw = data?.myClan?.data || data?.myClan || {};
    const clanRaw = data?.clanData?.data || data?.clanData || {};
    const profile =
      myClanRaw?.myProfile ||
      clanRaw?.myProfile ||
      myClanRaw?.my_profile ||
      clanRaw?.my_profile ||
      myClanRaw?.profile ||
      clanRaw?.profile ||
      null;
    const idCandidates = [
      profile?.id,
      profile?.userId,
      profile?.user_id,
      profile?.user?.id,
      myClanRaw?.userId,
      myClanRaw?.user_id,
      clanRaw?.userId,
      clanRaw?.user_id
    ];
    let resolvedId = null;
    for (const value of idCandidates) {
      const parsed = commParseNumber(value);
      if (Number.isFinite(parsed)) {
        resolvedId = Math.trunc(parsed);
        break;
      }
    }
    const aliasCandidates = [
      profile?.alias,
      profile?.name,
      profile?.username,
      profile?.userName,
      profile?.login,
      myClanRaw?.userName,
      myClanRaw?.username,
      clanRaw?.userName,
      clanRaw?.username
    ];
    let resolvedAlias = null;
    for (const value of aliasCandidates) {
      if (value && String(value).trim()) {
        resolvedAlias = String(value).trim();
        break;
      }
    }
    if (Number.isFinite(resolvedId) || resolvedAlias) {
      commStorePersonalIdentity({ userId: resolvedId, alias: resolvedAlias });
    }
  } catch (err) {
    return commGetStoredPersonalUserId();
  }
  return commGetStoredPersonalUserId();
}

async function commCheckPersonalApiHealth() {
  if (personalApiHealthRunning) return false;
  const status = document.getElementById('personalApiStatus');
  personalApiHealthRunning = true;
  const start = Date.now();
  if (status) status.textContent = 'Checking API health...';
  try {
    const tab = await commFindMiningWarsTabWithAuth();
    if (!tab?.id) {
      if (status) status.textContent = 'No Mining Wars tab with auth token found.';
      return false;
    }
    const endpoint = 'https://api.gomining.com/api/nft-game/clan/get-my';
    const response = await commFetchGominingApiViaInjection(tab.id, endpoint, {}, { method: 'POST', skipLimit: true });
    const duration = Date.now() - start;
    if (!response?.ok) {
      if (status) status.textContent = `API health failed (${response?.status || 0}).`;
      return false;
    }
    if (status) status.textContent = `API health OK (${duration}ms).`;
    return true;
  } catch (err) {
    if (status) status.textContent = err?.message || 'API health check failed.';
    return false;
  } finally {
    personalApiHealthRunning = false;
  }
}

function commGetPersonalRewardsLastKey(userId) {
  const safeUser = String(userId || 'anon').replace(/[^a-z0-9:_-]/gi, '').slice(0, 64) || 'anon';
  return `${PERSONAL_REWARDS_LAST_FETCH_PREFIX}:${safeUser}`;
}

function commGetPersonalClanRewardsLastKey(userId) {
  const safeUser = String(userId || 'anon').replace(/[^a-z0-9:_-]/gi, '').slice(0, 64) || 'anon';
  return `${PERSONAL_REWARDS_CLAN_LAST_FETCH_PREFIX}:${safeUser}`;
}

function commExtractRewardsByUserArray(raw) {
  const inner = raw && typeof raw === 'object' && 'data' in raw ? raw.data : raw;
  if (Array.isArray(inner)) return inner;
  if (Array.isArray(inner?.data)) return inner.data;
  if (Array.isArray(inner?.array)) return inner.array;
  return [];
}

function commExtractRewardsCycleId(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const direct = [
    entry.cycleId,
    entry.cycle_id,
    entry.cycle
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  if (Number.isFinite(direct)) return Math.trunc(direct);
  const round = entry.round || entry.roundInfo || entry.round_data;
  const nested = [
    round?.cycleId,
    round?.cycle_id,
    round?.cycle
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  return Number.isFinite(nested) ? Math.trunc(nested) : null;
}

function commExtractRewardsLeagueId(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const direct = [
    entry.leagueId,
    entry.league_id,
    entry.league
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  if (Number.isFinite(direct)) return Math.trunc(direct);
  const round = entry.round || entry.roundInfo || entry.round_data;
  const nested = [
    round?.leagueId,
    round?.league_id,
    round?.league
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  return Number.isFinite(nested) ? Math.trunc(nested) : null;
}

function commExtractRewardsRoundId(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const direct = [
    entry.roundId,
    entry.round_id,
    entry.round,
    entry.blockId,
    entry.block_id,
    entry.blockNumber,
    entry.block_number
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  if (Number.isFinite(direct)) return Math.trunc(direct);
  const round = entry.round || entry.roundInfo || entry.round_data;
  const nested = [
    round?.id,
    round?.roundId,
    round?.round_id,
    round?.blockId,
    round?.block_id,
    round?.blockNumber,
    round?.block_number
  ].map(commParseNumber).find((val) => Number.isFinite(val));
  return Number.isFinite(nested) ? Math.trunc(nested) : null;
}

function commExtractRewardsAmount(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const directCandidates = [
    entry.gmtReward,
    entry.rewardGmt,
    entry.reward_gmt,
    entry.gmtIncome,
    entry.gmt_income,
    entry.gmt,
    entry.reward,
    entry.amount,
    entry.value,
    entry.rewardAmount,
    entry.reward_amount,
    entry.rewardValue,
    entry.reward_value
  ];
  for (const candidate of directCandidates) {
    const parsed = commParseNumber(candidate);
    if (Number.isFinite(parsed)) return parsed;
  }
  const rewardObj = entry.rewardInfo || entry.reward_data || entry.reward_details || entry.reward || null;
  if (rewardObj && typeof rewardObj === 'object') {
    const nestedCandidates = [
      rewardObj.gmt,
      rewardObj.amount,
      rewardObj.value,
      rewardObj.reward,
      rewardObj.gmtReward
    ];
    for (const candidate of nestedCandidates) {
      const parsed = commParseNumber(candidate);
      if (Number.isFinite(parsed)) return parsed;
    }
  }
  const keyMatches = (key) => /gmt|reward|income|payout|amount/i.test(key);
  const stack = [{ value: entry, path: [] }];
  while (stack.length) {
    const { value, path } = stack.pop();
    if (!value || typeof value !== 'object') continue;
    if (Array.isArray(value)) {
      value.forEach((item) => stack.push({ value: item, path }));
      continue;
    }
    for (const [key, val] of Object.entries(value)) {
      const nextPath = path.concat(key);
      if (keyMatches(key)) {
        const parsed = commParseNumber(val);
        if (Number.isFinite(parsed)) return parsed;
      }
      if (key === 'value' && path.some(keyMatches)) {
        const parsed = commParseNumber(val);
        if (Number.isFinite(parsed)) return parsed;
      }
      if (val && typeof val === 'object') {
        stack.push({ value: val, path: nextPath });
      }
    }
  }
  return null;
}

function commExtractRewardsBtc(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const directCandidates = [
    entry.btcValue,
    entry.btc_value,
    entry.btcReward,
    entry.rewardBtc,
    entry.reward_btc,
    entry.btcIncome,
    entry.btc_income,
    entry.btc,
    entry.rewardBtcValue
  ];
  for (const candidate of directCandidates) {
    const parsed = commParseNumber(candidate);
    if (Number.isFinite(parsed)) return parsed;
  }
  const rewardObj = entry.rewardInfo || entry.reward_data || entry.reward_details || entry.reward || null;
  if (rewardObj && typeof rewardObj === 'object') {
    const nestedCandidates = [
      rewardObj.btcValue,
      rewardObj.btc,
      rewardObj.btcReward,
      rewardObj.rewardBtc
    ];
    for (const candidate of nestedCandidates) {
      const parsed = commParseNumber(candidate);
      if (Number.isFinite(parsed)) return parsed;
    }
  }
  const keyMatches = (key) => /btc|bitcoin/i.test(key);
  const stack = [{ value: entry, path: [] }];
  while (stack.length) {
    const { value, path } = stack.pop();
    if (!value || typeof value !== 'object') continue;
    if (Array.isArray(value)) {
      value.forEach((item) => stack.push({ value: item, path }));
      continue;
    }
    for (const [key, val] of Object.entries(value)) {
      const nextPath = path.concat(key);
      if (keyMatches(key)) {
        const parsed = commParseNumber(val);
        if (Number.isFinite(parsed)) return parsed;
      }
      if (key === 'value' && path.some(keyMatches)) {
        const parsed = commParseNumber(val);
        if (Number.isFinite(parsed)) return parsed;
      }
      if (val && typeof val === 'object') {
        stack.push({ value: val, path: nextPath });
      }
    }
  }
  return null;
}

function commExtractRewardsType(entry) {
  if (!entry || typeof entry !== 'object') return '-';
  const raw = entry.type || entry.rewardType || entry.kind || entry.category || null;
  return raw ? String(raw) : '-';
}

function commExtractRewardsUserId(entries) {
  if (!Array.isArray(entries)) return null;
  for (const entry of entries) {
    const candidate = [
      entry?.userId,
      entry?.user_id,
      entry?.user?.id,
      entry?.user?.userId,
      entry?.userInfo?.id,
      entry?.user_info?.id,
      entry?.userProfile?.id,
      entry?.profile?.id,
      entry?.account?.id,
      entry?.rewardUserId,
      entry?.winnerUserId,
      entry?.userWinner?.id
    ];
    for (const value of candidate) {
      const parsed = commParseNumber(value);
      if (Number.isFinite(parsed)) return Math.trunc(parsed);
    }
  }
  return null;
}

function commExtractRewardsUserAlias(entries) {
  if (!Array.isArray(entries)) return null;
  for (const entry of entries) {
    const candidate = [
      entry?.alias,
      entry?.userName,
      entry?.username,
      entry?.userLogin,
      entry?.user_login,
      entry?.login,
      entry?.user_alias,
      entry?.user?.alias,
      entry?.user?.name,
      entry?.user?.username,
      entry?.user?.userName,
      entry?.user?.login,
      entry?.userInfo?.alias,
      entry?.userInfo?.name,
      entry?.userInfo?.username,
      entry?.userInfo?.login,
      entry?.user_info?.alias,
      entry?.user_info?.username,
      entry?.user_profile?.name,
      entry?.profile?.alias,
      entry?.profile?.name,
      entry?.profile?.username,
      entry?.account?.username,
      entry?.account?.name,
      entry?.userWinner?.alias
    ];
    for (const value of candidate) {
      if (value && String(value).trim()) return String(value).trim();
    }
  }
  return null;
}


function commLoadProfitSnapshotSettings() {
  const cost = commParseNumber(localStorage.getItem(PROFIT_SNAPSHOT_COST_KEY));
  const baseTh = commParseNumber(localStorage.getItem(PROFIT_SNAPSHOT_BASE_TH_KEY));
  const userTh = commParseNumber(localStorage.getItem(PROFIT_SNAPSHOT_USER_TH_KEY));
  return {
    cost: Number.isFinite(cost) ? cost : 50,
    baseTh: Number.isFinite(baseTh) ? baseTh : 220,
    userTh: Number.isFinite(userTh) ? userTh : null
  };
}

function commSaveProfitSnapshotSettings(settings) {
  if (!settings || typeof settings !== 'object') return;
  if (Number.isFinite(settings.cost)) {
    localStorage.setItem(PROFIT_SNAPSHOT_COST_KEY, String(settings.cost));
  }
  if (Number.isFinite(settings.baseTh)) {
    localStorage.setItem(PROFIT_SNAPSHOT_BASE_TH_KEY, String(settings.baseTh));
  }
  if (Number.isFinite(settings.userTh)) {
    localStorage.setItem(PROFIT_SNAPSHOT_USER_TH_KEY, String(settings.userTh));
  }
}

function commComputeCycleTotals(entries, amountFn) {
  const totals = new Map();
  (Array.isArray(entries) ? entries : []).forEach((entry) => {
    const cycleId = commExtractRewardsCycleId(entry);
    if (!Number.isFinite(cycleId)) return;
    const amount = amountFn(entry);
    if (!Number.isFinite(amount)) return;
    const row = totals.get(cycleId) || { total: 0, count: 0 };
    row.total += amount;
    row.count += 1;
    totals.set(cycleId, row);
  });
  const cycles = Array.from(totals.keys()).sort((a, b) => b - a);
  const grandTotal = cycles.reduce((sum, cycleId) => sum + (totals.get(cycleId)?.total || 0), 0);
  return { totals, cycles, grandTotal };
}

function commExtractRosterUserPower(row) {
  const direct = commParseNumber(row?.user_power ?? row?.power ?? row?.userPower);
  if (Number.isFinite(direct)) return direct;
  const rawPower = commParseNumber(row?.raw?.power ?? row?.raw?.userPower ?? row?.raw?.user_power);
  return Number.isFinite(rawPower) ? rawPower : null;
}

function commExtractRosterClanInfo(row) {
  if (!row) return {};
  const clanName = row?.clan_name ?? row?.raw?.name ?? row?.raw?.clanName ?? null;
  const clanPower = commParseNumber(row?.clan_power ?? row?.raw?.power ?? row?.raw?.clanPower);
  const usersCount = commParseNumber(row?.users_count ?? row?.raw?.usersCount ?? row?.raw?.membersCount);
  const gmtIncome = commParseNumber(row?.raw?.gmtIncome ?? row?.raw?.gmt_income ?? row?.gmt_income);
  return {
    clanName,
    clanPower: Number.isFinite(clanPower) ? clanPower : null,
    usersCount: Number.isFinite(usersCount) ? Math.trunc(usersCount) : null,
    gmtIncome: Number.isFinite(gmtIncome) ? gmtIncome : null
  };
}


async function commFetchRewardsBoostCostMap(entries, scope = 'user') {
  const map = new Map();
  const ids = Array.from(new Set(
    (Array.isArray(entries) ? entries : [])
      .map(commExtractRewardsRoundId)
      .filter((val) => Number.isFinite(val))
      .map((val) => Math.trunc(val))
  ));
  if (!ids.length) return map;
  const chunks = commChunkArray(ids, 50);
  for (const chunk of chunks) {
    const list = chunk.join(',');
    const queryParts = [
      'select=api_round_id,clan_winner,user_winner',
      `api_round_id=in.(${list})`
    ];
    const rows = await commFetch(COMM_TABLE_ROUND_CYCLE_API, queryParts.join('&'));
    (Array.isArray(rows) ? rows : []).forEach((row) => {
      const roundId = commParseNumber(row?.api_round_id);
      if (!Number.isFinite(roundId)) return;
      const winnerField = scope === 'clan' ? row?.clan_winner : row?.user_winner;
      const usedAbilities = commExtractUsedAbilitiesFromWinner(winnerField);
      const cost = estimateBoostCost(usedAbilities);
      map.set(Math.trunc(roundId), Number.isFinite(cost) ? cost : 0);
    });
  }
  return map;
}

async function commGenerateProfitabilitySnapshot() {
  const status = document.getElementById('profitSnapshotStatus');
  const output = document.getElementById('profitSnapshotOutput');
  if (!status || !output) return;
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) {
    status.textContent = 'Sign in to generate a snapshot.';
    output.innerHTML = '';
    return;
  }
  status.textContent = 'Generating snapshot...';
  output.innerHTML = '';
  try {
    const query = `user_id=eq.${encodeURIComponent(identity.userId)}&limit=1`;
    const [userRows, clanRows] = await Promise.all([
      commFetch(COMM_TABLE_USER_REWARDS, query),
      commFetch(COMM_TABLE_USER_REWARDS_CLAN, query)
    ]);
    const userRow = Array.isArray(userRows) && userRows.length ? userRows[0] : null;
    const clanRow = Array.isArray(clanRows) && clanRows.length ? clanRows[0] : null;
    const userEntries = commExtractRewardsByUserArray(userRow?.raw);
    const clanEntries = commExtractRewardsByUserArray(clanRow?.raw);

    if (!userEntries.length && !clanEntries.length) {
      status.textContent = 'No rewards stored yet. Use Personal refresh to fetch data.';
      return;
    }

    const userTotals = commComputeCycleTotals(userEntries, commExtractRewardsAmount);
    const clanTotals = commComputeCycleTotals(clanEntries, commExtractRewardsBtc);
    const cyclesCount = userTotals.cycles.length || 0;
    const avgGmtPerWeek = cyclesCount ? userTotals.grandTotal / cyclesCount : null;
    let gominingUserId = commExtractRewardsUserId(userEntries)
      ?? commExtractRewardsUserId(clanEntries)
      ?? commGetStoredPersonalUserId();
    if (!Number.isFinite(gominingUserId)) {
      gominingUserId = await commResolveGominingUserId();
    }
    let userPower = null;
    let clanId = null;
    if (Number.isFinite(gominingUserId)) {
      const rosterRows = await commFetch(
        COMM_TABLE_CLAN_ROSTER_MEMBERS,
        `user_id=eq.${encodeURIComponent(Math.trunc(gominingUserId))}&order=fetched_at.desc&limit=1`
      );
      const rosterRow = Array.isArray(rosterRows) && rosterRows.length ? rosterRows[0] : null;
      if (rosterRow) {
        userPower = commExtractRosterUserPower(rosterRow);
        const clanParsed = commParseNumber(rosterRow?.clan_id ?? rosterRow?.raw?.clanId);
        if (Number.isFinite(clanParsed)) clanId = Math.trunc(clanParsed);
      }
    }

    let userBoostCostTotal = null;
    let userBoostCostEntries = 0;
    try {
      const userCostMap = await commFetchRewardsBoostCostMap(userEntries, 'user');
      let totalCost = 0;
      (Array.isArray(userEntries) ? userEntries : []).forEach((entry) => {
        const roundId = commExtractRewardsRoundId(entry);
        if (!Number.isFinite(roundId)) return;
        if (!userCostMap.has(Math.trunc(roundId))) return;
        const cost = commParseNumber(userCostMap.get(Math.trunc(roundId)));
        if (!Number.isFinite(cost)) return;
        totalCost += cost;
        userBoostCostEntries += 1;
      });
      userBoostCostTotal = totalCost;
    } catch (err) {
      userBoostCostTotal = null;
    }

    let clanInfo = {};
    if (Number.isFinite(clanId)) {
      const clanInfoRows = await commFetch(
        COMM_TABLE_CLAN_ROSTER_CLANS,
        `clan_id=eq.${encodeURIComponent(clanId)}&order=fetched_at.desc&limit=1`
      );
      const infoRow = Array.isArray(clanInfoRows) && clanInfoRows.length ? clanInfoRows[0] : null;
      if (infoRow) clanInfo = commExtractRosterClanInfo(infoRow);
    }

    const settings = commLoadProfitSnapshotSettings();
    const manualUserTh = settings.userTh;
    const effectiveUserTh = Number.isFinite(manualUserTh) ? manualUserTh : userPower;
    const baseCost = Number.isFinite(settings.cost) ? settings.cost : 50;
    const baseTh = Number.isFinite(settings.baseTh) && settings.baseTh > 0 ? settings.baseTh : 220;
    const estimatedMaintenance = Number.isFinite(effectiveUserTh)
      ? (baseCost / baseTh) * effectiveUserTh
      : null;
    const boostCostSpan = cyclesCount || null;
    const netGmt = Number.isFinite(avgGmtPerWeek) && Number.isFinite(estimatedMaintenance)
      ? avgGmtPerWeek - estimatedMaintenance
      : null;
    const avgBoostCostPerWeek = Number.isFinite(userBoostCostTotal) && boostCostSpan
      ? userBoostCostTotal / boostCostSpan
      : null;
    const netAfterBoosts = Number.isFinite(avgGmtPerWeek) && Number.isFinite(estimatedMaintenance) && Number.isFinite(avgBoostCostPerWeek)
      ? avgGmtPerWeek - estimatedMaintenance - avgBoostCostPerWeek
      : null;
    const boostNetTotal = Number.isFinite(userTotals.grandTotal) && Number.isFinite(userBoostCostTotal)
      ? userTotals.grandTotal - userBoostCostTotal
      : null;
    const boostRoi = Number.isFinite(userTotals.grandTotal) && Number.isFinite(userBoostCostTotal) && userBoostCostTotal > 0
      ? userTotals.grandTotal / userBoostCostTotal
      : null;
    const boostCoverage = userEntries.length ? `${userBoostCostEntries}/${userEntries.length}` : '-';

    const metricRows = [
      ['User TH', Number.isFinite(effectiveUserTh) ? formatDecimal(effectiveUserTh, 1) : '-'],
      ['Avg GMT/week', Number.isFinite(avgGmtPerWeek) ? formatDecimal(avgGmtPerWeek, 2) : '-'],
      ['Estimated maintenance/week', Number.isFinite(estimatedMaintenance) ? formatDecimal(estimatedMaintenance, 2) : '-'],
      ['Net GMT/week', Number.isFinite(netGmt) ? formatDecimal(netGmt, 2) : '-'],
      ['Boost cost/week (est.)', Number.isFinite(avgBoostCostPerWeek) ? formatDecimal(avgBoostCostPerWeek, 2) : '-'],
      ['Net after boosts', Number.isFinite(netAfterBoosts) ? formatDecimal(netAfterBoosts, 2) : '-'],
      ['Boost cost (est., total)', Number.isFinite(userBoostCostTotal) ? formatDecimal(userBoostCostTotal, 2) : '-'],
      ['Boost net (total)', Number.isFinite(boostNetTotal) ? formatDecimal(boostNetTotal, 2) : '-'],
      ['Boost ROI (reward/cost)', Number.isFinite(boostRoi) ? formatDecimal(boostRoi, 2) : '-'],
      ['Boost cost coverage', boostCoverage]
    ];

    if (clanInfo?.clanName) {
      metricRows.unshift(['Clan', commEscape(clanInfo.clanName)]);
    }
    if (Number.isFinite(clanInfo?.clanPower)) {
      metricRows.push(['Clan power', formatCount(clanInfo.clanPower)]);
    }
    if (Number.isFinite(clanInfo?.usersCount)) {
      metricRows.push(['Clan members', commEscape(clanInfo.usersCount)]);
    }
    if (Number.isFinite(clanInfo?.gmtIncome)) {
      metricRows.push(['Clan GMT income', formatDecimal(clanInfo.gmtIncome, 2)]);
    }

    const advice = [];
    if (!Number.isFinite(effectiveUserTh)) {
      advice.push('Enter your TH to estimate maintenance and net profit.');
    }
    if (Number.isFinite(netGmt)) {
      if (netGmt < 0) {
        advice.push('Net is negative. Consider lowering boosts or moving to a higher GMT/TH clan.');
      } else if (netGmt < 10) {
        advice.push('Net is small. Consider boosting only on high multipliers (4x+).');
      } else {
        advice.push('Net is positive. Maintain strategy and track if the margin stays stable.');
      }
    }
    if (Number.isFinite(netAfterBoosts)) {
      if (netAfterBoosts < 0) {
        advice.push('After boost costs, net is negative. Consider reducing boosts or timing them for high multipliers only.');
      } else if (netAfterBoosts < 10) {
        advice.push('After boost costs, net is thin. Track ROI per boost type and cut low performers.');
      }
    }
    if (userBoostCostEntries && userEntries.length && userBoostCostEntries < userEntries.length) {
      advice.push('Boost cost is estimated from rounds data; some rewards had no matching boosts.');
    }
    if (cyclesCount <= 1) {
      advice.push('Only one cycle of data. Trends will improve after more cycles.');
    }

    const boostCostNote = 'Boost cost uses used-abilities from round winners (estimated).';

    const metricBody = metricRows.map(([label, value]) => `
      <tr><td>${commEscape(label)}</td><td class="stats-num">${value}</td></tr>
    `).join('');
    const metricTable = `
      <div class="stats-win-table-wrap">
        <table class="stats-win-table">
          <thead><tr><th>Metric</th><th>Value</th></tr></thead>
          <tbody>${metricBody}</tbody>
        </table>
      </div>
    `;

    const userCycleTable = commBuildRewardsSummaryTable(userEntries, { unitLabel: 'GMT', decimals: 2 });
    const clanCycleTable = clanEntries.length
      ? commBuildRewardsSummaryTable(clanEntries, { unitLabel: 'BTC', amountFn: commExtractRewardsBtc, decimals: 7 })
      : '<div class="muted">No clan BTC rewards found.</div>';

    const adviceHtml = advice.length
      ? `<ul class="stats-win-notes">${advice.map((item) => `<li>${commEscape(item)}</li>`).join('')}</ul>`
      : '<div class="muted">No strategy notes yet.</div>';

    output.innerHTML = `
      <div class="stats-win-title" style="margin-top:6px;">Snapshot metrics</div>
      ${metricTable}
      <div class="muted" style="margin-top:6px;">${commEscape(boostCostNote)}</div>
      <div class="stats-win-title" style="margin-top:10px;">User rewards per cycle (GMT)</div>
      ${userCycleTable}
      <div class="stats-win-title" style="margin-top:10px;">Clan rewards per cycle (BTC)</div>
      ${clanCycleTable}
      <div class="stats-win-title" style="margin-top:10px;">Strategy notes</div>
      ${adviceHtml}
    `;
    status.textContent = `Snapshot updated. Cycles used: ${cyclesCount || 0}.`;
  } catch (err) {
    status.textContent = err?.message || 'Failed to build snapshot.';
  }
}

function commBuildRewardsSummaryTable(entries, { unitLabel = 'GMT', amountFn = commExtractRewardsAmount, decimals = 3 } = {}) {
  if (!Array.isArray(entries) || entries.length === 0) {
    return '<div class="muted">No rewards data stored yet.</div>';
  }
  const totals = new Map();
  entries.forEach((entry) => {
    const cycleId = commExtractRewardsCycleId(entry);
    if (!Number.isFinite(cycleId)) return;
    const amount = amountFn(entry);
    if (!Number.isFinite(amount)) return;
    const row = totals.get(cycleId) || { total: 0, count: 0 };
    row.total += amount;
    row.count += 1;
    totals.set(cycleId, row);
  });
  if (!totals.size) {
    return `<div class="muted">No ${commEscape(unitLabel)} values found in rewards.</div>`;
  }
  const sorted = Array.from(totals.entries()).sort((a, b) => b[0] - a[0]);
  const body = sorted.map(([cycleId, row]) => `
    <tr>
      <td>${commEscape(cycleId)}</td>
      <td class="stats-num">${commEscape(formatDecimal(row.total, decimals))}</td>
      <td class="stats-num">${commEscape(row.count)}</td>
    </tr>
  `).join('');
  return `
    <div class="stats-win-table-wrap">
      <table class="stats-win-table">
        <thead>
          <tr>
            <th>Cycle</th>
            <th>Total ${commEscape(unitLabel)}</th>
            <th>Entries</th>
          </tr>
        </thead>
        <tbody>${body}</tbody>
      </table>
    </div>
  `;
}

function commBuildRewardsDetailsTable(entries, { unitLabel = 'GMT', amountFn = commExtractRewardsAmount, decimals = 3 } = {}) {
  if (!Array.isArray(entries) || entries.length === 0) {
    return '<div class="muted">No rewards data stored yet.</div>';
  }
  const rows = entries.map((entry) => {
    const cycleId = commExtractRewardsCycleId(entry);
    const leagueId = commExtractRewardsLeagueId(entry);
    const roundId = commExtractRewardsRoundId(entry);
    const amount = amountFn(entry);
    const type = commExtractRewardsType(entry);
    const createdAt = entry.createdAt || entry.created_at || entry.updatedAt || entry.updated_at || null;
    const createdLabel = createdAt ? commEscape(formatDateTimeValue(createdAt)) : '-';
    return `
      <tr>
        <td>${commEscape(Number.isFinite(cycleId) ? cycleId : '-')}</td>
        <td>${commEscape(Number.isFinite(leagueId) ? commFormatLeagueDisplay(leagueId) : '-')}</td>
        <td>${commEscape(Number.isFinite(roundId) ? roundId : '-')}</td>
        <td>${commEscape(type)}</td>
        <td class="stats-num">${Number.isFinite(amount) ? commEscape(formatDecimal(amount, decimals)) : '-'}</td>
        <td>${createdLabel}</td>
      </tr>
    `;
  }).join('');
  return `
    <div class="stats-win-table-wrap">
      <table class="stats-win-table">
        <thead>
          <tr>
            <th>Cycle</th>
            <th>League</th>
            <th>Round</th>
            <th>Type</th>
            <th>${commEscape(unitLabel)}</th>
            <th>Updated</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </div>
  `;
}

function commClampPersonalRewardsPage(page, total) {
  const pages = Math.max(1, Math.ceil(total / PERSONAL_REWARDS_PAGE_SIZE));
  const safePage = Math.min(Math.max(1, Math.trunc(page) || 1), pages);
  return { page: safePage, pages };
}

function commEnsurePersonalPageOptions(select, pages) {
  if (!select) return;
  const target = Math.max(1, pages);
  const currentOptions = select.options?.length || 0;
  if (currentOptions !== target) {
    select.innerHTML = Array.from({ length: target }, (_, idx) => {
      const value = idx + 1;
      return `<option value="${value}">${value}</option>`;
    }).join('');
  }
}

function commRenderPersonalRewardsList(entries = personalRewardsCache) {
  const list = document.getElementById('personalRewardsList');
  if (!list) return;
  const pageInfo = document.getElementById('personalRewardsPageInfo');
  const pageSelect = document.getElementById('personalRewardsPageSelect');
  const prevBtn = document.getElementById('personalRewardsPrev');
  const nextBtn = document.getElementById('personalRewardsNext');
  const total = Array.isArray(entries) ? entries.length : 0;
  const { page, pages } = commClampPersonalRewardsPage(personalRewardsPage, total);
  personalRewardsPage = page;
  const start = (page - 1) * PERSONAL_REWARDS_PAGE_SIZE;
  const pageEntries = Array.isArray(entries)
    ? entries.slice(start, start + PERSONAL_REWARDS_PAGE_SIZE)
    : [];
  list.innerHTML = commBuildRewardsDetailsTable(pageEntries, { unitLabel: 'GMT', decimals: 3 });
  if (pageInfo) {
    pageInfo.textContent = `Page ${page} of ${pages}`;
  }
  commEnsurePersonalPageOptions(pageSelect, pages);
  if (pageSelect && pageSelect.value !== String(page)) {
    pageSelect.value = String(page);
  }
  if (prevBtn) prevBtn.disabled = page <= 1;
  if (nextBtn) nextBtn.disabled = page >= pages;
}

function commRenderPersonalClanRewardsList(entries = personalClanRewardsCache) {
  const list = document.getElementById('personalClanRewardsList');
  if (!list) return;
  const pageInfo = document.getElementById('personalClanRewardsPageInfo');
  const pageSelect = document.getElementById('personalClanRewardsPageSelect');
  const prevBtn = document.getElementById('personalClanRewardsPrev');
  const nextBtn = document.getElementById('personalClanRewardsNext');
  const total = Array.isArray(entries) ? entries.length : 0;
  const { page, pages } = commClampPersonalRewardsPage(personalClanRewardsPage, total);
  personalClanRewardsPage = page;
  const start = (page - 1) * PERSONAL_REWARDS_PAGE_SIZE;
  const pageEntries = Array.isArray(entries)
    ? entries.slice(start, start + PERSONAL_REWARDS_PAGE_SIZE)
    : [];
  list.innerHTML = commBuildRewardsDetailsTable(pageEntries, {
    unitLabel: 'BTC',
    amountFn: commExtractRewardsBtc,
    decimals: 7
  });
  if (pageInfo) {
    pageInfo.textContent = `Page ${page} of ${pages}`;
  }
  commEnsurePersonalPageOptions(pageSelect, pages);
  if (pageSelect && pageSelect.value !== String(page)) {
    pageSelect.value = String(page);
  }
  if (prevBtn) prevBtn.disabled = page <= 1;
  if (nextBtn) nextBtn.disabled = page >= pages;
}

async function commLoadPersonalRewardsFromDb() {
  const status = document.getElementById('personalRewardsStatus');
  const summary = document.getElementById('personalRewardsSummary');
  const list = document.getElementById('personalRewardsList');
  const label = document.getElementById('personalUserLabel');
  if (!status || !summary || !list) return;
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) {
    status.textContent = 'Sign in to load rewards data.';
    summary.innerHTML = '';
    list.innerHTML = '';
    return;
  }
  const storedAlias = commGetPersonalUsername();
  const storedUserId = commGetStoredPersonalUserId();
  if (label) {
    label.textContent = storedAlias || (Number.isFinite(storedUserId) ? `User #${storedUserId}` : 'Unknown');
  }
  status.textContent = storedAlias
    ? `Loading rewards for ${storedAlias}...`
    : Number.isFinite(storedUserId)
      ? `Loading rewards for User #${storedUserId}...`
      : 'Loading rewards...';
  try {
    const query = `user_id=eq.${encodeURIComponent(identity.userId)}&limit=1`;
    const rows = await commFetch(COMM_TABLE_USER_REWARDS, query);
    const row = Array.isArray(rows) && rows.length ? rows[0] : null;
    const entries = commExtractRewardsByUserArray(row?.raw);
    personalRewardsCache = entries;
    personalRewardsPage = 1;
    if (!row?.raw) {
      status.textContent = 'No rewards data stored yet.';
      summary.innerHTML = '<div class="muted">No rewards data stored yet.</div>';
      list.innerHTML = '';
      commRenderPersonalRewardsList([]);
      if (label) {
        const fallbackId = commGetStoredPersonalUserId();
        label.textContent = Number.isFinite(fallbackId) ? `User #${fallbackId}` : 'Unknown';
      }
      return;
    }
    const rewardAlias = commExtractRewardsUserAlias(entries);
    const rewardUserId = commExtractRewardsUserId(entries);
    if (rewardAlias || Number.isFinite(rewardUserId)) {
      commStorePersonalIdentity({
        userId: rewardUserId,
        alias: rewardAlias
      });
    }
    if (!Number.isFinite(commGetStoredPersonalUserId())) {
      commResolveGominingUserId().catch(() => {});
    }
    if (label) {
      const storedAliasUpdated = commGetPersonalUsername();
      const storedUserIdUpdated = commGetStoredPersonalUserId();
      label.textContent = storedAliasUpdated || (Number.isFinite(storedUserIdUpdated) ? `User #${storedUserIdUpdated}` : 'Unknown');
    }
    summary.innerHTML = commBuildRewardsSummaryTable(entries, { unitLabel: 'GMT', decimals: 3 });
    commRenderPersonalRewardsList(entries);
    status.textContent = row ? `Rewards entries: ${entries.length}.` : 'No rewards data stored yet.';
    status.style.color = '';
  } catch (err) {
    status.textContent = err?.message || 'Failed to load rewards data.';
    status.style.color = 'rgba(248, 113, 113, 0.9)';
    summary.innerHTML = '';
    list.innerHTML = '';
    commRenderPersonalRewardsList([]);
  }
}

async function commFetchRewardsEntriesFromDb(tableName, userId) {
  if (!userId) return [];
  const query = `user_id=eq.${encodeURIComponent(userId)}&limit=1`;
  const rows = await commFetch(tableName, query);
  const row = Array.isArray(rows) && rows.length ? rows[0] : null;
  return commExtractRewardsByUserArray(row?.raw);
}

function commSyncPersonalIdentityFromRewards(entries) {
  const rewardAlias = commExtractRewardsUserAlias(entries);
  const rewardUserId = commExtractRewardsUserId(entries);
  if (rewardAlias || Number.isFinite(rewardUserId)) {
    commStorePersonalIdentity({
      userId: rewardUserId,
      alias: rewardAlias
    });
  }
}

async function commLoadPersonalClanRewardsFromDb() {
  const status = document.getElementById('personalClanRewardsStatus');
  const summary = document.getElementById('personalClanRewardsSummary');
  const list = document.getElementById('personalClanRewardsList');
  if (!status || !summary || !list) return;
  const identity = commGetApiUserIdentity();
  if (!identity?.userId) {
    status.textContent = 'Sign in to load clan rewards.';
    summary.innerHTML = '';
    list.innerHTML = '';
    return;
  }
  const storedAlias = commGetPersonalUsername();
  status.textContent = storedAlias ? `Loading clan rewards for ${storedAlias}...` : 'Loading clan rewards...';
  try {
    const query = `user_id=eq.${encodeURIComponent(identity.userId)}&limit=1`;
    const rows = await commFetch(COMM_TABLE_USER_REWARDS_CLAN, query);
    const row = Array.isArray(rows) && rows.length ? rows[0] : null;
    const entries = commExtractRewardsByUserArray(row?.raw);
    personalClanRewardsCache = entries;
    personalClanRewardsPage = 1;
    const rewardAlias = commExtractRewardsUserAlias(entries);
    const rewardUserId = commExtractRewardsUserId(entries);
    if (rewardAlias || Number.isFinite(rewardUserId)) {
      commStorePersonalIdentity({
        userId: rewardUserId,
        alias: rewardAlias
      });
    }
    summary.innerHTML = commBuildRewardsSummaryTable(entries, { unitLabel: 'BTC', amountFn: commExtractRewardsBtc, decimals: 7 });
    commRenderPersonalClanRewardsList(entries);
    status.textContent = row
      ? `Clan rewards entries: ${entries.length}.`
      : 'No clan rewards stored yet.';
    status.style.color = '';
  } catch (err) {
    status.textContent = err?.message || 'Failed to load clan rewards.';
    status.style.color = 'rgba(248, 113, 113, 0.9)';
    summary.innerHTML = '';
    list.innerHTML = '';
    commRenderPersonalClanRewardsList([]);
  }
}

function commStartPersonalRewardsTimer() {
  if (personalRewardsTimer) return;
  personalRewardsTimer = setInterval(() => {
    commRefreshPersonalRewards().catch(() => {});
    commRefreshPersonalClanRewards().catch(() => {});
  }, PERSONAL_REWARDS_REFRESH_MS);
}

function commStartPersonalRewardsRetryTimer() {
  if (personalRewardsRetryTimer) return;
  personalRewardsRetryTimer = setInterval(() => {
    if (!personalRewardsNeedsAuthRetry && !personalClanRewardsNeedsAuthRetry) {
      clearInterval(personalRewardsRetryTimer);
      personalRewardsRetryTimer = null;
      return;
    }
    commRefreshPersonalRewards({ force: true }).catch(() => {});
    commRefreshPersonalClanRewards({ force: true }).catch(() => {});
  }, PERSONAL_REWARDS_RETRY_MS);
}

async function commRefreshPersonalRewards({ force = false } = {}) {
  if (personalRewardsRunning) return false;
  const identity = commGetApiUserIdentity();
  const userId = identity?.userId;
  if (!userId) return false;
  const statusEl = document.getElementById('personalRewardsStatus');
  const setStatus = (message, error = false) => {
    if (!statusEl) return;
    statusEl.textContent = message || '';
    statusEl.style.color = error ? 'rgba(248, 113, 113, 0.9)' : '';
  };
  let lastAt = 0;
  try {
    const key = commGetPersonalRewardsLastKey(userId);
    lastAt = commParseNumber(localStorage.getItem(key));
  } catch (err) {
    lastAt = 0;
  }
  const now = Date.now();
  if (!force && lastAt && now - lastAt < PERSONAL_REWARDS_REFRESH_MS) return false;
  personalRewardsRunning = true;
  try {
    let tab = await commFindMiningWarsTabWithAuth();
    if (!tab) {
      tab = await commFindGominingTabWithAuth();
    }
    if (!tab) throw new Error('No GoMining tab with auth token found.');
    const endpoint = 'https://api.gomining.com/api/nft-game/rewards-by-user';
    const filters = { type: 'user' };
    const perRequest = PERSONAL_REWARDS_PAGE_SIZE;
    const maxRecords = PERSONAL_REWARDS_MAX_RECORDS;
    const combined = [];
    let lastRaw = null;
    for (let skip = 0; skip < maxRecords; skip += perRequest) {
      const pagePayload = { filters, pagination: { limit: perRequest, skip } };
      const response = await commFetchGominingApiViaInjection(tab.id, endpoint, pagePayload);
      if (!response?.ok) {
        const errorMsg = response?.error
          || (typeof response?.data === 'string' ? response.data : response?.data?.message || response?.data?.error);
        throw new Error(errorMsg || 'Rewards-by-user API error.');
      }
      lastRaw = response?.data ?? null;
      const pageRecords = commExtractRewardsByUserArray(lastRaw);
      if (Array.isArray(pageRecords) && pageRecords.length) {
        combined.push(...pageRecords);
      }
      if (!Array.isArray(pageRecords) || pageRecords.length < perRequest) {
        break;
      }
    }
    const raw = { data: combined, count: combined.length };
    const records = combined;
    const rewardAlias = commExtractRewardsUserAlias(records);
    const rewardUserId = commExtractRewardsUserId(records);
    const row = {
      user_id: userId,
      user_label: identity?.userLabel || null,
      license_key: identity?.licenseKey || null,
      username: rewardAlias || null,
      cycle_id: null,
      league_id: null,
      payload: { filters, pagination: { limit: perRequest, skip: 0, max: maxRecords } },
      raw,
      fetched_at: new Date().toISOString()
    };
    await commInsert(COMM_TABLE_USER_REWARDS, [row], { upsert: true, onConflict: 'user_id' });
    try {
      const key = commGetPersonalRewardsLastKey(userId);
      localStorage.setItem(key, String(Date.now()));
    } catch (err) {
      // ignore storage errors
    }
    if (rewardAlias || Number.isFinite(rewardUserId)) {
      commStorePersonalIdentity({
        userId: rewardUserId,
        alias: rewardAlias
      });
    }
    commLoadPersonalRewardsFromDb().catch(() => {});
    personalRewardsNeedsAuthRetry = false;
    window.debugLogger?.info?.('Rewards-by-user stored', {
      username: rewardAlias || null,
      cycleId: null,
      leagueId: null,
      count: Array.isArray(records) ? records.length : 0
    });
    return true;
  } catch (err) {
    const fallbackName = commGetPersonalUsername() || identity?.userLabel || 'user';
    window.debugLogger?.warn?.('Rewards-by-user fetch failed', {
      username: fallbackName,
      error: err?.message || String(err)
    });
    const message = err?.message || 'Failed to load rewards.';
    if (/no gomining tab with auth token/i.test(message) || /no mining wars tab with auth token/i.test(message)) {
      setStatus('Open GoMining while logged in to refresh earnings. Will retry every minute.', true);
      personalRewardsNeedsAuthRetry = true;
      commStartPersonalRewardsRetryTimer();
    } else if (message) {
      setStatus(message, true);
    }
    return false;
  } finally {
    personalRewardsRunning = false;
  }
}

async function commRefreshPersonalClanRewards({ force = false } = {}) {
  if (personalClanRewardsRunning) return false;
  const identity = commGetApiUserIdentity();
  const userId = identity?.userId;
  if (!userId) return false;
  const statusEl = document.getElementById('personalClanRewardsStatus');
  const setStatus = (message, error = false) => {
    if (!statusEl) return;
    statusEl.textContent = message || '';
    statusEl.style.color = error ? 'rgba(248, 113, 113, 0.9)' : '';
  };
  let lastAt = 0;
  try {
    const key = commGetPersonalClanRewardsLastKey(userId);
    lastAt = commParseNumber(localStorage.getItem(key));
  } catch (err) {
    lastAt = 0;
  }
  const now = Date.now();
  if (!force && lastAt && now - lastAt < PERSONAL_REWARDS_REFRESH_MS) return false;
  personalClanRewardsRunning = true;
  try {
    let tab = await commFindMiningWarsTabWithAuth();
    if (!tab) {
      tab = await commFindGominingTabWithAuth();
    }
    if (!tab) throw new Error('No GoMining tab with auth token found.');
    let gominingUserId = commGetStoredPersonalUserId();
    if (!Number.isFinite(gominingUserId)) {
      gominingUserId = await commResolveGominingUserId();
    }
    const endpoint = 'https://api.gomining.com/api/nft-game/rewards-by-user';
    const filters = { type: 'clan' };
    if (Number.isFinite(gominingUserId)) {
      filters.userId = Math.trunc(gominingUserId);
    } else {
      throw new Error('No GoMining user id available for clan rewards.');
    }
    const perRequest = PERSONAL_REWARDS_PAGE_SIZE;
    const maxRecords = PERSONAL_REWARDS_MAX_RECORDS;
    const combined = [];
    let lastRaw = null;
    for (let skip = 0; skip < maxRecords; skip += perRequest) {
      const pagePayload = { filters, pagination: { limit: perRequest, skip } };
      const response = await commFetchGominingApiViaInjection(tab.id, endpoint, pagePayload);
      if (!response?.ok) {
        const errorMsg = response?.error
          || (typeof response?.data === 'string' ? response.data : response?.data?.message || response?.data?.error);
        throw new Error(errorMsg || 'Clan rewards API error.');
      }
      lastRaw = response?.data ?? null;
      const pageRecords = commExtractRewardsByUserArray(lastRaw);
      if (Array.isArray(pageRecords) && pageRecords.length) {
        combined.push(...pageRecords);
      }
      if (!Array.isArray(pageRecords) || pageRecords.length < perRequest) {
        break;
      }
    }
    const raw = { data: combined, count: combined.length };
    const records = combined;
    const rewardAlias = commExtractRewardsUserAlias(records);
    const rewardUserId = commExtractRewardsUserId(records);
    const row = {
      user_id: userId,
      user_label: identity?.userLabel || null,
      license_key: identity?.licenseKey || null,
      username: rewardAlias || null,
      gomining_user_id: Number.isFinite(gominingUserId) ? Math.trunc(gominingUserId) : null,
      cycle_id: null,
      league_id: null,
      payload: { filters, pagination: { limit: perRequest, skip: 0, max: maxRecords } },
      raw,
      fetched_at: new Date().toISOString()
    };
    await commInsert(COMM_TABLE_USER_REWARDS_CLAN, [row], { upsert: true, onConflict: 'user_id' });
    try {
      const key = commGetPersonalClanRewardsLastKey(userId);
      localStorage.setItem(key, String(Date.now()));
    } catch (err) {
      // ignore storage errors
    }
    if (rewardAlias || Number.isFinite(rewardUserId)) {
      commStorePersonalIdentity({
        userId: rewardUserId,
        alias: rewardAlias
      });
    }
    commLoadPersonalClanRewardsFromDb().catch(() => {});
    personalClanRewardsNeedsAuthRetry = false;
    window.debugLogger?.info?.('Clan rewards stored', {
      username: rewardAlias || null,
      cycleId: null,
      leagueId: null,
      count: Array.isArray(records) ? records.length : 0
    });
    return true;
  } catch (err) {
    const fallbackName = commGetPersonalUsername() || identity?.userLabel || 'user';
    window.debugLogger?.warn?.('Clan rewards fetch failed', {
      username: fallbackName,
      error: err?.message || String(err)
    });
    const message = err?.message || 'Failed to load clan rewards.';
    if (/no gomining tab with auth token/i.test(message) || /no mining wars tab with auth token/i.test(message)) {
      setStatus('Open GoMining while logged in to refresh clan earnings. Will retry every minute.', true);
      personalClanRewardsNeedsAuthRetry = true;
      commStartPersonalRewardsRetryTimer();
    } else if (message) {
      setStatus(message, true);
    }
    return false;
  } finally {
    personalClanRewardsRunning = false;
  }
}

function commSetPersonalStatus(message, { error = false } = {}) {
  const status = document.getElementById('personalStatus');
  if (!status) return;
  status.textContent = message || '';
  status.style.color = error ? 'rgba(248, 113, 113, 0.9)' : '';
}

function commSetPersonalClanStatus(message, { error = false } = {}) {
  const status = document.getElementById('personalClanStatus');
  if (!status) return;
  status.textContent = message || '';
  status.style.color = error ? 'rgba(248, 113, 113, 0.9)' : '';
}

function commSelectClanFromWins(rows) {
  if (!Array.isArray(rows) || rows.length === 0) return null;
  const counts = new Map();
  rows.forEach((row) => {
    const userWinner = commExtractUserWinnerFromRow(row);
    let clanId = commParseNumber(userWinner?.clanId);
    let clanName = userWinner?.clanName;
    if (!Number.isFinite(clanId) && !clanName) {
      const clanWinner = commExtractClanWinnerFromRow(row);
      clanId = commParseNumber(clanWinner?.id);
      clanName = clanWinner?.name ?? null;
    }
    const key = Number.isFinite(clanId)
      ? `id:${Math.trunc(clanId)}`
      : (clanName ? `name:${clanName}` : null);
    if (!key) return;
    const entry = counts.get(key) || {
      clanId: Number.isFinite(clanId) ? Math.trunc(clanId) : null,
      clanName: clanName || null,
      count: 0
    };
    entry.count += 1;
    if (!entry.clanName && clanName) entry.clanName = clanName;
    counts.set(key, entry);
  });
  let best = null;
  counts.forEach((entry) => {
    if (!best || entry.count > best.count) best = entry;
  });
  return best;
}

function commBuildPersonalWinsRow(row) {
  const block = row?.block_number ?? '-';
  const cycle = row?.cycle_id ?? '-';
  const league = commFormatLeagueDisplay(row?.league_id);
  const multiplier = Number.isFinite(commParseNumber(row?.multiplier))
    ? `${Math.trunc(commParseNumber(row.multiplier))}x`
    : '-';
  const power = Number.isFinite(commParseNumber(row?.power))
    ? formatCount(Math.round(commParseNumber(row.power)))
    : '-';
  const userWinner = commExtractUserWinnerFromRow(row);
  const clanWinner = commExtractClanWinnerFromRow(row);
  const clanName = clanWinner?.name ? commEscape(clanWinner.name) : '-';
  const clanScore = Number.isFinite(commParseNumber(clanWinner?.score))
    ? formatCount(Math.round(commParseNumber(clanWinner.score)))
    : '-';
  const userName = userWinner?.alias ? commEscape(userWinner.alias) : '-';
  const userScore = Number.isFinite(commParseNumber(userWinner?.score))
    ? formatCount(Math.round(commParseNumber(userWinner.score)))
    : '-';
  const chanceRaw = commParseNumber(userWinner?.chance);
  const chance = Number.isFinite(chanceRaw) ? `${formatDecimal(chanceRaw, 2)}%` : '-';
  const rewardRaw = commParseNumber(userWinner?.reward);
  const reward = Number.isFinite(rewardRaw) ? formatDecimal(rewardRaw, 3) : '-';
  const endedAt = row?.ended_at ?? row?.updated_at ?? row?.created_at ?? null;
  const endedDisplay = endedAt ? commEscape(formatDateTimeValue(endedAt)) : '-';
  return `
    <tr>
      <td>${commEscape(block)}</td>
      <td>${commEscape(cycle)}</td>
      <td>${commEscape(league)}</td>
      <td>${commEscape(multiplier)}</td>
      <td class="stats-num">${commEscape(power)}</td>
      <td>${clanName}</td>
      <td class="stats-num">${commEscape(clanScore)}</td>
      <td>${userName}</td>
      <td class="stats-num">${commEscape(userScore)}</td>
      <td class="stats-num">${commEscape(chance)}</td>
      <td class="stats-num">${commEscape(reward)}</td>
      <td>${endedDisplay}</td>
    </tr>
  `;
}

function commBuildPersonalWinsTable(rows) {
  if (!Array.isArray(rows) || rows.length === 0) {
    return '<div class="muted">No wins found.</div>';
  }
  const header = `
    <tr>
      <th>Block</th>
      <th>Cycle</th>
      <th>League</th>
      <th>X</th>
      <th>Power</th>
      <th>Winning clan</th>
      <th>Clan score</th>
      <th>Winning user</th>
      <th>User score</th>
      <th>Chance</th>
      <th>Reward</th>
      <th>Ended</th>
    </tr>
  `;
  const body = rows.map(commBuildPersonalWinsRow).join('');
  return `
    <div class="stats-win-table-wrap">
      <table class="stats-win-table">
        <thead>${header}</thead>
        <tbody>${body}</tbody>
      </table>
    </div>
  `;
}

async function loadPersonalData({ force = false } = {}) {
  if (personalDataLoading) return;
  const list = document.getElementById('personalWinsList');
  const clanList = document.getElementById('personalClanWinsList');
  commLoadPersonalRewardsFromDb().catch(() => {});
  const storedUserId = commGetStoredPersonalUserId();
  const storedAlias = commGetPersonalUsername();
  const cacheKey = Number.isFinite(storedUserId)
    ? `id:${storedUserId}`
    : (storedAlias ? `alias:${storedAlias}` : null);
  if (!cacheKey) {
    commSetPersonalStatus('Load rewards to identify your GoMining account.');
    if (list) list.innerHTML = '<div class="muted">No personal wins yet.</div>';
    if (clanList) clanList.innerHTML = '<div class="muted">No clan wins yet.</div>';
    commSetPersonalClanStatus('');
    return;
  }
  if (!force && personalDataLoadedUser === cacheKey) return;
  personalDataLoading = true;
  commSetPersonalStatus(`Loading wins for ${storedAlias || 'your GoMining account'} across all cycles...`);
  commSetPersonalClanStatus('Loading clan wins for current cycle...');
  try {
    const currentCycleId = getCycleNumber(new Date());
    const selectFields =
      'select=block_number,cycle_id,league_id,multiplier,power,winner_clan_id,user_winner,clan_winner,created_at,ended_at,raw';
    const rows = [];
    let offset = 0;
    let reachedCap = false;
    while (offset < PERSONAL_ROUND_MAX) {
      const query = [
        selectFields,
        'order=block_number.desc',
        `limit=${PERSONAL_ROUND_PAGE_SIZE}`,
        `offset=${offset}`
      ].join('&');
      const page = await commFetch(COMM_TABLE_ROUND_CYCLE_API, query);
      if (!Array.isArray(page) || page.length === 0) break;
      rows.push(...page);
      if (page.length < PERSONAL_ROUND_PAGE_SIZE) break;
      offset += PERSONAL_ROUND_PAGE_SIZE;
      if (offset >= PERSONAL_ROUND_MAX) reachedCap = true;
    }
    const filtered = rows.filter((row) => {
      const user = commExtractUserWinnerFromRow(row);
      if (Number.isFinite(storedUserId)) {
        const direct = commParseNumber(row?.winner_user_id);
        const candidate = Number.isFinite(direct) ? Math.trunc(direct) : commParseNumber(user?.id);
        return Number.isFinite(candidate) && Math.trunc(candidate) === storedUserId;
      }
      return commMatchesUsername(user?.alias, storedAlias);
    });
    const currentCycleRows = Number.isFinite(currentCycleId)
      ? rows.filter((row) => commParseNumber(row?.cycle_id) === Math.trunc(currentCycleId))
      : [];
    const filteredCurrentCycle = Number.isFinite(currentCycleId)
      ? filtered.filter((row) => commParseNumber(row?.cycle_id) === Math.trunc(currentCycleId))
      : [];
    const clanSourceRows = filteredCurrentCycle.length ? filteredCurrentCycle : filtered;
    const clanCandidate = commSelectClanFromWins(clanSourceRows);
    let clanWins = [];
    if (clanCandidate) {
      const targetId = Number.isFinite(commParseNumber(clanCandidate.clanId))
        ? Math.trunc(commParseNumber(clanCandidate.clanId))
        : null;
      const targetName = clanCandidate.clanName ? normalizeClanName(clanCandidate.clanName) : null;
      clanWins = currentCycleRows.filter((row) => {
        const winnerId = commParseNumber(row?.winner_clan_id);
        if (Number.isFinite(targetId)) {
          if (Number.isFinite(winnerId)) return Math.trunc(winnerId) === targetId;
          const clanWinner = commExtractClanWinnerFromRow(row);
          const fallbackId = commParseNumber(clanWinner?.id);
          if (Number.isFinite(fallbackId)) return Math.trunc(fallbackId) === targetId;
        }
        if (targetName) {
          const clanWinner = commExtractClanWinnerFromRow(row);
          const name = clanWinner?.name ? normalizeClanName(clanWinner.name) : '';
          if (name && name === targetName) return true;
        }
        return false;
      });
    }
    personalDataLoadedUser = cacheKey;
    if (list) list.innerHTML = commBuildPersonalWinsTable(filtered);
    if (clanList) clanList.innerHTML = commBuildPersonalWinsTable(clanWins);
    const capNote = reachedCap ? ` (checked ${rows.length} latest rounds)` : '';
      commSetPersonalStatus(
        `Found ${filtered.length} win${filtered.length === 1 ? '' : 's'} for ${storedAlias || 'your GoMining account'} across all cycles${capNote}.`
      );
    if (!Number.isFinite(currentCycleId)) {
      commSetPersonalClanStatus('Current cycle unavailable.');
    } else if (!clanCandidate) {
      commSetPersonalClanStatus(`No clan found for ${storedAlias || 'your GoMining account'} in current cycle.`);
    } else {
      const label = clanCandidate.clanName || (Number.isFinite(clanCandidate.clanId) ? `Clan ${clanCandidate.clanId}` : 'Clan');
      commSetPersonalClanStatus(
        `Found ${clanWins.length} clan win${clanWins.length === 1 ? '' : 's'} for ${label} in cycle ${currentCycleId}.`
      );
    }
  } catch (err) {
    if (list) list.innerHTML = '<div class="muted">Failed to load personal wins.</div>';
    if (clanList) clanList.innerHTML = '<div class="muted">Failed to load clan wins.</div>';
    commSetPersonalStatus(err?.message || 'Failed to load personal wins.', { error: true });
    commSetPersonalClanStatus('Failed to load clan wins.', { error: true });
  } finally {
    personalDataLoading = false;
  }
}

function populatePersonalData() {
  const container = document.getElementById('personalGrid');
  if (!container) return;
  const html = `
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Personal data</div>
      </div>
      <div class="stats-filter-bar">
        <div class="stats-filter-group">
          <span class="muted">User</span>
          <span id="personalUserLabel" class="muted">Signed in</span>
        </div>
        <button id="personalRefreshBtn" class="btn sm ghost" type="button">Refresh</button>
        <button id="personalApiHealthBtn" class="btn sm ghost" type="button">API health</button>
      </div>
      <div id="personalApiStatus" class="muted"></div>
      <div class="stats-tabs">
        <button class="stats-tab active" type="button" data-personal-tab="user">Personal</button>
        <button class="stats-tab" type="button" data-personal-tab="clan">Clan</button>
      </div>
      <div class="stats-tab-panel active" data-personal-panel="user">
        <div class="stats-win-title" style="margin-top:8px;">Income per cycle</div>
        <div id="personalRewardsStatus" class="muted"></div>
        <div id="personalRewardsSummary"></div>
        <div class="stats-history-controls" style="margin-top:6px;">
          <button id="personalRewardsPrev" class="btn sm ghost" type="button">Prev</button>
          <select id="personalRewardsPageSelect" class="stats-filter-select"></select>
          <button id="personalRewardsNext" class="btn sm ghost" type="button">Next</button>
          <div id="personalRewardsPageInfo" class="muted">Page 1 of 1</div>
        </div>
        <div id="personalRewardsList"></div>
        <div class="stats-win-title" style="margin-top:12px;">Profitability snapshot</div>
        <div class="stats-filter-bar" style="flex-wrap: wrap; gap: 12px;">
          <div class="stats-filter-group">
            <span class="muted">Maintenance GMT/week</span>
            <input id="profitSnapshotCost" class="stats-filter-input" type="number" min="0" step="0.1">
          </div>
          <div class="stats-filter-group">
            <span class="muted">Maintenance TH</span>
            <input id="profitSnapshotBaseTh" class="stats-filter-input" type="number" min="0" step="0.1">
          </div>
          <div class="stats-filter-group">
            <span class="muted">Your TH</span>
            <input id="profitSnapshotUserTh" class="stats-filter-input" type="number" min="0" step="0.1" placeholder="Auto">
          </div>
          <button id="profitSnapshotBtn" class="btn sm ghost" type="button">Generate snapshot</button>
        </div>
        <div id="profitSnapshotStatus" class="muted"></div>
        <div id="profitSnapshotOutput"></div>
      </div>
      <div class="stats-tab-panel" data-personal-panel="clan">
        <div class="stats-win-title" style="margin-top:8px;">Clan income per cycle</div>
        <div id="personalClanRewardsStatus" class="muted"></div>
        <div id="personalClanRewardsSummary"></div>
        <div class="stats-history-controls" style="margin-top:6px;">
          <button id="personalClanRewardsPrev" class="btn sm ghost" type="button">Prev</button>
          <select id="personalClanRewardsPageSelect" class="stats-filter-select"></select>
          <button id="personalClanRewardsNext" class="btn sm ghost" type="button">Next</button>
          <div id="personalClanRewardsPageInfo" class="muted">Page 1 of 1</div>
        </div>
        <div id="personalClanRewardsList"></div>
      </div>
    </div>
  `;
  renderCards(container, [{ title: 'Personal data', htmlContent: html, id: 'personal-data-card' }]);
  const refreshBtn = container.querySelector('#personalRefreshBtn');
  const healthBtn = container.querySelector('#personalApiHealthBtn');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      personalDataLoadedUser = null;
      commRefreshPersonalRewards({ force: true }).catch(() => {});
      commLoadPersonalRewardsFromDb().catch(() => {});
      commRefreshPersonalClanRewards({ force: true }).catch(() => {});
      commLoadPersonalClanRewardsFromDb().catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  if (healthBtn && !healthBtn.dataset.bound) {
    healthBtn.addEventListener('click', () => {
      commCheckPersonalApiHealth().catch(() => {});
    });
    healthBtn.dataset.bound = '1';
  }
  const initPersonalTabs = () => {
    const card = container.querySelector('[data-card-id="personal-data-card"]') || container;
    if (!card || card.dataset.personalTabsBound) return;
    const tabs = Array.from(card.querySelectorAll('[data-personal-tab]'));
    const panels = Array.from(card.querySelectorAll('[data-personal-panel]'));
    const activate = (tabName) => {
      tabs.forEach((tab) => {
        tab.classList.toggle('active', tab.dataset.personalTab === tabName);
      });
      panels.forEach((panel) => {
        panel.classList.toggle('active', panel.dataset.personalPanel === tabName);
      });
    };
    tabs.forEach((tab) => {
      tab.addEventListener('click', () => {
        activate(tab.dataset.personalTab);
      });
    });
    if (tabs.length) {
      const activeTab = tabs.find((tab) => tab.classList.contains('active')) || tabs[0];
      if (activeTab) activate(activeTab.dataset.personalTab);
    }
    card.dataset.personalTabsBound = '1';
  };
  initPersonalTabs();
  const bindPersonalPagination = () => {
    const card = container.querySelector('[data-card-id="personal-data-card"]') || container;
    if (!card || card.dataset.personalPaginationBound) return;
    const userPrev = card.querySelector('#personalRewardsPrev');
    const userNext = card.querySelector('#personalRewardsNext');
    const userSelect = card.querySelector('#personalRewardsPageSelect');
    const clanPrev = card.querySelector('#personalClanRewardsPrev');
    const clanNext = card.querySelector('#personalClanRewardsNext');
    const clanSelect = card.querySelector('#personalClanRewardsPageSelect');
    if (userPrev) {
      userPrev.addEventListener('click', () => {
        personalRewardsPage = Math.max(1, personalRewardsPage - 1);
        commRenderPersonalRewardsList();
      });
    }
    if (userNext) {
      userNext.addEventListener('click', () => {
        personalRewardsPage += 1;
        commRenderPersonalRewardsList();
      });
    }
    if (userSelect) {
      userSelect.addEventListener('change', () => {
        personalRewardsPage = Math.max(1, Number(userSelect.value) || 1);
        commRenderPersonalRewardsList();
      });
    }
    if (clanPrev) {
      clanPrev.addEventListener('click', () => {
        personalClanRewardsPage = Math.max(1, personalClanRewardsPage - 1);
        commRenderPersonalClanRewardsList();
      });
    }
    if (clanNext) {
      clanNext.addEventListener('click', () => {
        personalClanRewardsPage += 1;
        commRenderPersonalClanRewardsList();
      });
    }
    if (clanSelect) {
      clanSelect.addEventListener('change', () => {
        personalClanRewardsPage = Math.max(1, Number(clanSelect.value) || 1);
        commRenderPersonalClanRewardsList();
      });
    }
    card.dataset.personalPaginationBound = '1';
  };
  bindPersonalPagination();
  const bindProfitSnapshot = () => {
    const card = container.querySelector('[data-card-id="personal-data-card"]') || container;
    if (!card || card.dataset.profitSnapshotBound) return;
    const costInput = card.querySelector('#profitSnapshotCost');
    const baseThInput = card.querySelector('#profitSnapshotBaseTh');
    const userThInput = card.querySelector('#profitSnapshotUserTh');
    const snapshotBtn = card.querySelector('#profitSnapshotBtn');
    const settings = commLoadProfitSnapshotSettings();
    if (costInput) costInput.value = settings.cost;
    if (baseThInput) baseThInput.value = settings.baseTh;
    if (userThInput && Number.isFinite(settings.userTh)) {
      userThInput.value = settings.userTh;
    }
    const saveSettings = () => {
      const cost = commParseNumber(costInput?.value);
      const baseTh = commParseNumber(baseThInput?.value);
      const userTh = commParseNumber(userThInput?.value);
      commSaveProfitSnapshotSettings({
        cost: Number.isFinite(cost) ? cost : settings.cost,
        baseTh: Number.isFinite(baseTh) ? baseTh : settings.baseTh,
        userTh: Number.isFinite(userTh) ? userTh : null
      });
    };
    [costInput, baseThInput, userThInput].forEach((input) => {
      if (!input) return;
      input.addEventListener('change', saveSettings);
      input.addEventListener('input', saveSettings);
    });
    if (snapshotBtn) {
      snapshotBtn.addEventListener('click', () => {
        saveSettings();
        commGenerateProfitabilitySnapshot().catch(() => {});
      });
    }
    card.dataset.profitSnapshotBound = '1';
  };
  bindProfitSnapshot();
  commLoadPersonalRewardsFromDb().catch(() => {});
  commLoadPersonalClanRewardsFromDb().catch(() => {});
  commStartPersonalRewardsTimer();
  commRefreshPersonalRewards({ force: true }).catch(() => {});
  commRefreshPersonalClanRewards({ force: true }).catch(() => {});
}

function populateGraphs() {
  const container = document.getElementById('graphsGrid');
  if (!container) return;
  const html = `
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Busiest hours</div>
        <button id="boostUseRefreshBtn" class="btn sm ghost" type="button">Refresh</button>
      </div>
      <div class="muted" style="margin-top:4px;">Shows when boost activity is most intense, using hourly boost samples collected from Live.</div>
      <div id="boostUseStatus" class="muted"></div>
      <div id="boostUseSummary" class="muted" style="margin-top:4px;"></div>
      <div class="boost-use-section">
        <div class="stats-win-title">Busiest hours of day</div>
        <div class="muted" style="margin-top:2px;">Average boost volume per hour (00-23), aggregated across all tracked days.</div>
        <div id="boostUseDayList" class="boost-use-grid"></div>
      </div>
      <div class="boost-use-section">
        <div class="stats-win-title">Day of week heatmap</div>
        <div class="muted" style="margin-top:2px;">Heatmap of average boost volume by weekday and hour; darker cells mean busier periods.</div>
        <div id="boostUseHeatmap" class="boost-use-heatmap-wrap"></div>
      </div>
      <div class="boost-use-section">
        <div class="stats-win-title">Top boost hours</div>
        <div class="muted" style="margin-top:2px;">Top hours ranked by total boost volume (most intense hours first).</div>
        <div id="boostUseTopList" class="boost-use-list"></div>
      </div>
      <div class="boost-use-section">
        <div class="stats-win-title">Busiest hours in cycle</div>
        <div class="muted" style="margin-top:2px;">Top hours within a selected cycle, ranked by boost volume.</div>
        <div class="stats-filter-bar" style="margin-top:6px;">
          <div class="stats-filter-group">
            <span class="muted">Cycle</span>
            <select id="boostUseCycleSelect" class="stats-filter-select"></select>
          </div>
        </div>
        <div id="boostUseCycleList" class="boost-use-list"></div>
      </div>
    </div>
  `;
  renderCards(container, [{ title: 'Busiest hours', htmlContent: html, id: 'boost-use-card' }]);

  const refreshBtn = container.querySelector('#boostUseRefreshBtn');
  const cycleSelect = container.querySelector('#boostUseCycleSelect');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      commRenderBoostUseGraphs({ hydrate: true }).catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  if (cycleSelect && !cycleSelect.dataset.bound) {
    cycleSelect.addEventListener('change', () => {
      const selected = commParseNumber(cycleSelect.value);
      if (Number.isFinite(selected)) {
        localStorage.setItem(BOOST_USE_CYCLE_STORAGE_KEY, String(Math.trunc(selected)));
      }
      commRenderBoostUseGraphs({ hydrate: false }).catch(() => {});
    });
    cycleSelect.dataset.bound = '1';
  }
  commRenderBoostUseGraphs({ hydrate: true }).catch(() => {});
}

function populateBoostImpact() {
  const container = document.getElementById('boostImpactGrid');
  if (!container) return;
  const html = `
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Boost impact</div>
        <button id="boostImpactRefreshBtn" class="btn sm ghost" type="button">Refresh</button>
      </div>
      <div class="muted" style="margin-top:4px;">How boosts influence outcomes, derived from rounds_cycle_api winners and Live boost samples.</div>
      <div id="boostImpactStatus" class="muted"></div>
      <div class="stats-tabs" style="margin-top:8px;">
        <button class="stats-tab active" type="button" data-boost-tab="ratio">Boost vs base ratio</button>
        <button class="stats-tab" type="button" data-boost-tab="multiplier">Multiplier impact</button>
        <button class="stats-tab" type="button" data-boost-tab="types">Boost type breakdown</button>
      </div>
      <div class="stats-tab-panel active" data-boost-panel="ratio">
        <div class="stats-win-title" style="margin-top:8px;">Boost vs base ratio</div>
        <div class="muted" style="margin-top:2px;">Share of boost score vs base score by hour; higher % means boosts dominate.</div>
        <div id="boostImpactRatioList" class="boost-use-grid"></div>
      </div>
      <div class="stats-tab-panel" data-boost-panel="multiplier">
        <div class="stats-win-title" style="margin-top:8px;">Multiplier impact</div>
        <div class="muted" style="margin-top:2px;">Average boost totals per multiplier, based on winner boost usage.</div>
        <div id="boostImpactMultiplierList" class="boost-use-list"></div>
      </div>
      <div class="stats-tab-panel" data-boost-panel="types">
        <div class="stats-win-title" style="margin-top:8px;">Boost type breakdown</div>
        <div class="muted" style="margin-top:2px;">Top boost types by total usage across recent rounds.</div>
        <div id="boostImpactTypeList" class="boost-use-list"></div>
      </div>
    </div>
  `;
  renderCards(container, [{ title: 'Boost impact', htmlContent: html, id: 'boost-impact-card' }]);

  const refreshBtn = container.querySelector('#boostImpactRefreshBtn');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      commRenderBoostImpactGraphs({ hydrate: true, force: true }).catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  const card = container.querySelector('[data-card-id="boost-impact-card"]') || container;
  if (card && !card.dataset.boostTabsBound) {
    const tabs = Array.from(card.querySelectorAll('[data-boost-tab]'));
    tabs.forEach((tab) => {
      tab.addEventListener('click', () => {
        showBoostImpactPanel(tab.dataset.boostTab, card);
      });
    });
    card.dataset.boostTabsBound = '1';
  }
  commRenderBoostImpactGraphs({ hydrate: true, force: false }).catch(() => {});
}

async function commRenderClanCompare({ force = false } = {}) {
  const status = document.getElementById('clanCompareStatus');
  const output = document.getElementById('clanCompareOutput');
  if (!status || !output) return;
  if (!hasSupabaseSession()) {
    status.textContent = 'Sign in to load clan vs league compare.';
    output.innerHTML = '';
    return;
  }
  status.textContent = 'Loading clan vs league compare...';
  output.innerHTML = '';
  const cycleId = getCycleNumber(new Date());
  await refreshLiveRosterCache({ force, limit: 10 });
  let leagueId = commResolveLiveLeagueId();
  if (!Number.isFinite(leagueId)) {
    leagueId = commParseNumber(liveRosterMyClan?.league_id ?? liveRosterMyClan?.leagueId);
  }
  if (!Number.isFinite(cycleId) || !Number.isFinite(leagueId)) {
    status.textContent = 'League or cycle not available yet.';
    return;
  }
  await refreshLiveRosterTotals(cycleId, leagueId);
  const myClan = liveRosterMyClan;
  const totals = liveRosterTotals;
  if (!myClan) {
    status.textContent = 'Your clan was not found in roster data yet.';
    return;
  }
  if (!totals || !Number.isFinite(totals.totalPower) || !Number.isFinite(totals.totalRows) || totals.totalRows <= 0) {
    status.textContent = 'League roster totals not available yet.';
    return;
  }

  const clanName = myClan?.clan_name ?? myClan?.clanName ?? (myClan?.clan_id ? `Clan ${myClan.clan_id}` : 'Your clan');
  const clanPower = commParseNumber(myClan?.clan_power ?? myClan?.clanPower);
  const avgPower = totals.totalPower / totals.totalRows;
  const ratio = Number.isFinite(clanPower) && avgPower > 0 ? (clanPower / avgPower) : null;
  const leagueLabel = commGetLeagueName(leagueId) || `League ${leagueId}`;

  const metricRows = [
    ['League', commEscape(leagueLabel)],
    ['Cycle', commEscape(String(cycleId))],
    ['Clan', commEscape(clanName)],
    ['Clan power', Number.isFinite(clanPower) ? formatCount(Math.round(clanPower)) : '-'],
    ['League avg power', Number.isFinite(avgPower) ? formatCount(Math.round(avgPower)) : '-'],
    ['Clan vs avg', Number.isFinite(ratio) ? `${formatDecimal(ratio * 100, 1)}%` : '-'],
    ['League clans', formatCount(totals.totalRows)]
  ];
  const metricBody = metricRows.map(([label, value]) => `
    <tr><td>${commEscape(label)}</td><td class="stats-num">${value}</td></tr>
  `).join('');
  const metricTable = `
    <div class="stats-win-table-wrap">
      <table class="stats-win-table">
        <thead><tr><th>Metric</th><th>Value</th></tr></thead>
        <tbody>${metricBody}</tbody>
      </table>
    </div>
  `;

  const topRows = Array.isArray(liveRosterBaseLeaders) ? liveRosterBaseLeaders.slice(0, 30) : [];
  const topList = topRows.length
    ? `
      <div class="boost-use-section" style="margin-top:10px;">
        <div class="stats-win-title">Top clans (power)</div>
        <div class="boost-use-list">
          ${topRows.map((row, idx) => {
            const name = row?.clan_name ?? row?.clanName ?? (row?.clan_id ? `Clan ${row.clan_id}` : 'Clan');
            const power = commParseNumber(row?.clan_power ?? row?.clanPower);
            return `
              <div class="boost-use-row">
                <div class="boost-use-label">#${idx + 1}</div>
                <div class="boost-use-bar"><span class="boost-use-bar-fill" style="width:${100 - idx * 6}%"></span></div>
                <div class="boost-use-value">${commEscape(name)}  -  ${commEscape(Number.isFinite(power) ? formatCount(Math.round(power)) : '-')}</div>
              </div>
            `;
          }).join('')}
        </div>
      </div>
    `
    : '';

  output.innerHTML = `
    ${metricTable}
    ${topList}
  `;
  status.textContent = 'Clan vs league compare updated.';
}

function populateClanCompare() {
  const container = document.getElementById('clanCompareGrid');
  if (!container) return;
  const html = `
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Clan vs league compare</div>
        <button id="clanCompareRefreshBtn" class="btn sm ghost" type="button">Refresh</button>
      </div>
      <div class="muted" style="margin-top:4px;">Compares your clan power to the league average and highlights top clans.</div>
      <div id="clanCompareStatus" class="muted"></div>
      <div id="clanCompareOutput" style="margin-top:8px;"></div>
    </div>
  `;
  renderCards(container, [{ title: 'Clan vs league compare', htmlContent: html, id: 'clan-compare-card' }]);
  const refreshBtn = container.querySelector('#clanCompareRefreshBtn');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      commRenderClanCompare({ force: true }).catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  commRenderClanCompare({ force: false }).catch(() => {});
}

function populateMultiplierTime() {
  const container = document.getElementById('multiplierTimeGrid');
  if (!container) return;
  const storedLeague = localStorage.getItem(MULTIPLIER_TIME_LEAGUE_KEY) || '';
  const leagueOptions = buildClanSelectionLeagueFilterOptions(storedLeague);
  const storedFrom = localStorage.getItem(MULTIPLIER_TIME_CYCLE_FROM_KEY) || '';
  const storedTo = localStorage.getItem(MULTIPLIER_TIME_CYCLE_TO_KEY) || '';
  const hide1x = localStorage.getItem(MULTIPLIER_TIME_HIDE_1X_KEY) === '1';
  const hide1xRatio = localStorage.getItem(MULTIPLIER_TIME_HIDE_1X_RATIO_KEY) === '1';
  const html = `
    <div class="stats-win-section">
      <div class="stats-win-header">
        <div class="stats-win-title">Multiplier per time</div>
        <button id="multiplierTimeRefreshBtn" class="btn sm ghost" type="button">Refresh</button>
      </div>
      <div class="muted" style="margin-top:4px;">Shows how often each multiplier appears per hour, based on rounds_cycle_api.</div>
      <div id="multiplierTimeStatus" class="muted"></div>
      <div id="multiplierTimeSummary" class="muted" style="margin-top:4px;"></div>
      <div class="stats-filter-bar" style="margin-top:8px; flex-wrap: wrap;">
        <div class="stats-filter-group">
          <span class="muted">League</span>
          <select id="multiplierTimeLeague" class="stats-filter-select">
            ${leagueOptions}
          </select>
        </div>
        <div class="stats-filter-group">
          <span class="muted">Cycle from</span>
          <input id="multiplierTimeCycleFrom" class="stats-filter-input" type="number" min="1" step="1" placeholder="Any" value="${commEscape(storedFrom)}">
        </div>
        <div class="stats-filter-group">
          <span class="muted">Cycle to</span>
          <input id="multiplierTimeCycleTo" class="stats-filter-input" type="number" min="1" step="1" placeholder="Any" value="${commEscape(storedTo)}">
        </div>
        <div class="stats-filter-group">
          <label class="muted" style="display:flex; align-items:center; gap:6px;">
            <input id="multiplierTimeHide1x" type="checkbox" ${hide1x ? 'checked' : ''}>
            Hide 1x
          </label>
        </div>
        <div class="stats-filter-group">
          <label class="muted" style="display:flex; align-items:center; gap:6px;">
            <input id="multiplierTimeHide1xRatio" type="checkbox" ${hide1xRatio ? 'checked' : ''}>
            Hide first 50% of 1x (ratio)
          </label>
        </div>
      </div>
      <div class="boost-use-section">
        <div class="stats-win-title">Multiplier frequency per hour</div>
        <div class="muted" style="margin-top:2px;">Colored segments show how often each multiplier appears per hour, with counts and percentages.</div>
        <div id="multiplierTimeList" class="multiplier-time-list"></div>
      </div>
    </div>
  `;
  renderCards(container, [{ title: 'Multiplier per time', htmlContent: html, id: 'multiplier-time-card' }]);
  const refreshBtn = container.querySelector('#multiplierTimeRefreshBtn');
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      commRenderMultiplierTime({ force: true }).catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  const leagueSelect = container.querySelector('#multiplierTimeLeague');
  const cycleFromInput = container.querySelector('#multiplierTimeCycleFrom');
  const cycleToInput = container.querySelector('#multiplierTimeCycleTo');
  const hide1xInput = container.querySelector('#multiplierTimeHide1x');
  const hide1xRatioInput = container.querySelector('#multiplierTimeHide1xRatio');
  const saveFilters = () => {
    if (leagueSelect) localStorage.setItem(MULTIPLIER_TIME_LEAGUE_KEY, String(leagueSelect.value || ''));
    if (cycleFromInput) {
      const value = String(cycleFromInput.value || '').trim();
      if (value) localStorage.setItem(MULTIPLIER_TIME_CYCLE_FROM_KEY, value);
      else localStorage.removeItem(MULTIPLIER_TIME_CYCLE_FROM_KEY);
    }
    if (cycleToInput) {
      const value = String(cycleToInput.value || '').trim();
      if (value) localStorage.setItem(MULTIPLIER_TIME_CYCLE_TO_KEY, value);
      else localStorage.removeItem(MULTIPLIER_TIME_CYCLE_TO_KEY);
    }
    if (hide1xInput) {
      localStorage.setItem(MULTIPLIER_TIME_HIDE_1X_KEY, hide1xInput.checked ? '1' : '0');
    }
    if (hide1xRatioInput) {
      localStorage.setItem(MULTIPLIER_TIME_HIDE_1X_RATIO_KEY, hide1xRatioInput.checked ? '1' : '0');
    }
    commRenderMultiplierTime({ force: true }).catch(() => {});
  };
  [leagueSelect, cycleFromInput, cycleToInput, hide1xInput, hide1xRatioInput].forEach((el) => {
    if (!el) return;
    if (el.dataset.bound) return;
    el.addEventListener('change', saveFilters);
    el.dataset.bound = '1';
  });
  commRenderMultiplierTime({ force: false }).catch(() => {});
}

async function populateClans(containerId = 'clansGrid') {
  const container = document.getElementById(containerId);
  if (!container) return;
  const pageOptions = Array.from({ length: STATS_ROUND_HISTORY_MAX_PAGES }, (_, idx) => {
    const value = idx + 1;
    return `<option value="${value}">${value}</option>`;
  }).join('');
  const storedLeague = localStorage.getItem(STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY) || '';
  const leagueOptions = buildClanSelectionLeagueFilterOptions(storedLeague);
    const html = `
      <div class="stats-history-toolbar">
        <div class="stats-history-left">
          <div id="statsRoundHistoryStatus" class="muted">Loading round history...</div>
          <div id="statsRoundHistoryPageInfo" class="muted">Page ${statsRoundHistoryPage} of ${STATS_ROUND_HISTORY_MAX_PAGES}</div>
        </div>
        <div class="stats-history-filters">
          <div class="stats-filter-group">
            <span class="muted">Cycle</span>
            <input id="statsRoundHistoryCycleInput" class="stats-filter-input" type="number" min="1" placeholder="All">
          </div>
          <div class="stats-filter-group">
            <span class="muted">League</span>
            <select id="statsRoundHistoryLeagueInput" class="stats-filter-select">
              ${leagueOptions}
            </select>
          </div>
        </div>
        <div class="stats-history-controls">
          <button id="statsRoundHistoryPrev" class="btn sm ghost" type="button">Prev</button>
          <select id="statsRoundHistoryPageSelect" class="stats-filter-select">
            ${pageOptions}
          </select>
          <button id="statsRoundHistoryNext" class="btn sm ghost" type="button">Next</button>
          <button id="statsRoundHistoryRefresh" class="btn sm ghost" type="button">Refresh</button>
          <button id="statsRoundHistoryExport" class="btn sm ghost" type="button">Export XLSX</button>
        </div>
      </div>
      <div id="statsRoundHistoryList" class="stats-round-history"></div>
    `;
  renderCards(container, [{ title: 'Round history', htmlContent: html, id: 'round-history-card' }]);
  const card = container.querySelector('[data-card-id="round-history-card"]');
  const refreshBtn = card?.querySelector('#statsRoundHistoryRefresh');
  const exportBtn = card?.querySelector('#statsRoundHistoryExport');
  const prevBtn = card?.querySelector('#statsRoundHistoryPrev');
  const nextBtn = card?.querySelector('#statsRoundHistoryNext');
  const pageSelect = card?.querySelector('#statsRoundHistoryPageSelect');
  const cycleFilterInput = card?.querySelector('#statsRoundHistoryCycleInput');
  const leagueFilterInput = card?.querySelector('#statsRoundHistoryLeagueInput');
  if (refreshBtn) {
    refreshBtn.addEventListener('click', () => {
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true }).catch(() => {});
    });
  }
  if (exportBtn && !exportBtn.dataset.bound) {
    exportBtn.addEventListener('click', () => {
      exportRoundHistoryToXlsx().catch(() => {});
    });
    exportBtn.dataset.bound = '1';
  }
  if (cycleFilterInput && !cycleFilterInput.dataset.bound) {
    const storedCycle = localStorage.getItem(STATS_ROUND_HISTORY_CYCLE_FILTER_KEY) || '';
    if (storedCycle) cycleFilterInput.value = storedCycle;
    cycleFilterInput.addEventListener('change', () => {
      const value = cycleFilterInput.value.trim();
      if (value) {
        localStorage.setItem(STATS_ROUND_HISTORY_CYCLE_FILTER_KEY, value);
      } else {
        localStorage.removeItem(STATS_ROUND_HISTORY_CYCLE_FILTER_KEY);
      }
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true, page: 1 }).catch(() => {});
    });
    cycleFilterInput.dataset.bound = '1';
  }
  if (leagueFilterInput && !leagueFilterInput.dataset.bound) {
    if (storedLeague) leagueFilterInput.value = storedLeague;
    leagueFilterInput.addEventListener('change', () => {
      const value = leagueFilterInput.value.trim();
      if (value) {
        localStorage.setItem(STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY, value);
      } else {
        localStorage.removeItem(STATS_ROUND_HISTORY_LEAGUE_FILTER_KEY);
      }
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true, page: 1 }).catch(() => {});
    });
    leagueFilterInput.dataset.bound = '1';
  }
  if (pageSelect && !pageSelect.dataset.bound) {
    pageSelect.value = String(statsRoundHistoryPage);
    pageSelect.addEventListener('change', () => {
      const selected = Math.min(
        Math.max(1, Number(pageSelect.value) || 1),
        STATS_ROUND_HISTORY_MAX_PAGES
      );
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true, page: selected }).catch(() => {});
    });
    pageSelect.dataset.bound = '1';
  }
  if (prevBtn && !prevBtn.dataset.bound) {
    prevBtn.addEventListener('click', () => {
      const nextPage = Math.max(1, statsRoundHistoryPage - 1);
      if (nextPage === statsRoundHistoryPage) return;
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true, page: nextPage }).catch(() => {});
    });
    prevBtn.dataset.bound = '1';
  }
  if (nextBtn && !nextBtn.dataset.bound) {
    nextBtn.addEventListener('click', () => {
      const nextPage = Math.min(STATS_ROUND_HISTORY_MAX_PAGES, statsRoundHistoryPage + 1);
      if (nextPage === statsRoundHistoryPage) return;
      statsRoundHistoryLoaded = false;
      loadRoundHistory({ force: true, page: nextPage }).catch(() => {});
    });
    nextBtn.dataset.bound = '1';
  }
  statsRoundHistoryLoaded = false;
  loadRoundHistory({ force: true, page: statsRoundHistoryPage }).catch(() => {});
}

function commLoadClanSelectionFilters() {
  try {
    const raw = localStorage.getItem(CLAN_SELECTION_FILTERS_KEY);
    if (!raw) return {};
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === 'object' ? parsed : {};
  } catch (err) {
    return {};
  }
}

function commSaveClanSelectionFilters(filters) {
  try {
    localStorage.setItem(CLAN_SELECTION_FILTERS_KEY, JSON.stringify(filters || {}));
  } catch (err) {
    /* ignore */
  }
}

function commLoadClanSelectionExtraFilters() {
  try {
    const raw = localStorage.getItem(CLAN_SELECTION_EXTRA_FILTERS_KEY);
    if (!raw) return { noWhale: false, minTh: '', maxTh: '', minAvgBlocks: '', clanType: 'all' };
    const parsed = JSON.parse(raw);
    return {
      noWhale: !!parsed?.noWhale,
      minTh: parsed?.minTh == null ? '' : String(parsed.minTh),
      maxTh: parsed?.maxTh == null ? '' : String(parsed.maxTh),
      minAvgBlocks: parsed?.minAvgBlocks == null ? '' : String(parsed.minAvgBlocks),
      clanType: parsed?.clanType ? String(parsed.clanType) : 'all'
    };
  } catch (err) {
    return { noWhale: false, minTh: '', maxTh: '', minAvgBlocks: '', clanType: 'all' };
  }
}

function commSaveClanSelectionExtraFilters(filters) {
  try {
    localStorage.setItem(CLAN_SELECTION_EXTRA_FILTERS_KEY, JSON.stringify(filters || {}));
  } catch (err) {
    /* ignore */
  }
}

function commGetClanSelectionExtraFilters(card) {
  const stored = commLoadClanSelectionExtraFilters();
  const result = { ...stored };
  if (!card) return result;
  const noWhaleInput = card.querySelector('#clanSelectionNoWhale');
  const minThInput = card.querySelector('#clanSelectionMinTh');
  const maxThInput = card.querySelector('#clanSelectionMaxTh');
  const minAvgInput = card.querySelector('#clanSelectionMinAvg');
  const clanTypeInput = card.querySelector('#clanSelectionClanType');
  if (noWhaleInput) result.noWhale = noWhaleInput.checked;
  if (minThInput) result.minTh = minThInput.value.trim();
  if (maxThInput) result.maxTh = maxThInput.value.trim();
  if (minAvgInput) result.minAvgBlocks = minAvgInput.value.trim();
  if (clanTypeInput) result.clanType = clanTypeInput.value.trim() || 'all';
  return result;
}

function commGetClanSelectionFilters(card) {
  const stored = commLoadClanSelectionFilters();
  const filters = {};
  CLAN_SELECTION_FILTER_KEYS.forEach((key) => {
    filters[key] = stored[key] == null ? '' : String(stored[key]);
  });
  if (!card) return filters;
  CLAN_SELECTION_FILTER_KEYS.forEach((key) => {
    const input = card.querySelector(`[data-clan-filter="${key}"]`);
    if (input) filters[key] = input.value.trim();
  });
  return filters;
}

function commStringifyRaw(value) {
  if (value === null || value === undefined) return '';
  if (typeof value === 'string') return value;
  try {
    return JSON.stringify(value);
  } catch (err) {
    return String(value);
  }
}

function commFormatClanSelectionRaw(value) {
  if (value === null || value === undefined) return '-';
  if (typeof value === 'string') return value;
  try {
    return JSON.stringify(value, null, 2);
  } catch (err) {
    return String(value);
  }
}

function commApplyClanSelectionFilters(rows, filters) {
  if (!Array.isArray(rows) || rows.length === 0) return [];
  const normalized = {};
  CLAN_SELECTION_FILTER_KEYS.forEach((key) => {
    normalized[key] = (filters?.[key] || '').toString().trim().toLowerCase();
  });
  return rows.filter((row) => {
    if (!row) return false;
    const rawText = commStringifyRaw(row?.raw).toLowerCase();
    const clanNameValue = String(row?.clan_name ?? '').toLowerCase();
    const cycle = String(row?.cycle_id ?? '').toLowerCase();
    if (normalized.cycle_id && !cycle.includes(normalized.cycle_id)) return false;
    const leagueFilter = normalized.league_id;
    const leagueValue = commParseNumber(row?.league_id);
    if (leagueFilter) {
      if (leagueFilter === 'dune-all' || leagueFilter === 'dune') {
        if (!Number.isFinite(leagueValue)) return false;
        if (leagueValue < CLAN_SELECTION_DUNE_START || leagueValue > CLAN_SELECTION_DUNE_END) return false;
      } else {
        const leagueFilterNum = commParseNumber(leagueFilter);
        if (Number.isFinite(leagueFilterNum)) {
          if (!Number.isFinite(leagueValue)) return false;
          if (Math.trunc(leagueValue) !== Math.trunc(leagueFilterNum)) return false;
        } else {
          const leagueText = String(row?.league_id ?? '').toLowerCase();
          if (!leagueText.includes(leagueFilter)) return false;
        }
      }
    }
    if (normalized.clan_name && !`${clanNameValue} ${rawText}`.includes(normalized.clan_name)) return false;
    const clanPower = String(row?.clan_power ?? '').toLowerCase();
    if (normalized.clan_power && !clanPower.includes(normalized.clan_power)) return false;
    const usersCount = String(row?.users_count ?? '').toLowerCase();
    if (normalized.users_count && !usersCount.includes(normalized.users_count)) return false;
    return true;
  });
}

function commTruncateText(text, max = 140) {
  if (!text) return '';
  const value = String(text);
  if (value.length <= max) return value;
  return `${value.slice(0, Math.max(0, max - 1))}\u2026`;
}

function commFormatJoinRequirements(requirements) {
  if (!Array.isArray(requirements)) return '';
  let minGmt = null;
  let minTh = null;
  requirements.forEach((req) => {
    const type = req?.type;
    const value = commParseNumber(req?.data?.value ?? req?.value);
    if (!Number.isFinite(value)) return;
    if (type === 'min-gmt') minGmt = value;
    if (type === 'min-th') minTh = value;
  });
  const parts = [];
  if (Number.isFinite(minGmt)) parts.push(`Min GMT ${formatCount(minGmt)}`);
  if (Number.isFinite(minTh)) parts.push(`Min TH ${formatCount(minTh)}`);
  return parts.join(' \u2022 ');
}

function commBuildClanSelectionOverview(row) {
  const raw = parseMaybeJson(row?.raw) || {};
  const name = raw?.name ?? row?.clan_name ?? '-';
  const leagueId = raw?.leagueId ?? row?.league_id ?? '-';
  const rank = raw?.clanRank ?? null;
  const type = raw?.type ?? null;
  const power = raw?.power ?? row?.clan_power ?? null;
  const powerValue = commParseNumber(power);
  const blocksCycleValue = commGetClanAvgCycleValue(row, raw);
  const users = raw?.usersCount ?? row?.users_count ?? null;
  const nfts = raw?.nftsCount ?? null;
  const blocks = raw?.blocksMined ?? null;
  const btcIncome = raw?.btcIncome ?? null;
  const gmtIncome = raw?.gmtIncome ?? null;
  const powerShare = raw?.powerShare ?? null;
  const scoreShare = raw?.scoreShare ?? null;
  const telegram = raw?.telegramName ?? null;
  const description = raw?.description ?? null;
  const joinReq = commFormatJoinRequirements(raw?.joinRequirements);
  const subParts = [];
  const leagueLabel = commFormatLeagueDisplay(leagueId);
  if (leagueLabel !== '-') subParts.push(leagueLabel);
  if (rank != null) subParts.push(`Rank ${rank}`);
  if (type) subParts.push(type);
  const statsParts = [];
  if (Number.isFinite(powerValue)) statsParts.push(`Power ${formatCount(Math.round(powerValue))}`);
  if (Number.isFinite(blocksCycleValue)) statsParts.push(`Blocks/cycle ${formatCount(Math.round(blocksCycleValue))}`);
  if (users != null) statsParts.push(`Users ${formatCount(users)}`);
  if (nfts != null) statsParts.push(`NFTs ${formatCount(nfts)}`);
  if (blocks != null) statsParts.push(`Blocks ${formatCount(blocks)}`);
  const incomeParts = [];
  if (btcIncome != null) incomeParts.push(`BTC ${formatDecimal(btcIncome, 3)}`);
  if (gmtIncome != null) incomeParts.push(`GMT ${formatCount(gmtIncome)}`);
  const shareParts = [];
  if (powerShare != null) shareParts.push(`P${formatDecimal(powerShare, 3)}`);
  if (scoreShare != null) shareParts.push(`S${formatDecimal(scoreShare, 3)}`);
  return `
    <div class="clan-selection-overview">
      <div class="clan-selection-overview-title">${commEscape(name)}</div>
      ${subParts.length ? `<div class="clan-selection-overview-sub">${commEscape(subParts.join(' \u2022 '))}</div>` : ''}
      ${statsParts.length ? `<div class="clan-selection-overview-line">${commEscape(statsParts.join(' \u2022 '))}</div>` : ''}
      ${incomeParts.length ? `<div class="clan-selection-overview-line">${commEscape(incomeParts.join(' \u2022 '))}</div>` : ''}
      ${shareParts.length ? `<div class="clan-selection-overview-line">${commEscape(shareParts.join(' \u2022 '))}</div>` : ''}
      ${joinReq ? `<div class="clan-selection-overview-line">${commEscape(joinReq)}</div>` : ''}
      ${telegram ? `<div class="clan-selection-overview-meta">Telegram: ${commEscape(telegram)}</div>` : ''}
      ${description ? `<div class="clan-selection-overview-desc">${commEscape(commTruncateText(description, 160))}</div>` : ''}
    </div>
  `;
}

function commBuildClanMemberStatsKey(row) {
  const cycle = commParseNumber(row?.cycle_id ?? row?.cycleId);
  const league = commParseNumber(row?.league_id ?? row?.leagueId);
  const clan = commParseNumber(row?.clan_id ?? row?.clanId);
  const cycleKey = Number.isFinite(cycle) ? Math.trunc(cycle) : 'x';
  const leagueKey = Number.isFinite(league) ? Math.trunc(league) : 'x';
  const clanKey = Number.isFinite(clan) ? Math.trunc(clan) : 'x';
  return `${cycleKey}:${leagueKey}:${clanKey}`;
}

async function commFetchClanMemberStatsGroup({ cycleId, leagueId, clanIds }) {
  const results = [];
  if (!Array.isArray(clanIds) || clanIds.length === 0) return results;
  const limit = 1000;
  const chunkSize = 100;
  for (let i = 0; i < clanIds.length; i += chunkSize) {
    const chunk = clanIds.slice(i, i + chunkSize);
    if (!chunk.length) continue;
    let offset = 0;
    while (true) {
      const queryParts = [
        'select=clan_id,user_power,raw',
        `clan_id=in.(${chunk.join(',')})`,
        `limit=${limit}`,
        `offset=${offset}`
      ];
      if (Number.isFinite(cycleId)) {
        queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleId))}`);
      }
      if (Number.isFinite(leagueId)) {
        queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueId))}`);
      }
      const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_MEMBERS, queryParts.join('&'));
      if (!Array.isArray(rows) || rows.length === 0) break;
      results.push(...rows);
      if (rows.length < limit) break;
      offset += limit;
    }
  }
  return results;
}

async function commEnsureClanSelectionMemberStats(rows, status) {
  if (!Array.isArray(rows) || rows.length === 0) return;
  const groups = new Map();
  rows.forEach((row) => {
    const cycleId = commParseNumber(row?.cycle_id ?? row?.cycleId);
    const leagueId = commParseNumber(row?.league_id ?? row?.leagueId);
    const clanId = commParseNumber(row?.clan_id ?? row?.clanId);
    if (!Number.isFinite(clanId)) return;
    const groupKey = `${Number.isFinite(cycleId) ? Math.trunc(cycleId) : 'x'}:${Number.isFinite(leagueId) ? Math.trunc(leagueId) : 'x'}`;
    if (!groups.has(groupKey)) {
      groups.set(groupKey, {
        cycleId: Number.isFinite(cycleId) ? Math.trunc(cycleId) : null,
        leagueId: Number.isFinite(leagueId) ? Math.trunc(leagueId) : null,
        clanIds: new Set()
      });
    }
    groups.get(groupKey).clanIds.add(Math.trunc(clanId));
  });
  for (const group of groups.values()) {
    const missing = [];
    group.clanIds.forEach((clanId) => {
      const key = `${Number.isFinite(group.cycleId) ? group.cycleId : 'x'}:${Number.isFinite(group.leagueId) ? group.leagueId : 'x'}:${clanId}`;
      if (!clanSelectionMemberStats.has(key)) missing.push(clanId);
    });
    if (!missing.length) continue;
    if (status) status.textContent = `Loading member stats for ${missing.length} clans...`;
    const rows = await commFetchClanMemberStatsGroup({
      cycleId: group.cycleId,
      leagueId: group.leagueId,
      clanIds: missing
    });
    rows.forEach((member) => {
      const clanId = commParseNumber(member?.clan_id);
      if (!Number.isFinite(clanId)) return;
      const key = `${Number.isFinite(group.cycleId) ? group.cycleId : 'x'}:${Number.isFinite(group.leagueId) ? group.leagueId : 'x'}:${Math.trunc(clanId)}`;
      const power = commParseNumber(member?.user_power);
      const stats = clanSelectionMemberStats.get(key) || {
        maxPower: null,
        hasWhale: false,
        members: 0,
        boostedMembers: 0,
        totalBoosts: 0,
        avgBoostsPerMember: null,
        boostedPercent: null
      };
      if (Number.isFinite(power)) {
        stats.maxPower = stats.maxPower == null ? power : Math.max(stats.maxPower, power);
        if (power >= CLAN_SELECTION_WHALE_THRESHOLD) stats.hasWhale = true;
      }
      stats.members += 1;
      const memberRaw = parseMaybeJson(member?.raw) || {};
      const abilities =
        Array.isArray(memberRaw?.usedNftGameAbilities) ? memberRaw.usedNftGameAbilities :
        (Array.isArray(memberRaw?.usedAbilities) ? memberRaw.usedAbilities : []);
      let memberBoosts = 0;
      if (Array.isArray(abilities)) {
        abilities.forEach((ability) => {
          const count = commParseNumber(ability?.count);
          if (Number.isFinite(count)) memberBoosts += count;
        });
      }
      stats.totalBoosts += memberBoosts;
      if (memberBoosts > 0) stats.boostedMembers += 1;
      if (stats.members > 0) {
        stats.avgBoostsPerMember = stats.totalBoosts / stats.members;
        stats.boostedPercent = stats.boostedMembers / stats.members;
      }
      clanSelectionMemberStats.set(key, stats);
    });
    missing.forEach((clanId) => {
      const key = `${Number.isFinite(group.cycleId) ? group.cycleId : 'x'}:${Number.isFinite(group.leagueId) ? group.leagueId : 'x'}:${clanId}`;
      if (!clanSelectionMemberStats.has(key)) {
        clanSelectionMemberStats.set(key, {
          maxPower: null,
          hasWhale: false,
          members: 0,
          boostedMembers: 0,
          totalBoosts: 0,
          avgBoostsPerMember: null,
          boostedPercent: null
        });
      }
    });
  }
}

async function commApplyClanSelectionMemberFilters(rows, extraFilters, status) {
  const noWhale = !!extraFilters?.noWhale;
  const minClanTh = commParseNumber(extraFilters?.minTh);
  const maxClanTh = commParseNumber(extraFilters?.maxTh);
  const minAvgBlocks = commParseNumber(extraFilters?.minAvgBlocks);
  const clanType = (extraFilters?.clanType || 'all').toLowerCase();
  const needsTypeFilter = clanType !== 'all';
  if (
    !noWhale &&
    !needsTypeFilter &&
    !Number.isFinite(minClanTh) &&
    !Number.isFinite(maxClanTh) &&
    !Number.isFinite(minAvgBlocks)
  ) {
    return rows;
  }
  let filtered = rows;
  if (Number.isFinite(minClanTh) || Number.isFinite(maxClanTh) || Number.isFinite(minAvgBlocks)) {
    filtered = rows.filter((row) => {
      const raw = parseMaybeJson(row?.raw);
      const power = commParseNumber(row?.clan_power ?? raw?.power);
      if (!Number.isFinite(power)) return false;
      if (Number.isFinite(minClanTh) && power < minClanTh) return false;
      if (Number.isFinite(maxClanTh) && power > maxClanTh) return false;
      if (Number.isFinite(minAvgBlocks)) {
        const avgCycle = commGetClanAvgCycleValue(row, raw);
        if (!Number.isFinite(avgCycle)) return false;
        if (avgCycle < minAvgBlocks) return false;
      }
      return true;
    });
  }
  if (!noWhale && !needsTypeFilter) return filtered;
  await commEnsureClanSelectionMemberStats(filtered, status);
  return filtered.filter((row) => {
    const key = commBuildClanMemberStatsKey(row);
    const stats = clanSelectionMemberStats.get(key);
    if (!stats) return false;
    if (noWhale && stats.hasWhale) return false;
    if (needsTypeFilter) {
      const avgBoosts = stats.avgBoostsPerMember ?? 0;
      const boostedPct = stats.boostedPercent ?? 0;
      const isBoosting = avgBoosts >= 1 || boostedPct >= 0.3;
      const isFarming = avgBoosts <= 0.2;
      if (clanType === 'boosting' && !isBoosting) return false;
      if (clanType === 'farming' && !isFarming) return false;
    }
    return true;
  });
}

function ensureClanSelectionRawPopup() {
  if (clanSelectionRawPopupOverlay) return;
  const overlay = document.createElement('div');
  overlay.className = 'round-history-popup-overlay clan-selection-raw-popup-overlay';
  overlay.innerHTML = `
    <div class="round-history-popup clan-selection-raw-popup" role="dialog" aria-modal="true">
      <div class="round-history-popup-header">
        <div class="round-history-popup-title"></div>
        <button type="button" class="round-history-popup-close" aria-label="Close">x</button>
      </div>
      <div class="round-history-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  clanSelectionRawPopupOverlay = overlay;
  clanSelectionRawPopupTitle = overlay.querySelector('.round-history-popup-title');
  clanSelectionRawPopupBody = overlay.querySelector('.round-history-popup-body');
  const closeBtn = overlay.querySelector('.round-history-popup-close');
  if (closeBtn) {
    closeBtn.addEventListener('click', () => {
      overlay.style.display = 'none';
    });
  }
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) overlay.style.display = 'none';
  });
}

function showClanSelectionRawPopup(index) {
  ensureClanSelectionRawPopup();
  if (!clanSelectionRawPopupOverlay || !clanSelectionRawPopupTitle || !clanSelectionRawPopupBody) return;
  const row = clanSelectionVisibleRows[index];
  if (!row) return;
  const raw = parseMaybeJson(row?.raw) ?? row?.raw;
  const rawObj = raw && typeof raw === 'object' ? raw : null;
  const name = rawObj?.name ?? row?.clan_name ?? 'Unknown clan';
  const id = rawObj?.id ?? row?.clan_id ?? '-';
  clanSelectionRawPopupTitle.textContent = `${name} (RAW #${id})`;
  clanSelectionRawPopupBody.innerHTML = commBuildKorrelRawTable(raw);
  clanSelectionRawPopupOverlay.style.display = 'flex';
}

function ensureClanSelectionMembersPopup() {
  if (clanSelectionMembersPopupOverlay) return;
  const overlay = document.createElement('div');
  overlay.className = 'round-history-popup-overlay clan-selection-members-popup-overlay';
  overlay.innerHTML = `
    <div class="round-history-popup clan-selection-members-popup" role="dialog" aria-modal="true">
      <div class="round-history-popup-header clan-selection-members-header">
        <div class="round-history-popup-title"></div>
        <div class="clan-selection-members-controls">
          <button type="button" class="btn sm ghost" id="clanMembersPrevBtn">Prev</button>
          <select id="clanMembersPageSelect" class="stats-filter-select"></select>
          <button type="button" class="btn sm ghost" id="clanMembersNextBtn">Next</button>
        </div>
        <button type="button" class="round-history-popup-close" aria-label="Close">x</button>
      </div>
      <div id="clanMembersStatus" class="muted" style="margin-bottom:8px;"></div>
      <div class="round-history-popup-body"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  clanSelectionMembersPopupOverlay = overlay;
  clanSelectionMembersPopupTitle = overlay.querySelector('.round-history-popup-title');
  clanSelectionMembersPopupBody = overlay.querySelector('.round-history-popup-body');
  clanSelectionMembersPopupStatus = overlay.querySelector('#clanMembersStatus');
  clanSelectionMembersPageSelect = overlay.querySelector('#clanMembersPageSelect');
  clanSelectionMembersPrevBtn = overlay.querySelector('#clanMembersPrevBtn');
  clanSelectionMembersNextBtn = overlay.querySelector('#clanMembersNextBtn');
  const closeBtn = overlay.querySelector('.round-history-popup-close');
  if (closeBtn) {
    closeBtn.addEventListener('click', () => {
      overlay.style.display = 'none';
    });
  }
  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) overlay.style.display = 'none';
  });
  if (clanSelectionMembersPrevBtn) {
    clanSelectionMembersPrevBtn.addEventListener('click', () => {
      if (!clanSelectionMembersState) return;
      const prev = Math.max(1, clanSelectionMembersState.page - 1);
      if (prev === clanSelectionMembersState.page) return;
      loadClanSelectionMembersPage(prev).catch(() => {});
    });
  }
  if (clanSelectionMembersNextBtn) {
    clanSelectionMembersNextBtn.addEventListener('click', () => {
      if (!clanSelectionMembersState) return;
      const next = Math.min(clanSelectionMembersState.pages || 1, clanSelectionMembersState.page + 1);
      if (next === clanSelectionMembersState.page) return;
      loadClanSelectionMembersPage(next).catch(() => {});
    });
  }
  if (clanSelectionMembersPageSelect) {
    clanSelectionMembersPageSelect.addEventListener('change', () => {
      if (!clanSelectionMembersState) return;
      const selected = Math.max(1, Number(clanSelectionMembersPageSelect.value) || 1);
      if (selected === clanSelectionMembersState.page) return;
      loadClanSelectionMembersPage(selected).catch(() => {});
    });
  }
}

function commBuildClanMembersTable(rows) {
  const header = `
    <tr>
      <th>User ID</th>
      <th>Name</th>
      <th>Power</th>
      <th>Owner</th>
      <th>Joined</th>
      <th>Boosts</th>
    </tr>
  `;
  const body = Array.isArray(rows) && rows.length
    ? rows.map((row) => {
      const raw = parseMaybeJson(row?.raw) || {};
      const userId = row?.user_id ?? raw?.id ?? '-';
      const name = row?.user_name ?? raw?.alias ?? raw?.user?.alias ?? raw?.name ?? '-';
      const power = row?.user_power ?? raw?.power ?? null;
      const owner = raw?.isOwner ? 'yes' : '-';
      const joinDate = raw?.joinDate ?? raw?.joinedAt ?? null;
      const boosts = buildBoostPillsHtml(raw?.usedNftGameAbilities ?? raw?.usedAbilities ?? []);
      return `
        <tr>
          <td>${commEscape(userId)}</td>
          <td>${commEscape(name)}</td>
          <td>${commEscape(power == null ? '-' : formatDecimal(power, 2))}</td>
          <td>${commEscape(owner)}</td>
          <td>${commEscape(joinDate ? formatDateTimeValue(joinDate) : '-')}</td>
          <td>${boosts}</td>
        </tr>
      `;
    }).join('')
    : `<tr><td colspan="6" class="muted">No members found.</td></tr>`;
  return `
    <table class="clan-selection-members-table">
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

async function loadClanSelectionMembersPage(page = 1) {
  ensureClanSelectionMembersPopup();
  if (!clanSelectionMembersState || !clanSelectionMembersPopupBody) return;
  const state = clanSelectionMembersState;
  const offset = (page - 1) * CLAN_SELECTION_MEMBERS_PER_PAGE;
  if (clanSelectionMembersPopupStatus) {
    clanSelectionMembersPopupStatus.textContent = 'Loading members...';
  }
  const queryParts = [
    'select=user_id,user_name,user_power,fetched_at,raw',
    `clan_id=eq.${encodeURIComponent(state.clanId)}`,
    `limit=${CLAN_SELECTION_MEMBERS_PER_PAGE}`,
    `offset=${offset}`,
    'order=user_power.desc.nullslast'
  ];
  if (Number.isFinite(state.cycleId)) {
    queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(state.cycleId))}`);
  }
  if (Number.isFinite(state.leagueId)) {
    queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(state.leagueId))}`);
  }
  try {
    const { rows, count } = await commFetchWithCount(COMM_TABLE_CLAN_ROSTER_MEMBERS, queryParts.join('&'));
    const total = Number.isFinite(count) ? count : rows.length;
    const pages = Math.max(1, Math.ceil(total / CLAN_SELECTION_MEMBERS_PER_PAGE));
    state.page = Math.min(Math.max(1, page), pages);
    state.total = total;
    state.pages = pages;
    if (clanSelectionMembersPopupTitle) {
      clanSelectionMembersPopupTitle.textContent = `${state.clanName} members`;
    }
    if (clanSelectionMembersPopupStatus) {
      clanSelectionMembersPopupStatus.textContent = `Page ${state.page} of ${pages} (${total} members)`;
    }
    if (clanSelectionMembersPageSelect) {
      clanSelectionMembersPageSelect.innerHTML = Array.from({ length: pages }, (_, idx) => {
        const value = idx + 1;
        return `<option value="${value}">${value}</option>`;
      }).join('');
      clanSelectionMembersPageSelect.value = String(state.page);
    }
    if (clanSelectionMembersPrevBtn) {
      clanSelectionMembersPrevBtn.disabled = state.page <= 1;
    }
    if (clanSelectionMembersNextBtn) {
      clanSelectionMembersNextBtn.disabled = state.page >= pages;
    }
    clanSelectionMembersPopupBody.innerHTML = commBuildClanMembersTable(rows);
    clanSelectionMembersPopupOverlay.style.display = 'flex';
  } catch (err) {
    if (clanSelectionMembersPopupStatus) {
      clanSelectionMembersPopupStatus.textContent = err?.message || 'Failed to load members.';
    }
    clanSelectionMembersPopupBody.innerHTML = '<div class="muted">Failed to load members.</div>';
    clanSelectionMembersPopupOverlay.style.display = 'flex';
  }
}

function showClanSelectionMembersPopup(index) {
  const row = clanSelectionVisibleRows[index];
  if (!row) return;
  const raw = parseMaybeJson(row?.raw) || {};
  const clanName = raw?.name ?? row?.clan_name ?? `Clan ${row?.clan_id ?? ''}`.trim();
  clanSelectionMembersState = {
    clanId: commParseNumber(row?.clan_id),
    leagueId: commParseNumber(row?.league_id),
    cycleId: commParseNumber(row?.cycle_id),
    clanName: clanName || 'Clan members',
    page: 1,
    total: null,
    pages: 1
  };
  loadClanSelectionMembersPage(1).catch(() => {});
}

function buildClanSelectionLeagueFilterOptions(currentValue) {
  const current = currentValue == null ? '' : String(currentValue);
  const options = [];
  const addOption = (value, label) => {
    const selected = current === String(value);
    options.push(
      `<option value="${commEscape(String(value))}"${selected ? ' selected' : ''}>${commEscape(label)}</option>`
    );
  };
  addOption('', 'All');
  addOption('1', 'Odyssey');
  addOption('3', 'Eclipse');
  addOption('4', 'Horizon');
  addOption('dune-all', 'Dune (all)');
  for (let id = CLAN_SELECTION_DUNE_START; id <= CLAN_SELECTION_DUNE_END; id += 1) {
    addOption(String(id), commGetLeagueName(id));
  }
  return options.join('');
}

function buildClanSelectionTable(rows, filters) {
  clanSelectionVisibleRows = Array.isArray(rows) ? rows : [];
  const safeFilters = filters || {};
  const leagueFilterValue = (safeFilters.league_id || '').toString().trim();
  const filterValue = (key) => commEscape(safeFilters[key] || '');
  const cycleOptions = Array.from({ length: 5 }, (_, idx) => {
    const cycle = getCycleNumber(new Date()) - idx;
    if (!Number.isFinite(cycle)) return null;
    return cycle;
  }).filter((value) => Number.isFinite(value));
  const cycleFilterValue = (safeFilters.cycle_id || '').toString().trim();
  const cycleOptionsHtml = cycleOptions.map((cycle) => {
    const selected = String(cycle) === cycleFilterValue;
    return `<option value="${commEscape(String(cycle))}"${selected ? ' selected' : ''}>${commEscape(String(cycle))}</option>`;
  }).join('');
  const leagueOptions = buildClanSelectionLeagueFilterOptions(leagueFilterValue);
  const header = `
    <tr>
      <th class="clan-selection-col-cycle">Cycle</th>
      <th class="clan-selection-col-league">League</th>
      <th class="clan-selection-col-name">Clan overview</th>
      <th class="clan-selection-col-power">Power</th>
      <th class="clan-selection-col-users">Users</th>
      <th class="clan-selection-col-raw">Raw</th>
    </tr>
    <tr class="clan-selection-filter-row">
      <th>
        <select class="stats-filter-select clan-selection-filter-input" data-clan-filter="cycle_id">
          ${cycleOptionsHtml}
        </select>
      </th>
      <th>
        <select class="stats-filter-select clan-selection-filter-input" data-clan-filter="league_id">
          ${leagueOptions}
        </select>
      </th>
      <th><input class="stats-filter-input clan-selection-filter-input" data-clan-filter="clan_name" type="text" value="${filterValue('clan_name')}" placeholder="Search raw"></th>
      <th><input class="stats-filter-input clan-selection-filter-input" data-clan-filter="clan_power" type="text" value="${filterValue('clan_power')}" placeholder="Any"></th>
      <th><input class="stats-filter-input clan-selection-filter-input" data-clan-filter="users_count" type="text" value="${filterValue('users_count')}" placeholder="Any"></th>
      <th class="clan-selection-filter-empty"><span class="muted">-</span></th>
    </tr>
  `;
  const body = Array.isArray(rows) && rows.length
    ? rows.map((row, index) => {
      const cycle = row?.cycle_id ?? '-';
      const league = commFormatLeagueDisplay(row?.league_id);
      const clanOverview = commBuildClanSelectionOverview(row);
      const clanPowerValue = commParseNumber(row?.clan_power);
      const clanPower = Number.isFinite(clanPowerValue) ? formatCount(Math.round(clanPowerValue)) : '-';
      const usersCount = row?.users_count != null ? formatCount(row.users_count) : '-';
      const rawButton = `
        <div class="clan-selection-action-buttons">
          <button type="button" class="clan-selection-raw-btn" data-raw-index="${index}">RAW</button>
          <button type="button" class="clan-selection-members-btn" data-members-index="${index}">Members</button>
        </div>
      `;
      return `
        <tr>
          <td class="clan-selection-col-cycle">${commEscape(cycle)}</td>
          <td class="clan-selection-col-league">${commEscape(league)}</td>
          <td class="clan-selection-col-name">${clanOverview}</td>
          <td class="clan-selection-col-power">${commEscape(clanPower)}</td>
          <td class="clan-selection-col-users">${commEscape(usersCount)}</td>
          <td class="clan-selection-col-raw">${rawButton}</td>
        </tr>
      `;
    }).join('')
    : `<tr><td colspan="6" class="muted">No clans found.</td></tr>`;
  return `
    <table class="clan-selection-table">
      <thead>${header}</thead>
      <tbody>${body}</tbody>
    </table>
  `;
}

async function loadClanSelection({ force = false, filters = null, extraFilters = null } = {}) {
  if (clanSelectionLoading) return;
  if (clanSelectionLoaded && !force && clanSelectionRowsCache.length) {
    const wrap = document.getElementById('clanSelectionTableWrap');
    const status = document.getElementById('clanSelectionStatus');
    const activeFilters = filters || commGetClanSelectionFilters(wrap?.closest('[data-card-id]'));
    const activeExtraFilters = extraFilters || commGetClanSelectionExtraFilters(wrap?.closest('[data-card-id]'));
    await commEnsureClanSelectionCycleBlockCounts(activeFilters);
    const filteredBase = commApplyClanSelectionFilters(clanSelectionRowsCache, activeFilters);
    const filteredMembers = await commApplyClanSelectionMemberFilters(filteredBase, activeExtraFilters, status);
    if (wrap) wrap.innerHTML = buildClanSelectionTable(filteredMembers, activeFilters);
    if (status) status.textContent = `Loaded ${filteredMembers.length} clans (cached ${clanSelectionRowsCache.length}).`;
    return;
  }
  clanSelectionLoading = true;
  const status = document.getElementById('clanSelectionStatus');
  if (status) status.textContent = 'Loading clan selection...';
  const activeFilters = filters || commGetClanSelectionFilters(document.getElementById('clanSelectionGrid'));
  const activeExtraFilters = extraFilters || commGetClanSelectionExtraFilters(document.getElementById('clanSelectionGrid'));
  try {
    const latestCycle = getCycleNumber(new Date());
    if (Number.isFinite(latestCycle) && !activeFilters.cycle_id) {
      activeFilters.cycle_id = String(Math.trunc(latestCycle));
      commSaveClanSelectionFilters(activeFilters);
    }
    await commEnsureClanSelectionCycleBlockCounts(activeFilters);
    const queryParts = [
      'select=cycle_id,league_id,clan_id,clan_name,clan_power,users_count,raw',
      `limit=${COMM_KORREL_ROSTER_DB_LIMIT}`,
      'order=clan_name.asc'
    ];
    const cycleParsed = commParseNumber(activeFilters.cycle_id);
    if (Number.isFinite(cycleParsed)) {
      queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleParsed))}`);
    }
    const leagueParsed = commParseNumber(activeFilters.league_id);
    if (Number.isFinite(leagueParsed)) {
      queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueParsed))}`);
    }
    const rows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, queryParts.join('&'));
    clanSelectionRowsCache = Array.isArray(rows) ? rows : [];
    const filteredBase = commApplyClanSelectionFilters(clanSelectionRowsCache, activeFilters);
    const filtered = await commApplyClanSelectionMemberFilters(filteredBase, activeExtraFilters, status);
    const wrap = document.getElementById('clanSelectionTableWrap');
    if (wrap) wrap.innerHTML = buildClanSelectionTable(filtered, activeFilters);
    if (status) status.textContent = `Loaded ${filtered.length} clans (total ${clanSelectionRowsCache.length}).`;
    clanSelectionLoaded = true;
  } catch (err) {
    const wrap = document.getElementById('clanSelectionTableWrap');
    if (wrap) wrap.innerHTML = '<div class="muted">Failed to load clan selection.</div>';
    if (status) status.textContent = err?.message || 'Failed to load clan selection.';
  } finally {
    clanSelectionLoading = false;
  }
}

async function fetchAllClanSelectionRows(activeFilters, onProgress) {
  const rows = [];
  const pageSize = COMM_KORREL_ROSTER_DB_LIMIT;
  let offset = 0;
  let page = 1;
  for (;;) {
    const queryParts = [
      'select=cycle_id,league_id,clan_id,clan_name,clan_power,users_count,raw',
      `limit=${pageSize}`,
      `offset=${offset}`,
      'order=clan_name.asc'
    ];
    const cycleParsed = commParseNumber(activeFilters?.cycle_id);
    if (Number.isFinite(cycleParsed)) {
      queryParts.unshift(`cycle_id=eq.${encodeURIComponent(Math.trunc(cycleParsed))}`);
    }
    const leagueParsed = commParseNumber(activeFilters?.league_id);
    if (Number.isFinite(leagueParsed)) {
      queryParts.unshift(`league_id=eq.${encodeURIComponent(Math.trunc(leagueParsed))}`);
    }
    const pageRows = await commFetch(COMM_TABLE_CLAN_ROSTER_CLANS, queryParts.join('&'));
    const safeRows = Array.isArray(pageRows) ? pageRows : [];
    rows.push(...safeRows);
    if (typeof onProgress === 'function') {
      onProgress({ page, pageSize: safeRows.length, total: rows.length });
    }
    if (!safeRows.length || safeRows.length < pageSize) break;
    offset += pageSize;
    page += 1;
  }
  return rows;
}

async function exportClanSelectionToXlsx() {
  if (clanSelectionExporting) return;
  if (!ensureXlsxLibrary()) return;
  const status = document.getElementById('clanSelectionStatus');
  const exportBtn = document.getElementById('clanSelectionExport');
  const card = document.querySelector('[data-card-id="clan-selection-card"]');
  clanSelectionExporting = true;
  if (exportBtn) exportBtn.disabled = true;
  try {
    const activeFilters = commGetClanSelectionFilters(card);
    const activeExtraFilters = commGetClanSelectionExtraFilters(card);
    if (status) status.textContent = 'Exporting clan selection...';
    await commEnsureClanSelectionCycleBlockCounts(activeFilters);
    const allRows = await fetchAllClanSelectionRows(activeFilters, (progress) => {
      if (status) {
        status.textContent = `Exporting clan selection... page ${progress.page} (${progress.total} rows)`;
      }
    });
    const filteredBase = commApplyClanSelectionFilters(allRows, activeFilters);
    const filtered = await commApplyClanSelectionMemberFilters(filteredBase, activeExtraFilters, status);
    if (!filtered.length) {
      if (status) status.textContent = 'No clan selection data available to export.';
      return;
    }
    const filename = `clan_selection_${getExportTimestamp()}.xlsx`;
    exportRowsToXlsx(filtered, filename, 'Clan selection');
    if (status) status.textContent = `Exported ${filtered.length} clans.`;
  } catch (err) {
    if (status) status.textContent = err?.message || 'Failed to export clan selection.';
  } finally {
    clanSelectionExporting = false;
    if (exportBtn) exportBtn.disabled = false;
  }
}

function populateClanSelection() {
  const container = document.getElementById('clanSelectionGrid');
  if (!container) return;
  const html = `
    <div class="clan-selection-toolbar">
      <div class="clan-selection-toolbar-left">
        <div id="clanSelectionStatus" class="muted">Loading clan selection...</div>
        <label class="stats-filter-toggle clan-selection-toggle">
          <input id="clanSelectionNoWhale" type="checkbox">
          No whale (350+ TH)
        </label>
        <div class="stats-filter-group clan-selection-min-th">
          <span class="muted">Min clan TH</span>
          <input id="clanSelectionMinTh" class="stats-filter-input" type="number" min="0" step="1" placeholder="Any">
        </div>
        <div class="stats-filter-group clan-selection-max-th">
          <span class="muted">Max clan TH</span>
          <input id="clanSelectionMaxTh" class="stats-filter-input" type="number" min="0" step="1" placeholder="Any">
        </div>
        <div class="stats-filter-group clan-selection-min-avg">
          <span class="muted">Min blocks/cycle</span>
          <input id="clanSelectionMinAvg" class="stats-filter-input" type="number" min="0" step="1" placeholder="Any">
        </div>
        <div class="stats-filter-group clan-selection-boost-type">
          <span class="muted">Type</span>
          <select id="clanSelectionClanType" class="stats-filter-select">
            <option value="all">All</option>
            <option value="boosting">Boosting</option>
            <option value="farming">Farming</option>
          </select>
        </div>
      </div>
      <div class="clan-selection-toolbar-actions">
        <button id="clanSelectionRefresh" class="btn sm ghost" type="button">Refresh</button>
        <button id="clanSelectionExport" class="btn sm ghost" type="button">Export XLSX</button>
      </div>
    </div>
    <div id="clanSelectionTableWrap" class="clan-selection-table-wrap"></div>
  `;
  renderCards(container, [{ title: 'Clan selection', htmlContent: html, id: 'clan-selection-card' }]);
  const card = container.querySelector('[data-card-id="clan-selection-card"]');
  if (!card) return;
  const refreshBtn = card.querySelector('#clanSelectionRefresh');
  const exportBtn = card.querySelector('#clanSelectionExport');
  const noWhaleInput = card.querySelector('#clanSelectionNoWhale');
  const minThInput = card.querySelector('#clanSelectionMinTh');
  const maxThInput = card.querySelector('#clanSelectionMaxTh');
  const minAvgInput = card.querySelector('#clanSelectionMinAvg');
  const clanTypeInput = card.querySelector('#clanSelectionClanType');
  const extraFilters = commLoadClanSelectionExtraFilters();
  if (noWhaleInput) noWhaleInput.checked = !!extraFilters.noWhale;
  if (minThInput) minThInput.value = extraFilters.minTh || '';
  if (maxThInput) maxThInput.value = extraFilters.maxTh || '';
  if (minAvgInput) minAvgInput.value = extraFilters.minAvgBlocks || '';
  if (clanTypeInput) clanTypeInput.value = extraFilters.clanType || 'all';
  if (refreshBtn && !refreshBtn.dataset.bound) {
    refreshBtn.addEventListener('click', () => {
      clanSelectionLoaded = false;
      loadClanSelection({ force: true }).catch(() => {});
    });
    refreshBtn.dataset.bound = '1';
  }
  if (exportBtn && !exportBtn.dataset.bound) {
    exportBtn.addEventListener('click', () => {
      exportClanSelectionToXlsx().catch(() => {});
    });
    exportBtn.dataset.bound = '1';
  }
  if (noWhaleInput && !noWhaleInput.dataset.bound) {
    noWhaleInput.addEventListener('change', () => {
      const filters = commGetClanSelectionExtraFilters(card);
      commSaveClanSelectionExtraFilters(filters);
      clanSelectionLoaded = false;
      loadClanSelection({ force: true, extraFilters: filters }).catch(() => {});
    });
    noWhaleInput.dataset.bound = '1';
  }
  if (maxThInput && !maxThInput.dataset.bound) {
    maxThInput.addEventListener('input', () => {
      const filters = commGetClanSelectionExtraFilters(card);
      commSaveClanSelectionExtraFilters(filters);
      if (clanSelectionFilterTimer) clearTimeout(clanSelectionFilterTimer);
      clanSelectionFilterTimer = setTimeout(() => {
        clanSelectionLoaded = false;
        loadClanSelection({ force: true, extraFilters: filters }).catch(() => {});
      }, 300);
    });
    maxThInput.dataset.bound = '1';
  }
  if (minThInput && !minThInput.dataset.bound) {
    minThInput.addEventListener('input', () => {
      const filters = commGetClanSelectionExtraFilters(card);
      commSaveClanSelectionExtraFilters(filters);
      if (clanSelectionFilterTimer) clearTimeout(clanSelectionFilterTimer);
      clanSelectionFilterTimer = setTimeout(() => {
        clanSelectionLoaded = false;
        loadClanSelection({ force: true, extraFilters: filters }).catch(() => {});
      }, 300);
    });
    minThInput.dataset.bound = '1';
  }
  if (minAvgInput && !minAvgInput.dataset.bound) {
    minAvgInput.addEventListener('input', () => {
      const filters = commGetClanSelectionExtraFilters(card);
      commSaveClanSelectionExtraFilters(filters);
      if (clanSelectionFilterTimer) clearTimeout(clanSelectionFilterTimer);
      clanSelectionFilterTimer = setTimeout(() => {
        clanSelectionLoaded = false;
        loadClanSelection({ force: true, extraFilters: filters }).catch(() => {});
      }, 300);
    });
    minAvgInput.dataset.bound = '1';
  }
  if (clanTypeInput && !clanTypeInput.dataset.bound) {
    clanTypeInput.addEventListener('change', () => {
      const filters = commGetClanSelectionExtraFilters(card);
      commSaveClanSelectionExtraFilters(filters);
      clanSelectionLoaded = false;
      loadClanSelection({ force: true, extraFilters: filters }).catch(() => {});
    });
    clanTypeInput.dataset.bound = '1';
  }
  if (!card.dataset.filtersBound) {
    card.addEventListener('input', (event) => {
      const input = event.target.closest('[data-clan-filter]');
      if (!input) return;
      const filters = commGetClanSelectionFilters(card);
      commSaveClanSelectionFilters(filters);
      if (clanSelectionFilterTimer) clearTimeout(clanSelectionFilterTimer);
      clanSelectionFilterTimer = setTimeout(() => {
        loadClanSelection({ force: false, filters }).catch(() => {});
      }, 200);
    });
    card.addEventListener('change', (event) => {
      const input = event.target.closest('[data-clan-filter]');
      if (!input) return;
      const filters = commGetClanSelectionFilters(card);
      commSaveClanSelectionFilters(filters);
      if (clanSelectionFilterTimer) clearTimeout(clanSelectionFilterTimer);
      clanSelectionFilterTimer = setTimeout(() => {
        loadClanSelection({ force: false, filters }).catch(() => {});
      }, 200);
    });
    card.dataset.filtersBound = '1';
  }
  if (!card.dataset.rawBound) {
    card.addEventListener('click', (event) => {
      const btn = event.target.closest('.clan-selection-raw-btn');
      if (!btn) return;
      const index = Number(btn.dataset.rawIndex);
      if (!Number.isFinite(index)) return;
      showClanSelectionRawPopup(index);
    });
    card.dataset.rawBound = '1';
  }
  if (!card.dataset.membersBound) {
    card.addEventListener('click', (event) => {
      const btn = event.target.closest('.clan-selection-members-btn');
      if (!btn) return;
      const index = Number(btn.dataset.membersIndex);
      if (!Number.isFinite(index)) return;
      showClanSelectionMembersPopup(index);
    });
    card.dataset.membersBound = '1';
  }
  const storedFilters = commLoadClanSelectionFilters();
  const storedExtra = commLoadClanSelectionExtraFilters();
  if (Object.keys(storedFilters).length || storedExtra.noWhale || storedExtra.maxTh) {
    loadClanSelection({ force: true, filters: storedFilters, extraFilters: storedExtra }).catch(() => {});
  } else {
    loadClanSelection({ force: true }).catch(() => {});
  }
}

function setSubscriptionSuggestStatus(message, { error = false } = {}) {
  subscriptionSuggestStatus = {
    message: message || '',
    error: !!error
  };
}

function formatSubscriptionPublic(value) {
  if (value === true) return 'Public';
  if (value === false) return 'Private';
  return 'Unspecified';
}

function buildSubscriptionLicenseInfo(licenseRow, storedKey) {
  const key = licenseRow?.license_key || storedKey || '-';
  const active = licenseRow?.active ?? (homeLicenseState?.valid ? true : null);
  const expiresAt = licenseRow?.expires_at || homeLicenseState?.expires_at || null;
  const note = licenseRow?.note || homeLicenseState?.message || '-';
  return `
    <div class="subscription-section">
      <h3 class="subscription-title">License details</h3>
      <table class="subscription-license-table">
        <tbody>
          <tr>
            <th>License key</th>
            <td>${commEscape(key || '-')}</td>
          </tr>
          <tr>
            <th>Expires at</th>
            <td>${expiresAt ? commEscape(formatDateTimeValue(expiresAt)) : '-'}</td>
          </tr>
          <tr>
            <th>Active</th>
            <td>${active === true ? 'Yes' : active === false ? 'No' : '-'}</td>
          </tr>
          <tr>
            <th>Note</th>
            <td>${commEscape(note || '-')}</td>
          </tr>
        </tbody>
      </table>
    </div>
  `;
}

function buildSubscriptionSuggestionForm() {
  const status = subscriptionSuggestStatus.message
    ? `<div class="subscription-status ${subscriptionSuggestStatus.error ? 'error' : 'ok'}">${commEscape(subscriptionSuggestStatus.message)}</div>`
    : '';
  return `
    <div class="subscription-section">
      <h3 class="subscription-title">Submit a suggestion</h3>
      <div class="subscription-note">License holders can submit suggestions.</div>
      <div class="subscription-note">A valid license key is required to submit suggestions.</div>
      ${status}
      <form id="subscriptionSuggestForm" class="subscription-suggest-form">
        <label>
          Name suggestion
          <input id="subscriptionSuggestName" type="text" placeholder="Short title">
        </label>
        <label>
          Description suggestion
          <textarea id="subscriptionSuggestDescription" rows="3" placeholder="Describe your suggestion"></textarea>
        </label>
        <div class="subscription-row">
          <label>
            Public?
            <select id="subscriptionSuggestPublic">
              <option value="">Unspecified</option>
              <option value="public">Public</option>
              <option value="private">Private</option>
            </select>
          </label>
          <label>
            Rationale (for private)
            <input id="subscriptionSuggestRationale" type="text" placeholder="Why should this stay private?">
          </label>
        </div>
        <button class="btn sm" type="submit">Submit suggestion</button>
      </form>
    </div>
  `;
}

async function fetchSubscriptionSuggestions(userId, licenseKey) {
  const fields = [
    'id',
    'license_holder_id',
    'license_key',
    'name',
    'description',
    'is_public',
    'rationale',
    'created_at'
  ].join(',');
  const queryParts = [
    `select=${fields}`,
    'order=created_at.desc',
    'limit=200'
  ];
  if (userId || licenseKey) {
    const filters = ['is_public.eq.true'];
    if (userId) filters.push(`license_holder_id.eq.${encodeURIComponent(userId)}`);
    if (licenseKey) filters.push(`license_key.eq.${encodeURIComponent(licenseKey)}`);
    queryParts.unshift(`or=(${filters.join(',')})`);
  } else {
    queryParts.unshift('is_public=eq.true');
  }
  const rows = await commFetch(COMM_TABLE_SUBSCRIPTION_SUGGESTIONS, queryParts.join('&'));
  return Array.isArray(rows) ? rows : [];
}

function buildSubscriptionSuggestionsTable(rows, userId, licenseKey) {
  const safeRows = Array.isArray(rows) ? rows : [];
  const visibleRows = userId || licenseKey
    ? safeRows
    : safeRows.filter((row) => row?.is_public === true);
  if (!visibleRows.length) {
    return `
      <div class="subscription-section">
        <h3 class="subscription-title">Suggestions</h3>
        <div class="muted">No suggestions yet.</div>
      </div>
    `;
  }
  const body = visibleRows.map((row) => {
    return `
      <tr>
        <td>${commEscape(row?.name || '-')}</td>
        <td>${commEscape(row?.description || '-')}</td>
        <td>${commEscape(formatSubscriptionPublic(row?.is_public))}</td>
        <td>${row?.created_at ? commEscape(formatDateTimeValue(row.created_at)) : '-'}</td>
      </tr>
    `;
  }).join('');
  return `
    <div class="subscription-section">
      <h3 class="subscription-title">Suggestions</h3>
      <table class="subscription-suggestions-table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Visibility</th>
            <th>Created</th>
          </tr>
        </thead>
        <tbody>${body}</tbody>
      </table>
    </div>
  `;
}

function bindSubscriptionHandlers(container) {
  const form = container.querySelector('#subscriptionSuggestForm');
  if (!form) return;
  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    const userId = getHomeAuthUserId();
    const storedKey = String(localStorage.getItem(COMM_LICENSE_STORAGE_KEY) || '').trim();
    if (!storedKey) {
      setSubscriptionSuggestStatus('Enter a license key to submit suggestions.', { error: true });
      populateSubscription();
      return;
    }
    if (!userId) {
      const licenseState = await validateLicenseKey(storedKey);
      if (!licenseState?.valid) {
        setSubscriptionSuggestStatus(licenseState?.reason || 'License required to submit suggestions.', { error: true });
        populateSubscription();
        return;
      }
    }
    const nameInput = container.querySelector('#subscriptionSuggestName');
    const descInput = container.querySelector('#subscriptionSuggestDescription');
    const publicInput = container.querySelector('#subscriptionSuggestPublic');
    const rationaleInput = container.querySelector('#subscriptionSuggestRationale');
    const name = nameInput?.value?.trim() || '';
    const description = descInput?.value?.trim() || '';
    const visibility = publicInput?.value || '';
    const rationale = rationaleInput?.value?.trim() || '';
    if (!name || !description) {
      setSubscriptionSuggestStatus('Please provide a name and description.', { error: true });
      populateSubscription();
      return;
    }
    if (visibility === 'private' && !rationale) {
      setSubscriptionSuggestStatus('Private suggestions require a rationale.', { error: true });
      populateSubscription();
      return;
    }
    const payload = {
      license_holder_id: userId || null,
      license_key: storedKey || null,
      name,
      description,
      rationale: rationale || null
    };
    if (visibility === 'public') payload.is_public = true;
    if (visibility === 'private') payload.is_public = false;
    try {
      await commInsert(COMM_TABLE_SUBSCRIPTION_SUGGESTIONS, [payload]);
      setSubscriptionSuggestStatus('Suggestion submitted.', { error: false });
    } catch (err) {
      setSubscriptionSuggestStatus(err?.message || 'Failed to submit suggestion.', { error: true });
    }
    populateSubscription();
  });
}

async function populateSubscription(containerId = 'subscriptionGrid') {
  const container = document.getElementById(containerId);
  if (!container) return;
  try {
    loadHomeGoogleProfile();
    await ensureHomeAuthUser();
    const storedKey = String(localStorage.getItem(COMM_LICENSE_STORAGE_KEY) || '').trim();
    const licenseRow = storedKey ? await fetchLicenseRow(storedKey) : null;
    const licenseInfo = buildSubscriptionLicenseInfo(licenseRow, storedKey);
    const userId = getHomeAuthUserId();
    const suggestions = await fetchSubscriptionSuggestions(userId, storedKey || null);
    const html = `
      <div class="subscription-block">
        ${licenseInfo}
        ${buildSubscriptionSuggestionForm()}
        ${buildSubscriptionSuggestionsTable(suggestions, userId, storedKey || null)}
      </div>
    `;
    renderCards(container, [{ id: 'subscription-card', htmlContent: html }]);
    bindSubscriptionHandlers(container);
  } catch (err) {
    renderCards(container, [{ title: 'Subscriptions', description: err.message, meta: '' }]);
  }
}

// API Statistics tracking
const apiStats = {
  calls: {},
  history: []
};
const trackedApiPayloadCache = new Map();

const gameApiMonitor = {
  startTime: Date.now(),
  calls: new Map()
};

function buildGameApiMonitorSummary() {
  return {
    startTime: gameApiMonitor.startTime,
    calls: Array.from(gameApiMonitor.calls.values()).map((entry) => ({
      endpoint: entry.endpoint,
      method: entry.method,
      count: entry.count,
      avgInterval: entry.avgInterval,
      callsPerMinute: entry.callsPerMinute,
      lastCall: entry.lastCall
    }))
  };
}

function renderGameApiMonitorSection(summary) {
  const container = document.getElementById('gameApiMonitorSummary');
  if (!container) return;
  const calls = Array.isArray(summary?.calls) ? summary.calls : [];
  const totalCalls = calls.reduce((sum, row) => sum + (Number(row.count) || 0), 0);
  const uniqueEndpoints = calls.length;
  const lastCallRow = calls.reduce((latest, row) => {
    if (!row?.lastCall) return latest;
    if (!latest || row.lastCall > latest.lastCall) return row;
    return latest;
  }, null);
  const lastCallText = lastCallRow && lastCallRow.lastCall
    ? new Date(lastCallRow.lastCall).toLocaleTimeString()
    : 'Never';

  // Show ALL endpoints sorted by call count
  const allCalls = calls.slice().sort((a, b) => (Number(b.count) || 0) - (Number(a.count) || 0));

  container.innerHTML = `
    <div class="admin-api-monitor-summary" style="padding: 12px; border-radius: 8px; border: 1px solid rgba(148, 163, 184, 0.4); background: rgba(15, 23, 42, 0.6); margin-bottom: 16px;">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
        <div>
          <strong style="font-size: 1rem;">Live API Live API Monitor</strong>
          <div style="font-size: 0.85rem; color: rgba(148, 163, 184, 0.9);">Tracking calls from gomining.com</div>
        </div>
        <div style="font-size: 0.85rem; color: rgba(148, 163, 184, 0.9);">
          Total endpoints: ${uniqueEndpoints}
        </div>
      </div>
      <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px;">
        <div style="padding: 8px; border-radius: 6px; background: rgba(30, 41, 59, 0.8);">
          <div style="font-size: 0.7rem; color: #94a3b8; text-transform: uppercase;">Total calls</div>
          <div style="font-size: 1.3rem; font-weight: 600;">${totalCalls}</div>
        </div>
        <div style="padding: 8px; border-radius: 6px; background: rgba(30, 41, 59, 0.8);">
          <div style="font-size: 0.7rem; color: #94a3b8; text-transform: uppercase;">Last call</div>
          <div style="font-size: 1rem; font-weight: 600;">${lastCallText}</div>
        </div>
        <div style="padding: 8px; border-radius: 6px; background: rgba(30, 41, 59, 0.8);">
          <div style="font-size: 0.7rem; color: #94a3b8; text-transform: uppercase;">Active since</div>
          <div style="font-size: 1rem; font-weight: 600;">${new Date(summary?.startTime || Date.now()).toLocaleTimeString()}</div>
        </div>
      </div>
      <div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; font-weight: 600;">All Endpoints</div>
      <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px;">
        ${allCalls.map((entry) => {
          // Extract everything after /nft-game/ from the endpoint
          const nftGameIndex = entry.endpoint.indexOf('/nft-game/');
          const apiPath = nftGameIndex !== -1
            ? entry.endpoint.substring(nftGameIndex + '/nft-game/'.length)
            : entry.endpoint.split('/').slice(-2).join('/'); // fallback to last 2 parts

          return `
          <div style="padding: 8px; border-radius: 6px; background: rgba(15, 23, 42, 0.9); border: 1px solid rgba(59, 130, 246, 0.3); cursor: help;" title="${entry.endpoint}">
            <div style="font-size: 0.7rem; color: #94a3b8;">${entry.method}</div>
            <div style="font-size: 0.8rem; font-weight: 500; color: #e2e8f0; word-break: break-word; line-height: 1.3;">${apiPath}</div>
            <div style="font-size: 0.95rem; font-weight: 600; margin-top: 4px; color: #3b82f6;">${entry.count || 0} calls</div>
          </div>
        `;
        }).join('')}
        ${allCalls.length === 0 ? '<div style="grid-column: span 3; font-size: 0.85rem; color: #94a3b8; text-align: center; padding: 20px;">No data yet; perform API calls from the game tab to begin monitoring.</div>' : ''}
      </div>
    </div>
  `;
}

async function fetchGameApiMonitorSummary() {
  try {
    const rows = await commFetch('game_api_monitoring', '?select=endpoint,method,total_calls,avg_interval_ms,calls_per_minute,last_call');
    if (!Array.isArray(rows)) throw new Error('Invalid monitor rows');
    return {
      startTime: latestGameApiMonitorSummary.startTime || Date.now(),
      calls: rows.map((row) => ({
        endpoint: row?.endpoint || '',
        method: row?.method || 'GET',
        count: Number(row?.total_calls) || 0,
        avgInterval: Number(row?.avg_interval_ms) || 0,
        callsPerMinute: Number(row?.calls_per_minute) || 0,
        lastCall: row?.last_call ? new Date(row.last_call).getTime() : null
      }))
    };
  } catch (err) {
    console.warn('[MW] Failed to fetch game API monitor summary:', err);
    return latestGameApiMonitorSummary || buildGameApiMonitorSummary();
  }
}

function trackGameApiCall(endpoint, method = 'GET') {
  if (!endpoint) return;
  const normalizedEndpoint = String(endpoint).split('?')[0];
  const normalizedMethod = (method || 'GET').toUpperCase();
  const key = `${normalizedMethod} ${normalizedEndpoint}`;
  const now = Date.now();
  const existing = gameApiMonitor.calls.get(key) || {
    endpoint: normalizedEndpoint,
    method: normalizedMethod,
    count: 0,
    avgInterval: 0,
    intervalCount: 0,
    callsPerMinute: 0,
    lastCall: null
  };
  if (existing.lastCall && now > existing.lastCall) {
    const interval = now - existing.lastCall;
    existing.intervalCount += 1;
    existing.avgInterval = Math.round(((existing.avgInterval || 0) * (existing.intervalCount - 1) + interval) / existing.intervalCount);
  }
  existing.count += 1;
  existing.lastCall = now;
  const elapsedMs = Math.max(now - gameApiMonitor.startTime, 1);
  const cpm = (existing.count / (elapsedMs / 60000));
  existing.callsPerMinute = Number.isFinite(cpm) ? Number(cpm.toFixed(2)) : 0;
  gameApiMonitor.calls.set(key, existing);
  latestGameApiMonitorSummary = buildGameApiMonitorSummary();
  renderGameApiMonitorSection(latestGameApiMonitorSummary);
}

let latestGameApiMonitorSummary = buildGameApiMonitorSummary();

function mergeGameApiMonitorSummary(summary) {
  if (!summary?.calls) return;
  latestGameApiMonitorSummary = {
    startTime: summary.startTime || latestGameApiMonitorSummary.startTime,
    calls: Array.isArray(summary.calls) ? summary.calls : latestGameApiMonitorSummary.calls
  };
  renderGameApiMonitorSection(latestGameApiMonitorSummary);
}

async function saveGameApiStatsToDb() {
  if (!COMM_SUPABASE_URL || !COMM_SUPABASE_KEY) return;

  const summary = await fetchGameApiMonitorSummary();
  const stats = (summary.calls || []).map((stat) => ({
    endpoint: stat.endpoint,
    method: stat.method,
    total_calls: stat.count,
    avg_interval_ms: stat.avgInterval,
    calls_per_minute: stat.callsPerMinute,
    last_call: stat.lastCall ? new Date(stat.lastCall).toISOString() : null
  }));
  if (!stats.length) return;

  try {
    await commInsert('game_api_monitoring', stats, { onConflict: 'endpoint,method' });
  } catch (err) {
    console.error('[MW] Failed to save game API stats to Supabase:', err);
  }
}

// Save stats every 5 minutes
setInterval(() => saveGameApiStatsToDb(), 300000);

const ROUND_STATE_ENDPOINT = 'https://api.gomining.com/api/nft-game/round/get-state';
const API_DEFINITIONS = {
  'user-league': {
    name: 'User League Data',
    description: 'Warning Requires auth - Get current user league position (used in Live tab)',
    endpoint: 'https://api.gomining.com/api/nft-game/league/get-user-positions-data',
    method: 'POST',
    usedIn: ['Live tab (auto)', 'Game website'],
    requiresAuth: true,
    fields: [],
    example: {}
  },
  'league-index': {
    name: 'League Index',
    description: 'Get all leagues with details and requirements',
    endpoint: 'https://api.gomining.com/api/nft-game/league/index',
    method: 'POST',
    usedIn: ['Game website', 'Admin Tools'],
    requiresAuth: false,
    fields: [
      { name: 'calculatedAt', type: 'datetime', description: 'Calculation timestamp', default: () => new Date().toISOString() }
    ],
    example: { calculatedAt: '2026-01-14T00:00:00.000Z' }
  },
  'league-maintenance': {
    name: 'League Maintenance Status',
    description: 'Check if league system is under maintenance',
    endpoint: 'https://api.gomining.com/api/nft-game/league/maintenance-status',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: false,
    fields: [],
    example: {}
  },
  'clan-leaderboard': {
    name: 'Clan Leaderboard V2',
    description: 'Get clan rankings with NFT power for a league (use limit: 1 for best performance)',
    endpoint: 'https://api.gomining.com/api/nft-game/clan-leaderboard/index-v2',
    method: 'POST',
    usedIn: ['Live tab (auto 3min)', 'Game leaderboard'],
    requiresAuth: false,
    fields: [
      { name: 'calculatedAt', type: 'datetime', description: 'Timestamp', default: () => new Date().toISOString() },
      { name: 'leagueId', type: 'number', description: 'League ID (1-13)', default: () => commKorrelCurrentLeagueId || 13 },
      { name: 'pagination.skip', type: 'number', description: 'Skip records', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Max records (use 1 for efficiency)', default: 1 }
    ],
    example: {
      calculatedAt: '2026-01-14T00:00:00.000Z',
      leagueId: 13,
      pagination: { skip: 0, limit: 1 }
    }
  },
  'user-leaderboard': {
    name: 'User Leaderboard',
    description: 'Get individual user rankings for a league',
    endpoint: 'https://api.gomining.com/api/nft-game/user-leaderboard/index',
    method: 'POST',
    usedIn: ['Game leaderboard'],
    requiresAuth: false,
    useTrackedPayload: true,
    forceTrackedPayloadFallback: true,
    trackedPayloadFallback: {
      pagination: { skip: 0, limit: 1 },
      calculatedAt: () => new Date().toISOString(),
      leagueId: () => commKorrelCurrentLeagueId || 13
    },
    useTrackedPayload: true,
    fields: [
      { name: 'calculatedAt', type: 'datetime', description: 'Timestamp', default: () => new Date().toISOString() },
      { name: 'leagueId', type: 'number', description: 'League ID', default: () => commKorrelCurrentLeagueId || 13 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 50 }
    ],
    example: {
      calculatedAt: '2026-01-14T00:00:00.000Z',
      leagueId: 13,
      pagination: { skip: 0, limit: 50 }
    }
  },
  'round-get-last': {
    name: 'Get Last Round',
    description: 'Get the most recent round information',
    endpoint: 'https://api.gomining.com/api/nft-game/round/get-last',
    method: 'GET',
    usedIn: ['Game website'],
    requiresAuth: true,
    fields: [],
    example: null
  },
  'round-state': {
    name: 'Round State (Live Scores)',
    description: 'Warning Requires auth - Get LIVE scores for current round - clans or users',
    endpoint: 'https://api.gomining.com/api/nft-game/round/get-state',
    method: 'POST',
    usedIn: ['Game leaderboard', 'Could replace calculated scores'],
    requiresAuth: true,
    fields: [
      { name: 'type', type: 'select', options: ['clans', 'users'], description: 'Leaderboard type', default: 'clans' },
      { name: 'leagueId', type: 'number', description: 'League ID', default: () => commKorrelCurrentLeagueId || 13 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 50 }
    ],
    example: {
      type: 'clans',
      pagination: { skip: 0, limit: 50 },
      leagueId: 13
    }
  },
  'round-by-cycle': {
    name: 'Round by Cycle ID',
    description: 'Get round data by cycle ID with multipliers',
    endpoint: 'https://api.gomining.com/api/nft-game/round/find-by-cycleId',
    method: 'POST',
    usedIn: ['Game history'],
    requiresAuth: false,
    fields: [
      { name: 'cycleId', type: 'number', description: 'Cycle ID', default: 126 },
      { name: 'leagueId', type: 'number', description: 'League ID', default: () => commKorrelCurrentLeagueId || 13 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 50 }
    ],
    example: {
      cycleId: 126,
      multipliers: [1, 1.5, 2, 3, 5, 10],
      pagination: { skip: 0, limit: 50 },
      leagueId: 13
    }
  },
  'round-clan-leaderboard': {
    name: 'Round Clan Leaderboard',
    description: 'Get clan leaderboard for specific finished round',
    endpoint: 'https://api.gomining.com/api/nft-game/round/clan-leaderboard',
    method: 'POST',
    usedIn: ['Game history'],
    requiresAuth: false,
    useTrackedPayload: true,
    forceTrackedPayloadFallback: true,
    trackedPayloadFallback: {
      pagination: { skip: 0, limit: 1 },
      calculatedAt: () => new Date().toISOString(),
      leagueId: () => commKorrelCurrentLeagueId || 13
    },
    fields: [
      { name: 'calculatedAt', type: 'datetime', description: 'Timestamp', default: () => new Date().toISOString() },
      { name: 'leagueId', type: 'number', description: 'League ID', default: () => commKorrelCurrentLeagueId || 13 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 1 }
    ],
    example: {
      calculatedAt: new Date().toISOString(),
      leagueId: 13,
      pagination: { skip: 0, limit: 1 }
    }
  },
  'round-user-leaderboard': {
    name: 'Round User Leaderboard',
    description: 'Get user leaderboard for specific finished round',
    endpoint: 'https://api.gomining.com/api/nft-game/round/user-leaderboard',
    method: 'POST',
    usedIn: ['Game history'],
    requiresAuth: false,
    fields: [
      { name: 'roundId', type: 'number', description: 'Round ID', default: 642195 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 50 }
    ],
    example: {
      roundId: 642195,
      pagination: { skip: 0, limit: 50 }
    }
  },
  'clan-get-my': {
    name: 'Get My Clan',
    description: 'Warning Requires auth - Get current user clan details',
    endpoint: 'https://api.gomining.com/api/nft-game/clan/get-my',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: true,
    fields: [],
    example: {}
  },
  'clan-get-by-id': {
    name: 'Get Clan by ID',
    description: 'Warning Requires auth - Get specific clan details with members',
    endpoint: 'https://api.gomining.com/api/nft-game/clan/get-by-id',
    method: 'POST',
    usedIn: ['Game clan view'],
    requiresAuth: true,
    fields: [
      { name: 'clanId', type: 'number', description: 'Clan ID', default: 38244 },
      { name: 'pagination.skip', type: 'number', description: 'Skip', default: 0 },
      { name: 'pagination.limit', type: 'number', description: 'Limit', default: 50 }
    ],
    example: {
      clanId: 38244,
      pagination: { skip: 0, limit: 50 },
      filters: {},
      sort: {}
    }
  },
  'user-profile': {
    name: 'Get My Profile',
    description: 'Warning Requires auth - Get current user profile data',
    endpoint: 'https://api.gomining.com/api/nft-game/nft-game-user-profile/get-my',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: true,
    fields: [],
    example: {}
  },
  'abilities': {
    name: 'Find All Abilities',
    description: 'Warning Requires auth - Get all available abilities/boosts',
    endpoint: 'https://api.gomining.com/api/nft-game/nft-game-ability/find-all',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: true,
    fields: [],
    example: {}
  },
  'token-balances': {
    name: 'Get Token Balances',
    description: 'Warning Requires auth - Get user token/coin balances',
    endpoint: 'https://api.gomining.com/api/nft-game/nft-game-token/get-balances',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: true,
    fields: [],
    example: {}
  },
  'chat-messages': {
    name: 'Chat Messages',
    description: 'Warning Requires auth - Get chat messages for clan',
    endpoint: 'https://api.gomining.com/api/nft-game/chat-message/find-by-destination',
    method: 'POST',
    usedIn: ['Game chat'],
    requiresAuth: true,
    fields: [
      { name: 'limit', type: 'number', description: 'Max messages', default: 20 },
      { name: 'destination', type: 'select', options: ['clan', 'global'], description: 'Chat type', default: 'clan' }
    ],
    example: {
      limit: 20,
      destination: 'clan'
    }
  },
  'maintenance-state': {
    name: 'Maintenance State',
    description: 'Check if game is under maintenance',
    endpoint: 'https://api.gomining.com/api/action/get-maintenance-state',
    method: 'POST',
    usedIn: ['Game website'],
    requiresAuth: false,
    fields: [],
    example: {}
  }
};

async function populateAdminTools() {
  const container = document.getElementById('adminToolsGrid');
  if (!container) return;

  const apiTabs = Object.keys(API_DEFINITIONS).map(key => {
    const def = API_DEFINITIONS[key];
    return `<button class="admin-api-tab" data-api="${key}">${def.name}</button>`;
  }).join('');

  const korrelToolsHtml = buildKorrelToolsMarkup();
  const html = `
    <div class="admin-tools-block">
      <div class="admin-tools-tabs">
        <button class="admin-tools-tab active" type="button" data-admin-tab="api">API Explorer</button>
        <button class="admin-tools-tab" type="button" data-admin-tab="korrel">Korrel tools</button>
      </div>
      <div class="admin-tools-panel active" data-admin-panel="api">
        <div id="adminGameTabWarning" class="admin-game-tab-warning" style="display:none;"></div>
        <h3>API Explorer</h3>
        <p class="muted">Professional API testing and monitoring tool</p>
        ${WEBSITE_MODE ? '<div class="comm-card" style="margin-top:8px;">Website mode: GoMining API injection is disabled.</div>' : ''}

        <div class="admin-api-tabs" style="display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; padding-bottom: 12px; border-bottom: 2px solid #333;">
          ${apiTabs}
          <button class="admin-api-tab" data-api="stats">?Y"S Statistics</button>
        </div>

        <div id="gameApiMonitorSummary"></div>

        <div id="adminApiContent"></div>
      </div>
      <div class="admin-tools-panel" data-admin-panel="korrel">
        ${korrelToolsHtml}
      </div>
    </div>
  `;

  renderCards(container, [{ id: 'admin-tools-card', htmlContent: html }]);
  const tabButtons = Array.from(container.querySelectorAll('.admin-tools-tab'));
  const tabPanels = Array.from(container.querySelectorAll('.admin-tools-panel'));
  const korrelPanel = container.querySelector('[data-admin-panel="korrel"]');
  const isKorrelUser = commIsKorrel();
  const korrelCard = container.querySelector('#commKorrelTools');
  if (korrelCard) {
    korrelCard.style.display = 'block';
  }
  if (!isKorrelUser && korrelPanel) {
    const notice = document.createElement('div');
    notice.className = 'comm-card';
    notice.style.marginBottom = '10px';
    notice.innerHTML = `
      <div style="font-weight:600;margin-bottom:4px;">Korrel tools</div>
      <div class="muted" style="font-size:0.78rem;">Limited mode: sign in as Korrel to unlock full write access.</div>
    `;
    korrelPanel.prepend(notice);
  }
  const setAdminTab = (target) => {
    if (!target) return;
    tabButtons.forEach((btn) => btn.classList.toggle('active', btn.dataset.adminTab === target));
    tabPanels.forEach((panel) => panel.classList.toggle('active', panel.dataset.adminPanel === target));
  };
  tabButtons.forEach((btn) => {
    if (btn.dataset.bound) return;
    btn.addEventListener('click', () => setAdminTab(btn.dataset.adminTab));
    btn.dataset.bound = '1';
  });
  setAdminTab('api');
  commLoadKorrelApiSettings();
  commUpdateKorrelApiSettingsUI();
  initClanComm();
  bindAdminToolsHandlers();
  renderGameApiMonitorSection(latestGameApiMonitorSummary);
  // Load first API by default
  if (!WEBSITE_MODE) {
    showApiTab(Object.keys(API_DEFINITIONS)[0]);
  }
  refreshGameTabWarning();
}

function buildGameApiMonitoringSection(endpoint, method) {
  const key = `${method} ${endpoint}`;
  const monitorData = gameApiMonitor.calls.get(key);

  if (!monitorData || monitorData.count === 0) {
    return `
      <div style="background: rgba(100, 116, 139, 0.1); border: 1px solid rgba(100, 116, 139, 0.2); border-radius: 8px; padding: 12px; margin: 16px 0;">
        <strong style="color: #94a3b8;">Stats Game Website Usage</strong>
        <p style="margin: 8px 0 0 0; font-size: 0.9rem; color: rgba(226, 232, 240, 0.6);">
          No calls detected from the game website yet. This API hasn't been used by the game since monitoring started.
        </p>
      </div>
    `;
  }

  const intervalText = monitorData.avgInterval
    ? monitorData.avgInterval < 1000
      ? `${monitorData.avgInterval}ms`
      : `${(monitorData.avgInterval / 1000).toFixed(1)}s`
    : 'N/A';

  const lastCallText = monitorData.lastCall
    ? new Date(monitorData.lastCall).toLocaleTimeString()
    : 'Never';

  return `
    <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 12px; margin: 16px 0;">
      <strong style="color: #3b82f6;">Stats Game Website Usage Statistics</strong>
      <p style="margin: 8px 0 0 0; font-size: 0.9rem; color: rgba(226, 232, 240, 0.8);">
        Real-time monitoring of how often the game website calls this API:
      </p>
      <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px;">
        <div style="background: rgba(0, 0, 0, 0.3); padding: 8px; border-radius: 4px;">
          <div style="font-size: 0.75rem; color: rgba(226, 232, 240, 0.6); text-transform: uppercase; margin-bottom: 4px;">Total Calls</div>
          <div style="font-size: 1.25rem; font-weight: 600; color: #3b82f6;">${monitorData.count}</div>
        </div>
        <div style="background: rgba(0, 0, 0, 0.3); padding: 8px; border-radius: 4px;">
          <div style="font-size: 0.75rem; color: rgba(226, 232, 240, 0.6); text-transform: uppercase; margin-bottom: 4px;">Calls/Minute</div>
          <div style="font-size: 1.25rem; font-weight: 600; color: #22c55e;">${monitorData.callsPerMinute || '0'}</div>
        </div>
        <div style="background: rgba(0, 0, 0, 0.3); padding: 8px; border-radius: 4px;">
          <div style="font-size: 0.75rem; color: rgba(226, 232, 240, 0.6); text-transform: uppercase; margin-bottom: 4px;">Avg Interval</div>
          <div style="font-size: 1.25rem; font-weight: 600; color: #f59e0b;">${intervalText}</div>
        </div>
        <div style="background: rgba(0, 0, 0, 0.3); padding: 8px; border-radius: 4px;">
          <div style="font-size: 0.75rem; color: rgba(226, 232, 240, 0.6); text-transform: uppercase; margin-bottom: 4px;">Last Call</div>
          <div style="font-size: 1.25rem; font-weight: 600; color: #a78bfa;">${lastCallText}</div>
        </div>
      </div>
    </div>
  `;
}

function showApiTab(apiKey) {
  const contentDiv = document.getElementById('adminApiContent');
  if (!contentDiv) return;

  // Update active tab
  document.querySelectorAll('.admin-api-tab').forEach(tab => {
    tab.classList.toggle('active', tab.dataset.api === apiKey);
  });

  if (apiKey === 'stats') {
    showApiStats(contentDiv);
    return;
  }

  const def = API_DEFINITIONS[apiKey];
  if (!def) return;

  const stats = apiStats.calls[apiKey] || { count: 0, lastCall: null, avgTime: 0 };
  const historyEntries = apiStats.history.filter(h => h.apiKey === apiKey).slice(-5);

  const fieldsHtml = def.fields.length > 0 ? `
    <div class="admin-api-fields">
      <h4>Parameters</h4>
      ${def.fields.map(field => {
        const defaultVal = typeof field.default === 'function' ? field.default() : field.default;
        if (field.type === 'select') {
          return `
            <div class="admin-api-field">
              <label>${field.name}</label>
              <select data-field="${field.name}">
                ${field.options.map(opt => `<option value="${opt}" ${opt === defaultVal ? 'selected' : ''}>${opt}</option>`).join('')}
              </select>
              <span class="muted">${field.description}</span>
            </div>
          `;
        }
        return `
          <div class="admin-api-field">
            <label>${field.name}</label>
            <input type="${field.type === 'datetime' ? 'text' : field.type}" data-field="${field.name}" value="${defaultVal || ''}" placeholder="${field.description}">
            <span class="muted">${field.description}</span>
          </div>
        `;
      }).join('')}
    </div>
  ` : '<p class="muted">No parameters required</p>';

  const authWarning = def.requiresAuth ? `
    <div style="background: rgba(251, 146, 60, 0.15); border: 1px solid rgba(251, 146, 60, 0.3); border-radius: 8px; padding: 12px; margin: 16px 0;">
      <strong style="color: #fb923c;">Auth Authentication Required</strong>
      <p style="margin: 8px 0 0 0; font-size: 0.9rem; color: rgba(226, 232, 240, 0.8);">
        This API requires authentication cookies. Make sure you have <strong>gomining.com</strong> open and are logged in.
        The API will be called through the game tab with your session cookies.
      </p>
    </div>
  ` : `
    <div style="background: rgba(34, 197, 94, 0.15); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 8px; padding: 12px; margin: 16px 0;">
      <strong style="color: #22c55e;">OK Public API</strong>
      <p style="margin: 8px 0 0 0; font-size: 0.9rem; color: rgba(226, 232, 240, 0.8);">
        This API can be called without authentication from this extension.
      </p>
    </div>
  `;

  contentDiv.innerHTML = `
    <div class="admin-api-view">
      <div class="admin-api-header">
        <h4>${def.name}</h4>
        <div style="display: flex; gap: 8px; margin-top: 8px;">
          <span class="badge">${def.method}</span>
          <span class="badge ghost">Used ${stats.count}x</span>
        </div>
      </div>

      <p>${def.description}</p>
      <p class="muted"><strong>Endpoint:</strong> <code style="background: #1a1a1a; padding: 2px 6px; border-radius: 3px;">${def.endpoint}</code></p>
      <p class="muted"><strong>Used in:</strong> ${def.usedIn.join(', ')}</p>

      ${authWarning}

      ${buildGameApiMonitoringSection(def.endpoint, def.method)}

      ${fieldsHtml}

      <div class="admin-api-actions" style="margin: 16px 0;">
        <button id="adminApiExecute" class="btn">Execute API Call</button>
        <button id="adminApiReset" class="btn ghost">Reset to Defaults</button>
      </div>
      <div id="adminCopyStatus" class="muted" style="min-height: 18px;"></div>

      <div class="admin-api-tabs" style="display: flex; gap: 8px; margin: 16px 0; border-bottom: 1px solid #333;">
        <button class="admin-api-subtab active" data-view="payload">Payload</button>
        <button class="admin-api-subtab" data-view="table">Table View</button>
        <button class="admin-api-subtab" data-view="headers">Headers</button>
        <button class="admin-api-subtab" data-view="raw">Raw JSON</button>
        <button class="admin-api-subtab" data-view="history">History (${historyEntries.length})</button>
      </div>

      <div id="adminApiOutput">
        <div class="admin-api-output-view" data-view="payload">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
            <h5>Request Payload</h5>
        <button class="btn sm ghost" onclick="copyApiPayload()">Copy</button>
          </div>
          <pre id="adminPayloadPre" style="background: #1a1a1a; padding: 12px; border-radius: 4px; overflow-x: auto; max-height: 400px;">${JSON.stringify(def.example, null, 2)}</pre>
          <div id="adminPayloadStatus" class="admin-payload-status" style="display:none;margin-top:8px;"></div>
        </div>

        <div class="admin-api-output-view" data-view="table" style="display: none;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
            <h5>Response Table</h5>
        <button class="btn sm ghost" onclick="copyApiTable()">Copy Table</button>
          </div>
          <div id="adminTableOutput" class="muted">Execute API call to see results...</div>
        </div>

        <div class="admin-api-output-view" data-view="headers" style="display: none;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
            <h5>Request Headers</h5>
            <button class="btn sm ghost" onclick="copyApiHeaders()">Copy</button>
          </div>
          <pre id="adminHeadersPre" style="background: #1a1a1a; padding: 12px; border-radius: 4px; overflow-x: auto; max-height: 400px;">Headers will appear after API call</pre>
        </div>

        <div class="admin-api-output-view" data-view="raw" style="display: none;">
          <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
            <h5>Raw Response</h5>
        <button class="btn sm ghost" onclick="copyApiRaw()">Copy</button>
          </div>
          <pre id="adminRawPre" style="background: #1a1a1a; padding: 12px; border-radius: 4px; overflow-x: auto; max-height: 400px;">Execute API call to see results...</pre>
        </div>

        <div class="admin-api-output-view" data-view="history" style="display: none;">
          <h5>Recent Calls</h5>
          <div id="adminHistoryOutput"></div>
        </div>
        </div>
      </div>
    </div>
  `;

  // Bind subtab switching
  document.querySelectorAll('.admin-api-subtab').forEach(tab => {
    tab.addEventListener('click', () => {
      const view = tab.dataset.view;
      document.querySelectorAll('.admin-api-subtab').forEach(t => t.classList.toggle('active', t === tab));
      document.querySelectorAll('.admin-api-output-view').forEach(v => {
        v.style.display = v.dataset.view === view ? 'block' : 'none';
      });
    });
  });

  // Bind execute button
  const executeBtn = document.getElementById('adminApiExecute');
  if (executeBtn) {
    console.log('[MW Admin] Binding execute button for', apiKey);
    executeBtn.addEventListener('click', async () => {
      console.log('[MW Admin] Execute button clicked');
      try {
        await executeApiCall(apiKey);
      } catch (err) {
        console.error('[MW Admin] Execute button handler error:', err);
      }
    });
  } else {
    console.error('[MW Admin] Execute button not found!');
  }

  const resetBtn = document.getElementById('adminApiReset');
  if (resetBtn) {
    resetBtn.addEventListener('click', () => showApiTab(apiKey));
  }

  // Live update payload preview when inputs change
  const updatePayloadPreview = () => {
    const payload = {};
    def.fields.forEach(field => {
      const input = document.querySelector(`[data-field="${field.name}"]`);
      if (!input) return;

      const keys = field.name.split('.');
      let current = payload;
      for (let i = 0; i < keys.length - 1; i++) {
        if (!current[keys[i]]) current[keys[i]] = {};
        current = current[keys[i]];
      }

      let value = input.value;
      if (field.type === 'number') value = Number(value);
      current[keys[keys.length - 1]] = value;
    });

    const payloadPreview = document.getElementById('adminPayloadPre');
    if (payloadPreview) {
      payloadPreview.textContent = JSON.stringify(payload, null, 2);
    }
  };

  // Bind input change listeners
  def.fields.forEach(field => {
    const input = document.querySelector(`[data-field="${field.name}"]`);
    if (input) {
      input.addEventListener('input', updatePayloadPreview);
      input.addEventListener('change', updatePayloadPreview);
    }
  });

  // Initial update
  updatePayloadPreview();

  applyTrackedPayloadToForm(apiKey, def);
  renderHistoryEntries(apiKey, contentDiv);
  refreshGameTabWarning();
}

function showApiStats(contentDiv) {
  const totalCalls = Object.values(apiStats.calls).reduce((sum, stat) => sum + stat.count, 0);
  const statsHtml = Object.entries(apiStats.calls).map(([key, stat]) => {
    const def = API_DEFINITIONS[key];
    return `
      <tr>
        <td>${def?.name || key}</td>
        <td>${stat.count}</td>
        <td>${stat.avgTime}ms</td>
        <td>${stat.lastCall ? new Date(stat.lastCall).toLocaleString() : 'Never'}</td>
        <td><button class="btn sm ghost" onclick="showApiTab('${key}')">View</button></td>
      </tr>
    `;
  }).join('');

  // Build game website monitoring stats
  const monitoringStatsHtml = Array.from(gameApiMonitor.calls.entries())
    .sort((a, b) => b[1].count - a[1].count) // Sort by call count descending
    .map(([key, stat]) => {
      const intervalText = stat.avgInterval
        ? stat.avgInterval < 1000
          ? `${stat.avgInterval}ms`
          : `${(stat.avgInterval / 1000).toFixed(1)}s`
        : 'N/A';

      const lastCallText = stat.lastCall
        ? new Date(stat.lastCall).toLocaleTimeString()
        : 'Never';

      return `
        <tr>
          <td style="padding: 8px; font-family: monospace; font-size: 0.85rem;">${stat.method}</td>
          <td style="padding: 8px; font-size: 0.85rem; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${stat.endpoint}">${stat.endpoint}</td>
          <td style="padding: 8px; text-align: center;">${stat.count}</td>
          <td style="padding: 8px; text-align: center; color: #22c55e;">${stat.callsPerMinute || '0'}</td>
          <td style="padding: 8px; text-align: center; color: #f59e0b;">${intervalText}</td>
          <td style="padding: 8px; text-align: center; color: #a78bfa; font-size: 0.85rem;">${lastCallText}</td>
        </tr>
      `;
    }).join('');

  const monitoringCount = gameApiMonitor.calls.size;
  const monitoringStartTime = new Date(gameApiMonitor.startTime).toLocaleString();

  contentDiv.innerHTML = `
    <div class="admin-stats-view">
      <h4>API Usage Statistics</h4>
      <p class="muted">Total API calls made from extension: <strong>${totalCalls}</strong></p>
      <p class="muted">Total history entries: <strong>${apiStats.history.length}</strong></p>

      <table style="width: 100%; margin-top: 16px; border-collapse: collapse;">
        <thead>
          <tr style="border-bottom: 2px solid #333;">
            <th style="text-align: left; padding: 8px;">API</th>
            <th style="text-align: left; padding: 8px;">Calls</th>
            <th style="text-align: left; padding: 8px;">Avg Time</th>
            <th style="text-align: left; padding: 8px;">Last Call</th>
            <th style="text-align: left; padding: 8px;">Actions</th>
          </tr>
        </thead>
        <tbody>
          ${statsHtml || '<tr><td colspan="5" class="muted">No API calls yet</td></tr>'}
        </tbody>
      </table>

      <div style="margin-top: 32px; padding-top: 24px; border-top: 2px solid #333;">
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
          <div>
            <h4 style="margin: 0;">Stats Game Website API Monitoring</h4>
            <p class="muted" style="margin: 4px 0 0 0;">
              Real-time tracking of all API calls from gomining.com
              <br>Monitoring since: <strong>${monitoringStartTime}</strong> | APIs tracked: <strong>${monitoringCount}</strong>
            </p>
          </div>
          <div style="display: flex; gap: 8px;">
            <button id="adminSaveStatsBtn" class="btn">Save Save to Database Now</button>
            <button id="adminRefreshStatsBtn" class="btn ghost">Refresh Refresh</button>
          </div>
        </div>

        <div id="adminSaveStatsStatus" style="display: none; margin-bottom: 12px;"></div>

        ${monitoringStatsHtml ? `
          <div style="overflow-x: auto;">
            <table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
              <thead>
                <tr style="border-bottom: 2px solid #333; background: rgba(59, 130, 246, 0.1);">
                  <th style="padding: 8px; text-align: left;">Method</th>
                  <th style="padding: 8px; text-align: left;">Endpoint</th>
                  <th style="padding: 8px; text-align: center;">Total Calls</th>
                  <th style="padding: 8px; text-align: center;">Calls/Min</th>
                  <th style="padding: 8px; text-align: center;">Avg Interval</th>
                  <th style="padding: 8px; text-align: center;">Last Call</th>
                </tr>
              </thead>
              <tbody>
                ${monitoringStatsHtml}
              </tbody>
            </table>
          </div>
        ` : `
          <div style="background: rgba(100, 116, 139, 0.1); border: 1px solid rgba(100, 116, 139, 0.2); border-radius: 8px; padding: 16px; text-align: center;">
            <p style="color: #94a3b8; margin: 0;">No API calls detected from the game website yet.</p>
            <p style="color: rgba(226, 232, 240, 0.6); font-size: 0.9rem; margin: 8px 0 0 0;">
              Open <strong>gomining.com</strong> and navigate to the Wars tab to start monitoring.
            </p>
          </div>
        `}

        <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 12px; margin-top: 16px;">
          <strong style="color: #3b82f6;">Info Database Storage</strong>
          <p style="margin: 8px 0 0 0; font-size: 0.9rem; color: rgba(226, 232, 240, 0.8);">
            Statistics are automatically saved to the database every 5 minutes. Use the "Save to Database Now" button to trigger an immediate save.
            Make sure you've created the <code>game_api_monitoring</code> table in Supabase using the provided SQL schema file.
          </p>
        </div>
      </div>

      <h5 style="margin-top: 24px;">Suggestions</h5>
      <ul class="muted">
        <li>Use the Clan Leaderboard API to get live roster power for all clans</li>
        <li>Use Round State API to get actual scores instead of calculating them</li>
        <li>Cache API responses for 1-3 minutes to reduce server load</li>
        <li>Monitor API response times to detect performance issues</li>
      </ul>
    </div>
  `;

  // Bind save stats button
  const saveBtn = document.getElementById('adminSaveStatsBtn');
  const refreshBtn = document.getElementById('adminRefreshStatsBtn');
  const statusDiv = document.getElementById('adminSaveStatsStatus');

  if (saveBtn) {
    saveBtn.addEventListener('click', async () => {
      saveBtn.disabled = true;
      saveBtn.textContent = 'Save Saving...';
      statusDiv.style.display = 'block';
      statusDiv.style.background = 'rgba(59, 130, 246, 0.15)';
      statusDiv.style.border = '1px solid rgba(59, 130, 246, 0.3)';
      statusDiv.style.padding = '8px 12px';
      statusDiv.style.borderRadius = '4px';
      statusDiv.style.color = '#3b82f6';
      statusDiv.textContent = 'Saving statistics to database...';

      try {
        const statCount = gameApiMonitor.calls.size;
        await saveGameApiStatsToDb();

        statusDiv.style.background = 'rgba(34, 197, 94, 0.15)';
        statusDiv.style.border = '1px solid rgba(34, 197, 94, 0.3)';
        statusDiv.style.color = '#22c55e';
        statusDiv.textContent = `OK Successfully saved ${statCount} API statistics to database!`;

        setTimeout(() => {
          statusDiv.style.display = 'none';
        }, 3000);
      } catch (err) {
        statusDiv.style.background = 'rgba(239, 68, 68, 0.15)';
        statusDiv.style.border = '1px solid rgba(239, 68, 68, 0.3)';
        statusDiv.style.color = '#ef4444';
        statusDiv.textContent = `Error Failed to save: ${err.message}`;
      } finally {
        saveBtn.disabled = false;
        saveBtn.textContent = 'Save Save to Database Now';
      }
    });
  }

  if (refreshBtn) {
    refreshBtn.addEventListener('click', () => {
      showApiStats(contentDiv);
    });
  }
}

async function fetchTrackedApiPayload(tabId, endpoint, method) {
  try {
    return await commRequestPageViaTab('FETCH_TRACKED_API_STATE', { endpoint, method });
  } catch (err) {
    console.warn('[MW Admin] Failed to fetch tracked API payload', err);
    return null;
  }
}

function clonePayload(value) {
  if (value === null || value === undefined) return value;
  if (typeof structuredClone === 'function') {
    try {
      return structuredClone(value);
    } catch (_) { /* fall through */ }
  }
  try {
    return JSON.parse(JSON.stringify(value));
  } catch (_) {
    return value;
  }
}

function resolveTrackedFallbackPayload(def) {
  if (!def || !def.trackedPayloadFallback) return null;
  const resolveRecursively = (value) => {
    if (typeof value === 'function') return value();
    if (Array.isArray(value)) {
      return value.map(resolveRecursively);
    }
    if (value && typeof value === 'object') {
      const result = {};
      Object.entries(value).forEach(([key, child]) => {
        result[key] = resolveRecursively(child);
      });
      return result;
    }
    return value;
  };
  return resolveRecursively(def.trackedPayloadFallback);
}

commEnsureSupabaseFetch();

function commWithApiKey(url) {
  if (!url) return url;
  if (url.includes('apikey=')) return url;
  const joiner = url.includes('?') ? '&' : '?';
  return `${url}${joiner}apikey=${encodeURIComponent(COMM_SUPABASE_KEY)}`;
}

function commEnsureSupabaseFetch() {
  if (window.__mwSupabaseFetchWrapped) return;
  const originalFetch = window.fetch.bind(window);
  window.fetch = (input, init = {}) => {
    const requestUrl = typeof input === 'string' ? input : input?.url;
    if (!requestUrl || !requestUrl.includes('hkybarlwyujbzrvcwpvz.supabase.co/rest/v1')) {
      return originalFetch(input, init);
    }
    const headers = new Headers(init.headers || (input && input.headers) || {});
    if (!headers.has('apikey')) headers.set('apikey', COMM_SUPABASE_KEY);
    if (!headers.has('Authorization')) headers.set('Authorization', `Bearer ${COMM_SUPABASE_KEY}`);
    const finalUrl = commWithApiKey(requestUrl);
    if (input instanceof Request) {
      const nextRequest = new Request(finalUrl, input);
      return originalFetch(nextRequest, { ...init, headers });
    }
    return originalFetch(finalUrl, { ...init, headers });
  };
  window.__mwSupabaseFetchWrapped = true;
}

function buildRoundClanLeaderboardPayload(trackedPayload = null) {
  const leagueId = commKorrelCurrentLeagueId || 13;
  const calculatedAt =
    trackedPayload?.calculatedAt ||
    trackedPayload?.calculated_at ||
    new Date().toISOString();
  return {
    pagination: { skip: 0, limit: 1 },
    calculatedAt,
    leagueId
  };
}

function deepEqual(a, b) {
  if (a === b) return true;
  if (typeof a !== typeof b) return false;
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i += 1) {
      if (!deepEqual(a[i], b[i])) return false;
    }
    return true;
  }
  if (a && b && typeof a === 'object' && typeof b === 'object') {
    const keysA = Object.keys(a).sort();
    const keysB = Object.keys(b).sort();
    if (keysA.length !== keysB.length) return false;
    for (let i = 0; i < keysA.length; i += 1) {
      if (keysA[i] !== keysB[i]) return false;
      if (!deepEqual(a[keysA[i]], b[keysB[i]])) return false;
    }
    return true;
  }
  return false;
}

function getNestedField(obj, path) {
  if (!obj || typeof path !== 'string') return undefined;
  const keys = path.split('.');
  let current = obj;
  for (const key of keys) {
    if (!current || typeof current !== 'object') return undefined;
    current = current[key];
  }
  return current;
}

function setNestedField(obj, path, value) {
  if (!obj || typeof path !== 'string') return;
  const keys = path.split('.');
  let current = obj;
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    if (i === keys.length - 1) {
      current[key] = value;
      return;
    }
    if (!current[key] || typeof current[key] !== 'object') {
      current[key] = {};
    }
    current = current[key];
  }
}

function overrideTrackedFields(payload, trackedPayload, fieldNames) {
  if (!payload || !trackedPayload || !Array.isArray(fieldNames) || fieldNames.length === 0) return;
  fieldNames.forEach((fieldName) => {
    const trackedValue = getNestedField(trackedPayload, fieldName);
    if (trackedValue !== undefined && trackedValue !== null) {
      setNestedField(payload, fieldName, trackedValue);
    }
  });
}

async function resolveTrackedApiPayload(def, apiKey, tabId = null) {
  if (!def || !apiKey) return null;
  if (trackedApiPayloadCache.has(apiKey)) {
    return clonePayload(trackedApiPayloadCache.get(apiKey));
  }
  let resolvedTabId = tabId;
  if (!resolvedTabId) {
    const tab = await commFindMiningWarsTab();
    resolvedTabId = tab?.id;
  }
  if (!resolvedTabId) return null;
  try {
    const tracked = await fetchTrackedApiPayload(resolvedTabId, def.endpoint, def.method);
    const payload = tracked?.payload;
    if (payload && typeof payload === 'object') {
      const stored = clonePayload(payload);
      trackedApiPayloadCache.set(apiKey, stored);
      return clonePayload(stored);
    }
  } catch (_) {
    // ignore
  }
  return null;
}

async function applyTrackedPayloadToForm(apiKey, def) {
  if (!def || !def.fields || !def.fields.length) return;
  try {
    const tracked = await resolveTrackedApiPayload(def, apiKey);
    let payloadForForm = tracked;
    if (apiKey === 'round-clan-leaderboard') {
      payloadForForm = buildRoundClanLeaderboardPayload(tracked);
    } else if ((!tracked || typeof tracked !== 'object') && def.useTrackedPayload) {
      payloadForForm = resolveTrackedFallbackPayload(def);
    }
    if (!payloadForForm || typeof payloadForForm !== 'object') return;
    def.fields.forEach((field) => {
      const trackedValue = getNestedField(payloadForForm, field.name);
      if (trackedValue === undefined || trackedValue === null) return;
      const input = document.querySelector(`[data-field="${field.name}"]`);
      if (!input) return;
      input.value = typeof trackedValue === 'object' ? JSON.stringify(trackedValue) : String(trackedValue);
    });
    const payloadPreview = document.getElementById('adminPayloadPre');
    if (payloadPreview) {
      payloadPreview.textContent = JSON.stringify(tracked, null, 2);
    }
  } catch (err) {
    console.warn('[MW Admin] Failed to apply tracked payload', err);
  }
}

function normalizeTimestampValue(value) {
  if (value == null) return null;
  if (typeof value === 'string') {
    const parsed = Date.parse(value);
    if (!Number.isNaN(parsed)) return new Date(parsed).toISOString();
    const trimmed = value.trim();
    return trimmed || null;
  }
  if (typeof value === 'number' && Number.isFinite(value)) {
    return new Date(value).toISOString();
  }
  return null;
}

function resolveLeagueIdForPayload(payload) {
  const leagueIdValue = getNestedField(payload, 'leagueId');
  const parsed = typeof leagueIdValue === 'number' ? leagueIdValue : Number(leagueIdValue);
  if (Number.isFinite(parsed)) {
    return Math.trunc(parsed);
  }
  if (Number.isFinite(commKorrelCurrentLeagueId)) {
    return Math.trunc(commKorrelCurrentLeagueId);
  }
  return null;
}

async function fetchRoundStateCalculatedAt(tabId, leagueId) {
  if (!tabId) return null;
  try {
    const payload = { type: 'current' };
    if (Number.isFinite(leagueId)) payload.leagueId = Math.trunc(leagueId);
    const injected = await commFetchGominingApiViaInjection(tabId, ROUND_STATE_ENDPOINT, payload);
    const raw = injected?.data;
    const inner = raw && typeof raw === 'object' ? (raw.data ?? raw) : raw;
    if (!inner || typeof inner !== 'object') return null;
    const containers = [
      inner,
      inner?.data,
      inner?.state,
      inner?.payload,
      inner?.payload?.data,
      inner?.liveRoundState,
      inner?.leagueState,
      inner?.currentLeagueState
    ].filter(Boolean);
    const fieldsToCheck = [
      'calculatedAt',
      'calculated_at',
      'calculationTimestamp',
      'calculation_timestamp',
      'calculatedTimestamp',
      'calculated_timestamp',
      'calculated_time',
      'snapshotAt',
      'snapshot_at',
      'timestamp',
      'time'
    ];
    for (const container of containers) {
      for (const field of fieldsToCheck) {
        const candidate = container[field];
        const normalized = normalizeTimestampValue(candidate);
        if (normalized) return normalized;
      }
    }
  } catch (err) {
    console.warn('[MW Admin] Failed to fetch calculatedAt via round/get-state', err);
  }
  return null;
}

function isPayloadEmpty(obj) {
  return !obj || Object.keys(obj).length === 0;
}

async function executeApiCall(apiKey) {
  console.log('[MW Admin] executeApiCall called for', apiKey);
  const def = API_DEFINITIONS[apiKey];
  if (!def) {
    console.error('[MW Admin] No definition found for', apiKey);
    return;
  }

  trackedApiPayloadCache.delete(apiKey);

  await refreshGameTabWarning();

  const startTime = Date.now();

  // Build payload from form inputs
  const payload = {};
  def.fields.forEach(field => {
    const input = document.querySelector(`[data-field="${field.name}"]`);
    if (!input) {
      console.warn('[MW Admin] Field input not found:', field.name);
      return;
    }

    const keys = field.name.split('.');
    let current = payload;
    for (let i = 0; i < keys.length - 1; i++) {
      if (!current[keys[i]]) current[keys[i]] = {};
      current = current[keys[i]];
    }

    let value = input.value;
    if (field.type === 'number') value = Number(value);
    current[keys[keys.length - 1]] = value;
  });

  const needsTab = def.requiresAuth || apiKey === 'league-index';
  let tab = null;
  if (needsTab) {
    tab = await commFindMiningWarsTab();
    if (!tab?.id) {
      await refreshGameTabWarning(GAME_TAB_WARNING_MESSAGE);
    }
    if (!tab?.id && def.requiresAuth) {
      throw new Error('No gomining.com tab found. Please open https://gomining.com to use authenticated APIs.');
    }
  }

  const trackedBody = await resolveTrackedApiPayload(def, apiKey, tab?.id);
  let requestPayload = payload;
  let payloadStatusMessage = '';
  let payloadStatusKind = 'info';
  const useTrackedPayload = Boolean(trackedBody && def.useTrackedPayload);
  const fallbackPayload = def.useTrackedPayload ? resolveTrackedFallbackPayload(def) : null;
  const trackedFieldNames = def.fields
    .filter((field) => field?.name && field.name.toLowerCase().includes('calculatedat'))
    .map((field) => field.name);

  if (trackedBody && typeof trackedBody === 'object') {
    if (def.requiresAuth && isPayloadEmpty(payload)) {
      requestPayload = clonePayload(trackedBody);
      payloadStatusMessage = 'Autofilling required payload from tracked request.';
    } else if (def.useTrackedPayload) {
      requestPayload = clonePayload(trackedBody);
      payloadStatusMessage = 'Using the payload captured from the game tab to match the live request.';
    } else if (trackedFieldNames.length) {
      overrideTrackedFields(requestPayload, trackedBody, trackedFieldNames);
    }
  }

  if (apiKey === 'round-clan-leaderboard') {
    requestPayload = buildRoundClanLeaderboardPayload(trackedBody);
    payloadStatusMessage = 'Using forced live-shape payload for round clan leaderboard.';
  }

  if ((def.useTrackedPayload && !useTrackedPayload && fallbackPayload) || def.forceTrackedPayloadFallback) {
    requestPayload = clonePayload(fallbackPayload);
    payloadStatusMessage = payloadStatusMessage || 'Using default payload shaped like the live request.';
  }

  if (trackedBody && typeof trackedBody === 'object') {
    const matches = deepEqual(trackedBody, requestPayload);
    if (!matches && !useTrackedPayload) {
      payloadStatusMessage = payloadStatusMessage || 'Payload deviates from the tracked request; review the values before sending.';
      payloadStatusKind = 'warn';
    }
  }

  if (apiKey === 'league-index' && tab?.id) {
    const leagueIdForSnapshot = resolveLeagueIdForPayload(requestPayload);
    const snapshotTimestamp = await fetchRoundStateCalculatedAt(tab.id, leagueIdForSnapshot);
    if (snapshotTimestamp) {
      requestPayload.calculatedAt = snapshotTimestamp;
    }
  }

  const payloadPreview = document.getElementById('adminPayloadPre');
  if (payloadPreview) {
    payloadPreview.textContent = JSON.stringify(requestPayload, null, 2);
  }
  updatePayloadStatus(payloadStatusMessage, payloadStatusKind);

  try {
    let response, data;
    let injectedResponse = null;

    if (def.requiresAuth) {
    console.log('[MW Admin] Running authenticated API call via injection', def.endpoint);
    const requestMethod = (def.method || 'POST').toUpperCase();
    const injected = await commFetchGominingApiViaInjection(tab.id, def.endpoint, requestPayload, { method: requestMethod });
      if (!injected) {
        throw new Error('Injected API call did not return a response.');
      }
      injectedResponse = injected;
      if (!injected.ok) {
        throw new Error(`API returned status ${injected.status}: ${JSON.stringify(injected.data)}`);
      }
      data = injected.data;
      response = { ok: injected.ok, status: injected.status || 200 };
    } else {
      // Public API - call directly from extension
      response = await fetch(def.endpoint, {
        method: def.method,
        headers: { 'Content-Type': 'application/json' },
        body: def.method === 'POST' ? JSON.stringify(requestPayload) : undefined
      });

      data = await response.json();
    }

    const duration = Date.now() - startTime;

    // Update stats
    if (!apiStats.calls[apiKey]) {
      apiStats.calls[apiKey] = { count: 0, avgTime: 0, lastCall: null };
    }
    const stats = apiStats.calls[apiKey];
    stats.count++;
    stats.avgTime = Math.round((stats.avgTime * (stats.count - 1) + duration) / stats.count);
    stats.lastCall = Date.now();

    // Add to history
    apiStats.history.push({
      apiKey,
      timestamp: Date.now(),
      success: response.ok,
      duration,
      payload: requestPayload,
      response: data,
      headers: injectedResponse?.meta?.requestHeaders || {}
    });
    if (apiStats.history.length > 50) apiStats.history.shift();

    // Update raw view
    document.getElementById('adminRawPre').textContent = JSON.stringify(data, null, 2);
    const headerPreview = document.getElementById('adminHeadersPre');
    if (headerPreview) {
      headerPreview.textContent = JSON.stringify(injectedResponse?.meta?.requestHeaders || {}, null, 2);
    }

    // Update table view
    const tableDiv = document.getElementById('adminTableOutput');
    if (tableDiv) {
      tableDiv.innerHTML = buildApiTable(data, apiKey);
    }

    // Switch to raw view
    document.querySelector('.admin-api-subtab[data-view="raw"]')?.click();

    const apiContent = document.getElementById('adminApiContent');
    if (apiContent) {
      renderHistoryEntries(apiKey, apiContent);
    }

  } catch (err) {
    console.error('[MW Admin] API call failed:', err);
    document.getElementById('adminRawPre').textContent = `Error: ${err.message}\n\nStack: ${err.stack}`;
    const headerPreview = document.getElementById('adminHeadersPre');
    if (headerPreview) {
      headerPreview.textContent = `Unavailable (${err.message})`;
    }
    apiStats.history.push({
      apiKey,
      timestamp: Date.now(),
      success: false,
      duration: Date.now() - startTime,
      payload: requestPayload,
      response: { error: err.message }
    });
    const apiContent = document.getElementById('adminApiContent');
    if (apiContent) {
      renderHistoryEntries(apiKey, apiContent);
    }
  }
}

function extractArrayValue(source, path) {
  if (!source || !path) return undefined;
  const keys = path.split('.');
  let current = source;
  for (const key of keys) {
    if (!current || typeof current !== 'object') {
      return undefined;
    }
    current = current[key];
  }
  return Array.isArray(current) ? current : undefined;
}

function resolveApiArray(data, customPaths = []) {
  const defaultPaths = [
    'data',
    'data.array',
    'data.items',
    'data.data',
    'data.result',
    'data.leagues',
    'data.clans',
    'items',
    'array'
  ];
  for (const path of [...customPaths, ...defaultPaths]) {
    const arr = extractArrayValue(data, path);
    if (Array.isArray(arr)) return arr;
  }
  if (Array.isArray(data)) return data;
  return [];
}

function buildApiTable(data, apiKey) {
  // Extract array data based on API type
  let rows = [];

  if (apiKey === 'clan-leaderboard') {
    const allClans = [
      ...(data?.data?.clansPromoted || []),
      ...(data?.data?.clansRelegated || []),
      ...(data?.data?.clansRemaining || [])
    ];
    rows = allClans.map(clan => ({
      Position: clan.position,
      'Clan ID': clan.clanId,
      'Clan Name': clan.clan?.name || '',
      'NFT Power': clan.nftPower,
      'Blocks Mined': clan.blocksMined || 0
    }));
  } else if (apiKey === 'round-state') {
    const stateRows = resolveApiArray(data, [
      'data.participants',
      'data',
      'data.leaderboard',
      'data.clans',
      'data.leaderboard.clans'
    ]);
    rows = stateRows.map(item => {
      const clanId = item.clanId ?? item.clan_id ?? item.id ?? '';
      const clanName = item.clanName ?? item.clan_name ?? item.name ?? '';
      const userId = item.userId ?? item.user_id ?? item.id ?? '';
      const userName = item.userName ?? item.user_name ?? item.alias ?? item.name ?? '';
      const score = Number(item.score ?? item.currentScore ?? item.totalScore ?? 0);
      const power = Number(item.rosterPower ?? item.roster_power ?? item.clanPower ?? item.clan_power ?? item.userPower ?? item.user_power ?? item.nftPower ?? item.power ?? 0);
      const boostScore = Number(item.activeBoostScore ?? item.active_boost_score ?? item.addedScore ?? item.boostScore ?? 0);

      // For clans leaderboard
      if (clanId || clanName) {
        return {
          Position: item.position || item.rank || '',
          'Clan ID': clanId,
          'Clan Name': clanName,
          Score: score,
          Power: power,
          'Boost Score': boostScore
        };
      }
      // For users leaderboard
      return {
        Position: item.position || item.rank || '',
        'User ID': userId,
        'User Name': userName,
        Score: score,
        Power: power,
        'Boost Score': boostScore
      };
    });
  } else if (apiKey === 'user-leaderboard' || apiKey === 'round-user-leaderboard') {
    const leaderboardRows = resolveApiArray(data, [
      'data.participants',
      'data.players',
      'data.users',
      'data',
      'data.leaderboard',
      'data.leaderboard.users',
      'data.leaderboard.records',
      'data.leaderboard.players',
      'data.records',
      'data.records.users',
      'data.results',
      'data.results.users'
    ]);
    rows = leaderboardRows.map(item => {
      const user = item.user || item.userData || item.user_info || item.userInfo || {};
      const userId = user?.userId ?? user?.id ?? item.userId ?? item.id ?? '';
      const userName =
        (user?.alias || user?.userName || user?.name || user?.aliasName || user?.displayName)?.trim() ||
        item.userName ||
        item.alias ||
        '';
      return {
        Position: item.position || '',
        'User ID': userId,
        'User Name': userName,
        Power: item.userPower || item.nftPower || 0,
        Score: item.score || 0
      };
    });
  } else if (apiKey === 'round-clan-leaderboard') {
    const clanRows = resolveApiArray(data, [
      'data.participants',
      'data',
      'data.clans',
      'data.leaderboard',
      'data.leaderboard.clans',
      'data.results',
      'data.results.clans'
    ]);
    rows = clanRows.map(item => ({
      Position: item.position || '',
      'Clan ID': item.clanId,
      'Clan Name': item.clanName || item.clan?.name || '',
      Score: item.score || 0,
      Power: item.clanPower || 0
    }));
  } else if (apiKey === 'league-index') {
    const leagueRows = resolveApiArray(data, ['data', 'data.array', 'data.leagueIndex.array']);
    rows = leagueRows.map((league) => {
      const multipliers = Array.isArray(league.roundMultiplierConfig || league.round_multiplier_config)
        ? (league.roundMultiplierConfig || league.round_multiplier_config).map((entry) => `${entry.v}@${entry.p}`).join(', ')
        : '';
      return {
        ID: league.id,
        Name: league.name || league.label || '',
        Test: league.test || false,
        Level: league.level,
        'Total Clans': league.totalClansCount || league.total_clans_count || league.total_clans || league.clans?.length || 0,
        'Relegation To': league.relegationToLeagueId ?? league.relegation_to_league_id ?? '-',
        'Promotion To': league.promotionToLeagueId ?? league.promotion_to_league_id ?? '-',
        'Dynamic Movement': league.isDynamicClansMovement ?? league.is_dynamic_clans_movement ?? false,
        'Round Multipliers': multipliers || '-'
      };
    });
  } else if (apiKey === 'round-by-cycle') {
    rows = (data?.data || []).map(round => ({
      'Round ID': round.id,
      'Cycle ID': round.cycleId,
      'League ID': round.leagueId,
      Status: round.status || '',
      Started: round.startedAt ? new Date(round.startedAt).toLocaleString() : '',
      Finished: round.finishedAt ? new Date(round.finishedAt).toLocaleString() : ''
    }));
  } else if (apiKey === 'round-get-last') {
    const clans = resolveApiArray(data, ['data.allClansState', 'data.all_clans_state', 'data.activeClans', 'data.clans']);
    rows = clans.map(item => ({
      Position: item.position || '',
      'Clan ID': item.clanId || item.clan_id || '',
      'Clan Name': item.clan?.name || item.clanName || '',
      Power: item.clanPower || item.power || 0,
      'Boost Score': item.currentAddedScore || item.addedScore || 0,
      'Active Boost Score': item.activeBoostScore || '',
      'Members': item.memberCount || item.member_count || '',
      'GMT Rewards': item.gmtRewards ?? '',
      'Used Abilities': (Array.isArray(item.usedAbilities) ? item.usedAbilities.map(ua => `${ua.nftGameAbilityId}@${ua.count}`).join(', ') : '')
    }));
  } else if (apiKey === 'clan-get-by-id') {
    const clan = data?.data;
    if (clan?.members) {
      rows = clan.members.map(member => ({
        'User ID': member.userId,
        'User Name': member.userName || member.user?.name || '',
        Power: member.nftPower || 0,
        Role: member.role || 'Member'
      }));
    } else if (clan) {
      rows = [{
        'Clan ID': clan.id,
        'Clan Name': clan.name,
        'Total Power': clan.totalPower || 0,
        'Member Count': clan.memberCount || 0,
        'League ID': clan.leagueId
      }];
    }
  } else {
    // Generic table for other APIs
    const arr = resolveApiArray(data);
    rows = arr.slice(0, 50);
  }

  if (rows.length === 0) {
    return '<p class="muted">No tabular data available</p>';
  }

  const headers = Object.keys(rows[0]);
  return `
    <div style="overflow-x: auto;">
      <table style="width: 100%; border-collapse: collapse;">
        <thead>
          <tr style="border-bottom: 2px solid #333;">
            ${headers.map(h => `<th style="text-align: left; padding: 8px;">${h}</th>`).join('')}
          </tr>
        </thead>
        <tbody>
          ${rows.map(row => `
            <tr style="border-bottom: 1px solid #222;">
              ${headers.map(h => `<td style="padding: 8px;">${row[h] != null ? row[h] : '-'}</td>`).join('')}
            </tr>
          `).join('')}
        </tbody>
      </table>
    </div>
  `;
}

window.showApiTab = showApiTab;

function copyAdminTextToClipboard(text) {
  if (!text) return Promise.reject(new Error('Nothing to copy'));
  if (navigator.clipboard?.writeText) {
    return navigator.clipboard.writeText(text);
  }
  return new Promise((resolve, reject) => {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    textarea.style.pointerEvents = 'none';
    document.body.appendChild(textarea);
    textarea.select();
    textarea.setSelectionRange(0, text.length);
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    if (success) {
      resolve();
    } else {
      reject(new Error('Copy command failed'));
    }
  });
}

function setCopyStatus(message, isError = false) {
  const statusEl = document.getElementById('adminCopyStatus');
  if (!statusEl) return;
  statusEl.textContent = message;
  statusEl.style.color = isError ? '#dc2626' : '#22c55e';
}

function updatePayloadStatus(message = '', kind = 'info') {
  const statusEl = document.getElementById('adminPayloadStatus');
  if (!statusEl) return;
  if (!message) {
    statusEl.style.display = 'none';
    statusEl.textContent = '';
    statusEl.classList.remove('warn');
    return;
  }
  statusEl.textContent = message;
  statusEl.classList.toggle('warn', kind === 'warn');
  statusEl.style.display = 'block';
}

function copyFromElement(elementId, label) {
  const element = document.getElementById(elementId);
  if (!element) {
    setCopyStatus(`${label} missing`, true);
    return;
  }
  copyAdminTextToClipboard(element.textContent || '')
    .then(() => setCopyStatus(`${label} copied!`))
    .catch((err) => {
      console.warn('[MW Admin] Copy failed', err);
      setCopyStatus(`Copy failed`, true);
    });
}

function copyApiPayload() {
  copyFromElement('adminPayloadPre', 'Payload');
}

function copyApiRaw() {
  copyFromElement('adminRawPre', 'Raw response');
}

function copyApiHeaders() {
  copyFromElement('adminHeadersPre', 'Request headers');
}

function copyApiTable() {
  const table = document.querySelector('#adminTableOutput table');
  if (!table) {
    setCopyStatus('No table to copy', true);
    return;
  }
  let text = '';
  table.querySelectorAll('tr').forEach(row => {
    const cells = Array.from(row.querySelectorAll('th, td'));
    text += cells.map(cell => cell.textContent).join('\t') + '\n';
  });
  copyAdminTextToClipboard(text)
    .then(() => setCopyStatus('Table copied!'))
    .catch((err) => {
      console.warn('[MW Admin] Table copy failed', err);
      setCopyStatus('Copy failed', true);
    });
}

window.copyTableData = copyApiTable;

function buildHistoryItem(entry) {
  const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : 'Unknown';
  const badgeClass = entry.success ? '' : 'error';
  const statusLabel = entry.success ? 'Success' : 'Failed';
  const responsePreview = entry.response ? JSON.stringify(entry.response, null, 2).substring(0, 500) : '{}';
  return `
    <div class="admin-history-item" style="background: #1a1a1a; padding: 12px; margin: 8px 0; border-radius: 4px;">
      <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
        <span><strong>${timestamp}</strong></span>
        <span class="badge ${badgeClass}">${statusLabel} (${entry.duration}ms)</span>
      </div>
      <details>
        <summary style="cursor: pointer; margin: 4px 0;">View Details</summary>
        <pre style="margin-top: 8px; font-size: 11px;">${responsePreview}...</pre>
      </details>
    </div>
  `;
}

const GAME_TAB_WARNING_MESSAGE = '';

function getGlobalGameTabWarningElement() {
  let el = document.getElementById('globalGameTabWarning');
  if (el) return el;
  el = document.createElement('div');
  el.id = 'globalGameTabWarning';
  el.className = 'global-game-tab-warning';
  document.body.insertBefore(el, document.body.firstChild);
  return el;
}

function updateWarningElement(element, message) {
  if (!element) return;
  if (message) {
    element.textContent = message;
    element.style.display = 'block';
  } else {
    element.textContent = '';
    element.style.display = 'none';
  }
}

function showGameTabWarning(message) {
  const warningEl = document.getElementById('adminGameTabWarning');
  const globalWarningEl = getGlobalGameTabWarningElement();
  updateWarningElement(warningEl, message);
  updateWarningElement(globalWarningEl, message);
}

async function refreshGameTabWarning(forcedMessage = null) {
  if (forcedMessage !== null) {
    showGameTabWarning(forcedMessage);
    return;
  }
  const warningEl = document.getElementById('adminGameTabWarning');
  if (!warningEl) return;
  try {
    const tab = await commFindMiningWarsTab();
    showGameTabWarning(tab?.id ? '' : GAME_TAB_WARNING_MESSAGE);
  } catch (_) {
    showGameTabWarning(GAME_TAB_WARNING_MESSAGE);
  }
}

function renderHistoryEntries(apiKey, container = document) {
  const output = container.querySelector('#adminHistoryOutput');
  if (!output) return;
  const entries = apiStats.history.filter(h => h.apiKey === apiKey).slice(-5).reverse();
  output.innerHTML = entries.length
    ? entries.map(buildHistoryItem).join('')
    : '<p class="muted">No history yet</p>';
  const historyTab = container.querySelector('.admin-api-subtab[data-view="history"]');
  if (historyTab) {
    historyTab.textContent = `History (${entries.length})`;
  }
}

function bindAdminToolsHandlers() {
  // Bind main API tab switching
  document.querySelectorAll('.admin-api-tab').forEach(tab => {
    tab.addEventListener('click', () => {
      showApiTab(tab.dataset.api);
    });
  });
}

if (resizer) {
  let dragging = false;
  resizer.addEventListener('pointerdown', () => {
    dragging = true;
  });
  window.addEventListener('pointermove', (evt) => {
    if (!dragging) return;
    const newWidth = Math.min(Math.max(window.innerWidth - evt.clientX, 360), 650);
    document.querySelector('.app-shell').style.width = `${newWidth + 4}px`;
  });
  window.addEventListener('pointerup', () => {
    dragging = false;
  });
}

// Toggle section visibility for all toggle buttons
const toggleButtons = document.querySelectorAll('.toggle-section-btn');
const visibilityState = {}; // Track visibility state for each section

toggleButtons.forEach((btn) => {
  const targetId = btn.dataset.target;
  if (!targetId) return;

  const targetElement = document.getElementById(targetId);
  if (!targetElement) return;

  // Initialize visibility state
  visibilityState[targetId] = true;

  // Determine the correct display value based on element
  const getDisplayValue = (element) => {
    if (element.classList.contains('card-grid')) return 'grid';
    return 'block'; // For header section and other block elements
  };

  const originalDisplay = getDisplayValue(targetElement);

  btn.addEventListener('click', () => {
    visibilityState[targetId] = !visibilityState[targetId];
    const isVisible = visibilityState[targetId];

    targetElement.style.display = isVisible ? originalDisplay : 'none';
    btn.textContent = isVisible ? 'Visible' : 'Hidden';
    btn.title = isVisible ? 'Hide section' : 'Show section';
  });
});

// Card Recovery Popup functionality (functions defined outside DOMContentLoaded)
let cardPopupOverlay;
let cardPopupList;
let addCardsBtn;
let closeCardPopup;

function openCardPopup() {
  // Find all deleted cards across all containers
  const allContainers = document.querySelectorAll('.card-grid');
  const deletedCards = [];

  allContainers.forEach(container => {
    const cards = container.querySelectorAll('.card.deleted');

    cards.forEach(card => {
      const cardId = card.dataset.cardId;
      let cardTitle = 'Unnamed Card';

      // Try to get card title
      const h3 = card.querySelector('h3');
      if (h3) {
        cardTitle = h3.textContent;
      } else if (card.querySelector('.current-round-card')) {
        const infoValues = card.querySelectorAll('.info-value');
        if (infoValues.length > 0) {
          cardTitle = `Round ${infoValues[0].textContent}`;
        }
      } else if (card.querySelector('.boost-summary-card')) {
        cardTitle = 'Boost Summary';
      } else {
        const p = card.querySelector('p');
        if (p) {
          cardTitle = p.textContent.substring(0, 50);
        }
      }

      deletedCards.push({
        id: cardId,
        title: cardTitle.trim(),
        element: card,
        container: container
      });
    });
  });

  // Populate the popup list
  cardPopupList.innerHTML = '';

  if (deletedCards.length === 0) {
    cardPopupList.innerHTML = '<div class="card-popup-empty">No deleted cards to recover</div>';
  } else {
    deletedCards.forEach(cardData => {
      const item = document.createElement('div');
      item.className = 'card-popup-item';

      // Clone the actual card for preview
      const cardPreview = cardData.element.cloneNode(true);
      cardPreview.className = 'card-popup-preview';
      // Remove delete button from preview
      const deleteBtn = cardPreview.querySelector('.card-delete-btn');
      if (deleteBtn) deleteBtn.remove();

      // Create plus button
      const plusBtn = document.createElement('button');
      plusBtn.className = 'card-popup-plus-btn';
      plusBtn.innerHTML = '+';
      plusBtn.title = 'Restore card';
      plusBtn.dataset.cardId = cardData.id;

      plusBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        // Restore this specific card
        cardData.element.classList.remove('deleted');
        // Remove from popup list
        item.remove();
        // If no more cards, show empty message
        if (cardPopupList.children.length === 0) {
          cardPopupList.innerHTML = '<div class="card-popup-empty">No deleted cards to recover</div>';
        }
      });

      item.appendChild(cardPreview);
      item.appendChild(plusBtn);

      cardPopupList.appendChild(item);
    });
  }

  cardPopupOverlay.classList.add('active');
}

function closeCardPopupFn() {
  cardPopupOverlay.classList.remove('active');
}

// ============ Debug Panel ============
const DEBUG_STORAGE_KEY = 'mw_debug_log';
const MAX_DEBUG_ENTRIES = 300;
let debugStorageDisabled = false;
let debugTextStorageDisabled = false;

// Debug panel elements (will be initialized after DOMContentLoaded)
let debugPanel;
let debugHeader;
let debugLog;
let debugExportBtn;
let debugClearBtn;
const DEBUG_PANEL_REVEAL_CLICKS = 10;
let debugPanelRevealClicks = 0;
let debugPanelRevealed = false;

// Debug logger utility
window.debugLogger = {
  log(type, message, data = null) {
    const timestamp = new Date().toLocaleTimeString();
    const entry = {
      type,
      message,
      data,
      timestamp,
      time: Date.now()
    };

    // Add to UI
    this.addEntryToUI(entry);

    // Store in localStorage
    this.storeEntry(entry);

    // Also log to console for development
    const consoleMsg = `[MW Debug] ${message}`;
    if (type === 'error') {
      console.error(consoleMsg, data || '');
    } else if (type === 'warning') {
      console.warn(consoleMsg, data || '');
    } else {
      console.log(consoleMsg, data || '');
    }
  },

  success(message, data) {
    this.log('success', message, data);
  },

  error(message, data) {
    this.log('error', message, data);
  },

  info(message, data) {
    this.log('info', message, data);
  },

  warning(message, data) {
    this.log('warning', message, data);
  },

  addEntryToUI(entry) {
    if (!debugLog) return;

    const entryEl = document.createElement('div');
    entryEl.className = `debug-entry ${entry.type}`;

    const timeEl = document.createElement('span');
    timeEl.className = 'debug-time';
    timeEl.textContent = entry.timestamp;

    const messageEl = document.createElement('span');
    messageEl.className = 'debug-message';
    messageEl.textContent = entry.message;
    if (entry.data) {
      const dataStr = typeof entry.data === 'string' ? entry.data : JSON.stringify(entry.data, null, 2);
      messageEl.textContent += ` | ${dataStr}`;
    }

    entryEl.appendChild(timeEl);
    entryEl.appendChild(messageEl);

    debugLog.appendChild(entryEl);

    // Auto-scroll to bottom
    debugLog.scrollTop = debugLog.scrollHeight;

    // Limit entries in UI
    while (debugLog.children.length > MAX_DEBUG_ENTRIES) {
      debugLog.removeChild(debugLog.firstChild);
    }
  },

  storeEntry(entry) {
    if (debugStorageDisabled) {
      return;
    }
    try {
      const stored = JSON.parse(localStorage.getItem(DEBUG_STORAGE_KEY) || '[]');
      stored.push(entry);

      // Keep only entries from the last hour
      const oneHourAgo = Date.now() - (60 * 60 * 1000);
      const filtered = stored.filter(e => e.time >= oneHourAgo);

      // Also enforce max entries as a backup limit
      while (filtered.length > MAX_DEBUG_ENTRIES) {
        filtered.shift();
      }

      localStorage.setItem(DEBUG_STORAGE_KEY, JSON.stringify(filtered));

      // Also append to text file log
      this.appendToTextFile(entry);
    } catch (err) {
      debugStorageDisabled = true;
      console.warn('[MW] Failed to store debug entry - disabling storage:', err);
    }
  },

  appendToTextFile(entry) {
    if (debugTextStorageDisabled) {
      return;
    }
    try {
      // Get existing entries array from localStorage
      const existingEntries = JSON.parse(localStorage.getItem('mw_debug_entries_array') || '[]');

      // Add new entry
      existingEntries.push(entry);

      // Keep only entries from the last hour
      const oneHourAgo = Date.now() - (60 * 60 * 1000);
      const recentEntries = existingEntries.filter(e => e.time >= oneHourAgo);

      // Store filtered entries array
      localStorage.setItem('mw_debug_entries_array', JSON.stringify(recentEntries));

      // Convert to text format
      const textLog = recentEntries.map(e => {
        const date = new Date(e.time);
        const dateStr = date.toISOString();
        const typeLabel = e.type.toUpperCase().padEnd(8);
        const dataStr = e.data ? `\n    Data: ${JSON.stringify(e.data)}` : '';
        return `[${dateStr}] ${typeLabel} ${e.message}${dataStr}`;
      }).join('\n') + '\n';

      // Store as text for backwards compatibility
      localStorage.setItem('mw_debug_text_log', textLog);

      // Automatically write to file
      this.writeToFile(textLog);
    } catch (err) {
      debugTextStorageDisabled = true;
      console.warn('[MW] Failed to append to text log - disabling text storage:', err);
    }
  },

  writeToFile(content) {
    try {
      // Send message to background script to write file
      chrome.runtime.sendMessage({
        type: 'WRITE_DEBUG_LOG',
        content: content
      }).catch(err => {
        // Silently fail - don't spam console
      });
    } catch (err) {
      // Silently fail
    }
  },

  loadStoredEntries() {
    try {
      const stored = JSON.parse(localStorage.getItem(DEBUG_STORAGE_KEY) || '[]');
      stored.forEach(entry => this.addEntryToUI(entry));
    } catch (err) {
      console.warn('[MW] Failed to load stored debug entries:', err);
    }
  },

  clear() {
    if (debugLog) {
      debugLog.innerHTML = '';
    }
    localStorage.removeItem(DEBUG_STORAGE_KEY);
    localStorage.removeItem('mw_debug_text_log');
    debugStorageDisabled = false;
    debugTextStorageDisabled = false;
    this.info('Debug log cleared');
  },

  async exportLogFile() {
    try {
      // Get logs from all contexts
      const appLog = localStorage.getItem(CONSOLE_LOG_STORAGE_KEY) || '';

      // Get logs from content.js and background.js via chrome.storage
      const storageData = await chrome.storage.local.get([
        'mw_content_console_log',
        'mw_background_console_log'
      ]);

      const contentLog = storageData.mw_content_console_log || '';
      const backgroundLog = storageData.mw_background_console_log || '';

      // Merge all logs with headers
      const mergedLog = [
        '='.repeat(80),
        'COMPLETE CONSOLE LOG - ALL CONTEXTS',
        '='.repeat(80),
        '',
        '--- SIDE PANEL (app.js) ---',
        appLog || '(No app logs)',
        '',
        '--- CONTENT SCRIPT (content.js) ---',
        contentLog || '(No content logs)',
        '',
        '--- BACKGROUND SCRIPT (background.js) ---',
        backgroundLog || '(No background logs)',
        '',
        '='.repeat(80),
        'END OF LOG',
        '='.repeat(80)
      ].join('\n');

      const blob = new Blob([mergedLog], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
      a.href = url;
      a.download = `mw_complete_log_${timestamp}.txt`;
      a.click();
      URL.revokeObjectURL(url);
      this.success('Complete console log exported');
    } catch (err) {
      originalConsole.error('[MW] Failed to export log file:', err);
      this.error('Failed to export log file', err.message);
    }
  },

  resetLog() {
    // Clear old logs on startup
    if (debugLog) {
      debugLog.innerHTML = '';
    }
    localStorage.removeItem(DEBUG_STORAGE_KEY);
    localStorage.removeItem('mw_debug_text_log');
  }
};

// ============ Main Initialization ============
// Function to initialize everything after DOM is ready
async function startApp() {
  console.log('[MW] Starting app initialization');

  captureAuthFromUrl();

  await commLoadUserSettingsFromDb();
  // Initialize app (tab buttons, theme, etc.)
  initializeApp();
  initFeatureOverview();
  maybeShowFeatureOverviewOnLoad();
  updateFloatingChatVisibility();
  fetchAppNotice().then(renderAppNotice).catch(() => {});

  // Initialize card popup elements
  cardPopupOverlay = document.getElementById('cardPopupOverlay');
  cardPopupList = document.getElementById('cardPopupList');
  addCardsBtn = document.getElementById('addCardsBtn');
  closeCardPopup = document.getElementById('closeCardPopup');

  // Event listeners for popup
  if (addCardsBtn && cardPopupOverlay && closeCardPopup) {
    addCardsBtn.addEventListener('click', openCardPopup);
    closeCardPopup.addEventListener('click', closeCardPopupFn);

    // Close popup when clicking outside
    cardPopupOverlay.addEventListener('click', (e) => {
      if (e.target === cardPopupOverlay) {
        closeCardPopupFn();
      }
    });
  }

  // Initialize debug panel elements
  debugPanel = document.getElementById('debugPanel');
  debugHeader = document.getElementById('debugHeader');
  debugLog = document.getElementById('debugLog');
  debugExportBtn = document.getElementById('debugExportBtn');
  debugClearBtn = document.getElementById('debugClearBtn');
  if (debugPanel && !debugPanelRevealed) {
    debugPanel.style.display = 'none';
    document.body.classList.remove('debug-panel-visible');
  }

  // Toggle debug panel collapse
  if (debugHeader) {
    debugHeader.addEventListener('click', (e) => {
      if (e.target === debugClearBtn || e.target === debugExportBtn) return; // Don't toggle when clicking buttons
      debugPanel.classList.toggle('collapsed');
    });
  }

  // Export button
  if (debugExportBtn) {
    debugExportBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      window.debugLogger.exportLogFile();
    });
  }

  // Clear button
  if (debugClearBtn) {
    debugClearBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      window.debugLogger.clear();
    });
  }

  // Reset log on startup (overwrite old logs)
  if (debugLog) {
    window.debugLogger.resetLog();
    window.debugLogger.info('Side panel loaded - Debug log initialized');
  }

  // Check for pending round finished events that arrived before sidepanel was open
  if (HAS_CHROME_RUNTIME && chrome?.storage?.local) {
    console.log('[MW] Checking chrome.storage.local for pending round finished events...');
    chrome.storage.local.get(['mw_pending_round_finished'], (result) => {
      if (chrome.runtime.lastError) {
        console.error('[MW] Error checking for pending events:', chrome.runtime.lastError);
        return;
      }

      console.log('[MW] Storage check result:', result);

      if (result.mw_pending_round_finished) {
        console.log('[MW] ===== FOUND PENDING ROUND FINISHED EVENT =====');
        console.log('[MW] Event details:', result.mw_pending_round_finished);
        window.debugLogger?.info('Processing Pending Round Finished Event', {
          timestamp: result.mw_pending_round_finished.timestamp
        });

        // Process the pending event
        const event = result.mw_pending_round_finished;
        if (event.type === 'MW_ROUND_EVENT' && event.payload) {
          console.log('[MW] Dispatching pending event through runtime messaging');
          // Trigger the round event handler via the normal runtime path
          chrome.runtime.sendMessage(event).catch(() => {
            console.warn('[MW] Failed to dispatch pending round event through runtime messaging');
          });
        }

        // Clear the pending event
        chrome.storage.local.remove(['mw_pending_round_finished'], () => {
          if (chrome.runtime.lastError) {
            console.error('[MW] Error clearing pending event:', chrome.runtime.lastError);
          } else {
            console.log('[MW] Successfully cleared pending round finished event from storage');
          }
        });
      } else {
        console.log('[MW] No pending round finished events found in storage');
      }
    });
  }

  // Populate all sections
  populateHome();
  populateLive();
  populateCalculator();
  populateTools();
  populateStats();
  populatePersonalData();
  populateAdminTools();
  commRenderFloatingChat();
  commStartUserSettingsSyncTimer();
  commSaveUserSettingsToDb().catch(() => {});
}

// Check if DOM is already loaded (important for Chrome extension side panels)
if (document.readyState === 'loading') {
  console.log('[MW] DOM still loading, waiting for DOMContentLoaded');
  document.addEventListener('DOMContentLoaded', startApp);
} else {
  console.log('[MW] DOM already loaded, initializing immediately');
  startApp();
}
