const { useState, useEffect, useRef, useCallback, useMemo } = React;

/* ─────────────── session + socket ─────────────── */

function randomId() {
  return 'sid_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
}

function useSession() {
  const [sessionId] = useState(() => {
    let sid = localStorage.getItem('nertz.sessionId');
    if (!sid) {
      sid = randomId();
      localStorage.setItem('nertz.sessionId', sid);
    }
    return sid;
  });
  const [displayName, setDisplayName] = useState(
    () => localStorage.getItem('nertz.displayName') || ''
  );
  const save = useCallback((name) => {
    setDisplayName(name);
    localStorage.setItem('nertz.displayName', name);
  }, []);
  return { sessionId, displayName, setDisplayName: save };
}

function useGameSocket({ onMessage, getRejoinPayload }) {
  const wsRef = useRef(null);
  const onMessageRef = useRef(onMessage);
  onMessageRef.current = onMessage;
  const getRejoinPayloadRef = useRef(getRejoinPayload);
  getRejoinPayloadRef.current = getRejoinPayload;
  const pendingRef = useRef([]);
  const retryAttemptRef = useRef(0);
  const wasConnectedRef = useRef(false);
  const retryTimerRef = useRef(null);
  const mountedRef = useRef(true);
  const [connected, setConnected] = useState(false);
  const [reconnecting, setReconnecting] = useState(false);

  useEffect(() => {
    mountedRef.current = true;

    function computeBackoff(attempt) {
      const schedule = [500, 1000, 2000, 4000];
      return attempt < schedule.length ? schedule[attempt] : 5000;
    }

    function open() {
      if (!mountedRef.current) return;
      const proto = location.protocol === 'https:' ? 'wss' : 'ws';
      const ws = new WebSocket(`${proto}://${location.host}/ws`);
      wsRef.current = ws;

      ws.onopen = () => {
        setConnected(true);
        setReconnecting(false);
        retryAttemptRef.current = 0;
        if (wasConnectedRef.current) {
          const getter = getRejoinPayloadRef.current;
          const payload = getter ? getter() : null;
          if (payload) {
            ws.send(JSON.stringify({ type: 'join_table', ...payload }));
          }
        }
        wasConnectedRef.current = true;
        for (const queued of pendingRef.current) ws.send(queued);
        pendingRef.current = [];
      };

      ws.onclose = () => {
        setConnected(false);
        if (!mountedRef.current) return;
        setReconnecting(true);
        const delay = computeBackoff(retryAttemptRef.current);
        retryAttemptRef.current += 1;
        retryTimerRef.current = setTimeout(open, delay);
      };

      ws.onerror = () => { /* onclose follows */ };

      ws.onmessage = (ev) => {
        try {
          onMessageRef.current(JSON.parse(ev.data));
        } catch (err) {
          console.error('bad ws msg', err);
        }
      };
    }

    open();

    return () => {
      mountedRef.current = false;
      if (retryTimerRef.current) {
        clearTimeout(retryTimerRef.current);
        retryTimerRef.current = null;
      }
      const ws = wsRef.current;
      if (ws) {
        ws.onclose = null;
        ws.close();
      }
    };
  }, []);

  const send = useCallback((obj) => {
    const payload = JSON.stringify(obj);
    const ws = wsRef.current;
    if (ws && ws.readyState === 1) ws.send(payload);
    else pendingRef.current.push(payload);
  }, []);

  return { connected, reconnecting, send };
}

/* ─────────────── small UI atoms ─────────────── */

function Icon({ name, className = '' }) {
  return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
}

function BrandHeader({ right }) {
  return (
    <header className="flex justify-between items-center px-6 h-16 bg-[#0F3D2E]/80 backdrop-blur-xl border-b border-white/10 shadow-2xl">
      <h1 className="font-headline italic text-2xl tracking-tight text-secondary">
        Nertz Online
      </h1>
      <div className="flex items-center gap-3">{right}</div>
    </header>
  );
}

function PrimaryButton({ children, onClick, disabled, className = '' }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`bg-secondary text-on-secondary font-bold uppercase tracking-widest text-sm rounded-full px-6 py-3 shadow-[0_8px_20px_rgba(225,195,135,0.25)] hover:brightness-110 active:scale-95 transition disabled:opacity-40 disabled:cursor-not-allowed ${className}`}
    >
      {children}
    </button>
  );
}

function GhostButton({ children, onClick, className = '' }) {
  return (
    <button
      onClick={onClick}
      className={`bg-surface-container-low/60 backdrop-blur-md text-on-surface font-semibold uppercase tracking-widest text-sm rounded-full px-6 py-3 ghost-border hover:bg-surface-container-high transition ${className}`}
    >
      {children}
    </button>
  );
}

/* ─────────────── screens ─────────────── */

function HomeScreen({ session, onGoto, error }) {
  const [nameDraft, setNameDraft] = useState(session.displayName);
  useEffect(() => setNameDraft(session.displayName), [session.displayName]);

  const commitName = () => {
    const trimmed = nameDraft.trim();
    if (trimmed && trimmed !== session.displayName) {
      session.setDisplayName(trimmed);
    }
  };

  const hasName = nameDraft.trim().length > 0;

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader />
      <main className="flex-1 flex items-center justify-center px-6">
        <div className="w-full max-w-md flex flex-col gap-8 py-12">
          <div className="flex flex-col items-center gap-3 text-center">
            <div className="w-20 h-20 rounded-full bg-secondary/10 ghost-border flex items-center justify-center">
              <Icon name="playing_cards" className="text-secondary text-5xl" />
            </div>
            <h2 className="font-headline text-3xl text-on-surface">Race to empty your pile.</h2>
            <p className="text-on-surface-variant text-sm">
              Real-time multiplayer Nertz. 2–6 players per table.
            </p>
          </div>

          <div className="flex flex-col gap-2">
            <label className="text-xs uppercase tracking-widest text-on-surface-variant">
              Display Name
            </label>
            <input
              type="text"
              value={nameDraft}
              onChange={(e) => setNameDraft(e.target.value)}
              onBlur={commitName}
              placeholder="Your name at the table"
              className="bg-surface-container rounded-full px-5 py-3 ghost-border focus:outline-none focus:ring-2 focus:ring-secondary text-on-surface"
            />
          </div>

          {error && (
            <div className="text-error bg-error-container/30 ghost-border rounded-lg px-4 py-2 text-sm text-center">
              {error}
            </div>
          )}

          <div className="flex flex-col gap-3">
            <PrimaryButton
              disabled={!hasName}
              onClick={() => { commitName(); onGoto('create'); }}
            >
              Create Table
            </PrimaryButton>
            <GhostButton onClick={() => { commitName(); onGoto('join'); }}>
              Join Table
            </GhostButton>
            <button
              onClick={() => onGoto('leaderboard')}
              className="text-on-surface-variant hover:text-secondary text-sm uppercase tracking-widest transition"
            >
              View Leaderboard
            </button>
          </div>
        </div>
      </main>
    </div>
  );
}

function CreateTableScreen({ session, onCreated, onBack }) {
  const [targetScore, setTargetScore] = useState(100);
  const [maxPlayers, setMaxPlayers] = useState(4);
  const [stockFlipMode, setStockFlipMode] = useState(3);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  const submit = async () => {
    setBusy(true);
    setError(null);
    try {
      const res = await fetch('/api/tables', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          sessionId: session.sessionId,
          displayName: session.displayName,
          targetScore,
          maxPlayers,
          stockFlipMode
        })
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed');
      onCreated(data.table.code);
    } catch (err) {
      setError(err.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader
        right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>}
      />
      <main className="flex-1 flex items-center justify-center px-6">
        <div className="w-full max-w-lg flex flex-col gap-6 py-12">
          <h2 className="font-headline text-3xl text-on-surface">New Table</h2>

          <div className="flex flex-col gap-2">
            <label className="text-xs uppercase tracking-widest text-on-surface-variant">
              Target Score: <span className="text-secondary">{targetScore}</span>
            </label>
            <input
              type="range"
              min="50"
              max="200"
              step="10"
              value={targetScore}
              onChange={(e) => setTargetScore(Number(e.target.value))}
              className="accent-secondary"
            />
          </div>

          <div className="flex flex-col gap-2">
            <label className="text-xs uppercase tracking-widest text-on-surface-variant">
              Max Players
            </label>
            <div className="grid grid-cols-4 gap-2">
              {[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((n) => (
                <button
                  key={n}
                  onClick={() => setMaxPlayers(n)}
                  className={`rounded-full py-3 font-bold ghost-border transition ${
                    maxPlayers === n
                      ? 'bg-secondary text-on-secondary'
                      : 'bg-surface-container text-on-surface hover:bg-surface-container-high'
                  }`}
                >
                  {n}
                </button>
              ))}
            </div>
          </div>

          <div className="flex flex-col gap-2">
            <label className="text-xs uppercase tracking-widest text-on-surface-variant">
              Stock Flip
            </label>
            <div className="grid grid-cols-2 gap-2">
              {[1, 3].map((n) => (
                <button
                  key={n}
                  onClick={() => setStockFlipMode(n)}
                  className={`rounded-full py-3 font-bold ghost-border transition ${
                    stockFlipMode === n
                      ? 'bg-secondary text-on-secondary'
                      : 'bg-surface-container text-on-surface hover:bg-surface-container-high'
                  }`}
                >
                  Flip {n}
                </button>
              ))}
            </div>
          </div>

          {error && (
            <div className="text-error bg-error-container/30 ghost-border rounded-lg px-4 py-2 text-sm">
              {error}
            </div>
          )}

          <PrimaryButton onClick={submit} disabled={busy}>
            {busy ? 'Creating…' : 'Create Table'}
          </PrimaryButton>
        </div>
      </main>
    </div>
  );
}

function JoinTableScreen({ onJoin, onBack }) {
  const [code, setCode] = useState(() => {
    try { return localStorage.getItem('nertz.lastCode') || ''; } catch (_) { return ''; }
  });

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader
        right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>}
      />
      <main className="flex-1 flex items-center justify-center px-6">
        <div className="w-full max-w-md flex flex-col gap-6 py-12">
          <div className="flex flex-col items-center gap-2 text-center">
            <h2 className="font-headline text-3xl text-on-surface">Join a Table</h2>
            <p className="text-on-surface-variant text-sm">
              Enter the 6-character code your host shared.
            </p>
          </div>

          <input
            value={code}
            onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))}
            placeholder="ABC123"
            className="bg-surface-container rounded-2xl px-6 py-6 text-center text-4xl tracking-[0.5em] font-bold font-mono ghost-border focus:outline-none focus:ring-2 focus:ring-secondary text-secondary"
          />

          <PrimaryButton
            disabled={code.length !== 6}
            onClick={() => onJoin(code)}
          >
            Join
          </PrimaryButton>
        </div>
      </main>
    </div>
  );
}

function LobbyScreen({ table, you, onStart, onLeave }) {
  const isHost = you && table.hostPlayerId === you.playerId;
  const emptySeats = table.maxPlayers - table.seats.length;

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader
        right={
          <>
            <div className="text-secondary font-mono text-xl tracking-[0.3em] px-4 py-2 bg-surface-container-low/60 rounded-full ghost-border">
              {table.code}
            </div>
            <GhostButton onClick={onLeave} className="px-4 py-2 text-xs">Leave</GhostButton>
          </>
        }
      />
      <main className="flex-1 felt-texture dot-grid p-6">
        <div className="max-w-5xl mx-auto flex flex-col gap-8">
          <div className="text-center">
            <h2 className="font-headline text-3xl text-on-surface">Lobby</h2>
            <p className="text-on-surface-variant text-sm">
              Waiting for players ({table.seats.length} / {table.maxPlayers})
            </p>
          </div>

          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
            {table.seats.map((s) => (
              <div
                key={s.seat}
                className="glass-panel rounded-2xl p-5 flex flex-col items-center gap-3 relative"
              >
                {table.hostPlayerId === s.playerId && (
                  <Icon name="workspace_premium" className="absolute top-2 right-3 text-secondary text-xl" />
                )}
                <div className={`w-14 h-14 rounded-full flex items-center justify-center font-bold text-xl text-on-primary bg-${s.cardBackColor}-400`}
                  style={{ backgroundColor: colorHex(s.cardBackColor) }}
                >
                  {s.displayName.slice(0, 1).toUpperCase()}
                </div>
                <div className="text-center">
                  <div className="font-bold text-on-surface">{s.displayName}</div>
                  <div className="text-xs text-on-surface-variant uppercase tracking-widest">
                    {s.connected ? 'Ready' : 'Connecting…'}
                  </div>
                </div>
              </div>
            ))}
            {Array.from({ length: emptySeats }).map((_, i) => (
              <div
                key={`empty-${i}`}
                className="border-2 border-dashed border-outline-variant/40 rounded-2xl p-5 flex flex-col items-center justify-center gap-2 min-h-[160px] text-on-surface-variant/50"
              >
                <Icon name="person_add" className="text-3xl" />
                <span className="text-xs uppercase tracking-widest">Open Seat</span>
              </div>
            ))}
          </div>

          <div className="glass-panel rounded-2xl p-5 grid grid-cols-3 gap-4 text-center">
            <div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant">Target</div>
              <div className="font-headline text-2xl text-secondary">{table.targetScore}</div>
            </div>
            <div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant">Max</div>
              <div className="font-headline text-2xl text-secondary">{table.maxPlayers}</div>
            </div>
            <div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant">Flip</div>
              <div className="font-headline text-2xl text-secondary">{table.stockFlipMode}</div>
            </div>
          </div>

          {isHost ? (
            <PrimaryButton
              disabled={table.seats.length < 2}
              onClick={onStart}
            >
              {table.seats.length < 2 ? 'Waiting for another player…' : 'Start Round'}
            </PrimaryButton>
          ) : (
            <div className="text-center text-on-surface-variant text-sm">
              Waiting for host to start the round…
            </div>
          )}
        </div>
      </main>
    </div>
  );
}

function colorHex(name) {
  const map = {
    emerald: '#10b981',
    amber: '#f59e0b',
    rose: '#f43f5e',
    violet: '#8b5cf6',
    cyan: '#06b6d4',
    orange: '#f97316',
    teal: '#14b8a6',
    lime: '#84cc16',
    fuchsia: '#d946ef',
    indigo: '#6366f1',
    sky: '#0ea5e9',
    yellow: '#eab308',
    gray: '#6b7280'
  };
  return map[name] || '#6b7280';
}

/* ─────────────── cards + piles ─────────────── */

const SUIT_GLYPH = { H: '♥', D: '♦', S: '♠', C: '♣' };
const SUIT_RED = { H: true, D: true, S: false, C: false };

function PlayingCard({ rank, suit, size = 'md', tilt = 0, faceDown = false, ownerColor }) {
  const dims = {
    sm: 'w-10 h-14 text-xs',
    md: 'w-14 h-20 text-sm',
    lg: 'w-20 h-28 text-base',
    responsive: 'w-14 h-20 md:w-20 md:h-28 text-sm md:text-base'
  }[size];
  const rotate = tilt ? `rotate(${tilt}deg)` : undefined;

  if (faceDown) {
    return (
      <div
        className={`${dims} rounded-md card-shadow relative overflow-hidden border border-secondary-container`}
        style={{
          transform: rotate,
          background:
            'repeating-linear-gradient(45deg, #584414 0 6px, #6a5418 6px 12px)'
        }}
      >
        <div className="absolute inset-1 border border-secondary/30 rounded flex items-center justify-center">
          <Icon name="star" className="text-secondary/50 text-xs" />
        </div>
      </div>
    );
  }

  const red = SUIT_RED[suit];
  const color = red ? 'text-[#e53935]' : 'text-[#111]';
  return (
    <div
      className={`${dims} rounded-md bg-white card-shadow relative flex flex-col justify-between p-1.5`}
      style={{ transform: rotate }}
    >
      {ownerColor && (
        <div
          className="absolute -top-1.5 -right-1.5 w-3.5 h-3.5 rounded-full border-2 border-white z-10"
          style={{ backgroundColor: ownerColor }}
        />
      )}
      <div className={`flex flex-col items-start leading-none ${color}`}>
        <span className="font-bold">{rank}</span>
        <span className="text-xs">{SUIT_GLYPH[suit]}</span>
      </div>
      <div className={`self-center font-bold text-xl ${color}`}>{SUIT_GLYPH[suit]}</div>
    </div>
  );
}

function EmptySlot({ size = 'md', label }) {
  const dims = {
    sm: 'w-10 h-14',
    md: 'w-14 h-20',
    lg: 'w-20 h-28',
    responsive: 'w-14 h-20 md:w-20 md:h-28'
  }[size];
  return (
    <div className={`${dims} rounded-md border-2 border-dashed border-outline-variant/30 flex items-center justify-center`}>
      {label ? (
        <span className="text-[10px] text-on-surface-variant/40 uppercase tracking-widest">{label}</span>
      ) : (
        <Icon name="add" className="text-outline-variant/40 text-xl" />
      )}
    </div>
  );
}

/* ─────────────── drag + drop ─────────────── */

const DragContext = React.createContext(null);

function DragDropProvider({ onDrop, onReject, children }) {
  const [drag, setDrag] = useState(null);
  const [toast, setToast] = useState(null);
  const targetsRef = useRef(new Map());
  const [highlights, setHighlights] = useState({});

  const register = useCallback((id, target) => {
    targetsRef.current.set(id, target);
    return () => targetsRef.current.delete(id);
  }, []);

  const startDrag = useCallback((payload) => setDrag(payload), []);

  const showRejection = useCallback((reason) => {
    setToast(reason);
    const h = setTimeout(() => setToast(null), 2200);
    return () => clearTimeout(h);
  }, []);

  useEffect(() => {
    if (!onReject) return;
    onReject(showRejection);
  }, [onReject, showRejection]);

  useEffect(() => {
    if (!drag) {
      setHighlights({});
      return;
    }
    function inRange(rect, px, py) {
      if (px >= rect.left && px <= rect.right && py >= rect.top && py <= rect.bottom) return 0;
      const dx = Math.max(rect.left - px, 0, px - rect.right);
      const dy = Math.max(rect.top - py, 0, py - rect.bottom);
      return Math.hypot(dx, dy);
    }
    const onMove = (e) => {
      setDrag((d) => (d ? { ...d, pointerX: e.clientX, pointerY: e.clientY } : d));
      const next = {};
      for (const [id, t] of targetsRef.current) {
        const rect = t.getRect();
        if (!rect) continue;
        const dist = inRange(rect, e.clientX, e.clientY);
        if (dist <= 40) {
          next[id] = t.validate(drag) ? 'valid' : 'invalid';
        }
      }
      setHighlights(next);
    };
    const onUp = (e) => {
      let best = null;
      for (const [id, t] of targetsRef.current) {
        const rect = t.getRect();
        if (!rect) continue;
        if (
          e.clientX < rect.left - 40 ||
          e.clientX > rect.right + 40 ||
          e.clientY < rect.top - 40 ||
          e.clientY > rect.bottom + 40
        ) continue;
        if (!t.validate(drag)) continue;
        const cx = (rect.left + rect.right) / 2;
        const cy = (rect.top + rect.bottom) / 2;
        const dist = Math.hypot(cx - e.clientX, cy - e.clientY);
        if (!best || dist < best.dist) best = { id, t, dist };
      }
      if (best) {
        onDrop(drag, best.t.descriptor, { x: e.clientX, y: e.clientY });
      }
      setDrag(null);
      setHighlights({});
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    window.addEventListener('pointercancel', onUp);
    return () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      window.removeEventListener('pointercancel', onUp);
    };
  }, [drag, onDrop]);

  return (
    <DragContext.Provider value={{ drag, highlights, register, startDrag }}>
      {children}
      {drag && (
        <div
          className="fixed pointer-events-none z-[100]"
          style={{
            left: drag.pointerX - drag.ghostOffsetX,
            top: drag.pointerY - drag.ghostOffsetY
          }}
        >
          <div className="relative">
            {drag.cards.map((c, i) => (
              <div
                key={i}
                className="absolute"
                style={{ top: `${i * 24}px`, left: 0 }}
              >
                <PlayingCard rank={c.rank} suit={c.suit} size="responsive" />
              </div>
            ))}
          </div>
        </div>
      )}
      {toast && (
        <div className="fixed bottom-6 left-1/2 -translate-x-1/2 bg-error text-on-error px-4 py-2 rounded-full text-xs uppercase tracking-widest shadow-lg z-[101]">
          {toast}
        </div>
      )}
    </DragContext.Provider>
  );
}

function useDraggable({ source, cards, disabled }) {
  const ctx = React.useContext(DragContext);
  const isDragging =
    ctx && ctx.drag &&
    JSON.stringify(ctx.drag.source) === JSON.stringify(source);
  const onPointerDown = useCallback(
    (e) => {
      if (disabled || !ctx || !cards || cards.length === 0) return;
      e.preventDefault();
      const rect = e.currentTarget.getBoundingClientRect();
      ctx.startDrag({
        source,
        cards,
        ghostOffsetX: e.clientX - rect.left,
        ghostOffsetY: e.clientY - rect.top,
        pointerX: e.clientX,
        pointerY: e.clientY
      });
    },
    [ctx, source, cards, disabled]
  );
  return { onPointerDown, isDragging };
}

function DropTarget({ descriptor, validate, children }) {
  const ctx = React.useContext(DragContext);
  const ref = useRef(null);
  const id = useMemo(() => Math.random().toString(36).slice(2), []);
  useEffect(() => {
    if (!ctx) return;
    return ctx.register(id, {
      descriptor,
      validate,
      getRect: () => (ref.current ? ref.current.getBoundingClientRect() : null)
    });
  }, [ctx, id, descriptor, validate]);
  const highlight = ctx ? ctx.highlights[id] || null : null;
  return children({ ref, highlight });
}

/* ─────────────── pannable canvas ─────────────── */

function FoundationPile({ pile }) {
  return (
    <DropTarget
      descriptor={{ kind: 'foundation', foundationId: pile.id }}
      validate={(drag) => {
        if (!drag || drag.cards.length !== 1) return false;
        const c = drag.cards[0];
        if (pile.topRank === 'K') return false;
        if (c.suit !== pile.suit) return false;
        const order = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
        return order[order.indexOf(pile.topRank) + 1] === c.rank;
      }}
    >
      {({ ref, highlight }) => (
        <div
          ref={ref}
          data-pile
          className={`absolute rounded-md transition ${
            highlight === 'valid' ? 'ring-2 ring-emerald-400' :
            highlight === 'invalid' ? 'ring-2 ring-error' : ''
          }`}
          style={{
            left: `calc(50% + ${pile.x}px)`,
            top: `calc(50% + ${pile.y}px)`,
            transform: 'translate(-50%, -50%)'
          }}
        >
          <PlayingCard rank={pile.topRank} suit={pile.suit} size="responsive" ownerColor={pile.ownerColor} />
        </div>
      )}
    </DropTarget>
  );
}

function PannableCanvas({ piles, viewport, setViewport }) {
  const ref = useRef(null);
  const drag = useRef(null);
  const pointers = useRef(new Map());    // pointerId → {x, y}
  const pinch = useRef(null);             // { initialDist, initialZoom } when pinching

  const onPointerDown = (e) => {
    if (e.target.closest('[data-pile]')) return;
    // Only primary button (left-click / touch).
    if (e.pointerType === 'mouse' && e.button !== 0) return;
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
    pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });

    if (pointers.current.size === 2) {
      // Second finger landed → start a pinch. Cancel any single-finger pan.
      const [a, b] = Array.from(pointers.current.values());
      pinch.current = {
        initialDist: Math.hypot(a.x - b.x, a.y - b.y),
        initialZoom: viewport.zoom
      };
      drag.current = null;
    } else if (pointers.current.size === 1) {
      // First finger → start a pan.
      drag.current = {
        pointerId: e.pointerId,
        startX: e.clientX,
        startY: e.clientY,
        origX: viewport.x,
        origY: viewport.y
      };
    }
    if (ref.current) ref.current.style.cursor = 'grabbing';
  };

  const onPointerMove = (e) => {
    if (!pointers.current.has(e.pointerId)) return;
    pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });

    if (pointers.current.size === 2 && pinch.current) {
      const [a, b] = Array.from(pointers.current.values());
      const dist = Math.hypot(a.x - b.x, a.y - b.y);
      const ratio = dist / pinch.current.initialDist;
      const nextZoom = Math.max(0.5, Math.min(1.8, pinch.current.initialZoom * ratio));
      setViewport((v) => ({ ...v, zoom: nextZoom }));
      return;
    }

    if (pointers.current.size === 1) {
      const d = drag.current;
      if (!d || d.pointerId !== e.pointerId) return;
      const nextX = d.origX + (e.clientX - d.startX);
      const nextY = d.origY + (e.clientY - d.startY);
      setViewport((v) => ({ ...v, x: nextX, y: nextY }));
    }
  };

  const endDrag = (e) => {
    pointers.current.delete(e.pointerId);
    try { e.currentTarget.releasePointerCapture?.(e.pointerId); } catch (_) {}
    if (pinch.current && pointers.current.size < 2) {
      pinch.current = null;
    }
    if (drag.current && drag.current.pointerId === e.pointerId) {
      drag.current = null;
    }
    if (pointers.current.size === 0 && ref.current) {
      ref.current.style.cursor = 'grab';
    }
  };
  const onWheel = (e) => {
    if (!e.ctrlKey && !e.metaKey) return;
    e.preventDefault();
    const delta = -Math.sign(e.deltaY) * 0.1;
    setViewport((v) => ({
      ...v,
      zoom: Math.max(0.5, Math.min(1.8, v.zoom + delta))
    }));
  };

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);

  return (
    <DropTarget
      descriptor={{
        kind: 'new_foundation',
        toCanvasCoords: (pointer) => {
          const el = ref.current;
          if (!el) return { x: 0, y: 0 };
          const rect = el.getBoundingClientRect();
          return {
            x: (pointer.x - rect.left - rect.width / 2 - viewport.x) / viewport.zoom,
            y: (pointer.y - rect.top - rect.height / 2 - viewport.y) / viewport.zoom
          };
        }
      }}
      validate={(drag) => {
        if (!drag || drag.cards.length !== 1) return false;
        return drag.cards[0].rank === 'A';
      }}
    >
      {({ ref: dropRef, highlight }) => (
        <div
          ref={(el) => { ref.current = el; dropRef.current = el; }}
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={endDrag}
          onPointerCancel={endDrag}
          className={`relative w-full h-full bg-[#06170f] overflow-hidden cursor-grab select-none transition ${
            highlight === 'valid' ? 'ring-4 ring-emerald-400/50 ring-inset' : ''
          }`}
        >
          <div
            className="absolute inset-0 origin-center"
            style={{
              transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
              transition: drag.current ? 'none' : 'transform 200ms ease-out'
            }}
          >
            {/* Bounded table — rounded-rect felt with a wooden edge. Sized for up to ~12 players' foundations. */}
            <div
              className="absolute felt-texture dot-grid rounded-[48px] shadow-[0_0_0_12px_#2a1d0f,0_30px_80px_rgba(0,0,0,0.6)] ring-1 ring-black/40"
              style={{
                left: 'calc(50% - 1050px)',
                top: 'calc(50% - 675px)',
                width: '2100px',
                height: '1350px'
              }}
            />
            {piles.map((p) => (
              <FoundationPile key={p.id} pile={p} />
            ))}
            {piles.length === 0 && (
              <div
                className="absolute flex items-center justify-center text-on-surface-variant/50 pointer-events-none"
                style={{
                  left: 'calc(50% - 700px)',
                  top: 'calc(50% - 450px)',
                  width: '1400px',
                  height: '900px'
                }}
              >
                <div className="text-center">
                  <Icon name="playing_cards" className="text-5xl text-secondary/40" />
                  <p className="mt-2 text-sm uppercase tracking-widest">
                    Drop an Ace anywhere to start a pile
                  </p>
                </div>
              </div>
            )}
          </div>
        </div>
      )}
    </DropTarget>
  );
}

function CanvasControls({ viewport, setViewport }) {
  return (
    <div className="absolute top-4 right-4 glass-panel rounded-lg p-2 flex flex-col gap-1 z-20">
      <button
        aria-label="Recenter"
        onClick={() => setViewport({ x: 0, y: 0, zoom: 1 })}
        className="p-2 hover:bg-surface-container-highest rounded-full transition-colors"
      >
        <Icon name="center_focus_strong" className="text-secondary" />
      </button>
      <div className="h-px bg-outline-variant/40 my-1" />
      <button
        aria-label="Zoom in"
        onClick={() => setViewport((v) => ({ ...v, zoom: Math.min(1.8, v.zoom + 0.1) }))}
        className="p-2 hover:bg-surface-container-highest rounded-full transition-colors"
      >
        <Icon name="add" className="text-on-surface" />
      </button>
      <button
        aria-label="Zoom out"
        onClick={() => setViewport((v) => ({ ...v, zoom: Math.max(0.5, v.zoom - 0.1) }))}
        className="p-2 hover:bg-surface-container-highest rounded-full transition-colors"
      >
        <Icon name="remove" className="text-on-surface" />
      </button>
      <div className="px-2 text-[10px] text-center text-on-surface-variant font-mono">
        {(viewport.zoom * 100).toFixed(0)}%
      </div>
    </div>
  );
}

/* ─────────────── opponent strip + player board ─────────────── */

function OpponentStrip({ opponents }) {
  return (
    <div className="flex gap-2 overflow-x-auto no-scrollbar px-4 py-2">
      {opponents.map((o) => (
        <div
          key={o.playerId}
          className="shrink-0 flex items-center gap-2 bg-surface-container-low/60 backdrop-blur-md rounded-full px-3 py-1.5 ghost-border"
        >
          <div
            className="w-7 h-7 rounded-full flex items-center justify-center font-bold text-xs text-on-primary"
            style={{ backgroundColor: colorHex(o.cardBackColor) }}
          >
            {o.displayName.slice(0, 1).toUpperCase()}
          </div>
          <div className="flex flex-col leading-tight">
            <span className="text-xs font-semibold text-on-surface">{o.displayName}</span>
            <span className="text-[10px] text-secondary tracking-wider">
              NERTZ: {o.nertzCount ?? 13}
            </span>
          </div>
        </div>
      ))}
    </div>
  );
}

// Fan-offset bounds: cards stack vertically with each one offset below
// the last by `offset` px. MAX gives comfortable spacing for short piles;
// MIN keeps the rank corner visible even on very long (K→2) runs.
const FAN_MAX_OFFSET = 24;
const FAN_MIN_OFFSET = 14;
const FAN_TARGET_H = 260; // px — keep piles within this before extra growth kicks in
const FAN_CARD_H = 80;    // px — mobile baseline (desktop is taller; extra room is fine)

function computeFanOffset(n) {
  if (n <= 1) return FAN_MAX_OFFSET;
  const ideal = (FAN_TARGET_H - FAN_CARD_H) / (n - 1);
  return Math.max(FAN_MIN_OFFSET, Math.min(FAN_MAX_OFFSET, ideal));
}

function WorkPileColumn({ pileIndex, pile }) {
  const n = pile.length;
  const offset = computeFanOffset(n);
  const columnH = n > 0 ? FAN_CARD_H + (n - 1) * offset : FAN_CARD_H;

  return (
    <div className="flex flex-col items-center gap-1">
      <span className="text-[10px] text-on-surface-variant/50 tracking-widest uppercase">
        {pileIndex + 1}
      </span>
      <DropTarget
        descriptor={{ kind: 'work', pileIndex }}
        validate={(drag) => {
          if (!drag) return false;
          const base = drag.cards[0];
          if (!base) return false;
          if (pile.length === 0) return true;
          const top = pile[pile.length - 1];
          if (top.suit === base.suit) return false;
          const tIsRed = top.suit === 'H' || top.suit === 'D';
          const bIsRed = base.suit === 'H' || base.suit === 'D';
          if (tIsRed === bIsRed) return false;
          const order = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
          return order[order.indexOf(top.rank) - 1] === base.rank;
        }}
      >
        {({ ref, highlight }) => (
          <div
            ref={ref}
            className={`relative w-14 md:w-20 rounded-md transition ${
              highlight === 'valid' ? 'ring-2 ring-emerald-400' :
              highlight === 'invalid' ? 'ring-2 ring-error' : ''
            }`}
            style={{ height: `${Math.max(columnH, 176)}px` }}
          >
            {pile.length === 0 ? (
              <div className="absolute inset-x-0 top-0"><EmptySlot size="responsive" /></div>
            ) : (
              pile.map((card, j) => (
                <DraggableWorkCard
                  key={j}
                  pileIndex={pileIndex}
                  pile={pile}
                  cardIndex={j}
                  offset={offset}
                />
              ))
            )}
          </div>
        )}
      </DropTarget>
    </div>
  );
}

function DraggableWorkCard({ pileIndex, pile, cardIndex, offset = FAN_MAX_OFFSET }) {
  const cards = pile.slice(cardIndex);
  const card = pile[cardIndex];
  const { onPointerDown, isDragging } = useDraggable({
    source: { kind: 'work', pileIndex, cardIndex },
    cards
  });
  return (
    <div
      className={`absolute inset-x-0 ${isDragging ? 'opacity-30' : ''}`}
      style={{ top: `${cardIndex * offset}px` }}
      onPointerDown={onPointerDown}
    >
      <PlayingCard rank={card.rank} suit={card.suit} size="responsive" />
    </div>
  );
}

function DraggableTopCard({ source, card, ownerColor }) {
  const cards = card ? [card] : [];
  const { onPointerDown, isDragging } = useDraggable({ source, cards, disabled: !card });
  if (!card) return null;
  return (
    <div onPointerDown={onPointerDown} className={isDragging ? 'opacity-30' : ''}>
      <PlayingCard
        rank={card.rank}
        suit={card.suit}
        size="responsive"
        ownerColor={ownerColor}
      />
    </div>
  );
}

function PlayerBoard({ you, hand, flipMode = 3, onStockClick }) {
  const {
    nertzPile = { top: null, count: 0 },
    workPiles = [[], [], [], []],
    stockCount = 0,
    discard = []
  } = hand || {};
  const ownerColor = colorHex(you?.cardBackColor);
  const discardTop = discard.length ? discard[discard.length - 1] : null;

  return (
    <section className="bg-surface-container-high border-t border-outline-variant/20 shadow-[0_-20px_40px_rgba(0,0,0,0.5)] px-4 md:px-8 pt-5 pb-4 relative min-h-[260px] md:min-h-[300px] flex flex-col md:flex-row md:items-start gap-4 md:gap-6">
      {/* Mobile wraps Nertz+Stock+Discard into one row above the work piles.
          md:contents flattens the wrapper into direct flex children of the
          section on desktop so order classes can reposition them. */}
      <div className="flex flex-row justify-between items-start gap-4 md:contents">
        {/* Nertz pile — 13 cards, top face-up, goal is to empty */}
        <div className="flex flex-col items-center gap-2 shrink-0 md:order-1">
          <span className="text-[10px] text-secondary tracking-widest uppercase">Nertz</span>
          <div className="relative w-14 h-20 md:w-20 md:h-28">
            <div className="absolute -bottom-1.5 -right-1.5 w-full h-full rounded-md bg-surface-bright card-shadow" />
            <div className="absolute -bottom-[3px] -right-[3px] w-full h-full rounded-md bg-surface-container-highest card-shadow" />
            <div className="relative">
              {nertzPile.top ? (
                <DraggableTopCard
                  source={{ kind: 'nertz' }}
                  card={nertzPile.top}
                  ownerColor={ownerColor}
                />
              ) : (
                <EmptySlot label="Empty" size="responsive" />
              )}
            </div>
            <div className="absolute -top-2 -right-2 w-7 h-7 rounded-full bg-error text-on-error font-bold text-xs flex items-center justify-center shadow-lg border-2 border-surface-container-high z-20">
              {nertzPile.count}
            </div>
          </div>
        </div>

        {/* Stock (flip 1 or 3) + Discard */}
        <div className="flex gap-2 md:gap-3 shrink-0 items-start md:order-3">
          <div className="flex flex-col items-center gap-2">
            <span className="text-[10px] text-secondary tracking-widest uppercase">
              Stock · Flip {flipMode}
            </span>
            <button
              type="button"
              onClick={onStockClick}
              className="cursor-pointer hover:brightness-110 active:scale-95 transition"
              aria-label={stockCount > 0 ? 'Flip stock' : 'Reset discard to stock'}
            >
              {stockCount > 0 ? (
                <PlayingCard faceDown size="responsive" />
              ) : (
                <EmptySlot size="responsive" label="Reset" />
              )}
            </button>
            <span className="text-[10px] text-on-surface-variant">{stockCount}</span>
          </div>
          <div className="flex flex-col items-center gap-2">
            <span className="text-[10px] text-secondary tracking-widest uppercase">Discard</span>
            {/* Klondike-style flip-3 fan: top 3 of discard offset right; rightmost is playable */}
            <div className="relative h-20 md:h-28 w-[6.5rem] md:w-[8rem]">
              {discard.length === 0 ? (
                <div className="absolute left-0 top-0"><EmptySlot size="responsive" /></div>
              ) : (
                discard.slice(-3).map((card, i, arr) => {
                  const isTop = i === arr.length - 1;
                  return (
                    <div
                      key={discard.length - arr.length + i}
                      className="absolute top-0"
                      style={{ left: `${i * 24}px` }}
                    >
                      {isTop ? (
                        <DraggableTopCard source={{ kind: 'discard' }} card={card} />
                      ) : (
                        <PlayingCard rank={card.rank} suit={card.suit} size="responsive" />
                      )}
                    </div>
                  );
                })
              )}
            </div>
            <span className="text-[10px] text-on-surface-variant">{discard.length}</span>
          </div>
        </div>
      </div>

      {/* 4 work piles — fan down, build descending (slice 2).
          Full-width on mobile (row 2), between Nertz and Stock/Discard on desktop. */}
      <div className="flex gap-2 md:gap-4 flex-1 justify-center items-start md:order-2">
        {workPiles.map((pile, i) => (
          <WorkPileColumn key={i} pileIndex={i} pile={pile} />
        ))}
      </div>
    </section>
  );
}

/* ─────────────── game table ─────────────── */

function GameTableScreen({ table, you, roundState, onFlipStock, onCallNertz, onSetStuck, onLeave }) {
  const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 });

  const hand = roundState?.you || null;

  const opponents = useMemo(() => {
    const byId = new Map(
      (roundState?.opponents || []).map((o) => [o.playerId, o])
    );
    return (table?.seats || [])
      .filter((s) => s.playerId !== you?.playerId)
      .map((s) => ({
        ...s,
        nertzCount: byId.get(s.playerId)?.nertzCount ?? 13
      }));
  }, [table?.seats, you?.playerId, roundState]);

  const piles = useMemo(() => {
    const foundations = roundState?.foundations || [];
    const colorByPlayerId = new Map(
      (table?.seats || []).map((s) => [s.playerId, colorHex(s.cardBackColor)])
    );
    return foundations.map((f) => ({
      id: f.foundationId,
      x: f.x,
      y: f.y,
      suit: f.suit,
      topRank: f.topRank,
      ownerColor: colorByPlayerId.get(f.startedByPlayerId) || '#6b7280'
    }));
  }, [roundState, table?.seats]);

  if (!hand) {
    return (
      <div className="h-screen flex items-center justify-center text-on-surface-variant">
        <div className="flex flex-col items-center gap-3">
          <Icon name="progress_activity" className="text-4xl animate-spin text-secondary" />
          <span className="text-xs uppercase tracking-widest">Dealing…</span>
        </div>
      </div>
    );
  }

  return (
    <div className="h-screen flex flex-col overflow-hidden">
      <BrandHeader
        right={
          <>
            <div className="text-xs text-on-surface-variant font-mono uppercase tracking-widest">
              {table.code}
            </div>
            <GhostButton onClick={onLeave} className="px-4 py-2 text-xs">Leave</GhostButton>
          </>
        }
      />
      <OpponentStrip opponents={opponents} />
      <main className="flex-1 flex flex-col min-h-0 relative">
        <div className="flex-1 relative min-h-0">
          <PannableCanvas piles={piles} viewport={viewport} setViewport={setViewport} />
          <CanvasControls viewport={viewport} setViewport={setViewport} />
          {(() => {
            const youStuck = !!(roundState?.you?.stuck);
            const stuckCount =
              (roundState?.you?.stuck ? 1 : 0) +
              (roundState?.opponents || []).filter((o) => o.stuck).length;
            const totalConnected = 1 + (roundState?.opponents || []).filter((o) => {
              const seat = (table?.seats || []).find((s) => s.playerId === o.playerId);
              return seat && seat.connected;
            }).length;
            return (
              <div className="absolute bottom-4 left-4 flex flex-col items-start gap-2 z-30">
                <span className="text-[10px] uppercase tracking-widest text-on-surface-variant/70 font-mono px-2">
                  {stuckCount}/{totalConnected} stuck
                </span>
                <button
                  onClick={() => onSetStuck(!youStuck)}
                  className={`font-headline font-bold text-sm py-2 px-4 rounded-full border transition active:scale-95 tracking-widest uppercase ${
                    youStuck
                      ? 'bg-secondary text-on-secondary border-secondary shadow-[0_0_20px_rgba(225,195,135,0.4)]'
                      : 'bg-surface-container-low/60 backdrop-blur-md text-on-surface ghost-border hover:bg-surface-container-high'
                  }`}
                >
                  {youStuck ? "I'm Stuck ✓" : "I'm Stuck?"}
                </button>
              </div>
            );
          })()}
          <button
            onClick={onCallNertz}
            disabled={!hand || hand.nertzPile.count !== 0}
            className="absolute bottom-4 right-4 bg-gradient-to-br from-error to-error-container text-on-error font-headline font-bold text-xl py-3 px-6 rounded-full shadow-[0_0_30px_rgba(147,0,10,0.6)] border border-error/50 active:scale-95 transition z-30 tracking-widest uppercase disabled:opacity-30 disabled:cursor-not-allowed disabled:shadow-none"
          >
            NERTZ!
          </button>
        </div>
        <PlayerBoard
          you={you}
          hand={hand}
          flipMode={table.stockFlipMode}
          onStockClick={onFlipStock}
        />
      </main>
    </div>
  );
}

/* ─────────────── app shell ─────────────── */

function SummaryScreen({ table, you, roundSummary, onNextRound, onLeave }) {
  if (!roundSummary) {
    return (
      <div className="min-h-screen flex items-center justify-center text-on-surface-variant">
        <div className="flex flex-col items-center gap-3">
          <Icon name="progress_activity" className="text-4xl animate-spin text-secondary" />
          <span className="text-xs uppercase tracking-widest">Loading summary…</span>
        </div>
      </div>
    );
  }

  const isHost = you && table.hostPlayerId === you.playerId;
  const matchEnded = roundSummary.matchEnded;
  const winners = roundSummary.players.filter((p) =>
    roundSummary.winnerPlayerIds.includes(p.playerId)
  );
  const caller = roundSummary.players.find(
    (p) => p.playerId === roundSummary.round.callerPlayerId
  );

  const sorted = [...roundSummary.players].sort(
    (a, b) => b.totalScore - a.totalScore
  );

  let matchEndText = '';
  if (matchEnded) {
    if (winners.length === 1) {
      matchEndText = `Match Complete — ${winners[0].displayName} wins!`;
    } else if (winners.length === 2) {
      matchEndText = `Match Complete — ${winners[0].displayName} & ${winners[1].displayName} tie!`;
    } else if (winners.length >= 3) {
      const names = winners.map((w) => w.displayName);
      const last = names.pop();
      matchEndText = `Match Complete — ${names.join(', ')} & ${last} tie!`;
    }
  }

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader
        right={
          <>
            <div className="text-xs text-on-surface-variant font-mono uppercase tracking-widest">
              {table.code}
            </div>
            <GhostButton onClick={onLeave} className="px-4 py-2 text-xs">Leave</GhostButton>
          </>
        }
      />
      <main className="flex-1 felt-texture dot-grid p-6">
        <div className="max-w-3xl mx-auto flex flex-col gap-6">
          <div className="text-center">
            {matchEnded ? (
              <h2 className="font-headline text-3xl text-secondary">{matchEndText}</h2>
            ) : roundSummary.round.callerPlayerId === null ? (
              <h2 className="font-headline text-3xl text-on-surface">
                Round {roundSummary.round.roundNumber} · Ended by consensus (stuck)
              </h2>
            ) : (
              <h2 className="font-headline text-3xl text-on-surface">
                Round {roundSummary.round.roundNumber} · {caller?.displayName || 'Someone'} called NERTZ!
              </h2>
            )}
            <p className="text-on-surface-variant text-sm mt-1">
              Target: {table.targetScore}
            </p>
          </div>

          <div className="glass-panel rounded-2xl overflow-hidden">
            <div className="grid grid-cols-[auto_1fr_auto_auto_auto_auto] gap-x-4 gap-y-2 px-5 py-4 items-center text-sm">
              <div className="text-xs uppercase tracking-widest text-on-surface-variant"></div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant">Player</div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant text-right">Foundations</div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant text-right">Nertz -2×</div>
              <div className="text-xs uppercase tracking-widest text-on-surface-variant text-right">Round</div>
              <div className="text-xs uppercase tracking-widest text-secondary text-right">Total</div>
              {sorted.map((p) => {
                const isCaller = p.playerId === roundSummary.round.callerPlayerId;
                const isWinner = matchEnded && roundSummary.winnerPlayerIds.includes(p.playerId);
                return (
                  <React.Fragment key={p.playerId}>
                    <div
                      className="w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs text-on-primary"
                      style={{ backgroundColor: colorHex(p.cardBackColor) }}
                    >
                      {p.displayName.slice(0, 1).toUpperCase()}
                    </div>
                    <div className="flex items-center gap-2">
                      <span className={`font-semibold ${isWinner ? 'text-secondary' : 'text-on-surface'}`}>
                        {p.displayName}
                      </span>
                      {isCaller && (
                        <span className="text-[10px] uppercase tracking-widest bg-secondary/20 text-secondary px-2 py-0.5 rounded-full">
                          called
                        </span>
                      )}
                      {isWinner && (
                        <Icon name="workspace_premium" className="text-secondary text-base" />
                      )}
                    </div>
                    <div className="text-right font-mono">+{p.foundationCards}</div>
                    <div className="text-right font-mono text-error">
                      {p.leftoverNertz > 0 ? `-${2 * p.leftoverNertz}` : '—'}
                    </div>
                    <div className={`text-right font-mono ${p.roundScore < 0 ? 'text-error' : 'text-on-surface'}`}>
                      {p.roundScore > 0 ? `+${p.roundScore}` : p.roundScore}
                    </div>
                    <div className="text-right font-headline text-xl text-secondary">
                      {p.totalScore}
                    </div>
                  </React.Fragment>
                );
              })}
            </div>
          </div>

          {matchEnded ? (
            <PrimaryButton onClick={onLeave}>Back to Lobby</PrimaryButton>
          ) : isHost ? (
            <PrimaryButton onClick={onNextRound}>Next Round</PrimaryButton>
          ) : (
            <div className="text-center text-on-surface-variant text-sm uppercase tracking-widest">
              Waiting for host to start the next round…
            </div>
          )}
        </div>
      </main>
    </div>
  );
}

function LeaderboardScreen({ onBack }) {
  const [rows, setRows] = useState(null);
  const [error, setError] = useState(null);
  const youPlayerId = (() => {
    try {
      const v = localStorage.getItem('nertz.playerId');
      return v ? Number(v) : null;
    } catch (_) {
      return null;
    }
  })();

  useEffect(() => {
    let cancelled = false;
    fetch('/api/leaderboard')
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then((data) => {
        if (!cancelled) setRows(data.rows || []);
      })
      .catch((err) => {
        if (!cancelled) setError(err.message);
      });
    return () => { cancelled = true; };
  }, []);

  if (error) {
    return (
      <div className="min-h-screen flex flex-col">
        <BrandHeader right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>} />
        <main className="flex-1 flex items-center justify-center p-6">
          <div className="flex flex-col items-center gap-4 max-w-md text-center">
            <div className="text-error bg-error-container/30 ghost-border rounded-lg px-4 py-3 text-sm">
              Failed to load leaderboard: {error}
            </div>
            <PrimaryButton
              onClick={() => {
                setError(null);
                setRows(null);
                fetch('/api/leaderboard')
                  .then((r) => {
                    if (!r.ok) throw new Error(`HTTP ${r.status}`);
                    return r.json();
                  })
                  .then((data) => setRows(data.rows || []))
                  .catch((err) => setError(err.message));
              }}
              className="px-6 py-2 text-xs"
            >
              Retry
            </PrimaryButton>
          </div>
        </main>
      </div>
    );
  }

  if (rows === null) {
    return (
      <div className="min-h-screen flex flex-col">
        <BrandHeader right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>} />
        <main className="flex-1 flex items-center justify-center text-on-surface-variant">
          <div className="flex flex-col items-center gap-3">
            <Icon name="progress_activity" className="text-4xl animate-spin text-secondary" />
            <span className="text-xs uppercase tracking-widest">Loading rankings…</span>
          </div>
        </main>
      </div>
    );
  }

  if (rows.length === 0) {
    return (
      <div className="min-h-screen flex flex-col">
        <BrandHeader right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>} />
        <main className="flex-1 flex items-center justify-center p-6">
          <div className="text-center">
            <Icon name="trophy" className="text-5xl text-secondary/60" />
            <p className="mt-3 text-sm uppercase tracking-widest text-on-surface-variant">No matches played yet</p>
            <p className="mt-1 text-xs text-on-surface-variant/60">Start a table and be the first on the board.</p>
          </div>
        </main>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex flex-col">
      <BrandHeader right={<GhostButton onClick={onBack} className="px-4 py-2 text-xs">Back</GhostButton>} />
      <main className="flex-1 p-4 md:p-8 overflow-auto">
        <div className="max-w-4xl mx-auto">
          <h2 className="font-headline text-3xl md:text-4xl text-on-surface mb-6">Leaderboard</h2>
          <div className="glass-panel rounded-xl overflow-hidden ghost-border">
            <table className="w-full text-left border-collapse">
              <thead>
                <tr className="bg-surface-container-low/50 text-[10px] uppercase tracking-widest text-on-surface-variant">
                  <th className="px-3 md:px-4 py-3">#</th>
                  <th className="px-3 md:px-4 py-3">Player</th>
                  <th className="px-3 md:px-4 py-3 text-right">Wins</th>
                  <th className="px-3 md:px-4 py-3 text-right hidden sm:table-cell">Matches</th>
                  <th className="px-3 md:px-4 py-3 text-right">Avg</th>
                  <th className="px-3 md:px-4 py-3 text-right hidden sm:table-cell">Nertz</th>
                  <th className="px-3 md:px-4 py-3 text-right">Best</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-white/5">
                {rows.map((row, i) => {
                  const isYou = youPlayerId !== null && row.playerId === youPlayerId;
                  return (
                    <tr
                      key={row.playerId}
                      aria-current={isYou ? 'true' : undefined}
                      className={`transition-colors ${isYou ? 'bg-secondary/10 border-l-4 border-secondary' : 'hover:bg-white/5'}`}
                    >
                      <td className="px-3 md:px-4 py-3 font-mono text-sm text-on-surface-variant">{i + 1}</td>
                      <td className="px-3 md:px-4 py-3">
                        <div className="flex items-center gap-2">
                          <span
                            className="inline-block w-3 h-3 rounded-full ghost-border shrink-0"
                            style={{ backgroundColor: colorHex(row.cardBackColor) }}
                          />
                          <span className="font-headline text-base md:text-lg text-on-surface">{row.displayName}</span>
                          {isYou && (
                            <span aria-label="This is you" className="text-[9px] uppercase tracking-widest text-secondary font-bold">You</span>
                          )}
                        </div>
                      </td>
                      <td className="px-3 md:px-4 py-3 text-right font-mono text-on-surface">{row.wins}</td>
                      <td className="px-3 md:px-4 py-3 text-right font-mono text-on-surface-variant hidden sm:table-cell">{row.matches}</td>
                      <td className="px-3 md:px-4 py-3 text-right font-mono text-on-surface-variant">{Number(row.avgRoundScore).toFixed(1)}</td>
                      <td className="px-3 md:px-4 py-3 text-right font-mono text-on-surface-variant hidden sm:table-cell">{row.nertzCalls}</td>
                      <td className="px-3 md:px-4 py-3 text-right font-mono text-on-surface-variant">{row.bestMatchScore}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      </main>
    </div>
  );
}

function App() {
  const session = useSession();
  const [route, setRoute] = useState('home');
  const [table, setTable] = useState(null);
  const [you, setYou] = useState(null);
  const [roundState, setRoundState] = useState(null);
  const [roundSummary, setRoundSummary] = useState(null);
  const [joinError, setJoinError] = useState(null);
  const rejoinPayloadRef = useRef(null);
  const getRejoinPayload = useCallback(() => rejoinPayloadRef.current, []);

  const handleMessage = useCallback((msg) => {
    if (msg.type === 'table_state') {
      setTable(msg.table);
      if (msg.you) {
        setYou(msg.you);
        rejoinPayloadRef.current = {
          code: msg.table.code,
          sessionId: session.sessionId,
          displayName: session.displayName || 'Guest'
        };
        try { localStorage.setItem('nertz.lastCode', msg.table.code); } catch (_) {}
        try { localStorage.setItem('nertz.playerId', String(msg.you.playerId)); } catch (_) {}
      }
      if (msg.table.status === 'active' && route !== 'game') setRoute('game');
      if ((msg.table.status === 'summary' || msg.table.status === 'ended') && route !== 'summary') setRoute('summary');
    } else if (msg.type === 'round_state') {
      setRoundState({ round: msg.round, you: msg.you, opponents: msg.opponents, foundations: msg.foundations });
    } else if (msg.type === 'round_summary') {
      setRoundSummary(msg.summary);
    } else if (msg.type === 'move_rejected') {
      if (rejectionHandlerRef.current) rejectionHandlerRef.current(msg.reason);
    } else if (msg.type === 'error') {
      const midSession = route === 'lobby' || route === 'game' || route === 'summary';
      if (msg.reason === 'Table not found' && midSession) {
        const hadSeat = you !== null;
        rejoinPayloadRef.current = null;
        try { localStorage.removeItem('nertz.lastCode'); } catch (_) {}
        setTable(null);
        setYou(null);
        setRoundState(null);
        setRoundSummary(null);
        setRoute('home');
        setJoinError(hadSeat ? 'That match has ended.' : 'Table not found.');
      } else if (route !== 'home' || !joinError) {
        setJoinError(msg.reason);
      }
    }
  }, [route, session.sessionId, session.displayName, you, joinError]);

  const { connected, reconnecting, send } = useGameSocket({
    onMessage: handleMessage,
    getRejoinPayload
  });

  const joinTable = useCallback((code) => {
    setJoinError(null);
    send({
      type: 'join_table',
      code,
      sessionId: session.sessionId,
      displayName: session.displayName || 'Guest'
    });
    setRoute('lobby');
  }, [send, session]);

  const leaveTable = useCallback(() => {
    send({ type: 'leave_table' });
    setTable(null);
    setYou(null);
    setRoundState(null);
    setRoundSummary(null);
    rejoinPayloadRef.current = null;
    try { localStorage.removeItem('nertz.lastCode'); } catch (_) {}
    setRoute('home');
  }, [send]);

  useEffect(() => {
    if (route === 'home') {
      rejoinPayloadRef.current = null;
      try { localStorage.removeItem('nertz.lastCode'); } catch (_) {}
    } else {
      setJoinError(null);
    }
  }, [route]);

  const startRound = useCallback(() => {
    send({ type: 'start_round' });
  }, [send]);

  const flipStock = useCallback(() => {
    send({ type: 'flip_stock' });
  }, [send]);

  const callNertz = useCallback(() => {
    send({ type: 'call_nertz' });
  }, [send]);

  const setStuck = useCallback((stuck) => {
    send({ type: 'set_stuck', stuck });
  }, [send]);

  const rejectionHandlerRef = useRef(null);
  const onReject = useCallback((fn) => { rejectionHandlerRef.current = fn; }, []);

  const dispatchMove = useCallback((drag, descriptor, pointer) => {
    const { source } = drag;
    let target;
    if (descriptor.kind === 'work') {
      target = { kind: 'work', pileIndex: descriptor.pileIndex };
    } else if (descriptor.kind === 'foundation') {
      target = { kind: 'foundation', foundationId: descriptor.foundationId };
    } else if (descriptor.kind === 'new_foundation') {
      const { x, y } = descriptor.toCanvasCoords(pointer);
      target = { kind: 'new_foundation', x, y };
    } else {
      return;
    }
    send({ type: 'play_card', source, target });
  }, [send]);

  const screen = (() => {
    if (route === 'home') {
      return <HomeScreen session={session} onGoto={setRoute} error={joinError} />;
    }
    if (route === 'create') {
      return (
        <CreateTableScreen
          session={session}
          onCreated={(code) => joinTable(code)}
          onBack={() => setRoute('home')}
        />
      );
    }
    if (route === 'join') {
      return (
        <JoinTableScreen
          onJoin={(code) => joinTable(code)}
          onBack={() => setRoute('home')}
        />
      );
    }
    if (route === 'lobby') {
      if (!table) {
        return (
          <div className="min-h-screen flex items-center justify-center text-on-surface-variant">
            <div className="flex flex-col items-center gap-3">
              <Icon name="progress_activity" className="text-4xl animate-spin text-secondary" />
              <span className="text-xs uppercase tracking-widest">
                {connected ? 'Joining table…' : 'Connecting…'}
              </span>
              {joinError && <div className="text-error text-sm">{joinError}</div>}
            </div>
          </div>
        );
      }
      return <LobbyScreen table={table} you={you} onStart={startRound} onLeave={leaveTable} />;
    }
    if (route === 'game') {
      if (!table) return null;
      return (
        <GameTableScreen
          table={table}
          you={you}
          roundState={roundState}
          onFlipStock={flipStock}
          onCallNertz={callNertz}
          onSetStuck={setStuck}
          onLeave={leaveTable}
        />
      );
    }
    if (route === 'summary') {
      if (!table) return null;
      return (
        <SummaryScreen
          table={table}
          you={you}
          roundSummary={roundSummary}
          onNextRound={startRound}
          onLeave={leaveTable}
        />
      );
    }
    if (route === 'leaderboard') {
      return <LeaderboardScreen onBack={() => setRoute('home')} />;
    }
    return null;
  })();

  return (
    <DragDropProvider onDrop={dispatchMove} onReject={onReject}>
      {reconnecting && (
        <div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 bg-surface-container-high/90 backdrop-blur-md ghost-border rounded-full px-4 py-2 text-xs uppercase tracking-widest text-on-surface flex items-center gap-2 shadow-lg">
          <Icon name="progress_activity" className="animate-spin text-secondary" />
          Reconnecting…
        </div>
      )}
      {screen}
    </DragDropProvider>
  );
}

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, info: null };
  }
  static getDerivedStateFromError(error) {
    return { error };
  }
  componentDidCatch(error, info) {
    console.error('[nertz:error-boundary] render crash:', error, info);
    this.setState({ info });
  }
  render() {
    if (this.state.error) {
      const stack = (this.state.error && this.state.error.stack) || String(this.state.error);
      const componentStack = this.state.info && this.state.info.componentStack;
      return (
        <div className="min-h-screen flex items-center justify-center p-6 bg-background text-on-surface">
          <div className="w-full max-w-2xl bg-surface-container rounded-2xl ghost-border p-6 flex flex-col gap-4">
            <h2 className="font-headline text-2xl text-error">Something crashed.</h2>
            <p className="text-on-surface-variant text-sm">
              The UI hit an unhandled error. Reload to get back to the game — your seat on the server is still open if a match is in progress.
            </p>
            <pre className="bg-surface-container-low rounded-md p-3 text-[11px] text-error overflow-auto whitespace-pre-wrap max-h-64">{stack}{componentStack || ''}</pre>
            <div className="flex gap-2">
              <button
                onClick={() => window.location.reload()}
                className="bg-secondary text-on-secondary font-bold uppercase tracking-widest text-sm rounded-full px-6 py-3"
              >
                Reload
              </button>
              <button
                onClick={() => this.setState({ error: null, info: null })}
                className="bg-surface-container-high text-on-surface font-bold uppercase tracking-widest text-sm rounded-full px-6 py-3"
              >
                Try to recover
              </button>
            </div>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);
