/* UNO — веб-интерфейс на React (без сборки, JSX трансформирует Babel standalone) */

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

const COLORS = ["red", "yellow", "green", "blue"];
const COLOR_RU = { red: "красный", yellow: "жёлтый", green: "зелёный", blue: "синий" };
const POLL_MS = 1200;

/* Telegram Mini App: Telegram открывает страницу с tgWebAppData в URL-фрагменте */
const TG_HASH = window.location.hash || "";
const IS_TELEGRAM = TG_HASH.includes("tgWebAppData=") || /Telegram/i.test(navigator.userAgent);

/* стиль овала игрового поля для каждой темы (рамка рисуется клиентом
   по реальным позициям игроков, поэтому совпадает при любом разрешении) */
const TABLE_OVAL_STYLES = {
  default:                { border: "#deba5c", width: 5, glow: "rgba(222,186,92,0.28)",  fill: "transparent" },
  table_green_classic:    { border: "#deba5c", width: 6, glow: "rgba(222,186,92,0.32)",  fill: "transparent" },
  table_blue_ocean:       { border: "#cdddeb", width: 6, glow: "rgba(205,222,235,0.30)", fill: "transparent" },
  table_red_casino:       { border: "#deba5c", width: 7, glow: "rgba(222,186,92,0.34)",  fill: "transparent" },
  table_purple_night:     { border: "#c8aaff", width: 5, glow: "rgba(200,170,255,0.30)", fill: "transparent" },
  table_wood_royal:       { border: "#deba5c", width: 6, glow: "rgba(222,186,92,0.30)",  fill: "rgba(23, 77, 47, 0.55)" },
  table_graphite_minimal: { border: "#d2d2dc", width: 3, glow: "rgba(210,210,220,0.20)", fill: "transparent" },
};

/* Рассадка соперников: равномерно по эллипсу стола, наш игрок всегда снизу.
   Все места вместе с нашим делят круг поровну: угол k-го соперника (по часовой
   от нас, снизу) = (k+1)·360°/(N+1); веер поворачивается на угол места,
   поэтому карты каждого игрока «смотрят» в центр стола. */
function seatAngles(n) {
  return Array.from({ length: n }, (_, k) => ((k + 1) * 360) / (Math.max(n, 1) + 1));
}

function useIsDesktop() {
  const [isDesktop, setIsDesktop] = useState(
    () => !window.matchMedia || window.matchMedia("(min-width: 901px)").matches
  );
  useEffect(() => {
    if (!window.matchMedia) return undefined;
    const mq = window.matchMedia("(min-width: 901px)");
    const fn = (e) => setIsDesktop(e.matches);
    if (mq.addEventListener) mq.addEventListener("change", fn);
    else mq.addListener(fn);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", fn);
      else mq.removeListener(fn);
    };
  }, []);
  return isDesktop;
}

/* Геометрия стола для рассадки: центр — посередине между шапкой и рукой,
   радиусы эллипса с запасом под панель сиденья. Координаты относительно
   контейнера .game; пересчёт при resize/скролле и по таймеру (Telegram webview). */
function useSeatRing() {
  const [ring, setRing] = useState(null);
  const recalc = useCallback(() => {
    const game = document.querySelector(".game");
    const header = document.querySelector(".game-header");
    const hand = document.querySelector(".hand-area");
    if (!game || !header || !hand) return;
    const g = game.getBoundingClientRect();
    const hd = header.getBoundingClientRect();
    const h = hand.getBoundingClientRect();
    const top = hd.bottom - g.top + 12;
    const bottom = h.top - g.top - 12;
    setRing({
      cx: g.width / 2,
      cy: (top + bottom) / 2,
      rx: Math.max(g.width / 2 - 115, 150),
      ry: Math.max((bottom - top) / 2 - 92, 60),
    });
  }, []);
  useEffect(() => {
    recalc();
    window.addEventListener("resize", recalc);
    window.addEventListener("scroll", recalc, { passive: true });
    let ro = null;
    if (window.ResizeObserver) {
      ro = new ResizeObserver(recalc);
      ro.observe(document.body);
    }
    const iv = setInterval(recalc, 1000);
    return () => {
      window.removeEventListener("resize", recalc);
      window.removeEventListener("scroll", recalc);
      if (ro) ro.disconnect();
      clearInterval(iv);
    };
  }, [recalc]);
  return ring;
}

const AV_CLASSES = ["av-purple", "av-orange", "av-yellow", "av-green", "av-blue", "av-red"];

function hashStr(s) {
  let h = 0;
  s = String(s);
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
  return h;
}

/* детерминированный наклон карты в кучке сброса по её uid */
function rotFor(uid) {
  return (hashStr(uid) % 25) - 12;
}

/* сортировка руки:
   «color» — по масти (красные → жёлтые → зелёные → синие → дикие), внутри по типу и значению;
   «value» — по значению (все нули, все единицы…, затем действия, затем дикие), внутри по масти */
const COLOR_ORDER = { red: 0, yellow: 1, green: 2, blue: 3, black: 4 };
const KIND_ORDER = { number: 0, skip: 1, reverse: 2, draw2: 3, wild: 4, wild_draw4: 5 };

function sortHand(hand, mode) {
  if (!mode) return hand;
  const co = (c) => (c.color in COLOR_ORDER ? COLOR_ORDER[c.color] : 9);
  const ko = (c) => (c.kind in KIND_ORDER ? KIND_ORDER[c.kind] : 9);
  const no = (c) => (typeof c.number === "number" ? c.number : -1);
  const arr = hand.slice();
  if (mode === "color") {
    arr.sort((a, b) => co(a) - co(b) || ko(a) - ko(b) || no(a) - no(b));
  } else if (mode === "value") {
    arr.sort((a, b) => ko(a) - ko(b) || no(a) - no(b) || co(a) - co(b));
  }
  return arr;
}

/* Овал игрового поля: строится по bounding box соперников, колоды/сброса
   и руки, пересчитывается при resize/скролле и каждом обновлении состояния */
function TableOval({ themeId, recalcKey }) {
  const ref = useRef(null);

  const recalc = useCallback(() => {
    const el = ref.current;
    if (!el) return;
    const panels = [...document.querySelectorAll(".opponent")];
    const hand = document.querySelector(".hand");
    if (!panels.length || !hand) { el.style.opacity = "0"; return; }
    const rects = panels.map((p) => p.getBoundingClientRect());
    for (const sel of [".pile.draw", ".pile.discard"]) {
      const el2 = document.querySelector(sel);
      if (el2) rects.push(el2.getBoundingClientRect());
    }
    rects.push(hand.getBoundingClientRect());
    let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
    for (const r of rects) {
      left = Math.min(left, r.left); right = Math.max(right, r.right);
      top = Math.min(top, r.top); bottom = Math.max(bottom, r.bottom);
    }
    // запас: подъём карты при наведении, бейдж «думает…» над панелью
    const padX = 48, padTop = 30, padBottom = 44;
    el.style.opacity = "1";
    el.style.left = (left - padX) + "px";
    el.style.top = (top - padTop) + "px";
    el.style.width = ((right - left) + padX * 2) + "px";
    el.style.height = ((bottom - top) + padTop + padBottom) + "px";
  }, []);

  useEffect(() => {
    recalc();
    window.addEventListener("resize", recalc);
    window.addEventListener("scroll", recalc, { passive: true });
    // ResizeObserver + интервал: в webview Telegram resize-событие может не приходить
    let ro = null;
    if (window.ResizeObserver) {
      ro = new ResizeObserver(recalc);
      ro.observe(document.body);
    }
    const iv = setInterval(recalc, 1000);
    return () => {
      window.removeEventListener("resize", recalc);
      window.removeEventListener("scroll", recalc);
      if (ro) ro.disconnect();
      clearInterval(iv);
    };
  }, [recalc, recalcKey]);

  const s = TABLE_OVAL_STYLES[themeId] || TABLE_OVAL_STYLES.default;
  return (
    <div
      ref={ref}
      className="table-oval"
      style={{
        borderColor: s.border,
        borderWidth: s.width,
        background: s.fill,
        boxShadow: `0 0 60px ${s.glow}, inset 0 0 120px ${s.glow}`,
      }}
    />
  );
}

const img = (path) => "/" + path;

/* ------------------------------ API ---------------------------------- */

async function api(path, body) {
  const res = await fetch("/api" + path, {
    method: body ? "POST" : "GET",
    headers: body ? { "Content-Type": "application/json" } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  let data = {};
  try { data = await res.json(); } catch (e) { /* пустой ответ */ }
  if (!res.ok) {
    const err = new Error(data.error || "Ошибка HTTP " + res.status);
    err.status = res.status;
    throw err;
  }
  return data;
}

/* --------------------------- вспомогательное -------------------------- */

function isPlayable(card, top, activeColor) {
  if (!top) return false;
  if (card.kind === "wild" || card.kind === "wild_draw4") return true;
  if (card.color === activeColor) return true;
  if (card.kind === "number" && top.kind === "number" && card.number === top.number) return true;
  if (card.kind !== "number" && card.kind === top.kind) return true;
  return false;
}

function loadProfile() {
  try { return JSON.parse(sessionStorage.getItem("uno-profile")); } catch (e) { return null; }
}
function saveProfile(p) {
  // sessionStorage: профиль живёт в своей вкладке, вторая вкладка может войти другим игроком
  if (p) sessionStorage.setItem("uno-profile", JSON.stringify(p));
  else sessionStorage.removeItem("uno-profile");
}

function loadAuth() {
  try { return JSON.parse(localStorage.getItem("uno-auth")); } catch (e) { return null; }
}
function saveAuth(a) {
  // аккаунт, наоборот, общий для всех вкладок и переживает перезапуск браузера
  if (a) localStorage.setItem("uno-auth", JSON.stringify(a));
  else localStorage.removeItem("uno-auth");
}

/* --------------------------- Вход и статистика ------------------------- */

function AuthBar({ auth, onAuth, onLogout, onShowStats, onShowSettings, tg }) {
  const [loginName, setLoginName] = useState("");
  const [loginPass, setLoginPass] = useState("");
  const [authError, setAuthError] = useState("");

  const doAuth = async (mode) => {
    setAuthError("");
    try {
      const data = await api(`/auth/${mode}`, { username: loginName, password: loginPass });
      setLoginPass("");
      onAuth(data);
    } catch (e) {
      setAuthError(e.message);
    }
  };

  if (auth) {
    return (
      <div className="auth-bar logged">
        <span className="who">Вы вошли как <b>{auth.username}</b></span>
        <span className="spacer" />
        <button className="ghost small" onClick={onShowStats}>Статистика</button>
        <button className="ghost small" onClick={onShowSettings}>Настройки</button>
        <button className="ghost small" onClick={onLogout}>Выйти</button>
      </div>
    );
  }
  if (tg && tg.active) {
    // внутри Telegram вход выполняется автоматически по подписи initData
    return (
      <div className="auth-bar">
        <div className="auth-title">
          {tg.status === "pending" ? "Вход через Telegram…" : "Не удалось войти через Telegram"}
        </div>
        {tg.status === "error" && <div className="auth-error">{tg.error}</div>}
        <div className="hint">Откройте игру через кнопку бота — вход выполнится сам.
          Можно играть и без входа, но без статистики.</div>
      </div>
    );
  }
  return (
    <div className="auth-bar">
      <div className="auth-title">Вход — чтобы копилась статистика</div>
      <div className="auth-row">
        <input
          type="text"
          placeholder="Логин"
          maxLength="20"
          value={loginName}
          onChange={(e) => setLoginName(e.target.value)}
        />
        <input
          type="password"
          placeholder="Пароль"
          value={loginPass}
          onChange={(e) => setLoginPass(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") doAuth("login"); }}
        />
      </div>
      <div className="auth-row">
        <button className="ghost small" onClick={() => doAuth("login")}>Войти</button>
        <button className="ghost small" onClick={() => doAuth("register")}>Регистрация</button>
      </div>
      {authError && <div className="auth-error">{authError}</div>}
      <div className="hint">Можно играть и без входа — тогда партии не попадают в статистику.</div>
    </div>
  );
}

function StatsModal({ auth, onClose }) {
  const [me, setMe] = useState(null);
  const [top, setTop] = useState([]);

  useEffect(() => {
    api(`/auth/me?token=${encodeURIComponent(auth.token)}`)
      .then(setMe)
      .catch(() => setMe({ error: true }));
    api("/stats/top").then((d) => setTop(d.top || [])).catch(() => {});
  }, [auth.token]);

  const placeWord = (p) => (p === 1 ? "🥇 1" : p === 2 ? "🥈 2" : p === 3 ? "🥉 3" : p);

  return (
    <div className="overlay" onClick={onClose}>
      <div className="stats-modal" onClick={(e) => e.stopPropagation()}>
        <h2>Статистика</h2>
        {!me ? (
          <div className="hint">Загрузка…</div>
        ) : me.error ? (
          <div className="hint">Сессия истекла — войдите заново.</div>
        ) : (
          <>
            <div className="stats-grid">
              <div><b>{me.stats.games}</b><span>партий</span></div>
              <div><b>{me.stats.wins}</b><span>побед</span></div>
              <div><b>{me.stats.losses}</b><span>поражений</span></div>
              <div><b>{me.stats.winRate === null ? "—" : me.stats.winRate + "%"}</b><span>винрейт</span></div>
            </div>

            <h3>Последние партии</h3>
            {me.stats.history.length === 0 ? (
              <div className="hint">Пока пусто — сыграйте первую партию.</div>
            ) : (
              <table className="stats-table">
                <thead><tr><th>Дата</th><th>Место</th><th>Карт осталось</th><th>Игроков</th></tr></thead>
                <tbody>
                  {me.stats.history.map((h, i) => (
                    <tr key={i}>
                      <td>{h.date}</td>
                      <td>{placeWord(h.place)}</td>
                      <td>{h.cardsLeft}</td>
                      <td>{h.players}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}

            <h3>Лидеры</h3>
            {top.length === 0 ? (
              <div className="hint">Статистики ещё нет ни у кого.</div>
            ) : (
              <table className="stats-table">
                <thead><tr><th>Игрок</th><th>Партий</th><th>Побед</th></tr></thead>
                <tbody>
                  {top.map((r, i) => (
                    <tr key={i} className={r.username === auth.username ? "me-row" : ""}>
                      <td>{r.username}</td><td>{r.games}</td><td>{r.wins}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
          </>
        )}
        <div className="modal-actions">
          <button onClick={onClose}>Закрыть</button>
        </div>
      </div>
    </div>
  );
}

/* --------------------------- Настройки аккаунта ----------------------- */

function SettingsModal({ auth, tableBg, onSaved, onClose }) {
  const [tables, setTables] = useState([]);
  const [current, setCurrent] = useState(tableBg || "default");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    api("/tables").then((d) => setTables(d.tables || [])).catch(() => {});
  }, []);

  const pick = async (id) => {
    if (busy || id === current) return;
    setBusy(true);
    setError("");
    try {
      await api("/auth/settings", { token: auth.token, tableBg: id });
      setCurrent(id);
      onSaved(id); // фон меняется сразу — живое превью за модалкой
    } catch (e) {
      setError(e.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="overlay" onClick={onClose}>
      <div className="stats-modal" onClick={(e) => e.stopPropagation()}>
        <h2>Настройки аккаунта</h2>
        <h3>Фон стола</h3>
        <div className="bg-grid">
          <div className={"bg-card" + (current === "default" ? " selected" : "")}
               onClick={() => pick("default")}>
            <div className="bg-thumb felt" />
            <div className="bg-name">Стандартный</div>
          </div>
          {tables.map((t) => (
            <div key={t.id}
                 className={"bg-card" + (current === t.id ? " selected" : "")}
                 title={t.label}
                 onClick={() => pick(t.id)}>
              <div className="bg-thumb" style={{ backgroundImage: `url(${t.svg || t.jpg})` }} />
              <div className="bg-name">{t.label}</div>
            </div>
          ))}
        </div>
        {error && <div className="auth-error">{error}</div>}
        <div className="modal-actions"><button onClick={onClose}>Готово</button></div>
      </div>
    </div>
  );
}

/* ------------------------------ Лобби --------------------------------- */

function Lobby({ onEnter, auth, onAuth, onLogout, tableBg, onTableBg, tg }) {
  const [name, setName] = useState(localStorage.getItem("uno-name") || "");
  const [bots, setBots] = useState(2);
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);
  const [statsOpen, setStatsOpen] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const joinId = new URLSearchParams(window.location.search).get("game");

  useEffect(() => {
    if (auth && !name) setName(auth.username);
  }, [auth]); // eslint-disable-line react-hooks/exhaustive-deps

  const enter = async (mode) => {
    setError("");
    setBusy(true);
    try {
      localStorage.setItem("uno-name", name);
      let created;
      if (mode === "join") {
        created = await api(`/games/${joinId}/join`, { name: name || "Игрок", authToken: auth && auth.token });
      } else {
        created = await api("/games", { name: name || "Игрок 1", bots: mode === "bots" ? bots : 0, authToken: auth && auth.token });
        if (mode === "bots") await api(`/games/${created.gameId}/start`, { token: created.token });
      }
      saveProfile(created);
      onEnter(created);
    } catch (e) {
      setError(e.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="lobby">
      <div className="lobby-card">
        <div className="lobby-logo">UN<span>O</span></div>
        <div className="lobby-sub">Локальная карточная игра · сервер на Python</div>

        <AuthBar auth={auth} onAuth={onAuth} onLogout={onLogout}
                 onShowStats={() => setStatsOpen(true)}
                 onShowSettings={() => setSettingsOpen(true)}
                 tg={tg} />

        <label>Ваше имя</label>
        <input
          type="text"
          value={name}
          maxLength="20"
          placeholder="Игрок 1"
          onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter" && joinId) enter("join"); }}
        />

        {!joinId && (
          <>
            <label>Количество ботов-соперников</label>
            <div className="bot-row">
              {[0, 1, 2, 3, 4, 5, 6, 7].map((n) => (
                <button key={n} className={bots === n ? "active" : ""} onClick={() => setBots(n)}>
                  {n === 0 ? "нет" : n}
                </button>
              ))}
            </div>
            <div className="lobby-actions">
              <button disabled={busy || bots === 0} onClick={() => enter("bots")}>
                {busy ? "…" : "Играть с ботами"}
              </button>
              <button className="ghost" disabled={busy} onClick={() => enter("room")}>
                Создать комнату для друзей
              </button>
            </div>
            {bots === 0 && (
              <div className="hint">Для игры с ботами выберите хотя бы одного бота,
                либо создайте комнату и позовите друзей по ссылке.</div>
            )}
          </>
        )}

        {joinId && (
          <div className="lobby-actions">
            <button disabled={busy} onClick={() => enter("join")}>
              {busy ? "…" : `Присоединиться к игре ${joinId}`}
            </button>
          </div>
        )}

        <div className="error">{error}</div>
        {statsOpen && auth && <StatsModal auth={auth} onClose={() => setStatsOpen(false)} />}
        {settingsOpen && auth && (
          <SettingsModal auth={auth} tableBg={tableBg} onSaved={onTableBg}
                         onClose={() => setSettingsOpen(false)} />
        )}
      </div>
    </div>
  );
}

/* --------------------------- Компоненты игры --------------------------- */

function Opponent({ p, angle, isCurrent, seatStyle }) {
  const backs = Array.from({ length: Math.min(p.handCount, 8) });
  const av = AV_CLASSES[hashStr(p.id) % AV_CLASSES.length];
  return (
    <div className={"opponent" + (isCurrent ? " current" : "")} data-pid={p.id} style={seatStyle}>
      <div className="turn-indicator" style={{ display: isCurrent ? "block" : "none" }}>
        {p.isBot ? "думает…" : "ход"}
      </div>
      <div className={"avatar " + av}>{p.isBot ? "🤖" : "🙂"}</div>
      <div className="name">{p.name} {p.isBot && <span className="badge">бот</span>}</div>
      <div className="fan-box">
        <div className="fan"
             style={angle == null ? undefined : { "--fanrot": angle.toFixed(1) + "deg" }}>
          {backs.map((_, i) => <img key={i} src={img("assets/cards/back.jpg")} alt="" />)}
        </div>
        <div className="fan-badge" title="Карт в руке">{p.handCount}</div>
      </div>
    </div>
  );
}

function ColorPicker({ onPick, onCancel }) {
  return (
    <div className="overlay" onClick={onCancel}>
      <div className="color-picker" onClick={(e) => e.stopPropagation()}>
        <h3>Выберите цвет</h3>
        <div className="color-grid">
          {COLORS.map((c) => (
            <div key={c} className={"color-swatch " + c} title={COLOR_RU[c]} onClick={() => onPick(c)} />
          ))}
        </div>
      </div>
    </div>
  );
}

function LogPanel({ logRef, log, open }) {
  return (
    <div className={"log-panel" + (open ? " open" : "")}>
      <h3>Ход игры</h3>
      <ul ref={logRef}>
        {log.map((line, i) => <li key={i}>{line}</li>)}
      </ul>
    </div>
  );
}

/* Кольцо направления хода: две дуги со стрелками, вращаются вокруг сброса;
   при направлении против часовой вращение реверсируется */
function TurnRing({ ccw }) {
  return (
    <div className={"turn-ring" + (ccw ? " ccw" : "")} aria-hidden="true">
      <svg viewBox="0 0 120 120">
        {[0, 180].map((a) => (
          <g key={a} transform={"rotate(" + a + " 60 60)"}>
            <path className="arc" d="M60 16 A44 44 0 0 1 104 60" />
            <path className="head" d="M104 71 L96 55.5 L112 55.5 Z" />
          </g>
        ))}
      </svg>
    </div>
  );
}

/* Летящая карта: вылетает из точки from и приземляется в to (границы в px) */
function FlyingCard({ fly, onDone }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    let raf2;
    const raf1 = requestAnimationFrame(() => {
      raf2 = requestAnimationFrame(() => {
        const sx = fly.to.width / fly.from.width;
        const sy = fly.to.height / fly.from.height;
        el.style.transform =
          `translate(${fly.to.x - fly.from.x}px, ${fly.to.y - fly.from.y}px) ` +
          `scale(${sx}, ${sy}) rotate(0deg)`;
      });
    });
    const t = setTimeout(onDone, 480);
    return () => { cancelAnimationFrame(raf1); if (raf2) cancelAnimationFrame(raf2); clearTimeout(t); };
  }, [fly, onDone]);
  return (
    <div
      ref={ref}
      className="fly-card"
      style={{ left: fly.from.x, top: fly.from.y, width: fly.from.width, height: fly.from.height }}
    >
      <img src={img(fly.image)} alt="" />
    </div>
  );
}

/* ------------------------------ Экран игры ----------------------------- */

function GameView({ profile, onLeave, tableBg }) {
  const [state, setState] = useState(null);
  const [error, setError] = useState("");
  const [pendingWild, setPendingWild] = useState(null);
  const [fly, setFly] = useState(null);
  const [flyDraw, setFlyDraw] = useState(null);     // полёт карты из колоды
  const [hiddenDrawUid, setHiddenDrawUid] = useState(null); // взятая карта скрыта, пока летит
  const [discardHistory, setDiscardHistory] = useState([]); // кучка сброса с наклонами
  const [logOpen, setLogOpen] = useState(false);
  const [seenLog, setSeenLog] = useState(0);
  const [unoFlash, setUnoFlash] = useState(false);
  const [sortMode, setSortMode] = useState(() => {
    try { return localStorage.getItem("uno-hand-sort") || ""; } catch (e) { return ""; }
  });
  const unoTimer = useRef(null);
  const prevHandUidsRef = useRef(null); // состав руки на прошлом обновлении — для поиска взятой карты
  const logRef = useRef(null);
  const isDesktop = useIsDesktop();
  const seatRing = useSeatRing();

  /* переключение сортировки руки (повторный клик по активной отключает) */
  const toggleSort = useCallback((m) => {
    setSortMode((cur) => {
      const next = cur === m ? "" : m;
      try {
        if (next) localStorage.setItem("uno-hand-sort", next);
        else localStorage.removeItem("uno-hand-sort");
      } catch (e) { /* приватный режим — просто не сохраняем */ }
      return next;
    });
  }, []);
  const versionRef = useRef(0);
  const lastTopUid = useRef(undefined);      // uid верхней карты сброса с прошлого опроса
  const lastDrawVersion = useRef(null);      // версия, для которой анимировали взятие
  const flyRects = useRef(new Map());        // где в руке лежала сыгранная нами карта
  const clearFly = useCallback(() => setFly(null), []);
  const clearFlyDraw = useCallback(() => { setFlyDraw(null); setHiddenDrawUid(null); }, []);

  const applyState = useCallback((data) => {
    if (!data || data.unchanged) return;
    versionRef.current = data.version;
    setState(data);
  }, []);

  /* анимация выкладывания карты на сброс: следим за сменой верхней карты */
  useEffect(() => {
    if (!state || !state.discardTop) { lastTopUid.current = null; return; }
    const uid = state.discardTop.uid;
    if (uid === lastTopUid.current) return;
    const isFirstState = lastTopUid.current === undefined;
    lastTopUid.current = uid;
    const entry = { uid, image: state.discardTop.image, rot: rotFor(uid) };
    if (isFirstState) {
      setDiscardHistory([entry]); // при загрузке страницы карту на столе не анимируем
      return;
    }
    setDiscardHistory((h) => [...h.slice(-5), entry]); // кучка хранит последние ~6 карт

    // откуда летит: наша карта из руки, карта бота из его веера, либо колода
    let from = null;
    if (state.you && state.lastPlayPid === state.you.id && flyRects.current.has(uid)) {
      from = flyRects.current.get(uid);
      flyRects.current.delete(uid);
    } else if (state.lastPlayPid) {
      const el = document.querySelector(`.opponent[data-pid="${state.lastPlayPid}"]`);
      if (el) {
        const r = el.getBoundingClientRect();
        from = { x: r.left + r.width / 2 - 22, y: r.top + r.height / 2 - 33, width: 44, height: 66 };
      }
    }
    if (!from) {
      const pile = document.querySelector(".pile.draw img");
      if (pile) {
        const r = pile.getBoundingClientRect();
        from = { x: r.left, y: r.top, width: r.width, height: r.height };
      }
    }
    const target = document.querySelector(".pile.discard .stack-card:last-of-type");
    if (!from || !target) return;
    setFly({ uid, image: state.discardTop.image, from, to: target.getBoundingClientRect() });
  }, [state && state.discardTop && state.discardTop.uid]);

  /* анимация взятия карты из колоды: к нам — лицом, к сопернику — рубашкой */
  useEffect(() => {
    if (!state) return;
    if (lastDrawVersion.current === null) { lastDrawVersion.current = state.version; return; }
    if (state.version === lastDrawVersion.current) return;
    lastDrawVersion.current = state.version;
    if (state.status !== "playing" || !state.lastDrawPid) return;

    const deck = document.querySelector(".pile.draw img");
    if (!deck) return;
    const from = deck.getBoundingClientRect();
    if (state.you && state.lastDrawPid === state.you.id && state.you.hand.length) {
      const hand = state.you.hand;
      const prev = prevHandUidsRef.current;
      // взятая карта — та, которой не было в руке на прошлом обновлении
      // (при сортированной руке новая карта не обязана быть последней)
      const card = (prev && hand.find((c) => !prev.has(c.uid))) || hand[hand.length - 1];
      const el = document.querySelector(`.hand .card[data-uid="${card.uid}"]`);
      if (!el) return;
      const to = el.getBoundingClientRect();
      setHiddenDrawUid(card.uid);
      setFlyDraw({ uid: "draw" + state.version, image: card.image, from, to });
    } else {
      const el = document.querySelector(`.opponent[data-pid="${state.lastDrawPid}"]`);
      if (!el) return;
      const r = el.getBoundingClientRect();
      setFlyDraw({
        uid: "draw" + state.version,
        image: "assets/cards/back.jpg",
        from,
        to: { x: r.left + r.width / 2 - 20, y: r.top + r.height / 2 - 30, width: 40, height: 60 },
      });
    }
  }, [state && state.version]);

  /* запоминаем текущий состав руки — на следующем обновлении по этому набору
     определяется взятая карта (эффект выше читает значение с прошлого рендера) */
  useEffect(() => {
    const hand = state && state.you && state.you.hand;
    prevHandUidsRef.current = hand ? new Set(hand.map((c) => c.uid)) : null;
  }, [state && state.version]);

  /* опрос состояния */
  useEffect(() => {
    let stopped = false;
    const tick = async () => {
      try {
        const data = await api(`/games/${profile.gameId}?token=${profile.token}&v=${versionRef.current}`);
        if (!stopped) applyState(data);
      } catch (e) {
        if (e.status === 400) { saveProfile(null); onLeave(); } // игра закончилась на сервере
      }
    };
    tick();
    const id = setInterval(tick, POLL_MS);
    return () => { stopped = true; clearInterval(id); };
  }, [profile, applyState, onLeave]);

  /* автоскролл лога */
  useEffect(() => {
    if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
  }, [state && state.log.length]);

  /* непрочитанные строки лога: открытие шторки сбрасывает счётчик */
  const logLen = state ? state.log.length : 0;
  useEffect(() => {
    if (logOpen) setSeenLog(logLen);
  }, [logOpen, logLen]);
  const unread = Math.max(0, logLen - seenLog);

  const act = async (path, body) => {
    try {
      setError("");
      applyState(await api(`/games/${profile.gameId}${path}`, { token: profile.token, ...body }));
    } catch (e) {
      setError(e.message);
      setTimeout(() => setError(""), 3000);
    }
  };

  if (!state) return <div className="lobby"><div className="lobby-card">Загрузка…</div></div>;

  const you = state.you;
  const others = state.players.filter((p) => !you || p.id !== you.id);
  const top = state.discardTop;
  const handRaw = you ? you.hand : [];
  const hand = sortHand(handRaw, sortMode);

  const clickCard = (e, card) => {
    if (!state.yourTurn) return;
    if (!isPlayable(card, top, state.activeColor)) return;
    if (e && e.currentTarget) {
      // запоминаем, где карта лежала в руке — отсюда она полетит на стол
      flyRects.current.set(card.uid, e.currentTarget.getBoundingClientRect());
    }
    if (card.kind === "wild" || card.kind === "wild_draw4") setPendingWild(card);
    else act("/play", { cardId: card.uid });
  };

  const playWild = (color) => {
    const card = pendingWild;
    setPendingWild(null);
    if (card) act("/play", { cardId: card.uid, color });
  };

  return (
    <div className="game">
      <TableOval themeId={tableBg} recalcKey={state && state.version} />
      <div className="game-header">
        <div className="title">UN<span>O</span> <span className="gid">#{state.gameId}</span></div>
        {state.status !== "lobby" && (
          <div className="status">
            Колода: {state.drawPileCount} · Направление: {state.direction === 1 ? "↻" : "↺"}
          </div>
        )}
        <div className="header-actions">
          <button className="ghost small" onClick={() => setLogOpen((o) => !o)}>
            История{unread > 0 && !logOpen ? ` · ${unread}` : ""}
          </button>
          <button className="ghost" onClick={() => { saveProfile(null); onLeave(); }}>Выйти</button>
        </div>
      </div>

      <LogPanel logRef={logRef} log={state.log} open={logOpen} />

      {/* ------- ожидание в комнате ------- */}
      {state.status === "lobby" && (
        <div className="lobby" style={{ gridArea: "center" }}>
          <div className="lobby-card">
            <h2 style={{ marginBottom: 12 }}>Комната #{state.gameId}</h2>
            <div className="room-info">
              Ссылка для друзей:<br />
              <span className="link">{window.location.origin + "/?game=" + state.gameId}</span>
              <ul>
                {state.players.map((p) => (
                  <li key={p.id}>{p.isBot ? "🤖" : "🙂"} {p.name}{you && p.id === you.id ? " (вы)" : ""}</li>
                ))}
              </ul>
            </div>
            <div className="lobby-actions">
              {you && state.players[0].id === you.id ? (
                <button disabled={state.players.length < 2}
                        onClick={() => act("/start")}>
                  Начать игру
                </button>
              ) : (
                <div className="hint">Ожидаем начала игры — начнёт хост.</div>
              )}
            </div>
            {state.players.length < 2 && <div className="hint">Нужно минимум два игрока: позовите друга по ссылке или создайте игру с ботами.</div>}
          </div>
        </div>
      )}

      {/* ------- игровой стол ------- */}
      {state.status !== "lobby" && (
        <>
          <div className="opponents">
            {others.map((p, i) => {
              const onRing = isDesktop && seatRing;
              const angles = seatAngles(others.length);
              const rad = (angles[i] * Math.PI) / 180;
              const seatStyle = onRing ? {
                left: Math.round(seatRing.cx - seatRing.rx * Math.sin(rad)),
                top: Math.round(seatRing.cy + seatRing.ry * Math.cos(rad)),
                transform: "translate(-50%, -50%)",
              } : undefined;
              return (
                <Opponent key={p.id} p={p}
                          angle={onRing ? angles[i] : null}
                          seatStyle={seatStyle}
                          isCurrent={state.currentPlayerId === p.id && state.status === "playing"} />
              );
            })}
          </div>

          <div className={"table-center glow-" + (state.activeColor || "none")}>
            <div className={"pile draw" + (state.canDraw ? " can-take" : "")}>
              <img
                src={img("assets/cards/back.jpg")}
                alt="Колода"
                title={state.canDraw ? "Взять карту из колоды" : "Колода"}
                onClick={() => state.canDraw && act("/draw")}
              />
              <div className="label">Колода · {state.drawPileCount}</div>
              {state.canDraw && (
                <div className="deck-hint">Нажмите на колоду,<br />чтобы взять карту</div>
              )}
              {state.canPass && (
                <button className="pass-btn" onClick={() => act("/pass")}>Пас</button>
              )}
            </div>

            <div className="discard-zone">
              <TurnRing ccw={state.direction === -1} />
              <div className="pile discard">
                {discardHistory.map((c, i) => {
                  const isTop = top && c.uid === top.uid && i === discardHistory.length - 1;
                  return (
                    <img
                      key={c.uid + ":" + i}
                      className="stack-card"
                      src={img(c.image)}
                      alt=""
                      style={{
                        transform: `rotate(${c.rot}deg)`,
                        zIndex: i + 2,
                        visibility: isTop && fly && fly.uid === top.uid ? "hidden" : "visible",
                      }}
                    />
                  );
                })}
                {top && <div className={"color-ring " + state.activeColor} />}
              </div>
              <div className="label">
                Активный цвет: <b>{COLOR_RU[state.activeColor] || state.activeColor || "—"}</b>
              </div>
            </div>
          </div>

          <div className="hand-area">
            <div className="turn-banner">
              {state.status === "finished" ? (
                <span>Игра окончена</span>
              ) : state.yourTurn ? (
                <span className="me">Ваш ход{state.hasPlayableHint || ""}</span>
              ) : (
                <span>Ход игрока: <b>{(state.players.find((p) => p.id === state.currentPlayerId) || {}).name}</b>…</span>
              )}
            </div>

            <div className="hand">
              {hand.map((card, i) => {
                const playable = state.yourTurn && isPlayable(card, top, state.activeColor);
                const n = Math.max(handRaw.length, 1);
                const per = Math.min(5, 44 / n);              // градусов на шаг веера
                const t = i - (n - 1) / 2;
                const maxLift = ((n - 1) / 2) * per * 1.5;    // центр веера выше краёв
                return (
                  <div
                    key={card.uid}
                    data-uid={card.uid}
                    className={"card " + (playable ? "playable" : "dimmed")
                      + (card.uid === hiddenDrawUid ? " drawn-hide" : "")}
                    onClick={(e) => clickCard(e, card)}
                    title={playable ? "Положить карту" : "Нельзя сыграть"}
                    style={{
                      "--rot": (t * per).toFixed(2) + "deg",
                      "--lift": (Math.abs(t) * per * 1.5 - maxLift).toFixed(1) + "px",
                    }}
                  >
                    <img src={img(card.image)} alt={card.image} />
                  </div>
                );
              })}
            </div>

            <div className="hand-actions">
              <div className="sort-controls">
                <button
                  className={"ghost small" + (sortMode === "color" ? " on" : "")}
                  title="Сгруппировать карты по цвету — повторный клик отключает сортировку"
                  onClick={() => toggleSort("color")}
                >
                  По цвету
                </button>
                <button
                  className={"ghost small" + (sortMode === "value" ? " on" : "")}
                  title="Сгруппировать карты по значению — повторный клик отключает сортировку"
                  onClick={() => toggleSort("value")}
                >
                  По значению
                </button>
              </div>
              {error && <div style={{ color: "#ff9d9d", alignSelf: "center" }}>{error}</div>}
              {state.yourTurn && state.status === "playing" && (
                <button disabled={!state.canDraw} onClick={() => act("/draw")}>Взять карту</button>
              )}
            </div>
          </div>
        </>
      )}

      <button
        className={"uno-btn" + (you && you.hand && you.hand.length === 1 ? " hot" : "")}
        title="UNO объявляется автоматически — кнопка для настроения"
        onClick={() => {
          setUnoFlash(true);
          clearTimeout(unoTimer.current);
          unoTimer.current = setTimeout(() => setUnoFlash(false), 950);
        }}
      >
        UNO!
      </button>
      {unoFlash && <div className="uno-flash">UNO!</div>}

      {fly && <FlyingCard fly={fly} onDone={clearFly} />}
      {flyDraw && <FlyingCard fly={flyDraw} onDone={clearFlyDraw} />}
      {pendingWild && <ColorPicker onPick={playWild} onCancel={() => setPendingWild(null)} />}

      {state.status === "finished" && state.winner && (
        <div className="winner-overlay">
          <div className="winner-card">
            <div className="emoji">🏆</div>
            <h2>{state.winner.name} победил(а)!</h2>
            <p>{you && state.winner.id === you.id ? "Поздравляем с победой!" : "Попробуйте ещё раз."}</p>
            <button onClick={() => { saveProfile(null); onLeave(); }}>В главное меню</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ------------------------------- App ----------------------------------- */

function App() {
  const [profile, setProfile] = useState(() => {
    const saved = loadProfile();
    const urlGame = new URLSearchParams(window.location.search).get("game");
    // по ссылке с чужой комнатой всегда показываем экран входа
    if (urlGame && (!saved || saved.gameId !== urlGame)) return null;
    return saved;
  });
  const [auth, setAuth] = useState(loadAuth());
  const [tableBg, setTableBg] = useState("default");
  const [tg, setTg] = useState({ active: IS_TELEGRAM,
                                 status: IS_TELEGRAM ? "pending" : null, error: "" });
  const tgStarted = useRef(false);
  const enter = (p) => { setProfile(p); window.history.replaceState({}, "", "/"); };
  const leave = () => setProfile(null);
  const handleAuth = (a) => { saveAuth(a); setAuth(a); };
  const handleLogout = () => {
    if (auth) api("/auth/logout", { token: auth.token }).catch(() => {});
    saveAuth(null);
    setAuth(null);
    setTableBg("default");
  };

  /* выбранный фон стола подтягивается при входе */
  useEffect(() => {
    let live = true;
    if (auth) {
      api(`/auth/me?token=${encodeURIComponent(auth.token)}`)
        .then((d) => { if (live) setTableBg(d.tableBg || "default"); })
        .catch(() => {});
    } else {
      setTableBg("default");
    }
    return () => { live = false; };
  }, [auth]);

  /* применяем фон к странице (лобби и игровой стол) */
  useEffect(() => {
    document.body.style.background = tableBg && tableBg !== "default"
      ? `#0d3b24 url("/assets/tables/${tableBg}.svg") center / cover no-repeat fixed`
      : "";
  }, [tableBg]);

  /* Telegram Mini App: загружаем SDK, входим по подписи initData */
  useEffect(() => {
    if (!IS_TELEGRAM || tgStarted.current) return;
    tgStarted.current = true;
    const script = document.createElement("script");
    script.src = "https://telegram.org/js/telegram-web-app.js";
    script.onload = async () => {
      try {
        const wa = window.Telegram && window.Telegram.WebApp;
        if (wa) { wa.ready(); wa.expand(); }
        const initData = (wa && wa.initData)
          || new URLSearchParams(TG_HASH.slice(1)).get("tgWebAppData") || "";
        const r = await api("/auth/telegram", { initData });
        handleAuth(r);
        setTg((t) => ({ ...t, status: "done" }));
      } catch (e) {
        setTg((t) => ({ ...t, status: "error", error: e.message }));
      }
    };
    script.onerror = () => setTg((t) => ({ ...t, status: "error",
      error: "Не удалось загрузить Telegram SDK (нужен доступ к telegram.org)" }));
    document.head.appendChild(script);
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    document.title = profile ? "UNO — игра" : "UNO — локальная игра";
  }, [profile]);

  return profile
    ? <GameView profile={profile} onLeave={leave} tableBg={tableBg} />
    : <Lobby onEnter={enter} auth={auth} onAuth={handleAuth} onLogout={handleLogout}
             tableBg={tableBg} onTableBg={setTableBg} tg={tg} />;
}

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