package plugin

import (
	"fmt"
	"log/slog"
	"math/rand/v2"
	"strings"
	"sync"
	"time"

	"gogobee/internal/db"

	"maunium.net/go/mautrix/id"
)

// ---------------------------------------------------------------------------
// Multiplayer UNO types
// ---------------------------------------------------------------------------

type unoMultiPhase int

const (
	unoMultiPhasePlay             unoMultiPhase = iota
	unoMultiPhaseChooseColor                    // active player must pick a color
	unoMultiPhaseDrawnPlayable                  // active player drew a playable card, yes/no
	unoMultiPhaseChallenge                      // next player may challenge a Wild Draw Four
	unoMultiPhaseChooseSwapTarget               // No Mercy: player played a 7, must choose swap target
)

type unoMultiPlayer struct {
	userID      id.UserID
	displayName string // set at creation; bots use their assigned name
	dmRoomID    id.RoomID
	hand        []unoCard
	calledUno   bool
	isBot       bool
	active      bool // false if forfeited/left
	autoPlays   int  // consecutive auto-plays
}

type unoMultiGame struct {
	id          string
	roomID      id.RoomID // games room
	ante        float64
	players     []*unoMultiPlayer // in turn order (includes bot)
	currentIdx  int
	direction   int // +1 or -1
	drawPile    []unoCard
	discardTop  unoCard
	topColor    unoColor
	phase       unoMultiPhase
	drawnCard   *unoCard // card drawn this turn
	pendingCard *unoCard // wild waiting for color

	// Wild Draw Four challenge state
	wd4Player    *unoMultiPlayer // who played the WD4
	wd4Victim    *unoMultiPlayer // who can challenge
	wd4PrevColor unoColor        // color before the wild was played
	turns        int
	turnID       int // monotonic, used to invalidate stale timers
	startedAt    time.Time
	done         bool
	bookDown     bool

	// No Mercy mode
	noMercy       bool
	sevenZeroRule bool
	stackTotal    int // cumulative draw penalty during stacking
	stackMinValue int // minimum draw value to stack (0 = not stacking)

	// Antes paid by addbot bots (funded from community pot)
	botAntes int

	// Sudden death (long-game point scoring)
	suddenDeath     bool
	suddenDeathTurn int

	timer         *time.Timer
	inactiveTimer *time.Timer // 10-minute game timeout
	mu            sync.Mutex  // per-game lock
}

type unoMultiLobby struct {
	roomID        id.RoomID
	creator       id.UserID
	ante          float64
	players       []id.UserID
	botNames      []string // bots added via !uno addbot
	createdAt     time.Time
	timer         *time.Timer
	noMercy       bool
	sevenZeroRule bool
}

// unoExtraBotNames are the names available for !uno addbot (in order).
// The default bot name comes from unoBotName() and is always present.
var unoExtraBotNames = []string{"WinBee", "GwinBee"}

// lobbySlots returns the number of lobby slots taken (humans + addbot bots).
func (l *unoMultiLobby) lobbySlots() int {
	return len(l.players) + len(l.botNames)
}

// buildLobbyDisplay renders the lobby message. Caller should hold p.mu or
// have a stable snapshot of the lobby fields.
func (p *UnoPlugin) buildLobbyDisplay(lobby *unoMultiLobby) string {
	var sb strings.Builder
	modeTag := ""
	if lobby.noMercy {
		modeTag = " 🔥 NO MERCY"
		if lobby.sevenZeroRule {
			modeTag += " (7-0)"
		}
	}
	slots := lobby.lobbySlots()
	sb.WriteString(fmt.Sprintf("🃏 **UNO Lobby**%s — Ante: €%d\nPlayers (%d/4):\n", modeTag, int(lobby.ante), slots))
	idx := 1
	for _, uid := range lobby.players {
		name := p.DisplayName(uid)
		label := ""
		if uid == lobby.creator {
			label = " (host)"
		}
		sb.WriteString(fmt.Sprintf("  %d. %s%s\n", idx, name, label))
		idx++
	}
	for _, bn := range lobby.botNames {
		sb.WriteString(fmt.Sprintf("  %d. 🤖 %s\n", idx, bn))
		idx++
	}
	sb.WriteString("\nType `!uno join` to join, `!uno addbot` for a bot, or `!uno go` to start!")
	return sb.String()
}

// ---------------------------------------------------------------------------
// Game helpers
// ---------------------------------------------------------------------------

// multiName returns the display name for a multiplayer UNO player.
// Bots use their assigned displayName; humans use the Matrix display name.
func (p *UnoPlugin) multiName(pl *unoMultiPlayer) string {
	if pl.isBot {
		return pl.displayName
	}
	return p.DisplayName(pl.userID)
}

func (g *unoMultiGame) currentPlayer() *unoMultiPlayer {
	return g.players[g.currentIdx]
}

func (g *unoMultiGame) nextActiveIdx() int {
	idx := g.currentIdx
	n := len(g.players)
	for i := 0; i < n; i++ {
		idx = (idx + g.direction + n) % n
		if g.players[idx].active {
			return idx
		}
	}
	return g.currentIdx // only one left
}

func (g *unoMultiGame) activePlayers() []*unoMultiPlayer {
	var active []*unoMultiPlayer
	for _, p := range g.players {
		if p.active {
			active = append(active, p)
		}
	}
	return active
}

func (g *unoMultiGame) activeHumanCount() int {
	count := 0
	for _, p := range g.players {
		if p.active && !p.isBot {
			count++
		}
	}
	return count
}

func (g *unoMultiGame) minOpponentCards(excludeIdx int) int {
	min := 999
	for i, p := range g.players {
		if i != excludeIdx && p.active && len(p.hand) < min {
			min = len(p.hand)
		}
	}
	return min
}

func (g *unoMultiGame) playerByUserID(userID id.UserID) *unoMultiPlayer {
	for _, p := range g.players {
		if p.userID == userID {
			return p
		}
	}
	return nil
}

func (g *unoMultiGame) draw(n int) []unoCard {
	var drawn []unoCard
	for i := 0; i < n; i++ {
		if len(g.drawPile) == 0 {
			g.reshuffleDiscard()
		}
		if len(g.drawPile) == 0 {
			break
		}
		drawn = append(drawn, g.drawPile[0])
		g.drawPile = g.drawPile[1:]
	}
	return drawn
}

func (g *unoMultiGame) reshuffleDiscard() {
	var fresh []unoCard
	if g.noMercy {
		fresh = newNoMercyDeck()
	} else {
		fresh = newUnoDeck()
	}
	inPlay := make(map[unoCard]int)
	for _, p := range g.players {
		for _, c := range p.hand {
			inPlay[c]++
		}
	}
	inPlay[g.discardTop]++

	var pile []unoCard
	for _, c := range fresh {
		if inPlay[c] > 0 {
			inPlay[c]--
			continue
		}
		pile = append(pile, c)
	}
	rand.Shuffle(len(pile), func(i, j int) { pile[i], pile[j] = pile[j], pile[i] })
	g.drawPile = pile
}

func (g *unoMultiGame) hasPlayable(hand []unoCard) bool {
	for _, c := range hand {
		if c.canPlayOn(g.discardTop, g.topColor) {
			return true
		}
	}
	return false
}

func (g *unoMultiGame) updateBookState() bool {
	// Bot pays attention when any opponent has <= 3 cards
	shouldBeDown := false
	for _, p := range g.players {
		if !p.isBot && p.active && len(p.hand) <= 3 {
			shouldBeDown = true
			break
		}
	}
	if shouldBeDown != g.bookDown {
		g.bookDown = shouldBeDown
		return true
	}
	return false
}

// ---------------------------------------------------------------------------
// Lobby commands
// ---------------------------------------------------------------------------

func (p *UnoPlugin) handleMultiStart(ctx MessageContext, amountStr string, noMercy, sevenZeroRule bool) error {
	if !isGamesRoom(ctx.RoomID) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Multiplayer Uno can only be started in the games channel!")
	}

	amountStr = strings.TrimPrefix(amountStr, "€")
	var amount float64
	fmt.Sscanf(amountStr, "%f", &amount)

	minBet := envFloat("UNO_MULTI_MIN_BET", envFloat("UNO_MIN_BET", 10))
	maxBet := envFloat("UNO_MULTI_MAX_BET", 500)
	if amount < minBet {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			fmt.Sprintf("Minimum ante is €%d. Usage: `!uno start €amount`", int(minBet)))
	}
	if amount > maxBet {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			fmt.Sprintf("Maximum ante is €%d.", int(maxBet)))
	}

	p.mu.Lock()
	if _, exists := p.lobbies[ctx.RoomID]; exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "A lobby is already open! Use `!uno join` to join or `!uno cancel` to cancel it.")
	}

	// Check player isn't already in a game
	if _, active := p.games[ctx.Sender]; active {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "You already have a solo Uno game in progress!")
	}
	for _, mg := range p.multiGames {
		if !mg.done {
			for _, pl := range mg.players {
				if pl.userID == ctx.Sender && pl.active && !pl.isBot {
					p.mu.Unlock()
					return p.SendReply(ctx.RoomID, ctx.EventID, "You're already in a multiplayer Uno game!")
				}
			}
		}
	}

	// Debit ante while still holding the lock
	if !p.euro.Debit(ctx.Sender, amount, "uno_multi_ante") {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Insufficient balance for that ante.")
	}

	timeout := envInt("UNO_MULTI_LOBBY_TIMEOUT", 300)
	lobby := &unoMultiLobby{
		roomID:        ctx.RoomID,
		creator:       ctx.Sender,
		ante:          amount,
		players:       []id.UserID{ctx.Sender},
		createdAt:     time.Now(),
		noMercy:       noMercy,
		sevenZeroRule: sevenZeroRule,
	}

	lobby.timer = time.AfterFunc(time.Duration(timeout)*time.Second, func() {
		p.lobbyExpired(ctx.RoomID)
	})

	p.lobbies[ctx.RoomID] = lobby
	msg := p.buildLobbyDisplay(lobby)
	p.mu.Unlock()

	return p.SendMessage(ctx.RoomID, msg)
}

func (p *UnoPlugin) handleMultiJoin(ctx MessageContext) error {
	if !isGamesRoom(ctx.RoomID) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Use this command in the games channel!")
	}

	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open. Start one with `!uno start €amount`.")
	}

	// Check if already in lobby
	for _, uid := range lobby.players {
		if uid == ctx.Sender {
			p.mu.Unlock()
			return p.SendReply(ctx.RoomID, ctx.EventID, "You're already in the lobby!")
		}
	}

	if lobby.lobbySlots() >= 4 {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Lobby is full! (%d/4)", lobby.lobbySlots()))
	}

	// Check player isn't in another game
	if _, active := p.games[ctx.Sender]; active {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "You already have a solo Uno game in progress!")
	}
	for _, mg := range p.multiGames {
		if !mg.done {
			for _, pl := range mg.players {
				if pl.userID == ctx.Sender && pl.active && !pl.isBot {
					p.mu.Unlock()
					return p.SendReply(ctx.RoomID, ctx.EventID, "You're already in a multiplayer Uno game!")
				}
			}
		}
	}

	// Debit ante while still holding the lock
	if !p.euro.Debit(ctx.Sender, lobby.ante, "uno_multi_ante") {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Insufficient balance for the ante.")
	}

	lobby.players = append(lobby.players, ctx.Sender)
	msg := p.buildLobbyDisplay(lobby)
	p.mu.Unlock()

	return p.SendMessage(ctx.RoomID, msg)
}

func (p *UnoPlugin) handleMultiLeave(ctx MessageContext) error {
	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open.")
	}

	found := false
	for i, uid := range lobby.players {
		if uid == ctx.Sender {
			lobby.players = append(lobby.players[:i], lobby.players[i+1:]...)
			found = true
			break
		}
	}
	if !found {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "You're not in the lobby.")
	}

	// If creator leaves, cancel the lobby
	if ctx.Sender == lobby.creator || len(lobby.players) == 0 {
		lobby.timer.Stop()
		delete(p.lobbies, ctx.RoomID)
		// Refund remaining players
		for _, uid := range lobby.players {
			p.euro.Credit(uid, lobby.ante, "uno_multi_refund")
		}
		p.mu.Unlock()
		p.euro.Credit(ctx.Sender, lobby.ante, "uno_multi_refund")
		return p.SendMessage(ctx.RoomID, "🃏 Lobby cancelled — all antes refunded.")
	}
	p.mu.Unlock()

	// Refund the leaving player
	p.euro.Credit(ctx.Sender, lobby.ante, "uno_multi_refund")
	name := p.DisplayName(ctx.Sender)
	return p.SendMessage(ctx.RoomID, fmt.Sprintf("🃏 **%s** left the lobby. (%d/4)", name, lobby.lobbySlots()))
}

func (p *UnoPlugin) handleMultiCancel(ctx MessageContext) error {
	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open.")
	}

	if ctx.Sender != lobby.creator && !p.IsAdmin(ctx.Sender) {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Only the host can cancel the lobby.")
	}

	lobby.timer.Stop()
	players := lobby.players
	ante := lobby.ante
	delete(p.lobbies, ctx.RoomID)
	p.mu.Unlock()

	for _, uid := range players {
		p.euro.Credit(uid, ante, "uno_multi_refund")
	}

	return p.SendMessage(ctx.RoomID, "🃏 Lobby cancelled — all antes refunded.")
}

func (p *UnoPlugin) handleMultiAddBot(ctx MessageContext) error {
	if !isGamesRoom(ctx.RoomID) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Use this command in the games channel!")
	}

	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open. Start one with `!uno start €amount`.")
	}

	if lobby.lobbySlots() >= 4 {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Lobby is full! (%d/4)", lobby.lobbySlots()))
	}

	botIdx := len(lobby.botNames)
	if botIdx >= len(unoExtraBotNames) {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID,
			fmt.Sprintf("Maximum %d extra bots already added.", len(unoExtraBotNames)))
	}

	lobby.botNames = append(lobby.botNames, unoExtraBotNames[botIdx])
	msg := p.buildLobbyDisplay(lobby)
	p.mu.Unlock()

	return p.SendMessage(ctx.RoomID, msg)
}

func (p *UnoPlugin) handleMultiRemoveBot(ctx MessageContext) error {
	if !isGamesRoom(ctx.RoomID) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Use this command in the games channel!")
	}

	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open.")
	}

	if len(lobby.botNames) == 0 {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No bots to remove.")
	}

	removed := lobby.botNames[len(lobby.botNames)-1]
	lobby.botNames = lobby.botNames[:len(lobby.botNames)-1]
	msg := p.buildLobbyDisplay(lobby)
	p.mu.Unlock()

	p.SendMessage(ctx.RoomID, fmt.Sprintf("🤖 %s removed from the lobby.", removed))
	return p.SendMessage(ctx.RoomID, msg)
}

func (p *UnoPlugin) lobbyExpired(roomID id.RoomID) {
	p.mu.Lock()
	lobby, exists := p.lobbies[roomID]
	if !exists {
		p.mu.Unlock()
		return
	}
	players := lobby.players
	ante := lobby.ante
	delete(p.lobbies, roomID)
	p.mu.Unlock()

	for _, uid := range players {
		p.euro.Credit(uid, ante, "uno_multi_refund")
	}

	p.SendMessage(roomID, "🃏 Lobby expired — all antes refunded.")
}

func (p *UnoPlugin) handleMultiGo(ctx MessageContext) error {
	p.mu.Lock()
	lobby, exists := p.lobbies[ctx.RoomID]
	if !exists {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "No lobby open.")
	}

	if ctx.Sender != lobby.creator {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Only the host can start the game.")
	}

	// Need at least 2 total participants (humans + lobby bots + default bot)
	totalParticipants := len(lobby.players) + len(lobby.botNames) + 1 // +1 for default bot
	if totalParticipants < 2 {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Need at least 2 players to start.")
	}

	lobby.timer.Stop()
	players := lobby.players
	lobbyBots := lobby.botNames
	ante := lobby.ante
	roomID := lobby.roomID
	noMercy := lobby.noMercy
	sevenZeroRule := lobby.sevenZeroRule
	delete(p.lobbies, ctx.RoomID)
	p.mu.Unlock()

	// Resolve DM rooms for all players
	var resolved []playerDMPair
	for _, uid := range players {
		dmRoom, err := p.GetDMRoom(uid)
		if err != nil {
			slog.Error("uno_multi: failed to get DM room", "user", uid, "err", err)
			// Refund all and abort
			for _, u := range players {
				p.euro.Credit(u, ante, "uno_multi_refund")
			}
			return p.SendMessage(roomID, fmt.Sprintf("🃏 Game cancelled — couldn't open DMs with %s.", p.DisplayName(uid)))
		}
		resolved = append(resolved, playerDMPair{uid, dmRoom})
	}

	// Debit bot antes from community pot
	for range lobbyBots {
		p.claimFromPot(ante)
	}

	// Build game
	game := p.initMultiGame(resolved, lobbyBots, roomID, ante, noMercy, sevenZeroRule)
	game.botAntes = len(lobbyBots)

	p.mu.Lock()
	p.multiGames[game.id] = game
	for _, pl := range game.players {
		if !pl.isBot {
			p.dmToMulti[pl.dmRoomID] = game.id
		}
	}
	p.mu.Unlock()

	// Announce
	var sb strings.Builder
	modeTag := ""
	if noMercy {
		modeTag = " 🔥 NO MERCY"
		if sevenZeroRule {
			modeTag += " (7-0)"
		}
	}
	totalPot := int(ante) * (len(players) + len(lobbyBots))
	sb.WriteString(fmt.Sprintf("🃏 **Multiplayer UNO!**%s Pot: €%d\n\nPlayers:\n", modeTag, totalPot))
	for i, pl := range game.players {
		name := p.multiName(pl)
		marker := ""
		if i == game.currentIdx {
			marker = " ← first turn"
		}
		sb.WriteString(fmt.Sprintf("  %d. %s%s\n", i+1, name, marker))
	}
	startComment := pickCommentary("start")
	if noMercy {
		startComment = pickNoMercyCommentary("nomercy_start")
	}
	sb.WriteString(fmt.Sprintf("\nStarting card: %s\n%s\n\n[Check your DMs!]",
		game.discardTop.DisplayWithColor(game.topColor), startComment))
	p.SendMessage(roomID, sb.String())

	// Start first turn
	game.mu.Lock()
	defer game.mu.Unlock()
	p.startInactivityTimer(game)
	p.executeMultiTurn(game)

	return nil
}

// ---------------------------------------------------------------------------
// Game initialization
// ---------------------------------------------------------------------------

type playerDMPair struct {
	userID   id.UserID
	dmRoomID id.RoomID
}

func (p *UnoPlugin) initMultiGame(players []playerDMPair, lobbyBots []string, roomID id.RoomID, ante float64, noMercy, sevenZeroRule bool) *unoMultiGame {
	var deck []unoCard
	if noMercy {
		deck = newNoMercyDeck()
	} else {
		deck = newUnoDeck()
	}
	cardsPerPlayer := 7
	cardIdx := 0

	// Build players list and deal cards
	var unshuffled []*unoMultiPlayer
	for _, pd := range players {
		hand := make([]unoCard, cardsPerPlayer)
		copy(hand, deck[cardIdx:cardIdx+cardsPerPlayer])
		cardIdx += cardsPerPlayer
		unshuffled = append(unshuffled, &unoMultiPlayer{
			userID:      pd.userID,
			displayName: p.DisplayName(pd.userID),
			dmRoomID:    pd.dmRoomID,
			hand:        hand,
			active:      true,
		})
	}

	// Default bot (always present)
	addBot := func(name string) {
		botHand := make([]unoCard, cardsPerPlayer)
		copy(botHand, deck[cardIdx:cardIdx+cardsPerPlayer])
		cardIdx += cardsPerPlayer
		unshuffled = append(unshuffled, &unoMultiPlayer{
			userID:      id.UserID(name),
			displayName: name,
			hand:        botHand,
			isBot:       true,
			active:      true,
		})
	}
	addBot(unoBotName())

	// Additional bots from lobby
	for _, bn := range lobbyBots {
		addBot(bn)
	}

	// Shuffle turn order
	rand.Shuffle(len(unshuffled), func(i, j int) { unshuffled[i], unshuffled[j] = unshuffled[j], unshuffled[i] })

	// Remaining deck
	remaining := make([]unoCard, len(deck)-cardIdx)
	copy(remaining, deck[cardIdx:])

	// Starting card — must be a number card
	var startCard unoCard
	startIdx := -1
	for i, c := range remaining {
		if !c.Value.isAction() && !c.isWild() {
			startCard = c
			startIdx = i
			break
		}
	}
	if startIdx >= 0 {
		remaining = append(remaining[:startIdx], remaining[startIdx+1:]...)
	} else {
		startCard = remaining[0]
		remaining = remaining[1:]
	}

	gameID := fmt.Sprintf("multi_%d", time.Now().UnixNano())

	return &unoMultiGame{
		id:            gameID,
		roomID:        roomID,
		ante:          ante,
		players:       unshuffled,
		currentIdx:    0,
		direction:     1,
		drawPile:      remaining,
		discardTop:    startCard,
		topColor:      startCard.Color,
		phase:         unoMultiPhasePlay,
		turns:         0,
		turnID:        0,
		startedAt:     time.Now(),
		noMercy:       noMercy,
		sevenZeroRule: sevenZeroRule,
	}
}

// ---------------------------------------------------------------------------
// Turn execution engine
// ---------------------------------------------------------------------------

// executeMultiTurn runs bot turns synchronously and sets up the next human turn.
// Caller must hold game.mu.
func (p *UnoPlugin) executeMultiTurn(game *unoMultiGame) {
	var roomBuf strings.Builder
	botTurnsInRow := 0
	loopMsgsSent := 0 // track messages sent across loop iterations to add jitter

	for {
		if game.done {
			return
		}

		// Jitter between rapid-fire messages to avoid Matrix rate limits.
		// Drop the lock during sleep so DM handlers aren't blocked.
		if loopMsgsSent > 0 {
			savedTurn := game.turnID
			game.mu.Unlock()
			time.Sleep(time.Duration(300+rand.IntN(400)) * time.Millisecond)
			game.mu.Lock()
			// A DM handler may have advanced the game while we slept.
			if game.done || game.turnID != savedTurn {
				return
			}
		}

		player := game.currentPlayer()

		// Skip eliminated players (e.g. mercy-killed during their own turn)
		if !player.active {
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
			continue
		}

		if player.isBot {
			botTurnsInRow++
			if botTurnsInRow > 10 {
				// Safety: shouldn't happen, but prevent infinite loops
				break
			}
			p.botMultiTurn(game, &roomBuf)
			loopMsgsSent++
			if game.done {
				if roomBuf.Len() > 0 {
					p.SendMessage(game.roomID, roomBuf.String())
				}
				return
			}
			if p.checkMultiSuddenDeath(game) {
				if roomBuf.Len() > 0 {
					p.SendMessage(game.roomID, roomBuf.String())
				}
				return
			}
			continue
		}

		// Human's turn — flush bot play buffer first so the turn announcement arrives
		// before any auto-action happens.
		if roomBuf.Len() > 0 {
			p.SendMessage(game.roomID, roomBuf.String())
			roomBuf.Reset()
		}

		// No Mercy stacking: check if player must absorb
		if game.noMercy && game.stackMinValue > 0 {
			if !hasStackableCard(player.hand, game.topColor, game.stackMinValue) {
				name := p.DisplayName(player.userID)
				drawn := game.draw(game.stackTotal)
				player.hand = append(player.hand, drawn...)
				p.SendMessage(player.dmRoomID, fmt.Sprintf("💥 No stackable card! You draw %d cards.\n%s",
					game.stackTotal, pickNoMercyCommentary("stack_absorbed")))
				absorbMsg := fmt.Sprintf("💥 %s absorbs the stack! Draws %d cards. (%d cards now)",
					name, game.stackTotal, len(player.hand))
				game.stackTotal = 0
				game.stackMinValue = 0
				if p.checkMultiMercyElimination(game, player) {
					p.SendMessage(game.roomID, absorbMsg)
					loopMsgsSent++
					if game.done {
						return
					}
					game.currentIdx = game.nextActiveIdx()
					game.turnID++
					continue
				}
				// Skip this player's turn
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				p.SendMessage(game.roomID, absorbMsg+"\n"+p.nextTurnLabel(game))
				loopMsgsSent++
				continue
			}
			// Player has stackable cards — show stacking prompt
			game.phase = unoMultiPhasePlay
			p.sendMultiHandDisplayStacking(game, player)
			p.startMultiAutoPlayTimer(game)
			return
		}

		// Auto-draw if no playable cards
		if !game.hasPlayable(player.hand) {
			if game.noMercy {
				// Draw until playable
				var allDrawn []unoCard
				var playableCard *unoCard
				for {
					cards := game.draw(1)
					if len(cards) == 0 {
						break
					}
					card := cards[0]
					player.hand = append(player.hand, card)
					allDrawn = append(allDrawn, card)
					if p.checkMultiMercyElimination(game, player) {
						if game.done {
							return
						}
						game.currentIdx = game.nextActiveIdx()
						game.turnID++
						break
					}
					if card.canPlayOn(game.discardTop, game.topColor) {
						playableCard = &card
						break
					}
				}
				if !player.active {
					continue // mercy-killed
				}
				name := p.DisplayName(player.userID)
				if len(allDrawn) == 0 {
					p.SendMessage(player.dmRoomID, "No playable cards and deck is empty. Turn passes.")
					game.currentIdx = game.nextActiveIdx()
					game.turnID++
					p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s has no playable cards and deck is empty. Turn passes. (%d cards)\n%s", name, len(player.hand), p.nextTurnLabel(game)))
					loopMsgsSent += 2
					continue
				}
				if playableCard != nil {
					game.drawnCard = playableCard
					game.phase = unoMultiPhaseDrawnPlayable
					p.SendMessage(player.dmRoomID,
						fmt.Sprintf("No playable cards — drew %d card(s): %s\nLast card is playable! Play it? (**yes** / **no**)",
							len(allDrawn), formatDrawnCards(allDrawn)))
					p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws %d card(s). (%d cards)", name, len(allDrawn), len(player.hand)))
					p.startMultiAutoPlayTimer(game)
					return
				}
				p.SendMessage(player.dmRoomID,
					fmt.Sprintf("No playable cards — drew %d card(s). None playable. Turn passes.", len(allDrawn)))
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws %d card(s). Turn passes. (%d cards)\n%s", name, len(allDrawn), len(player.hand), p.nextTurnLabel(game)))
				loopMsgsSent += 2
				continue
			}

			// Classic: draw 1
			drawn := game.draw(1)
			if len(drawn) == 0 {
				name := p.DisplayName(player.userID)
				p.SendMessage(player.dmRoomID, "No playable cards and deck is empty. Turn passes.")
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s has no playable cards and deck is empty. Turn passes. (%d cards)\n%s", name, len(player.hand), p.nextTurnLabel(game)))
				loopMsgsSent += 2
				continue
			}

			card := drawn[0]
			player.hand = append(player.hand, card)

			if card.canPlayOn(game.discardTop, game.topColor) {
				game.drawnCard = &card
				game.phase = unoMultiPhaseDrawnPlayable
				p.SendMessage(player.dmRoomID,
					fmt.Sprintf("No playable cards — drew automatically: %s\nIt's playable! Play it? (**yes** / **no**)", card.Display()))
				p.startMultiAutoPlayTimer(game)
				return
			}

			name := p.DisplayName(player.userID)
			p.SendMessage(player.dmRoomID,
				fmt.Sprintf("No playable cards — drew automatically: %s\nNot playable. Turn passes.", card.Display()))
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
			p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws a card. Turn passes. (%d cards)\n%s", name, len(player.hand), p.nextTurnLabel(game)))
			loopMsgsSent += 2
			continue
		}

		// Show hand and wait for input
		game.phase = unoMultiPhasePlay
		p.sendMultiHandDisplay(game, player)
		p.startMultiAutoPlayTimer(game)
		return
	}
}

// advanceAndExecute moves to the next player and calls executeMultiTurn.
// Caller must hold game.mu.
func (p *UnoPlugin) advanceAndExecute(game *unoMultiGame) {
	game.currentIdx = game.nextActiveIdx()
	game.turnID++
	game.turns++
	if p.checkMultiSuddenDeath(game) {
		return
	}
	p.executeMultiTurn(game)
}

// nextTurnLabel returns "It's X's turn." for the current player.
func (p *UnoPlugin) nextTurnLabel(game *unoMultiGame) string {
	return fmt.Sprintf("It's %s's turn.", p.multiName(game.currentPlayer()))
}

// ---------------------------------------------------------------------------
// DM input handling
// ---------------------------------------------------------------------------

func (p *UnoPlugin) handleMultiDMInput(ctx MessageContext, game *unoMultiGame) error {
	game.mu.Lock()
	defer game.mu.Unlock()

	if game.done {
		return nil
	}

	// Status query — available to any player, any time
	input := strings.TrimSpace(strings.ToLower(ctx.Body))
	if strings.HasPrefix(input, "!") {
		input = strings.TrimPrefix(input, "!")
	}
	if input == "uno" || input == "" {
		// Could be status request or UNO call — only treat as status if not active player
		current := game.currentPlayer()
		if current.userID != ctx.Sender {
			caller := game.playerByUserID(ctx.Sender)
			if caller != nil && caller.active {
				p.sendMultiStatus(game, caller)
			}
			return nil
		}
	}

	player := game.currentPlayer()
	if player.userID != ctx.Sender || player.isBot {
		return nil // not this player's turn
	}

	// Reset auto-play counter and inactivity timer on manual input
	player.autoPlays = 0
	p.startInactivityTimer(game)

	// Cancel turn timer
	if game.timer != nil {
		game.timer.Stop()
	}

	if input == "quit" || input == "forfeit" {
		p.multiPlayerForfeit(game, player)
		return nil
	}

	switch game.phase {
	case unoMultiPhaseChallenge:
		return p.handleMultiChallengeInput(game, player, input)

	case unoMultiPhaseChooseColor:
		return p.handleMultiColorChoice(game, player, input)

	case unoMultiPhaseDrawnPlayable:
		return p.handleMultiDrawnPlayable(game, player, input)

	case unoMultiPhaseChooseSwapTarget:
		return p.handleMultiSwapChoice(game, player, input)

	case unoMultiPhasePlay:
		if input == "uno" {
			player.calledUno = true
			p.SendMessage(player.dmRoomID, "✅ UNO called!")
			return nil
		}
		// No Mercy stacking: accept
		if game.noMercy && game.stackMinValue > 0 && (input == "accept" || input == "a") {
			name := p.DisplayName(player.userID)
			drawn := game.draw(game.stackTotal)
			player.hand = append(player.hand, drawn...)
			p.SendMessage(player.dmRoomID, fmt.Sprintf("💥 You accept the stack and draw %d cards.\n%s",
				game.stackTotal, pickNoMercyCommentary("stack_absorbed")))
			absorbMsg := fmt.Sprintf("💥 %s absorbs the stack! Draws %d cards. (%d cards now)",
				name, game.stackTotal, len(player.hand))
			game.stackTotal = 0
			game.stackMinValue = 0
			if p.checkMultiMercyElimination(game, player) {
				p.SendMessage(game.roomID, absorbMsg)
				if game.done {
					return nil
				}
				p.advanceAndExecute(game)
				return nil
			}
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
			game.turns++
			p.SendMessage(game.roomID, absorbMsg+"\n"+p.nextTurnLabel(game))
			if p.checkMultiSuddenDeath(game) {
				return nil
			}
			p.executeMultiTurn(game)
			return nil
		}
		// "accept" alias handled above; "draw" during stacking = same as accept
		if input == "draw" {
			if game.noMercy && game.stackMinValue > 0 {
				p.SendMessage(player.dmRoomID, "You must play a draw card to stack, or type **accept** to draw the stack.")
				return nil
			}
			return p.handleMultiPlayerDraw(game, player)
		}
		var cardIdx int
		if _, err := fmt.Sscanf(input, "%d", &cardIdx); err != nil || cardIdx < 1 || cardIdx > len(player.hand) {
			if game.noMercy && game.stackMinValue > 0 {
				p.SendMessage(player.dmRoomID,
					fmt.Sprintf("Reply with a card number (1-%d) to stack, or **accept** to draw %d cards.", len(player.hand), game.stackTotal))
			} else {
				p.SendMessage(player.dmRoomID,
					fmt.Sprintf("Reply with a number (1-%d) to play, or **draw** to draw.", len(player.hand)))
			}
			return nil
		}
		return p.handleMultiPlayerPlay(game, player, cardIdx-1)
	}

	return nil
}

// ---------------------------------------------------------------------------
// Play handlers
// ---------------------------------------------------------------------------

func (p *UnoPlugin) handleMultiChallengeInput(game *unoMultiGame, player *unoMultiPlayer, input string) error {
	switch input {
	case "challenge", "c":
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", name))
		p.resolveWD4Challenge(game, true)
	case "accept", "a":
		p.resolveWD4Challenge(game, false)
	default:
		p.SendMessage(player.dmRoomID, "Type **challenge** or **accept**.")
	}
	return nil
}

func (p *UnoPlugin) handleMultiPlayerPlay(game *unoMultiGame, player *unoMultiPlayer, idx int) error {
	card := player.hand[idx]

	// No Mercy stacking validation
	if game.noMercy && game.stackMinValue > 0 {
		if !card.canPlayOnStacking(game.topColor, game.stackMinValue) {
			p.SendMessage(player.dmRoomID,
				fmt.Sprintf("You can't stack %s — need a draw card (matching color or wild). Type **accept** to draw %d.",
					card.Display(), game.stackTotal))
			return nil
		}
	} else if !card.canPlayOn(game.discardTop, game.topColor) {
		p.SendMessage(player.dmRoomID, fmt.Sprintf("You can't play %s on %s.",
			card.Display(), game.discardTop.DisplayWithColor(game.topColor)))
		return nil
	}

	// UNO penalty check — had 2 cards, didn't call UNO (skip during stacking)
	if game.stackMinValue == 0 && len(player.hand) == 2 && !player.calledUno {
		drawn := game.draw(2)
		player.hand = append(player.hand, drawn...)
		player.calledUno = false
		p.SendMessage(player.dmRoomID, "⚠️ You forgot to call UNO! Draw 2 as penalty.")
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forgot to call UNO! 2 card penalty.", name))
		if game.noMercy {
			if p.checkMultiMercyElimination(game, player) {
				if game.done {
					return nil
				}
				p.advanceAndExecute(game)
				return nil
			}
		}
		p.sendMultiHandDisplay(game, player)
		p.startMultiAutoPlayTimer(game)
		return nil
	}

	// Play the card
	player.hand = append(player.hand[:idx], player.hand[idx+1:]...)
	game.discardTop = card
	game.turns++

	// UNO tracking
	if len(player.hand) == 2 {
		player.calledUno = false
	}

	// No Mercy: Discard All — remove remaining cards of same color
	if game.noMercy && card.Value == unoDiscardAll {
		discarded := discardAllOfColor(&player.hand, card.Color)
		if discarded > 0 {
			p.SendMessage(player.dmRoomID, fmt.Sprintf("Discarded %d additional %s cards!", discarded, card.Color))
		}
	}

	// Wild — need color choice
	if card.isWild() {
		game.pendingCard = &card
		if card.Value == unoWildDrawFour {
			game.wd4PrevColor = game.topColor
		}
		game.phase = unoMultiPhaseChooseColor
		p.SendMessage(player.dmRoomID,
			fmt.Sprintf("You played **%s**! Choose a color:\n1. 🟥 Red\n2. 🟦 Blue\n3. 🟨 Yellow\n4. 🟩 Green", card.Value))
		p.startMultiAutoPlayTimer(game)
		return nil
	}

	game.topColor = card.Color

	// No Mercy: 7-0 rule
	if game.noMercy && game.sevenZeroRule {
		if card.Value == unoSeven && len(game.activePlayers()) > 2 {
			// Choose swap target
			game.phase = unoMultiPhaseChooseSwapTarget
			var sb strings.Builder
			sb.WriteString("You played a **7**! Choose a player to swap hands with:\n")
			i := 1
			for _, pl := range game.players {
				if pl == player || !pl.active {
					continue
				}
				name := p.multiName(pl)
				sb.WriteString(fmt.Sprintf("%d. %s (%d cards)\n", i, name, len(pl.hand)))
				i++
			}
			p.SendMessage(player.dmRoomID, sb.String())
			p.startMultiAutoPlayTimer(game)
			return nil
		}
		if card.Value == unoSeven {
			// 2-player: swap with the other player
			for _, pl := range game.players {
				if pl != player && pl.active {
					swapHandsMulti(player, pl)
					name := p.DisplayName(player.userID)
					otherName := p.multiName(pl)
					p.SendMessage(game.roomID, fmt.Sprintf("🔄 %s swaps hands with %s! %s",
						name, otherName, pickNoMercyCommentary("hand_swap")))
					if !pl.isBot {
						p.SendMessage(pl.dmRoomID, fmt.Sprintf("🔄 %s swapped hands with you! You now have %d cards.", name, len(pl.hand)))
					}
					break
				}
			}
		}
		if card.Value == unoZero {
			rotateHandsMulti(game)
			p.SendMessage(game.roomID, fmt.Sprintf("🔄 Hands rotated! %s", pickNoMercyCommentary("hand_rotate")))
			// Notify all active human players about their new hand size
			for _, pl := range game.players {
				if pl.active && !pl.isBot {
					p.SendMessage(pl.dmRoomID, fmt.Sprintf("🔄 Hands rotated! You now have %d cards.", len(pl.hand)))
				}
			}
		}
	}

	// Check win (after discard all, after swap)
	if len(player.hand) == 0 {
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s plays: %s", name, card.Display()))
		p.multiPlayerWins(game, player)
		return nil
	}

	// Apply effects and announce
	p.applyAndAnnounce(game, player, card)
	return nil
}

func (p *UnoPlugin) handleMultiColorChoice(game *unoMultiGame, player *unoMultiPlayer, input string) error {
	var color unoColor
	switch input {
	case "1", "red":
		color = unoRed
	case "2", "blue":
		color = unoBlue
	case "3", "yellow":
		color = unoYellow
	case "4", "green":
		color = unoGreen
	default:
		p.SendMessage(player.dmRoomID, "Choose a color: **1** (Red), **2** (Blue), **3** (Yellow), **4** (Green)")
		return nil
	}

	game.topColor = color
	pendingCard := game.pendingCard
	game.pendingCard = nil
	game.phase = unoMultiPhasePlay

	p.SendMessage(player.dmRoomID, fmt.Sprintf("Color set to %s %s.", color.Emoji(), color))

	// Check win
	if len(player.hand) == 0 {
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s plays: %s (chose %s %s)",
			name, pendingCard.Display(), color.Emoji(), color))
		p.multiPlayerWins(game, player)
		return nil
	}

	// No Mercy: Color Roulette — next player flips until chosen color
	if game.noMercy && pendingCard != nil && pendingCard.Value == unoWildColorRoulette {
		name := p.DisplayName(player.userID)
		nextIdx := game.nextActiveIdx()
		target := game.players[nextIdx]
		targetName := p.multiName(target)

		flipped := p.executeColorRouletteMulti(game, target, color)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s plays: %s (chose %s %s)\n🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
			name, pendingCard.Display(), color.Emoji(), color,
			targetName, len(flipped), color, pickNoMercyCommentary("color_roulette")))
		if !target.isBot {
			p.SendMessage(target.dmRoomID, fmt.Sprintf("🎰 Color Roulette! You drew %d cards. You now have %d cards.", len(flipped), len(target.hand)))
		}

		if p.checkMultiMercyElimination(game, target) {
			if game.done {
				return nil
			}
		}

		// Skip the target player, advance to next
		game.currentIdx = nextIdx
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
		if p.checkMultiSuddenDeath(game) {
			return nil
		}
		p.executeMultiTurn(game)
		return nil
	}

	// Apply effects
	p.applyAndAnnounce(game, player, *pendingCard)
	return nil
}

func (p *UnoPlugin) handleMultiPlayerDraw(game *unoMultiGame, player *unoMultiPlayer) error {
	// No Mercy: draw until playable
	if game.noMercy {
		return p.handleMultiPlayerDrawNoMercy(game, player)
	}

	drawn := game.draw(1)
	if len(drawn) == 0 {
		p.SendMessage(player.dmRoomID, "No cards left to draw! Turn passes.")
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s can't draw — deck is empty. Turn passes.", name))
		p.advanceAndExecute(game)
		return nil
	}

	card := drawn[0]
	player.hand = append(player.hand, card)

	if card.canPlayOn(game.discardTop, game.topColor) {
		game.drawnCard = &card
		game.phase = unoMultiPhaseDrawnPlayable
		p.SendMessage(player.dmRoomID,
			fmt.Sprintf("You drew: %s\nIt's playable! Play it? (**yes** / **no**)", card.Display()))
		p.startMultiAutoPlayTimer(game)
		return nil
	}

	name := p.DisplayName(player.userID)
	p.SendMessage(player.dmRoomID, fmt.Sprintf("You drew: %s\nNot playable. Turn passes.", card.Display()))
	p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws a card. Turn passes. (%d cards)", name, len(player.hand)))
	p.advanceAndExecute(game)
	return nil
}

func (p *UnoPlugin) handleMultiPlayerDrawNoMercy(game *unoMultiGame, player *unoMultiPlayer) error {
	var allDrawn []unoCard
	var playableCard *unoCard

	for {
		drawn := game.draw(1)
		if len(drawn) == 0 {
			break
		}
		card := drawn[0]
		player.hand = append(player.hand, card)
		allDrawn = append(allDrawn, card)
		if p.checkMultiMercyElimination(game, player) {
			if game.done {
				return nil
			}
			p.advanceAndExecute(game)
			return nil
		}
		if card.canPlayOn(game.discardTop, game.topColor) {
			playableCard = &card
			break
		}
	}

	name := p.DisplayName(player.userID)

	if len(allDrawn) == 0 {
		p.SendMessage(player.dmRoomID, "No cards left to draw! Turn passes.")
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s can't draw — deck is empty. Turn passes.", name))
		p.advanceAndExecute(game)
		return nil
	}

	if playableCard != nil {
		game.drawnCard = playableCard
		game.phase = unoMultiPhaseDrawnPlayable
		p.SendMessage(player.dmRoomID,
			fmt.Sprintf("Drew %d card(s): %s\nLast card is playable! Play it? (**yes** / **no**)",
				len(allDrawn), formatDrawnCards(allDrawn)))
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws %d card(s). (%d cards)", name, len(allDrawn), len(player.hand)))
		p.startMultiAutoPlayTimer(game)
		return nil
	}

	p.SendMessage(player.dmRoomID,
		fmt.Sprintf("Drew %d card(s). None playable. Turn passes.", len(allDrawn)))
	p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws %d card(s). Turn passes. (%d cards)", name, len(allDrawn), len(player.hand)))
	p.advanceAndExecute(game)
	return nil
}

func (p *UnoPlugin) handleMultiDrawnPlayable(game *unoMultiGame, player *unoMultiPlayer, input string) error {
	if input != "yes" && input != "y" && input != "no" && input != "n" {
		p.SendMessage(player.dmRoomID, "Play the drawn card? (**yes** / **no**)")
		return nil
	}

	drawnCard := *game.drawnCard
	game.drawnCard = nil
	game.phase = unoMultiPhasePlay

	if input == "no" || input == "n" {
		name := p.DisplayName(player.userID)
		p.SendMessage(player.dmRoomID, "Card kept. Turn passes.")
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s draws a card. Turn passes. (%d cards)", name, len(player.hand)))
		p.advanceAndExecute(game)
		return nil
	}

	// Play the drawn card
	for i, c := range player.hand {
		if c == drawnCard {
			player.hand = append(player.hand[:i], player.hand[i+1:]...)
			break
		}
	}
	game.discardTop = drawnCard
	game.turns++

	if len(player.hand) == 2 {
		player.calledUno = false
	}

	if drawnCard.isWild() {
		game.pendingCard = &drawnCard
		if drawnCard.Value == unoWildDrawFour {
			game.wd4PrevColor = game.topColor
		}
		game.phase = unoMultiPhaseChooseColor
		p.SendMessage(player.dmRoomID,
			fmt.Sprintf("You played **%s**! Choose a color:\n1. 🟥 Red\n2. 🟦 Blue\n3. 🟨 Yellow\n4. 🟩 Green", drawnCard.Value))
		p.startMultiAutoPlayTimer(game)
		return nil
	}

	game.topColor = drawnCard.Color

	if len(player.hand) == 0 {
		name := p.DisplayName(player.userID)
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s plays: %s", name, drawnCard.Display()))
		p.multiPlayerWins(game, player)
		return nil
	}

	p.applyAndAnnounce(game, player, drawnCard)
	return nil
}

// ---------------------------------------------------------------------------
// Card effects & turn announcement
// ---------------------------------------------------------------------------

// cardEffectResult describes what happened when a card's effects were applied.
type cardEffectResult struct {
	skippedName    string // non-empty if a player was skipped
	reversed       bool   // true if direction was reversed
	drawnCount     int    // cards drawn by the victim (2 or 4)
	needsChallenge bool   // true if WD4 challenge phase should start
	stackPending   bool   // true if draw stacking is in progress (No Mercy)
}

// applyCardEffects applies skip/reverse/draw effects and advances the turn.
// Caller must hold game.mu.
func (p *UnoPlugin) applyCardEffects(game *unoMultiGame, card unoCard) cardEffectResult {
	var result cardEffectResult

	nextIdx := game.nextActiveIdx()
	nextPlayer := game.players[nextIdx]
	nextName := p.multiName(nextPlayer)

	switch card.Value {
	case unoSkip:
		result.skippedName = nextName
		game.currentIdx = nextIdx
		game.currentIdx = game.nextActiveIdx()
		game.turnID++

	case unoSkipEveryone:
		if game.noMercy {
			// Skip all others — current player goes again
			var skippedNames []string
			for _, pl := range game.players {
				if pl != game.currentPlayer() && pl.active {
					name := p.multiName(pl)
					skippedNames = append(skippedNames, name)
				}
			}
			result.skippedName = strings.Join(skippedNames, ", ")
			// Don't advance — current player keeps their turn
			game.turnID++
		} else {
			// Treat as skip in classic
			result.skippedName = nextName
			game.currentIdx = nextIdx
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		}

	case unoReverse:
		game.direction *= -1
		result.reversed = true
		if len(game.activePlayers()) == 2 {
			result.skippedName = nextName
			game.currentIdx = nextIdx
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		} else {
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		}

	case unoDrawTwo, unoDrawFour:
		if game.noMercy {
			// No Mercy: stacking
			dv := cardDrawValue(card.Value)
			game.stackTotal += dv
			game.stackMinValue = dv
			result.stackPending = true
			result.skippedName = nextName
			result.drawnCount = game.stackTotal
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		} else if card.Value == unoDrawTwo {
			// Classic Draw Two
			drawn := game.draw(2)
			nextPlayer.hand = append(nextPlayer.hand, drawn...)
			result.skippedName = nextName
			result.drawnCount = 2
			game.currentIdx = nextIdx
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		} else {
			// DrawFour shouldn't appear in classic, but handle gracefully
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		}

	case unoWildDrawFour:
		if game.noMercy {
			// Should not appear in No Mercy deck, but handle gracefully
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		} else {
			result.needsChallenge = true
			result.skippedName = nextName
		}

	case unoWildReverseDraw4:
		if game.noMercy {
			game.direction *= -1
			result.reversed = true
			dv := cardDrawValue(card.Value)
			game.stackTotal += dv
			game.stackMinValue = dv
			result.stackPending = true
			// After reverse, get next player in new direction
			nextIdx = game.nextActiveIdx()
			nextPlayer = game.players[nextIdx]
			nextName = p.multiName(nextPlayer)
			result.skippedName = nextName
			result.drawnCount = game.stackTotal
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		}

	case unoWildDrawSix, unoWildDrawTen:
		if game.noMercy {
			dv := cardDrawValue(card.Value)
			game.stackTotal += dv
			game.stackMinValue = dv
			result.stackPending = true
			result.skippedName = nextName
			result.drawnCount = game.stackTotal
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
		}

	case unoWildColorRoulette:
		// Color roulette effect is handled in color choice handler, not here
		game.currentIdx = game.nextActiveIdx()
		game.turnID++

	case unoDiscardAll:
		// Discard effect already applied before calling this function
		game.currentIdx = game.nextActiveIdx()
		game.turnID++

	default:
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
	}

	return result
}

// writeEffectLines appends human-readable effect descriptions to a string builder.
func writeEffectLines(sb *strings.Builder, eff cardEffectResult) {
	if eff.needsChallenge {
		sb.WriteString(fmt.Sprintf("\n  %s may challenge! ⚡", eff.skippedName))
	} else if eff.stackPending {
		sb.WriteString(fmt.Sprintf("\n  🔥 Stack incoming! (+%d) — %s must stack or absorb!", eff.drawnCount, eff.skippedName))
		if eff.reversed {
			sb.WriteString("\n  Direction reversed!")
		}
	} else if eff.drawnCount > 0 {
		sb.WriteString(fmt.Sprintf("\n  %s draws %d and is skipped!", eff.skippedName, eff.drawnCount))
	} else if eff.skippedName != "" && eff.reversed {
		sb.WriteString(fmt.Sprintf("\n  %s is skipped! (reverse)", eff.skippedName))
	} else if eff.skippedName != "" {
		sb.WriteString(fmt.Sprintf("\n  %s is skipped!", eff.skippedName))
	} else if eff.reversed {
		sb.WriteString("\n  Direction reversed!")
	}
}

func (p *UnoPlugin) applyAndAnnounce(game *unoMultiGame, player *unoMultiPlayer, card unoCard) {
	name := p.DisplayName(player.userID)
	var roomMsg strings.Builder

	roomMsg.WriteString(fmt.Sprintf("🃏 %s plays: %s", name, card.DisplayWithColor(game.topColor)))

	eff := p.applyCardEffects(game, card)
	writeEffectLines(&roomMsg, eff)

	// UNO announcement
	if len(player.hand) == 1 && player.calledUno {
		roomMsg.WriteString(fmt.Sprintf("\n  %s calls UNO! 🔥", name))
	}

	// Wild Draw Four — enter challenge phase
	if eff.needsChallenge {
		p.SendMessage(game.roomID, roomMsg.String())
		p.startWD4Challenge(game, player)
		return
	}

	// Book state commentary (occasionally)
	if game.turns%4 == 0 {
		if changed := game.updateBookState(); changed {
			if game.bookDown {
				roomMsg.WriteString("\n\n" + pickCommentary("book_down"))
			} else {
				roomMsg.WriteString("\n\n" + pickCommentary("book_up"))
			}
		}
	}

	// Next player
	nextUpName := p.multiName(game.currentPlayer())
	roomMsg.WriteString(fmt.Sprintf("\n  It's %s's turn.", nextUpName))

	p.SendMessage(game.roomID, roomMsg.String())
	if p.checkMultiSuddenDeath(game) {
		return
	}
	p.executeMultiTurn(game)
}

// ---------------------------------------------------------------------------
// Wild Draw Four challenge
// ---------------------------------------------------------------------------

// startWD4Challenge enters the challenge phase. The victim can challenge or accept.
// Caller must hold game.mu.
func (p *UnoPlugin) startWD4Challenge(game *unoMultiGame, wd4Player *unoMultiPlayer) {
	nextIdx := game.nextActiveIdx()
	victim := game.players[nextIdx]

	game.wd4Player = wd4Player
	game.wd4Victim = victim
	game.phase = unoMultiPhaseChallenge
	game.currentIdx = nextIdx
	game.turnID++

	playerName := p.multiName(wd4Player)

	if victim.isBot {
		// Bot decides whether to challenge
		p.botHandleWD4Challenge(game)
		return
	}

	p.SendMessage(victim.dmRoomID,
		fmt.Sprintf("⚡ **%s** played Wild Draw Four!\nYou can **challenge** — if they had a %s %s card, they draw 4 instead.\nIf the challenge fails, you draw 6.\n\nType **challenge** or **accept**.",
			playerName, game.wd4PrevColor.Emoji(), game.wd4PrevColor))
	p.startMultiAutoPlayTimer(game)
}

// resolveWD4Challenge resolves the challenge. Caller must hold game.mu.
func (p *UnoPlugin) resolveWD4Challenge(game *unoMultiGame, challenged bool) {
	wd4Player := game.wd4Player
	victim := game.wd4Victim
	wd4Name := p.multiName(wd4Player)
	victimName := p.multiName(victim)

	// Clear challenge state
	game.wd4Player = nil
	game.wd4Victim = nil
	game.phase = unoMultiPhasePlay

	if !challenged {
		// Victim accepts — draw 4, get skipped
		drawn := game.draw(4)
		victim.hand = append(victim.hand, drawn...)
		p.SendMessage(game.roomID,
			fmt.Sprintf("🃏 %s accepts the Wild Draw Four. Draws 4 and is skipped!", victimName))
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
		if p.checkMultiSuddenDeath(game) {
			return
		}
		p.executeMultiTurn(game)
		return
	}

	// Check if the WD4 player had a card matching the previous color
	hadMatch := false
	for _, c := range wd4Player.hand {
		if c.Color == game.wd4PrevColor {
			hadMatch = true
			break
		}
	}

	if hadMatch {
		// Challenge succeeds — WD4 player drew illegally, they draw 4
		drawn := game.draw(4)
		wd4Player.hand = append(wd4Player.hand, drawn...)
		p.SendMessage(game.roomID,
			fmt.Sprintf("⚡ **Challenge successful!** %s had a %s %s card. %s draws 4!",
				wd4Name, game.wd4PrevColor.Emoji(), game.wd4PrevColor, wd4Name))
		// Victim is NOT skipped — turn continues from victim
		game.turnID++
		if p.checkMultiSuddenDeath(game) {
			return
		}
		p.executeMultiTurn(game)
	} else {
		// Challenge fails — victim draws 6 (4 + 2 penalty)
		drawn := game.draw(6)
		victim.hand = append(victim.hand, drawn...)
		p.SendMessage(game.roomID,
			fmt.Sprintf("⚡ **Challenge failed!** %s played legally. %s draws 6!",
				wd4Name, victimName))
		// Victim is skipped
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
		if p.checkMultiSuddenDeath(game) {
			return
		}
		p.executeMultiTurn(game)
	}
}

// botHandleWD4Challenge decides whether the bot challenges a WD4. Caller must hold game.mu.
func (p *UnoPlugin) botHandleWD4Challenge(game *unoMultiGame) {
	// More cards = more likely they had a matching color = more likely to challenge.
	// 1-3 cards: 10%, 4-5: 30%, 6-7: 50%, 8+: 70%
	cards := len(game.wd4Player.hand)
	threshold := 10
	if cards >= 8 {
		threshold = 70
	} else if cards >= 6 {
		threshold = 50
	} else if cards >= 4 {
		threshold = 30
	}
	challenged := rand.IntN(100) < threshold
	bn := game.currentPlayer().displayName

	if challenged {
		p.SendMessage(game.roomID, fmt.Sprintf("⚡ %s challenges the Wild Draw Four!", bn))
	} else {
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s accepts the Wild Draw Four.", bn))
	}

	p.resolveWD4Challenge(game, challenged)
}

// ---------------------------------------------------------------------------
// Bot turn (multiplayer)
// ---------------------------------------------------------------------------

func (p *UnoPlugin) botMultiTurn(game *unoMultiGame, roomBuf *strings.Builder) {
	bot := game.currentPlayer()
	bn := bot.displayName

	// No Mercy stacking: bot must stack or absorb
	if game.noMercy && game.stackMinValue > 0 {
		stackCard, sIdx := botPickStackCard(bot.hand, game.topColor, game.stackMinValue)
		if sIdx < 0 {
			// Bot absorbs the stack
			drawn := game.draw(game.stackTotal)
			bot.hand = append(bot.hand, drawn...)
			if roomBuf.Len() > 0 {
				roomBuf.WriteString("\n")
			}
			roomBuf.WriteString(fmt.Sprintf("💥 %s absorbs the stack! Draws %d cards. (%d cards now)",
				bn, game.stackTotal, len(bot.hand)))
			game.stackTotal = 0
			game.stackMinValue = 0
			if p.checkMultiMercyElimination(game, bot) {
				if roomBuf.Len() > 0 {
					p.SendMessage(game.roomID, roomBuf.String())
					roomBuf.Reset()
				}
				return
			}
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
			return
		}
		// Bot stacks
		bot.hand = append(bot.hand[:sIdx], bot.hand[sIdx+1:]...)
		game.discardTop = stackCard
		dv := cardDrawValue(stackCard.Value)
		game.stackTotal += dv
		game.stackMinValue = dv
		if stackCard.isWild() {
			game.topColor = botPickColor(bot.hand)
		} else {
			game.topColor = stackCard.Color
		}
		if roomBuf.Len() > 0 {
			roomBuf.WriteString("\n")
		}
		roomBuf.WriteString(fmt.Sprintf("🔥 %s stacks: %s! Total penalty: +%d",
			bn, stackCard.DisplayWithColor(game.topColor), game.stackTotal))
		if stackCard.Value == unoWildReverseDraw4 {
			game.direction *= -1
			roomBuf.WriteString(" (direction reversed!)")
		}
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
		game.turns++
		return
	}

	var card unoCard
	var idx int
	if game.noMercy {
		card, idx = botPickCardNoMercy(bot.hand, game.discardTop, game.topColor, game.bookDown, game.minOpponentCards(game.currentIdx), 0)
	} else {
		card, idx = botPickCard(bot.hand, game.discardTop, game.topColor, game.bookDown, game.minOpponentCards(game.currentIdx))
	}

	if idx < 0 {
		// Bot draws
		if game.noMercy {
			// Draw until playable
			var allDrawn []unoCard
			var playableCard *unoCard
			for {
				cards := game.draw(1)
				if len(cards) == 0 {
					break
				}
				c := cards[0]
				bot.hand = append(bot.hand, c)
				allDrawn = append(allDrawn, c)
				if p.checkMultiMercyElimination(game, bot) {
					if roomBuf.Len() > 0 {
						p.SendMessage(game.roomID, roomBuf.String())
						roomBuf.Reset()
					}
					return
				}
				if c.canPlayOn(game.discardTop, game.topColor) {
					playableCard = &c
					break
				}
			}
			if len(allDrawn) == 0 {
				if roomBuf.Len() > 0 {
					roomBuf.WriteString("\n")
				}
				roomBuf.WriteString(fmt.Sprintf("🃏 %s can't draw — deck empty. Turn passes.", bn))
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				return
			}
			if playableCard != nil {
				// Remove from hand and play
				for i := len(bot.hand) - 1; i >= 0; i-- {
					if bot.hand[i] == *playableCard {
						bot.hand = append(bot.hand[:i], bot.hand[i+1:]...)
						break
					}
				}
				if roomBuf.Len() > 0 {
					roomBuf.WriteString("\n")
				}
				roomBuf.WriteString(fmt.Sprintf("🃏 %s draws %d card(s).", bn, len(allDrawn)))
				card = *playableCard
				// Fall through to play the card below
			} else {
				if roomBuf.Len() > 0 {
					roomBuf.WriteString("\n")
				}
				roomBuf.WriteString(fmt.Sprintf("🃏 %s draws %d card(s). No playable card.", bn, len(allDrawn)))
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				return
			}
		} else {
			drawn := game.draw(1)
			if len(drawn) == 0 {
				if roomBuf.Len() > 0 {
					roomBuf.WriteString("\n")
				}
				roomBuf.WriteString(fmt.Sprintf("🃏 %s can't draw — deck empty. Turn passes.", bn))
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				return
			}

			bot.hand = append(bot.hand, drawn[0])

			if drawn[0].canPlayOn(game.discardTop, game.topColor) {
				bot.hand = bot.hand[:len(bot.hand)-1]
				card = drawn[0]
			} else {
				if game.turns%3 == 0 {
					commentKey := "bot_draw_normal"
					if game.bookDown {
						commentKey = "bot_draw_bookdown"
					}
					if roomBuf.Len() > 0 {
						roomBuf.WriteString("\n")
					}
					roomBuf.WriteString(pickCommentary(commentKey))
				} else {
					if roomBuf.Len() > 0 {
						roomBuf.WriteString("\n")
					}
					roomBuf.WriteString(fmt.Sprintf("🃏 %s draws a card.", bn))
				}
				game.currentIdx = game.nextActiveIdx()
				game.turnID++
				return
			}
		}
	}

	// Play the card
	if idx >= 0 {
		bot.hand = append(bot.hand[:idx], bot.hand[idx+1:]...)
	}
	game.discardTop = card

	// No Mercy: Discard All — bot discards remaining cards of same color
	if game.noMercy && card.Value == unoDiscardAll {
		discardAllOfColor(&bot.hand, card.Color)
	}

	// Wild color choice
	if card.isWild() {
		if card.Value == unoWildDrawFour {
			game.wd4PrevColor = game.topColor
		}
		if game.noMercy && card.Value == unoWildColorRoulette {
			game.topColor = botRouletteColor(bot.hand)
		} else {
			game.topColor = botPickColor(bot.hand)
		}
	} else {
		game.topColor = card.Color
	}

	// No Mercy: 7-0 rule
	if game.noMercy && game.sevenZeroRule {
		if card.Value == unoSeven {
			target := botChooseSwapTarget(game, bot)
			if target != nil {
				swapHandsMulti(bot, target)
				if !target.isBot {
					p.SendMessage(target.dmRoomID, fmt.Sprintf("🔄 %s swapped hands with you! You now have %d cards.", bn, len(target.hand)))
				}
			}
		}
		if card.Value == unoZero {
			rotateHandsMulti(game)
			for _, pl := range game.players {
				if pl.active && !pl.isBot {
					p.SendMessage(pl.dmRoomID, fmt.Sprintf("🔄 Hands rotated! You now have %d cards.", len(pl.hand)))
				}
			}
		}
	}

	// Check bot win
	if len(bot.hand) == 0 {
		if roomBuf.Len() > 0 {
			roomBuf.WriteString("\n")
		}
		roomBuf.WriteString(fmt.Sprintf("🃏 %s plays: %s", bn, card.DisplayWithColor(game.topColor)))
		if roomBuf.Len() > 0 {
			p.SendMessage(game.roomID, roomBuf.String())
			roomBuf.Reset()
		}
		p.multiBotWins(game, bot)
		return
	}

	// Bot UNO call
	if len(bot.hand) == 1 {
		if roomBuf.Len() > 0 {
			roomBuf.WriteString("\n")
		}
		roomBuf.WriteString(fmt.Sprintf("🃏 %s plays: %s\n  %s calls UNO! 🔥", bn, card.DisplayWithColor(game.topColor), bn))
	} else {
		if roomBuf.Len() > 0 {
			roomBuf.WriteString("\n")
		}
		// Add commentary occasionally
		if game.turns%3 == 0 {
			commentKey := "bot_play_normal"
			if game.bookDown {
				commentKey = "bot_play_bookdown"
			}
			roomBuf.WriteString(fmt.Sprintf("🃏 %s", fmt.Sprintf(pickCommentary(commentKey), card.DisplayWithColor(game.topColor))))
		} else {
			roomBuf.WriteString(fmt.Sprintf("🃏 %s plays: %s", bn, card.DisplayWithColor(game.topColor)))
		}
	}

	// Apply action effects
	eff := p.applyCardEffects(game, card)
	writeEffectLines(roomBuf, eff)

	// WD4 challenge — flush buffer and enter challenge phase
	if eff.needsChallenge {
		if roomBuf.Len() > 0 {
			p.SendMessage(game.roomID, roomBuf.String())
			roomBuf.Reset()
		}
		game.turns++
		p.startWD4Challenge(game, bot)
		return
	}

	// No Mercy: Color Roulette — next player flips cards
	// applyCardEffects already advanced currentIdx to the victim
	if game.noMercy && card.Value == unoWildColorRoulette {
		target := game.players[game.currentIdx]
		targetName := p.multiName(target)
		flipped := p.executeColorRouletteMulti(game, target, game.topColor)
		roomBuf.WriteString(fmt.Sprintf("\n  🎰 Color Roulette! %s flips %d cards until finding %s %s.",
			targetName, len(flipped), game.topColor.Emoji(), game.topColor))
		if !target.isBot {
			p.SendMessage(target.dmRoomID, fmt.Sprintf("🎰 Color Roulette! You drew %d cards. You now have %d cards.", len(flipped), len(target.hand)))
		}

		if p.checkMultiMercyElimination(game, target) {
			if roomBuf.Len() > 0 {
				p.SendMessage(game.roomID, roomBuf.String())
				roomBuf.Reset()
			}
			if game.done {
				return
			}
		}

		// Skip past the roulette victim
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
	}

	// No Mercy: 7-0 room announcements (swap already happened above)
	if game.noMercy && game.sevenZeroRule {
		if card.Value == unoSeven {
			roomBuf.WriteString("\n  🔄 Hands swapped!")
		}
		if card.Value == unoZero {
			roomBuf.WriteString("\n  🔄 Hands rotated!")
		}
	}

	// Next turn label (applyCardEffects already advanced currentIdx)
	nextUpName := p.multiName(game.currentPlayer())
	roomBuf.WriteString(fmt.Sprintf("\n  It's %s's turn.", nextUpName))

	game.turns++
}

// ---------------------------------------------------------------------------
// Auto-play timer
// ---------------------------------------------------------------------------

func (p *UnoPlugin) startMultiAutoPlayTimer(game *unoMultiGame) {
	if game.timer != nil {
		game.timer.Stop()
	}

	timeout := envInt("UNO_MULTI_TURN_TIMEOUT", 30)
	savedTurnID := game.turnID
	gameID := game.id

	game.timer = time.AfterFunc(time.Duration(timeout)*time.Second, func() {
		p.mu.Lock()
		mg, exists := p.multiGames[gameID]
		p.mu.Unlock()
		if !exists {
			return
		}

		mg.mu.Lock()
		defer mg.mu.Unlock()

		if mg.done || mg.turnID != savedTurnID {
			return // turn already changed
		}

		player := mg.currentPlayer()
		if player.isBot {
			return
		}

		player.autoPlays++
		maxAutoPlays := envInt("UNO_MULTI_MAX_AUTOPLAY", 3)

		name := p.DisplayName(player.userID)

		if player.autoPlays >= maxAutoPlays {
			p.SendMessage(mg.roomID, fmt.Sprintf("🃏 %s was auto-played %d times in a row — forfeited!", name, maxAutoPlays))
			p.multiPlayerForfeit(mg, player)
			return
		}

		// Auto-play: find first playable non-action card
		p.autoPlayMultiTurn(mg, player)
	})
}

// startInactivityTimer starts (or resets) the 10-minute game timeout.
// If no human input occurs within the window, the game ends and remaining players are refunded.
// Caller must hold game.mu.
func (p *UnoPlugin) startInactivityTimer(game *unoMultiGame) {
	if game.inactiveTimer != nil {
		game.inactiveTimer.Stop()
	}

	timeout := envInt("UNO_MULTI_INACTIVITY_TIMEOUT", 600)
	gameID := game.id

	game.inactiveTimer = time.AfterFunc(time.Duration(timeout)*time.Second, func() {
		p.mu.Lock()
		mg, exists := p.multiGames[gameID]
		p.mu.Unlock()
		if !exists {
			return
		}

		mg.mu.Lock()
		defer mg.mu.Unlock()

		if mg.done {
			return
		}

		mg.done = true
		if mg.timer != nil {
			mg.timer.Stop()
		}

		// Refund remaining human players
		for _, pl := range mg.players {
			if !pl.isBot && pl.active {
				p.euro.Credit(pl.userID, mg.ante, "uno_multi_timeout_refund")
			}
		}

		p.SendMessage(mg.roomID, "🃏 **Game timed out** — no human input for 10 minutes. All antes refunded.")
		p.recordMultiGame(mg, id.UserID(""), "timeout")
		p.cleanupMultiGame(mg)
	})
}

func (p *UnoPlugin) autoPlayMultiTurn(game *unoMultiGame, player *unoMultiPlayer) {
	name := p.DisplayName(player.userID)

	switch game.phase {
	case unoMultiPhaseChallenge:
		// Auto-play: accept the WD4 (safe default)
		p.SendMessage(player.dmRoomID, "*Auto-played:* Accepted Wild Draw Four.")
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Accepts the Wild Draw Four.", name))
		p.resolveWD4Challenge(game, false)
		return

	case unoMultiPhaseChooseColor:
		// Auto-pick most common color
		color := botPickColor(player.hand)
		game.topColor = color
		pendingCard := game.pendingCard
		game.pendingCard = nil
		game.phase = unoMultiPhasePlay

		p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Color set to %s %s.", color.Emoji(), color))

		if len(player.hand) == 0 {
			p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
				name, pendingCard.Display(), color.Emoji(), color))
			p.multiPlayerWins(game, player)
			return
		}

		p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
			name, pendingCard.Display(), color.Emoji(), color))

		// No Mercy: Color Roulette — apply roulette effect before generic effects
		if game.noMercy && pendingCard != nil && pendingCard.Value == unoWildColorRoulette {
			nextIdx := game.nextActiveIdx()
			target := game.players[nextIdx]
			targetName := p.multiName(target)
			flipped := p.executeColorRouletteMulti(game, target, color)
			p.SendMessage(game.roomID, fmt.Sprintf("🎰 Color Roulette! %s flips %d cards until finding %s.\n%s",
				targetName, len(flipped), color, pickNoMercyCommentary("color_roulette")))
			if !target.isBot {
				p.SendMessage(target.dmRoomID, fmt.Sprintf("🎰 Color Roulette! You drew %d cards. You now have %d cards.", len(flipped), len(target.hand)))
			}
			if p.checkMultiMercyElimination(game, target) {
				if game.done {
					return
				}
			}
			// Skip past the roulette victim
			game.currentIdx = nextIdx
			game.currentIdx = game.nextActiveIdx()
			game.turnID++
			if p.checkMultiSuddenDeath(game) {
				return
			}
			p.executeMultiTurn(game)
			return
		}

		p.applyAutoEffects(game, player, *pendingCard)
		return

	case unoMultiPhaseDrawnPlayable:
		// Auto-play: play the drawn card
		drawnCard := *game.drawnCard
		game.drawnCard = nil
		game.phase = unoMultiPhasePlay

		for i, c := range player.hand {
			if c == drawnCard {
				player.hand = append(player.hand[:i], player.hand[i+1:]...)
				break
			}
		}
		game.discardTop = drawnCard
		game.turns++

		if drawnCard.isWild() {
			if drawnCard.Value == unoWildDrawFour {
				game.wd4PrevColor = game.topColor
			}
			color := botPickColor(player.hand)
			game.topColor = color
			p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s (chose %s %s).", drawnCard.Display(), color.Emoji(), color))

			if len(player.hand) == 0 {
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
					name, drawnCard.Display(), color.Emoji(), color))
				p.multiPlayerWins(game, player)
				return
			}

			p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
				name, drawnCard.Display(), color.Emoji(), color))
			p.applyAutoEffects(game, player, drawnCard)
			return
		}

		game.topColor = drawnCard.Color
		p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s.", drawnCard.Display()))

		if len(player.hand) == 0 {
			p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s", name, drawnCard.Display()))
			p.multiPlayerWins(game, player)
			return
		}

		p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s", name, drawnCard.Display()))
		p.applyAutoEffects(game, player, drawnCard)
		return

	case unoMultiPhaseChooseSwapTarget:
		// Auto-play: swap with player who has fewest cards
		target := botChooseSwapTarget(game, player)
		if target != nil {
			swapHandsMulti(player, target)
			targetName := p.multiName(target)
			p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Swapped hands with %s.", targetName))
			p.SendMessage(game.roomID, fmt.Sprintf("🔄 *%s was auto-played.* Swaps hands with %s!", name, targetName))
		}
		game.phase = unoMultiPhasePlay
		if len(player.hand) == 0 {
			p.multiPlayerWins(game, player)
			return
		}
		p.advanceAndExecute(game)
		return

	case unoMultiPhasePlay:
		// No Mercy stacking: auto-accept
		if game.noMercy && game.stackMinValue > 0 {
			drawn := game.draw(game.stackTotal)
			player.hand = append(player.hand, drawn...)
			p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Accepted stack. Drew %d cards.", game.stackTotal))
			p.SendMessage(game.roomID, fmt.Sprintf("💥 *%s was auto-played.* Absorbs the stack! Draws %d cards. (%d cards now)",
				name, game.stackTotal, len(player.hand)))
			game.stackTotal = 0
			game.stackMinValue = 0
			if p.checkMultiMercyElimination(game, player) {
				if game.done {
					return
				}
			}
			p.advanceAndExecute(game)
			return
		}

		// Find first playable non-action card
		playIdx := -1
		for i, c := range player.hand {
			if c.canPlayOn(game.discardTop, game.topColor) && !c.Value.isAction() && c.Value != unoWildDrawFour {
				playIdx = i
				break
			}
		}
		// If only action cards, play first playable
		if playIdx < 0 {
			for i, c := range player.hand {
				if c.canPlayOn(game.discardTop, game.topColor) {
					playIdx = i
					break
				}
			}
		}

		if playIdx >= 0 {
			card := player.hand[playIdx]
			player.hand = append(player.hand[:playIdx], player.hand[playIdx+1:]...)
			game.discardTop = card
			game.turns++

			if card.isWild() {
				if card.Value == unoWildDrawFour {
					game.wd4PrevColor = game.topColor
				}
				color := botPickColor(player.hand)
				game.topColor = color
				p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s (chose %s %s).", card.Display(), color.Emoji(), color))

				if len(player.hand) == 0 {
					p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
						name, card.Display(), color.Emoji(), color))
					p.multiPlayerWins(game, player)
					return
				}

				p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s (chose %s %s)",
					name, card.Display(), color.Emoji(), color))
				p.applyAutoEffects(game, player, card)
				return
			}

			game.topColor = card.Color
			p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* %s.", card.Display()))

			if len(player.hand) == 0 {
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s", name, card.Display()))
				p.multiPlayerWins(game, player)
				return
			}

			p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Plays: %s", name, card.Display()))
			p.applyAutoEffects(game, player, card)
			return
		}

		// No playable card — draw
		drawn := game.draw(1)
		if len(drawn) == 0 {
			p.SendMessage(player.dmRoomID, "*Auto-played:* No cards to draw. Turn passes.")
			p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* No cards to draw.", name))
			p.advanceAndExecute(game)
			return
		}

		card := drawn[0]
		player.hand = append(player.hand, card)

		if card.canPlayOn(game.discardTop, game.topColor) {
			// Auto-play the drawn card
			for i, c := range player.hand {
				if c == card {
					player.hand = append(player.hand[:i], player.hand[i+1:]...)
					break
				}
			}
			game.discardTop = card
			game.turns++

			if card.isWild() {
				if card.Value == unoWildDrawFour {
					game.wd4PrevColor = game.topColor
				}
				color := botPickColor(player.hand)
				game.topColor = color
				p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Drew and played %s (chose %s %s).", card.Display(), color.Emoji(), color))
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Drew and played %s (chose %s %s)",
					name, card.Display(), color.Emoji(), color))
			} else {
				game.topColor = card.Color
				p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Drew and played %s.", card.Display()))
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Drew and played %s", name, card.Display()))
			}

			if len(player.hand) == 0 {
				p.multiPlayerWins(game, player)
				return
			}

			p.applyAutoEffects(game, player, card)
			return
		}

		p.SendMessage(player.dmRoomID, fmt.Sprintf("*Auto-played:* Drew %s. Not playable. Turn passes.", card.Display()))
		p.SendMessage(game.roomID, fmt.Sprintf("🃏 *%s was auto-played.* Draws a card. Turn passes. (%d cards)", name, len(player.hand)))
		p.advanceAndExecute(game)
	}
}

// applyAutoEffects applies card effects after an auto-play and advances the turn.
func (p *UnoPlugin) applyAutoEffects(game *unoMultiGame, player *unoMultiPlayer, card unoCard) {
	eff := p.applyCardEffects(game, card)
	if eff.needsChallenge {
		p.startWD4Challenge(game, player)
		return
	}
	if p.checkMultiSuddenDeath(game) {
		return
	}
	p.executeMultiTurn(game)
}

// ---------------------------------------------------------------------------
// Sudden Death (multiplayer — only when 2 active players remain)
// ---------------------------------------------------------------------------

// checkMultiSuddenDeath checks whether sudden death should be announced or resolved.
// Only applies when exactly 2 active players remain. Returns true if the game ended.
// Caller must hold game.mu.
func (p *UnoPlugin) checkMultiSuddenDeath(game *unoMultiGame) bool {
	if game.done {
		return true
	}

	active := game.activePlayers()
	if len(active) != 2 {
		return false
	}

	// Resolve
	if game.suddenDeath && game.turns >= game.suddenDeathTurn {
		p.multiSuddenDeathWinner(game, active)
		return true
	}

	// Announce
	if !game.suddenDeath && game.turns >= suddenDeathAnnounce {
		game.suddenDeath = true
		game.suddenDeathTurn = game.turns + suddenDeathCountdown
		remaining := game.suddenDeathTurn - game.turns

		p.SendMessage(game.roomID, fmt.Sprintf(
			"⏰ **SUDDEN DEATH!** Match ends in %d turns — lowest hand value wins!\n\n%s",
			remaining, pickCommentary("sudden_death")))

		for _, pl := range active {
			if !pl.isBot {
				p.SendMessage(pl.dmRoomID, fmt.Sprintf(
					"⏰ **Sudden Death!** %d turns remaining — dump your high-value cards!", remaining))
			}
		}
		return false
	}

	// Countdown reminders
	if game.suddenDeath {
		remaining := game.suddenDeathTurn - game.turns
		switch remaining {
		case 10:
			p.SendMessage(game.roomID, "⏰ **10 turns remaining!**")
		case 5:
			p.SendMessage(game.roomID, "⏰ **5 turns remaining!**")
		}
	}

	return false
}

func (p *UnoPlugin) multiSuddenDeathWinner(game *unoMultiGame, active []*unoMultiPlayer) {
	if game.done {
		return
	}
	game.done = true

	if game.timer != nil {
		game.timer.Stop()
	}
	if game.inactiveTimer != nil {
		game.inactiveTimer.Stop()
	}

	a, b := active[0], active[1]
	aScore := scoreHand(a.hand)
	bScore := scoreHand(b.hand)

	breakdown := fmt.Sprintf("**Final Scores:**\n%s: %s\n%s: %s",
		p.multiName(a), formatHandScore(a.hand),
		p.multiName(b), formatHandScore(b.hand))

	var winner, loser *unoMultiPlayer
	switch {
	case aScore < bScore:
		winner, loser = a, b
	case bScore < aScore:
		winner, loser = b, a
	case len(a.hand) < len(b.hand):
		winner, loser = a, b
		breakdown += "\n*Tiebreaker: fewer cards!*"
	case len(b.hand) < len(a.hand):
		winner, loser = b, a
		breakdown += "\n*Tiebreaker: fewer cards!*"
	default:
		// Tiebreaker: player whose turn it is NOT wins
		current := game.currentPlayer()
		if current == a {
			winner, loser = b, a
		} else {
			winner, loser = a, b
		}
		breakdown += "\n*Tiebreaker: dead even — advantage to the opponent!*"
	}
	_ = loser // used implicitly via winner != loser

	p.SendMessage(game.roomID, fmt.Sprintf(
		"⏰ **SUDDEN DEATH!** %s wins on points!\n%s\n\n%s",
		p.multiName(winner), breakdown, pickCommentary("sudden_death_win")))

	if winner.isBot {
		p.multiBotWins2(game)
	} else {
		p.multiPlayerWins2(game, winner)
	}
}

// multiPlayerWins2 / multiBotWins2 handle payout without duplicate done/timer logic
// (already handled by multiSuddenDeathWinner).
func (p *UnoPlugin) multiPlayerWins2(game *unoMultiGame, winner *unoMultiPlayer) {
	humanCount := 0
	for _, pl := range game.players {
		if !pl.isBot {
			humanCount++
		}
	}
	totalPot := game.ante * float64(humanCount+game.botAntes)
	net, _ := communityTax(winner.userID, totalPot, 0.05)
	p.euro.Credit(winner.userID, net, "uno_multi_win")

	p.recordMultiGame(game, winner.userID, "sudden_death_win")
	p.cleanupMultiGame(game)
}

func (p *UnoPlugin) multiBotWins2(game *unoMultiGame) {
	humanCount := 0
	for _, pl := range game.players {
		if !pl.isBot {
			humanCount++
		}
	}
	totalPot := game.ante * float64(humanCount+game.botAntes)
	p.addToPot(totalPot)

	for _, pl := range game.players {
		if !pl.isBot {
			recordBotDefeat(pl.userID, "uno_multi")
		}
	}
	p.recordMultiGame(game, id.UserID("bot"), "sudden_death_bot")
	p.cleanupMultiGame(game)
}

// ---------------------------------------------------------------------------
// Win / Forfeit / Cleanup
// ---------------------------------------------------------------------------

func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer) {
	if game.done {
		return
	}
	game.done = true

	if game.timer != nil {
		game.timer.Stop()
	}
	if game.inactiveTimer != nil {
		game.inactiveTimer.Stop()
	}

	// Calculate pot (all human antes)
	humanCount := 0
	for _, pl := range game.players {
		if !pl.isBot {
			humanCount++
		}
	}
	totalPot := game.ante * float64(humanCount+game.botAntes)
	net, _ := communityTax(winner.userID, totalPot, 0.05)

	name := p.DisplayName(winner.userID)
	p.euro.Credit(winner.userID, net, "uno_multi_win")

	p.SendMessage(game.roomID, fmt.Sprintf(
		"🎉 **%s wins Multiplayer UNO!**\nPot: €%d | Turns: %d\n\n%s",
		name, int(totalPot), game.turns, pickCommentary("player_win")))

	p.recordMultiGame(game, winner.userID, "win")
	p.cleanupMultiGame(game)
}

func (p *UnoPlugin) multiBotWins(game *unoMultiGame, winner *unoMultiPlayer) {
	if game.done {
		return
	}
	game.done = true

	if game.timer != nil {
		game.timer.Stop()
	}
	if game.inactiveTimer != nil {
		game.inactiveTimer.Stop()
	}

	humanCount := 0
	for _, pl := range game.players {
		if !pl.isBot {
			humanCount++
		}
	}
	totalPot := game.ante * float64(humanCount+game.botAntes)

	// Pot goes to community
	p.addToPot(totalPot)

	bn := winner.displayName
	p.SendMessage(game.roomID, fmt.Sprintf(
		"💀 **%s wins Multiplayer UNO!**\n€%d goes to the community pot.\n\n%s",
		bn, int(totalPot), pickCommentary("bot_win")))

	for _, pl := range game.players {
		if !pl.isBot {
			recordBotDefeat(pl.userID, "uno_multi")
		}
	}
	p.recordMultiGame(game, id.UserID("bot"), "bot_win")
	p.cleanupMultiGame(game)
}

func (p *UnoPlugin) multiPlayerForfeit(game *unoMultiGame, player *unoMultiPlayer) {
	player.active = false
	name := p.DisplayName(player.userID)

	// Shuffle cards back into draw pile
	game.drawPile = append(game.drawPile, player.hand...)
	rand.Shuffle(len(game.drawPile), func(i, j int) { game.drawPile[i], game.drawPile[j] = game.drawPile[j], game.drawPile[i] })
	player.hand = nil

	// Check if game should end
	active := game.activePlayers()
	if len(active) <= 1 {
		if len(active) == 1 {
			winner := active[0]
			if winner.isBot {
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. All humans out!", name))
				p.multiBotWins(game, winner)
			} else {
				p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited. Only one player remains!", name))
				p.multiPlayerWins(game, winner)
			}
		} else {
			game.done = true
			p.SendMessage(game.roomID, "🃏 Game ended — no players remaining.")
			p.cleanupMultiGame(game)
		}
		return
	}

	p.SendMessage(game.roomID, fmt.Sprintf("🃏 %s forfeited! Game continues with %d players.", name, len(active)))

	// If it was this player's turn, advance
	if game.currentPlayer() == player || !game.currentPlayer().active {
		game.currentIdx = game.nextActiveIdx()
		game.turnID++
		p.executeMultiTurn(game)
	}
}

func (p *UnoPlugin) cleanupMultiGame(game *unoMultiGame) {
	p.mu.Lock()
	defer p.mu.Unlock()

	delete(p.multiGames, game.id)
	for roomID, gID := range p.dmToMulti {
		if gID == game.id {
			delete(p.dmToMulti, roomID)
		}
	}
}

func (p *UnoPlugin) recordMultiGame(game *unoMultiGame, winnerID id.UserID, result string) {
	playerIDs := make([]string, 0)
	for _, pl := range game.players {
		if !pl.isBot {
			playerIDs = append(playerIDs, string(pl.userID))
		}
	}

	humanCount := len(playerIDs)
	totalPot := game.ante * float64(humanCount+game.botAntes)

	db.Exec("uno_multi: record game",
		`INSERT INTO uno_multi_games (room_id, ante, pot_total, winner_id, player_ids, result, turns, started_at, ended_at)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		string(game.roomID), game.ante, totalPot, string(winnerID),
		strings.Join(playerIDs, ","), result, game.turns,
		game.startedAt.UTC().Format("2006-01-02 15:04:05"),
		time.Now().UTC().Format("2006-01-02 15:04:05"),
	)
}

// ---------------------------------------------------------------------------
// Display
// ---------------------------------------------------------------------------

func (p *UnoPlugin) sendMultiStatus(game *unoMultiGame, player *unoMultiPlayer) {
	var sb strings.Builder
	currentName := p.multiName(game.currentPlayer())

	sb.WriteString(fmt.Sprintf("🃏 **UNO Status**\nDiscard pile: %s\nCurrent turn: %s\n\n",
		game.discardTop.DisplayWithColor(game.topColor), currentName))

	sb.WriteString("**Your hand:**\n")
	for i, c := range player.hand {
		playable := ""
		if c.canPlayOn(game.discardTop, game.topColor) {
			playable = " ✅"
		}
		sb.WriteString(fmt.Sprintf("%d. %s%s\n", i+1, c.Display(), playable))
	}

	sb.WriteString("\nCard counts: ")
	var counts []string
	for _, pl := range game.players {
		if pl == player || !pl.active {
			continue
		}
		counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
	}
	sb.WriteString(strings.Join(counts, " | "))

	p.SendMessage(player.dmRoomID, sb.String())
}

func (p *UnoPlugin) sendMultiHandDisplay(game *unoMultiGame, player *unoMultiPlayer) {
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("🃏 **Your turn!**\nDiscard pile: %s\n\n**Your hand:**\n",
		game.discardTop.DisplayWithColor(game.topColor)))

	for i, c := range player.hand {
		playable := ""
		if c.canPlayOn(game.discardTop, game.topColor) {
			playable = " ✅"
		}
		sb.WriteString(fmt.Sprintf("%d. %s%s\n", i+1, c.Display(), playable))
	}

	// Card counts for all opponents
	sb.WriteString("\nCard counts: ")
	counts := make([]string, 0)
	for _, pl := range game.players {
		if pl == player || !pl.active {
			continue
		}
		counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
	}
	sb.WriteString(strings.Join(counts, " | "))

	if game.noMercy && len(player.hand) >= 20 {
		sb.WriteString(fmt.Sprintf("\n⚠️ **You have %d cards! (25 = eliminated)**", len(player.hand)))
	}

	sb.WriteString("\n\nReply with a card number to play, or **draw** to draw.")

	p.SendMessage(player.dmRoomID, sb.String())
}

// sendMultiHandDisplayStacking shows hand during stacking — only stackable cards are playable.
func (p *UnoPlugin) sendMultiHandDisplayStacking(game *unoMultiGame, player *unoMultiPlayer) {
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("⚠️ **Stack incoming: +%d!**\nDiscard pile: %s\n\n**Your hand:**\n",
		game.stackTotal, game.discardTop.DisplayWithColor(game.topColor)))

	for i, c := range player.hand {
		marker := ""
		if c.canPlayOnStacking(game.topColor, game.stackMinValue) {
			marker = " ✅"
		}
		sb.WriteString(fmt.Sprintf("%d. %s%s\n", i+1, c.Display(), marker))
	}

	sb.WriteString("\nCard counts: ")
	counts := make([]string, 0)
	for _, pl := range game.players {
		if pl == player || !pl.active {
			continue
		}
		counts = append(counts, fmt.Sprintf("%s (%d)", p.multiName(pl), len(pl.hand)))
	}
	sb.WriteString(strings.Join(counts, " | "))

	sb.WriteString(fmt.Sprintf("\n\nPlay a draw card to stack, or type **accept** to draw %d cards.", game.stackTotal))

	p.SendMessage(player.dmRoomID, sb.String())
}

// handleMultiSwapChoice handles the player choosing a swap target for the 7-0 rule.
func (p *UnoPlugin) handleMultiSwapChoice(game *unoMultiGame, player *unoMultiPlayer, input string) error {
	// Parse target number
	var targetIdx int
	if _, err := fmt.Sscanf(input, "%d", &targetIdx); err != nil || targetIdx < 1 {
		p.SendMessage(player.dmRoomID, "Choose a player number to swap hands with.")
		return nil
	}

	// Map number to active player
	i := 1
	var target *unoMultiPlayer
	for _, pl := range game.players {
		if pl == player || !pl.active {
			continue
		}
		if i == targetIdx {
			target = pl
			break
		}
		i++
	}

	if target == nil {
		p.SendMessage(player.dmRoomID, "Invalid player number. Try again.")
		return nil
	}

	swapHandsMulti(player, target)
	game.phase = unoMultiPhasePlay

	name := p.DisplayName(player.userID)
	targetName := p.multiName(target)

	p.SendMessage(player.dmRoomID, fmt.Sprintf("Swapped hands with %s! You now have %d cards.", targetName, len(player.hand)))
	if !target.isBot {
		p.SendMessage(target.dmRoomID, fmt.Sprintf("🔄 %s swapped hands with you! You now have %d cards.", name, len(target.hand)))
	}
	p.SendMessage(game.roomID, fmt.Sprintf("🔄 %s swaps hands with %s! %s",
		name, targetName, pickNoMercyCommentary("hand_swap")))

	// Check win (if player got an empty hand from swap)
	if len(player.hand) == 0 {
		p.multiPlayerWins(game, player)
		return nil
	}

	// Continue to next turn
	p.advanceAndExecute(game)
	return nil
}
