// Declarations for global dependencies provided at runtime.
declare const React: any;
declare const ReactDOM: any;
declare const Board: any;
declare const Modal: any;

interface WinLine {
  start: { row: number; col: number };
  end: { row: number; col: number };
}

function readPrefs() {
  try {
    const m = document.cookie.match(/(?:^|; )prefs=([^;]+)/);
    if (m) {
      return JSON.parse(decodeURIComponent(m[1]));
    }
  } catch {}
  return {} as any;
}

function writePrefs(p: any) {
  document.cookie = `prefs=${encodeURIComponent(JSON.stringify(p))}; path=/; max-age=${365 * 24 * 60 * 60}`;
}

function calcWinLine(board: string[][], winner: string): WinLine | null {
  if (!winner) return null;
  for (let i = 0; i < 3; i++) {
    if (
      board[i][0] === winner &&
      board[i][1] === winner &&
      board[i][2] === winner
    ) {
      return { start: { row: i, col: 0 }, end: { row: i, col: 2 } };
    }
    if (
      board[0][i] === winner &&
      board[1][i] === winner &&
      board[2][i] === winner
    ) {
      return { start: { row: 0, col: i }, end: { row: 2, col: i } };
    }
  }
  if (
    board[0][0] === winner &&
    board[1][1] === winner &&
    board[2][2] === winner
  ) {
    return { start: { row: 0, col: 0 }, end: { row: 2, col: 2 } };
  }
  if (
    board[0][2] === winner &&
    board[1][1] === winner &&
    board[2][0] === winner
  ) {
    return { start: { row: 0, col: 2 }, end: { row: 2, col: 0 } };
  }
  return null;
}

function App() {
  const prefs = React.useMemo(() => readPrefs(), []);
  const [board, setBoard] = React.useState([
    ["", "", ""],
    ["", "", ""],
    ["", "", ""],
  ]);
  const [turn, setTurn] = React.useState("X");
  const [winner, setWinner] = React.useState("");
  const [turns, setTurns] = React.useState({ X: 0, O: 0 });
  const [hintPos, setHintPos] = React.useState(null);
  const [menuOpen, setMenuOpen] = React.useState(false);
  const [showHelp, setShowHelp] = React.useState(false);
  const [showAbout, setShowAbout] = React.useState(false);
  const [showSettings, setShowSettings] = React.useState(false);
  const [theme, setTheme] = React.useState(prefs.theme || "system");
  const initialDark = (() => {
    const systemDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    const t = prefs.theme || "system";
    return t === "dark" || (t === "system" && systemDark);
  })();
  const [isDarkMode, setIsDarkMode] = React.useState(initialDark);
  const [winLine, setWinLine] = React.useState(null as WinLine | null);
  const [user, setUser] = React.useState(
    prefs.username ? { username: prefs.username, name: prefs.name } : null,
  );
  const [showRegister, setShowRegister] = React.useState(false);
  const [regName, setRegName] = React.useState("");
  const [hasSession, setHasSession] = React.useState(false);
  const [sessionMsg, setSessionMsg] = React.useState("No active sessions");
  const [sessionKey, setSessionKey] = React.useState("");
  const [opponent, setOpponent] = React.useState(null as any);
  const [showConnected, setShowConnected] = React.useState(false);
  const [showCopied, setShowCopied] = React.useState(false);
  const [showTerminate, setShowTerminate] = React.useState(false);
  const [inGame, setInGame] = React.useState(false);
  const [mySymbol, setMySymbol] = React.useState("X");
  const [gameID, setGameID] = React.useState("");
  const [gameList, setGameList] = React.useState([] as any[]);
  const [showGames, setShowGames] = React.useState(false);
  const [mySkips, setMySkips] = React.useState(0);
  const [version, setVersion] = React.useState("dev");
  const createCtrl = React.useRef(null as AbortController | null);
  const nameRef = React.useRef<HTMLInputElement | null>(null);

  React.useEffect(() => {
    fetch("/version")
      .then((r: any) => r.text())
      .then((v: string) => setVersion(v.trim()))
      .catch(() => {});
  }, []);

  const copyText = async (text: string) => {
    const fallback = () => {
      const inp = document.createElement("input");
      inp.value = text;
      document.body.appendChild(inp);
      inp.select();
      inp.setSelectionRange(0, inp.value.length);
      const ok = document.execCommand("copy");
      document.body.removeChild(inp);
      return ok;
    };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      try {
        await navigator.clipboard.writeText(text);
        setShowCopied(true);
        return true;
      } catch {
        if (fallback()) {
          setShowCopied(true);
          return true;
        }
        return false;
      }
    }
    if (fallback()) {
      setShowCopied(true);
      return true;
    }
    return false;
  };

  const copyInvite = async (key?: string) => {
    const k = typeof key === "string" ? key : sessionKey;
    if (!k) return;
    const url = `${window.location.origin}?key=${k}`;
    await copyText(url);
  };

  React.useEffect(() => {
    writePrefs({ username: user?.username, name: user?.name, theme });
  }, [user, theme]);

  React.useEffect(() => {
    if (!showCopied) return;
    const t = setTimeout(() => setShowCopied(false), 5000);
    return () => clearTimeout(t);
  }, [showCopied]);

  React.useEffect(() => {
    if (!showConnected) return;
    const t = setTimeout(() => setShowConnected(false), 3000);
    return () => clearTimeout(t);
  }, [showConnected]);

  React.useEffect(() => {
    const overlay = showRegister || !hasSession || (hasSession && !inGame);
    if (overlay) {
      window.scrollTo(0, 0);
      document.body.classList.add("overflow-hidden");
    } else {
      document.body.classList.remove("overflow-hidden");
    }
    return () => document.body.classList.remove("overflow-hidden");
  }, [showRegister, hasSession, inGame]);

  React.useEffect(() => {
    const handler = () => {
      if (sessionKey) {
        const url = `/session/${encodeURIComponent(sessionKey)}/abandon`;
        if (navigator.sendBeacon) {
          navigator.sendBeacon(url);
        }
      }
    };
    window.addEventListener("pagehide", handler);
    window.addEventListener("beforeunload", handler);
    return () => {
      window.removeEventListener("pagehide", handler);
      window.removeEventListener("beforeunload", handler);
    };
  }, [sessionKey]);

  const refresh = () => {
    if (!gameID) return;
    fetch(`/game/${gameID}/board`)
      .then((r) => r.json())
      .then(setBoard);
    fetch(`/game/${gameID}/turn`)
      .then((r) => r.json())
      .then((d) => setTurn(d.turn));
    fetch(`/game/${gameID}/turns`)
      .then((r) => r.json())
      .then(setTurns);
  };
  React.useEffect(() => {
    refresh();
  }, [gameID]);
  React.useEffect(() => {
    fetch("/user/me").then(async (r) => {
      if (r.status === 200) {
        const u = await r.json();
        setUser({
          username: u.username || u.Username,
          name: u.name || u.Name,
          activeSession: u.activeSession || u.ActiveSession,
        });
        if (u.activeSession) {
          setSessionKey(u.activeSession);
          setHasSession(true);
        }
      } else {
        setShowRegister(true);
      }
    });
  }, []);
  React.useEffect(() => {
    if (!gameID) return;
    const es = new EventSource(`/game/${gameID}/events`);
    es.onmessage = (e: any) => {
      const d = JSON.parse(e.data);
      setBoard(d.board);
      setTurn(d.turn);
      setTurns(d.turns);
      setWinner(d.winner);
      setWinLine(calcWinLine(d.board, d.winner));
      if (!inGame && user) {
        setMySymbol(d.playerX === user.username ? "X" : "O");
        setInGame(true);
      }
    };
    return () => es.close();
  }, [gameID, inGame, user]);
  React.useEffect(() => {
    if (!user) return;
    const params = new URLSearchParams(window.location.search);
    const key = params.get("key");
    if (key) {
      fetch(`/session/${encodeURIComponent(key)}/join`, {
        method: "POST",
      }).then(async (r) => {
        if (r.status === 200) {
          const s = await r.json();
          const me = user.username;
          const other = s.creator.username === me ? s.joiner : s.creator;
          if (other) {
            setOpponent(other);
            setSessionMsg(`Connected with ${other.name}`);
            setHasSession(true);
            setShowConnected(true);
          }
          setSessionKey(s.key);
          const url = new URL(window.location.href);
          url.searchParams.delete("key");
          window.history.replaceState({}, "", url.pathname + url.search);
        }
      });
    }
  }, [user]);
  React.useEffect(() => {
    if (!user) return;
    fetch("/sessions").then(async (r) => {
      if (r.status !== 200) return;
      const list = await r.json();
      const mine = list.filter(
        (s: any) =>
          (s.creator && s.creator.username === user.username) ||
          (s.joiner && s.joiner.username === user.username),
      );
      const active = mine
        .filter((s: any) => !s.endTime)
        .sort(
          (a: any, b: any) =>
            new Date(b.inviteTime).getTime() - new Date(a.inviteTime).getTime(),
        )[0];
      if (active) {
        setHasSession(true);
        setSessionKey(active.key);
        const other =
          active.creator.username === user.username
            ? active.joiner
            : active.creator;
        if (other) {
          if (!opponent) {
            setOpponent(other);
            setShowConnected(true);
          }
          setSessionMsg(`Connected with ${other.name}`);
        } else {
          setSessionMsg("Waiting for opponent");
        }
      }
    });
  }, [user]);
  React.useEffect(() => {
    if (!hasSession || !sessionKey || !user) return;
    const es = new EventSource(
      `/session/${encodeURIComponent(sessionKey)}/events`,
    );
    es.onmessage = (e: any) => {
      const info = JSON.parse(e.data);
      if (info.endTime) {
        setOpponent(null);
        setInGame(false);
        setGameID("");
        setGameList([]);
        setShowGames(false);
        setMySkips(0);
        setSessionMsg(
          "Opponent disconnected. Share the invite URL to reconnect.",
        );
        return;
      }
      const other =
        info.creator.username === user.username ? info.joiner : info.creator;
      if (other) {
        setOpponent(other);
        setSessionMsg(`Connected with ${other.name}`);
        setShowConnected(true);
      } else {
        setOpponent(null);
        setSessionMsg("Waiting for opponent");
      }
      if (info.activeGame) {
        if (!inGame || gameID !== info.activeGame) {
          setGameID(info.activeGame);
          fetch(`/game/${info.activeGame}/state`)
            .then((r) => r.json())
            .then((d) => {
              setBoard(d.board);
              setTurn(d.turn);
              setWinner(d.winner);
              setWinLine(calcWinLine(d.board, d.winner));
              const mySym = d.playerX === user.username ? "X" : "O";
              setMySymbol(mySym);
              setInGame(true);
            });
          setShowGames(false);
        }
      } else {
        setInGame(false);
        setGameID("");
      }
      const active = info.games.filter(
        (gg: any) => !gg.complete && !gg.abandoned,
      );
      if (showGames) {
        setGameList(active);
      } else if (active.length > 1 && !inGame) {
        setGameList(active);
        setShowGames(true);
      }
    };
    return () => es.close();
  }, [hasSession, sessionKey, user, inGame, gameID, showGames]);
  React.useEffect(() => {
    setWinLine(calcWinLine(board, winner));
  }, [board, winner]);
  React.useEffect(() => {
    const apply = () => {
      document.body.classList.remove("theme-dark", "theme-light");
      const systemDark = window.matchMedia(
        "(prefers-color-scheme: dark)",
      ).matches;
      const dark = theme === "dark" || (theme === "system" && systemDark);
      if (dark) document.body.classList.add("theme-dark");
      else document.body.classList.add("theme-light");
      setIsDarkMode(dark);
    };
    apply();
    if (theme === "system") {
      const m = window.matchMedia("(prefers-color-scheme: dark)");
      m.addEventListener("change", apply);
      return () => m.removeEventListener("change", apply);
    }
  }, [theme]);
  const menuRef = React.useRef(null);
  const menuButtonRef = React.useRef(null);
  React.useEffect(() => {
    if (!menuOpen) return;
    const handler = (e: any) => {
      if (
        menuRef.current &&
        !menuRef.current.contains(e.target) &&
        !menuButtonRef.current.contains(e.target)
      )
        setMenuOpen(false);
    };
    document.addEventListener("click", handler);
    return () => document.removeEventListener("click", handler);
  }, [menuOpen]);

  const click = (r: number, c: number) => {
    if (winner || !inGame || turn !== mySymbol) return;
    if (!gameID) return;
    fetch(`/game/${gameID}/move`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ row: r, col: c }),
    })
      .then((r) => r.json())
      .then((d) => {
        setBoard(d.board);
        setWinner(d.winner);
        setWinLine(calcWinLine(d.board, d.winner));
        refresh();
      });
  };
  const newGame = () => {
    if (!sessionKey) {
      alert("No active session. Create or join a session first.");
      return;
    }
    fetch("/game/start?session=" + encodeURIComponent(sessionKey), {
      method: "POST",
    }).then(async (r) => {
      if (r.status !== 200) {
        r.text().then(alert);
        return;
      }
      const data = await r.json();
      const gid = data.id;
      setSessionKey(sessionKey);
      setGameID(gid);
      setBoard(data.board);
      setTurn(data.turn);
      setWinner("");
      setTurns({ X: 0, O: 0 });
      setHintPos(null);
      setWinLine(null);
      setMySymbol(data.playerX === user.username ? "X" : "O");
      setInGame(true);
      setMySkips(0);
      fetch(`/session/${encodeURIComponent(sessionKey)}/activate/${gid}`, {
        method: "POST",
      });
    });
    setMenuOpen(false);
  };
  const hint = () => {
    if (!gameID) return;
    fetch(`/game/${gameID}/hint`).then(async (r) => {
      if (r.status === 200) {
        const d = await r.json();
        setHintPos({ row: d.row, col: d.col });
        setTimeout(() => setHintPos(null), 5000);
      } else {
        alert("No more hints");
      }
    });
  };
  const skip = () => {
    if (!gameID) return;
    fetch(`/game/${gameID}/skip`, { method: "POST" }).then((r) => {
      if (r.status === 200) {
        setMySkips((s) => s + 1);
        refresh();
      } else {
        r.text().then((t) => alert(t));
      }
    });
  };
  const register = () => {
    fetch("/user/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: regName.trim(),
      }),
    }).then(async (r) => {
      if (r.status === 200) {
        const u = await r.json();
        setUser({
          username: u.username || u.Username,
          name: u.name || u.Name,
        });
        setShowRegister(false);
      } else {
        r.text().then(alert);
      }
    });
  };
  const createSession = async () => {
    if (createCtrl.current) createCtrl.current.abort();
    if (sessionKey) {
      fetch(`/session/${encodeURIComponent(sessionKey)}/abandon`, {
        method: "POST",
      });
    }
    const ctrl = new AbortController();
    createCtrl.current = ctrl;
    try {
      const r = await fetch("/session/new", {
        method: "POST",
        signal: ctrl.signal,
      });
      if (r.status === 200) {
        const d = await r.json();
        await copyInvite(d.key);
        setSessionKey(d.key);
        setSessionMsg("Waiting for opponent");
        setHasSession(true);
        setOpponent(null);
        setInGame(false);
      } else {
        r.text().then(alert);
      }
    } finally {
      createCtrl.current = null;
      setMenuOpen(false);
    }
  };
  const terminateSession = () => {
    if (!sessionKey) return;
    fetch(`/session/${encodeURIComponent(sessionKey)}/abandon`, {
      method: "POST",
    }).then(() => {
      setHasSession(false);
      setSessionKey("");
      setOpponent(null);
      setGameID("");
      setInGame(false);
      setGameList([]);
      setShowGames(false);
      setSessionMsg("No active sessions");
      setShowTerminate(false);
    });
  };
  const openHelp = () => {
    setMenuOpen(false);
    setShowHelp(true);
  };
  const openAbout = () => {
    setMenuOpen(false);
    setShowAbout(true);
  };
  const openSettings = () => {
    setMenuOpen(false);
    setShowSettings(true);
  };
  const disableCreate = hasSession && !!opponent;
  const disableNewGame = !hasSession;
  const showHint = turns.X >= 4 && turns.O >= 4;
  const boardSize = "calc(var(--cell-size) * 3 + 4vmin)";
  const headerWidth = "calc((var(--cell-size) * 3 + 4vmin) * 1.2)";
  return (
    <>
      <div className="fixed inset-0 z-0 flex items-center justify-center overflow-hidden pointer-events-none select-none">
        <img
          src={`/assets/background-${isDarkMode ? "dark" : "light"}.png`}
          alt=""
          className="opacity-20 w-full h-full object-contain"
        />
      </div>
      <div
        className="relative flex flex-col items-center w-full"
        style={{ paddingTop: "calc(env(safe-area-inset-top) + 4rem)" }}
      >
        <div
          className="fixed left-1/2 transform -translate-x-1/2 flex items-center justify-between z-30"
          style={{ width: headerWidth, top: "env(safe-area-inset-top)" }}
        >
          <button
            ref={menuButtonRef}
            className="p-2"
            onClick={() => setMenuOpen(!menuOpen)}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="h-6 w-6"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="M4 6h16M4 12h16M4 18h16"
              />
            </svg>
          </button>
          <h1
            className="flex-grow flex justify-center items-center h-12 sm:h-16 px-4 rounded"
            style={{
              backgroundColor: isDarkMode ? "#000000" : "#fffaf0",
            }}
          >
            <img
              src={`/assets/title-${isDarkMode ? "dark" : "light"}.png`}
              alt="Tic-Tac-Twist"
              className="h-full w-auto"
            />
          </h1>
          <div className="w-6"></div>
          {menuOpen && (
            <div
              ref={menuRef}
              className="absolute left-0 top-8 shadow rounded z-30 w-40 bg-yellow-100 text-gray-900 dark:bg-yellow-100 dark:text-gray-900"
            >
              <button
                className={`block w-full text-left px-4 py-2 whitespace-nowrap ${disableCreate ? "text-gray-400 cursor-not-allowed" : "hover:bg-yellow-200 dark:hover:bg-yellow-200"}`}
                onClick={disableCreate ? undefined : createSession}
              >
                Create Session
              </button>
              {hasSession && (
                <button
                  className="block w-full text-left px-4 py-2 hover:bg-yellow-200 dark:hover:bg-yellow-200 text-sm whitespace-nowrap"
                  onClick={() => {
                    setShowTerminate(true);
                    setMenuOpen(false);
                  }}
                >
                  Terminate Session
                </button>
              )}
              <button
                className={`block w-full text-left px-4 py-2 whitespace-nowrap ${disableNewGame ? "text-gray-400 cursor-not-allowed" : "hover:bg-yellow-200 dark:hover:bg-yellow-200"}`}
                onClick={disableNewGame ? undefined : newGame}
              >
                New Game
              </button>
              <button
                className="block w-full text-left px-4 py-2 hover:bg-yellow-200 dark:hover:bg-yellow-200 whitespace-nowrap"
                onClick={openSettings}
              >
                Settings
              </button>
              <button
                className="block w-full text-left px-4 py-2 hover:bg-yellow-200 dark:hover:bg-yellow-200 whitespace-nowrap"
                onClick={openHelp}
              >
                Help
              </button>
              <button
                className="block w-full text-left px-4 py-2 hover:bg-yellow-200 dark:hover:bg-yellow-200 whitespace-nowrap"
                onClick={openAbout}
              >
                About
              </button>
            </div>
          )}
        </div>
        {inGame && (
          <div className="mt-8 mb-4 text-lg font-semibold">
            {winner ? (
              <button
                className="px-3 py-1 bg-yellow-300 text-gray-900 rounded"
                onClick={newGame}
              >
                New Game
              </button>
            ) : (
              <>
                You are {mySymbol}. It is{" "}
                {turn === mySymbol ? "your" : `${opponent?.name}'s`} turn.
              </>
            )}
          </div>
        )}
        <div
          className="relative flex justify-center items-center"
          style={{
            width: boardSize,
            height: boardSize,
          }}
        >
          {inGame && (
            <div className="relative z-10">
              <Board
                board={board}
                highlight={hintPos}
                onCell={click}
                winLine={winLine}
                theme={theme}
              />
            </div>
          )}
          {!hasSession && (
            <div className="fixed inset-0 z-10 flex items-center justify-center bg-gray-400 bg-opacity-50">
              <button
                className="px-3 py-1 bg-yellow-300 text-gray-900 rounded"
                onClick={createSession}
              >
                Create New Session
              </button>
            </div>
          )}
          {hasSession && !inGame && (
            <div className="fixed inset-0 z-10 flex items-center justify-center bg-gray-400 bg-opacity-50">
              {opponent ? (
                <button
                  className="px-3 py-1 bg-yellow-300 text-gray-900 rounded"
                  onClick={newGame}
                >
                  New Game
                </button>
              ) : (
                <div className="space-y-2">
                  <button
                    className="px-3 py-1 bg-yellow-300 text-gray-900 rounded w-full"
                    onClick={createSession}
                  >
                    Create New Session
                  </button>
                  <button
                    className="px-3 py-1 bg-yellow-300 text-gray-900 rounded w-full"
                    onClick={() => copyInvite()}
                  >
                    Copy URL
                  </button>
                </div>
              )}
            </div>
          )}
        </div>
        {winner && (
          <>
            <div className="mt-4 text-2xl">{`${winner === mySymbol ? user.name : opponent?.name} wins`}</div>
            <button
              className="mt-2 px-3 py-1 bg-yellow-300 text-gray-900 rounded"
              onClick={newGame}
            >
              New Game
            </button>
          </>
        )}
        {inGame && !winner && (
          <div className="mt-4 space-x-2 flex justify-center">
            {showHint && (
              <>
                <button
                  className={`px-2 py-1 rounded ${turn === mySymbol ? "bg-yellow-300 text-gray-900" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`}
                  onClick={hint}
                  disabled={turn !== mySymbol}
                >
                  Hint
                </button>
                <button
                  className={`px-2 py-1 rounded ${turn === mySymbol && mySkips < 4 ? "bg-green-300 text-gray-900" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`}
                  onClick={skip}
                  disabled={turn !== mySymbol || mySkips >= 4}
                >
                  Skip
                </button>
              </>
            )}
          </div>
        )}
      </div>
      {showHelp && (
        <Modal onClose={() => setShowHelp(false)} theme={theme}>
          <h2 className="text-xl font-semibold mb-2">How to Play</h2>
          <p className="mb-2">
            Create a session and share the invite URL with a friend. Wait for
            the friend to register and join before starting or resuming a game.
          </p>
          <p className="mb-2">Only one player can move at a time.</p>
          <p className="mb-2">
            Your oldest mark disappears after your fifth move to ensure that the
            game ends in a Win. Before such a move, you may use Hints (up to 3
            times per game) to view the oldest mark that will disappear. You may
            also choose to Skip your turn, which will yield the turn to your
            opponent.
          </p>
        </Modal>
      )}
      {showAbout && (
        <Modal onClose={() => setShowAbout(false)} theme={theme}>
          <h2 className="text-xl font-semibold mb-2">About</h2>
          <img
            src="/apple-touch-icon.png"
            alt="Tic-Tac-Twist icon"
            className="mx-auto my-4 h-32 w-32"
          />
          <div className="text-right">
            <p>Tic-Tac-Twist v{version}</p>
            <p>Created by Deepak Amin</p>
          </div>
        </Modal>
      )}
      {showSettings && (
        <Modal onClose={() => setShowSettings(false)} theme={theme}>
          <h2 className="text-xl font-semibold mb-2">Settings</h2>
          <div className="mb-2">
            <label className="mr-2">Theme:</label>
            <select
              value={theme}
              onChange={(e) => setTheme((e.target as HTMLSelectElement).value)}
              className="border rounded p-1 text-black"
            >
              <option value="light">Light</option>
              <option value="dark">Dark</option>
              <option value="system">System</option>
            </select>
          </div>
        </Modal>
      )}
      {showConnected && (
        <div className="fixed bottom-24 inset-x-0 flex justify-center">
          <div className="bg-yellow-100 text-gray-900 dark:bg-yellow-100 dark:text-gray-900 px-4 py-2 rounded shadow">
            Connected with {opponent?.name}
          </div>
        </div>
      )}
      {showCopied && (
        <Modal onClose={() => setShowCopied(false)} theme={theme}>
          <p>Session created, and Invite URL copied to clipboard</p>
        </Modal>
      )}
      {showGames && (
        <Modal onClose={() => setShowGames(false)} theme={theme}>
          <h2 className="text-xl font-semibold mb-2">Resume Game</h2>
          <ul className="space-y-2">
            {gameList.map((g: any) => (
              <li key={g.id} className="flex justify-between items-center">
                <button
                  className="px-2 py-1 bg-yellow-300 text-gray-900 rounded"
                  onClick={() => {
                    fetch(`/game/${g.id}/state`)
                      .then((r) => r.json())
                      .then((d) => {
                        setBoard(d.board);
                        setTurn(d.turn);
                        setWinner(d.winner);
                        setWinLine(calcWinLine(d.board, d.winner));
                        const mySym = d.playerX === user.username ? "X" : "O";
                        setMySymbol(mySym);
                        setMySkips(0);
                      });
                    setGameID(g.id);
                    fetch(
                      `/session/${encodeURIComponent(sessionKey)}/activate/${g.id}`,
                      {
                        method: "POST",
                      },
                    );
                    setInGame(true);
                    setShowGames(false);
                  }}
                >
                  {g.id.slice(0, 8)} ({new Date(g.updatedAt).toLocaleString()})
                </button>
                <button
                  className="text-red-600"
                  onClick={() => {
                    fetch(`/game/${g.id}/abandon`, { method: "POST" }).then(
                      () => {
                        setGameList(gameList.filter((x) => x.id !== g.id));
                        if (g.id === gameID) {
                          setGameID("");
                          setInGame(false);
                        }
                      },
                    );
                  }}
                >
                  🗑
                </button>
              </li>
            ))}
          </ul>
        </Modal>
      )}
      {showTerminate && (
        <Modal onClose={() => setShowTerminate(false)} theme={theme}>
          <p className="mb-4">Terminate session with {opponent?.name}?</p>
          <div className="flex justify-end space-x-2">
            <button
              className="px-3 py-1 bg-yellow-300 text-gray-900 rounded"
              onClick={terminateSession}
            >
              Yes
            </button>
            <button
              className="px-3 py-1 bg-gray-300 text-gray-900 rounded"
              onClick={() => setShowTerminate(false)}
            >
              No
            </button>
          </div>
        </Modal>
      )}
      {showRegister && (
        <Modal onClose={() => {}} theme={theme}>
          <h2 className="text-xl font-semibold mb-2">Register</h2>
          <div className="space-y-2">
            <input
              ref={nameRef}
              className="w-full p-1 border rounded text-black"
              placeholder="Name"
              value={regName}
              onChange={(e) => setRegName((e.target as HTMLInputElement).value)}
              onKeyDown={(e) => {
                if (e.key === "Enter" && regName) {
                  register();
                }
              }}
            />
            <button
              className="px-3 py-1 bg-yellow-300 text-gray-900 rounded w-full"
              disabled={!regName}
              onClick={register}
            >
              Submit
            </button>
          </div>
        </Modal>
      )}
      <div className="fixed bottom-2 left-2 right-2 flex justify-between text-sm">
        <span className="hidden sm:inline truncate max-w-[40%]">
          {user ? `Hello, ${user.name}!` : ""}
        </span>
        <span className="truncate text-right">
          {sessionMsg}
          {sessionKey && !opponent && (
            <button className="ml-2 underline" onClick={() => copyInvite()}>
              Copy URL
            </button>
          )}
        </span>
      </div>
    </>
  );
}

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