// Group session screen — main matching UI
const { useState: useState_grp, useMemo: useMemo_grp, useEffect: useEffect_grp } = React;

function GroupSession({ store, groupId, onBack, onOpenSettings, onShare }) {
  const { groups, players, updateGroup, addLog, can, ownsGroup, isSuper, userById, systemConfig, touchGroupActivity, clearGroupSession } = store;
  const group = groups.find(g => g.id === groupId);
  const toast = useToast();
  const [resultModal, setResultModal] = useState_grp(null); // { courtId }
  const [addPlayersOpen, setAddPlayersOpen] = useState_grp(false);
  const [swapTarget, setSwapTarget] = useState_grp(null); // { courtId, outId }
  const [courtSettingsId, setCourtSettingsId] = useState_grp(null); // courtId being edited
  const [randomizeMatchId, setRandomizeMatchId] = useState_grp(null); // queue matchId to re-roll
  const [placeTarget, setPlaceTarget] = useState_grp(null); // { matchId } awaiting court pick
  const [manualQueueOpen, setManualQueueOpen] = useState_grp(false); // hand-pick a queue match
  const [playsEditOpen, setPlaysEditOpen] = useState_grp(false); // modal แก้ไขรอบเล่น

  // Auto-end matches that have been active over the configured threshold (default 120 min)
  useEffect_grp(() => {
    if (!group || !can.runMatching) return;
    const autoMs = (systemConfig?.matchAutoEndMinutes ?? 120) * 60 * 1000;
    const check = () => {
      group.courts.forEach(c => {
        const m = group.active[c.id];
        if (m?.startedAt && Date.now() - m.startedAt >= autoMs) {
          endMatch(c.id, null, { auto: true });
        }
      });
    };
    check();
    const id = setInterval(check, 60 * 1000);
    return () => clearInterval(id);
  }, [group?.id, group?.active, can.runMatching, systemConfig?.matchAutoEndMinutes]);

  // Auto-clear group session after configured inactivity (default 4h)
  useEffect_grp(() => {
    if (!group || !can.runMatching) return;
    const clearMs = (systemConfig?.sessionClearHours ?? 4) * 60 * 60 * 1000;
    const lastAct = group.lastActivityAt;
    if (lastAct && Date.now() - lastAct >= clearMs) {
      clearGroupSession(group.id, { auto: true });
      toast(`เคลียก๊วนอัตโนมัติ — ไม่มีการเคลื่อนไหวนาน ${systemConfig?.sessionClearHours ?? 4} ชั่วโมง`);
    }
    const id = setInterval(() => {
      const g2 = store.groups.find(x => x.id === groupId);
      if (!g2 || !g2.lastActivityAt) return;
      if (Date.now() - g2.lastActivityAt >= clearMs) {
        clearGroupSession(g2.id, { auto: true });
        toast(`เคลียก๊วนอัตโนมัติ — ไม่มีการเคลื่อนไหวนาน ${systemConfig?.sessionClearHours ?? 4} ชั่วโมง`);
      }
    }, 5 * 60 * 1000);
    return () => clearInterval(id);
  }, [group?.id, can.runMatching, systemConfig?.sessionClearHours]);

  if (!group) return null;

  // Ownership check — admin without ownership cannot open group (unless super)
  const owned = ownsGroup(group);
  if (!owned) {
    return (
      <div className="page" data-screen-label="No Access">
        <div className="empty">
          <div className="em">🔒</div>
          <h2 style={{margin:"6px 0"}}>ไม่มีสิทธิ์เข้าถึง</h2>
          <div className="muted small">ก๊วนนี้เป็นของผู้ใช้อื่น — กรุณาติดต่อเจ้าของเพื่อขอ share link</div>
          <button className="btn primary mt-2" onClick={onBack}>← กลับ Dashboard</button>
        </div>
      </div>
    );
  }

  const owner = userById?.[group.ownerId];

  const groupPlayers = useMemo_grp(
    () => group.playerIds.map(id => players.find(p => p.id === id)).filter(Boolean),
    [group.playerIds, players]
  );
  const playerById = useMemo_grp(() => Object.fromEntries(groupPlayers.map(p => [p.id, p])), [groupPlayers]);

  // Players currently live on a court (not queue). The hard invariant is that
  // none of these may appear on a second court — placement is validated against it.
  const activePlayerIds = new Set();
  Object.values(group.active || {}).forEach(m => m?.players?.forEach(id => activePlayerIds.add(id)));

  const playingIds = new Set(activePlayerIds);
  group.upcoming?.forEach(m => m.players?.forEach(id => playingIds.add(id)));

  const bench = groupPlayers.filter(p => !playingIds.has(p.id));
  const freeCourtCount = group.courts.filter(c => !group.active[c.id]).length;

  // "Plan next round" is available whenever the roster can form a match. It pulls
  // from the whole roster (incl. players currently on court) and can be pressed
  // repeatedly to stack as many fair future rounds as wanted — players recur across
  // rounds by design. A planned round can't be placed on a court until its players
  // free up (place guard), so it's safe with any number of courts.
  const needPerMatch = group.mode === "double" ? 4 : 2;
  const canPlanNextRound = groupPlayers.length >= needPerMatch;

  // ---- formatting helpers for richer audit-log descriptions ----
  const fmtName = (id) => playerById[id]?.name?.split(" ")[0] || "?";
  const fmtLvOf = (id) => { const l = playerById[id]?.level; return l == null ? "?" : formatLevel(l, systemConfig); };
  const fmtTeam = (ids) => (ids || []).map(id => `${fmtName(id)} (Lv ${fmtLvOf(id)})`).join(" & ");
  const fmtMatchup = (pl) => { const h = Math.ceil((pl || []).length / 2); return `${fmtTeam((pl||[]).slice(0, h))} พบ ${fmtTeam((pl||[]).slice(h))}`; };
  const courtNameOf = (cid) => group.courts.find(c => c.id === cid)?.name || "?";
  const fmtDur = (s) => s == null ? null : (s >= 60 ? `${Math.round(s/60)} นาที` : `${s} วินาที`);

  const balanceScore = useMemo_grp(() => {
    const all = [...Object.values(group.active || {}).filter(Boolean), ...(group.upcoming || [])];
    if (!all.length) return null;
    let totalDiff = 0;
    all.forEach(m => {
      if (group.mode === "double" && m.players.length === 4) {
        const p = m.players.map(id => playerById[id]?.level || 0);
        const s1 = p[0] + p[1], s2 = p[2] + p[3];
        totalDiff += Math.abs(s1 - s2);
      } else if (m.players.length >= 2) {
        const p = m.players.map(id => playerById[id]?.level || 0);
        totalDiff += Math.abs(p[0] - p[1]);
      }
    });
    const max = group.mode === "double" ? 4 : 9;
    return Math.max(0, Math.round((1 - (totalDiff / all.length) / max) * 100));
  }, [group, playerById]);

  /* ---- Actions ---- */
  function shuffleAll() {
    const hasActive = Object.values(group.active || {}).some(Boolean);
    const hasQueue = (group.upcoming || []).length > 0;
    if (hasActive || hasQueue) {
      const lines = [];
      if (hasActive) lines.push(`• มีผู้เล่นกำลังเล่นอยู่ในสนาม (แมตช์จะถูกยกเลิก)`);
      if (hasQueue) lines.push(`• มีคิวรออยู่ ${group.upcoming.length} แมตช์ (คิวจะถูกล้างทั้งหมด)`);
      if (!confirm(`สุ่มจัดคู่ใหม่ทั้งหมด?\n\n${lines.join("\n")}\n\nยืนยันจะเริ่มรอบใหม่?`)) return;
    }
    // Build recent context from history so partner-repeat penalty and the rest
    // rule both work on the very first shuffle too. recentMatches = last 2 matches
    // per court; recentRounds = history chunked into rounds (courtCount per round).
    const historyRecent = (group.history || []).slice(0, 2).map(h => ({
      court: h.court, players: h.players,
    }));
    const courtCount = group.courts.length || 1;
    const historyRounds = [];
    const hist = group.history || [];
    for (let i = 0; i < Math.min(hist.length, courtCount * 3); i += courtCount) {
      historyRounds.push(hist.slice(i, i + courtCount).flatMap(h => h.players));
    }
    const matchSettings = { ...group.settings, matchPriority: systemConfig?.matchPriority };
    const result = generateMatches({
      players: groupPlayers,
      courts: group.courts,
      mode: group.mode,
      plays: group.plays || {},
      settings: matchSettings,
      recentMatches: historyRecent,
      recentRounds: historyRounds,
    });
    if (!result) {
      toast(`ต้องมีผู้เล่นอย่างน้อย ${group.courts.length * (group.mode === "double" ? 4 : 2)} คน`);
      return;
    }
    // All matches go to upcoming queue — courts stay empty until user presses "ลงสนาม"
    const active = {};
    const upcoming = [];
    result.matches.forEach(m => {
      if (!m.players || m.players.length === 0) return;
      if (new Set(m.players).size !== m.players.length) return;
      upcoming.push({
        id: "m" + Math.random().toString(36).slice(2, 6),
        court: m.court,
        players: m.players,
        teams: m.teams,
      });
    });
    // Generate additional upcoming rounds. Simulate plays from the first queued round
    // so rotation stays fair when the queue is consumed.
    const simPlays = { ...(group.plays || {}) };
    groupPlayers.forEach(p => {
      if (result.matches.some(m => m.players.includes(p.id)))
        simPlays[p.id] = (simPlays[p.id] || 0) + 1;
    });
    // seed simRecent with the current active round (per-court for partner penalty,
    // and per-round all-players for the consecutive-play rest rule)
    let simRecentMatches = result.matches.map(m => ({ court: m.court, players: m.players }));
    let simRounds = [result.matches.flatMap(m => m.players)];
    const perRound = group.courts.length * (group.mode === "double" ? 4 : 2);
    const maxRounds = perRound > 0 ? Math.min(2, Math.floor(groupPlayers.length / perRound)) : 0;
    for (let r = 0; r < maxRounds; r++) {
      const rr = generateMatches({
        players: groupPlayers,
        courts: group.courts,
        mode: group.mode,
        plays: simPlays,
        settings: matchSettings,
        recentMatches: simRecentMatches,
        recentRounds: simRounds,
      });
      if (!rr) break;
      const roundIds = [];
      rr.matches.forEach(m => {
        if (!m.players || m.players.length === 0) return;
        // Defensive: never enqueue a match with a duplicated player.
        if (new Set(m.players).size !== m.players.length) return;
        upcoming.push({
          id: "m" + Math.random().toString(36).slice(2, 6),
          court: m.court,
          players: m.players,
          teams: m.teams,
        });
        m.players.forEach(id => { simPlays[id] = (simPlays[id] || 0) + 1; roundIds.push(id); });
      });
      // Slide the recent windows forward: new round becomes most-recent
      simRecentMatches = [
        ...rr.matches.map(m => ({ court: m.court, players: m.players })),
        ...simRecentMatches,
      ].slice(0, group.courts.length * 2);
      simRounds = [roundIds, ...simRounds].slice(0, 3);
    }
    updateGroup(group.id, { active, upcoming });
    const modeLabel = group.settings?.balanceMode === "similar" ? "A: ระดับใกล้เคียง" : "B: รวมทีมสมดุล";
    const queuedCount = upcoming.length;
    addLog({ type: "shuffle",
      desc: `สุ่มจัดคู่: ${group.name} • Mode ${modeLabel} • Balance ${result.balance}% • คิวรอลงสนาม ${queuedCount} แมตช์` +
        (result.violations > 0 ? ` • ⚠ ${result.violations} สนามเกิน skill gap` : ""),
    });
    if (result.violations > 0) {
      toast(`สุ่มสำเร็จ • Balance ${result.balance}% • ⚠ ${result.violations} สนามเกิน skill gap`);
    } else {
      toast(`สุ่มจัดคู่สำเร็จ • Mode ${group.settings?.balanceMode === "similar" ? "A" : "B"} • ${result.balance}%`);
    }
  }

  function endMatch(courtId, winnersTeam, opts = {}) {
    const match = group.active[courtId];
    if (!match) return;
    // Update plays count
    const plays = { ...(group.plays || {}) };
    match.players.forEach(id => { plays[id] = (plays[id] || 0) + 1; });
    // Determine winners
    let winners = null;
    if (winnersTeam !== null) {
      winners = group.mode === "double"
        ? (winnersTeam === 1 ? match.players.slice(0,2) : match.players.slice(2,4))
        : [match.players[winnersTeam]];
    }
    const duration = match.startedAt ? Math.floor((Date.now() - match.startedAt) / 1000) : null;
    const history = [
      { id: "h" + Math.random().toString(36).slice(2,6), at: Date.now(), court: courtId, players: match.players, winners, duration },
      ...(group.history || []),
    ];
    // Court goes free. We do NOT auto-promote a queued match here: a waiting
    // queue may contain players still live on another court, and auto-promoting
    // would put the same player on two courts at once. The user places a queue
    // onto a free court manually (validated against currently-active players).
    const active = { ...group.active, [courtId]: null };
    updateGroup(group.id, { active, history, plays });
    const durTxt = fmtDur(duration);
    if (opts.auto) {
      addLog({
        type: "match.end", entityType: "court", entityId: courtId,
        desc: `⏰ จบแมตช์อัตโนมัติ (เกิน 2 ชม.): ${group.name} • สนาม ${courtNameOf(courtId)} • ${fmtMatchup(match.players)}` +
          (durTxt ? ` • ใช้เวลา ${durTxt}` : ""),
        newValue: { players: match.players, winners: null, durationSec: duration, autoEnded: true },
      });
      toast(`⏰ สนาม ${courtNameOf(courtId)} จบอัตโนมัติ — เล่นนานเกิน 2 ชั่วโมง`);
    } else {
      addLog({
        type: "match.end", entityType: "court", entityId: courtId,
        desc: `จบแมตช์: ${group.name} • สนาม ${courtNameOf(courtId)} • ${fmtMatchup(match.players)}` +
          (winners ? ` — 🏆 ชนะ: ${fmtTeam(winners)}` : " — ไม่บันทึกผล (เสมอ/ยกเลิก)") +
          (durTxt ? ` • ใช้เวลา ${durTxt}` : ""),
        newValue: { players: match.players, winners, durationSec: duration },
      });
      toast("บันทึกผลแล้ว — เลือกคิวลงสนามที่ว่างได้");
    }
    touchGroupActivity(group.id);
    setResultModal(null);
  }

  function swapPlayer(courtId, outId, inId, fromUpcoming) {
    const match = group.active[courtId];
    if (!match) return;
    // Guard: never swap in a player who is already on this court (would create a
    // duplicate). The swap modal already filters these out, but enforce it here too.
    if (inId !== outId && match.players.includes(inId)) {
      toast(`${playerById[inId]?.name?.split(" ")[0] || "ผู้เล่น"} อยู่ในสนามนี้แล้ว`);
      return;
    }
    const outName = playerById[outId]?.name?.split(" ")[0] || "?";
    const inName = playerById[inId]?.name?.split(" ")[0] || "?";

    let upcoming = group.upcoming || [];
    if (fromUpcoming) {
      // Critical guard: if the displaced player (outId) is ALREADY in the same queue
      // match as the incoming player (inId), replacing inId→outId there would put
      // outId into that match twice (the "Saima ซ้ำ" bug). Block instead of corrupting.
      const conflict = upcoming.find(m => m.players.includes(inId) && m.players.includes(outId));
      if (conflict) {
        toast(`${outName} อยู่ในคิวเดียวกับ ${inName} อยู่แล้ว — สลับไม่ได้ ลองสุ่มคิวใหม่แทน`);
        return;
      }
    }

    const newPlayers = match.players.map(id => id === outId ? inId : id);
    const active = { ...group.active, [courtId]: { ...match, players: newPlayers } };
    if (fromUpcoming) {
      // Send the outbound player to the inbound player's queue slot — in BOTH the
      // flat players[] and the teams[][] arrays (teams was previously left stale).
      const repl = (id) => (id === inId ? outId : id);
      upcoming = upcoming.map(m =>
        m.players.includes(inId)
          ? { ...m,
              players: m.players.map(repl),
              teams: m.teams ? m.teams.map(t => t.map(repl)) : m.teams }
          : m
      );
    }
    updateGroup(group.id, { active, upcoming });
    addLog({
      type: "swap", entityType: "court", entityId: courtId,
      desc: `เปลี่ยนตัว: ${group.name} • สนาม ${courtNameOf(courtId)} — เอาออก ${outName} (Lv ${fmtLvOf(outId)}) ➜ ใส่ ${inName} (Lv ${fmtLvOf(inId)})` +
        (fromUpcoming ? " • ดึงจากคิว" : ""),
      newValue: { out: outId, in: inId },
    });
    toast(`เปลี่ยนตัว: ${outName} → ${inName}`);
    setSwapTarget(null);
  }

  function updateCourt(courtId, patch) {
    const courts = group.courts.map(c => c.id === courtId ? { ...c, ...patch } : c);
    updateGroup(group.id, { courts });
    addLog({ type: "court.edit", desc: `ปรับตั้งค่าสนาม: ${group.name} • สนาม ${patch.name || group.courts.find(c=>c.id===courtId)?.name}` });
  }

  function doAddQueue() {
    const res = store.addQueueMatch(group.id);
    if (!res.ok) {
      if (res.reason === "not-enough") {
        const canPlan = groupPlayers.length >= (group.mode === "double" ? 4 : 2);
        toast(`คนพักรอไม่พอสร้างคิว — ต้องการ ${res.need} คน แต่ว่างจริงแค่ ${res.have} คน` +
          (canPlan ? ` • กด "วางแผนรอบถัดไป" เพื่อจัดคิวโดยรวมคนที่กำลังเล่นด้วย` : ` • รอแมตช์จบหรือลบคิวเพื่อปล่อยคนออกมาพักรอ`));
      } else toast("สร้างคิวไม่สำเร็จ");
      return;
    }
    toast(res.violations > 0
      ? `เพิ่มคิวแล้ว • ⚠ ${res.violations} แมตช์เกิน skill gap`
      : `เพิ่มคิวแล้ว • Balance ${res.balance}%`);
  }

  function doPlanNextRound() {
    const res = store.planNextRoundMatch(group.id);
    if (!res.ok) {
      if (res.reason === "not-enough")
        toast(`วางแผนรอบถัดไปไม่ได้ — ก๊วนนี้มีผู้เล่นที่ยังไม่ถูกจองคิว ${res.have} คน (ต้องการ ${res.need} คน)`);
      else if (res.reason === "dup-guard")
        toast("เกิดข้อผิดพลาดในการจัดคู่ — ลองใหม่อีกครั้ง");
      else toast("วางแผนรอบถัดไปไม่สำเร็จ");
      return;
    }
    toast(res.violations > 0
      ? `วางแผนรอบถัดไปแล้ว • ⚠ ${res.violations} แมตช์เกิน skill gap • วางลงสนามได้เมื่อผู้เล่นว่าง`
      : `วางแผนรอบถัดไปแล้ว • Balance ${res.balance}% • วางลงสนามได้เมื่อผู้เล่นว่าง`);
  }

  function doRandomizeQueue(matchId) {
    const res = store.randomizeQueueMatch(group.id, matchId);
    // Keep the modal open so the result shows immediately and the user can re-roll
    // again without reopening. The modal re-renders with the updated match.
    if (!res.ok) {
      if (res.reason === "not-enough")
        toast(`คนพักรอไม่พอสุ่มคิว — ต้องการ ${res.need} คน ว่างจริง ${res.have} คน • ลอง "สุ่มใหม่" เพื่อรวมคนที่กำลังเล่น`);
      else toast("สุ่มคิวไม่สำเร็จ");
      return;
    }
    toast(res.violations > 0
      ? `สุ่มคิวใหม่แล้ว • ⚠ ${res.violations} แมตช์เกิน skill gap`
      : `สุ่มคิวใหม่แล้ว • Balance ${res.balance}%`);
  }

  // "สุ่มใหม่" — re-roll this queue slot from the WHOLE roster (works like วางแผนรอบถัดไป).
  function doReplanQueue(matchId) {
    const res = store.replanQueueMatch(group.id, matchId);
    // Keep the modal open (see doRandomizeQueue).
    if (!res.ok) {
      if (res.reason === "not-enough")
        toast(`ผู้เล่นไม่พอ — ต้องการ ${res.need} คน มี ${res.have} คน`);
      else if (res.reason === "dup-guard") toast("เกิดข้อผิดพลาด — ลองใหม่อีกครั้ง");
      else toast("สุ่มใหม่ไม่สำเร็จ");
      return;
    }
    toast(res.violations > 0
      ? `สุ่มใหม่แล้ว • ⚠ ${res.violations} แมตช์เกิน skill gap`
      : `สุ่มใหม่แล้ว • Balance ${res.balance}%`);
  }

  // Replace ONE player inside a specific queue slot (tap-to-swap in the randomize modal).
  // Free pick: incoming may be anyone except someone already in THIS match (no dup).
  function replaceQueuePlayer(matchId, outId, inId) {
    if (!inId || outId === inId) return;
    const upcoming = (group.upcoming || []).slice();
    const idx = upcoming.findIndex(m => m.id === matchId);
    if (idx < 0) return;
    const m = upcoming[idx];
    if (m.players.includes(inId)) { toast(`${fmtName(inId)} อยู่ในคิวนี้แล้ว`); return; }
    const repl = (id) => (id === outId ? inId : id);
    upcoming[idx] = { ...m, players: m.players.map(repl), teams: m.teams ? m.teams.map(t => t.map(repl)) : m.teams };
    updateGroup(group.id, { upcoming });
    addLog({
      type: "queue.edit", entityType: "group", entityId: group.id,
      desc: `เปลี่ยนตัวในคิว: ${group.name} • คิว #${idx + 1} — ${fmtName(outId)} ➜ ${fmtName(inId)} (Lv ${fmtLvOf(inId)})`,
    });
    toast(`เปลี่ยนตัวในคิว: ${fmtName(outId)} → ${fmtName(inId)}`);
  }

  // Open the manual-queue modal. Free pick: the only requirement is that the group
  // has enough players overall — the host may choose anyone (incl. players currently
  // on a court or already in another queue), since this is a planned queue and the
  // place-on-court step re-validates court conflicts.
  function openManualQueue() {
    const need = group.mode === "double" ? 4 : 2;
    if (groupPlayers.length < need) {
      toast(`ก๊วนนี้มีผู้เล่น ${groupPlayers.length} คน — ต้องมีอย่างน้อย ${need} คนจึงจะสร้างคิวได้`);
      return;
    }
    setManualQueueOpen(true);
  }

  // Hand-pick a queue match: host explicitly chooses who is on Team A vs Team B.
  // FREE pick — players currently on a court or in another queue ARE allowed (this is
  // a planned queue; placeQueueOnCourt blocks placement until they actually free up).
  // The only hard rule is no duplicate player within this single match.
  function addManualQueue(teamA, teamB) {
    const need = group.mode === "double" ? 2 : 1;
    if (teamA.length !== need || teamB.length !== need) {
      toast(`ต้องเลือกผู้เล่นให้ครบทั้งสองฝั่ง (ฝั่งละ ${need} คน)`);
      return;
    }
    const picks = [...teamA, ...teamB];
    // No duplicate within this match (same person can't be on both sides / twice).
    if (new Set(picks).size !== picks.length) {
      toast("มีผู้เล่นซ้ำกันในแมตช์เดียวกัน");
      return;
    }
    const teams = [teamA.slice(), teamB.slice()];
    const match = { id: "m" + Math.random().toString(36).slice(2, 6), players: picks, teams };
    updateGroup(group.id, { upcoming: [...(group.upcoming || []), match] });
    const lv = (id) => { const l = playerById[id]?.level; return l == null ? "?" : formatLevel(l, systemConfig); };
    const fmtT = (ids) => ids.map(id => `${playerById[id]?.name?.split(" ")[0] || "?"} (Lv ${lv(id)})`).join(" & ");
    const aNames = fmtT(teamA), bNames = fmtT(teamB);
    const queuedNow = picks.filter(id => activePlayerIds.has(id)).map(id => playerById[id]?.name?.split(" ")[0]);
    addLog({
      type: "queue.add", entityType: "group", entityId: group.id,
      desc: `สร้างคิวเอง (เลือกเอง): ${group.name} • คิว #${(group.upcoming||[]).length + 1} — ${aNames} พบ ${bNames}` +
        (queuedNow.length ? ` • รอผู้เล่นจบเกมก่อน: ${queuedNow.join(", ")}` : ""),
      newValue: { teamA, teamB },
    });
    toast(`เพิ่มคิวแล้ว: ${teamA.map(id=>playerById[id]?.name?.split(" ")[0]).join(" & ")} vs ${teamB.map(id=>playerById[id]?.name?.split(" ")[0]).join(" & ")}`);
    setManualQueueOpen(false);
  }

  /* ---- Queue (upcoming) management ---- */
  // Hard invariant: a player can never be active on two courts at once. Queues
  // are court-agnostic while waiting; the user picks a free court at placement
  // time, and we re-validate that none of the queue's players are currently live.
  function placeQueueOnCourt(matchId, courtId) {
    const upcoming = group.upcoming || [];
    const m = upcoming.find(x => x.id === matchId);
    if (!m) return;
    if (group.active[courtId]) { toast("สนามนี้กำลังเล่นอยู่ — เลือกสนามว่าง"); return; }
    // Guard: never place a match that already contains a duplicated player.
    if (new Set(m.players).size !== m.players.length) {
      toast("คิวนี้มีผู้เล่นซ้ำกัน — กรุณาสุ่มคิวใหม่");
      return;
    }
    const activeIds = new Set();
    Object.values(group.active || {}).forEach(mm => mm?.players?.forEach(id => activeIds.add(id)));
    const conflict = m.players.filter(id => activeIds.has(id));
    if (conflict.length) {
      const names = conflict.map(id => playerById[id]?.name?.split(" ")[0] || "?").join(", ");
      toast(`มีผู้เล่นซ้ำกับในสนาม: ${names} — รอให้จบก่อน`);
      return;
    }
    const active = { ...group.active, [courtId]: { players: m.players, startedAt: Date.now() } };
    updateGroup(group.id, { active, upcoming: upcoming.filter(x => x.id !== matchId) });
    const courtName = group.courts.find(c => c.id === courtId)?.name || "?";
    addLog({
      type: "match.start", entityType: "court", entityId: courtId,
      desc: `เริ่มแมตช์ (ลงสนามจากคิว): ${group.name} • สนาม ${courtName} • ${fmtMatchup(m.players)}`,
      newValue: { players: m.players },
    });
    touchGroupActivity(group.id);
    toast(`ลงสนาม ${courtName} แล้ว`);
    setPlaceTarget(null);
  }

  // From the queue card "ลงสนาม" button: if exactly one court is free, place
  // immediately; otherwise open the court picker so the user chooses.
  function startPlace(matchId) {
    const m = (group.upcoming || []).find(x => x.id === matchId);
    if (!m) return;
    const activeIds = new Set();
    Object.values(group.active || {}).forEach(mm => mm?.players?.forEach(id => activeIds.add(id)));
    const conflict = m.players.filter(id => activeIds.has(id));
    if (conflict.length) {
      const names = conflict.map(id => playerById[id]?.name?.split(" ")[0] || "?").join(", ");
      toast(`มีผู้เล่นซ้ำกับในสนาม: ${names} — รอให้จบก่อน`);
      return;
    }
    const free = group.courts.filter(c => !group.active[c.id]);
    if (!free.length) { toast("ไม่มีสนามว่าง"); return; }
    if (free.length === 1) { placeQueueOnCourt(matchId, free[0].id); return; }
    setPlaceTarget({ matchId });
  }

  // From an empty court's "ลงคิวถัดไป" button: place the first queue (in order)
  // whose players don't conflict with anyone currently live.
  function fillCourtWithNextValid(courtId) {
    if (group.active[courtId]) return;
    const activeIds = new Set();
    Object.values(group.active || {}).forEach(mm => mm?.players?.forEach(id => activeIds.add(id)));
    const m = (group.upcoming || []).find(x => x.players.every(id => !activeIds.has(id)));
    if (!m) { toast("ไม่มีคิวที่ลงได้ — ผู้เล่นซ้ำกับสนามอื่น"); return; }
    placeQueueOnCourt(m.id, courtId);
  }

  function deleteUpcomingMatch(matchId) {
    const upcoming = group.upcoming || [];
    const idx = upcoming.findIndex(x => x.id === matchId);
    if (idx < 0) return;
    const m = upcoming[idx];
    updateGroup(group.id, { upcoming: upcoming.filter(x => x.id !== matchId) });
    const names = m.players.map(id => playerById[id]?.name?.split(" ")[0] || "?").join(", ");
    addLog({
      type: "queue.delete", entityType: "group", entityId: group.id,
      desc: `ลบคิว: ${group.name} • คิว #${idx + 1}`,
      oldValue: { queue: m.players, names },
    });
    toast(`ลบคิว #${idx + 1} แล้ว`);
  }

  function addCourt() {
    // Auto-pick next name: try numbers first, then letters
    const existingNames = new Set(group.courts.map(c => c.name));
    let name = String(group.courts.length + 1);
    let n = group.courts.length + 1;
    while (existingNames.has(name)) { n += 1; name = String(n); }
    const newCourt = { id: (store.sbMode && crypto.randomUUID) ? crypto.randomUUID() : "c" + Math.random().toString(36).slice(2, 8), name };
    updateGroup(group.id, {
      courts: [...group.courts, newCourt],
      active: { ...group.active, [newCourt.id]: null },
    });
    addLog({ type: "court.add", desc: `เพิ่มสนาม "${name}" ในก๊วน ${group.name}` });
    toast(`เพิ่มสนาม ${name} แล้ว`);
  }

  function removeCourt(courtId) {
    const court = group.courts.find(c => c.id === courtId);
    if (!court) return;
    const match = group.active[courtId];
    if (match) {
      if (!confirm(`สนาม ${court.name} กำลังเล่นอยู่\nผู้เล่นจะกลับไปอยู่ในม้านั่ง — ลบสนามนี้?`)) return;
    } else {
      if (!confirm(`ลบสนาม ${court.name}?`)) return;
    }
    const courts = group.courts.filter(c => c.id !== courtId);
    const active = { ...group.active };
    delete active[courtId];
    // Also drop any upcoming queued for this court
    const upcoming = (group.upcoming || []).filter(m => m.court !== courtId);
    updateGroup(group.id, { courts, active, upcoming });
    addLog({ type: "court.remove", desc: `ลบสนาม "${court.name}" จากก๊วน ${group.name}` });
    toast(`ลบสนาม ${court.name} แล้ว`);
  }

  return (
    <div className="page" data-screen-label="Group Session">
      <div className="sec-head">
        <div>
          <button className="btn ghost sm" onClick={onBack} style={{marginBottom: 6, marginLeft: -8}}>
            <Icon name="chevL" size={14} /> ก๊วนทั้งหมด
          </button>
          <h2 className="sec-title">
            <span className="dot" style={{background: group.color, width:12, height:12, marginRight: 8, verticalAlign:"middle"}} />
            {group.name}
          </h2>
          <div className="sec-sub">
            {group.venue} · {group.mode === "double" ? "ประเภทคู่ (Double)" : "ประเภทเดี่ยว (Single)"}
            · {groupPlayers.length} ผู้เล่น
            {balanceScore !== null && <> · <span style={{color: "var(--green)", fontWeight:600}}>Balance {balanceScore}%</span></>}
            {isSuper && owner && <> · เจ้าของ: <b>{owner.name}</b></>}
          </div>
        </div>
        <div className="row gap-2">
          {can.editPlayers && (
            <button className="btn" onClick={() => setAddPlayersOpen(true)}>
              <Icon name="user" size={14} /> จัดการผู้เล่น
            </button>
          )}
          {can.runMatching && (
            <InfoTip text="สุ่มผู้เล่นลงสนามใหม่ตามกฎจับคู่ — ระบบจะเลือกคนที่ลงน้อยที่สุดก่อน แล้วพยายามจับคู่ให้สมดุลตามกฎของก๊วน">
              <button className="btn primary" onClick={shuffleAll}>
                <Icon name="refresh" size={14} stroke={2} /> สุ่มจัดคู่ใหม่
              </button>
            </InfoTip>
          )}
        </div>
      </div>

      {/* Settings summary banner */}
      <div className="card-pad mb-2" style={{background:"var(--bg-elev-2)", borderRadius:12, border:"0.5px solid var(--sep-soft)"}}>
        <div className="row gap-3" style={{flexWrap:"wrap", fontSize:13}}>
          <span className="row gap-1">
            <Icon name="settings" size={13} />
            <b>Mode {group.settings?.balanceMode === "similar" ? "A: Similar Skill" : "B: Total Balance"}</b>
          </span>
          <span className="muted">·</span>
          <span>Skill Gap: <b>±{group.settings?.skillGap ?? 2}</b></span>
          <span className="muted">·</span>
          <span>Rotation: <b>{group.settings?.rotation === "random" ? "Random" : group.settings?.rotation === "competitive" ? "Competitive" : "Fair"}</b></span>
          <span className="muted">·</span>
          <span>
            <InfoTip text="โหมดเพศของการจับคู่ — ถ้าผู้เล่นไม่พอตามเงื่อนไข ระบบจะจัดแมตช์ใกล้เคียงที่สุด—ไม่บล็อกการจัดคู่">เพศ</InfoTip>: <b>{genderModeLabel(group.settings?.genderMode || "any")}</b>
          </span>
          <div className="grow" />
          {can.manageGroups && onOpenSettings && (
            <button className="btn sm ghost" onClick={onOpenSettings}>
              ปรับการตั้งค่า →
            </button>
          )}
        </div>
      </div>

      {/* Courts */}
      <section>
        <div className="spread mb-1">
          <h3 style={{margin:0, fontSize:15, fontWeight:600, color:"var(--text-3)", letterSpacing:"0.04em", textTransform:"uppercase"}}>
            กำลังเล่นอยู่ · {group.courts.length} สนาม
          </h3>
          {can.manageGroups && (
            <button className="btn sm" onClick={addCourt}>
              <Icon name="plus" size={12} stroke={2.4} /> เพิ่มสนาม
            </button>
          )}
        </div>
        <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(min(320px, 100%), 1fr))", gap: 16, overflow:"hidden"}}>
          {group.courts.map(c => (
            <CourtCard
              key={c.id}
              court={c}
              match={group.active[c.id]}
              mode={c.settings?.mode || group.mode}
              groupSettings={group.settings}
              playerById={playerById}
              plays={group.plays || {}}
              onEnd={can.runMatching ? () => setResultModal({ courtId: c.id }) : null}
              onFill={can.runMatching ? () => fillCourtWithNextValid(c.id) : null}
              onSwap={can.runMatching ? (outId) => setSwapTarget({ courtId: c.id, outId }) : null}
              onRemove={can.manageGroups ? () => removeCourt(c.id) : null}
              onEditCourt={can.manageGroups ? () => setCourtSettingsId(c.id) : null}
              hasUpcoming={(group.upcoming || []).length > 0}
              canFill={(group.upcoming || []).some(m => m.players.every(id => !activePlayerIds.has(id)))}
              warnMinutes={systemConfig?.matchWarnMinutes ?? 60}
            />
          ))}
          {can.manageGroups && (
            <button className="court add-court-tile" onClick={addCourt}>
              <Icon name="plus" size={26} stroke={1.6} />
              <div style={{marginTop:6, fontWeight:500}}>เพิ่มสนาม</div>
              <div className="small muted">สนามที่ {group.courts.length + 1}</div>
            </button>
          )}
        </div>
      </section>

      {/* Upcoming queue */}
      <section>
        <div className="sec-head">
          <h3 style={{margin:0, fontSize:15, fontWeight:600, color:"var(--text-3)", letterSpacing:"0.04em", textTransform:"uppercase"}}>
            <InfoTip text="รายชื่อผู้เล่นที่รอจะได้ลงสนามถัดไป — พอมีสนามว่างจะขยับขึ้นมาเล่นต่อได้ทันที">คิวรอลงสนาม</InfoTip>
          </h3>
          <div className="row gap-2">
            {can.runMatching && (
              <InfoTip text="เลือกผู้เล่นเองว่าใครอยู่ฝั่งไหน — เลือกใครก็ได้ในก๊วน รวมถึงคนที่กำลังเล่นหรืออยู่ในคิว (คิวจะวางลงสนามได้เมื่อผู้เล่นว่าง)">
                <button className="btn sm" onClick={openManualQueue}>
                  <Icon name="user" size={12} /> สร้างคิวเอง
                </button>
              </InfoTip>
            )}
            {can.runMatching && (
              <InfoTip text="สร้างคิวใหม่จากผู้เล่นที่กำลังพักรอ (ไม่ดึงคนที่กำลังเล่นอยู่) — ใช้ตอนคิวหมดแต่ยังมีสนามว่างและคนรอ • ถ้าคนพักไม่พอจะแจ้งเตือน">
                <button className="btn sm" onClick={doAddQueue}>
                  <Icon name="plus" size={12} stroke={2.4} /> สุ่มคิวจากคนพักรอ
                </button>
              </InfoTip>
            )}
            {can.runMatching && canPlanNextRound && (
              <InfoTip text="วางแผนแมตช์รอบถัดไปจากผู้เล่นทั้งก๊วน (รวมคนที่กำลังเล่นอยู่ โดยให้คนที่พัก/ลงน้อยได้เล่นก่อน) — กดซ้ำได้เรื่อยๆ เพื่อจัดคิวล่วงหน้าหลายรอบ เหมาะกับเคสคนน้อย เช่น 6 คน/1 สนาม • แต่ละคิวจะวางลงสนามได้เมื่อผู้เล่นเล่นจบและว่าง">
                <button className="btn sm primary" onClick={doPlanNextRound}>
                  <Icon name="history" size={12} /> วางแผนรอบถัดไป
                </button>
              </InfoTip>
            )}
            <div className="muted small">{(group.upcoming || []).length} แมตช์</div>
          </div>
        </div>
        {(group.upcoming || []).length ? (
          <div className="queue">
            {group.upcoming.map((m, i) => {
              const conflictIds = m.players.filter(id => activePlayerIds.has(id));
              const conflictNames = conflictIds.map(id => playerById[id]?.name?.split(" ")[0] || "?").join(", ");
              return (
                <QueueCard key={m.id} match={m} index={i + 1} mode={group.mode}
                  playerById={playerById}
                  plays={group.plays || {}}
                  canManage={can.runMatching}
                  conflictNames={conflictNames}
                  freeCourtCount={freeCourtCount}
                  onPlace={can.runMatching ? () => startPlace(m.id) : null}
                  onRandomize={can.runMatching ? () => setRandomizeMatchId(m.id) : null}
                  onDelete={can.runMatching ? () => deleteUpcomingMatch(m.id) : null} />
              );
            })}
          </div>
        ) : (
          <div className="card empty">
            <div className="em">🕒</div>
            <div>ยังไม่มีคิวรอ — กด "สุ่มจัดคู่ใหม่" เพื่อสร้างคิว</div>
          </div>
        )}
      </section>

      {/* Bench */}
      <section>
        <div className="sec-head">
          <h3 style={{margin:0, fontSize:15, fontWeight:600, color:"var(--text-3)", letterSpacing:"0.04em", textTransform:"uppercase"}}>
            พักรอ · {bench.length} คน
          </h3>
          <div className="row gap-2" style={{alignItems:"center"}}>
            <div className="muted small">เรียงตามจำนวนการลงสนาม (น้อย → มาก)</div>
            {can.runMatching && (
              <button className="btn sm ghost" onClick={() => setPlaysEditOpen(true)}>
                <Icon name="edit" size={12} /> แก้ไขรอบเล่น
              </button>
            )}
          </div>
        </div>
        {bench.length ? (
          <div className="bench-grid">
            {bench
              .slice()
              .sort((a, b) => (group.plays?.[a.id] || 0) - (group.plays?.[b.id] || 0))
              .map(p => (
                <div key={p.id} className="bench-card">
                  <span className="bench-dot" />
                  <span className="grow" style={{minWidth:0, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{p.name.split(" ")[0]}</span>
                  <span className="play-count" title={`ลงสนามแล้ว ${group.plays?.[p.id] || 0} รอบ`}>{group.plays?.[p.id] || 0}</span>
                  <LevelPill level={p.level} cfg={systemConfig} />
                </div>
              ))}
          </div>
        ) : (
          // bench = players NOT on any court AND NOT in any queue
          // So 0 bench means everyone is either playing OR waiting in queue
          (() => {
            const inQueue = new Set();
            (group.upcoming || []).forEach(m => m.players?.forEach(id => inQueue.add(id)));
            const hasQueue = inQueue.size > 0;
            const hasActive = activePlayerIds.size > 0;
            let msg, sub;
            if (hasActive && hasQueue) {
              msg = "ทุกคนอยู่ในสนามหรือคิวแล้ว";
              sub = `${activePlayerIds.size} คนกำลังเล่น · ${inQueue.size} คนรอในคิว`;
            } else if (hasActive) {
              msg = "ทุกคนกำลังลงสนามอยู่ 🏸";
              sub = null;
            } else if (hasQueue) {
              msg = "ทุกคนอยู่ในคิวรอลงสนาม";
              sub = "เลือกสนามว่างเพื่อเริ่มเล่น";
            } else {
              msg = "ยังไม่มีผู้เล่นในก๊วนนี้";
              sub = null;
            }
            return (
              <div className="card empty" style={{flexDirection:"column", gap:4}}>
                <div>{msg}</div>
                {sub && <div className="small muted">{sub}</div>}
              </div>
            );
          })()
        )}
      </section>

      {/* History */}
      <section>
        <h3 className="mb-1" style={{fontSize:15, fontWeight:600, color:"var(--text-3)", letterSpacing:"0.04em", textTransform:"uppercase"}}>
          ประวัติแมตช์ในก๊วนนี้
        </h3>
        {(group.history || []).length ? (
          <div className="card">
            {group.history.slice(0, 8).map(h => (
              <div key={h.id} className="card-row">
                <div className="grow">
                  <div style={{fontWeight:500}}>
                    สนาม {group.courts.find(c=>c.id===h.court)?.name} —
                    {" "}{h.players.map(id => playerById[id]?.name?.split(" ")[0] || "?").join(", ")}
                  </div>
                  <div className="small muted">
                    {fmtTime(h.at)}
                    {h.duration != null && <> · <Icon name="clock" size={11} /> {fmtElapsed(h.duration)}</>}
                    {" · "}{h.winners ? <>ผู้ชนะ: <b>{h.winners.map(id => playerById[id]?.name?.split(" ")[0]).join(" & ")}</b></> : "ไม่บันทึกผล"}
                  </div>
                </div>
                {h.winners && <span className="chip green"><Icon name="trophy" size={12} stroke={2} /> มีผล</span>}
              </div>
            ))}
          </div>
        ) : <div className="card empty"><div>ยังไม่มีประวัติแมตช์</div></div>}
      </section>

      {/* Result modal */}
      {resultModal && (() => {
        const m = group.active[resultModal.courtId];
        if (!m) { setResultModal(null); return null; }
        const court = group.courts.find(c => c.id === resultModal.courtId);
        return (
          <Modal open title={`บันทึกผล · สนาม ${court.name}`} onClose={() => setResultModal(null)}>
            <p className="muted small mb-2">ใครชนะแมตช์นี้? (เลือก "ไม่บันทึก" ก็ได้)</p>
            <div className="result-pick">
              <button className="result-card win" onClick={() => endMatch(resultModal.courtId, 1)}>
                <div className="rc-title">{group.mode === "double" ? "ทีม A ชนะ" : `${playerById[m.players[0]]?.name?.split(" ")[0]} ชนะ`}</div>
                <div className="small">{m.players.slice(0, group.mode==="double" ? 2 : 1).map(id => playerById[id]?.name?.split(" ")[0]).join(" & ")}</div>
              </button>
              <button className="result-card lose" onClick={() => endMatch(resultModal.courtId, 2)}>
                <div className="rc-title">{group.mode === "double" ? "ทีม B ชนะ" : `${playerById[m.players[1]]?.name?.split(" ")[0]} ชนะ`}</div>
                <div className="small">{m.players.slice(group.mode==="double" ? 2 : 1).map(id => playerById[id]?.name?.split(" ")[0]).join(" & ")}</div>
              </button>
            </div>
            <button className="btn block mt-2" onClick={() => endMatch(resultModal.courtId, null)}>
              ไม่บันทึกผล (จบแมตช์)
            </button>
          </Modal>
        );
      })()}

      {/* Court settings modal */}
      {courtSettingsId && (() => {
        const c = group.courts.find(x => x.id === courtSettingsId);
        if (!c) { setCourtSettingsId(null); return null; }
        return (
          <CourtSettingsModal
            court={c}
            groupSettings={group.settings}
            groupMode={group.mode}
            systemConfig={store.systemConfig}
            onSave={(patch) => updateCourt(courtSettingsId, patch)}
            onClose={() => setCourtSettingsId(null)}
          />
        );
      })()}

      {/* Randomize a single queue match (court-agnostic) confirm modal */}
      {randomizeMatchId && (() => {
        const idx = (group.upcoming || []).findIndex(m => m.id === randomizeMatchId);
        if (idx < 0) { setRandomizeMatchId(null); return null; }
        return (
          <RandomizeQueueModal
            match={group.upcoming[idx]}
            index={idx + 1}
            mode={group.mode}
            settings={group.settings}
            players={groupPlayers}
            playerById={playerById}
            plays={group.plays || {}}
            activeIds={activePlayerIds}
            queuedIds={new Set((group.upcoming || []).flatMap(m => m.players || []))}
            systemConfig={systemConfig}
            onClose={() => setRandomizeMatchId(null)}
            onBenchRandomize={() => doRandomizeQueue(randomizeMatchId)}
            onReplan={() => doReplanQueue(randomizeMatchId)}
            onReplacePlayer={(outId, inId) => replaceQueuePlayer(randomizeMatchId, outId, inId)}
          />
        );
      })()}

      {/* Place queue onto a free court (when more than one is free) */}
      {placeTarget && (() => {
        const m = (group.upcoming || []).find(x => x.id === placeTarget.matchId);
        if (!m) { setPlaceTarget(null); return null; }
        const free = group.courts.filter(c => !group.active[c.id]);
        return (
          <Modal open title="เลือกสนามที่จะลง" onClose={() => setPlaceTarget(null)}
            footer={<button className="btn" onClick={() => setPlaceTarget(null)}>ยกเลิก</button>}>
            <p className="muted small mb-2">เลือกสนามว่างที่จะนำคิวนี้ลงเล่น</p>
            <div className="row gap-1 mb-2" style={{flexWrap:"wrap"}}>
              {m.players.map(id => (
                <span key={id} className="chip">{playerById[id]?.name?.split(" ")[0] || "?"}</span>
              ))}
            </div>
            <div className="bench-grid">
              {free.map(c => (
                <button key={c.id} className="btn" onClick={() => placeQueueOnCourt(placeTarget.matchId, c.id)}>
                  <Icon name="play" size={13} stroke={2.2} /> สนาม {c.name}
                </button>
              ))}
            </div>
            {!free.length && <div className="muted small">ไม่มีสนามว่าง</div>}
          </Modal>
        );
      })()}

      {/* Swap player modal */}
      {swapTarget && (
        <SwapPlayerModal
          group={group}
          playerById={playerById}
          bench={bench}
          swap={swapTarget}
          onClose={() => setSwapTarget(null)}
          onPick={(inId, fromUpcoming) => swapPlayer(swapTarget.courtId, swapTarget.outId, inId, fromUpcoming)}
        />
      )}

      {/* Manual queue creation modal */}
      {manualQueueOpen && (
        <ManualQueueModal
          mode={group.mode}
          players={groupPlayers}
          activeIds={activePlayerIds}
          queuedIds={new Set((group.upcoming || []).flatMap(m => m.players || []))}
          plays={group.plays || {}}
          systemConfig={systemConfig}
          onClose={() => setManualQueueOpen(false)}
          onConfirm={addManualQueue}
        />
      )}

      {/* Manage players modal */}
      {addPlayersOpen && (
        <ManagePlayersModal
          store={store}
          group={group}
          onClose={() => setAddPlayersOpen(false)}
        />
      )}

      {/* Edit plays modal */}
      {playsEditOpen && (
        <PlaysEditModal
          group={group}
          players={groupPlayers}
          systemConfig={systemConfig}
          onClose={() => setPlaysEditOpen(false)}
          onSave={(newPlays) => {
            updateGroup(group.id, { plays: newPlays });
            addLog({ type: "group.plays.edit", entityType: "group", entityId: group.id,
                     desc: `แก้ไขรอบเล่น: ${group.name}`, newValue: newPlays });
            toast("บันทึกรอบเล่นแล้ว");
            setPlaysEditOpen(false);
          }}
          onReset={() => {
            if (!confirm(`รีเซ็ตรอบเล่นทั้งหมดของ "${group.name}" เป็น 0?\n\nระบบจะเรียงใหม่จากศูนย์ทุกคน`)) return;
            const zeroed = Object.fromEntries(group.playerIds.map(id => [id, 0]));
            updateGroup(group.id, { plays: zeroed });
            addLog({ type: "group.plays.reset", entityType: "group", entityId: group.id,
                     desc: `รีเซ็ตรอบเล่นทั้งหมด: ${group.name}` });
            toast("รีเซ็ตรอบเล่นทั้งหมดแล้ว");
            setPlaysEditOpen(false);
          }}
        />
      )}
    </div>
  );
}

/* -------- Court card -------- */
function CourtCard({ court, match, mode, groupSettings, playerById, plays = {}, onEnd, onFill, onSwap, onRemove, onEditCourt, hasUpcoming, canFill, warnMinutes = 60 }) {
  const [elapsed, setElapsed] = useState_grp(0);
  useEffect_grp(() => {
    if (!match) return;
    const tick = () => setElapsed(Math.floor((Date.now() - match.startedAt) / 1000));
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [match]);

  // Effective settings: court overrides > group defaults
  const cs = court.settings || {};
  const effective = {
    mode: cs.mode || mode,
    balanceMode: cs.balanceMode || groupSettings?.balanceMode || "total",
    skillGap: cs.skillGap ?? groupSettings?.skillGap ?? 2,
    rotation: cs.rotation || groupSettings?.rotation || "fair",
  };
  const hasOverride = !!(cs.mode || cs.balanceMode || cs.skillGap !== undefined || cs.rotation || cs.genderMode);

  if (!match) {
    return (
      <div className={`court ${hasOverride ? "has-override" : ""}`}>
        <div className="court-head">
          <div className="court-title">
            <div className="court-num">{court.name}</div>
            <div>
              <div style={{fontWeight:600, display:"flex", alignItems:"center", gap:6}}>
                สนาม {court.name}
                {hasOverride && <span className="chip purple" style={{fontSize:10, padding:"1px 6px"}}>Custom</span>}
              </div>
              <div className="court-status">ว่าง · {effective.mode === "double" ? "คู่" : "เดี่ยว"} · ±{effective.skillGap.toFixed(1)}</div>
            </div>
          </div>
          <div className="row gap-2">
            {onEditCourt && (
              <button className="btn icon" onClick={onEditCourt} title="ตั้งค่าสนาม">
                <Icon name="settings" size={13} />
              </button>
            )}
            {onRemove && (
              <button className="btn icon" onClick={onRemove} title="ลบสนาม">
                <Icon name="trash" size={13} />
              </button>
            )}
          </div>
        </div>
        <div className="court-net" style={{minHeight:160, display:"grid", gridTemplateColumns:"1fr", placeItems:"center"}}>
          <div className="muted small center">
            ว่างอยู่<br/>
            {hasUpcoming && onFill && (canFill
              ? <button className="btn primary sm mt-1" onClick={onFill}>
                  <Icon name="play" size={12} stroke={2.2} /> ลงคิวถัดไป
                </button>
              : <div className="mt-1" style={{maxWidth:200}}>รอผู้เล่นในสนามอื่นจบก่อน</div>)}
          </div>
        </div>
      </div>
    );
  }

  const team1 = effective.mode === "double" ? match.players.slice(0,2) : [match.players[0]];
  const team2 = effective.mode === "double" ? match.players.slice(2,4) : [match.players[1]];
  const avg = (arr) => (arr.reduce((s, id) => s + (playerById[id]?.level || 0), 0) / arr.length).toFixed(1);
  const isOverdue = elapsed >= warnMinutes * 60;

  return (
    <div className={`court playing ${hasOverride ? "has-override" : ""}${isOverdue ? " court-overdue" : ""}`}>
      <div className="court-head">
        <div className="court-title">
          <div className="court-num">{court.name}</div>
          <div>
            <div style={{fontWeight:600, display:"flex", alignItems:"center", gap:6}}>
              สนาม {court.name}
              {hasOverride && <span className="chip purple" style={{fontSize:10, padding:"1px 6px"}}>Custom</span>}
              {isOverdue && <span className="chip orange" style={{fontSize:10, padding:"1px 6px"}}>⏰ นานเกิน 1 ชม.</span>}
            </div>
            <div className={`court-status live${isOverdue ? " overdue" : ""}`}>กำลังเล่น · {fmtElapsed(elapsed)} · Mode {effective.balanceMode === "similar" ? "A" : "B"} ±{effective.skillGap.toFixed(1)}</div>
          </div>
        </div>
        <div className="row gap-2">
          {onEditCourt && (
            <button className="btn icon" onClick={onEditCourt} title="ตั้งค่าสนาม">
              <Icon name="settings" size={13} />
            </button>
          )}
          {onRemove && (
            <button className="btn icon" onClick={onRemove} title="ลบสนาม">
              <Icon name="trash" size={13} />
            </button>
          )}
          {onEnd && (
            <button className="btn primary sm" onClick={onEnd}>
              <Icon name="check" size={12} stroke={2.4} /> จบแมตช์
            </button>
          )}
        </div>
      </div>
      <div className="court-net">
        <div className="court-side">
          <div className="small muted spread"><span>{effective.mode==="double" ? "ทีม A" : "ผู้เล่น 1"}</span><span>เฉลี่ย Lv {avg(team1)}</span></div>
          {team1.map(id => <PlayerOnCourt key={id} p={playerById[id]} playCount={plays[id] || 0} onSwap={() => onSwap && onSwap(id)} />)}
        </div>
        <div className="court-divider" />
        <div className="court-side">
          <div className="small muted spread"><span>{effective.mode==="double" ? "ทีม B" : "ผู้เล่น 2"}</span><span>เฉลี่ย Lv {avg(team2)}</span></div>
          {team2.map(id => <PlayerOnCourt key={id} p={playerById[id]} playCount={plays[id] || 0} onSwap={() => onSwap && onSwap(id)} />)}
        </div>
      </div>
    </div>
  );
}

function PlayerOnCourt({ p, onSwap, playCount = 0 }) {
  if (!p) return null;
  const swappable = !!onSwap;
  return (
    <div className={`player-pill ${swappable ? "swappable" : ""}`}
         onClick={swappable ? onSwap : undefined}
         title={swappable ? "คลิกเพื่อเปลี่ยนตัว" : undefined}>
      <div className="avatar" style={{width:26, height:26, fontSize:11, background: `linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>
        {initials(p.name)}
      </div>
      <div className="player-pill-name">{p.name}</div>
      <span className="play-count" title={`ลงสนามแล้ว ${playCount} รอบ`}>{playCount}</span>
      <LevelPill level={p.level} />
      {swappable && <span className="swap-hint"><Icon name="refresh" size={12} stroke={2} /></span>}
    </div>
  );
}

function QueueCard({ match, index, mode, playerById, plays = {}, canManage,
                    conflictNames, freeCourtCount, onPlace, onRandomize, onDelete }) {
  const team1 = mode === "double" ? match.players.slice(0,2) : [match.players[0]];
  const team2 = mode === "double" ? match.players.slice(2,4) : [match.players[1]];
  const hasConflict = !!conflictNames;
  const noFreeCourt = freeCourtCount === 0;
  const placeable = !hasConflict && !noFreeCourt;
  return (
    <div className="queue-card">
      <div className="queue-head">
        <span>คิว #{index}</span>
        {hasConflict && <span className="chip orange" style={{fontSize:10}}>ผู้เล่นยังเล่นอยู่</span>}
      </div>
      <div className="pair">
        <div className="pair-side">
          {team1.map(id => (
            <div key={id} className="pname">
              <span>{playerById[id]?.name?.split(" ")[0]}</span>
              <span className="play-count" title={`ลงสนามแล้ว ${plays[id]||0} รอบ`}>{plays[id]||0}</span>
              <LevelPill level={playerById[id]?.level} />
            </div>
          ))}
        </div>
        <div className="pair-vs">VS</div>
        <div className="pair-side">
          {team2.map(id => (
            <div key={id} className="pname">
              <span>{playerById[id]?.name?.split(" ")[0]}</span>
              <span className="play-count" title={`ลงสนามแล้ว ${plays[id]||0} รอบ`}>{plays[id]||0}</span>
              <LevelPill level={playerById[id]?.level} />
            </div>
          ))}
        </div>
      </div>
      {canManage && (
        <div className="queue-foot">
          {placeable && onPlace ? (
            <button className="btn primary sm" onClick={onPlace}
              title="เลือกสนามว่างแล้วนำคิวนี้ลงเล่น">
              <Icon name="play" size={12} stroke={2.2} /> ลงสนาม
            </button>
          ) : hasConflict ? (
            <span className="queue-order-hint" title={`รอให้ ${conflictNames} เล่นจบก่อน`}>
              รอ {conflictNames} จบก่อน
            </span>
          ) : (
            <span className="queue-order-hint" title="ยังไม่มีสนามว่าง">ไม่มีสนามว่าง</span>
          )}
          <div className="grow" />
          {onRandomize && (
            <button className="btn icon" onClick={onRandomize} title="สุ่มผู้เล่นในคิวนี้ใหม่">
              <Icon name="refresh" size={12} stroke={2} />
            </button>
          )}
          {onDelete && (
            <button className="btn icon danger" onClick={onDelete} title="ลบคิวนี้">
              <Icon name="trash" size={12} />
            </button>
          )}
        </div>
      )}
    </div>
  );
}

/* -------- Re-roll a queue slot: bench-only / full-roster / tap-to-replace -------- */
function RandomizeQueueModal({ match, index, mode, settings, players, playerById, plays, activeIds, queuedIds, systemConfig, onClose, onBenchRandomize, onReplan, onReplacePlayer }) {
  const [replacing, setReplacing] = useState_grp(null); // outId currently being replaced
  const playersSig = match.players.join(",");
  // Clear the "replace this player" selection whenever the queue is re-rolled so the
  // candidate picker never points at a player who's no longer in the match.
  useEffect_grp(() => { setReplacing(null); }, [playersSig]);
  const eff = {
    mode,
    balanceMode: settings?.balanceMode || "total",
    skillGap: settings?.skillGap ?? 2,
    genderMode: settings?.genderMode || "any",
  };
  const _active = activeIds || new Set();
  const _queued = queuedIds || new Set();
  const statusOf = (id) => _active.has(id) ? "active" : (_queued.has(id) ? "queued" : "free");
  const inThisMatch = new Set(match.players);
  const rank = { free: 0, queued: 1, active: 2 };
  const candidates = players
    .filter(p => !inThisMatch.has(p.id))
    .sort((a, b) => (rank[statusOf(a.id)] - rank[statusOf(b.id)]) || ((plays[a.id] || 0) - (plays[b.id] || 0)));

  return (
    <Modal open title={`สุ่มคิวใหม่ · คิว #${index}`} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose}>ยกเลิก</button>
        <button className="btn" onClick={onBenchRandomize} title="สุ่มเฉพาะคนที่พักรอ/ว่าง (ไม่ดึงคนกำลังเล่น)">
          <Icon name="plus" size={13} stroke={2.2} /> สุ่มคิวจากคนพักรอ
        </button>
        <button className="btn primary" onClick={onReplan} title="สุ่มใหม่จากผู้เล่นทั้งก๊วน (รวมคนกำลังเล่น) แบบหมุนเวียนยุติธรรม">
          <Icon name="refresh" size={13} stroke={2} /> สุ่มใหม่
        </button>
      </>}>
      <div className="card-pad" style={{background:"var(--bg-elev-2)", borderRadius:10, fontSize:13}}>
        <div className="row gap-2" style={{flexWrap:"wrap"}}>
          <span>ประเภท: <b>{eff.mode === "double" ? "คู่" : "เดี่ยว"}</b></span><span className="muted">·</span>
          <span>Mode <b>{eff.balanceMode === "similar" ? "A" : "B"}</b></span><span className="muted">·</span>
          <span>Skill Gap <b>±{eff.skillGap.toFixed(1)}</b></span><span className="muted">·</span>
          <span>เพศ: <b>{genderModeLabel(eff.genderMode)}</b></span>
        </div>
      </div>

      <div className="mt-2 small">
        <div className="muted mb-1">คิวเดิม — แตะชื่อเพื่อเปลี่ยนตัวเอง</div>
        <div className="swap-grid">
          {match.players.map(id => {
            const p = playerById[id];
            const sel = replacing === id;
            return (
              <button key={id} className="swap-card" onClick={() => setReplacing(sel ? null : id)}
                style={sel ? { outline: "2px solid var(--tint)", outlineOffset: "-1px" } : undefined}>
                <div className="avatar" style={{width:28,height:28,fontSize:11,background:`linear-gradient(135deg, ${hashColor(id)}, ${hashColor(id+"x")})`}}>{initials(p?.name||"?")}</div>
                <div className="grow" style={{textAlign:"left",minWidth:0}}>
                  <div style={{fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{p?.name?.split(" ")[0]||"?"}</div>
                  <div className="small muted">ลง {plays[id]||0} ครั้ง</div>
                </div>
                <LevelPill level={p?.level} cfg={systemConfig} />
                <Icon name="refresh" size={12} stroke={2} />
              </button>
            );
          })}
        </div>
      </div>

      {replacing && (
        <div className="mt-2 small">
          <div className="row spread mb-1" style={{alignItems:"center"}}>
            <div className="muted">เลือกคนลงแทน <b>{playerById[replacing]?.name?.split(" ")[0]}</b></div>
            <button className="btn ghost sm" onClick={() => setReplacing(null)}>ยกเลิก</button>
          </div>
          <div className="swap-grid">
            {candidates.map(p => {
              const st = statusOf(p.id);
              return (
                <button key={p.id} className="swap-card" onClick={() => { onReplacePlayer(replacing, p.id); setReplacing(null); }}>
                  <div className="avatar" style={{width:28,height:28,fontSize:11,background:`linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>{initials(p.name)}</div>
                  <div className="grow" style={{textAlign:"left",minWidth:0}}>
                    <div style={{fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>
                      {p.name}
                      {st === "active" && <span className="chip" style={{marginLeft:6,fontSize:9,padding:"1px 5px",background:"var(--green-soft)",color:"var(--green)"}}>กำลังเล่น</span>}
                      {st === "queued" && <span className="chip" style={{marginLeft:6,fontSize:9,padding:"1px 5px",background:"var(--bg-inset)",color:"var(--text-3)"}}>ในคิว</span>}
                    </div>
                    <div className="small muted">ลง {plays[p.id]||0} ครั้ง</div>
                  </div>
                  <LevelPill level={p.level} cfg={systemConfig} />
                </button>
              );
            })}
          </div>
        </div>
      )}
    </Modal>
  );
}

/* -------- Swap player modal -------- */
function SwapPlayerModal({ group, playerById, bench, swap, onClose, onPick }) {
  const out = playerById[swap.outId];
  const court = group.courts.find(c => c.id === swap.courtId);
  const match = group.active[swap.courtId];
  const teammateIds = match
    ? (group.mode === "double"
        ? (match.players.slice(0,2).includes(swap.outId) ? match.players.slice(0,2) : match.players.slice(2,4))
        : [])
    : [];
  const teammateAvg = teammateIds
    .filter(id => id !== swap.outId)
    .reduce((s, id) => s + (playerById[id]?.level || 0), 0) /
    Math.max(1, teammateIds.length - 1);

  // From upcoming queue: list candidates (deduped) — each player appears once
  // Exclude: the outgoing player, anyone currently playing on ANY active court
  // (a player can be both on a court now AND scheduled in upcoming — must not
  // appear as a swap candidate or they'd be on two courts at once).
  const activeIds = new Set();
  Object.values(group.active || {}).forEach(m => m?.players?.forEach(id => activeIds.add(id)));
  const seenInQueue = new Set();
  const upcomingCands = [];
  (group.upcoming || []).forEach((m, mi) => {
    m.players.forEach(pid => {
      if (pid === swap.outId) return;              // exclude outgoing
      if (activeIds.has(pid)) return;              // exclude anyone playing right now
      if (seenInQueue.has(pid)) return;            // dedupe by player id
      seenInQueue.add(pid);
      const p = playerById[pid];
      if (p) upcomingCands.push({ player: p, fromQueueIdx: mi + 1, courtId: m.court });
    });
  });

  // Bench candidates — already excludes anyone in active/upcoming (computed in GroupSession),
  // but guard against the outgoing player and dedupe defensively
  const seenBench = new Set();
  const benchUniq = bench.filter(p => {
    if (p.id === swap.outId) return false;
    if (activeIds.has(p.id)) return false;
    if (seenBench.has(p.id)) return false;
    seenBench.add(p.id);
    return true;
  });

  function levelDelta(p) {
    return Math.abs(p.level - (out?.level || 0));
  }

  const benchSorted = benchUniq.slice().sort((a, b) => {
    // Suggest closest level first; tiebreak by fewer plays
    const da = levelDelta(a), db = levelDelta(b);
    if (da !== db) return da - db;
    return (group.plays?.[a.id] || 0) - (group.plays?.[b.id] || 0);
  });

  return (
    <Modal open size="large" onClose={onClose}
      title={`เปลี่ยนตัว · สนาม ${court?.name}`}
      footer={<button className="btn" onClick={onClose}>ยกเลิก</button>}>
      <div className="swap-out">
        <div className="small muted">ผู้เล่นที่จะออก</div>
        <div className="row gap-2 mt-1" style={{padding: "10px 12px", background: "var(--red-soft)", borderRadius: 12, border: "1px solid rgba(255,69,58,0.25)"}}>
          <div className="avatar" style={{width:32, height:32, fontSize:12, background:`linear-gradient(135deg, ${hashColor(out?.id || "")}, ${hashColor((out?.id || "")+"x")})`}}>
            {initials(out?.name || "?")}
          </div>
          <div className="grow">
            <div style={{fontWeight: 600}}>{out?.name}</div>
            <div className="small muted">{out?.hand ? `ถนัด${out.hand==="L"?"ซ้าย":"ขวา"}` : "—"}{out?.age ? ` · ${out.age} ปี` : ""}</div>
          </div>
          <LevelPill level={out?.level} />
        </div>
      </div>

      <div className="mt-2 mb-1 row spread">
        <div style={{fontWeight: 600}}>เลือกคนเข้าแทน</div>
        <div className="small muted">เรียงตามความใกล้เคียงระดับ</div>
      </div>

      <div className="swap-section">
        <div className="swap-section-head">
          <span className="bench-dot" style={{background: "var(--green)"}} />
          <span>กำลังพักรอ ({benchSorted.length})</span>
        </div>
        <div className="swap-grid">
          {benchSorted.length ? benchSorted.map(p => (
            <SwapCandidate key={p.id} p={p} out={out} plays={group.plays?.[p.id] || 0}
              onPick={() => onPick(p.id, false)} />
          )) : <div className="muted small" style={{padding: 12}}>ไม่มีผู้เล่นพักรอ</div>}
        </div>
      </div>

      {upcomingCands.length > 0 && (
        <div className="swap-section">
          <div className="swap-section-head">
            <span className="bench-dot" style={{background: "var(--orange)"}} />
            <span>อยู่ในคิวรอลงสนาม ({upcomingCands.length})</span>
            <span className="small muted" style={{marginLeft:"auto", fontWeight: 400}}>
              เลือกแล้วจะสลับตำแหน่งกัน
            </span>
          </div>
          <div className="swap-grid">
            {upcomingCands.map((c, i) => (
              <SwapCandidate key={c.player.id + "-" + i} p={c.player} out={out}
                badge={`คิว #${c.fromQueueIdx}`}
                onPick={() => onPick(c.player.id, true)} />
            ))}
          </div>
        </div>
      )}
    </Modal>
  );
}

function SwapCandidate({ p, out, plays, badge, onPick }) {
  const delta = Math.abs(p.level - (out?.level || 0));
  const tone = delta === 0 ? "green" : delta === 1 ? "blue" : delta === 2 ? "orange" : "red";
  return (
    <button className="swap-card" onClick={onPick}>
      <div className="avatar" style={{width:30, height:30, fontSize:12, background:`linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>
        {initials(p.name)}
      </div>
      <div className="grow" style={{textAlign:"left", minWidth: 0}}>
        <div style={{fontWeight:500, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{p.name}</div>
        <div className="small muted">
          {plays !== undefined && <>ลง {plays} ครั้ง · </>}
          <span style={{color: `var(--${tone === "blue" ? "tint" : tone === "green" ? "green" : tone === "orange" ? "orange" : "red"})`}}>
            {delta === 0 ? "ระดับเท่ากัน" : `ต่างกัน ${delta} ระดับ`}
          </span>
        </div>
      </div>
      {badge && <span className="chip orange" style={{fontSize: 10}}>{badge}</span>}
      <LevelPill level={p.level} />
    </button>
  );
}

/* -------- Manage players modal -------- */
function ManagePlayersModal({ store, group, onClose }) {
  const { visiblePlayers: players, updateGroup, joinActiveGroup, endActiveGroup, groups } = store;
  const [selected, setSelected] = useState_grp(new Set(group.playerIds));
  const [search, setSearch] = useState_grp("");
  const toast = useToast();
  const groupById = useMemo_grp(() => Object.fromEntries(groups.map(g => [g.id, g])), [groups]);

  const filtered = useMemo_grp(
    () => players.filter(p => p.name.toLowerCase().includes(search.toLowerCase())),
    [players, search]
  );

  // A player locked to ANOTHER active online group cannot join this one
  const lockedElsewhere = (p) => p.activeGroupId && p.activeGroupId !== group.id;

  const toggle = (id) => {
    const p = players.find(x => x.id === id);
    if (lockedElsewhere(p)) {
      toast(`${p.name} กำลังอยู่ในก๊วน ${groupById[p.activeGroupId]?.name || "อื่น"} — กด "จบก๊วนเดิม" ก่อน`);
      return;
    }
    setSelected(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  const save = () => {
    const prevIds = new Set(group.playerIds);
    const added = [...selected].filter(id => !prevIds.has(id));
    const removed = [...prevIds].filter(id => !selected.has(id));
    updateGroup(group.id, { playerIds: Array.from(selected) });
    // Joining this group checks the player into its active online session
    added.forEach(id => joinActiveGroup(id, group.id));
    // Leaving clears their active session if it was this group
    removed.forEach(id => {
      const pl = players.find(p => p.id === id);
      if (pl?.activeGroupId === group.id) endActiveGroup(id);
    });
    onClose();
  };

  const lockedCount = filtered.filter(lockedElsewhere).length;

  return (
    <Modal open title="จัดการผู้เล่นในก๊วน" size="large"
      footer={<>
        <button className="btn" onClick={onClose}>ยกเลิก</button>
        <button className="btn primary" onClick={save}>บันทึก ({selected.size} คน)</button>
      </>}
      onClose={onClose}>
      <div className="row gap-2 small muted mb-2" style={{alignItems:"flex-start"}}>
        <InfoTip text="ผู้เล่นหนึ่งคนเข้าร่วมได้เพียงก๊วนออนไลน์เดียวในเวลาเดียวกัน — ถ้ากำลังอยู่ในก๊วนอื่น ต้อง “จบก๊วนเดิม” ก่อนจึงจะย้ายมาก๊วนนี้ได้">
          <span>กฎการเข้าร่วม</span>
        </InfoTip>
        <span>เลือกผู้เล่นที่จะเข้าร่วมก๊วนนี้ — คนที่กำลังอยู่ในก๊วนอื่นจะถูกล็อกไว้</span>
      </div>
      <div style={{position:"relative", marginBottom:12}}>
        <input placeholder="ค้นหาชื่อผู้เล่น…" value={search} onChange={e => setSearch(e.target.value)}
          style={{paddingLeft: 36}} />
        <div style={{position:"absolute", left:12, top:"50%", transform:"translateY(-50%)", color:"var(--text-3)", pointerEvents:"none"}}>
          <Icon name="search" size={16} />
        </div>
      </div>
      <div className="card" style={{maxHeight: 360, overflow:"auto"}}>
        {filtered.map(p => {
          const locked = lockedElsewhere(p);
          const here = p.activeGroupId === group.id;
          return (
            <div key={p.id} className={`list-item ${locked ? "" : "selectable"} ${selected.has(p.id) ? "checked" : ""}`}
                 style={locked ? {opacity: 0.7} : undefined}
                 onClick={locked ? undefined : () => toggle(p.id)}>
              <div className="check-circle">{selected.has(p.id) && <Icon name="check" size={14} stroke={2.4} />}</div>
              <div className="grow" style={{minWidth:0}}>
                <div className="name" style={{display:"flex", gap:6, alignItems:"center", flexWrap:"wrap"}}>
                  {p.name}
                  {here && <span className="chip green" style={{fontSize:10}}>อยู่ก๊วนนี้</span>}
                  {locked && <span className="chip orange" style={{fontSize:10}}>
                    <span className="bench-dot" style={{background:"var(--orange)"}} /> {groupById[p.activeGroupId]?.name || "ก๊วนอื่น"}
                  </span>}
                </div>
                <div className="meta">
                  {genderLabel(p.gender)}
                  {p.hand ? ` · ถนัด${p.hand === "L" ? "ซ้าย" : "ขวา"}` : ""}
                  {locked && p.activeSince ? ` · เข้าร่วมตั้งแต่ ${fmtTime(p.activeSince)}` : ""}
                </div>
              </div>
              {locked && (
                <button className="btn sm" onClick={(e) => {
                  e.stopPropagation();
                  if (confirm(`จบก๊วนเดิมของ "${p.name}" (${groupById[p.activeGroupId]?.name || ""}) เพื่อย้ายมาก๊วนนี้?`)) {
                    endActiveGroup(p.id); toast(`${p.name} ออกจากก๊วนเดิมแล้ว — เลือกเข้าก๊วนนี้ได้`);
                  }
                }}>จบก๊วนเดิม</button>
              )}
              <LevelPill level={p.level} />
            </div>
          );
        })}
        {!filtered.length && <div className="empty">ไม่พบผู้เล่น</div>}
      </div>
      {lockedCount > 0 && (
        <div className="small muted mt-1">
          <Icon name="shield" size={11} /> มี {lockedCount} คนกำลังอยู่ในก๊วนอื่น — จบก๊วนเดิมก่อนจึงจะย้ายมาได้
        </div>
      )}
    </Modal>
  );
}

/* -------- Manual queue modal -------- */
// Host hand-picks who plays whom. Only bench players are eligible. The host
// taps a player to drop them into the currently-targeted team; the target
// auto-advances to the other team when one side fills.
function ManualQueueModal({ mode, players, activeIds, queuedIds, plays, systemConfig, onClose, onConfirm }) {
  const need = mode === "double" ? 2 : 1;
  const [teamA, setTeamA] = useState_grp([]);
  const [teamB, setTeamB] = useState_grp([]);
  const [target, setTarget] = useState_grp("A");

  const _active = activeIds || new Set();
  const _queued = queuedIds || new Set();
  const statusOf = (id) => _active.has(id) ? "active" : (_queued.has(id) ? "queued" : "free");

  const picked = new Set([...teamA, ...teamB]);
  // FREE pick: everyone in the group is selectable. Sort free/resting players first
  // (the natural picks), then those already playing or queued, each by fewest plays.
  const rank = { free: 0, queued: 1, active: 2 };
  const candidates = players
    .slice()
    .filter(p => !picked.has(p.id))
    .sort((a, b) => (rank[statusOf(a.id)] - rank[statusOf(b.id)]) || ((plays[a.id] || 0) - (plays[b.id] || 0)));

  const canConfirm = teamA.length === need && teamB.length === need;

  function pick(id) {
    // If already placed, remove from whichever team holds them
    if (teamA.includes(id)) { setTeamA(teamA.filter(x => x !== id)); return; }
    if (teamB.includes(id)) { setTeamB(teamB.filter(x => x !== id)); return; }
    // Add to the target team if it has room; otherwise to the other side
    if (target === "A" && teamA.length < need) {
      const next = [...teamA, id];
      setTeamA(next);
      if (next.length === need && teamB.length < need) setTarget("B");
    } else if (target === "B" && teamB.length < need) {
      const next = [...teamB, id];
      setTeamB(next);
      if (next.length === need && teamA.length < need) setTarget("A");
    } else if (teamA.length < need) {
      const next = [...teamA, id]; setTeamA(next);
      if (next.length === need && teamB.length < need) setTarget("B");
    } else if (teamB.length < need) {
      const next = [...teamB, id]; setTeamB(next);
    }
  }

  function removeFrom(team, id) {
    if (team === "A") { setTeamA(teamA.filter(x => x !== id)); setTarget("A"); }
    else { setTeamB(teamB.filter(x => x !== id)); setTarget("B"); }
  }

  const playerById = Object.fromEntries(players.map(p => [p.id, p]));

  return (
    <Modal open size="large" onClose={onClose}
      title="สร้างคิวเอง — เลือกคู่แข่ง"
      footer={<>
        <button className="btn" onClick={onClose}>ยกเลิก</button>
        <button className="btn primary" disabled={!canConfirm} onClick={() => onConfirm(teamA, teamB)}>
          <Icon name="plus" size={13} stroke={2.2} /> เพิ่มเข้าคิว
        </button>
      </>}>
      <div className="row gap-2 small muted mb-2" style={{alignItems:"flex-start"}}>
        <InfoTip text="เลือกใครก็ได้ในก๊วน — รวมถึงคนที่กำลังเล่นอยู่หรืออยู่ในคิวแล้ว (จะวางลงสนามได้เมื่อผู้เล่นเล่นจบและว่าง). เหมาะกับเคสที่ผู้เล่นขอเจอกับคู่ที่ต้องการ">
          <span>เลือกเองอิสระ</span>
        </InfoTip>
        <span>แตะผู้เล่นเพื่อใส่ลงฝั่งที่กำลังเลือก — แตะซ้ำเพื่อเอาออก</span>
      </div>

      {/* Team panels */}
      <div className="manual-teams">
        <ManualTeamPanel side="A" label={mode === "double" ? "ทีม A" : "ฝั่ง 1"}
          ids={teamA} need={need} active={target === "A"}
          playerById={playerById} systemConfig={systemConfig}
          onFocus={() => setTarget("A")} onRemove={(id) => removeFrom("A", id)} />
        <div className="manual-vs">VS</div>
        <ManualTeamPanel side="B" label={mode === "double" ? "ทีม B" : "ฝั่ง 2"}
          ids={teamB} need={need} active={target === "B"}
          playerById={playerById} systemConfig={systemConfig}
          onFocus={() => setTarget("B")} onRemove={(id) => removeFrom("B", id)} />
      </div>

      {!canConfirm && (
        <div className="manual-hint mt-2">
          <Icon name="user" size={13} />
          <span>
            ยังเลือกไม่ครบ — {mode === "double" ? "ต้องเลือกฝั่งละ 2 คน" : "ต้องเลือกฝั่งละ 1 คน"}
            {" "}(ทีม A ขาด {Math.max(0, need - teamA.length)} · ทีม B ขาด {Math.max(0, need - teamB.length)})
          </span>
        </div>
      )}

      <div className="mt-2 mb-1 row spread">
        <div style={{fontWeight: 600}}>ผู้เล่นทั้งหมด ({candidates.length})</div>
        <div className="small muted">คนพัก/ลงน้อยอยู่บนสุด</div>
      </div>
      <div className="swap-grid">
        {candidates.length ? candidates.map(p => {
          const st = statusOf(p.id);
          return (
            <button key={p.id} className="swap-card" onClick={() => pick(p.id)}>
              <div className="avatar" style={{width:30, height:30, fontSize:12, background:`linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>
                {initials(p.name)}
              </div>
              <div className="grow" style={{textAlign:"left", minWidth: 0}}>
                <div style={{fontWeight:500, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>
                  {p.name}
                  {st === "active" && <span className="chip" style={{marginLeft:6, fontSize:9, padding:"1px 5px", background:"var(--green-soft)", color:"var(--green)"}}>กำลังเล่น</span>}
                  {st === "queued" && <span className="chip" style={{marginLeft:6, fontSize:9, padding:"1px 5px", background:"var(--bg-inset)", color:"var(--text-3)"}}>ในคิว</span>}
                </div>
                <div className="small muted">ลง {plays[p.id] || 0} ครั้ง</div>
              </div>
              <LevelPill level={p.level} cfg={systemConfig} />
            </button>
          );
        }) : <div className="muted small" style={{padding: 12}}>เลือกครบทุกคนแล้ว</div>}
      </div>
    </Modal>
  );
}

function ManualTeamPanel({ side, label, ids, need, active, playerById, systemConfig, onFocus, onRemove }) {
  const slots = [];
  for (let i = 0; i < need; i++) slots.push(ids[i] || null);
  return (
    <div className={`manual-team ${active ? "active" : ""}`} onClick={onFocus}>
      <div className="manual-team-label">
        {label}
        {active && <span className="chip blue" style={{fontSize:9, padding:"1px 6px"}}>กำลังเลือก</span>}
      </div>
      {slots.map((id, i) => {
        const p = id ? playerById[id] : null;
        return (
          <div key={i} className={`manual-slot ${p ? "filled" : ""}`}
            onClick={p ? (e) => { e.stopPropagation(); onRemove(id); } : undefined}
            title={p ? "แตะเพื่อเอาออก" : "ว่าง"}>
            {p ? (
              <>
                <div className="avatar" style={{width:26, height:26, fontSize:11, background:`linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>
                  {initials(p.name)}
                </div>
                <span className="grow" style={{textAlign:"left", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{p.name.split(" ")[0]}</span>
                <LevelPill level={p.level} cfg={systemConfig} />
                <Icon name="trash" size={12} />
              </>
            ) : (
              <span className="muted small">ช่องว่าง</span>
            )}
          </div>
        );
      })}
    </div>
  );
}

function fmtElapsed(s) {
  const m = Math.floor(s / 60), ss = s % 60;
  return `${m}:${String(ss).padStart(2,"0")}`;
}

function hashColor(seed) {
  let h = 0;
  for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) & 0xffffff;
  const hue = h % 360;
  return `hsl(${hue}, 65%, 60%)`;
}

/* -------- Plays Edit Modal -------- */
function PlaysEditModal({ group, players, systemConfig, onClose, onSave, onReset }) {
  const [draft, setDraft] = useState_grp(() => {
    const base = {};
    group.playerIds.forEach(id => { base[id] = group.plays?.[id] ?? 0; });
    return base;
  });

  const sorted = players.slice().sort((a, b) => (draft[a.id] ?? 0) - (draft[b.id] ?? 0));

  return (
    <Modal open title="แก้ไขรอบการลงเล่น" onClose={onClose}>
      <div className="small muted mb-3">
        แก้ไขจำนวนรอบที่แต่ละคนลงเล่นในก๊วนนี้ — ใช้เพื่อปรับความสมดุลก่อนสุ่มรอบถัดไป
      </div>
      <div className="col gap-1" style={{maxHeight:360, overflowY:"auto"}}>
        {sorted.map(p => (
          <div key={p.id} className="card-row" style={{gap:12, padding:"8px 4px"}}>
            <div className="avatar" style={{width:28, height:28, fontSize:11, flexShrink:0,
              background:`linear-gradient(135deg, ${hashColor(p.id)}, ${hashColor(p.id+"x")})`}}>
              {initials(p.name)}
            </div>
            <div className="grow" style={{minWidth:0}}>
              <div style={{fontWeight:500, fontSize:13}}>{p.name.split(" ")[0]}</div>
              <div className="small muted"><LevelPill level={p.level} cfg={systemConfig} /></div>
            </div>
            <div className="row gap-1" style={{alignItems:"center", flexShrink:0}}>
              <button className="btn icon" style={{width:26,height:26}}
                onClick={() => setDraft(d => ({ ...d, [p.id]: Math.max(0, (d[p.id]??0) - 1) }))}>
                <Icon name="minus" size={12} stroke={2.5} />
              </button>
              <input
                type="number" min={0} max={999}
                value={draft[p.id] ?? 0}
                onChange={e => setDraft(d => ({ ...d, [p.id]: Math.max(0, parseInt(e.target.value) || 0) }))}
                style={{width:52, textAlign:"center", fontWeight:600}}
              />
              <button className="btn icon" style={{width:26,height:26}}
                onClick={() => setDraft(d => ({ ...d, [p.id]: (d[p.id]??0) + 1 }))}>
                <Icon name="plus" size={12} stroke={2.5} />
              </button>
            </div>
          </div>
        ))}
      </div>
      <div className="row gap-2 mt-3" style={{justifyContent:"space-between"}}>
        <button className="btn danger sm" onClick={onReset}>
          <Icon name="refresh" size={12} /> รีเซ็ตทั้งหมดเป็น 0
        </button>
        <div className="row gap-2">
          <button className="btn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn primary" onClick={() => onSave(draft)}>
            <Icon name="check" size={13} stroke={2.4} /> บันทึก
          </button>
        </div>
      </div>
    </Modal>
  );
}

Object.assign(window, { GroupSession });
