import { useEffect, useMemo, useRef, useState } from "react";

import { Board } from "./components/Board";
import { Card, CardBack } from "./components/Card";
import { ResultCard, RoundOverlay } from "./components/Scoreboard";
import { cardColor, cardName } from "./game/cards";
import {
  boardScore,
  currentHandSorted,
  decodeAction,
  encodeAction,
  previewPlacement,
} from "./game/engine";
import {
  N_ROUNDS,
  matchAdvanceRound,
  matchFromOrders,
  matchLegalActionMask,
  matchScore,
  matchStep,
  roundScore,
  type MatchState,
} from "./game/match";
import { MODEL_CODENAME, MODEL_HASH } from "./model/policy";
import { matchFromSeed, normalizeSeed, randomSeed } from "./game/random";
import { parseSavedGame, SAVED_GAME_KEY, type SavedGame } from "./game/persistence";
import { DRAW_DECK, N_CARDS, PLAY, type PlaceType } from "./game/types";
import { fallbackHeuristicPolicy, loadPolicy, type Policy } from "./model/policy";
import { useCardMotion } from "./ui/useCardMotion";

/** One deal, or the classic three-round match decided on the summed total. */
export type Mode = 1 | 3;

const MODE_KEY = "lost-cities.mode";

function loadMode(): Mode {
  const requested = new URLSearchParams(window.location.search).get("rounds");
  if (requested === "3") return 3;
  if (requested === "1") return 1;
  // The classic three-round match is the default; a stored "1" opts down to a deal.
  return window.localStorage.getItem(MODE_KEY) === "1" ? 1 : 3;
}

// Off by default: taking moves back is a training aid, not how the game is played,
// and the default should be the honest game. When on, it is limited to the current
// turn (see turnFloor) -- you can revise a misclick, not rewind the rival.
const UNDO_KEY = "lost-cities.undo";

function loadUndoEnabled(): boolean {
  return window.localStorage.getItem(UNDO_KEY) === "on";
}

type Selection = { handSlot: number | null; placeType: PlaceType | null };
const EMPTY_SELECTION: Selection = { handSlot: null, placeType: null };

/**
 * One undoable step. A turn is several of these — picking a card, choosing where
 * it goes, then drawing — and each is undone on its own, so undo never throws
 * away a whole turn's thinking. A rival move is one frame too, which is what
 * makes a finished game reviewable ply by ply.
 */
interface Frame {
  state: MatchState;
  selection: Selection;
  move?: { player: 0 | 1; action: number };
}

// The hand row is centered, so the binding constraint is the score plaque
// column on the left (mirrored as a keep-out on the right to stay centered).
// Available width for the hand = viewport - 2 * (plaque right edge + gutter).
// Card geometry must mirror styles.css (.card / the max-width:1250 override).
const HAND_GUTTER = 14;
const HAND_LAYOUTS = [
  // maxWidth is the CSS breakpoint; keepOut = plaque left inset + plaque width.
  { maxWidth: 1250, cardWidth: 98, keepOut: 16 + 152, minVisible: 62 },
  { maxWidth: Infinity, cardWidth: 116, keepOut: 24 + 236, minVisible: 72 },
];

function handLayout(viewportWidth: number) {
  return HAND_LAYOUTS.find((entry) => viewportWidth <= entry.maxWidth) ?? HAND_LAYOUTS[1];
}

/** Horizontal advance per hand card: as roomy as the viewport allows, never
 *  below the readability floor, never wider than a fully-gapped hand. */
function handCardStep(viewportWidth: number, count: number): number {
  const { cardWidth, keepOut, minVisible } = handLayout(viewportWidth);
  const relaxed = cardWidth + 12;
  if (count <= 1) return relaxed;
  const available = Math.max(0, viewportWidth - 2 * (keepOut + HAND_GUTTER));
  const fitted = Math.floor((available - cardWidth) / (count - 1));
  return Math.max(minVisible, Math.min(relaxed, fitted));
}

/** A `?seed=` in the URL loads that exact deal, so a game can be shared or replayed.
 *  In match mode the seed fixes all three deals and the coin flips too, so the
 *  whole match is a pure function of it. */
function openingFrame(seed: string, mode: Mode): Frame {
  const { deckOrders, coinFlips } = matchFromSeed(seed);
  return { state: matchFromOrders(deckOrders, coinFlips, mode), selection: EMPTY_SELECTION };
}

/** Keep the address bar in sync so the current deal stays shareable. */
function publishSeed(seed: string): void {
  const url = new URL(window.location.href);
  url.searchParams.set("seed", seed);
  window.history.replaceState(null, "", url);
}

function newGameId(): string {
  if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
  if (typeof crypto.getRandomValues === "function") {
    const bytes = crypto.getRandomValues(new Uint8Array(16));
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
    return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
  }
  return `game-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

function initialGame(): Pick<SavedGame, "seed" | "frames" | "cursor" | "resultOpen" | "mode"> {
  const requested = new URLSearchParams(window.location.search).get("seed");
  const mode = loadMode();
  const saved = parseSavedGame(window.localStorage.getItem(SAVED_GAME_KEY));
  const requestedSeed = requested === null ? null : normalizeSeed(requested);
  // A saved game only resumes into the mode it was played in.
  if (saved && saved.mode === mode && (requestedSeed === null || requestedSeed === saved.seed)) {
    return saved;
  }
  const seed = requestedSeed || randomSeed();
  return { seed, mode, frames: [openingFrame(seed, mode)], cursor: 0, resultOpen: true };
}

function App() {
  const [initial] = useState(initialGame);
  const [seed, setSeed] = useState(initial.seed);
  const [mode, setMode] = useState<Mode>(initial.mode as Mode);
  const [frames, setFrames] = useState<Frame[]>(initial.frames);
  const [cursor, setCursor] = useState(initial.cursor);
  const [seedDraft, setSeedDraft] = useState("");
  const [resultOpen, setResultOpen] = useState(initial.resultOpen);
  const [policy, setPolicy] = useState<Policy | null>(null);
  const [modelMessage, setModelMessage] = useState("BOREALIS 불러오는 중");
  const [thinking, setThinking] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth);
  const [undoEnabled, setUndoEnabled] = useState(loadUndoEnabled);
  const generation = useRef(0);
  const gameId = useRef(newGameId());
  const startedAt = useRef(new Date().toISOString());
  const loggedGame = useRef<string | null>(null);
  const { cardRef, deckRef, overlayRef, resetMotion } = useCardMotion();

  const { state: match, selection } = frames[cursor];
  // The board on screen is the round in play; the match is what decides the game.
  const state = match.round;
  // The start of the current turn: the most recent committed move at or before the
  // cursor (a rival move, or your own draw). Undo cannot cross it, so you can take
  // back a card selection or placement within your turn but never rewind into the
  // rival's move or an earlier turn.
  const turnFloor = useMemo(() => {
    for (let index = cursor; index > 0; index -= 1) {
      if (frames[index].move) return index;
    }
    return 0;
  }, [frames, cursor]);
  const canUndo = undoEnabled && cursor > turnFloor;
  const canRedo = undoEnabled && cursor < frames.length - 1;
  const latestMatch = useRef(match);

  useEffect(() => { publishSeed(seed); }, [seed]);
  useEffect(() => { latestMatch.current = match; }, [match]);
  useEffect(() => { window.localStorage.setItem(MODE_KEY, String(mode)); }, [mode]);
  useEffect(() => {
    window.localStorage.setItem(UNDO_KEY, undoEnabled ? "on" : "off");
  }, [undoEnabled]);
  useEffect(() => {
    const saved: SavedGame = { version: 2, seed, mode, frames, cursor, resultOpen };
    window.localStorage.setItem(SAVED_GAME_KEY, JSON.stringify(saved));
  }, [cursor, frames, mode, resultOpen, seed]);

  useEffect(() => {
    function onResize() { setViewportWidth(window.innerWidth); }
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  const humanHand = useMemo(() => currentHandSorted(state, 0), [state]);
  const opponentHand = useMemo(() => currentHandSorted(state, 1), [state]);
  const legal = useMemo(() => matchLegalActionMask(match), [match]);
  const selectedCard = selection.handSlot === null ? null : humanHand[selection.handSlot];
  const selectedColor = selectedCard === null ? null : cardColor(selectedCard);
  const displayState = useMemo(
    () => selection.handSlot === null || selection.placeType === null
      ? state
      : previewPlacement(state, selection.handSlot, selection.placeType),
    [selection, state],
  );
  /** This round's board. */
  const roundScores = useMemo(() => boardScore(displayState), [displayState]);
  /** Rounds already banked plus the board in play — the number that decides a match. */
  const scores = useMemo(
    (): [number, number] => [
      match.carry[0] + roundScores[0],
      match.carry[1] + roundScores[1],
    ],
    [match.carry, roundScores],
  );

  useEffect(() => {
    let cancelled = false;
    loadPolicy().then(({ policy: loaded, warning }) => {
      if (cancelled) return;
      setPolicy(loaded);
      setModelMessage(warning ?? "BOREALIS");
    });
    return () => { cancelled = true; };
  }, []);

  /** Record a new step and make it the position on screen. Anything that had
   *  been undone is discarded — acting is what branches the game. */
  function pushFrame(frame: Frame) {
    setFrames((items) => [...items.slice(0, cursor + 1), frame]);
    setCursor(cursor + 1);
  }

  // Suspended while there are undone moves ahead: otherwise stepping back into
  // the rival's turn would instantly replay the very move being reviewed.
  const rivalSuspended = canRedo;

  useEffect(() => {
    // No move while a finished round is being shown -- the player advances it.
    if (!policy || rivalSuspended || match.done || match.roundComplete || state.toMove !== 1) return;
    const currentGeneration = generation.current;
    const timer = window.setTimeout(async () => {
      setThinking(true);
      try {
        let action: number;
        try {
          action = await policy.action(match);
        } catch (error) {
          console.error("AI action failed; falling back to heuristic policy", error);
          const fallback = fallbackHeuristicPolicy();
          action = await fallback.action(match);
          if (generation.current !== currentGeneration) return;
          setPolicy(fallback);
          setModelMessage("휴리스틱 대체 (모델 오류)");
        }
        if (generation.current !== currentGeneration) return;
        pushFrame({
          state: matchStep(match, action),
          selection: EMPTY_SELECTION,
          move: { player: 1, action },
        });
      } catch (error) {
        console.error("AI action failed even with heuristic fallback", error);
        setModelMessage("모델 추론 오류");
      } finally {
        if (generation.current === currentGeneration) setThinking(false);
      }
    }, 620);
    return () => window.clearTimeout(timer);
  }, [policy, rivalSuspended, match, state.toMove]);

  /** Deal the next round from the round-complete pause. */
  function advanceRound() {
    generation.current += 1;
    resetMotion();
    setFrames((items) => [...items.slice(0, cursor + 1), {
      state: matchAdvanceRound(match),
      selection: EMPTY_SELECTION,
    }]);
    setCursor(cursor + 1);
  }

  /** Deal a game. Without a seed this rolls a fresh one; the same seed always
   *  reproduces the same deal, so `restart(seed)` also replays the current one. */
  function restart(nextSeed: string = randomSeed(), nextMode: Mode = mode) {
    generation.current += 1;
    resetMotion();
    gameId.current = newGameId();
    startedAt.current = new Date().toISOString();
    loggedGame.current = null;
    setSeed(nextSeed);
    setMode(nextMode);
    setFrames([openingFrame(nextSeed, nextMode)]);
    setCursor(0);
    setSeedDraft("");
    setResultOpen(true);
    setThinking(false);
    setMenuOpen(false);
  }

  function loadSeed() {
    const requested = normalizeSeed(seedDraft);
    if (requested !== "") restart(requested);
  }

  function moveCursor(next: number) {
    if (next < 0 || next >= frames.length) return;
    generation.current += 1;
    setCursor(next);
    setThinking(false);
    setMenuOpen(false);
  }

  const undo = () => { if (canUndo) moveCursor(cursor - 1); };
  const redo = () => { if (canRedo) moveCursor(cursor + 1); };

  function chooseCard(handSlot: number) {
    if (state.toMove !== 0 || match.done) return;
    const next = selection.handSlot !== handSlot || selection.placeType !== null
      ? { handSlot, placeType: null }
      : EMPTY_SELECTION;
    pushFrame({ state: match, selection: next });
  }

  function choosePlace(placeType: PlaceType) {
    if (selection.handSlot === null) return;
    pushFrame({ state: match, selection: { ...selection, placeType } });
  }

  function cancelPlace() {
    if (selection.handSlot === null || selection.placeType === null) return;
    pushFrame({ state: match, selection: { ...selection, placeType: null } });
  }

  useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.target instanceof HTMLInputElement) return;
      if (event.key === "Escape") undo();
      if (event.key === "ArrowLeft") undo();
      if (event.key === "ArrowRight") redo();
    }
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  });

  function legalDraw(drawSource: number): boolean {
    if (selection.handSlot === null || selection.placeType === null) return false;
    return legal[encodeAction(selection.handSlot, selection.placeType, drawSource)];
  }

  function commit(drawSource: number) {
    if (!legalDraw(drawSource) || selection.handSlot === null || selection.placeType === null) return;
    const action = encodeAction(selection.handSlot, selection.placeType, drawSource);
    pushFrame({
      state: matchStep(match, action),
      selection: EMPTY_SELECTION,
      move: { player: 0, action },
    });
  }

  // Cards actually drawn in the hand row (the selected card moves into the
  // lane preview mid-placement); spacing is derived from this rendered list so
  // the leftmost visible card never carries a stale margin.
  const renderedHand = humanHand.flatMap((card, slot) =>
    selection.placeType !== null && selection.handSlot === slot ? [] : [{ card, slot }],
  );
  const handCardMarginLeft =
    handCardStep(viewportWidth, renderedHand.length) - handLayout(viewportWidth).cardWidth;

  const canPlaySelected = selection.handSlot !== null && Array.from(
    { length: 6 },
    (_, draw) => legal[encodeAction(selection.handSlot!, PLAY, draw)],
  ).some(Boolean);


  const outcome = scores[0] === scores[1]
    ? "무승부"
    : scores[0] > scores[1] ? "승리" : "패배";

  useEffect(() => {
    if (!match.done || loggedGame.current === gameId.current) return;
    loggedGame.current = gameId.current;
    const moves = frames.flatMap((frame, index) => {
      if (!frame.move || index === 0) return [];
      const beforeMatch = frames[index - 1].state;
      const afterMatch = frame.state;
      const before = beforeMatch.round;
      const after = afterMatch.round;
      const decoded = decodeAction(frame.move.action);
      const beforeHand = currentHandSorted(before, frame.move.player);
      // A move that ends a round is followed by a fresh deal, so the drawn card
      // has to be read from the hand it was drawn into, not from the next round's.
      const rolledOver = afterMatch.roundIdx !== beforeMatch.roundIdx;
      const afterHand = rolledOver ? beforeHand : currentHandSorted(after, frame.move.player);
      return [{
        ply: before.stepCount + 1,
        round: beforeMatch.roundIdx + 1,
        player: frame.move.player === 0 ? "human" : "ai",
        action: frame.move.action,
        playedCard: beforeHand[decoded.handSlot],
        placeType: decoded.placeType === PLAY ? "play" : "discard",
        drawSource: decoded.drawSource === DRAW_DECK ? "deck" : `discard_${decoded.drawSource - 1}`,
        drawnCard: rolledOver
          ? null
          : afterHand.find((card) => !beforeHand.includes(card)) ?? null,
        handsBefore: {
          human: currentHandSorted(before, 0),
          ai: currentHandSorted(before, 1),
        },
        scoresAfter: matchScore(afterMatch),
      }];
    });
    const record = {
      format: "lost-cities-web-game-v2",
      gameId: gameId.current,
      seed,
      // Which model actually played. The old records stored the on-screen label,
      // which stops identifying anything the moment there is a second model.
      opponent: { codename: MODEL_CODENAME, hash: MODEL_HASH },
      mode,
      startedAt: startedAt.current,
      finishedAt: new Date().toISOString(),
      deckOrders: match.deckOrders.slice(0, mode),
      coinFlips: match.coinFlips.slice(0, mode),
      moves,
      finalScores: scores,
      roundScores: match.roundHistory,
      outcome,
      finalState: match,
      policy: modelMessage,
    };
    void fetch("./api/game-records", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(record),
      keepalive: true,
    }).then((response) => {
      if (!response.ok) throw new Error(`game log HTTP ${response.status}`);
    }).catch((error) => {
      loggedGame.current = null;
      console.warn("Game record was not saved", error);
    });
  }, [frames, match, mode, modelMessage, outcome, scores, seed, state.done]);
  // While the rival is paused on its own turn (or on the final position) nothing
  // will happen until the move is redone or play is resumed — say so. On your own
  // turn the normal prompt still applies: you can simply play on from here.
  const reviewingRival = rivalSuspended && (match.done || state.toMove === 1);
  const status = reviewingRival
    ? "되돌리는 중 — 다시 진행하려면 앞으로"
    : match.done
      ? outcome
      : match.roundComplete
        ? `${match.roundIdx + 1}라운드 종료`
        : state.toMove === 1
          ? thinking ? "상대가 생각 중이에요" : "상대 차례"
          : selection.handSlot === null
            ? "낼 카드나 버릴 카드를 고르세요"
            : selection.placeType === null
              ? `${cardName(selectedCard!)} 놓을 곳을 고르세요`
              : "카드를 뽑으세요 — 덱 또는 버림패";

  return (
    <main className="app-shell">
      <section className="score-plaque score-plaque--rival" aria-label={`상대 점수 ${scores[1]}`}>
        <div><strong>상대</strong><small>{modelMessage}</small></div><b>{scores[1]}</b>
      </section>

      <div className="opponent-hand" aria-label="상대 손패">
        {opponentHand.map((card) => (
          state.handPublic[card]
            ? <Card mini card={card} key={card} innerRef={cardRef(card)} />
            : <CardBack key={card} innerRef={cardRef(card)} />
        ))}
      </div>

      <div className="menu-wrap">
        <button className="menu-button" onClick={() => setMenuOpen((open) => !open)}>메뉴</button>
        {menuOpen && (
          <div className="menu-popover">
            <button onClick={() => restart()}>새 게임</button>
            <button onClick={() => restart(seed)}>같은 시드로 다시</button>
            <div className="menu-modes" role="group" aria-label="게임 길이">
              <button
                className={mode === 3 ? "is-active" : ""}
                onClick={() => restart(randomSeed(), 3)}
              >
                3라운드 매치
              </button>
              <button
                className={mode === 1 ? "is-active" : ""}
                onClick={() => restart(randomSeed(), 1)}
              >
                단판
              </button>
            </div>
            <label className="menu-seed">
              <span>시드 · {seed}</span>
              <input
                value={seedDraft}
                placeholder="시드 입력…"
                onChange={(event) => setSeedDraft(event.target.value)}
                onKeyDown={(event) => { if (event.key === "Enter") loadSeed(); }}
                aria-label="시드로 딜 불러오기"
              />
            </label>
            <button onClick={loadSeed} disabled={normalizeSeed(seedDraft) === ""}>이 시드로 시작</button>
            <label className="menu-toggle">
              <input
                type="checkbox"
                checked={undoEnabled}
                onChange={(event) => setUndoEnabled(event.target.checked)}
              />
              <span>되돌리기 / 다시 · 내 턴 안에서만</span>
            </label>
          </div>
        )}
      </div>

      {mode === N_ROUNDS && (
        <div className="round-strip" aria-label={`${N_ROUNDS}라운드 중 ${match.roundIdx + 1}라운드`}>
          {Array.from({ length: N_ROUNDS }, (_, round) => (
            <span
              key={round}
              className={
                round < match.roundIdx ? "is-done" : round === match.roundIdx ? "is-live" : ""
              }
            >
              {round + 1}
            </span>
          ))}
          <em>누적 {match.carry[0]} : {match.carry[1]}</em>
        </div>
      )}

      <div className="table-center">
        <Board
          state={displayState}
          selectedColor={selectedColor}
          selectedPlace={selection.placeType}
          canPlay={canPlaySelected}
          canDraw={legalDraw}
          cardRef={cardRef}
          onChoosePlace={choosePlace}
          onCancelPlace={cancelPlace}
          onDraw={commit}
        />
      </div>

      <button
        type="button"
        className={`deck-stack ${legalDraw(DRAW_DECK) ? "is-draw-target" : ""}`}
        disabled={!legalDraw(DRAW_DECK)}
        onClick={() => commit(DRAW_DECK)}
        aria-label={`덱에서 뽑기, ${N_CARDS - state.drawPtr}장 남음`}
      >
        <CardBack innerRef={deckRef} />
        <strong>{N_CARDS - state.drawPtr}</strong>
        <span>남은 카드</span>
      </button>

      <div className="prompt-row">
        <p className={`turn-prompt ${thinking ? "turn-prompt--thinking" : ""} ${reviewingRival ? "turn-prompt--review" : ""}`}>
          {status}
        </p>
      </div>

      <section className="human-hand" aria-label="내 손패">
        {renderedHand.map(({ card, slot }, position) => (
          <Card
            card={card}
            key={card}
            selected={selection.handSlot === slot}
            disabled={state.toMove !== 0 || match.done}
            onClick={() => chooseCard(slot)}
            style={position === 0 ? undefined : { marginLeft: handCardMarginLeft }}
            innerRef={cardRef(card)}
          />
        ))}
      </section>

      <section className="score-plaque score-plaque--human" aria-label={`내 점수 ${scores[0]}`}>
        <div><strong>나 <i /></strong><small>{state.toMove === 0 && !match.done ? "내 차례" : "현재 점수"}</small></div><b>{scores[0]}</b>
      </section>

      <div className="control-stack">
        {undoEnabled && (
          <>
            <button type="button" onClick={undo} disabled={!canUndo} aria-label="한 단계 되돌리기" title="되돌리기 (←)">
              ↶ <span>되돌리기</span>
            </button>
            <button type="button" onClick={redo} disabled={!canRedo} aria-label="한 단계 다시" title="다시 (→)">
              ↷ <span>다시</span>
            </button>
          </>
        )}
        <button
          type="button"
          className={`control-stack__score ${match.done && !resultOpen ? "is-available" : ""}`}
          onClick={() => setResultOpen(true)}
          disabled={!match.done || resultOpen}
          aria-hidden={!match.done || resultOpen}
        >
          결과
        </button>
      </div>

      {/* Cards in flight back to the deck (undo of a draw) are re-parented here
          after React has already removed them from the table. */}
      <div className="motion-overlay" ref={overlayRef} aria-hidden="true" />

      {match.roundComplete && (
        <RoundOverlay
          state={state}
          roundIdx={match.roundIdx}
          totalRounds={mode}
          matchTotals={scores}
          onContinue={advanceRound}
        />
      )}

      {match.done && resultOpen && (
        <ResultCard
          state={state}
          outcome={outcome}
          roundHistory={mode > 1 ? match.roundHistory : [roundScore(match)]}
          totalRounds={mode}
          onPlayAgain={() => restart()}
        />
      )}
    </main>
  );
}

export default App;
