// RWS shared components (globals via window) — v1.7
// v1.1: real server IPs + BattleMetrics IDs; live-data hook (useLiveServers,
//       mergeLive); wipe-schedule helpers.
// v1.2: wipe schedule moved to America/Chicago (Central Time, DST-aware);
//       fixed Quad SERVERS entry (tagline + maxGroup were copy-pasted from Solo).
// v1.3: mobile pass — useIsMobile hook (matchMedia ≤768px); NavBar collapses
//       to hamburger button + slide-in drawer on mobile; SiteFooter stacks
//       4-col grid to 1-col with reduced padding. Desktop ≥769px is unchanged.
// v1.4: leaderboards + community tabs archived (NavBar items trimmed,
//       LEADERBOARD mock data removed, footer "Community" column unlinked
//       leaderboards). Pages preserved at archive/leaderboards-page.jsx and
//       archive/community-page.jsx for future revival.
// v1.5: live Discord stats — useDiscordStats hook hits /api/discord
//       (Pages Function backed by Discord's invite endpoint). Returns
//       {members, online, invite, updatedAt} or null while loading.
// v1.6: stats are gated behind DISCORD_STATS_MIN_MEMBERS (default 50) via
//       isDiscordStatsVisible() so small numbers don't ship publicly.
// v1.7: SiteFooter columns wired to real targets — Servers links launch Rust
//       via steam://connect/<ip:port>, Server status routes to the Servers
//       tab; Community Discord button opens the invite, Rules routes to the
//       Rules tab. SiteFooter now accepts an onNav prop; FooterCol items
//       can be plain strings or {label, href, target, onClick} objects.
// Depends on React being loaded.

const { useState, useEffect, useRef, useMemo } = React;

// ─────────────────────────────────────────────
// useIsMobile — matchMedia-driven responsive hook (v1.3)
// Returns true when viewport ≤ maxWidth. Used by NavBar /
// SiteFooter / page components to branch inline-style layout.
// ─────────────────────────────────────────────
function useIsMobile(maxWidth = 768) {
  const query = `(max-width: ${maxWidth}px)`;
  const get = () =>
    typeof window !== 'undefined' && window.matchMedia
      ? window.matchMedia(query).matches
      : false;
  const [isMobile, setIsMobile] = useState(get);
  useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;
    const mql = window.matchMedia(query);
    const handler = (e) => setIsMobile(e.matches);
    if (mql.addEventListener) mql.addEventListener('change', handler);
    else mql.addListener(handler); // Safari <14
    setIsMobile(mql.matches);
    return () => {
      if (mql.removeEventListener) mql.removeEventListener('change', handler);
      else mql.removeListener(handler);
    };
  }, [query]);
  return isMobile;
}

// ─────────────────────────────────────────────
// MascotBadge — the wojak in a dog-tag frame
// Used as logo + throughout the site
// ─────────────────────────────────────────────
function MascotBadge({ size = 56, showRing = true }) {
  return (
    <div style={{
      position: 'relative',
      width: size, height: size,
      borderRadius: '50%',
      background: 'linear-gradient(180deg, #3a4028, #1c2016)',
      border: showRing ? '2px solid var(--od-400)' : 'none',
      boxShadow: 'inset 0 0 0 2px var(--od-800), 0 2px 8px rgba(0,0,0,0.5)',
      overflow: 'hidden',
      flexShrink: 0,
    }}>
      <img src="assets/rws-mascot.png" alt="RWS"
        style={{
          position: 'absolute',
          left: '50%', top: '50%',
          transform: 'translate(-50%, -45%) scale(1.4)',
          width: '100%', height: 'auto',
          filter: 'grayscale(0.2) contrast(1.05)',
        }}
      />
      <div style={{
        position: 'absolute', inset: 0,
        background: 'radial-gradient(circle at 50% 30%, transparent 40%, rgba(20,22,15,0.55) 100%)',
        pointerEvents: 'none',
      }} />
    </div>
  );
}

// ─────────────────────────────────────────────
// Logotype — mascot + "RWS" stacked
// v1.3: added 'xs' size for the mobile NavBar.
// ─────────────────────────────────────────────
function Logotype({ size = 'md' }) {
  const sizes = {
    xs: { badge: 32, h: 12, sub: 7 },
    sm: { badge: 36, h: 14, sub: 8 },
    md: { badge: 48, h: 18, sub: 9 },
    lg: { badge: 72, h: 28, sub: 11 },
  };
  const s = sizes[size];
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
      <MascotBadge size={s.badge} />
      <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}>
        <div className="stencil" style={{ fontSize: s.h, color: 'var(--paper)', letterSpacing: '0.18em' }}>
          RUSTY WAGE SLAVES
        </div>
        <div className="mono" style={{ fontSize: s.sub, color: 'var(--od-300)', letterSpacing: '0.32em', marginTop: 4 }}>
          RWS COLLECTIVE · MONTHLY WIPES
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────
// NavBar — site-wide header (sticky).
// v1.3: collapses to hamburger + slide-in drawer on ≤768px.
// v1.4: leaderboards + community tabs archived (2026-05-06) — see archive/.
// ─────────────────────────────────────────────
function NavBar({ active = 'home', onNav = () => {}, compact = false }) {
  const items = [
    { id: 'home', label: 'Home' },
    { id: 'servers', label: 'Servers' },
    { id: 'rules', label: 'Rules' },
  ];
  const isMobile = useIsMobile();
  const [drawerOpen, setDrawerOpen] = useState(false);

  // Lock body scroll while drawer is open
  useEffect(() => {
    if (!isMobile) { setDrawerOpen(false); return; }
    document.body.style.overflow = drawerOpen ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [drawerOpen, isMobile]);

  // Close drawer on Escape
  useEffect(() => {
    if (!drawerOpen) return;
    const onKey = (e) => { if (e.key === 'Escape') setDrawerOpen(false); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [drawerOpen]);

  const goto = (id) => { setDrawerOpen(false); onNav(id); };

  return (
    <>
      <header style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between',
        padding: isMobile ? '12px 16px' : (compact ? '14px 28px' : '20px 40px'),
        borderBottom: '1px solid var(--od-700)',
        background: 'linear-gradient(180deg, rgba(28,32,22,0.95), rgba(20,22,15,0.85))',
        backdropFilter: 'blur(6px)',
        position: 'sticky', top: 0, zIndex: 10,
        gap: 12,
      }}>
        <div onClick={() => goto('home')} style={{ cursor: 'pointer', minWidth: 0 }}>
          <Logotype size={isMobile ? 'xs' : (compact ? 'sm' : 'md')} />
        </div>

        {isMobile ? (
          <button
            type="button"
            className={`nav-hamburger${drawerOpen ? ' open' : ''}`}
            aria-label={drawerOpen ? 'Close menu' : 'Open menu'}
            aria-expanded={drawerOpen}
            onClick={() => setDrawerOpen(o => !o)}
          >
            <span />
          </button>
        ) : (
          <nav style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
            {items.map(it => (
              <button key={it.id} onClick={() => onNav(it.id)} className="stencil"
                style={{
                  background: 'transparent',
                  border: 0,
                  padding: '10px 16px',
                  color: active === it.id ? 'var(--paper)' : 'var(--od-300)',
                  fontSize: 13,
                  letterSpacing: '0.14em',
                  textTransform: 'uppercase',
                  cursor: 'pointer',
                  borderBottom: active === it.id ? '2px solid var(--blood)' : '2px solid transparent',
                  fontFamily: 'var(--f-stencil)',
                  fontWeight: 600,
                }}>
                {it.label}
              </button>
            ))}
            <div style={{ width: 1, height: 20, background: 'var(--od-600)', margin: '0 12px' }} />
            <a href="https://discord.gg/f5kBdjfDFR" target="_blank" rel="noopener" className="btn primary" style={{ padding: '10px 16px', fontSize: 12 }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
                <path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 00-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 00-4.8 0c-.15-.34-.36-.76-.55-1.09-.01-.02-.04-.03-.07-.03-1.5.26-2.93.71-4.27 1.33-.01 0-.02.01-.03.02-2.72 4.07-3.47 8.03-3.1 11.95 0 .02.01.04.03.05 1.8 1.32 3.53 2.12 5.24 2.65.03.01.06 0 .07-.02.4-.55.76-1.13 1.07-1.74.02-.04 0-.08-.04-.09-.57-.22-1.11-.48-1.64-.78-.04-.02-.04-.08-.01-.11.11-.08.22-.17.33-.25.02-.02.05-.02.07-.01 3.44 1.57 7.15 1.57 10.55 0 .02-.01.05 0 .07.01.11.09.22.17.33.26.04.03.04.09-.01.11-.52.31-1.07.56-1.64.78-.04.01-.05.06-.04.09.32.61.68 1.19 1.07 1.74.03.01.06.02.09.01 1.72-.53 3.45-1.33 5.25-2.65.02-.01.03-.03.03-.05.44-4.53-.73-8.46-3.1-11.95-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.83 2.12-1.89 2.12z" />
              </svg>
              Discord
            </a>
          </nav>
        )}
      </header>

      {/* Mobile drawer */}
      {isMobile && (
        <>
          <div
            className={`nav-drawer-backdrop${drawerOpen ? ' open' : ''}`}
            onClick={() => setDrawerOpen(false)}
            aria-hidden="true"
          />
          <aside
            className={`nav-drawer${drawerOpen ? ' open' : ''}`}
            aria-hidden={!drawerOpen}
            role="dialog"
            aria-label="Site navigation"
          >
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
              {items.map(it => (
                <button
                  key={it.id}
                  onClick={() => goto(it.id)}
                  className={`nav-drawer-link${active === it.id ? ' active' : ''}`}
                >
                  {it.label}
                </button>
              ))}
            </div>
            <div style={{ marginTop: 24 }}>
              <a
                href="https://discord.gg/f5kBdjfDFR"
                target="_blank"
                rel="noopener"
                className="btn primary"
                style={{ width: '100%', justifyContent: 'center', fontSize: 13 }}
                onClick={() => setDrawerOpen(false)}
              >
                <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
                  <path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 00-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 00-4.8 0c-.15-.34-.36-.76-.55-1.09-.01-.02-.04-.03-.07-.03-1.5.26-2.93.71-4.27 1.33-.01 0-.02.01-.03.02-2.72 4.07-3.47 8.03-3.1 11.95 0 .02.01.04.03.05 1.8 1.32 3.53 2.12 5.24 2.65.03.01.06 0 .07-.02.4-.55.76-1.13 1.07-1.74.02-.04 0-.08-.04-.09-.57-.22-1.11-.48-1.64-.78-.04-.02-.04-.08-.01-.11.11-.08.22-.17.33-.25.02-.02.05-.02.07-.01 3.44 1.57 7.15 1.57 10.55 0 .02-.01.05 0 .07.01.11.09.22.17.33.26.04.03.04.09-.01.11-.52.31-1.07.56-1.64.78-.04.01-.05.06-.04.09.32.61.68 1.19 1.07 1.74.03.01.06.02.09.01 1.72-.53 3.45-1.33 5.25-2.65.02-.01.03-.03.03-.05.44-4.53-.73-8.46-3.1-11.95-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.83 2.12-1.89 2.12z" />
                </svg>
                Join Discord
              </a>
            </div>
            <div className="mono" style={{
              marginTop: 'auto',
              paddingTop: 24,
              fontSize: 9,
              color: 'var(--od-400)',
              letterSpacing: '0.2em',
              textTransform: 'uppercase',
            }}>
              CLOCK IN · CLOCK OUT · GET RAIDED
            </div>
          </aside>
        </>
      )}
    </>
  );
}

// ─────────────────────────────────────────────
// Footer — v1.3: stacks columns on mobile, reduced padding.
// v1.7: accepts onNav for in-app tab routing; Servers + Community columns
//       carry real link targets (Rust steam:// joins, Discord invite, tabs).
// ─────────────────────────────────────────────
function SiteFooter({ onNav }) {
  const isMobile = useIsMobile();

  // Pull live IPs from SERVERS so footer links stay in sync with the canonical
  // server config above. Fall back to the known production IPs if a server is
  // ever renamed/removed so the footer never silently links nowhere.
  const solo = SERVERS.find(s => s.id === 'solo');
  const quad = SERVERS.find(s => s.id === 'quad');
  const soloIp = (solo && solo.ip) || '72.48.113.139:28018';
  const quadIp = (quad && quad.ip) || '72.48.113.139:28015';

  const goto = (id) => (e) => {
    if (e && e.preventDefault) e.preventDefault();
    if (typeof onNav === 'function') onNav(id);
  };

  return (
    <footer style={{
      padding: isMobile ? '32px 18px 24px' : '40px 40px 32px',
      borderTop: '1px solid var(--od-700)',
      background: 'var(--od-900)',
      display: 'grid',
      gridTemplateColumns: isMobile ? '1fr' : '1.5fr 1fr 1fr 1fr',
      gap: isMobile ? 28 : 40,
    }}>
      <div>
        <Logotype size="sm" />
        <p style={{ color: 'var(--od-300)', fontSize: 12, lineHeight: 1.6, marginTop: 16, maxWidth: 300 }}>
          A community for the tired, the determined, the caffeinated.
          We host servers. We build bases. We get raided. We come back.
        </p>
      </div>
      <FooterCol title="Servers" items={[
        { label: 'Solo Only',     href: `steam://connect/${soloIp}` },
        { label: 'Quads',         href: `steam://connect/${quadIp}` },
        { label: 'Server status', href: '#servers', onClick: goto('servers') },
      ]} />
      <FooterCol title="Community" items={[
        { label: 'Discord',     href: 'https://discord.gg/f5kBdjfDFR', target: '_blank', rel: 'noopener noreferrer' },
        { label: 'Rules',       href: '#rules', onClick: goto('rules') },
        { label: 'Ban appeals' },
      ]} />
      <FooterCol title="Support" items={['FAQ', 'Report a cheater', 'Contact staff', 'VIP info']} />
      <div style={{
        gridColumn: '1 / -1',
        borderTop: '1px dashed var(--od-700)',
        paddingTop: 20,
        marginTop: 8,
        display: 'flex',
        flexDirection: isMobile ? 'column' : 'row',
        justifyContent: 'space-between',
        gap: isMobile ? 8 : 0,
        fontFamily: 'var(--f-mono)', fontSize: 10, color: 'var(--od-400)',
        letterSpacing: '0.18em', textTransform: 'uppercase',
      }}>
        <span>© 2026 RWS COLLECTIVE · Not affiliated with Facepunch Studios</span>
        <span>CLOCK IN · CLOCK OUT · GET RAIDED</span>
      </div>
    </footer>
  );
}

// FooterCol — items may be plain strings (placeholder, no link) OR objects
// of shape { label, href?, target?, rel?, onClick? }. v1.7.
function FooterCol({ title, items }) {
  return (
    <div>
      <div className="stencil" style={{ fontSize: 12, color: 'var(--paper)', letterSpacing: '0.18em', marginBottom: 14 }}>{title}</div>
      <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
        {items.map(raw => {
          const item = typeof raw === 'string' ? { label: raw } : raw;
          const { label, href, target, rel, onClick } = item;
          return (
            <li key={label}>
              <a
                href={href || '#'}
                target={target}
                rel={rel || (target === '_blank' ? 'noopener noreferrer' : undefined)}
                onClick={onClick}
                style={{ color: 'var(--od-300)', fontSize: 12, textDecoration: 'none' }}
                onMouseEnter={e => e.currentTarget.style.color = 'var(--paper)'}
                onMouseLeave={e => e.currentTarget.style.color = 'var(--od-300)'}>
                {label}
              </a>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

// ─────────────────────────────────────────────
// Mock data
// ─────────────────────────────────────────────
// SERVERS — static config. Live fields (players, maxPlayers, queue, map) are
// fetched from /api/servers and merged in via mergeLive(). The fallback values
// here are shown until /api/servers responds (or if a server has no bmId yet).
const SERVERS = [
  {
    id: 'solo',
    bmId: null, // Not yet indexed by BattleMetrics — paste the BM numeric ID once available
    name: 'RWS · SOLO ONLY',
    tagline: 'No teams. No groups. Just you.',
    url: 'solo.rustywageslaves.com',
    ip: '72.48.113.139:28018',
    gather: '2x',
    maxGroup: 1,
    wipe: 'First Thursday, monthly',
    map: 'Procedural Map',
    players: 0,
    maxPlayers: 100,
    queue: 0,
    status: 'LIVE',
    features: ['Solo only', '2x gather', 'Offline protection', 'No BPs wiped'],
    color: '#b33030',
  },
  {
    id: 'quad',
    bmId: '38912303', // BattleMetrics: RustyWageSlaves 2x Quads Offline Protection
    name: 'RWS · MAX QUAD',
    tagline: 'Squads of four. Run with your crew.',
    url: 'quads.rustywageslaves.com',
    ip: '72.48.113.139:28015',
    gather: '2x',
    maxGroup: 4,
    wipe: 'First Thursday, monthly',
    map: 'Procedural Map',
    players: 0,
    maxPlayers: 300,
    queue: 0,
    status: 'LIVE',
    features: ['Max 4 player teams', '2x gather', 'Offline protection', 'Active admins'],
    color: '#d4a017',
  },
];

// LEADERBOARD mock data archived 2026-05-06 — see archive/leaderboards-page.jsx.

const RULES = [
  {
    cat: '01',
    title: 'Cheating & exploits',
    body: 'No cheating, hacking, scripting, or third-party tools that provide an unfair advantage. No exploiting bugs, glitches, or unintended mechanics. Do not share or encourage cheats or exploits.',
    severity: 'permanent',
  },
  {
    cat: '02',
    title: 'Respect & conduct',
    body: 'No harassment, hate speech, or excessive toxicity (racism, sexism, threats, etc.). Trash talk is part of Rust — malicious or targeted harassment is not. Do not impersonate staff or other players.',
    severity: '3-day → permanent',
  },
  {
    cat: '03',
    title: 'Chat & communication',
    body: 'No spamming, advertising, or flooding chat/voice. Keep global chat reasonably readable. No links to malicious, illegal, or explicit content.',
    severity: 'warning → 7-day',
  },
  {
    cat: '04',
    title: 'Gameplay rules',
    body: 'No griefing beyond normal Rust gameplay (e.g. land-claim abuse, blocking monuments or spawns where restricted). Do not intentionally lag or crash the server. Team limits must be followed — no alliance abuse.',
    severity: '1-day → permanent',
  },
  {
    cat: '05',
    title: 'Raiding & PvP',
    body: 'PvP and raiding are allowed during raiding hours. Offlining during raiding hours carries a 2x cost. Combat logging or intentionally disconnecting to avoid PvP or raids is prohibited where detectable. No exploiting safe zones or monument mechanics for unfair PvP advantage.',
    severity: '1-day → 7-day',
  },
  {
    cat: '06',
    title: 'Bases & builds',
    body: 'No building that obstructs monuments, roads, or safe zones (unless explicitly allowed), or that causes excessive server lag. External TCs and base-spam abuse may be removed by staff without warning.',
    severity: 'removal → 3-day',
  },
  {
    cat: '07',
    title: 'Wipes & progress',
    body: 'All wipes, resets, and changes are final. No refunds or compensation for losses due to wipes, crashes, or player actions. Plan accordingly — everything is temporary.',
    severity: 'policy',
  },
  {
    cat: '08',
    title: 'Staff authority',
    body: 'Admin and moderator decisions are final. Do not argue enforcement actions in global chat — use the proper appeal channel on Discord. Abuse of staff or false reporting may lead to punishment.',
    severity: 'warning → permanent',
  },
  {
    cat: '09',
    title: 'Enforcement',
    body: 'Rules are enforced at staff discretion. Punishments may include warnings, kicks, temporary bans, or permanent bans. "I didn\'t know" is not an excuse — read the rules before you connect.',
    severity: 'policy',
  },
];

// ─────────────────────────────────────────────
// Wipe schedule — first Thursday of each month at 18:00 America/Chicago.
// DST-aware: that's 23:00 UTC during CDT and 00:00 UTC (next day) during CST.
// Display label is also Chicago-local ("MAY 7 · 18:00 CDT").
// ─────────────────────────────────────────────
const WIPE_TZ = 'America/Chicago';

// UTC offset in minutes for the given Date in WIPE_TZ. CDT=-300, CST=-360.
function getChicagoOffsetMinutes(d) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: WIPE_TZ, timeZoneName: 'longOffset',
  }).formatToParts(d);
  const off = parts.find(p => p.type === 'timeZoneName')?.value || 'GMT+00:00';
  const m = off.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
  if (!m) return 0;
  return (m[1] === '-' ? -1 : 1) * (parseInt(m[2], 10) * 60 + parseInt(m[3] || '0', 10));
}

// 0=Sun, 4=Thu — what day-of-week the given UTC instant is in WIPE_TZ.
function getDayInChicago(d) {
  const wd = new Intl.DateTimeFormat('en-US', { timeZone: WIPE_TZ, weekday: 'short' }).format(d);
  return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].indexOf(wd);
}

function getNextWipe(now = new Date()) {
  // Probe each day 1..7 at 18:00 Chicago, return the first one that is Thursday in Chicago.
  function firstThursdayChicagoAt6pm(year, monthIdx) {
    for (let day = 1; day <= 7; day++) {
      // Probe at 23:00 UTC — within the same Chicago day for both CDT and CST.
      const probe = new Date(Date.UTC(year, monthIdx, day, 23, 0));
      const off = getChicagoOffsetMinutes(probe);
      // 18:00 Chicago on this day → UTC = Date.UTC(...,18,0) - offset
      const wipe = new Date(Date.UTC(year, monthIdx, day, 18, 0) - off * 60000);
      if (getDayInChicago(wipe) === 4) return wipe;
    }
    return null;
  }
  let wipe = firstThursdayChicagoAt6pm(now.getUTCFullYear(), now.getUTCMonth());
  if (!wipe || wipe <= now) {
    const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 12, 0));
    wipe = firstThursdayChicagoAt6pm(next.getUTCFullYear(), next.getUTCMonth());
  }
  return wipe;
}

function formatWipeDate(d) {
  const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: WIPE_TZ,
    month: 'numeric', day: 'numeric',
    hour: '2-digit', minute: '2-digit', hour12: false,
    timeZoneName: 'short', // 'CDT' or 'CST'
  }).formatToParts(d);
  const get = (t) => parts.find(p => p.type === t)?.value || '';
  const m = parseInt(get('month'), 10) - 1;
  // Intl can render 24:XX instead of 00:XX in hour12:false mode in some engines — normalize.
  const hh = get('hour') === '24' ? '00' : get('hour');
  return `${months[m]} ${parseInt(get('day'), 10)} · ${hh}:${get('minute')} ${get('timeZoneName')}`;
}

function formatTimeUntil(target, now = new Date()) {
  const ms = Math.max(0, target.getTime() - now.getTime());
  const days = Math.floor(ms / 86400000);
  const hours = Math.floor((ms % 86400000) / 3600000);
  const mins = Math.floor((ms % 3600000) / 60000);
  if (days > 0) return `${days}D ${String(hours).padStart(2,'0')}H`;
  if (hours > 0) return `${hours}H ${String(mins).padStart(2,'0')}M`;
  return `${mins}M`;
}

// ─────────────────────────────────────────────
// Live Discord stats — fetches /api/discord and re-fetches every 5 minutes.
// The Pages Function caches the upstream Discord call at the edge for 5min,
// so client-side polling at the same cadence gives every visitor near-fresh
// numbers without ever hitting Discord's rate limits. Returns null until the
// first fetch resolves; consumers should check `stats?.members != null`
// before rendering numbers to avoid flashing placeholder text.
//
// DISCORD_STATS_MIN_MEMBERS gates whether the numbers are shown publicly —
// while the server is small the counts read as embarrassing rather than
// impressive. Bump or remove once we cross the threshold.
// ─────────────────────────────────────────────
const DISCORD_STATS_MIN_MEMBERS = 50;
const isDiscordStatsVisible = (stats) =>
  stats != null && typeof stats.members === 'number' && stats.members >= DISCORD_STATS_MIN_MEMBERS;

function useDiscordStats(refreshMs = 300000) {
  const [stats, setStats] = useState(null);
  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        const r = await fetch('/api/discord', { cache: 'no-store' });
        if (!r.ok) return;
        const json = await r.json();
        if (!cancelled) setStats(json);
      } catch { /* keep last successful payload */ }
    };
    load();
    const t = setInterval(load, refreshMs);
    return () => { cancelled = true; clearInterval(t); };
  }, [refreshMs]);
  return stats;
}

// ─────────────────────────────────────────────
// Live server data — fetches /api/servers and re-fetches every 30s.
// Falls back silently to static SERVERS values if the endpoint isn't available
// (e.g. opened from file://, design canvas, dev server without functions).
// ─────────────────────────────────────────────
function useLiveServers(refreshMs = 30000) {
  const [live, setLive] = useState(null); // null until first fetch resolves
  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        const r = await fetch('/api/servers', { cache: 'no-store' });
        if (!r.ok) return;
        const json = await r.json();
        if (!cancelled) setLive(json);
      } catch { /* leave previous live data in place */ }
    };
    load();
    const t = setInterval(load, refreshMs);
    return () => { cancelled = true; clearInterval(t); };
  }, [refreshMs]);
  return live;
}

// Merge live numbers into a static server entry. Live fields override static
// ones; if live is missing or errored for that server, static values are kept.
// Always recomputes nextWipe from the schedule rule.
function mergeLive(server, live) {
  const nextWipe = formatWipeDate(getNextWipe());
  const l = live && live[server.id];
  if (!l || l.error) return { ...server, nextWipe };
  return {
    ...server,
    players: l.players ?? server.players,
    maxPlayers: l.maxPlayers ?? server.maxPlayers,
    queue: l.queue ?? server.queue,
    map: l.map ?? server.map,
    nextWipe,
  };
}

// Expose
Object.assign(window, {
  MascotBadge, Logotype, NavBar, SiteFooter,
  useIsMobile,
  SERVERS, LEADERBOARD, RULES,
  getNextWipe, formatWipeDate, formatTimeUntil,
  getChicagoOffsetMinutes, getDayInChicago, WIPE_TZ,
  useLiveServers, mergeLive,
  useDiscordStats, isDiscordStatsVisible, DISCORD_STATS_MIN_MEMBERS,
});
