import React, { useEffect, useMemo, useRef, useState } from "react";
import {
  BrowserRouter,
  Navigate,
  NavLink,
  Route,
  Routes,
  useNavigate,
  useParams,
} from "react-router-dom";
import ChatListScreen from "./chat/ChatListScreen";
import ChatNewScreen from "./chat/ChatNewScreen";
import ChatThreadScreen from "./chat/ChatThreadScreen";
import { getLatestThreadTimestamp, getUnreadThreadIds } from "./chat/utils";
import {
  formatConnectionState,
  formatStatus,
  formatTransactionKind,
  getLocale,
  tStatic,
  useI18n,
} from "./i18n";
import QRCode from "qrcode";
import type { SessionSnapshot, Transaction } from "../../shared/types";

const STORAGE_KEY = "negopoly:lastSession";
const CHAT_READ_KEY = "negopoly:chatRead";

type StoredSession = {
  sessionId: string;
  playerId: string;
};

type JoinResponse = {
  sessionId: string;
  sessionCode: string;
  playerId: string;
  role: string;
  status: string;
};

type SessionPreviewPlayer = {
  id: string;
  name: string;
  role: string;
  isDummy: boolean;
  connected: boolean;
};

type SessionPreview = {
  sessionId: string;
  code: string;
  status: string;
  players: SessionPreviewPlayer[];
};

function readStoredSession(): StoredSession | null {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    return JSON.parse(raw) as StoredSession;
  } catch {
    return null;
  }
}

function writeStoredSession(sessionId: string, playerId: string) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify({ sessionId, playerId }));
}

function clearStoredSession() {
  localStorage.removeItem(STORAGE_KEY);
}

function getChatReadKey(sessionId: string, playerId: string) {
  return `${CHAT_READ_KEY}:${sessionId}:${playerId}`;
}

function readChatReadState(sessionId: string, playerId: string): Record<string, number> {
  try {
    const raw = localStorage.getItem(getChatReadKey(sessionId, playerId));
    if (!raw) return {};
    return JSON.parse(raw) as Record<string, number>;
  } catch {
    return {};
  }
}

function writeChatReadState(sessionId: string, playerId: string, state: Record<string, number>) {
  try {
    localStorage.setItem(getChatReadKey(sessionId, playerId), JSON.stringify(state));
  } catch {
    // ignore write failures
  }
}

function formatMoney(amount: number) {
  const value = new Intl.NumberFormat(getLocale(), {
    maximumFractionDigits: 0,
  }).format(amount);
  return `₦${value}`;
}

function formatTransactionTimestamp(value: number) {
  const date = new Date(value);
  const now = new Date();
  const sameDay =
    date.getFullYear() === now.getFullYear() &&
    date.getMonth() === now.getMonth() &&
    date.getDate() === now.getDate();
  const time = date.toLocaleTimeString(getLocale(), { hour: "2-digit", minute: "2-digit" });
  if (sameDay) return time;
  const day = date.toLocaleDateString(getLocale(), { month: "short", day: "numeric" });
  return `${day} ${time}`;
}

function getTransactionLabel(
  transaction: Transaction,
  t: ReturnType<typeof useI18n>["t"],
) {
  if (transaction.kind === "banker_adjust" || transaction.kind === "banker_force_transfer") {
    const note = transaction.note?.trim();
    return note || t("common.noReason");
  }
  return formatTransactionKind(transaction.kind, t);
}

function getTransactionDisplay(
  transaction: Transaction,
  viewerId: string | null | undefined,
  players: SessionSnapshot["players"],
  t: ReturnType<typeof useI18n>["t"],
) {
  const absAmount = Math.abs(transaction.amount);
  const label = getTransactionLabel(transaction, t);
  const findPlayer = (id: string | null) => players.find((player) => player.id === id);
  const from = findPlayer(transaction.fromId);
  const to = findPlayer(transaction.toId);
  let outgoing = false;
  let counterparty = t("common.bank");
  const timeLabel = formatTransactionTimestamp(transaction.createdAt);

  if (transaction.kind === "banker_adjust") {
    outgoing = transaction.amount < 0;
    counterparty = t("common.bank");
  } else if (transaction.kind === "transfer" || transaction.kind === "banker_force_transfer") {
    if (viewerId && transaction.fromId === viewerId) {
      outgoing = true;
      counterparty = to?.name ?? t("common.player");
    } else if (viewerId && transaction.toId === viewerId) {
      outgoing = false;
      counterparty = from?.name ?? t("common.player");
    } else {
      outgoing = true;
      counterparty = to?.name ?? t("common.player");
    }
  }

  return {
    label,
    subtitle: viewerId
      ? `${outgoing ? t("common.to") : t("common.from")} ${counterparty} · ${timeLabel}`
      : transaction.kind === "banker_adjust"
      ? outgoing
        ? `${to?.name ?? t("common.player")} → ${t("common.bank")} · ${timeLabel}`
        : `${t("common.bank")} → ${to?.name ?? t("common.player")} · ${timeLabel}`
      : `${from?.name ?? t("common.player")} → ${to?.name ?? t("common.player")} · ${timeLabel}`,
    amount: `${outgoing ? "-" : ""}${formatMoney(absAmount)}`,
    tone: outgoing ? "negative" : "positive",
  };
}

function formatTime(value: number) {
  return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}

function getWsUrl(sessionId: string, playerId: string) {
  const base = window.location.origin.replace("http", "ws");
  const params = new URLSearchParams({ sessionId, playerId });
  return `${base}/ws?${params.toString()}`;
}

function getBlackoutState(session: SessionSnapshot | null, tick: number) {
  tick;
  return {
    active: Boolean(session?.blackoutActive),
  };
}

function useSessionManager() {
  const stored = readStoredSession();
  const [sessionId, setSessionId] = useState(stored?.sessionId ?? "");
  const [playerId, setPlayerId] = useState(stored?.playerId ?? "");
  const [session, setSession] = useState<SessionSnapshot | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [pendingTakeoverId, setPendingTakeoverId] = useState<string | null>(null);
  const [connectionState, setConnectionState] = useState<
    "idle" | "connecting" | "open" | "error"
  >("idle");
  const [tick, setTick] = useState(0);
  const [chatReadState, setChatReadState] = useState<Record<string, number>>({});

  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const timer = setInterval(() => setTick((value) => value + 1), 1000);
    return () => clearInterval(timer);
  }, []);

  useEffect(() => {
    if (!sessionId || !playerId) {
      setChatReadState({});
      return;
    }
    setChatReadState(readChatReadState(sessionId, playerId));
  }, [sessionId, playerId]);

  useEffect(() => {
    if (!sessionId || !playerId) {
      setConnectionState("idle");
      setSession(null);
      return;
    }

    setConnectionState("connecting");
    const ws = new WebSocket(getWsUrl(sessionId, playerId));
    wsRef.current = ws;

    ws.onopen = () => {
      setConnectionState("open");
    };

    ws.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data);
        if (message.type === "state") {
          setSession(message.session as SessionSnapshot);
        }
        if (message.type === "error") {
          setError(message.message);
        }
        if (message.type === "takeover_approved") {
          const assignedId = message.assignedPlayerId as string;
          setPlayerId(assignedId);
          writeStoredSession(sessionId, assignedId);
        }
        } catch {
          setError(tStatic("error.parseResponse"));
        }
      };

    ws.onerror = () => {
      setConnectionState("error");
    };

    ws.onclose = () => {
      setConnectionState("error");
    };

    const pingTimer = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: "ping", sessionId, playerId }));
      }
    }, 15000);

    return () => {
      clearInterval(pingTimer);
      ws.close();
    };
  }, [sessionId, playerId]);

  useEffect(() => {
    if (!pendingTakeoverId) return;
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
    if (!sessionId || !playerId) return;
    wsRef.current.send(
      JSON.stringify({
        type: "takeover_request",
        sessionId,
        playerId,
        dummyId: pendingTakeoverId,
      }),
    );
    setPendingTakeoverId(null);
  }, [pendingTakeoverId, sessionId, playerId, connectionState]);

  const me = session?.players.find((player) => player.id === playerId) ?? null;
  const isBanker = me?.role === "banker";

  const players = useMemo(() => {
    if (!session) return [];
    return [...session.players].sort((a, b) => {
      if (a.role === b.role) {
        return a.name.localeCompare(b.name);
      }
      return a.role === "banker" ? -1 : 1;
    });
  }, [session]);

  async function createSession(bankerName: string) {
    setError(null);
    setSession(null);
    const response = await fetch("/api/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ bankerName }),
    });
    if (!response.ok) {
      setError(tStatic("error.createSession"));
      return null;
    }
    const data = (await response.json()) as JoinResponse;
    setSessionId(data.sessionId);
    setPlayerId(data.playerId);
    writeStoredSession(data.sessionId, data.playerId);
    return data;
  }

  async function joinSession(code: string, name: string) {
    setError(null);
    setSession(null);
    if (!code) {
      setError(tStatic("entry.alert.enterCode"));
      return null;
    }
    const storedNow = readStoredSession();
    const reusePlayerId = storedNow?.sessionId === code ? storedNow.playerId : undefined;
    const response = await fetch(`/api/session/${code}/join`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, playerId: reusePlayerId }),
    });
    if (!response.ok) {
      setError(tStatic("error.joinSession"));
      return null;
    }
    const data = (await response.json()) as JoinResponse;
    setSessionId(data.sessionId);
    setPlayerId(data.playerId);
    writeStoredSession(data.sessionId, data.playerId);
    return data;
  }

  function sendMessage(payload: Record<string, unknown>) {
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
      setError(tStatic("error.connectionNotReady"));
      return;
    }
    wsRef.current.send(JSON.stringify(payload));
  }

  function resetSession() {
    clearStoredSession();
    setSessionId("");
    setPlayerId("");
    setSession(null);
    setError(null);
    setPendingTakeoverId(null);
    setChatReadState({});
  }

  function markChatRead(threadId: string, timestamp: number) {
    if (!sessionId || !playerId) return;
    setChatReadState((prev) => {
      const next = {
        ...prev,
        [threadId]: Math.max(prev[threadId] ?? 0, timestamp),
      };
      writeChatReadState(sessionId, playerId, next);
      return next;
    });
  }

  return {
    sessionId,
    playerId,
    session,
    chatReadState,
    players,
    me,
    isBanker,
    connectionState,
    error,
    setError,
    tick,
    setSessionId,
    setPlayerId,
    setSession,
    createSession,
    joinSession,
    sendMessage,
    resetSession,
    setPendingTakeoverId,
    markChatRead,
  };
}

function useRouteSessionSync(manager: ReturnType<typeof useSessionManager>) {
  const params = useParams();
  const { sessionId, setSessionId, setSession, setPlayerId } = manager;
  useEffect(() => {
    const routeSessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
    if (!routeSessionId) return;
    if (routeSessionId !== sessionId) {
      setSessionId(routeSessionId);
      setSession(null);
    }
    const stored = readStoredSession();
    if (!stored || stored.sessionId !== routeSessionId) {
      setPlayerId("");
    }
  }, [params.sessionId, sessionId, setSessionId, setSession, setPlayerId]);
}

function GameTabs({
  role,
  sessionId,
  hasUnread,
}: {
  role: "banker" | "player";
  sessionId: string;
  hasUnread?: boolean;
}) {
  const { t } = useI18n();
  return (
    <nav className="tabs-nav">
      {role === "banker" ? (
        <>
          <NavLink
            to={`/play/${sessionId}/banker/dashboard`}
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            {t("tabs.dashboard")}
          </NavLink>
          <NavLink
            to={`/play/${sessionId}/banker/tools`}
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            {t("tabs.tools")}
          </NavLink>
        </>
      ) : (
        <>
          <NavLink
            to={`/play/${sessionId}/player/home`}
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            {t("tabs.home")}
          </NavLink>
          <NavLink
            to={`/play/${sessionId}/player/transfers`}
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            {t("tabs.transfers")}
          </NavLink>
        </>
      )}
      <NavLink
        to={`/play/${sessionId}/chat`}
        className={({ isActive }) => (isActive ? "active" : "")}
      >
        {t("tabs.chat")}
        {hasUnread && <span className="tab-unread" />}
      </NavLink>
    </nav>
  );
}

function EntryPage({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  const navigate = useNavigate();
  const { t } = useI18n();
  const [createName, setCreateName] = useState("");
  const [joinCode, setJoinCode] = useState("");
  const [joinName, setJoinName] = useState("");
  const [joinStep, setJoinStep] = useState<"code" | "choice">("code");
  const [joinPreview, setJoinPreview] = useState<SessionPreview | null>(null);
  const [takeoverDummyId, setTakeoverDummyId] = useState("");
  const [takeoverName, setTakeoverName] = useState("");

  const stored = readStoredSession();
  const storedPlayer = joinPreview?.players.find((player) => player.id === stored?.playerId);
  const takeoverDisabled = storedPlayer?.connected === true;
  const dummyOptions = joinPreview?.players.filter((player) => player.isDummy) ?? [];

  useEffect(() => {
    if (joinStep === "choice" && joinPreview && joinCode !== joinPreview.code) {
      setJoinStep("code");
      setJoinPreview(null);
      setJoinName("");
      setTakeoverDummyId("");
    }
  }, [joinCode, joinPreview, joinStep]);

  return (
    <div className="play-shell">
      <header className="play-header">
        <div className="brand">
          <h1>{t("app.name")}</h1>
          <span>{t("entry.tagline")}</span>
        </div>
        <div className="pills">
          <span className="pill">{t("entry.liveSessions")}</span>
          <span className="pill gold">{t("entry.bankerControlled")}</span>
        </div>
      </header>

      <div className="entry-layout">
        <section
          className="card reveal entry-panel create"
          style={{ "--delay": "0.05s" } as React.CSSProperties}
        >
          <h2>{t("entry.createTitle")}</h2>
          <p>{t("entry.createSubtitle")}</p>
          <div className="form">
            <label>{t("entry.bankerName")}</label>
            <input
              value={createName}
              onChange={(event) => setCreateName(event.target.value)}
              placeholder={t("entry.bankerName")}
            />
            <button
              className="button"
              type="button"
              onClick={async () => {
                const data = await manager.createSession(createName);
                if (data) {
                  navigate(`/play/${data.sessionId}/lobby`);
                }
              }}
            >
              {t("entry.openVault")}
            </button>
          </div>
        </section>

        <section
          className="card reveal entry-panel join"
          style={{ "--delay": "0.1s" } as React.CSSProperties}
        >
          <h2>{t("entry.joinTitle")}</h2>
          <p>{t("entry.joinSubtitle")}</p>
          <div className="form">
            <label>{t("entry.sessionCode")}</label>
            <input
              value={joinCode}
              onChange={(event) => setJoinCode(event.target.value.toUpperCase())}
              placeholder={t("entry.codePlaceholder")}
            />
            {joinStep === "code" && (
              <button
                className="button secondary"
                type="button"
                onClick={async () => {
                  manager.setError(null);
                  if (!joinCode.trim()) {
                    manager.setError(t("entry.alert.enterCode"));
                    return;
                  }
                  const response = await fetch(`/api/session/${joinCode}/info`);
                  if (!response.ok) {
                    manager.setError(t("entry.alert.sessionNotFound"));
                    return;
                  }
                  const data = (await response.json()) as SessionPreview;
                  setJoinPreview(data);
                  setJoinStep("choice");
                }}
              >
                {t("common.continue")}
              </button>
            )}
          </div>

          {joinStep === "choice" && joinPreview && (
            <div className="card-grid">
              <div className="form">
                <label>{t("entry.newPlayerLabel")}</label>
                <input
                  value={joinName}
                  onChange={(event) => setJoinName(event.target.value)}
                  placeholder={t("entry.playerName")}
                />
                <button
                  className="button secondary"
                  type="button"
                  onClick={async () => {
                    const data = await manager.joinSession(joinPreview.code, joinName);
                    if (data) {
                      navigate(`/play/${data.sessionId}/lobby`);
                    }
                  }}
                >
                  {t("entry.joinAsNew")}
                </button>
              </div>

              <div className="form">
                <label>{t("entry.takeoverTitle")}</label>
                {takeoverDisabled && (
                  <p className="helper">{t("entry.alreadyConnected")}</p>
                )}
                {!takeoverDisabled && (
                  <>
                    <select
                      value={takeoverDummyId}
                      onChange={(event) => setTakeoverDummyId(event.target.value)}
                    >
                      <option value="">{t("entry.selectDummy")}</option>
                      {dummyOptions.map((dummy) => (
                        <option key={dummy.id} value={dummy.id}>
                          {dummy.name}
                        </option>
                      ))}
                    </select>
                    <input
                      value={takeoverName}
                      onChange={(event) => setTakeoverName(event.target.value)}
                      placeholder={t("entry.yourNameOptional")}
                    />
                    <button
                      className="button secondary"
                      type="button"
                      onClick={async () => {
                        if (!takeoverDummyId) {
                          manager.setError(t("entry.alert.selectDummy"));
                          return;
                        }
                        const selectedDummy = dummyOptions.find(
                          (dummy) => dummy.id === takeoverDummyId,
                        );
                        const fallbackName =
                          takeoverName.trim() || selectedDummy?.name || t("common.guest");
                        const data = await manager.joinSession(
                          joinPreview.code,
                          fallbackName,
                        );
                        if (data) {
                          manager.setPendingTakeoverId(takeoverDummyId);
                          navigate(`/play/${data.sessionId}/lobby`);
                        }
                      }}
                    >
                      {t("entry.requestTakeover")}
                    </button>
                  </>
                )}
                {!takeoverDisabled && dummyOptions.length === 0 && (
                  <p className="helper">{t("entry.noDummies")}</p>
                )}
              </div>

              <button
                className="button secondary"
                type="button"
                onClick={() => {
                  setJoinStep("code");
                  setJoinPreview(null);
                  setJoinName("");
                  setTakeoverDummyId("");
                }}
              >
                {t("entry.changeCode")}
              </button>
            </div>
          )}
        </section>
      </div>

      {manager.error && (
        <section className="card">
          <strong>{t("common.notice")}</strong> {manager.error}
        </section>
      )}
    </div>
  );
}

function LobbyPage({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const { t } = useI18n();
  const { sessionId, playerId, setError } = manager;
  const [joinName, setJoinName] = useState("");
  const [qrCode, setQrCode] = useState<string>("");
  const [joinPreview, setJoinPreview] = useState<SessionPreview | null>(null);
  const [takeoverDummyId, setTakeoverDummyId] = useState("");
  const [takeoverName, setTakeoverName] = useState("");
  const [dummyName, setDummyName] = useState("");
  const [dummyBalance, setDummyBalance] = useState("1500");

  useEffect(() => {
    if (manager.session && manager.me && manager.session.status === "active") {
      navigate(
        manager.isBanker
          ? `/play/${manager.sessionId}/banker/dashboard`
          : `/play/${manager.sessionId}/player/home`,
      );
    }
  }, [manager.session, manager.me, manager.isBanker, manager.sessionId, navigate]);

  useEffect(() => {
    if (!sessionId) return;
    const joinUrl = `${window.location.origin}/play/${sessionId}`;
    QRCode.toDataURL(joinUrl, { width: 240, margin: 1 })
      .then((url) => setQrCode(url))
      .catch(() => setQrCode(""));
  }, [sessionId]);

  useEffect(() => {
    if (!sessionId || playerId) return;
    fetch(`/api/session/${sessionId}/info`)
      .then((response) => (response.ok ? response.json() : null))
      .then((data) => {
        if (data) {
          setJoinPreview(data as SessionPreview);
        }
      })
      .catch(() => {
        setError(t("lobby.errorLoadInfo"));
      });
  }, [sessionId, playerId, setError, t]);

  if (!sessionId) {
    return <Navigate to="/play" replace />;
  }

  if (!playerId) {
    const dummyOptions = joinPreview?.players.filter((player) => player.isDummy) ?? [];
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("lobby.title")}</h1>
            <span>{t("lobby.sessionLabel", { id: manager.sessionId })}</span>
          </div>
        </header>
        <section className="card">
          <h2>{t("lobby.joinTitle")}</h2>
          {!joinPreview && <p className="helper">{t("lobby.loadingInfo")}</p>}
          {joinPreview && (
            <div className="card-grid">
              <div className="form">
                <label>{t("entry.newPlayerLabel")}</label>
                <input
                  value={joinName}
                  onChange={(event) => setJoinName(event.target.value)}
                  placeholder={t("entry.playerName")}
                />
                <button
                  className="button"
                  type="button"
                  onClick={async () => {
                    const data = await manager.joinSession(joinPreview.code, joinName);
                    if (data) {
                      navigate(`/play/${data.sessionId}/lobby`);
                    }
                  }}
                >
                  {t("entry.joinAsNew")}
                </button>
              </div>

              <div className="form">
                <label>{t("entry.takeoverTitle")}</label>
                <select
                  value={takeoverDummyId}
                  onChange={(event) => setTakeoverDummyId(event.target.value)}
                >
                  <option value="">{t("entry.selectDummy")}</option>
                  {dummyOptions.map((dummy) => (
                    <option key={dummy.id} value={dummy.id}>
                      {dummy.name}
                    </option>
                  ))}
                </select>
                <input
                  value={takeoverName}
                  onChange={(event) => setTakeoverName(event.target.value)}
                  placeholder={t("entry.yourNameOptional")}
                />
                <button
                  className="button secondary"
                  type="button"
                  onClick={async () => {
                    if (!takeoverDummyId) {
                      manager.setError(t("entry.alert.selectDummy"));
                      return;
                    }
                    const selectedDummy = dummyOptions.find(
                      (dummy) => dummy.id === takeoverDummyId,
                    );
                    const fallbackName =
                      takeoverName.trim() || selectedDummy?.name || t("common.guest");
                    const data = await manager.joinSession(
                      joinPreview.code,
                      fallbackName,
                    );
                    if (data) {
                      manager.setPendingTakeoverId(takeoverDummyId);
                      navigate(`/play/${data.sessionId}/lobby`);
                    }
                  }}
                >
                  {t("entry.requestTakeover")}
                </button>
                {dummyOptions.length === 0 && (
                  <p className="helper">{t("entry.noDummies")}</p>
                )}
              </div>
            </div>
          )}
        </section>
        {manager.error && (
          <section className="card">
            <strong>{t("common.notice")}</strong> {manager.error}
          </section>
        )}
      </div>
    );
  }

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("lobby.title")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
          <div className="pills">
            <span className="pill">{formatConnectionState(manager.connectionState, t)}</span>
          </div>
        </header>
        <section className="card">
          <p>{t("lobby.waitingState")}</p>
          <div className="inline">
            <button
              className="button secondary"
              type="button"
              onClick={() => {
                manager.resetSession();
                navigate("/play");
              }}
            >
              {t("common.reset")}
            </button>
          </div>
        </section>
      </div>
    );
  }

  const players = manager.players;
  const pendingRequests = manager.session?.takeoverRequests.filter(
    (request) => request.status === "pending",
  );

  return (
    <div className="play-shell">
      <header className="play-header">
        <div className="brand">
          <h1>{t("lobby.header", { code: manager.session.code })}</h1>
          <span>
            {t("lobby.statusLine", {
              status: formatStatus(manager.session.status, t),
              count: players.length,
            })}
          </span>
        </div>
        <div className="pills">
          <span className="pill">{manager.me.name}</span>
          <span className="pill gold">
            {manager.isBanker ? t("common.banker") : t("common.player")}
          </span>
        </div>
      </header>

      <div className="layout">
        <section className="card reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
          <h2>{t("lobby.roster")}</h2>
          <div className="list">
            {players.map((player) => (
              <div key={player.id} className="list-item">
                <div>
                  <strong>{player.name}</strong>
                  <span>{player.role === "banker" ? t("common.banker") : t("common.player")}</span>
                </div>
                <div className="inline">
                  {player.isDummy && <span className="badge dummy">{t("common.dummy")}</span>}
                  {!player.connected && (
                    <span className="badge offline">{t("common.offline")}</span>
                  )}
                </div>
              </div>
            ))}
          </div>
          {manager.isBanker && manager.session.status !== "active" && (
            <button
              className="button"
              type="button"
              onClick={() =>
                manager.sendMessage({
                  type: "banker_start",
                  sessionId: manager.sessionId,
                  bankerId: manager.me.id,
                })
              }
            >
              {t("lobby.startGame")}
            </button>
          )}
          {!manager.isBanker && (
            <p className="helper">{t("lobby.waitingBanker")}</p>
          )}
          {manager.session.status === "ended" && (
            <p className="helper">{t("lobby.sessionClosed")}</p>
          )}
        </section>

        {manager.isBanker && (pendingRequests ?? []).length > 0 && (
          <section className="card reveal" style={{ "--delay": "0.08s" } as React.CSSProperties}>
            <h2>{t("banker.takeoverApprovals")}</h2>
            <div className="list">
              {(pendingRequests ?? []).map((request) => {
                const requester = manager.session?.players.find(
                  (player) => player.id === request.requesterId,
                );
                const dummy = manager.session?.players.find(
                  (player) => player.id === request.dummyId,
                );
                const requesterName =
                  requester?.name ?? request.requesterName ?? t("common.player");
                return (
                  <div key={request.id} className="list-item">
                    <div>
                      <strong>{requesterName}</strong>
                      <span>
                        {t("banker.wants", { name: dummy?.name ?? t("common.dummy") })}
                      </span>
                    </div>
                    <button
                      className="button small"
                      type="button"
                      onClick={() =>
                        manager.sendMessage({
                          type: "banker_takeover_approve",
                          sessionId: manager.sessionId,
                          bankerId: manager.me.id,
                          dummyId: request.dummyId,
                          requesterId: request.requesterId,
                        })
                      }
                    >
                      {t("banker.approve")}
                    </button>
                  </div>
                );
              })}
            </div>
          </section>
        )}

        {manager.isBanker && (
          <>
            <section className="card reveal" style={{ "--delay": "0.1s" } as React.CSSProperties}>
              <h2>{t("lobby.inviteQr")}</h2>
              <p>{t("lobby.scanToJoin")}</p>
              <div className="qr">
                {qrCode ? <img src={qrCode} alt={t("lobby.inviteQr")} /> : ""}
              </div>
            </section>

            <section className="card reveal" style={{ "--delay": "0.12s" } as React.CSSProperties}>
              <h2>{t("lobby.addDummyTitle")}</h2>
              <p>{t("lobby.addDummySubtitle")}</p>
              <div className="form">
                <label>{t("common.name")}</label>
                <input
                  value={dummyName}
                  onChange={(event) => setDummyName(event.target.value)}
                  placeholder={t("banker.dummyName")}
                />
                <label>{t("common.startingBalance")}</label>
                <input
                  value={dummyBalance}
                  onChange={(event) => setDummyBalance(event.target.value)}
                  placeholder={t("common.startingBalance")}
                />
                <button
                  className="button secondary"
                  type="button"
                  onClick={() => {
                    if (!dummyName.trim()) {
                      manager.setError(t("lobby.enterDummyName"));
                      return;
                    }
                    manager.sendMessage({
                      type: "banker_create_dummy",
                      sessionId: manager.sessionId,
                      bankerId: manager.me.id,
                      name: dummyName,
                      balance: Number(dummyBalance) || undefined,
                    });
                    setDummyName("");
                    setDummyBalance("1500");
                  }}
                >
                  {t("lobby.addDummyButton")}
                </button>
              </div>
            </section>
          </>
        )}
      </div>

      {manager.error && (
        <section className="card">
          <strong>{t("common.notice")}</strong> {manager.error}
        </section>
      )}
    </div>
  );
}

function BankerPage({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const params = useParams();
  const { t } = useI18n();
  const tab = params.tab ?? "dashboard";
  const [adjustTarget, setAdjustTarget] = useState("");
  const [adjustAmount, setAdjustAmount] = useState("");
  const [adjustNote, setAdjustNote] = useState("");

  const [forceFrom, setForceFrom] = useState("");
  const [forceTo, setForceTo] = useState("");
  const [forceAmount, setForceAmount] = useState("");
  const [forceNote, setForceNote] = useState("");

  const [dummyName, setDummyName] = useState("");
  const [dummyBalance, setDummyBalance] = useState("1500");

  const [blackoutReason, setBlackoutReason] = useState("");
  const [toolsTab, setToolsTab] = useState<"players" | "admin">("players");
  const [selectedPlayerId, setSelectedPlayerId] = useState("");

  const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
  const [autoSaveInterval, setAutoSaveInterval] = useState("3");
  const [autoSaveLimit, setAutoSaveLimit] = useState("5");
  const [autoSaveEntries, setAutoSaveEntries] = useState<
    { id: string; savedAt: number; state: SessionSnapshot }[]
  >([]);
  const [autoSaveStatus, setAutoSaveStatus] = useState<string | null>(null);
  const [loadStatus, setLoadStatus] = useState<string | null>(null);

  const autoSaveKey = useMemo(
    () => (manager.sessionId ? `negopoly:autosave:${manager.sessionId}` : ""),
    [manager.sessionId],
  );
  const autoSaveSettingsKey = useMemo(
    () => (manager.sessionId ? `negopoly:autosave:${manager.sessionId}:settings` : ""),
    [manager.sessionId],
  );

  useEffect(() => {
    if (!autoSaveSettingsKey || !autoSaveKey) return;
    try {
      const settingsRaw = localStorage.getItem(autoSaveSettingsKey);
      if (settingsRaw) {
        const settings = JSON.parse(settingsRaw) as {
          enabled?: boolean;
          intervalMinutes?: number;
          maxEntries?: number;
        };
        setAutoSaveEnabled(Boolean(settings.enabled));
        if (settings.intervalMinutes) {
          setAutoSaveInterval(String(settings.intervalMinutes));
        }
        if (settings.maxEntries) {
          setAutoSaveLimit(String(settings.maxEntries));
        }
      }
    } catch {
      // ignore invalid settings
    }

    try {
      const savedRaw = localStorage.getItem(autoSaveKey);
      if (savedRaw) {
        const entries = JSON.parse(savedRaw) as {
          id: string;
          savedAt: number;
          state: SessionSnapshot;
        }[];
        setAutoSaveEntries(entries);
      } else {
        setAutoSaveEntries([]);
      }
    } catch {
      setAutoSaveEntries([]);
    }
  }, [autoSaveKey, autoSaveSettingsKey]);

  useEffect(() => {
    if (!autoSaveSettingsKey) return;
    const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3);
    const maxEntries = Math.max(1, Number(autoSaveLimit) || 5);
    localStorage.setItem(
      autoSaveSettingsKey,
      JSON.stringify({
        enabled: autoSaveEnabled,
        intervalMinutes,
        maxEntries,
      }),
    );
  }, [autoSaveEnabled, autoSaveInterval, autoSaveLimit, autoSaveSettingsKey]);

  useEffect(() => {
    if (!manager.sessionId) {
      navigate("/play");
      return;
    }
    if (!manager.playerId) {
      navigate(`/play/${manager.sessionId}/lobby`);
      return;
    }
    if (manager.session && manager.session.status !== "active") {
      navigate(`/play/${manager.sessionId}/lobby`);
    }
  }, [manager.session, manager.sessionId, manager.playerId, navigate]);

  const players = manager.players;
  const eligiblePlayers = useMemo(
    () => players.filter((player) => player.role !== "banker"),
    [players],
  );
  const pendingRequests = manager.session?.takeoverRequests.filter(
    (request) => request.status === "pending",
  );
  const showPending = (pendingRequests ?? []).length > 0;
  const { active: blackoutActive } = getBlackoutState(
    manager.session,
    manager.tick,
  );
  const showBlackout = blackoutActive && !manager.isBanker;
  const selectedPlayer =
    eligiblePlayers.find((player) => player.id === selectedPlayerId) ?? null;
  const playerTransactions =
    manager.session?.transactions.filter(
      (transaction) =>
        transaction.fromId === selectedPlayerId || transaction.toId === selectedPlayerId,
    ) ?? [];

  useEffect(() => {
    if (eligiblePlayers.length === 0) {
      if (selectedPlayerId) {
        setSelectedPlayerId("");
      }
      return;
    }
    if (!selectedPlayerId || !eligiblePlayers.some((player) => player.id === selectedPlayerId)) {
      setSelectedPlayerId(eligiblePlayers[0].id);
    }
  }, [eligiblePlayers, selectedPlayerId]);

  useEffect(() => {
    if (!selectedPlayerId) return;
    setAdjustTarget(selectedPlayerId);
    setForceFrom(selectedPlayerId);
    if (!forceTo || forceTo === selectedPlayerId) {
      const fallback = eligiblePlayers.find((player) => player.id !== selectedPlayerId);
      setForceTo(fallback?.id ?? "");
    }
  }, [selectedPlayerId, eligiblePlayers, forceTo]);

  async function fetchGameState(): Promise<SessionSnapshot | null> {
    if (!manager.sessionId || !manager.me) return null;
    const response = await fetch(
      `/api/session/${manager.sessionId}/state?bankerId=${manager.me.id}`,
    );
    if (!response.ok) {
      return null;
    }
    return (await response.json()) as SessionSnapshot;
  }

  function persistAutoSaves(entries: { id: string; savedAt: number; state: SessionSnapshot }[]) {
    if (!autoSaveKey) return;
    localStorage.setItem(autoSaveKey, JSON.stringify(entries));
  }

  async function handleDownloadState() {
    setAutoSaveStatus(null);
    const snapshot = await fetchGameState();
    if (!snapshot) {
      setAutoSaveStatus(t("banker.stateDownloadError"));
      return;
    }
    const blob = new Blob([JSON.stringify(snapshot, null, 2)], {
      type: "application/json",
    });
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement("a");
    anchor.href = url;
    anchor.download = `negopoly-${snapshot.code}-${new Date().toISOString()}.json`;
    anchor.click();
    URL.revokeObjectURL(url);
    setAutoSaveStatus(t("banker.stateDownloaded"));
  }

  async function handleLoadState(snapshot: SessionSnapshot) {
    if (!manager.sessionId || !manager.me) return;
    setLoadStatus(null);
    const response = await fetch(`/api/session/${manager.sessionId}/state`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ bankerId: manager.me.id, state: snapshot }),
    });
    if (!response.ok) {
      setLoadStatus(t("banker.stateLoadError"));
      return;
    }
    setLoadStatus(t("banker.stateLoaded"));
  }

  async function handleAutoSaveNow() {
    if (!autoSaveKey) return;
    const snapshot = await fetchGameState();
    if (!snapshot) {
      setAutoSaveStatus(t("banker.autosaveFailed"));
      return;
    }
    const maxEntries = Math.max(1, Number(autoSaveLimit) || 5);
    const id =
      typeof crypto !== "undefined" && "randomUUID" in crypto
        ? crypto.randomUUID()
        : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
    const nextEntry = { id, savedAt: Date.now(), state: snapshot };
    setAutoSaveEntries((prev) => {
      const nextEntries = [nextEntry, ...prev].slice(0, maxEntries);
      persistAutoSaves(nextEntries);
      return nextEntries;
    });
    setAutoSaveStatus(t("banker.autosaveSaved"));
  }

  useEffect(() => {
    if (!autoSaveEnabled || !autoSaveKey) return;
    const intervalMinutes = Math.max(1, Number(autoSaveInterval) || 3);
    const intervalMs = intervalMinutes * 60 * 1000;
    const timer = setInterval(() => {
      handleAutoSaveNow();
    }, intervalMs);
    return () => clearInterval(timer);
  }, [autoSaveEnabled, autoSaveInterval, autoSaveKey, autoSaveLimit]);

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("banker.consoleTitle")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
        </header>
      </div>
    );
  }

  if (!manager.isBanker) {
    return <Navigate to={`/play/${manager.sessionId}/player/home`} replace />;
  }

  const unreadThreadIds = getUnreadThreadIds(
    manager.session,
    manager.me.id,
    manager.isBanker,
    manager.chatReadState,
  );
  const hasUnread = unreadThreadIds.size > 0;

  return (
    <div className="play-shell">
      {showBlackout && (
        <div className="blackout">
          <div>
            <h2>{t("blackout.title")}</h2>
            <span>
              {t("blackout.active")} ·{" "}
              {manager.session.blackoutReason || t("blackout.defaultReason")}
            </span>
          </div>
        </div>
      )}

      <header className="play-header">
        <div className="brand">
          <h1>{t("banker.consoleTitle")}</h1>
          <span>{t("common.sessionLive", { code: manager.session.code })}</span>
        </div>
        <div className="pills">
          <span className="pill">{manager.me.name}</span>
          <span className="pill gold">{t("common.banker")}</span>
        </div>
      </header>

      <div className="tabs-layout">
        <GameTabs role="banker" sessionId={manager.sessionId} hasUnread={hasUnread} />
        <div className="tabs-content">
          {(() => {
            const validTabs = ["dashboard", "tools"];
            const currentTab = tab ?? "dashboard";
            if (!validTabs.includes(currentTab)) {
              return <Navigate to={`/play/${manager.sessionId}/banker/dashboard`} replace />;
            }

            if (currentTab === "tools") {
              return (
                <div className="tools-container">
                  <div className="tools-tabs">
                    {[
                      { id: "players", label: t("banker.tools.playersTab") },
                      { id: "admin", label: t("banker.tools.adminTab") },
                    ].map((item) => (
                      <button
                        key={item.id}
                        className={`tools-tab ${toolsTab === item.id ? "active" : ""}`}
                        type="button"
                        onClick={() => setToolsTab(item.id as "players" | "admin")}
                      >
                        {item.label}
                      </button>
                    ))}
                  </div>

                  {toolsTab === "players" ? (
                    <div className="tools-layout">
                      <section className="card reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
                        <h2>{t("banker.playersTitle")}</h2>
                        {eligiblePlayers.length === 0 ? (
                          <span className="helper">{t("banker.noPlayers")}</span>
                        ) : (
                          <div className="player-roster">
                            {eligiblePlayers.map((player) => {
                              const active = player.id === selectedPlayerId;
                              return (
                                <button
                                  key={player.id}
                                  type="button"
                                  className={`player-roster-item ${active ? "active" : ""}`}
                                  onClick={() => setSelectedPlayerId(player.id)}
                                >
                                  <div className="player-roster-main">
                                    <strong>{player.name}</strong>
                                    <span>
                                      {player.isDummy ? t("common.dummy") : t("common.player")} ·{" "}
                                      {player.connected ? t("common.online") : t("common.offline")}
                                    </span>
                                  </div>
                                  <div className="player-roster-meta">
                                    {player.isDummy && <span className="badge dummy">{t("common.dummy")}</span>}
                                    {!player.connected && (
                                      <span className="badge offline">{t("common.offline")}</span>
                                    )}
                                    <span className="player-roster-balance">
                                      {formatMoney(player.balance)}
                                    </span>
                                  </div>
                                </button>
                              );
                            })}
                          </div>
                        )}
                      </section>

                      <section className="card reveal" style={{ "--delay": "0.08s" } as React.CSSProperties}>
                        <h2>{t("banker.playerOverview")}</h2>
                        {selectedPlayer ? (
                          <>
                            <div className="player-summary">
                              <div>
                                <strong>{selectedPlayer.name}</strong>
                                <span>
                                  {selectedPlayer.isDummy ? t("common.dummy") : t("common.player")} ·{" "}
                                  {selectedPlayer.connected ? t("common.online") : t("common.offline")}
                                </span>
                              </div>
                              <div className="player-balance">{formatMoney(selectedPlayer.balance)}</div>
                            </div>
                            <div className="list scrollable">
                              {playerTransactions.length === 0 && (
                                <span className="helper">{t("common.noActivity")}</span>
                              )}
                              {playerTransactions.map((transaction) => {
                                const display = getTransactionDisplay(
                                  transaction,
                                  selectedPlayerId,
                                  manager.session?.players ?? [],
                                  t,
                                );
                                return (
                                  <div key={transaction.id} className="list-item">
                                    <div>
                                      <strong>{display.label}</strong>
                                      <span>{display.subtitle}</span>
                                    </div>
                                    <div className={`amount ${display.tone}`}>{display.amount}</div>
                                  </div>
                                );
                              })}
                            </div>
                          </>
                        ) : (
                          <span className="helper">{t("banker.noPlayers")}</span>
                        )}
                      </section>

                      <section className="card reveal" style={{ "--delay": "0.1s" } as React.CSSProperties}>
                        <h2>{t("banker.controlsTitle")}</h2>
                        <div className="split">
                          <div className="form">
                            <label>{t("banker.adjustBalance")}</label>
                            <select
                              value={adjustTarget}
                              onChange={(event) => setAdjustTarget(event.target.value)}
                            >
                              <option value="">{t("common.selectPlayer")}</option>
                              {eligiblePlayers.map((player) => (
                                <option key={player.id} value={player.id}>
                                  {player.name}
                                </option>
                              ))}
                            </select>
                            <input
                              value={adjustAmount}
                              onChange={(event) => setAdjustAmount(event.target.value)}
                              placeholder={t("banker.adjustAmountPlaceholder")}
                            />
                            <input
                              value={adjustNote}
                              onChange={(event) => setAdjustNote(event.target.value)}
                              placeholder={t("common.reason")}
                            />
                            <button
                              className="button"
                              type="button"
                              onClick={() =>
                                manager.sendMessage({
                                  type: "banker_adjust",
                                  sessionId: manager.sessionId,
                                  bankerId: manager.me.id,
                                  targetId: adjustTarget,
                                  amount: Number(adjustAmount),
                                  note: adjustNote,
                                })
                              }
                            >
                              {t("common.apply")}
                            </button>
                          </div>

                          <div className="form">
                            <label>{t("banker.forceTransfer")}</label>
                            <select
                              value={forceFrom}
                              onChange={(event) => setForceFrom(event.target.value)}
                            >
                              <option value="">{t("common.from")}</option>
                              {eligiblePlayers.map((player) => (
                                <option key={player.id} value={player.id}>
                                  {player.name}
                                </option>
                              ))}
                            </select>
                            <select value={forceTo} onChange={(event) => setForceTo(event.target.value)}>
                              <option value="">{t("common.to")}</option>
                              {eligiblePlayers.map((player) => (
                                <option key={player.id} value={player.id}>
                                  {player.name}
                                </option>
                              ))}
                            </select>
                            <input
                              value={forceAmount}
                              onChange={(event) => setForceAmount(event.target.value)}
                              placeholder={t("common.amount")}
                            />
                            <input
                              value={forceNote}
                              onChange={(event) => setForceNote(event.target.value)}
                              placeholder={t("common.note")}
                            />
                            <button
                              className="button secondary"
                              type="button"
                              onClick={() =>
                                manager.sendMessage({
                                  type: "banker_force_transfer",
                                  sessionId: manager.sessionId,
                                  bankerId: manager.me.id,
                                  fromId: forceFrom,
                                  toId: forceTo,
                                  amount: Number(forceAmount),
                                  note: forceNote,
                                })
                              }
                            >
                              {t("common.force")}
                            </button>
                          </div>
                        </div>
                      </section>

                      <section className="card reveal" style={{ "--delay": "0.12s" } as React.CSSProperties}>
                        <h2>{t("banker.createDummy")}</h2>
                        <div className="form">
                          <input
                            value={dummyName}
                            onChange={(event) => setDummyName(event.target.value)}
                            placeholder={t("banker.dummyName")}
                          />
                          <input
                            value={dummyBalance}
                            onChange={(event) => setDummyBalance(event.target.value)}
                            placeholder={t("common.startingBalance")}
                          />
                          <button
                            className="button secondary"
                            type="button"
                            onClick={() => {
                              manager.sendMessage({
                                type: "banker_create_dummy",
                                sessionId: manager.sessionId,
                                bankerId: manager.me.id,
                                name: dummyName,
                                balance: Number(dummyBalance) || undefined,
                              });
                              setDummyName("");
                              setDummyBalance("1500");
                            }}
                          >
                            {t("banker.addDummy")}
                          </button>
                        </div>
                      </section>
                    </div>
                  ) : (
                    <div className="tools-layout">
                      <section className="card reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
                        <h2>{t("banker.adminControls")}</h2>
                        <div className="form">
                          <label>{t("banker.blackout")}</label>
                          <label className="checkbox-row">
                            <input
                              type="checkbox"
                              checked={blackoutActive}
                              onChange={(event) =>
                                manager.sendMessage({
                                  type: "banker_blackout",
                                  sessionId: manager.sessionId,
                                  bankerId: manager.me.id,
                                  active: event.target.checked,
                                  reason: event.target.checked ? blackoutReason : null,
                                })
                              }
                            />
                            <span>{t("blackout.active")}</span>
                          </label>
                          <input
                            value={blackoutReason}
                            onChange={(event) => setBlackoutReason(event.target.value)}
                            placeholder={t("banker.blackoutReason")}
                          />
                          <button
                            className="button danger"
                            type="button"
                            onClick={() =>
                              manager.sendMessage({
                                type: "banker_blackout",
                                sessionId: manager.sessionId,
                                bankerId: manager.me.id,
                                active: !blackoutActive,
                                reason: !blackoutActive ? blackoutReason : null,
                              })
                            }
                          >
                            {blackoutActive ? t("banker.blackoutDisable") : t("banker.blackoutEnable")}
                          </button>
                        </div>
                        <button
                          className="button danger"
                          type="button"
                          onClick={() =>
                            manager.sendMessage({
                              type: "banker_end",
                              sessionId: manager.sessionId,
                              bankerId: manager.me.id,
                            })
                          }
                        >
                          {t("banker.endSession")}
                        </button>
                      </section>

                      <section className="card reveal" style={{ "--delay": "0.08s" } as React.CSSProperties}>
                        <h2>{t("banker.stateTitle")}</h2>
                        <p>{t("banker.stateSubtitle")}</p>
                        <div className="split">
                          <div className="form">
                            <label>{t("banker.downloadState")}</label>
                            <button className="button" type="button" onClick={handleDownloadState}>
                              {t("common.download")}
                            </button>
                            {autoSaveStatus ? <span className="helper">{autoSaveStatus}</span> : null}
                          </div>

                          <div className="form">
                            <label>{t("banker.loadFromFile")}</label>
                            <input
                              type="file"
                              accept="application/json"
                              onChange={(event) => {
                                const file = event.target.files?.[0];
                                if (!file) return;
                                const reader = new FileReader();
                                reader.onload = () => {
                                  try {
                                    const parsed = JSON.parse(String(reader.result));
                                    handleLoadState(parsed as SessionSnapshot);
                                  } catch {
                                    setLoadStatus(t("banker.stateLoadInvalid"));
                                  }
                                };
                                reader.readAsText(file);
                              }}
                            />
                            {loadStatus ? <span className="helper">{loadStatus}</span> : null}
                          </div>

                          <div className="form">
                            <label>{t("banker.loadFromStorage")}</label>
                            <div className="autosave-list">
                              {autoSaveEntries.length === 0 ? (
                                <span className="helper">{t("banker.noAutosaves")}</span>
                              ) : (
                                autoSaveEntries.map((entry) => (
                                  <div key={entry.id} className="autosave-item">
                                    <span className="autosave-meta">
                                      {t("banker.savedAt", {
                                        time: new Date(entry.savedAt).toLocaleString(),
                                      })}
                                    </span>
                                    <button
                                      className="button small"
                                      type="button"
                                      onClick={() => handleLoadState(entry.state)}
                                    >
                                      {t("common.load")}
                                    </button>
                                  </div>
                                ))
                              )}
                            </div>
                          </div>
                        </div>
                      </section>

                      <section className="card reveal" style={{ "--delay": "0.1s" } as React.CSSProperties}>
                        <h2>{t("banker.autosaveTitle")}</h2>
                        <p>{t("banker.autosaveSubtitle")}</p>
                        <div className="split">
                          <div className="form">
                            <label>{t("banker.autosaveToggle")}</label>
                            <label className="checkbox-row">
                              <input
                                type="checkbox"
                                checked={autoSaveEnabled}
                                onChange={(event) => setAutoSaveEnabled(event.target.checked)}
                              />
                              <span>{t("banker.autosaveEnabled")}</span>
                            </label>
                          </div>
                          <div className="form">
                            <label>{t("banker.autosaveInterval")}</label>
                            <input
                              value={autoSaveInterval}
                              onChange={(event) => setAutoSaveInterval(event.target.value)}
                              placeholder={t("banker.autosaveMinutes")}
                            />
                          </div>
                          <div className="form">
                            <label>{t("banker.autosaveKeep")}</label>
                            <input
                              value={autoSaveLimit}
                              onChange={(event) => setAutoSaveLimit(event.target.value)}
                              placeholder={t("banker.autosaveCount")}
                            />
                          </div>
                          <div className="form">
                            <label>{t("banker.autosaveNow")}</label>
                            <button className="button secondary" type="button" onClick={handleAutoSaveNow}>
                              {t("common.save")}
                            </button>
                          </div>
                        </div>
                      </section>

                      {showPending && (
                        <section className="card reveal" style={{ "--delay": "0.12s" } as React.CSSProperties}>
                          <h2>{t("banker.takeoverApprovals")}</h2>
                          <div className="list">
                            {(pendingRequests ?? []).map((request) => {
                              const requester = manager.session?.players.find(
                                (player) => player.id === request.requesterId,
                              );
                              const dummy = manager.session?.players.find(
                                (player) => player.id === request.dummyId,
                              );
                              const requesterName =
                                requester?.name ?? request.requesterName ?? t("common.player");
                              return (
                                <div key={request.id} className="list-item">
                                  <div>
                                    <strong>{requesterName}</strong>
                                    <span>
                                      {t("banker.wants", { name: dummy?.name ?? t("common.dummy") })}
                                    </span>
                                  </div>
                                  <button
                                    className="button small"
                                    type="button"
                                    onClick={() =>
                                      manager.sendMessage({
                                        type: "banker_takeover_approve",
                                        sessionId: manager.sessionId,
                                        bankerId: manager.me.id,
                                        dummyId: request.dummyId,
                                        requesterId: request.requesterId,
                                      })
                                    }
                                  >
                                    {t("banker.approve")}
                                  </button>
                                </div>
                              );
                            })}
                          </div>
                        </section>
                      )}
                    </div>
                  )}
                </div>
              );
            }

            return (
              <div className="layout">
                <div className="card-grid">
                  <section className="card reveal" style={{ "--delay": "0.08s" } as React.CSSProperties}>
                    <h2>{t("common.transactions")}</h2>
                    <div className="list">
                      {manager.session.transactions.length === 0 && (
                        <span className="helper">{t("common.noActivity")}</span>
                      )}
                      {manager.session.transactions.slice(0, 8).map((transaction) => {
                        const display = getTransactionDisplay(
                          transaction,
                          null,
                          manager.session?.players ?? [],
                          t,
                        );
                        return (
                          <div key={transaction.id} className="list-item">
                            <div>
                              <strong>{display.label}</strong>
                              <span>{display.subtitle}</span>
                            </div>
                            <div className={`amount ${display.tone}`}>{display.amount}</div>
                          </div>
                        );
                      })}
                    </div>
                  </section>
                </div>
              </div>
            );
          })()}
        </div>
      </div>

      {manager.error && (
        <section className="card">
          <strong>{t("common.notice")}</strong> {manager.error}
        </section>
      )}
    </div>
  );
}

function PlayerPage({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const params = useParams();
  const { t } = useI18n();
  const tab = params.tab ?? "home";
  const [transferTarget, setTransferTarget] = useState("");
  const [transferAmount, setTransferAmount] = useState("");
  const [transferNote, setTransferNote] = useState("");

  useEffect(() => {
    if (!manager.sessionId) {
      navigate("/play");
      return;
    }
    if (!manager.playerId) {
      navigate(`/play/${manager.sessionId}/lobby`);
      return;
    }
    if (manager.session && manager.session.status !== "active") {
      navigate(`/play/${manager.sessionId}/lobby`);
    }
  }, [manager.session, manager.sessionId, manager.playerId, navigate]);

  const players = manager.players;
  const activePlayers = players.filter(
    (player) => !player.isDummy && player.role !== "banker",
  );
  const visibleTransactions = manager.session?.transactions.filter(
    (transaction) => transaction.fromId === manager.me?.id || transaction.toId === manager.me?.id,
  );
  const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick);
  const showBlackout = blackoutActive && !manager.isBanker;

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("player.deskTitle")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
        </header>
      </div>
    );
  }

  if (manager.isBanker) {
    return <Navigate to={`/play/${manager.sessionId}/banker/dashboard`} replace />;
  }

  const unreadThreadIds = getUnreadThreadIds(
    manager.session,
    manager.me.id,
    manager.isBanker,
    manager.chatReadState,
  );
  const hasUnread = unreadThreadIds.size > 0;

  return (
    <div className="play-shell">
      {showBlackout && (
        <div className="blackout">
          <div>
            <h2>{t("blackout.title")}</h2>
            <span>
              {t("blackout.active")} ·{" "}
              {manager.session.blackoutReason || t("blackout.defaultReason")}
            </span>
          </div>
        </div>
      )}

      <header className="play-header">
        <div className="brand">
          <h1>{t("player.deskTitle")}</h1>
          <span>{t("common.sessionLive", { code: manager.session.code })}</span>
        </div>
        <div className="pills">
          <span className="pill">{manager.me.name}</span>
          <span className="pill gold">{t("common.player")}</span>
        </div>
      </header>

      <div className="tabs-layout">
        <GameTabs role="player" sessionId={manager.sessionId} hasUnread={hasUnread} />
        <div className="tabs-content">
          {(() => {
            const validTabs = ["home", "transfers"];
            const currentTab = tab ?? "home";
            if (!validTabs.includes(currentTab)) {
              return <Navigate to={`/play/${manager.sessionId}/player/home`} replace />;
            }

            if (currentTab === "transfers") {
              return (
                <div className="layout">
                  <div className="card-grid">
                    <section className="card reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
                      <h2>{t("player.quickTransfer")}</h2>
                      <div className="form">
                        <label>{t("player.sendTo")}</label>
                        <select
                          value={transferTarget}
                          onChange={(event) => setTransferTarget(event.target.value)}
                        >
                          <option value="">{t("common.selectPlayer")}</option>
                          {activePlayers
                            .filter((player) => player.id !== manager.me?.id)
                            .map((player) => (
                              <option key={player.id} value={player.id}>
                                {player.name}
                              </option>
                            ))}
                        </select>
                        <label>{t("common.amount")}</label>
                        <input
                          value={transferAmount}
                          onChange={(event) => setTransferAmount(event.target.value)}
                          placeholder={t("common.amount")}
                        />
                        <label>{t("player.noteOptional")}</label>
                        <input
                          value={transferNote}
                          onChange={(event) => setTransferNote(event.target.value)}
                          placeholder={t("player.notePlaceholder")}
                        />
                        <button
                          className="button"
                          type="button"
                          onClick={() => {
                            if (!transferTarget || !transferAmount) {
                              manager.setError(t("transfers.error"));
                              return;
                            }
                            manager.sendMessage({
                              type: "transfer",
                              sessionId: manager.sessionId,
                              playerId: manager.me.id,
                              toPlayerId: transferTarget,
                              amount: Number(transferAmount),
                              note: transferNote,
                            });
                            setTransferAmount("");
                            setTransferNote("");
                          }}
                        >
                          {t("player.sendFunds")}
                        </button>
                      </div>
                    </section>
                  </div>
                </div>
              );
            }

            return (
              <div className="layout">
                <div className="card-grid">
                  <section className="card reveal" style={{ "--delay": "0.05s" } as React.CSSProperties}>
                    <h2>{t("home.balance")}</h2>
                    <div className="balance">
                      {formatMoney(manager.me.balance)}
                      <span>{t("player.lastUpdated", { time: formatTime(manager.me.lastActiveAt) })}</span>
                    </div>
                  </section>
                  <section className="card reveal" style={{ "--delay": "0.1s" } as React.CSSProperties}>
                    <h2>{t("common.transactions")}</h2>
                    <div className="list">
                      {(visibleTransactions ?? []).length === 0 && (
                        <span className="helper">{t("common.noActivity")}</span>
                      )}
                      {(visibleTransactions ?? []).slice(0, 6).map((transaction) => {
                        const display = getTransactionDisplay(
                          transaction,
                          manager.me?.id,
                          manager.session?.players ?? [],
                          t,
                        );
                        return (
                          <div key={transaction.id} className="list-item">
                            <div>
                              <strong>{display.label}</strong>
                              <span>{display.subtitle}</span>
                            </div>
                            <div className={`amount ${display.tone}`}>{display.amount}</div>
                          </div>
                        );
                      })}
                    </div>
                  </section>
                </div>
              </div>
            );
          })()}
        </div>
      </div>

      {manager.error && (
        <section className="card">
          <strong>{t("common.notice")}</strong> {manager.error}
        </section>
      )}
    </div>
  );
}

function ChatListRoute({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  const { t } = useI18n();

  useEffect(() => {
    if (!manager.sessionId) {
      navigate("/play");
      return;
    }
    if (!manager.playerId) {
      navigate(`/play/${manager.sessionId}/lobby`);
      return;
    }
    if (manager.session && manager.session.status !== "active" && !manager.isBanker) {
      navigate(`/play/${manager.sessionId}/lobby`);
    }
  }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]);

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("chat.title")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
        </header>
      </div>
    );
  }

  const backHref = manager.isBanker
    ? `/play/${manager.sessionId}/banker/dashboard`
    : `/play/${manager.sessionId}/player/home`;
  const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick);
  const showBlackout = blackoutActive && !manager.isBanker;
  const unreadThreadIds = getUnreadThreadIds(
    manager.session,
    manager.me.id,
    manager.isBanker,
    manager.chatReadState,
  );
  const hasUnread = unreadThreadIds.size > 0;

  return (
    <>
      {showBlackout && (
        <div className="blackout">
          <div>
            <h2>{t("blackout.title")}</h2>
            <span>
              {t("blackout.active")} ·{" "}
              {manager.session.blackoutReason || t("blackout.defaultReason")}
            </span>
          </div>
        </div>
      )}
      <div className="play-shell">
        <div className="tabs-layout">
          <GameTabs
            role={manager.isBanker ? "banker" : "player"}
            sessionId={sessionId || manager.sessionId}
            hasUnread={hasUnread}
          />
          <div className="tabs-content">
            <ChatListScreen
              session={manager.session}
              meId={manager.me.id}
              isBanker={manager.isBanker}
              sessionId={sessionId || manager.sessionId}
              backHref={backHref}
              unreadIds={unreadThreadIds}
            />
          </div>
        </div>
      </div>
    </>
  );
}

function ChatThreadRoute({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  const chatId = params.chatId ? decodeURIComponent(params.chatId) : "";
  const { t } = useI18n();

  useEffect(() => {
    if (!manager.sessionId) {
      navigate("/play");
      return;
    }
    if (!manager.playerId) {
      navigate(`/play/${manager.sessionId}/lobby`);
      return;
    }
    if (manager.session && manager.session.status !== "active" && !manager.isBanker) {
      navigate(`/play/${manager.sessionId}/lobby`);
    }
  }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]);

  useEffect(() => {
    if (!manager.session || !manager.me || !chatId) return;
    const latest = getLatestThreadTimestamp(manager.session, chatId);
    manager.markChatRead(chatId, latest ?? Date.now());
  }, [manager.session, manager.me, chatId]);

  if (!chatId) {
    return <Navigate to={`/play/${manager.sessionId}/chat`} replace />;
  }

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("tabs.chat")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
        </header>
      </div>
    );
  }
  const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick);
  const showBlackout = blackoutActive && !manager.isBanker;
  const unreadThreadIds = getUnreadThreadIds(
    manager.session,
    manager.me.id,
    manager.isBanker,
    manager.chatReadState,
  );
  const hasUnread = unreadThreadIds.size > 0;

  return (
    <>
      {showBlackout && (
        <div className="blackout">
          <div>
            <h2>{t("blackout.title")}</h2>
            <span>
              {t("blackout.active")} ·{" "}
              {manager.session.blackoutReason || t("blackout.defaultReason")}
            </span>
          </div>
        </div>
      )}
      <div className="play-shell">
        <div className="tabs-layout">
          <GameTabs
            role={manager.isBanker ? "banker" : "player"}
            sessionId={sessionId || manager.sessionId}
            hasUnread={hasUnread}
          />
          <div className="tabs-content">
            <ChatThreadScreen
              session={manager.session}
              meId={manager.me.id}
              isBanker={manager.isBanker}
              sessionId={sessionId || manager.sessionId}
              chatId={chatId}
              onSend={(body, groupId) =>
                manager.sendMessage({
                  type: "chat_send",
                  sessionId: manager.sessionId,
                  playerId: manager.me.id,
                  body,
                  groupId,
                })
              }
            />
          </div>
        </div>
      </div>
    </>
  );
}

function ChatNewRoute({ manager }: { manager: ReturnType<typeof useSessionManager> }) {
  useRouteSessionSync(manager);
  const navigate = useNavigate();
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  const { t } = useI18n();

  useEffect(() => {
    if (!manager.sessionId) {
      navigate("/play");
      return;
    }
    if (!manager.playerId) {
      navigate(`/play/${manager.sessionId}/lobby`);
      return;
    }
    if (manager.session && manager.session.status !== "active" && !manager.isBanker) {
      navigate(`/play/${manager.sessionId}/lobby`);
    }
  }, [manager.session, manager.sessionId, manager.playerId, manager.isBanker, navigate]);

  if (!manager.session || !manager.me) {
    return (
      <div className="play-shell">
        <header className="play-header">
          <div className="brand">
            <h1>{t("chat.newTitle")}</h1>
            <span>{t("common.connecting", { id: manager.sessionId })}</span>
          </div>
        </header>
      </div>
    );
  }
  const { active: blackoutActive } = getBlackoutState(manager.session, manager.tick);
  const showBlackout = blackoutActive && !manager.isBanker;
  const unreadThreadIds = getUnreadThreadIds(
    manager.session,
    manager.me.id,
    manager.isBanker,
    manager.chatReadState,
  );
  const hasUnread = unreadThreadIds.size > 0;

  return (
    <>
      {showBlackout && (
        <div className="blackout">
          <div>
            <h2>{t("blackout.title")}</h2>
            <span>
              {t("blackout.active")} ·{" "}
              {manager.session.blackoutReason || t("blackout.defaultReason")}
            </span>
          </div>
        </div>
      )}
      <div className="play-shell">
        <div className="tabs-layout">
          <GameTabs
            role={manager.isBanker ? "banker" : "player"}
            sessionId={sessionId || manager.sessionId}
            hasUnread={hasUnread}
          />
          <div className="tabs-content">
            <ChatNewScreen
              session={manager.session}
              meId={manager.me.id}
              sessionId={sessionId || manager.sessionId}
              onCreate={(name, memberIds) => {
                manager.sendMessage({
                  type: "group_create",
                  sessionId: manager.sessionId,
                  playerId: manager.me.id,
                  name,
                  memberIds,
                });
                navigate(`/play/${manager.sessionId}/chat`);
              }}
            />
          </div>
        </div>
      </div>
    </>
  );
}

function PlayRouter() {
  const manager = useSessionManager();

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/play" element={<EntryPage manager={manager} />} />
        <Route path="/play/:sessionId/lobby" element={<LobbyPage manager={manager} />} />
        <Route path="/play/:sessionId/banker" element={<RedirectToBankerDefault />} />
        <Route path="/play/:sessionId/banker/:tab" element={<BankerPage manager={manager} />} />
        <Route path="/play/:sessionId/player" element={<RedirectToPlayerDefault />} />
        <Route path="/play/:sessionId/player/:tab" element={<PlayerPage manager={manager} />} />
        <Route path="/play/:sessionId/chat/new" element={<ChatNewRoute manager={manager} />} />
        <Route path="/play/:sessionId/chat/:chatId" element={<ChatThreadRoute manager={manager} />} />
        <Route path="/play/:sessionId/chat" element={<ChatListRoute manager={manager} />} />
        <Route path="/play/:sessionId" element={<RedirectToLobby />} />
        <Route path="*" element={<Navigate to="/play" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

function RedirectToPlayerDefault() {
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  if (!sessionId) {
    return <Navigate to="/play" replace />;
  }
  return <Navigate to={`/play/${sessionId}/player/home`} replace />;
}

function RedirectToBankerDefault() {
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  if (!sessionId) {
    return <Navigate to="/play" replace />;
  }
  return <Navigate to={`/play/${sessionId}/banker/dashboard`} replace />;
}

function RedirectToLobby() {
  const params = useParams();
  const sessionId = params.sessionId ? decodeURIComponent(params.sessionId) : "";
  if (!sessionId) {
    return <Navigate to="/play" replace />;
  }
  return <Navigate to={`/play/${sessionId}/lobby`} replace />;
}

export default PlayRouter;
