<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import Header from '../components/Header.svelte';
  import Modal from '../components/Modal.svelte';
  import Board from './Board.svelte';
  import Rack from './Rack.svelte';
  import MakeMove from './MakeMove.svelte';
  import Controls from './Controls.svelte';
  import Chat from './Chat.svelte';
  import { gateway } from '../lib/gateway';
  import { app, handleError, showToast } from '../lib/app.svelte';
  import { t } from '../lib/i18n/index.svelte';
  import type { ChatMessage, Direction, EvalResult, MoveRecord, StateView } from '../lib/model';
  import { lastPlayTiles, replay } from '../lib/board';
  import { alphabet, centre, premiumGrid } from '../lib/premiums';
  import {
    BLANK,
    direction,
    newPlacement,
    place,
    rackView,
    recallAt,
    reset,
    toSubmit,
    type Placement,
  } from '../lib/placement';

  let { id }: { id: string } = $props();

  let view = $state<StateView | null>(null);
  let moves = $state<MoveRecord[]>([]);
  let placement = $state<Placement>(newPlacement([]));
  let preview = $state<EvalResult | null>(null);
  let dirOverride = $state<Direction | undefined>(undefined);
  let busy = $state(false);
  let zoomed = $state(false);
  let selected = $state<number | null>(null);
  let panel = $state<'none' | 'chat' | 'history'>('none');
  let menuOpen = $state(false);
  let blankPrompt = $state<{ rackIndex: number; row: number; col: number } | null>(null);
  let exchangeOpen = $state(false);
  let exchangeSel = $state<number[]>([]);
  let checkOpen = $state(false);
  let checkWord = $state('');
  let checkResult = $state<{ word: string; legal: boolean } | null>(null);
  let resignOpen = $state(false);
  let messages = $state<ChatMessage[]>([]);
  let drag = $state<{ letter: string; blank: boolean; x: number; y: number } | null>(null);

  const variant = $derived(view?.game.variant ?? 'english');
  const board = $derived(replay(moves));
  const premium = $derived(premiumGrid(variant));
  const ctr = $derived(centre(variant));
  const pendingMap = $derived(
    new Map(placement.pending.map((p) => [`${p.row},${p.col}`, { letter: p.letter, blank: p.blank }])),
  );
  const recent = $derived(new Set(lastPlayTiles(moves).map((tt) => `${tt.row},${tt.col}`)));
  const slots = $derived(rackView(placement));
  const isMyTurn = $derived(
    !!view && view.game.status === 'active' && view.game.toMove === view.seat,
  );
  const gameOver = $derived(!!view && view.game.status !== 'active');
  const dir = $derived(dirOverride ?? direction(placement) ?? 'H');
  const ambiguous = $derived(placement.pending.length === 1);

  async function load() {
    try {
      const [st, hist] = await Promise.all([gateway.gameState(id), gateway.gameHistory(id)]);
      view = st;
      moves = hist.moves;
      placement = newPlacement(st.rack);
      preview = null;
      selected = null;
      dirOverride = undefined;
    } catch (e) {
      handleError(e);
    }
  }

  async function loadChat() {
    try {
      messages = await gateway.chatList(id);
    } catch (e) {
      handleError(e);
    }
  }

  onMount(load);

  $effect(() => {
    const e = app.lastEvent;
    if (!e) return;
    if ((e.kind === 'opponent_moved' || e.kind === 'your_turn') && e.gameId === id) void load();
    else if (e.kind === 'chat_message' && e.message.gameId === id && panel === 'chat') void loadChat();
    else if (e.kind === 'nudge' && e.gameId === id && panel === 'chat') void loadChat();
  });

  function isCoarse(): boolean {
    return typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches;
  }

  // --- tile placement: pointer drag + tap, both feeding the placement model ---
  let downInfo: { index: number; x0: number; y0: number } | null = null;
  let dragMoved = false;
  let swallowClick = false;

  function onRackDown(e: PointerEvent, index: number) {
    if (!isMyTurn || busy) return;
    downInfo = { index, x0: e.clientX, y0: e.clientY };
    dragMoved = false;
    window.addEventListener('pointermove', onWinMove);
    window.addEventListener('pointerup', onWinUp);
  }
  function onWinMove(e: PointerEvent) {
    if (!downInfo) return;
    if (!dragMoved && Math.hypot(e.clientX - downInfo.x0, e.clientY - downInfo.y0) > 6) {
      dragMoved = true;
      const slot = placement.rack[downInfo.index];
      drag = { letter: slot, blank: slot === BLANK, x: e.clientX, y: e.clientY };
      if (isCoarse() && !zoomed) zoomed = true; // auto zoom-in on touch placement
    }
    if (drag) drag = { ...drag, x: e.clientX, y: e.clientY };
  }
  function onWinUp(e: PointerEvent) {
    window.removeEventListener('pointermove', onWinMove);
    window.removeEventListener('pointerup', onWinUp);
    const di = downInfo;
    downInfo = null;
    if (drag && dragMoved && di) {
      const el = (document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null)?.closest(
        '[data-cell]',
      ) as HTMLElement | null;
      drag = null;
      if (el?.dataset.row && el.dataset.col) {
        attemptPlace(di.index, Number(el.dataset.row), Number(el.dataset.col));
      }
      swallowClick = true;
      setTimeout(() => (swallowClick = false), 60);
    } else if (di) {
      selected = selected === di.index ? null : di.index;
      drag = null;
    }
  }
  onDestroy(() => {
    window.removeEventListener('pointermove', onWinMove);
    window.removeEventListener('pointerup', onWinUp);
  });

  function onCell(row: number, col: number) {
    if (swallowClick) return;
    if (pendingMap.has(`${row},${col}`)) {
      placement = recallAt(placement, row, col);
      recompute();
      return;
    }
    if (selected != null) {
      attemptPlace(selected, row, col);
      selected = null;
    }
  }

  function attemptPlace(index: number, row: number, col: number) {
    if (board[row]?.[col]) return;
    if (pendingMap.has(`${row},${col}`)) return;
    if (placement.rack[index] === BLANK) {
      blankPrompt = { rackIndex: index, row, col };
      return;
    }
    placement = place(placement, index, row, col);
    recompute();
  }

  function chooseBlank(letter: string) {
    if (!blankPrompt) return;
    placement = place(placement, blankPrompt.rackIndex, blankPrompt.row, blankPrompt.col, letter);
    blankPrompt = null;
    recompute();
  }

  let previewTimer: ReturnType<typeof setTimeout> | null = null;
  function recompute() {
    preview = null;
    if (previewTimer) clearTimeout(previewTimer);
    const sub = toSubmit(placement, dirOverride);
    if (!sub) return;
    previewTimer = setTimeout(async () => {
      try {
        preview = await gateway.evaluate(id, sub.dir, sub.tiles);
      } catch {
        /* preview is best-effort */
      }
    }, 250);
  }

  async function commit() {
    const sub = toSubmit(placement, dirOverride);
    if (!sub) return;
    busy = true;
    try {
      await gateway.submitPlay(id, sub.dir, sub.tiles);
      await load();
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }

  function resetPlacement() {
    placement = reset(placement);
    preview = null;
    selected = null;
    dirOverride = undefined;
  }

  async function doPass() {
    busy = true;
    try {
      await gateway.pass(id);
      await load();
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }

  async function doResign() {
    resignOpen = false;
    busy = true;
    try {
      await gateway.resign(id);
      await load();
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }

  async function doHint() {
    try {
      const h = await gateway.hint(id);
      const word = h.move.words[0] ?? h.move.tiles.map((x) => x.letter).join('');
      showToast(t('game.hintShown', { word, n: h.move.score }));
      if (view) view = { ...view, hintsRemaining: h.hintsRemaining };
    } catch (e) {
      handleError(e);
    }
  }

  function shuffle() {
    if (placement.pending.length > 0) return;
    const r = [...placement.rack];
    for (let i = r.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [r[i], r[j]] = [r[j], r[i]];
    }
    placement = newPlacement(r);
  }

  function toggleDir() {
    dirOverride = dir === 'H' ? 'V' : 'H';
    recompute();
  }

  function openExchange() {
    menuOpen = false;
    resetPlacement();
    exchangeSel = [];
    exchangeOpen = true;
  }
  function toggleExch(i: number) {
    exchangeSel = exchangeSel.includes(i) ? exchangeSel.filter((x) => x !== i) : [...exchangeSel, i];
  }
  async function doExchange() {
    if (!view || exchangeSel.length === 0) return;
    const tiles = exchangeSel.map((i) => view!.rack[i]);
    exchangeOpen = false;
    busy = true;
    try {
      await gateway.exchange(id, tiles);
      await load();
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }

  function openCheck() {
    menuOpen = false;
    checkWord = '';
    checkResult = null;
    checkOpen = true;
  }
  async function runCheck() {
    const w = checkWord.trim();
    if (!w) return;
    try {
      checkResult = await gateway.checkWord(id, w);
    } catch (e) {
      handleError(e);
    }
  }
  async function complain() {
    if (!checkResult) return;
    try {
      await gateway.complaint(id, checkResult.word, '');
      showToast(t('game.complaintSent'));
    } catch (e) {
      handleError(e);
    }
  }

  function openChat() {
    menuOpen = false;
    panel = 'chat';
    void loadChat();
  }
  async function sendChat(text: string) {
    try {
      const m = await gateway.chatPost(id, text);
      messages = [...messages, m];
    } catch (e) {
      handleError(e);
    }
  }
  async function nudge() {
    try {
      const m = await gateway.nudge(id);
      messages = [...messages, m];
    } catch (e) {
      handleError(e);
    }
  }

  function resultText(): string {
    if (!view) return '';
    const me = view.game.seats[view.seat];
    if (me?.isWinner) return t('game.won');
    return view.game.seats.some((s) => s.isWinner) ? t('game.lost') : t('game.tied');
  }
</script>

<Header title={t('app.title')} back="/">
  {#snippet menu()}
    <button class="icon" onclick={() => (menuOpen = !menuOpen)} aria-label="Menu">≡</button>
    {#if menuOpen}
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_static_element_interactions -->
      <div class="backdrop" onclick={() => (menuOpen = false)}></div>
      <div class="dropdown">
        <button onclick={() => { menuOpen = false; panel = 'history'; }}>{t('game.history')}</button>
        <button onclick={openChat}>{t('game.chat')}</button>
        <button onclick={openCheck}>{t('game.checkWord')}</button>
        <button onclick={() => { menuOpen = false; resignOpen = true; }}>{t('game.dropGame')}</button>
      </div>
    {/if}
  {/snippet}
</Header>

{#if view}
  <div class="scoreboard">
    {#each view.game.seats as s (s.seat)}
      <div class="seat" class:turn={view.game.toMove === s.seat && !gameOver} class:win={s.isWinner}>
        <div class="nm">{s.accountId === app.session?.userId ? t('common.you') : s.displayName}</div>
        <div class="sc">{s.score}</div>
      </div>
    {/each}
  </div>

  <div class="boardwrap">
    <Board
      {board}
      {premium}
      pending={pendingMap}
      {recent}
      centre={ctr}
      {zoomed}
      {variant}
      oncell={onCell}
      ontogglezoom={() => (zoomed = !zoomed)}
    />
  </div>

  <div class="status">
    <span>{t('game.bag', { n: view.bagLen })}</span>
    {#if gameOver}
      <strong class="over">{t('game.over')} — {resultText()}</strong>
    {:else}
      <span class="turn-ind">{isMyTurn ? t('game.yourTurn') : t('game.waiting', { name: view.game.seats[view.game.toMove]?.displayName ?? '' })}</span>
    {/if}
    <span>{t('game.hints', { n: view.hintsRemaining })}</span>
  </div>

  {#if !gameOver}
    <div class="rack-row">
      <div class="rack-wrap">
        <Rack {slots} {variant} {selected} ondown={onRackDown} />
      </div>
      {#if placement.pending.length > 0}
        <MakeMove
          label={t('game.makeMove')}
          resetLabel={t('game.reset')}
          onmake={commit}
          onreset={resetPlacement}
        />
      {/if}
    </div>

    <Controls
      {preview}
      hints={view.hintsRemaining}
      busy={busy || !isMyTurn}
      {ambiguous}
      {dir}
      ondraw={openExchange}
      onskip={doPass}
      onshuffle={shuffle}
      onhint={doHint}
      ondir={toggleDir}
    />
  {/if}
{:else}
  <p class="loading">{t('common.loading')}</p>
{/if}

{#if drag}
  <div class="ghost" style="left:{drag.x}px; top:{drag.y}px">
    <span>{drag.blank ? '' : drag.letter}</span>
  </div>
{/if}

{#if blankPrompt}
  <Modal title={t('game.chooseBlank')} onclose={() => (blankPrompt = null)}>
    <div class="alpha">
      {#each alphabet(variant) as ch (ch)}
        <button onclick={() => chooseBlank(ch)}>{ch}</button>
      {/each}
    </div>
  </Modal>
{/if}

{#if exchangeOpen && view}
  <Modal title={t('game.exchangeTitle')} onclose={() => (exchangeOpen = false)}>
    <div class="exch">
      {#each view.rack as letter, i (i)}
        <button class="etile" class:sel={exchangeSel.includes(i)} onclick={() => toggleExch(i)}>
          {letter === BLANK ? '?' : letter}
        </button>
      {/each}
    </div>
    <button class="confirm" disabled={exchangeSel.length === 0} onclick={doExchange}>
      {t('game.exchangeConfirm', { n: exchangeSel.length })}
    </button>
  </Modal>
{/if}

{#if checkOpen}
  <Modal title={t('game.checkWord')} onclose={() => (checkOpen = false)}>
    <div class="check">
      <input placeholder={t('game.checkWordPrompt')} bind:value={checkWord} onkeydown={(e) => e.key === 'Enter' && runCheck()} />
      <button onclick={runCheck}>{t('game.checkWord')}</button>
    </div>
    {#if checkResult}
      <p class:ok={checkResult.legal} class:bad={!checkResult.legal}>
        {checkResult.legal
          ? t('game.wordLegal', { word: checkResult.word })
          : t('game.wordIllegal', { word: checkResult.word })}
      </p>
      <button class="complain" onclick={complain}>{t('game.complain')}</button>
    {/if}
  </Modal>
{/if}

{#if resignOpen}
  <Modal title={t('game.confirmResign')} onclose={() => (resignOpen = false)}>
    <div class="confirm-row">
      <button class="cancel" onclick={() => (resignOpen = false)}>{t('common.cancel')}</button>
      <button class="danger" onclick={doResign}>{t('game.dropGame')}</button>
    </div>
  </Modal>
{/if}

{#if panel === 'chat'}
  <Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
    <Chat {messages} myId={app.session?.userId ?? ''} {busy} onsend={sendChat} onnudge={nudge} />
  </Modal>
{/if}

{#if panel === 'history' && view}
  <Modal title={t('game.history')} onclose={() => (panel = 'none')}>
    <ol class="history">
      {#each moves as m, i (i)}
        <li>
          <span class="hp">{view.game.seats[m.player]?.displayName ?? m.player}</span>
          <span class="ha">{m.action === 'play' ? m.words.join(', ') : m.action}</span>
          <span class="hs">{m.score}</span>
        </li>
      {/each}
    </ol>
  </Modal>
{/if}

<style>
  .scoreboard {
    display: flex;
    gap: 2px;
    padding: 6px var(--pad);
    background: var(--bg-elev);
    border-bottom: 1px solid var(--border);
  }
  .seat {
    flex: 1;
    text-align: center;
    padding: 4px;
    border-radius: var(--radius-sm);
  }
  .seat.turn {
    background: var(--surface-2);
    outline: 1px solid var(--accent);
  }
  .seat.win .sc {
    color: var(--ok);
  }
  .nm {
    font-size: 0.8rem;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .sc {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  .boardwrap {
    padding: 8px;
  }
  .status {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--pad) 8px;
    color: var(--text-muted);
    font-size: 0.85rem;
  }
  .turn-ind {
    font-weight: 600;
    color: var(--text);
  }
  .over {
    color: var(--accent);
  }
  .rack-row {
    display: flex;
    gap: 8px;
    align-items: stretch;
    padding: 0 var(--pad);
  }
  .rack-wrap {
    flex: 1;
    min-width: 0;
  }
  :global(.rack-row .wrap) {
    display: flex;
  }
  .loading {
    text-align: center;
    color: var(--text-muted);
    padding: 40px;
  }
  .icon {
    background: none;
    border: none;
    color: var(--text);
    font-size: 1.3rem;
    line-height: 1;
  }
  .backdrop {
    position: fixed;
    inset: 0;
    z-index: 8;
  }
  .dropdown {
    position: absolute;
    right: 8px;
    top: 44px;
    z-index: 9;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    min-width: 160px;
    overflow: hidden;
  }
  .dropdown button {
    padding: 11px 14px;
    text-align: left;
    background: none;
    border: none;
    color: var(--text);
  }
  .dropdown button:hover {
    background: var(--surface-2);
  }
  .ghost {
    position: fixed;
    width: 40px;
    height: 40px;
    transform: translate(-50%, -50%);
    background: var(--tile-pending);
    color: var(--tile-text);
    border-radius: 5px;
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: 1.3rem;
    box-shadow: var(--shadow);
    pointer-events: none;
    z-index: 60;
  }
  .alpha {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 6px;
  }
  .alpha button {
    aspect-ratio: 1;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
    font-weight: 700;
  }
  .exch {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
    margin-bottom: 12px;
  }
  .etile {
    aspect-ratio: 1;
    border: 1px solid var(--border);
    background: var(--tile-bg);
    color: var(--tile-text);
    border-radius: 5px;
    font-weight: 700;
  }
  .etile.sel {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
  }
  .confirm {
    width: 100%;
    padding: 11px;
    background: var(--accent);
    color: var(--accent-text);
    border: none;
    border-radius: var(--radius-sm);
    font-weight: 700;
  }
  .confirm:disabled {
    opacity: 0.5;
  }
  .check {
    display: flex;
    gap: 6px;
  }
  .check input {
    flex: 1;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
  }
  .check button {
    padding: 10px 12px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
  }
  .ok {
    color: var(--ok);
  }
  .bad {
    color: var(--danger);
  }
  .complain {
    background: none;
    border: none;
    color: var(--accent);
    padding: 4px 0;
  }
  .confirm-row {
    display: flex;
    gap: 8px;
  }
  .confirm-row button {
    flex: 1;
    padding: 11px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font-weight: 600;
  }
  .danger {
    background: var(--danger) !important;
    color: #fff !important;
    border-color: var(--danger) !important;
  }
  .history {
    margin: 0;
    padding-left: 20px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .history li {
    display: flex;
    justify-content: space-between;
    gap: 10px;
  }
  .hp {
    color: var(--text-muted);
  }
  .ha {
    flex: 1;
    text-align: center;
  }
  .hs {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
  }
</style>
