// Gameadzone Play — React app with AdSense banner + rewarded ad-gated play.
const { useState, useEffect, useMemo, useCallback, useRef, useId } = React;

// ──────────────────────────────────────────────────────────────
// Google Analytics 4 — thin wrapper.
// gtag() is defined by the GA4 snippet in index.html. If GA4 hasn't
// loaded yet (or the user blocks it), this is a no-op.
// ──────────────────────────────────────────────────────────────
function trackEvent(name, params) {
  try {
    if (typeof window.gtag === "function") {
      window.gtag("event", name, params || {});
    }
  } catch {}
}

// ──────────────────────────────────────────────────────────────
// GDPR / CCPA Consent management
// ──────────────────────────────────────────────────────────────
const CONSENT_KEY = "gadz_consent_v1";
// Consent popup has been removed — every visitor is treated as "accepted"
// (personalized ads) by default. AdSense / GPT still respect the user's
// browser-level / regional controls (TCF, GPC, etc.) when applicable.
// { state: 'accepted' | 'rejected', ts: epoch_ms }
const DEFAULT_CONSENT = { state: "accepted", ts: 0 };
function readConsent() {
  try {
    const raw = localStorage.getItem(CONSENT_KEY);
    if (!raw) return DEFAULT_CONSENT;
    const parsed = JSON.parse(raw);
    if (!parsed || !parsed.state) return DEFAULT_CONSENT;
    return parsed;
  } catch { return DEFAULT_CONSENT; }
}
function writeConsent(state) {
  try {
    localStorage.setItem(CONSENT_KEY, JSON.stringify({ state, ts: Date.now() }));
  } catch {}
}
function applyConsentToAdSense(consent) {
  // Before any adsbygoogle.push, Google reads this flag.
  window.adsbygoogle = window.adsbygoogle || [];
  if (consent?.state === "rejected") {
    window.adsbygoogle.requestNonPersonalizedAds = 1;
  } else {
    // Accepted (or unset → we won't push yet anyway)
    window.adsbygoogle.requestNonPersonalizedAds = 0;
  }
}

// ──────────────────────────────────────────────────────────────
// Coin system — per-user play currency
// ──────────────────────────────────────────────────────────────
// Default: every new user gets 1 free coin.
// Every game launch costs COIN_COST_PER_PLAY coin.
// When the wallet hits zero, the user is offered a rewarded ad
// that mints 1 coin, which is then immediately spent on the play.
const COINS_KEY = "gadz_coins_v1";
const DEFAULT_COINS = 1;
const COIN_COST_PER_PLAY = 1;
const COIN_REWARD_AMOUNT = 1;

function readCoins() {
  try {
    const raw = localStorage.getItem(COINS_KEY);
    if (raw === null) return DEFAULT_COINS;      // first-time user
    const n = parseInt(raw, 10);
    return Number.isFinite(n) && n >= 0 ? n : DEFAULT_COINS;
  } catch { return DEFAULT_COINS; }
}
function writeCoins(n) {
  try {
    localStorage.setItem(COINS_KEY, String(Math.max(0, n | 0)));
  } catch {}
}

const BASE = "https://www.madkidgames.com";
const FULL = (slug) => `${BASE}/full/${slug}`;
const THUMB = (slug) => `${BASE}/games/${slug}/thumb_2.jpg`;

function categorize(title) {
  const t = " " + title.toLowerCase() + " ";
  for (const rule of window.CATEGORY_RULES) {
    for (const key of rule.keys) {
      if (t.includes(key.toLowerCase())) return rule.cat;
    }
  }
  return "Arcade";
}

const ALL_GAMES = window.GAMES_RAW.map((g) => ({
  title: g.t,
  slug: g.s,
  url: FULL(g.s),
  thumb: THUMB(g.s),
  category: categorize(g.t),
}));

// Pin priority-category games to the top of the "All" view — Dress Up
// first, then Kids & Family, then everything else (original order
// preserved inside each bucket). Categorization itself is unchanged.
const PRIORITY_ORDER = ["Dress Up & Fashion", "Kids & Family"];
(function pinPriorityGames() {
  const buckets = PRIORITY_ORDER.map((cat) => ALL_GAMES.filter((g) => g.category === cat));
  const rest = ALL_GAMES.filter((g) => !PRIORITY_ORDER.includes(g.category));
  ALL_GAMES.length = 0;
  buckets.forEach((b) => ALL_GAMES.push(...b));
  ALL_GAMES.push(...rest);
})();

// Priority categories are surfaced first in the category bar, right after
// "All", regardless of how many games they contain. Order here = order shown.
const PRIORITY_CATEGORIES = ["Dress Up & Fashion", "Kids & Family"];

function getCategoryList(games) {
  const counts = {};
  games.forEach((g) => {
    counts[g.category] = (counts[g.category] || 0) + 1;
  });
  const all = Object.entries(counts);

  // Pull out priority categories (if present) and keep their declared order.
  const priority = PRIORITY_CATEGORIES
    .map((name) => all.find(([n]) => n === name))
    .filter(Boolean);

  // Remaining categories sorted by count desc.
  const rest = all
    .filter(([n]) => !PRIORITY_CATEGORIES.includes(n))
    .sort((a, b) => b[1] - a[1]);

  return [["All", games.length], ...priority, ...rest];
}

function Header({ search, setSearch, total, coins, onEarnCoin, earnLoading }) {
  const plural = coins === 1 ? "" : "s";
  return (
    <header className="header" role="banner">
      <div className="header-inner">
        {/* ── Brand ─────────────────────────────────────────────── */}
        <a className="brand" href="#" onClick={(e) => e.preventDefault()} aria-label="Gameadzone Play — home">
          <span className="brand-mark" aria-hidden="true">
            <svg viewBox="0 0 64 64" width="36" height="36" role="img">
              <defs>
                {/* Outer hex: lighter top → deeper blue bottom */}
                <linearGradient id="hexg" x1="0.5" y1="0" x2="0.5" y2="1">
                  <stop offset="0%"  stopColor="#38b6ff" />
                  <stop offset="55%" stopColor="#129cf0" />
                  <stop offset="100%" stopColor="#0a7cc8" />
                </linearGradient>
                {/* Soft inner swirl highlight */}
                <radialGradient id="hexglow" cx="0.35" cy="0.3" r="0.9">
                  <stop offset="0%"   stopColor="#8fd6ff" stopOpacity=".75" />
                  <stop offset="55%"  stopColor="#38b6ff" stopOpacity="0" />
                </radialGradient>
                {/* Subtle shadow on the ring */}
                <radialGradient id="ringShadow" cx="0.5" cy="0.55" r="0.55">
                  <stop offset="70%" stopColor="#000" stopOpacity="0" />
                  <stop offset="100%" stopColor="#000" stopOpacity=".18" />
                </radialGradient>
              </defs>

              {/* Hexagon body (flat-top, proportioned like the reference) */}
              <polygon
                points="32,2 58,17 58,47 32,62 6,47 6,17"
                fill="url(#hexg)"
                stroke="#0a6aa8"
                strokeWidth="1"
                strokeLinejoin="round"
              />
              {/* Swirl highlight overlay */}
              <polygon
                points="32,2 58,17 58,47 32,62 6,47 6,17"
                fill="url(#hexglow)"
              />
              {/* Diagonal swoosh — mimics the twisted ribbon in the reference */}
              <path
                d="M9 22 C 24 14, 40 52, 55 42 L 55 47 C 40 56, 24 18, 9 27 Z"
                fill="#ffffff" fillOpacity=".12"
              />
              <path
                d="M10 40 C 26 48, 42 12, 54 22 L 54 25 C 42 17, 26 52, 10 44 Z"
                fill="#000000" fillOpacity=".08"
              />

              {/* Inner white disc */}
              <circle cx="32" cy="32" r="16" fill="url(#ringShadow)" />
              <circle cx="32" cy="32" r="15.5" fill="#ffffff" />

              {/* Bold "G" — dark slate gray, clean geometric */}
              <text
                x="32" y="40"
                textAnchor="middle"
                fontFamily="'Helvetica Neue', Arial, sans-serif"
                fontSize="22"
                fontWeight="900"
                fill="#3a3a3e"
                letterSpacing="-0.5"
              >G</text>
            </svg>
          </span>
          <span className="brand-text">
            <span className="brand-name">Gameadzone<span className="brand-dot">·</span>Play</span>
            <span className="brand-tag">
              <span className="brand-stat">play instantly</span>
            </span>
          </span>
        </a>

        {/* ── Search (middle, grows) ────────────────────────────── */}
        <div className="search-wrap">
          <span className="search-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="11" cy="11" r="7" />
              <path d="m20 20-3.5-3.5" />
            </svg>
          </span>
          <input
            className="search"
            type="search"
            placeholder="Search 400+ games…"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            aria-label="Search games"
          />
          {search ? (
            <button
              type="button"
              className="search-clear"
              onClick={() => setSearch("")}
              aria-label="Clear search"
            >×</button>
          ) : (
            <kbd className="search-kbd" aria-hidden="true">/</kbd>
          )}
        </div>

        {/* ── Coin wallet (right) — whole pill is clickable to open the
             Earn Coins popup which then runs a rewarded ad. */}
        <button
          type="button"
          className={"coin-wallet" + (coins === 0 ? " is-empty" : "")}
          onClick={onEarnCoin}
          disabled={earnLoading}
          aria-label={`You have ${coins} coin${plural}. Click to earn more.`}
          title="Click to earn a coin (watch a short ad)"
        >
          <div className="coin-wallet-balance" title={`${coins} coin${plural} available`}>
            <span className="coin-wallet-icon" aria-hidden="true">
              <svg viewBox="0 0 24 24" width="18" height="18">
                <defs>
                  <linearGradient id="cg" x1="0" y1="0" x2="1" y2="1">
                    <stop offset="0%" stopColor="#ffe27a" />
                    <stop offset="100%" stopColor="#ff9c3b" />
                  </linearGradient>
                </defs>
                <circle cx="12" cy="12" r="9" fill="url(#cg)" stroke="#c76a14" strokeWidth="1" />
                <circle cx="12" cy="12" r="6" fill="none" stroke="#c76a14" strokeWidth=".8" opacity=".6" />
                <text x="12" y="16" textAnchor="middle" fontSize="9" fontWeight="900" fill="#7a3f00" fontFamily="system-ui">G</text>
              </svg>
            </span>
            <span className="coin-wallet-count">{coins}</span>
            <span className="coin-wallet-label">coin{plural}</span>
          </div>
          <span
            className="coin-wallet-earn"
            aria-hidden="true"
          >
            {earnLoading ? (
              <span className="coin-wallet-earn-label">…</span>
            ) : (
              <>
                <span className="coin-wallet-earn-plus">+</span>
                <span className="coin-wallet-earn-label">Earn</span>
              </>
            )}
          </span>
        </button>
      </div>
    </header>
  );
}

function CategoryBar({ categories, active, setActive }) {
  return (
    <nav className="cat-bar" aria-label="Categories">
      {categories.map(([name, count]) => (
        <button
          key={name}
          className={"cat-btn" + (active === name ? " active" : "")}
          onClick={() => setActive(name)}
        >
          {name}
          <span className="count">{count}</span>
        </button>
      ))}
    </nav>
  );
}

// AdSense banner slot with IntersectionObserver-based lazy loading.
// The ad script is only pushed once the slot is within 200px of the viewport
// AND the user has given (or rejected) cookie consent.
// In dev mode (ADS_ENABLED=false) a labeled placeholder is shown instead.
function AdBanner({ slot, format = "auto", responsive = true, label = "Advertisement", rootMargin = "200px", consent }) {
  const wrapRef = useRef(null);
  const pushedRef = useRef(false);
  const [inView, setInView] = useState(false);
  const {
    ADS_ENABLED, ADSENSE_PUBLISHER_ID, SLOTS, TEST_MODE,
    GPT_TEST_AD_UNIT, GPT_TEST_SIZES,
  } = window.ADS;
  const slotId = SLOTS[slot];

  // Stable unique DOM id for the GPT slot container.
  // useId gives something like ":r3:" — we strip colons for HTML-id safety.
  const reactId = useId();
  const gptDivId = `gadz-gpt-${slot}-${reactId.replace(/:/g, "")}`;
  const [gptW, gptH] = Array.isArray(GPT_TEST_SIZES?.[0])
    ? GPT_TEST_SIZES[0]
    : (GPT_TEST_SIZES || [300, 250]);

  // Observe the placeholder — flip inView once it nears the viewport.
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;

    // Older browsers without IntersectionObserver: load eagerly.
    if (typeof IntersectionObserver === "undefined") {
      setInView(true);
      return;
    }

    const io = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            setInView(true);
            io.disconnect();
            break;
          }
        }
      },
      { rootMargin, threshold: 0.01 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, [rootMargin]);

  // Once in view AND ads enabled AND consent given/denied, push exactly once.
  // No consent decision yet → hold back; push on consent change.
  // Banners always use Google Publisher Tag (GPT) with the configured banner
  // ad unit (GPT_TEST_AD_UNIT, despite the legacy name, holds the LIVE banner
  // unit path). This keeps every banner placement a distinct GPT slot with a
  // unique div id, so the 2nd/3rd/Nth banner on the page each trigger their
  // own ad request instead of silently reusing the first impression.
  useEffect(() => {
    if (!ADS_ENABLED || !inView || pushedRef.current) return;
    if (!consent) return; // waiting on user choice
    pushedRef.current = true;

    // Key GPT gotcha: once googletag.enableServices() has been called
    // (i.e., for banner #2, #3, …), googletag.display() alone does NOT
    // fetch an ad. You must additionally call pubads().refresh([slot]).
    // For the FIRST banner we do the classic enableServices() + display()
    // flow; for every subsequent banner we defineSlot + display() + refresh().
    try {
      window.googletag = window.googletag || { cmd: [] };
      window.googletag.cmd.push(() => {
        try {
          const pubads = window.googletag.pubads();

          // Idempotent: only define once per gptDivId.
          let slot = pubads.getSlots().find((s) => s.getSlotElementId() === gptDivId);
          if (!slot) {
            slot = window.googletag
              .defineSlot(GPT_TEST_AD_UNIT, GPT_TEST_SIZES, gptDivId);
            if (!slot) {
              console.warn("[ads] defineSlot returned null for", gptDivId);
              return;
            }
            slot.addService(pubads);
          }

          if (!window.__gadzServicesEnabled) {
            // FIRST slot: one-time service enable + display.
            pubads.collapseEmptyDivs(true);
            window.googletag.enableServices();
            window.__gadzServicesEnabled = true;
            window.googletag.display(gptDivId);
          } else {
            // Subsequent slots: register the div with GPT, then force a
            // refresh to actually fetch an ad for this slot.
            window.googletag.display(gptDivId);
            pubads.refresh([slot]);
          }
        } catch (e) {
          console.warn("[ads] GPT banner setup failed", e);
        }
      });
    } catch (e) {
      console.warn("[ads] GPT setup failed", e);
    }
  }, [ADS_ENABLED, inView, consent, gptDivId]);

  if (!ADS_ENABLED) {
    return (
      <div ref={wrapRef} className="ad-slot ad-slot-placeholder" aria-label={label}>
        <span className="ad-chip">Ad</span>
        <span className="ad-hint">
          {inView ? `Banner slot (${slot}) — activate in ads.js` : `Banner slot (${slot}) — lazy-loaded`}
        </span>
      </div>
    );
  }

  // Live GPT banner: fixed-size container matches the creative exactly,
  // so the skeleton→ad transition is CLS-free across all placements.
  return (
    <div
      ref={wrapRef}
      className="ad-slot"
      aria-label={label}
      style={{ minHeight: gptH + 10 }}
    >
      {inView ? (
        <div
          id={gptDivId}
          style={{ width: gptW, height: gptH, minWidth: gptW, minHeight: gptH }}
        />
      ) : (
        <div
          className="ad-skeleton"
          aria-hidden="true"
          style={{ width: gptW, height: gptH }}
        />
      )}
    </div>
  );
}

// Full-screen loading popup shown while any ad is being fetched / displayed.
// Blocks the whole app so the user can't trigger a second request during
// an in-flight one.
function AdLoadingModal() {
  return (
    <div
      className="modal-backdrop ad-loading-backdrop"
      role="dialog"
      aria-modal="true"
      aria-label="Loading ad"
    >
      <div className="modal ad-loading-modal">
        <div className="ad-loading-spinner" aria-hidden="true">
          <svg viewBox="0 0 50 50" width="44" height="44">
            <circle cx="25" cy="25" r="20" fill="none" stroke="rgba(167,139,255,.2)" strokeWidth="4" />
            <circle
              cx="25" cy="25" r="20" fill="none"
              stroke="url(#adSpinGrad)"
              strokeWidth="4" strokeLinecap="round"
              strokeDasharray="90 60"
            >
              <animateTransform
                attributeName="transform" type="rotate"
                from="0 25 25" to="360 25 25" dur="0.9s"
                repeatCount="indefinite"
              />
            </circle>
            <defs>
              <linearGradient id="adSpinGrad" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0%" stopColor="#ffd43b" />
                <stop offset="100%" stopColor="#a78bff" />
              </linearGradient>
            </defs>
          </svg>
        </div>
        <h2 className="ad-loading-title">Loading Ad…</h2>
        <p className="ad-loading-body">Please wait a moment while we fetch a short ad.</p>
      </div>
    </div>
  );
}

function ConsentBanner({ onChoose }) {
  return (
    <div className="consent-banner" role="dialog" aria-label="Cookie consent">
      <div className="consent-body">
        <div className="consent-text">
          <strong>We value your privacy</strong>
          <p>
            We use cookies and similar technologies to serve ads (Google AdSense) and remember your
            preferences. You can accept personalized ads or continue with non-personalized ads only.
            See our <a href="privacy.html">Privacy Policy</a> for details.
          </p>
        </div>
        <div className="consent-actions">
          <button className="btn btn-ghost" onClick={() => onChoose("rejected")}>Reject</button>
          <button className="btn btn-primary" onClick={() => onChoose("accepted")}>Accept All</button>
        </div>
      </div>
    </div>
  );
}

// Shown in two situations:
//   mode="outOfCoins" → user tried to play with 0 coins; offers rewarded
//                       ad → earn 1 coin → immediately launch the game
//   mode="earn"       → user tapped the coin wallet in the header; offers
//                       rewarded ad → earn 1 coin (no pending game)
function OutOfCoinsModal({ game, mode = "outOfCoins", onWatch, onClose, adLoading }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !adLoading) onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose, adLoading]);

  const isEarn = mode === "earn";

  return (
    <div
      className="modal-backdrop"
      onClick={() => !adLoading && onClose()}
      role="dialog"
      aria-modal="true"
      aria-labelledby="coins-modal-title"
    >
      <div className="modal coins-modal" onClick={(e) => e.stopPropagation()}>
        <div className="coins-modal-icon" aria-hidden="true">🪙</div>
        <h2 id="coins-modal-title">{isEarn ? "Earn a Coin" : "Out of Coins"}</h2>
        <p className="coins-modal-body">
          {isEarn ? (
            <>
              Watch a short rewarded ad to earn
              <strong> +{COIN_REWARD_AMOUNT} coin</strong>.
              <br />
              1 coin lets you play 1 game.
            </>
          ) : (
            <>
              You need <strong>{COIN_COST_PER_PLAY} coin</strong> to play
              {game ? <> <em className="coins-modal-game">“{game.title}”</em></> : null}.
              <br />
              Watch a short rewarded ad to earn <strong>+{COIN_REWARD_AMOUNT} coin</strong> and
              play right away.
            </>
          )}
        </p>
        <div className="coins-modal-actions">
          <button
            type="button"
            className="btn btn-ghost"
            onClick={onClose}
            disabled={adLoading}
          >
            Cancel
          </button>
          <button
            type="button"
            className="btn btn-primary coins-modal-watch"
            onClick={onWatch}
            disabled={adLoading}
            autoFocus
          >
            {adLoading ? (
              <>Loading ad…</>
            ) : (
              <>🎁 Watch Ad · <strong>+{COIN_REWARD_AMOUNT} Coin</strong></>
            )}
          </button>
        </div>
        <p className="coins-modal-fine">
          Ads are served by Google. Your coin balance is saved in this browser only.
        </p>
      </div>
    </div>
  );
}

function GameCard({ game, onPlay }) {
  const [imgError, setImgError] = useState(false);
  return (
    <article className="game-card">
      <button className="thumb-btn" onClick={() => onPlay(game)} aria-label={`Play ${game.title}`}>
        {imgError ? (
          <div className="thumb-fallback">{game.title[0]}</div>
        ) : (
          <img
            src={game.thumb}
            alt={game.title}
            loading="lazy"
            onError={() => setImgError(true)}
          />
        )}
        <span className="play-overlay">▶ Play</span>
      </button>
      <div className="game-meta">
        <h3 title={game.title}>{game.title}</h3>
        <div className="card-actions">
          <button className="btn btn-primary" onClick={() => onPlay(game)}>▶ Play</button>
        </div>
      </div>
    </article>
  );
}

// Full-viewport player — NOT a modal. The container is always 100vw × 100vh
// from the moment it mounts, so the iframe never sees a layout change
// (which is what crashed Unity/Phaser games when we tried auto-fullscreen
// on a resizing modal). We attempt the browser Fullscreen API once the
// iframe has loaded, which only strips the browser chrome — no layout shift.
function PlayerModal({ game, onClose }) {
  const containerRef = useRef(null);
  const loadedRef = useRef(false);

  const requestFS = useCallback(() => {
    const el = containerRef.current;
    if (!el) return false;
    const fn =
      el.requestFullscreen ||
      el.webkitRequestFullscreen ||
      el.mozRequestFullScreen ||
      el.msRequestFullscreen;
    if (!fn) return false;
    try {
      const p = fn.call(el);
      if (p && p.catch) p.catch(() => {});
      return true;
    } catch { return false; }
  }, []);

  useEffect(() => {
    const onKey = (e) => e.key === "Escape" && onClose();
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";

    // Exit browser fullscreen if the user hits Esc or ✕.
    const onFSChange = () => {
      const fsEl =
        document.fullscreenElement ||
        document.webkitFullscreenElement ||
        document.mozFullScreenElement ||
        document.msFullscreenElement;
      // If the user exits native fullscreen, keep the player open (they can
      // still play in the full-viewport view). Don't auto-close.
      void fsEl;
    };
    document.addEventListener("fullscreenchange", onFSChange);
    document.addEventListener("webkitfullscreenchange", onFSChange);

    return () => {
      document.removeEventListener("keydown", onKey);
      document.removeEventListener("fullscreenchange", onFSChange);
      document.removeEventListener("webkitfullscreenchange", onFSChange);
      document.body.style.overflow = "";
      // Exit native fullscreen on unmount if still active.
      const fsEl =
        document.fullscreenElement ||
        document.webkitFullscreenElement ||
        document.mozFullScreenElement ||
        document.msFullscreenElement;
      if (fsEl) {
        const exit =
          document.exitFullscreen ||
          document.webkitExitFullscreen ||
          document.mozCancelFullScreen ||
          document.msExitFullscreen;
        try { exit && exit.call(document); } catch {}
      }
    };
  }, [onClose]);

  const handleIframeLoad = () => {
    if (loadedRef.current) return;
    loadedRef.current = true;
    // Best-effort: request browser fullscreen once the game has loaded.
    // The container is ALREADY 100vw × 100vh, so fullscreen doesn't resize
    // anything — it just hides the browser chrome. Games don't crash.
    // If the gesture chain has expired, requestFullscreen silently rejects;
    // the user can still use the ⛶ button.
    requestFS();
  };

  const exitFS = () => {
    const fsEl =
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement ||
      document.msFullscreenElement;
    if (fsEl) {
      const exit =
        document.exitFullscreen ||
        document.webkitExitFullscreen ||
        document.mozCancelFullScreen ||
        document.msExitFullscreen;
      try { exit && exit.call(document); } catch {}
    }
    onClose();
  };

  return (
    <div ref={containerRef} id="game-modal" className="game-view" role="region" aria-label={`Playing ${game.title}`}>
      <div className="game-view-head">
        <h2 className="game-view-title" title={game.title}>{game.title}</h2>
        <div className="game-view-actions">
          <button className="btn btn-ghost" onClick={requestFS} title="Fullscreen" aria-label="Fullscreen">⛶</button>
          <button className="btn btn-ghost" onClick={exitFS} title="Close" aria-label="Close">✕</button>
        </div>
      </div>
      <iframe
        id="game-iframe"
        className="game-view-iframe"
        src={game.url}
        title={game.title}
        allow="autoplay; fullscreen; gamepad; accelerometer; gyroscope"
        allowFullScreen
        frameBorder="0"
        onLoad={handleIframeLoad}
      />
    </div>
  );
}

// Renders the filtered games with a banner ad injected every N cards.
function GameGrid({ games, visible, onPlay, adEvery = 12, consent }) {
  const items = [];
  const shown = games.slice(0, visible);
  shown.forEach((g, i) => {
    items.push(<GameCard key={g.slug} game={g} onPlay={onPlay} />);
    if ((i + 1) % adEvery === 0 && i !== shown.length - 1) {
      items.push(
        <div key={`ad-${i}`} className="grid-ad-span">
          <AdBanner slot="bannerMid" format="horizontal" consent={consent} />
        </div>
      );
    }
  });
  return <div className="grid">{items}</div>;
}

function App() {
  const [search, setSearch] = useState("");
  const [category, setCategory] = useState("All");
  const [playGame, setPlayGame] = useState(null);
  const [visible, setVisible] = useState(60);
  const [adLoading, setAdLoading] = useState(false);
  const [consent, setConsent] = useState(() => readConsent());

  // Coin wallet
  const [coins, setCoins] = useState(() => readCoins());
  const [outOfCoinsFor, setOutOfCoinsFor] = useState(null);   // pending game waiting on ad
  const [earnModalOpen, setEarnModalOpen] = useState(false);  // "Earn Coins" popup from wallet click
  const [coinFlash, setCoinFlash] = useState(false);          // briefly animate wallet on change

  // Persist every coin change to localStorage.
  useEffect(() => { writeCoins(coins); }, [coins]);

  // Debounced search tracking — fire one GA event after the user pauses typing,
  // so we don't emit per-keystroke noise.
  useEffect(() => {
    if (!search) return;
    const t = setTimeout(() => {
      trackEvent("search", { search_term: search });
    }, 700);
    return () => clearTimeout(t);
  }, [search]);

  // Flash the wallet whenever the balance changes (visual feedback).
  const prevCoinsRef = useRef(coins);
  useEffect(() => {
    if (prevCoinsRef.current !== coins) {
      setCoinFlash(true);
      const t = setTimeout(() => setCoinFlash(false), 450);
      prevCoinsRef.current = coins;
      return () => clearTimeout(t);
    }
  }, [coins]);

  // Persist consent choice + propagate to AdSense right away.
  const handleConsent = useCallback((state) => {
    writeConsent(state);
    const next = { state, ts: Date.now() };
    applyConsentToAdSense(next);
    setConsent(next);
  }, []);

  // Opens the player.
  // Open the game ON OUR SITE, inside the PlayerModal iframe. We show a
  // transition interstitial between the menu and the game — this is the
  // standard gaming-site flow (CrazyGames / Poki / GameDistribution), and
  // Google Publisher supports it as a content-transition interstitial as
  // long as the ad is user-dismissable. Flow:
  //   click Play → show GPT interstitial modal → user closes → game loads
  //   in iframe on gameadzoneplay.com
  // opts.skipInterstitial = rewarded-ad flow already showed one ad; we
  // skip the second ad to avoid double-serving.
  const launchGame = useCallback((game, opts = {}) => {
    trackEvent("game_play", {
      game_id: game.slug || game.id,
      game_title: game.title,
      game_category: game.category,
      via_rewarded: !!opts.skipInterstitial,
    });

    if (!opts.skipInterstitial && typeof window.showInterstitial === "function") {
      trackEvent("ad_impression_request", { ad_format: "interstitial" });
      setAdLoading(true);
      window.showInterstitial(
        () => { setAdLoading(false); setPlayGame(game); },  // onDone → load game
        () => setAdLoading(false)                           // onShown → hide loading spinner
      );
    } else {
      setPlayGame(game);
    }
  }, []);

  // Play flow: coin gate. If balance >= 1, spend a coin, show interstitial,
  // then launch. Otherwise queue the game and show the OutOfCoinsModal.
  const startPlay = useCallback((game) => {
    if (coins >= COIN_COST_PER_PLAY) {
      setCoins((c) => c - COIN_COST_PER_PLAY);
      launchGame(game);
    } else {
      setOutOfCoinsFor(game);
    }
  }, [coins, launchGame]);

  // Mint a coin and immediately spend it on the queued game.
  // Since games now render on our site (iframe in PlayerModal), there is
  // no pop-up-blocker concern — we can auto-launch directly.
  // skipInterstitial: true → user already watched a rewarded ad, so we
  // don't stack another interstitial on top.
  const grantCoinAndMaybePlay = useCallback((game) => {
    trackEvent("coin_earned", { amount: COIN_REWARD_AMOUNT, source: "rewarded_ad" });
    setCoins((c) => c + COIN_REWARD_AMOUNT);
    if (game) {
      setTimeout(() => {
        setCoins((c) => Math.max(0, c - COIN_COST_PER_PLAY));
        setOutOfCoinsFor(null);
        launchGame(game, { skipInterstitial: true });
      }, 250);
    } else {
      setOutOfCoinsFor(null);
    }
  }, [launchGame]);

  // User clicked "Watch Ad" in the OutOfCoinsModal.
  // Behavior: try rewarded ad first. If it succeeds → grant coin + play.
  // If it fails (no fill / dismissed), fall back to an interstitial ad;
  // once that finishes, still grant the coin + play so the user is never
  // soft-locked.
  const handleWatchAdForCoin = useCallback(() => {
    const game = outOfCoinsFor;
    if (!game) return;

    if (!consent) {
      grantCoinAndMaybePlay(game);
      return;
    }

    setAdLoading(true);
    let earned = false;
    window.showRewarded(
      () => { earned = true; },
      () => {
        if (earned) {
          grantCoinAndMaybePlay(game);
        } else {
          // Rewarded failed — fall back to interstitial, still grant the coin.
          if (typeof window.showInterstitial === "function") {
            setAdLoading(true); // re-arm loading popup for the interstitial fetch
            window.showInterstitial(
              () => grantCoinAndMaybePlay(game),
              () => setAdLoading(false)
            );
          } else {
            grantCoinAndMaybePlay(game);
          }
        }
      },
      () => setAdLoading(false)                   // onShown: hide loading popup
    );
  }, [outOfCoinsFor, consent, grantCoinAndMaybePlay]);

  // Coin wallet click → open the "Earn Coins" popup. The popup's
  // "Watch Ad" button then runs the rewarded flow (handleEarnCoinWatch).
  const handleEarnCoin = useCallback(() => {
    if (adLoading) return;
    setEarnModalOpen(true);
  }, [adLoading]);

  // "Watch Ad" click inside the Earn Coins popup → runs a real rewarded
  // ad. Same rewarded → interstitial-fallback chain; always grants a coin.
  const handleEarnCoinWatch = useCallback(() => {
    if (!consent) {
      setCoins((c) => c + COIN_REWARD_AMOUNT);
      setEarnModalOpen(false);
      return;
    }
    setAdLoading(true);
    let earned = false;
    window.showRewarded(
      () => { earned = true; },
      () => {
        const finalize = () => {
          setAdLoading(false);
          setCoins((c) => c + COIN_REWARD_AMOUNT);
          setEarnModalOpen(false);
        };
        if (earned) {
          finalize();
        } else if (typeof window.showInterstitial === "function") {
          setAdLoading(true);
          window.showInterstitial(
            finalize,
            () => setAdLoading(false)
          );
        } else {
          finalize();
        }
      },
      () => setAdLoading(false)                   // onShown: hide loading popup
    );
  }, [consent]);

  const cancelOutOfCoins = useCallback(() => {
    if (!adLoading) setOutOfCoinsFor(null);
  }, [adLoading]);

  const cancelEarnModal = useCallback(() => {
    if (!adLoading) setEarnModalOpen(false);
  }, [adLoading]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    return ALL_GAMES.filter((g) => {
      if (category !== "All" && g.category !== category) return false;
      if (q && !g.title.toLowerCase().includes(q)) return false;
      return true;
    });
  }, [search, category]);

  const categories = useMemo(() => getCategoryList(ALL_GAMES), []);

  useEffect(() => {
    setVisible(60);
  }, [search, category]);

  // Infinite scroll
  useEffect(() => {
    const onScroll = () => {
      const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 600;
      if (nearBottom) setVisible((v) => (v < filtered.length ? v + 40 : v));
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [filtered.length]);

  // "/" keyboard shortcut → focus the search box (only when not already
  // typing into a form field and no modal is open).
  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== "/") return;
      if (playGame || outOfCoinsFor) return;
      const t = e.target;
      if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
      const input = document.querySelector(".search");
      if (input) { e.preventDefault(); input.focus(); input.select && input.select(); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [playGame, outOfCoinsFor]);

  return (
    <div className={"app" + (coinFlash ? " coin-flash" : "")}>
      <Header
        search={search}
        setSearch={setSearch}
        total={ALL_GAMES.length}
        coins={coins}
        onEarnCoin={handleEarnCoin}
        earnLoading={adLoading && !outOfCoinsFor}
      />
      <AdBanner slot="bannerTop" format="horizontal" consent={consent} />
      <CategoryBar
        categories={categories}
        active={category}
        setActive={(c) => { trackEvent("category_select", { category: c }); setCategory(c); }}
      />
      <main>
        {filtered.length === 0 ? (
          <div className="empty">No games match your search.</div>
        ) : (
          <GameGrid games={filtered} visible={visible} onPlay={startPlay} consent={consent} />
        )}
        {visible < filtered.length && <div className="loading">Loading more...</div>}
      </main>
      <AdBanner slot="bannerFooter" format="horizontal" consent={consent} />
      <footer className="footer">
        <p>
          <a href="privacy.html">Privacy Policy</a>
          <span className="sep"> · </span>
          Games hosted by <a href="https://www.madkidgames.com" target="_blank" rel="noreferrer">madkidgames.com</a>
        </p>
        <p className="tiny">
          © 2026 Gameadzone Play · Ads served by Google AdSense
        </p>
      </footer>
      {playGame && <PlayerModal game={playGame} onClose={() => setPlayGame(null)} />}
      {outOfCoinsFor && (
        <OutOfCoinsModal
          game={outOfCoinsFor}
          mode="outOfCoins"
          onWatch={handleWatchAdForCoin}
          onClose={cancelOutOfCoins}
          adLoading={adLoading}
        />
      )}
      {earnModalOpen && !outOfCoinsFor && (
        <OutOfCoinsModal
          game={null}
          mode="earn"
          onWatch={handleEarnCoinWatch}
          onClose={cancelEarnModal}
          adLoading={adLoading}
        />
      )}
      {adLoading && <AdLoadingModal />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
