/* AcquiCup — root app: auth/loading gate, routing, store, X2 modal, tweaks */
const { useState, useEffect, useRef, useMemo } = React;

/* ----------------------------- URL routing -----------------------------
 * Every page has a dedicated, shareable URL. The `route` (+ `curMatch`) view
 * state mirrors window.location, so each screen is bookmarkable, the browser
 * back/forward buttons work, and a match can be linked directly. */
const ROUTE_PATHS = {
  dashboard: "/dashboard",
  matches: "/matches",
  standings: "/standings",
  leaderboard: "/leaderboard",
  companies: "/companies",
  acquiboard: "/acquiboard",
  rules: "/rules",
  admin: "/admin",
  settings: "/settings",
};

// view state -> URL path. The match-detail ("predict") screen carries the
// match id so it can be deep-linked; without a match it falls back to /matches.
function pathFor(route, match) {
  if (route === "predict") return match ? "/matches/" + encodeURIComponent(match.id) : "/matches";
  // The user-profile page carries the player id so it's deep-linkable; `match` here
  // is the selected user object (it has an `.id`). Without one, fall back to the leaderboard.
  if (route === "user") return match ? "/user/" + encodeURIComponent(match.id) : "/leaderboard";
  // The company-profile page carries the company id so it's deep-linkable; `match` here is
  // the selected company object (it has an `.id`). Without one, fall back to the companies board.
  if (route === "company") return match ? "/company/" + encodeURIComponent(match.id) : "/companies";
  return ROUTE_PATHS[route] || "/dashboard";
}

// URL path -> { route, matchId }. Unknown paths fall back to the dashboard.
function parsePath(pathname) {
  const parts = (pathname || "/").split("/").filter(Boolean);
  if (parts.length === 0) return { route: "dashboard", matchId: null };
  const head = parts[0];
  if (head === "matches") {
    return parts[1]
      ? { route: "predict", matchId: decodeURIComponent(parts[1]) }
      : { route: "matches", matchId: null };
  }
  // /user/:id → the player profile page (matchId carries the player id). A bare
  // /user with no id is meaningless, so resolve it to the leaderboard.
  if (head === "user") {
    return parts[1]
      ? { route: "user", matchId: decodeURIComponent(parts[1]) }
      : { route: "leaderboard", matchId: null };
  }
  // /company/:id → the company profile page (matchId carries the company id). A bare
  // /company with no id is meaningless, so resolve it to the companies board.
  if (head === "company") {
    return parts[1]
      ? { route: "company", matchId: decodeURIComponent(parts[1]) }
      : { route: "companies", matchId: null };
  }
  if (ROUTE_PATHS[head] && head !== "matches") return { route: head, matchId: null };
  // "/login" (auth gate) and anything else resolve to the dashboard once authed.
  return { route: "dashboard", matchId: null };
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "boardroom",
  "accent": "#1E72D6",
  "radius": 16
}/*EDITMODE-END*/;

function Modal({ children, onClose, width = 420 }) {
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 200, background: "rgba(8,20,38,.5)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20, backdropFilter: "blur(2px)" }}>
      <div className="card" onClick={(e) => e.stopPropagation()} style={{ width, maxWidth: "100%", boxShadow: "var(--shadow-pop)" }}>{children}</div>
    </div>
  );
}

/* ------------------------- first-run onboarding -------------------------
 * A 4-card swipeable carousel shown ONCE to a brand-new player (set by the
 * signup wizard via the `acq:newSignup` session flag) and never again. The
 * "seen" state persists per Firebase uid in localStorage, so it survives
 * reloads and is honoured even if the user only ticks "don't show again". */
const ONBOARD_KEY = (uid) => "acq:onboarded:" + (uid || "anon");
function hasOnboarded(uid) { try { return localStorage.getItem(ONBOARD_KEY(uid)) === "1"; } catch (_) { return false; } }
function markOnboarded(uid) { try { localStorage.setItem(ONBOARD_KEY(uid), "1"); } catch (_) { /* private mode */ } }
// One-shot: a fresh signup drops `acq:newSignup` in sessionStorage; we read+clear it.
function consumeNewSignupFlag() {
  try {
    const v = sessionStorage.getItem("acq:newSignup");
    if (v) sessionStorage.removeItem("acq:newSignup");
    return v === "1";
  } catch (_) { return false; }
}

function OnboardEmoji({ glyph }) {
  return <span style={{ fontSize: 16, lineHeight: 1, width: 22, textAlign: "center", flex: "none" }}>{glyph}</span>;
}
function OnboardRow({ glyph, children }) {
  return (
    <div className="row gap-10" style={{ alignItems: "flex-start", fontSize: 13.5, lineHeight: 1.4 }}>
      <OnboardEmoji glyph={glyph} />
      <span style={{ color: "var(--ink-2)" }}>{children}</span>
    </div>
  );
}

// The four steps. `fill` matches each glyph's filled/outline icon variant.
const ONBOARD_STEPS = [
  { key: "predict", icon: "target", fill: false, title: "Predict the score" },
  { key: "points", icon: "trophy", fill: true, title: "Points & your X2" },
  { key: "strategy", icon: "bolt", fill: true, title: "Auto-fill strategies" },
  { key: "boards", icon: "building", fill: false, title: "Two leaderboards" },
];

// The four real sign-up strategies, rendered read-only (first one shown selected) —
// reuses the live STRATEGY_OPTIONS data + the same chip styling as StrategyPicker.
function OnboardStrategy() {
  const opts = (window.STRATEGY_OPTIONS || []);
  return (
    <div className="col gap-8">
      {opts.map((o, idx) => {
        const on = idx === 0;
        return (
          <div key={o.id} className="row gap-12" style={{ alignItems: "flex-start", padding: "11px 12px", borderRadius: 12,
            background: on ? "var(--accent-soft)" : "var(--surface)", border: "1px solid " + (on ? "var(--accent)" : "var(--line)") }}>
            <span style={{ width: 32, height: 32, borderRadius: 9, flex: "none", display: "flex", alignItems: "center",
              justifyContent: "center", color: "var(--accent)", background: on ? "var(--surface)" : "var(--accent-soft)" }}>
              <Icon name={o.icon} size={17} stroke={2} />
            </span>
            <div className="col" style={{ gap: 2, minWidth: 0 }}>
              <span style={{ fontSize: 13.5, fontWeight: 700, color: on ? "var(--accent)" : "var(--ink)" }}>{o.title}</span>
              <span className="muted" style={{ fontSize: 12, lineHeight: 1.4 }}>{o.desc}</span>
            </div>
            {on && <Icon name="check" size={15} stroke={2.5} style={{ color: "var(--accent)", flex: "none", marginTop: 6 }} />}
          </div>
        );
      })}
    </div>
  );
}

// A real top-5 individual board, built from the live leaderboard data with the SAME
// `.lb` table markup the standings pages use.
function OnboardBoards() {
  const A = window.ACQ || {};
  const users = [...(A.USERS || [])].sort((a, b) => a.rank - b.rank).slice(0, 5);
  return (
    <div className="card" style={{ overflow: "hidden" }}>
      <div className="row between" style={{ padding: "11px 14px 9px" }}><span className="eyebrow">Top players</span></div>
      <table className="lb">
        <thead><tr><th style={{ width: 54 }}>Rank</th><th>Player</th><th style={{ textAlign: "right" }}>Pts</th></tr></thead>
        <tbody>
          {users.map((r) => (
            <tr key={r.id} className={"hov " + (r.you ? "me-row" : "")}>
              <td><span className="rank-num">{r.rank}</span></td>
              <td><div className="row gap-10" style={{ minWidth: 0, alignItems: "center" }}>
                <CompanyLogo id={r.company} size={22} />
                <span className="nowrap" style={{ fontWeight: 700, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis" }}>{userName(r)}{r.you ? " · you" : ""}</span>
                {r.isAi && <AiBadge style={{ marginLeft: 6 }} />}
              </div></td>
              <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 14 }}>{r.points}</span></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// The right-hand "live example" pane — a real component per step.
function OnboardPreview({ step, sample, finished }) {
  if (step === 0) {
    return sample ? <MatchCard match={sample} onOpen={() => {}} /> : null;
  }
  if (step === 1) {
    const pm = finished || sample;
    if (!pm) return null;
    // A settled match shows the earned-points badge (a real "X2 doubled an exact score"
    // example); pre-tournament, fall back to an upcoming card flagged X2.
    const pred = finished
      ? { home: finished.homeScore, away: finished.awayScore,
          earned: { total: potential(finished, { home: finished.homeScore, away: finished.awayScore }) * 2, isExact: true, outcome: 1 } }
      : { home: 2, away: 1 };
    return <MatchCard match={pm} pred={pred} x2On onOpen={() => {}} />;
  }
  if (step === 2) return <OnboardStrategy />;
  return <OnboardBoards />;
}

function OnboardingModal({ uid, onClose, onPredict }) {
  const [i, setI] = useState(0);
  const [dontShow, setDontShow] = useState(true);
  const touchX = useRef(null);
  const narrow = useNarrow(720); // desktop → wide two-pane; mobile → single column
  // A real UPCOMING fixture (with flags) makes the preview look like the live app —
  // prefer an upcoming match so the predict step never shows a finished/live card.
  const sample = useMemo(() => {
    const ms = (window.ACQ && window.ACQ.MATCHES) || [];
    const real = (m) => !isTbd(m) && T(m.home).iso && T(m.away).iso;
    return ms.find((m) => m.status === "upcoming" && real(m)) || ms.find(real) || ms[0] || null;
  }, []);
  // A finished match (if any) powers the points step's "earned" example.
  const finished = useMemo(() => {
    const ms = (window.ACQ && window.ACQ.MATCHES) || [];
    return ms.find((m) => m.status === "finished" && m.homeScore != null && m.awayScore != null) || null;
  }, []);

  const last = i === ONBOARD_STEPS.length - 1;
  const step = ONBOARD_STEPS[i];
  const move = (d) => setI((x) => Math.max(0, Math.min(ONBOARD_STEPS.length - 1, x + d)));

  function close(action) {
    if (dontShow) markOnboarded(uid);
    if (action === "predict") onPredict(); else onClose();
  }

  function onTouchStart(e) { touchX.current = e.touches[0].clientX; }
  function onTouchEnd(e) {
    if (touchX.current == null) return;
    const dx = e.changedTouches[0].clientX - touchX.current;
    touchX.current = null;
    if (dx < -45 && !last) move(1);
    else if (dx > 45 && i > 0) move(-1);
  }

  // Left-pane copy (the explanatory text); the live example lives in the right pane.
  let copy;
  if (i === 0) {
    copy = <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.55 }}>Call the exact final score before kickoff. Every pick locks the moment the match starts.</span>;
  } else if (i === 1) {
    copy = (
      <div className="col gap-10">
        <OnboardRow glyph="✅">Right winner scores <b>points</b> on the board.</OnboardRow>
        <OnboardRow glyph="🎯">Nail the exact score <b>= X2 Bonus</b>.</OnboardRow>
        <OnboardRow glyph="💡">Underdog wins pay <b>more</b> than favourites.</OnboardRow>
        <OnboardRow glyph="⚡">Use <b>two</b> X2 bonus during the contest.</OnboardRow>
      </div>
    );
  } else if (i === 2) {
    copy = <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.55 }}>In a hurry? Auto-fill every match in one tap with a strategy that matches your style, then tweak any pick before it locks.</span>;
  } else {
    copy = <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.55 }}>Climb the player leaderboard, and lift your whole company up the standings with you.</span>;
  }

  return (
    <Modal onClose={() => close("skip")} width={narrow ? 440 : 920}>
      <div className="col">
        {/* header */}
        <div className="row between" style={{ padding: "17px 22px 15px", borderBottom: "1px solid var(--line-2)" }}>
          <span className="row gap-8"><span className="eyebrow">Getting started</span><span className="muted" style={{ fontSize: 12 }}>· {i + 1} of {ONBOARD_STEPS.length}</span></span>
          <button className="btn btn-ghost btn-sm" style={{ padding: "5px 11px" }} onClick={() => close("skip")}>Skip</button>
        </div>

        {/* body: copy | live example (swipeable on mobile). Sized to fit — never scrolls. */}
        <div onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}
          style={{ display: "grid", gridTemplateColumns: narrow ? "1fr" : "minmax(0,330px) 1fr", minHeight: narrow ? undefined : 372 }}>
          {/* left — heading + copy, left-aligned, vertically centred beside the example */}
          <div className="col" style={{ gap: 15, justifyContent: narrow ? "flex-start" : "center", padding: narrow ? "22px 22px 18px" : "30px 30px", borderRight: narrow ? "none" : "1px solid var(--line-2)" }}>
            <div className="row gap-12">
              <div style={{ width: 42, height: 42, borderRadius: 12, background: "var(--accent-soft)", color: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}><Icon name={step.icon} size={21} fill={step.fill} /></div>
              <h3 style={{ fontSize: 19 }}>{step.title}</h3>
            </div>
            {copy}
          </div>
          {/* right — the real component, on a tinted "screen" surface */}
          <div className="col gap-12" style={{ justifyContent: "center", background: "var(--surface-2)", padding: narrow ? "20px 22px 22px" : "28px 30px", borderTop: narrow ? "1px solid var(--line-2)" : "none" }}>
            <span className="eyebrow" style={{ color: "var(--ink-soft)" }}>Live example</span>
            <OnboardPreview step={i} sample={sample} finished={finished} />
          </div>
        </div>

        {/* footer: back · dots · next / done */}
        <div className="row between" style={{ padding: "15px 22px", borderTop: "1px solid var(--line-2)" }}>
          <button className="btn btn-ghost btn-sm" style={{ visibility: i > 0 ? "visible" : "hidden" }} onClick={() => move(-1)}><Icon name="chevron" size={13} style={{ transform: "rotate(180deg)" }} />Back</button>
          <div className="row" style={{ gap: 7 }}>
            {ONBOARD_STEPS.map((_, d) => (
              <button key={d} aria-label={"Step " + (d + 1)} onClick={() => setI(d)} style={{ width: d === i ? 20 : 7, height: 7, borderRadius: 99, border: "none", padding: 0, cursor: "pointer",
                background: d === i ? "var(--accent)" : "var(--line)", transition: "width .18s ease, background .18s ease" }} />
            ))}
          </div>
          {last
            ? <button className="btn btn-primary btn-sm" onClick={() => close("done")}>Done<Icon name="check" size={13} stroke={2.4} /></button>
            : <button className="btn btn-primary btn-sm" onClick={() => move(1)}>Next<Icon name="chevron" size={13} stroke={2.4} /></button>}
        </div>

        {/* don't-show-again */}
        <label className="row center gap-8" style={{ padding: "8px 20px 16px", justifyContent: "center", cursor: "pointer" }}>
          <input type="checkbox" checked={dontShow} onChange={(e) => setDontShow(e.target.checked)} style={{ accentColor: "var(--accent)", cursor: "pointer" }} />
          <span className="muted" style={{ fontSize: 12 }}>Don't show this again</span>
        </label>
      </div>
    </Modal>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const tz = useTimezone(); // active display zone — re-renders match dates/times on change

  // Derive the initial view from the URL the user actually loaded, so deep
  // links (e.g. /matches/m01, /leaderboard) land on the right page.
  const initial = parsePath(window.location.pathname);
  // A deep-linked match id can't be resolved to a match object until bootstrap
  // has populated window.ACQ — remember where the user was headed until then.
  const redirectAfterAuthRef = useRef(initial);

  // auth / loading gate
  const [booted, setBooted] = useState(false);
  const [authed, setAuthed] = useState(false);
  // True when a restored/redirected Firebase session has NO app profile yet (a Google
  // user who hasn't finished onboarding) — the Auth screen opens straight into the
  // company/strategy steps instead of clearing the session.
  const [pendingGoogleOnboard, setPendingGoogleOnboard] = useState(false);
  // Pre-login gate: show the marketing landing page first; a CTA there opens the
  // Auth screen in the chosen mode ("signup" from Join, "signin" from Sign in).
  // Any deep link skips straight to Auth — including /login itself, so a shared
  // /login URL (or a reload while on the auth screen) lands on the sign-in form
  // rather than back on the landing page.
  const skipLanding = !["/", ""].includes(window.location.pathname);
  const [showAuth, setShowAuth] = useState(skipLanding);
  const [authMode, setAuthMode] = useState("signin");

  const [route, setRoute] = useState(initial.route);
  const [curMatch, setCurMatch] = useState(null);
  const [predictions, setPredictions] = useState({});
  const [x2MatchIds, setX2MatchIds] = useState([]); // up to two doubled matches
  const [x2Confirm, setX2Confirm] = useState(null); // matchId pending confirmation
  const [toast, setToast] = useState(null);
  const [dataVersion, setDataVersion] = useState(0);
  const [showOnboarding, setShowOnboarding] = useState(false); // first-run carousel

  // On mount: wait for Firebase to restore any persisted session, then bootstrap.
  useEffect(() => {
    let cancelled = false;
    async function boot() {
      // Complete any pending Google redirect sign-in (popup fallback) before reading
      // the restored session, so a redirect-authenticated user is recognized here.
      await API.completeRedirectSignIn().catch(() => {});
      const user = await API.authReady(); // resolves with the signed-in user or null
      if (cancelled) return;
      if (user) {
        try {
          await API.bootstrap();
          if (cancelled) return;
          setPredictions({ ...window.ACQ.MY_PREDICTIONS });
          setX2MatchIds(window.ACQ.me.x2MatchIds ?? []);
          initTimezone(window.ACQ.me && window.ACQ.me.timezone);
          setAuthed(true);
          setBooted(true);
          finishNavigationAfterAuth();
        } catch (e) {
          if (cancelled) return;
          // Signed into Firebase but no app profile. Only a GOOGLE session can finish
          // onboarding via the wizard (it has no name/password step) — route Google
          // users there and keep the session. Any other provider here is a rare orphaned
          // account (e.g. a password signup whose profile write was rolled back); there's
          // no self-heal path, so sign out and show the gate.
          const noProfile = e && (e.code === "NO_PROFILE" || (e.status === 401 && e.error === "User not found"));
          const isGoogleUser = !!(user && user.providerData && user.providerData.some((p) => p && p.providerId === "google.com"));
          if (noProfile && isGoogleUser) {
            setPendingGoogleOnboard(true);
          } else {
            await API.clearToken().catch(() => {}); // tear the session down before showing the gate
          }
          setBooted(true);
        }
      } else {
        setBooted(true);
      }
    }
    boot();
    return () => { cancelled = true; };
  }, []);

  // Browser back/forward: re-derive the view from the URL we just moved to.
  useEffect(() => {
    function onPop() {
      const parsed = parsePath(window.location.pathname);
      if (parsed.route === "predict") {
        const m = (window.ACQ && window.ACQ.MATCHES)
          ? window.ACQ.MATCHES.find((x) => String(x.id) === String(parsed.matchId))
          : null;
        if (m) { setRoute("predict"); setCurMatch(m); }
        else { setRoute("matches"); setCurMatch(null); }
      } else if (parsed.route === "user") {
        // curMatch is reused as the carrier for the selected user; UserPage only
        // needs its id, so a minimal {id} is enough when the full row isn't loaded yet.
        const u = (window.ACQ && window.ACQ.USERS)
          ? window.ACQ.USERS.find((x) => String(x.id) === String(parsed.matchId))
          : null;
        setRoute("user");
        setCurMatch(u || (parsed.matchId ? { id: parsed.matchId } : null));
      } else if (parsed.route === "company") {
        // curMatch is reused as the carrier for the selected company; CompanyPage only
        // needs its id, so a minimal {id} is enough when the full row isn't loaded yet.
        const c = (window.ACQ && window.ACQ.COMPANY_RANK)
          ? window.ACQ.COMPANY_RANK.find((x) => String(x.id) === String(parsed.matchId))
          : null;
        setRoute("company");
        setCurMatch(c || (parsed.matchId ? { id: parsed.matchId } : null));
      } else {
        setRoute(parsed.route);
        setCurMatch(null);
      }
    }
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // While the auth gate is showing, reflect it in the address bar as /login
  // (the original destination is preserved in redirectAfterAuthRef). The marketing
  // landing keeps the bare "/" — only the Auth screen rewrites to /login.
  useEffect(() => {
    const onAuth = showAuth || pendingGoogleOnboard;
    if (booted && !authed && onAuth && window.location.pathname !== "/login") {
      window.history.replaceState({ login: true }, "", "/login");
    }
  }, [booted, authed, showAuth, pendingGoogleOnboard]);

  // scroll top on nav
  useEffect(() => { window.scrollTo({ top: 0, behavior: "instant" in window ? "instant" : "auto" }); }, [route, curMatch]);

  async function handleAuthed() {
    // Caller (Auth) awaits this; rethrow on failure so it can surface the error
    // inline rather than leaving the user on a token-but-no-data limbo.
    try {
      await API.bootstrap();
      setPredictions({ ...window.ACQ.MY_PREDICTIONS });
      setX2MatchIds(window.ACQ.me.x2MatchIds ?? []);
      initTimezone(window.ACQ.me && window.ACQ.me.timezone);
      setAuthed(true);
      finishNavigationAfterAuth();
      // Brand-new signups (the wizard set the one-shot session flag) get the first-run
      // tour once; honour a prior "don't show again" / earlier dismissal per uid.
      const uid = window.ACQ.me && window.ACQ.me.id;
      if (consumeNewSignupFlag() && !hasOnboarded(uid)) setShowOnboarding(true);
    } catch (e) {
      // NO_PROFILE → authenticated but not onboarded; let the Auth screen branch into
      // onboarding (it catches this). Keep matching the legacy string for back-compat.
      if (e && (e.code === "NO_PROFILE" || (e.status === 401 && e.error === "User not found"))) {
        throw e;
      }
      await API.clearToken().catch(() => {}); // sign out before surfacing the error
      setAuthed(false);
      throw e;
    }
  }

  async function refresh() {
    await API.bootstrap();
    setPredictions({ ...window.ACQ.MY_PREDICTIONS });
    setX2MatchIds(window.ACQ.me.x2MatchIds ?? []);
    initTimezone(window.ACQ.me && window.ACQ.me.timezone);
    setDataVersion((v) => v + 1);
  }

  // Central navigation: update the URL (so every page is addressable) and the
  // matching view state together. `replace` is used to canonicalize on load
  // without adding a history entry.
  function navigate(r, match, opts) {
    const replace = opts && opts.replace;
    const path = pathFor(r, match);
    const histState = { route: r, matchId: match ? match.id : null };
    if (replace) window.history.replaceState(histState, "", path);
    else window.history.pushState(histState, "", path);
    setRoute(r);
    setCurMatch(match || null);
  }
  function go(r) { navigate(r, null); }
  function openMatch(m) { navigate("predict", m); }
  // Open a player's public profile page (reached by clicking them on a leaderboard).
  function openUser(u) { if (u && u.id != null) navigate("user", u); }
  // Open a company's public profile page (reached by clicking a company on a leaderboard).
  function openCompany(c) { if (c && c.id != null) navigate("company", c); }
  const hrefFor = (r) => pathFor(r, null);

  // After bootstrap, send the user to wherever the URL pointed (resolving a
  // deep-linked match now that window.ACQ exists, and guarding /admin), and
  // canonicalize the address bar (/, /login -> the real page) without a new
  // history entry.
  function finishNavigationAfterAuth() {
    const target = redirectAfterAuthRef.current || { route: "dashboard", matchId: null };
    redirectAfterAuthRef.current = null;
    let r = target.route;
    let m = null;
    if (r === "predict") {
      m = (window.ACQ.MATCHES || []).find((x) => String(x.id) === String(target.matchId)) || null;
      if (!m) r = "matches";
    }
    if (r === "user") {
      // Resolve a deep-linked /user/:id now that window.ACQ exists; UserPage only
      // needs the id, so keep a minimal {id} when the standings row isn't found.
      m = (window.ACQ.USERS || []).find((x) => String(x.id) === String(target.matchId))
        || (target.matchId ? { id: target.matchId } : null);
      if (!m) r = "leaderboard";
    }
    if (r === "company") {
      // Resolve a deep-linked /company/:id now that window.ACQ exists; CompanyPage only
      // needs the id, so keep a minimal {id} when the standings row isn't found.
      m = (window.ACQ.COMPANY_RANK || []).find((x) => String(x.id) === String(target.matchId))
        || (target.matchId ? { id: target.matchId } : null);
      if (!m) r = "companies";
    }
    if (r === "admin" && !(window.ACQ.me && window.ACQ.me.isAdmin)) r = "dashboard";
    if (r === "acquiboard" && !(window.ACQ.me && window.ACQ.me.isAcquisit)) r = "dashboard";
    navigate(r, m, { replace: true });
  }

  async function savePred(id, data) {
    // optimistic update for snappiness
    setPredictions((p) => ({ ...p, [id]: { ...(p[id] || {}), ...data } }));
    try {
      await API.savePrediction(id, data);
      await refresh();
      API.track("save_pick", { match_id: String(id), home: data.home, away: data.away });
      flash("Prediction saved");
    } catch (e) {
      flash((e && e.error) ? e.error : "Couldn't save prediction");
      // Roll back the optimistic write: re-sync from the server so a rejected
      // save (e.g. the match locked at kickoff → 403) can't leave the card
      // showing a false "Saved" state. Best-effort — keep the toast if offline.
      try { await refresh(); } catch (_) { /* nothing better to do */ }
    }
  }

  // Bulk: fill every still-editable, not-yet-predicted match from one strategy.
  // The server skips locked/placeholder matches and never overwrites a saved
  // pick, then we re-sync and report how many blanks were actually filled.
  async function applyStrategy(strategy) {
    try {
      const r = await API.applyStrategy(strategy);
      await refresh();
      const n = (r && r.applied) || 0;
      // Generic confirmation + a per-strategy variant so each can be funneled
      // separately in GA (aggressive/safe/random).
      API.track("autopredict_confirmation", { strategy: strategy, count: n });
      API.track("autopredict_" + strategy + "_confirmation", { count: n });
      flash(n ? `Filled ${n} match${n === 1 ? "" : "es"} with your strategy` : "No empty matches to fill");
    } catch (e) {
      flash((e && e.error) ? e.error : "Couldn't apply the strategy");
    }
  }

  // Bulk: clear every prediction on a still-editable match (locked/finished
  // picks are already scored and stay put).
  async function clearPredictions() {
    try {
      const r = await API.clearPredictions();
      await refresh();
      const n = (r && r.cleared) || 0;
      API.track("clear_all_predictions", { count: n });
      flash(n ? `Cleared ${n} prediction${n === 1 ? "" : "s"}` : "No predictions to clear");
    } catch (e) {
      flash((e && e.error) ? e.error : "Couldn't clear predictions");
    }
  }

  // Adding an X2 goes through a confirmation modal; removing one is immediate.
  function setX2(matchId) {
    if (matchId == null) return;
    setX2Confirm(matchId);
  }

  async function removeX2(matchId) {
    if (matchId == null) return;
    try {
      await API.removeX2(matchId);
      await refresh();
      flash("X2 removed — you can use it on another match");
    } catch (e) {
      flash((e && e.error) ? e.error : "Couldn't remove X2");
    }
  }

  async function confirmX2() {
    try {
      await API.setX2(x2Confirm);
      setX2Confirm(null);
      await refresh();
      flash("X2 locked in — points on this match will double");
    } catch (e) {
      setX2Confirm(null);
      flash((e && e.error) ? e.error : "Couldn't lock in X2");
    }
  }

  async function onLogout() {
    // Await sign-out so the Firebase session is actually torn down before we flip
    // the UI to the gate — otherwise freshToken() could still authenticate an
    // in-flight request as the "logged-out" user.
    API.track("signout"); // log while still authenticated, before the session is torn down
    await API.clearToken().catch(() => {});
    redirectAfterAuthRef.current = { route: "dashboard", matchId: null };
    setCurMatch(null);
    setAuthed(false);
    setRoute("dashboard");
    // Land on the marketing homepage (not the /login gate): drop the Auth screen
    // and put the bare "/" in the address bar so showLanding renders.
    setPendingGoogleOnboard(false);
    setShowAuth(false);
    window.history.replaceState({}, "", "/");
  }

  function flash(msg) { setToast(msg); clearTimeout(window.__t); window.__t = setTimeout(() => setToast(null), 2400); }

  // apply tweak overrides on a wrapper
  const rootStyle = {
    "--accent": t.accent,
    "--accent-soft": `color-mix(in srgb, ${t.accent} 12%, var(--surface))`,
    "--r-lg": t.radius + "px",
    "--r-md": Math.max(6, t.radius - 4) + "px",
    "--r-xl": (t.radius + 6) + "px",
    minHeight: "100vh",
  };

  // While restoring a session, show a minimal centered loading screen.
  if (!booted) {
    return (
      <div data-theme={t.theme} style={rootStyle}>
        <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-2)", fontSize: 15, fontWeight: 600 }}>Loading…</div>
        <TweakUI t={t} setTweak={setTweak} />
      </div>
    );
  }

  // Not authed → show the landing page, then Auth. window.ACQ is NOT yet populated,
  // so the main app (whose components read window.ACQ during render) must stay
  // unmounted. A pending Google onboard always goes straight to the Auth wizard.
  if (!authed) {
    const showLanding = !showAuth && !pendingGoogleOnboard;
    return (
      <div data-theme={t.theme} style={rootStyle}>
        {showLanding ? (
          <Landing
            onJoin={() => { setAuthMode("signup"); setShowAuth(true); }}
            onSignIn={() => { setAuthMode("signin"); setShowAuth(true); }}
          />
        ) : (
          <Auth
            onAuthed={handleAuthed}
            startGoogleOnboard={pendingGoogleOnboard}
            initialMode={authMode}
            onBack={skipLanding ? null : () => setShowAuth(false)}
          />
        )}
        <TweakUI t={t} setTweak={setTweak} />
      </div>
    );
  }

  // Authed: window.ACQ is populated. Safe to read it during render.
  const me = window.ACQ.me;
  const isAdmin = !!(window.ACQ.me && window.ACQ.me.isAdmin);
  const isAcquisit = !!(window.ACQ.me && window.ACQ.me.isAcquisit);

  // Maintenance takeover: when an admin has paused the app for an update, ordinary players
  // get a full-screen notice instead of the app. Admins are exempt (they keep full access,
  // plus a persistent reminder banner below) so they can finish the update while paused.
  const cfg = window.ACQ.CONFIG || {};
  if (cfg.paused && !isAdmin) {
    return (
      <div data-theme={t.theme} style={rootStyle}>
        <MaintenanceScreen message={cfg.pauseMessage} onLogout={onLogout} />
        <TweakUI t={t} setTweak={setTweak} />
      </div>
    );
  }

  const store = { me, predictions, x2MatchIds, savePred, setX2, removeX2, applyStrategy, clearPredictions, openUser, openCompany, dataVersion };

  let screen;
  if (route === "predict" && curMatch) screen = <PredictionDetail match={curMatch} store={store} back={() => go("matches")} save={savePred} openX2={setX2} removeX2={removeX2} />;
  else if (route === "user") screen = <UserPage store={store} go={go} userId={curMatch ? curMatch.id : null} />;
  else if (route === "company") screen = <CompanyPage store={store} go={go} companyId={curMatch ? curMatch.id : null} />;
  else if (route === "dashboard") screen = <Dashboard store={store} go={go} openMatch={openMatch} />;
  else if (route === "matches") screen = <Calendar store={store} openMatch={openMatch} />;
  else if (route === "standings") screen = <GroupStandings store={store} go={go} openMatch={openMatch} />;
  else if (route === "leaderboard") screen = <Standings store={store} go={go} />;
  else if (route === "companies") screen = <Standings store={store} go={go} />;
  else if (route === "acquiboard" && isAcquisit) screen = <AcquiBoard store={store} go={go} />;
  else if (route === "rules") screen = <Rules go={go} />;
  else if (route === "settings") screen = <Settings store={store} refresh={refresh} flash={flash} onLogout={onLogout} />;
  else if (route === "admin" && isAdmin) screen = <Admin store={store} refresh={refresh} />;
  else screen = <Dashboard store={store} go={go} openMatch={openMatch} />;

  // The profile page is reached from the leaderboard, so keep that nav item active.
  const navRoute = route === "predict" ? "matches" : route === "user" ? "leaderboard" : route === "company" ? "standings" : route;

  return (
    <div data-theme={t.theme} style={rootStyle}>
      <AppShell route={navRoute} go={go} hrefFor={hrefFor} me={me} isAdmin={isAdmin} isAcquisit={isAcquisit} onLogout={onLogout}>
        {screen}
      </AppShell>

      {/* X2 confirmation */}
      {x2Confirm && (() => {
        const m = window.ACQ.MATCHES.find((x) => x.id === x2Confirm);
        const remaining = Math.max(0, 2 - x2MatchIds.length);
        return (
          <Modal onClose={() => setX2Confirm(null)} width={400}>
            <div className="col" style={{ padding: "24px 24px", gap: 16 }}>
              <div className="row gap-12" style={{ alignItems: "center" }}>
                <div style={{ width: 42, height: 42, borderRadius: 12, background: "var(--brand-yellow)", color: "#5a4400", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}><Icon name="bolt" size={21} fill /></div>
                <h3 style={{ fontSize: 19 }}>Use an X2 bonus here?</h3>
              </div>
              <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.55 }}>You get two X2 bonuses for the whole tournament ({remaining} still available). It doubles every point your {T(m.home).code}–{T(m.away).code} prediction earns — and it can't be moved once the match locks.</span>
              <div className="card row between" style={{ background: "var(--surface-2)", padding: "11px 14px" }}><span style={{ fontWeight: 600, fontSize: 13.5 }}>{T(m.home).name} vs {T(m.away).name}</span><span className="muted mono" style={{ fontSize: 12 }}>{fmtMatchDate(m, tz)}</span></div>
              <div className="row gap-10" style={{ marginTop: 2 }}><button className="btn btn-ghost grow" style={{ justifyContent: "center" }} onClick={() => setX2Confirm(null)}>Not yet</button><button className="btn btn-primary grow" style={{ justifyContent: "center" }} onClick={confirmX2}>Confirm X2<Icon name="bolt" size={14} fill /></button></div>
            </div>
          </Modal>
        );
      })()}

      {/* first-run onboarding (new signups only, once) */}
      {showOnboarding && (
        <OnboardingModal
          uid={me.id}
          onClose={() => setShowOnboarding(false)}
          onPredict={() => { setShowOnboarding(false); go("matches"); }}
        />
      )}

      {/* Maintenance reminder — only an admin can be here while paused (non-admins are
          taken over above). A persistent strip so the admin never forgets the app is dark. */}
      {cfg.paused && isAdmin && (
        <div style={{ position: "fixed", bottom: 22, left: "50%", transform: "translateX(-50%)", zIndex: 290,
          background: "var(--brand-yellow)", color: "#5a4400", padding: "9px 16px", borderRadius: 999,
          fontSize: 12.5, fontWeight: 700, boxShadow: "var(--shadow-pop)", display: "flex", alignItems: "center", gap: 8,
          maxWidth: "92vw" }}>
          <Icon name="lock" size={14} />Maintenance mode is ON — players are paused. You have full access.
        </div>
      )}

      {/* toast */}
      {toast && (
        <div className="app-toast" style={{ position: "fixed", bottom: 22, left: "50%", transform: "translateX(-50%)", zIndex: 300,
          background: "var(--brand-navy)", color: "#fff", padding: "11px 18px", borderRadius: 999,
          fontSize: 13.5, fontWeight: 600, boxShadow: "var(--shadow-pop)", display: "flex", alignItems: "center", gap: 8, pointerEvents: "none" }}>
          <Icon name="check" size={15} stroke={2.5} style={{ color: "var(--brand-blue)" }} />{toast}
        </div>
      )}

      <TweakUI t={t} setTweak={setTweak} />
    </div>
  );
}

// Full-screen maintenance takeover shown to non-admin players while the admin has the
// app paused for an update. Built from the app's own primitives (CupMark/Icon) so it
// feels on-brand. `message` is the admin's notice (falls back to default copy); a small
// "Sign out" escape hatch is provided so a player isn't trapped.
function MaintenanceScreen({ message, onLogout }) {
  const text = (message && String(message).trim())
    || "AcquiCup is briefly offline for an update. We'll be back shortly — thanks for your patience!";
  return (
    <div className="acq-root" style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
      <div className="card card-pad col gap-16" style={{ maxWidth: 460, width: "100%", textAlign: "center", padding: "36px 28px", alignItems: "center" }}>
        <CupMark size={56} />
        <div style={{ width: 46, height: 46, borderRadius: 14, background: "var(--brand-yellow)", color: "#5a4400", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}>
          <Icon name="lock" size={22} />
        </div>
        <h2 style={{ fontSize: 22, margin: 0 }}>We'll be right back</h2>
        <p className="muted" style={{ fontSize: 14.5, lineHeight: 1.6, margin: 0, whiteSpace: "pre-wrap" }}>{text}</p>
        <button className="btn btn-ghost btn-sm" onClick={onLogout} style={{ marginTop: 4 }}>Sign out</button>
      </div>
    </div>
  );
}

function TweakUI({ t, setTweak }) {
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Visual style" />
      <TweakRadio label="Theme" value={t.theme} options={["boardroom", "stadium", "editorial"]} onChange={(v) => setTweak("theme", v)} />
      <TweakColor label="Accent" value={t.accent} options={["#1E72D6", "#0F2D52", "#1F8A6B", "#7A5AE0", "#E0892B"]} onChange={(v) => setTweak("accent", v)} />
      <TweakSection label="Shape" />
      <TweakSlider label="Corner radius" value={t.radius} min={4} max={26} unit="px" onChange={(v) => setTweak("radius", v)} />
    </TweaksPanel>
  );
}

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