"use client";
import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  Suspense,
} from "react";
import { useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";
import { FaRedo } from "react-icons/fa";
import { MdLibraryMusic } from "react-icons/md";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import {
  GameRoot,
  GameGlobalStyle,
  GameNavbar,
  GameContent,
  HUD,
  HudStat,
  HudValue,
  HudLabel,
  ComboValue,
  GameArea,
  UpcomingWrap,
  UpcomingLabel,
  UpcomingText,
  CurrentWrap,
  LineTimingMeta,
  LineTimingValue,
  LineTimingRow,
  LineTimingBar,
  LineTimingFill,
  CharRow,
  WordWrap,
  CharBox,
  ClearToast,
  GetReadyText,
  BackgroundVideo,
  OpacityControl,
  OpacityLabel,
  OpacitySlider,
  OpacityValue,
  CompletedLineFade,
  GameFooter,
  ControlBtn,
  ProgressWrap,
  ProgressFill,
  TimeText,
  StartOverlay,
  StartCard,
  CountdownNumber,
  SongTitleText,
  SongArtistText,
  StartBtn,
  CodeSection,
  CodeInputRow,
  CodeInputField,
  CodeLoadBtn,
  ResultsOverlay,
  ResultsCard,
  ResultsTitle,
  BigScore,
  StatsGrid,
  StatBlock,
  StatValue,
  StatLabel,
  ActionRow,
  PlayAgainBtn,
  HomeBtn,
} from "./page.styles";
import { gReducer, initialGState } from "./game.stat";
import { formatTime, parseLrcLines, calculateCPSNeeded } from "./game.utils";

type GamePhase = "idle" | "countdown" | "playing" | "paused" | "finished";

const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
const isVideoUrl = (url: string) => {
  if (!url) return false;
  const cleaned = url.split("?")[0].split("#")[0];
  const ext = cleaned.split(".").pop()?.toLowerCase();
  return !!ext && VIDEO_EXTENSIONS.has(ext);
};
const BACKGROUND_OPACITY_KEY = "lrcType.backgroundOpacity";

function GameInner() {
  const searchParams = useSearchParams();
  const router = useRouter();

  useEffect(() => {
    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
  }, []);

  const audioRef = useRef<HTMLAudioElement>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const gameStartTimeRef = useRef<number>(0);
  const lastHandledIdxRef = useRef(-1);
  const lastLineAdvanceAtRef = useRef(0);

  const [phase, setPhase] = useState<GamePhase>("idle");
  const [currentMs, setCurrentMs] = useState(0);
  const [lineTimingPct, setLineTimingPct] = useState(0);
  const [lineRemainingMs, setLineRemainingMs] = useState(0);
  const [currentLineTime, setCurrentLineTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [progressPct, setProgressPct] = useState(0);
  const [gameDurationMs, setGameDurationMs] = useState(0);

  const [lrcContent, setLrcContent] = useState("");
  const [audioUrl, setAudioUrl] = useState("");
  const [songTitle, setSongTitle] = useState("Unknown Title");
  const [songArtist, setSongArtist] = useState("Unknown Artist");
  const [offset, setOffset] = useState(0);
  const [loadingLrc, setLoadingLrc] = useState(false);

  const [codeInput, setCodeInput] = useState("");
  const [wrongChar, setWrongChar] = useState(false);
  const [clearShowing, setClearShowing] = useState(false);
  const [comboAnimKey, setComboAnimKey] = useState(0);
  const [countdown, setCountdown] = useState(0);
  const [backgroundOpacity, setBackgroundOpacity] = useState(() => {
    if (typeof window === "undefined") return 0;
    const stored = localStorage.getItem(BACKGROUND_OPACITY_KEY);
    if (stored === null) return 0;
    const parsed = Number(stored);
    if (!Number.isFinite(parsed)) return 0;
    return Math.min(100, Math.max(0, parsed));
  });
  const [skipBacking, setSkipBacking] = useState(false);
  const isVideo = useMemo(() => isVideoUrl(audioUrl), [audioUrl]);



  useEffect(() => {
    localStorage.setItem(BACKGROUND_OPACITY_KEY, String(backgroundOpacity));
  }, [backgroundOpacity]);

  const charRowRef = useRef<HTMLDivElement | null>(null);
  const charRefs = useRef<(HTMLSpanElement | null)[]>([]);
  const [wrapSpaceIndicators, setWrapSpaceIndicators] = useState<boolean[]>([]);
  const countdownIntervalRef = useRef<number | null>(null);

  const [g, dispatch] = useReducer(gReducer, initialGState);

  const gameLines = useMemo(
    () => parseLrcLines(lrcContent, { skipBacking }),
    [lrcContent, skipBacking]
  );
  const isReady = !loadingLrc && !!lrcContent && !!audioUrl;

  const accuracy =
    g.totalCorrect + g.totalMiss > 0
      ? Math.round((g.totalCorrect / (g.totalCorrect + g.totalMiss)) * 100)
      : 100;

  const elapsedMs =
    phase === "playing"
      ? Math.max(1, Date.now() - gameStartTimeRef.current)
      : gameDurationMs;

  const wpm =
    elapsedMs > 0 ? Math.round(g.totalCorrect / 5 / (elapsedMs / 60000)) : 0;

  const gRef = useRef(g);
  const currentLineContent =
    g.displayedLineIdx >= 0 ? gameLines[g.displayedLineIdx]?.content ?? "" : "";

  useEffect(() => {
    charRefs.current = [];
  }, [currentLineContent]);

  useLayoutEffect(() => {
    if (!charRowRef.current) return;
    let frame = 0;
    const text = currentLineContent.toLowerCase();

    const recompute = () => {
      const nodes = charRefs.current;
      const indicators = new Array(text.length).fill(false);
      for (let i = 0; i < text.length - 1; i += 1) {
        if (text[i] !== " ") continue;
        const curr = nodes[i];
        const next = nodes[i + 1];
        if (!curr || !next) continue;
        const currRect = curr.getBoundingClientRect();
        const nextRect = next.getBoundingClientRect();
        if (nextRect.top - currRect.top > 1) {
          indicators[i] = true;
        }
      }
      setWrapSpaceIndicators(indicators);
    };

    const schedule = () => {
      cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => {
        frame = requestAnimationFrame(recompute);
      });
    };

    schedule();

    if (document.fonts?.ready) {
      document.fonts.ready.then(schedule);
    }

    const observer = new ResizeObserver(schedule);
    observer.observe(charRowRef.current);
    window.addEventListener("resize", schedule);

    return () => {
      observer.disconnect();
      window.removeEventListener("resize", schedule);
      cancelAnimationFrame(frame);
    };
  }, [currentLineContent]);
  useEffect(() => {
    gRef.current = g;
  }, [g]);

  const phaseRef = useRef<GamePhase>("idle");
  useEffect(() => {
    phaseRef.current = phase;
  }, [phase]);

  const offsetRef = useRef(0);
  useEffect(() => {
    offsetRef.current = offset;
  }, [offset]);

  useEffect(() => {
    if (!("mediaSession" in navigator)) return;
    const mediaSession = navigator.mediaSession;
    mediaSession.setActionHandler("pause", () => {});
    return () => {
      mediaSession.setActionHandler("pause", null);
    };
  }, []);

  useEffect(() => {
    return () => {
      if (countdownIntervalRef.current !== null) {
        clearInterval(countdownIntervalRef.current);
        countdownIntervalRef.current = null;
      }
    };
  }, []);

  const lineAnimRef = useRef({ startMs: 0, endMs: 0, startPerf: 0 });

  const timeBasedLineIdx = useMemo(() => {
    if (!gameLines.length) return -1;
    let idx = -1;
    for (let i = 0; i < gameLines.length; i++) {
      if (gameLines[i].millisecond <= currentMs) idx = i;
      else break;
    }
    return idx;
  }, [currentMs, gameLines]);

  const intermissionData = useMemo(() => {
    const firstMs = gameLines[0]?.millisecond ?? 0;
    if (firstMs <= 0) {
      return { pct: 100, remainingMs: 0 };
    }
    const clampedCurrent = Math.max(0, currentMs);
    const remainingMs = Math.max(0, firstMs - clampedCurrent);
    const pct = Math.min(100, Math.max(0, (clampedCurrent / firstMs) * 100));
    return { pct, remainingMs };
  }, [gameLines, currentMs]);

  useEffect(() => {
    const idx = g.displayedLineIdx;
    if (idx < 0 || !gameLines[idx]) {
      lineAnimRef.current = { startMs: 0, endMs: 0, startPerf: 0 };
      setLineTimingPct(0);
      setLineRemainingMs(0);
      setCurrentLineTime(-1);
      return;
    }
    const start = gameLines[idx].millisecond;
    const end = gameLines[idx + 1]?.millisecond ?? start + 5000;
    lineAnimRef.current = {
      startMs: start,
      endMs: end,
      startPerf: performance.now(),
    };
    setLineTimingPct(0);
    const currentLineTime = end - start;
    setLineRemainingMs(Math.max(0, currentLineTime));
    setCurrentLineTime(Math.max(currentLineTime, currentLineTime));
  }, [g.displayedLineIdx, gameLines]);

  useEffect(() => {
    if (phase !== "playing") return;
    let rafId = 0;
    const tick = () => {
      const { startMs, endMs, startPerf } = lineAnimRef.current;
      if (endMs <= startMs) {
        setLineTimingPct(100);
        setLineRemainingMs(0);
      } else {
        const elapsed = performance.now() - startPerf;
        const duration = endMs - startMs;
        const pct = Math.min(100, Math.max(0, (elapsed / duration) * 100));
        const remaining = Math.max(0, duration - elapsed);
        setLineTimingPct(pct);
        setLineRemainingMs(remaining);
      }
      rafId = requestAnimationFrame(tick);
    };
    rafId = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafId);
  }, [phase]);

  useEffect(() => {
    const media = isVideo ? videoRef.current : audioRef.current;
    if (!media) return;
    const onTimeUpdate = () => {
      setCurrentMs(media.currentTime * 1000 + offsetRef.current);
      if (media.duration && !isNaN(media.duration)) {
        setDuration(media.duration * 1000);
        setProgressPct((media.currentTime / media.duration) * 100);
      }
    };
    const onLoadedMetadata = () => {
      if (!isNaN(media.duration)) {
        setDuration(media.duration * 1000);
        setGameDurationMs(media.duration * 1000);
      }
    };
    const onEnded = () => {
      setPhase("finished");
      setGameDurationMs(Date.now() - gameStartTimeRef.current);
    };
    media.addEventListener("timeupdate", onTimeUpdate);
    media.addEventListener("loadedmetadata", onLoadedMetadata);
    media.addEventListener("ended", onEnded);
    return () => {
      media.removeEventListener("timeupdate", onTimeUpdate);
      media.removeEventListener("loadedmetadata", onLoadedMetadata);
      media.removeEventListener("ended", onEnded);
    };
  }, [isVideo, audioUrl]);

  useEffect(() => {
    if (phaseRef.current !== "playing") return;
    if (timeBasedLineIdx < 0) return;
    if (timeBasedLineIdx <= lastHandledIdxRef.current) return;
    lastHandledIdxRef.current = timeBasedLineIdx;
    lastLineAdvanceAtRef.current = performance.now();
    dispatch({
      type: "ADVANCE",
      newIdx: timeBasedLineIdx,
      prevCompleted: gRef.current.lineCompleted,
    });
  }, [timeBasedLineIdx]);

  const loadData = useCallback((data: Record<string, unknown>) => {
    if (typeof data.lrc === "string" && data.lrc) {
      setLoadingLrc(true);
      fetch(data.lrc)
        .then((r) => r.text())
        .then((t) => {
          setLrcContent(t);
          setLoadingLrc(false);
        });
    }
    if (typeof data.file1 === "string") setAudioUrl(data.file1);
    if (typeof data.offset === "number") setOffset(data.offset);
    if (typeof data.offset === "string" && data.offset.trim() !== "")
      setOffset(Number(data.offset));
    if (typeof data.title === "string") setSongTitle(data.title);
    if (typeof data.artist === "string") setSongArtist(data.artist);
    if (typeof data.skip_backing === "boolean")
      setSkipBacking(data.skip_backing);
    if (typeof data.skip_backing === "string")
      setSkipBacking(data.skip_backing === "true");
  }, []);

  useEffect(() => {
    const code = searchParams.get("code");
    if (!code) return;
    try {
      const json = atob(code);
      const data = JSON.parse(json) as Record<string, unknown>;
      loadData(data);
    } catch {}
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  const handleStart = useCallback(() => {
    const media = isVideo ? videoRef.current : audioRef.current;
    if (!media || !lrcContent || !audioUrl) return;
    if (countdownIntervalRef.current !== null) {
      clearInterval(countdownIntervalRef.current);
      countdownIntervalRef.current = null;
    }
    dispatch({ type: "RESET" });
    lastHandledIdxRef.current = -1;
    media.pause();
    media.currentTime = 0;
    setPhase("countdown");
    setCountdown(5);
    setGameDurationMs(0);
    setProgressPct(0);
    setCurrentMs(0);

    const beginPlayback = () => {
      media.currentTime = 0;
      media.play();
      setPhase("playing");
      gameStartTimeRef.current = Date.now();
    };

    countdownIntervalRef.current = window.setInterval(() => {
      setCountdown((c) => {
        if (c <= 1) {
          if (countdownIntervalRef.current !== null) {
            clearInterval(countdownIntervalRef.current);
            countdownIntervalRef.current = null;
          }
          beginPlayback();
          return 0;
        }
        return c - 1;
      });
    }, 1000);
  }, [lrcContent, audioUrl, gameLines, isVideo]);

  const handleRestart = useCallback(() => {
    const media = isVideo ? videoRef.current : audioRef.current;
    if (media) {
      media.pause();
      media.currentTime = 0;
    }
    if (countdownIntervalRef.current !== null) {
      clearInterval(countdownIntervalRef.current);
      countdownIntervalRef.current = null;
    }
    setCountdown(0);
    dispatch({ type: "RESET" });
    lastHandledIdxRef.current = -1;
    setPhase("idle");
    setCurrentMs(0);
    setProgressPct(0);
  }, [isVideo]);

  const handleLoadCode = useCallback(() => {
    if (!codeInput.trim()) return;
    try {
      const json = atob(codeInput.trim());
      const data = JSON.parse(json) as Record<string, unknown>;
      loadData(data);
      handleRestart();
      toast.success("Song loaded!", { theme: "dark" });
    } catch {
      toast.error("Invalid code. Please check and try again.", {
        theme: "dark",
      });
    }
  }, [codeInput, loadData, handleRestart]);

  const handleKeyPress = useCallback(
    (char: string) => {
      if (phaseRef.current !== "playing") return;
      const line = gameLines[gRef.current.displayedLineIdx];
      if (!line || gRef.current.lineCompleted) return;
      const expected = line.content[gRef.current.typedCount];
      if (expected === undefined) return;
      if (char.toLowerCase() === expected.toLowerCase()) {
        const willComplete = gRef.current.typedCount + 1 >= line.content.length;
        dispatch({ type: "CORRECT", willComplete });
        if (willComplete) {
          setClearShowing(true);
          setTimeout(() => setClearShowing(false), 700);
          setComboAnimKey((k) => k + 1);
        }
      } else {
        if (performance.now() - lastLineAdvanceAtRef.current < 100) return;
        dispatch({ type: "WRONG" });
        setWrongChar(true);
        setTimeout(() => setWrongChar(false), 320);
      }
    },
    [gameLines],
  );

  useEffect(() => {
    if (phase !== "playing") return;
    const handler = (e: KeyboardEvent) => {
      if (e.key.length === 1) {
        e.preventDefault();
        handleKeyPress(e.key);
      }
    };
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, [phase, handleKeyPress]);

  return (
    <GameRoot>
      <ToastContainer theme="dark" />
      {!isVideo && (
        <audio ref={audioRef} src={audioUrl || undefined} preload="auto" />
      )}
      <GameNavbar style={{ justifyContent: "space-between" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
          <Link
            href="/"
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              textDecoration: "none",
              color: "#ffffff",
              fontWeight: 700,
              fontSize: 15,
            }}
          >
            <MdLibraryMusic style={{ fontSize: 20, color: "#a78bfa" }} />
            LRC-Type
          </Link>
          <Link
            href="/"
            style={{
              fontSize: 13,
              color: "rgba(255,255,255,0.6)",
              textDecoration: "none",
            }}
          >
            Karaoke
          </Link>
          <Link
            href="/create"
            style={{
              fontSize: 13,
              color: "rgba(255,255,255,0.6)",
              textDecoration: "none",
            }}
          >
            Create
          </Link>
        </div>
      </GameNavbar>

      <GameContent style={{ position: "relative" }}>
        {phase === "idle" && (
          <StartOverlay>
            <StartCard>
              {!isReady ? (
                <>
                  <SongTitleText>LRC-Type</SongTitleText>
                  <SongArtistText>Enter a game code to begin!</SongArtistText>
                </>
              ) : (
                <>
                  <SongTitleText>
                    {loadingLrc ? "Loading..." : songTitle}
                  </SongTitleText>
                  <SongArtistText>{songArtist}</SongArtistText>
                </>
              )}

              {isVideo && (
                <OpacityControl>
                  <OpacityLabel>
                    Background opacity
                    <OpacityValue>{backgroundOpacity}%</OpacityValue>
                  </OpacityLabel>
                  <OpacitySlider
                    type="range"
                    min="0"
                    max="100"
                    value={backgroundOpacity}
                    onChange={(e) =>
                      setBackgroundOpacity(Number(e.target.value))
                    }
                  />
                </OpacityControl>
              )}

              <StartBtn
                onClick={handleStart}
                disabled={!isReady}
                suppressHydrationWarning
              >
                {loadingLrc ? "Loading song..." : "▶  Start Game"}
              </StartBtn>
              <CodeSection>
                <div
                  style={{
                    fontSize: 11,
                    color: "rgba(255,255,255,0.3)",
                    letterSpacing: 1,
                    textTransform: "uppercase",
                  }}
                >
                  Load a chart
                </div>
                <CodeInputRow>
                  <CodeInputField
                    placeholder="Enter a MoekyunKaraoke or LRC-Type code..."
                    value={codeInput}
                    onChange={(e) => setCodeInput(e.target.value)}
                    onKeyDown={(e) => e.key === "Enter" && handleLoadCode()}
                  />
                  <CodeLoadBtn onClick={handleLoadCode}>Load</CodeLoadBtn>
                </CodeInputRow>
              </CodeSection>
            </StartCard>
          </StartOverlay>
        )}

        {phase === "countdown" && (
          <StartOverlay>
            <StartCard>
              <SongTitleText>Get Ready</SongTitleText>
              <CountdownNumber>{countdown}</CountdownNumber>
            </StartCard>
          </StartOverlay>
        )}

        {phase === "finished" && (
          <ResultsOverlay>
            <ResultsCard>
              <ResultsTitle>Results {songTitle}</ResultsTitle>
              <BigScore>{g.score.toLocaleString()}</BigScore>
              <StatsGrid>
                <StatBlock>
                  <StatValue>{accuracy}%</StatValue>
                  <StatLabel>Accuracy</StatLabel>
                </StatBlock>
                <StatBlock>
                  <StatValue>x{g.maxCombo}</StatValue>
                  <StatLabel>Max Combo</StatLabel>
                </StatBlock>
                <StatBlock>
                  <StatValue>{wpm}</StatValue>
                  <StatLabel>WPM</StatLabel>
                </StatBlock>
                <StatBlock>
                  <StatValue>{g.totalMiss}</StatValue>
                  <StatLabel>Missed Chars</StatLabel>
                </StatBlock>
              </StatsGrid>
              <ActionRow>
                <PlayAgainBtn onClick={handleRestart}>Play Again</PlayAgainBtn>
                <HomeBtn onClick={() => router.push("/")}>Home</HomeBtn>
              </ActionRow>
            </ResultsCard>
          </ResultsOverlay>
        )}

        <HUD>
          <HudStat>
            <HudValue>{g.score.toLocaleString()}</HudValue>
            <HudLabel>Score</HudLabel>
          </HudStat>
          <HudStat>
            <ComboValue
              $animate={comboAnimKey > 0}
              key={`combo-${comboAnimKey}`}
            >
              x{g.combo}
            </ComboValue>
            <HudLabel>Combo</HudLabel>
          </HudStat>
          <HudStat>
            <HudValue>{accuracy}%</HudValue>
            <HudLabel>Accuracy</HudLabel>
          </HudStat>
          <HudStat>
            <HudValue>{wpm}</HudValue>
            <HudLabel>WPM</HudLabel>
          </HudStat>
          <HudStat>
            <HudValue>{g.totalMiss}</HudValue>
            <HudLabel>Misses</HudLabel>
          </HudStat>
        </HUD>

        <GameArea>
          {isVideo && (
            <BackgroundVideo
              ref={videoRef}
              src={audioUrl || undefined}
              preload="auto"
              playsInline
              style={{ opacity: backgroundOpacity / 100 }}
            />
          )}
          {phase === "playing" &&
            g.displayedLineIdx < 0 &&
            gameLines.length > 0 && (
              <>
                <UpcomingWrap>
                  <UpcomingLabel>Next</UpcomingLabel>
                  <UpcomingText>
                    {gameLines[0] && gameLines[0].content.trim() === ""
                      ? "[INTERMISSION]"
                      : gameLines[0]?.content ?? ""}
                  </UpcomingText>
                </UpcomingWrap>
                <CurrentWrap style={{ position: "relative" }}>
                  <LineTimingRow>
                    <LineTimingMeta>
                      Time to first line:{" "}
                      <LineTimingValue>
                        {Math.max(0, intermissionData.remainingMs / 1000).toFixed(1)}s
                      </LineTimingValue>
                    </LineTimingMeta>
                  </LineTimingRow>
                  <LineTimingBar>
                    <LineTimingFill $pct={intermissionData.pct} />
                  </LineTimingBar>
                  <CharRow ref={charRowRef} />
                  <CompletedLineFade>[INTERMISSION]</CompletedLineFade>
                </CurrentWrap>
              </>
            )}
          {g.displayedLineIdx >= 0 && gameLines[g.displayedLineIdx] && (
            <>
              <UpcomingWrap>
                <UpcomingLabel>Next</UpcomingLabel>
                <UpcomingText>
                  {gameLines[g.displayedLineIdx + 1] &&
                  gameLines[g.displayedLineIdx + 1].content.trim() === ""
                    ? "[INTERMISSION]"
                    : gameLines[g.displayedLineIdx + 1]?.content ?? ""}
                </UpcomingText>
              </UpcomingWrap>
              <CurrentWrap style={{ position: "relative" }}>
                <LineTimingRow>
                  <LineTimingMeta>
                    Time left:{" "}
                    <LineTimingValue>
                      {Math.max(0, lineRemainingMs / 1000).toFixed(1)}s
                    </LineTimingValue>
                  </LineTimingMeta>
                  {gameLines[g.displayedLineIdx].content.trim() !== "" && (
                    <LineTimingMeta>
                      Estimated CPS:{" "}
                      <LineTimingValue>
                        {calculateCPSNeeded(
                          gameLines[g.displayedLineIdx].content,
                          currentLineTime / 1000
                        ).toFixed(1)}
                      </LineTimingValue>
                    </LineTimingMeta>
                  )}
                </LineTimingRow>
                <LineTimingBar>
                  <LineTimingFill $pct={lineTimingPct} />
                </LineTimingBar>
                <CharRow ref={charRowRef}>
                  {gameLines[g.displayedLineIdx].content.trim() !== "" &&
                    (() => {
                      const rawText = gameLines[g.displayedLineIdx].content;
                      const text = rawText.toLowerCase();
                      const tokens = text.split(/(\s+)/).filter(Boolean);
                      let renderIndex = 0;
                      return tokens.flatMap((token, tokenIdx) => {
                        if (/^\s+$/.test(token)) {
                          return token.split("").map((ch, spaceIdx) => {
                            let state: "typed" | "active" | "pending" | "wrong";
                            if (renderIndex < g.typedCount) state = "typed";
                            else if (renderIndex === g.typedCount)
                              state = wrongChar ? "wrong" : "active";
                            else state = "pending";
                            const charIndex = renderIndex;
                            const showIndicator =
                              ch === " " &&
                              wrapSpaceIndicators[charIndex] &&
                              state !== "typed";
                            const displayChar =
                              ch === " "
                                ? showIndicator
                                  ? "␣"
                                  : "\u00A0"
                                : ch;
                            const element = (
                              <CharBox
                                key={`space-${tokenIdx}-${spaceIdx}`}
                                $state={state}
                                ref={(el) => {
                                  charRefs.current[charIndex] = el;
                                }}
                              >
                                {displayChar}
                              </CharBox>
                            );
                            renderIndex += 1;
                            return element;
                          });
                        }

                        const wordChars = token.split("").map((ch, charIdx) => {
                          let state: "typed" | "active" | "pending" | "wrong";
                          if (renderIndex < g.typedCount) state = "typed";
                          else if (renderIndex === g.typedCount)
                            state = wrongChar ? "wrong" : "active";
                          else state = "pending";
                          const charIndex = renderIndex;
                          const element = (
                            <CharBox
                              key={`char-${tokenIdx}-${charIdx}`}
                              $state={state}
                              ref={(el) => {
                                charRefs.current[charIndex] = el;
                              }}
                            >
                              {ch}
                            </CharBox>
                          );
                          renderIndex += 1;
                          return element;
                        });

                        return (
                          <WordWrap key={`word-${tokenIdx}`}>
                            {wordChars}
                          </WordWrap>
                        );
                      });
                    })()}
                </CharRow>
                {clearShowing && <ClearToast>CLEAR!</ClearToast>}
                <CompletedLineFade>
                  {gameLines[g.displayedLineIdx].content.trim() === ""
                    ? "[INTERMISSION]"
                    : g.lineCompleted
                      ? "Cleared - waiting for next line..."
                      : gameLines[g.displayedLineIdx].content}
                </CompletedLineFade>
              </CurrentWrap>
            </>
          )}
          {phase === "idle" && (
            <GetReadyText style={{ opacity: 0.3 }}>
              Start the game to begin typing
            </GetReadyText>
          )}
        </GameArea>

        <GameFooter>
          <ControlBtn onClick={handleRestart} title="Restart">
            <FaRedo />
          </ControlBtn>
          <ProgressWrap>
            <ProgressFill $pct={progressPct} />
          </ProgressWrap>
          <TimeText>
            {formatTime(Math.max(0, currentMs))} / {formatTime(duration)}
          </TimeText>
        </GameFooter>
      </GameContent>
    </GameRoot>
  );
}

export default function GamePage() {
  return (
    <>
      <GameGlobalStyle />
      <Suspense
        fallback={
          <GameRoot>
            <div
              style={{
                flex: 1,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: 18,
                color: "rgba(255,255,255,0.5)",
              }}
            >
              Loading...
            </div>
          </GameRoot>
        }
      >
        <GameInner />
      </Suspense>
    </>
  );
}
