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

import { Board, type BoardHint } from "./components/Board";
import { Card, CardBack } from "./components/Card";
import { ResultCard } from "./components/ResultCard";
import { describeAction } from "./game/actions";
import { cardColor, cardName } from "./game/cards";
import {
  boardScore,
  currentHandSorted,
  decodeAction,
  encodeAction,
  legalActionMask,
  previewPlacement,
  resetFromOrder,
  step,
} from "./game/engine";
import { deckOrderFromSeed, normalizeSeed, randomSeed } from "./game/random";
import { DRAW_DECK, N_CARDS, PLAY, type GameState, type PlaceType } from "./game/types";
import { fallbackHeuristicPolicy, loadPolicy, type Policy } from "./model/policy";
import { useCardMotion } from "./ui/useCardMotion";

interface Hint {
  action: number;
  text: string;
  probability: number | null;
}

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: GameState;
  selection: Selection;
}

// 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. */
function initialSeed(): string {
  const fromUrl = new URLSearchParams(window.location.search).get("seed");
  const seed = fromUrl === null ? "" : normalizeSeed(fromUrl);
  return seed === "" ? randomSeed() : seed;
}

function openingFrame(seed: string): Frame {
  return { state: resetFromOrder(deckOrderFromSeed(seed)), 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 App() {
  const [seed, setSeed] = useState<string>(initialSeed);
  const [frames, setFrames] = useState<Frame[]>(() => [openingFrame(seed)]);
  const [cursor, setCursor] = useState(0);
  const [seedDraft, setSeedDraft] = useState("");
  const [resultOpen, setResultOpen] = useState(true);
  const [policy, setPolicy] = useState<Policy | null>(null);
  const [modelMessage, setModelMessage] = useState("LOADING FINAL PPO");
  const [thinking, setThinking] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth);
  const [hint, setHint] = useState<Hint | null>(null);
  const [hintPending, setHintPending] = useState(false);
  const generation = useRef(0);
  const { cardRef, deckRef, overlayRef, resetMotion } = useCardMotion();

  const { state, selection } = frames[cursor];
  const canUndo = cursor > 0;
  const canRedo = cursor < frames.length - 1;
  const latestState = useRef(state);

  useEffect(() => { publishSeed(seed); }, [seed]);
  useEffect(() => { latestState.current = state; }, [state]);

  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(() => legalActionMask(state), [state]);
  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],
  );
  const scores = useMemo(() => boardScore(displayState), [displayState]);

  useEffect(() => {
    let cancelled = false;
    loadPolicy().then(({ policy: loaded, warning }) => {
      if (cancelled) return;
      setPolicy(loaded);
      setModelMessage(warning ?? `${loaded.provider.toUpperCase()} · FINAL PPO`);
    });
    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(() => {
    if (!policy || rivalSuspended || state.done || state.toMove !== 1) return;
    const currentGeneration = generation.current;
    const timer = window.setTimeout(async () => {
      setThinking(true);
      try {
        let action: number;
        try {
          action = await policy.action(state);
        } catch (error) {
          console.error("AI action failed; falling back to heuristic policy", error);
          const fallback = fallbackHeuristicPolicy();
          action = await fallback.action(state);
          if (generation.current !== currentGeneration) return;
          setPolicy(fallback);
          setModelMessage("HEURISTIC FALLBACK (MODEL ERROR)");
        }
        if (generation.current !== currentGeneration) return;
        pushFrame({ state: step(state, action), selection: EMPTY_SELECTION });
      } catch (error) {
        console.error("AI action failed even with heuristic fallback", error);
        setModelMessage("MODEL INFERENCE ERROR");
      } finally {
        if (generation.current === currentGeneration) setThinking(false);
      }
    }, 620);
    return () => window.clearTimeout(timer);
  }, [policy, rivalSuspended, state]);

  /** 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()) {
    generation.current += 1;
    resetMotion();
    setSeed(nextSeed);
    setFrames([openingFrame(nextSeed)]);
    setCursor(0);
    setSeedDraft("");
    setResultOpen(true);
    setHint(null);
    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);
    setHint(null);
    setThinking(false);
    setMenuOpen(false);
  }

  const undo = () => moveCursor(cursor - 1);
  const redo = () => moveCursor(cursor + 1);

  /** Leave the reviewed position as the live one and let the rival play on. */
  function resumeFromHere() {
    setFrames((items) => items.slice(0, cursor + 1));
    setMenuOpen(false);
  }

  /** Ask the policy driving the rival what it would do in your seat. */
  async function requestHint() {
    if (!policy || state.done || state.toMove !== 0 || hintPending) return;
    const position = state;
    setHintPending(true);
    setMenuOpen(false);
    try {
      const ranked = await policy.rank(position).catch(async (error) => {
        console.error("hint inference failed; falling back to heuristic", error);
        return fallbackHeuristicPolicy().rank(position);
      });
      const [best] = ranked;
      // The position can move on while inference runs — a hint for a stale
      // position would point at the wrong hand slot.
      if (best === undefined || latestState.current !== position) return;
      setHint({
        action: best.action,
        text: describeAction(position, best.action),
        probability: best.probability,
      });
    } catch (error) {
      console.error("hint failed", error);
    } finally {
      setHintPending(false);
    }
  }

  /** A hint answers one position; drop it as soon as anything is done or undone. */
  useEffect(() => { setHint(null); }, [cursor, frames]);

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

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

  function cancelPlace() {
    if (selection.handSlot === null || selection.placeType === null) return;
    pushFrame({ state, 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: step(state, action), selection: EMPTY_SELECTION });
  }

  // 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 hintMove = hint === null ? null : decodeAction(hint.action);
  const hintedCard = hintMove === null ? undefined : humanHand[hintMove.handSlot];
  const boardHint: BoardHint | null = hintMove === null || hintedCard === undefined ? null : {
    color: cardColor(hintedCard),
    place: hintMove.placeType,
    drawSource: hintMove.drawSource,
  };
  const hintConfidence = hint !== null && hint.probability !== null
    ? `${Math.round(hint.probability * 100)}%`
    : null;
  const canHint = policy !== null && !state.done && state.toMove === 0;

  const outcome = scores[0] === scores[1]
    ? "DRAW"
    : scores[0] > scores[1] ? "YOU WIN" : "THE RIVAL WINS";
  // 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 && (state.done || state.toMove === 1);
  const status = reviewingRival
    ? `Reviewing ply ${state.stepCount} — redo, or play on from here`
    : state.done
      ? outcome
      : state.toMove === 1
        ? thinking ? "The rival is thinking ···" : "The rival's turn"
        : selection.handSlot === null
          ? "Play or discard a card"
          : selection.placeType === null
            ? `Choose a destination for ${cardName(selectedCard!)}`
            : "Draw — deck or a discard pile";

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

      <div className="opponent-hand" aria-label="Rival hand">
        {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)}>MENU</button>
        {menuOpen && (
          <div className="menu-popover">
            <button onClick={() => restart()}>NEW GAME</button>
            <button onClick={() => restart(seed)}>REPLAY THIS SEED</button>
            <label className="menu-seed">
              <span>DEAL SEED · {seed}</span>
              <input
                value={seedDraft}
                placeholder="load a deal seed…"
                onChange={(event) => setSeedDraft(event.target.value)}
                onKeyDown={(event) => { if (event.key === "Enter") loadSeed(); }}
                aria-label="Load a deal by seed"
              />
            </label>
            <button onClick={loadSeed} disabled={normalizeSeed(seedDraft) === ""}>DEAL THIS SEED</button>
            <span>THE SEED SHUFFLES THE DECK ONLY · THE POLICY IS DETERMINISTIC</span>
            <span>{policy ? `${policy.provider.toUpperCase()} POLICY` : "LOADING POLICY"}</span>
          </div>
        )}
      </div>

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

      <button
        type="button"
        className={`deck-stack ${legalDraw(DRAW_DECK) ? "is-draw-target" : ""} ${hintMove?.drawSource === DRAW_DECK ? "is-hinted" : ""}`}
        disabled={!legalDraw(DRAW_DECK)}
        onClick={() => commit(DRAW_DECK)}
        aria-label={`Draw from deck, ${N_CARDS - state.drawPtr} cards left`}
      >
        <CardBack innerRef={deckRef} />
        <strong>{N_CARDS - state.drawPtr}</strong>
        <span>CARDS LEFT</span>
      </button>

      <div className="prompt-row">
        <p className={`turn-prompt ${thinking ? "turn-prompt--thinking" : ""} ${reviewingRival ? "turn-prompt--review" : ""}`}>
          {hint ? `Hint — ${hint.text}${hintConfidence ? ` (${hintConfidence})` : ""}` : status}
        </p>
        <button
          type="button"
          className={`hint-button ${hint ? "is-active" : ""}`}
          onClick={requestHint}
          disabled={!canHint || hintPending}
        >
          {hintPending ? "THINKING…" : "HINT"}
        </button>
      </div>

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

      <section className="score-plaque score-plaque--human" aria-label={`Your score ${scores[0]}`}>
        <div><strong>YOU <i /></strong><small>{state.toMove === 0 && !state.done ? "YOUR TURN" : "EXPEDITION LEAD"}</small></div><b>{scores[0]}</b>
      </section>

      <div className="history-bar">
        <button type="button" onClick={undo} disabled={!canUndo} aria-label="Undo one action" title="Undo (←)">
          ↶ <span>UNDO</span>
        </button>
        <button type="button" onClick={redo} disabled={!canRedo} aria-label="Redo one action" title="Redo (→)">
          ↷ <span>REDO</span>
        </button>
        {rivalSuspended && (
          <button type="button" className="history-bar__resume" onClick={resumeFromHere}>
            PLAY FROM HERE
          </button>
        )}
        {state.done && !resultOpen && (
          <button type="button" onClick={() => setResultOpen(true)}>SCORE</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" />

      {state.done && resultOpen && (
        <ResultCard
          state={state}
          outcome={outcome}
          seed={seed}
          onReview={() => setResultOpen(false)}
          onPlayAgain={() => restart()}
        />
      )}
    </main>
  );
}

export default App;
