local _, namespace = ...
local GuildPing = namespace.GuildPing
local UI = GuildPing.UIFactory

if not UI then return end

local CreateWindow = UI.CreateWindow
local MakeDraggable = UI.MakeDraggable
local SetTitle = UI.SetTitle
local CreateButton = UI.CreateButton
local CreateLabel = UI.CreateLabel
local CreateInput = UI.CreateInput
local ROW_BACKDROP = UI.RowBackdrop
local GUILD_GREEN = UI.GuildGreen
local MUTED_COLOR = UI.MutedColor
local WARNING_GOLD = UI.WarningGold
local UPDATE_RED = UI.UpdateRed
local Unpack = unpack or table.unpack

local RESIGN_POPUP_KEY = "GUILD_PING_CONNECT_FOUR_RESIGN"
local END_PRACTICE_POPUP_KEY = "GUILD_PING_CONNECT_FOUR_END_PRACTICE"
local ABANDON_POPUP_KEY = "GUILD_PING_CONNECT_FOUR_ABANDON"
local BOARD_BLUE = { 0.075, 0.255, 0.72, 1 }
local BOARD_BLUE_HOVER = { 0.12, 0.38, 0.92, 1 }
local SLOT_DARK = { 0.035, 0.055, 0.11, 1 }
local YELLOW_DISC = { 1.0, 0.76, 0.08, 1 }
local RED_DISC = { 0.88, 0.11, 0.16, 1 }
local WIN_GOLD = { 1.0, 0.82, 0.24, 1 }
local DROP_START_Y = 30
local PIECE_COLOR_OPTIONS = {
	{ key = "DEFAULT", label = "Default" },
	{ key = "SKY", label = "Sky blue", color = { 0.34, 0.71, 0.91, 1 } },
	{ key = "TEAL", label = "Teal", color = { 0.00, 0.78, 0.58, 1 } },
	{ key = "VIOLET", label = "Violet", color = { 0.80, 0.48, 0.78, 1 } },
	{ key = "WHITE", label = "High contrast", color = { 0.96, 0.97, 1.00, 1 } },
}

local function GetPieceColorOption(colorKey)
	for _, option in ipairs(PIECE_COLOR_OPTIONS) do
		if option.key == colorKey then return option end
	end
	return PIECE_COLOR_OPTIONS[1]
end

local function FormatInviteAge(receivedAt)
	local currentTime = GetTime and GetTime() or 0
	local seconds = math.max(0, math.floor(currentTime - (receivedAt or currentTime)))
	return seconds < 60 and "just now" or string.format("%dm ago", math.floor(seconds / 60))
end

local function CreateInviteRow(parent, width)
	local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
	row:SetSize(width, 50)
	row:SetBackdrop(ROW_BACKDROP)
	row:SetBackdropColor(0.047, 0.055, 0.078, 0.92)
	row:SetBackdropBorderColor(0.180, 0.204, 0.275, 0.95)
	row.senderText = CreateLabel(row, "", "GameFontNormal")
	row.senderText:SetPoint("LEFT", 10, 0)
	row.senderText:SetPoint("RIGHT", -184, 0)
	row.senderText:SetWordWrap(false)
	row.declineButton = CreateButton(row, "Decline", 78, 24)
	row.declineButton:SetPoint("RIGHT", -8, 0)
	row.declineButton:SetScript("OnClick", function() if row.inviteID then GuildPing:DeclineConnectFourChallenge(row.inviteID) end end)
	row.acceptButton = CreateButton(row, "Accept", 82, 24, "primary")
	row.acceptButton:SetPoint("RIGHT", row.declineButton, "LEFT", -6, 0)
	row.acceptButton:SetScript("OnClick", function()
		local game, errorMessage = GuildPing:AcceptConnectFourChallenge(row.inviteID)
		if game then GuildPing:ShowConnectFourBoard() else GuildPing:Print(errorMessage) end
	end)
	row:Hide()
	return row
end

local function CreatePieceColorButton(parent, option)
	local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
	button:SetSize(32, 24)
	button:SetBackdrop(ROW_BACKDROP)
	button:SetBackdropColor(Unpack(SLOT_DARK))
	button:SetBackdropBorderColor(Unpack(BOARD_BLUE))
	button.colorKey = option.key
	button.colorLabel = option.label
	button.swatch = button:CreateTexture(nil, "ARTWORK")
	button.swatch:SetTexture("Interface\\CHARACTERFRAME\\TempPortraitAlphaMask")
	button.swatch:SetSize(16, 16)
	button.swatch:SetPoint("CENTER")
	button:SetScript("OnClick", function(self)
		local changed, errorMessage = GuildPing:SetConnectFourPieceColorSetting(self.colorKey)
		if not changed then GuildPing:Print(errorMessage) end
	end)
	button:SetScript("OnEnter", function(self)
		if not GameTooltip then return end
		GameTooltip:SetOwner(self, "ANCHOR_TOP")
		GameTooltip:SetText(self.colorLabel)
		GameTooltip:AddLine("Only changes how this side looks on your client.", 0.78, 0.82, 0.92, true)
		GameTooltip:Show()
	end)
	button:SetScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
	return button
end

function GuildPing:CreateConnectFourLobby()
	if self.connectFourLobby then return self.connectFourLobby end
	local frame = CreateWindow("GuildPingConnectFourLobbyFrame")
	frame:SetSize(620, 570)
	frame:SetPoint("CENTER")
	frame:SetFrameStrata("DIALOG")
	MakeDraggable(frame, frame.header)
	SetTitle(frame, "Guild Ping — Connect Four")
	frame.heading = CreateLabel(frame, "Connect Four", "GameFontNormalLarge")
	frame.heading:SetPoint("TOPLEFT", 24, -49)
	frame.description = CreateLabel(frame, "Drop discs into seven columns and connect four horizontally, vertically, or diagonally.", "GameFontHighlight")
	frame.description:SetPoint("TOPLEFT", 24, -79)
	frame.description:SetPoint("TOPRIGHT", -24, -79)
	frame.description:SetWordWrap(true)
	frame.challengeLabel = CreateLabel(frame, "Guild member", "GameFontNormal")
	frame.challengeLabel:SetPoint("TOPLEFT", 24, -132)
	frame.challengeInput = CreateInput(frame, 390, "Character or Character-Realm", 48)
	frame.challengeInput:SetPoint("TOPLEFT", 26, -154)
	frame.challengeButton = CreateButton(frame, "Challenge", 164, 27, "primary")
	frame.challengeButton:SetPoint("LEFT", frame.challengeInput, "RIGHT", 12, 0)
	frame.statusText = CreateLabel(frame, "Enter your own name for local practice.", "GameFontHighlightSmall")
	frame.statusText:SetPoint("TOPLEFT", 26, -192)
	frame.statusText:SetTextColor(MUTED_COLOR:GetRGB())
	frame.challengeButton:SetScript("OnClick", function()
		local challenge, errorMessage = GuildPing:ChallengeConnectFourPlayer(frame.challengeInput:GetText())
		if not challenge then GuildPing:Print(errorMessage)
		elseif challenge.practice then GuildPing:ShowConnectFourBoard()
		else frame.statusText:SetText("Challenge sent to " .. challenge.target .. ".") end
	end)
	frame.activeHeader = CreateLabel(frame, "Current game", "GameFontNormalLarge")
	frame.activeHeader:SetPoint("TOPLEFT", 24, -232)
	frame.activeText = CreateLabel(frame, "", "GameFontHighlight")
	frame.activeText:SetPoint("TOPLEFT", 26, -264)
	frame.activeText:SetPoint("RIGHT", -180, 0)
	frame.resumeButton = CreateButton(frame, "Open Board", 126, 25, "primary")
	frame.resumeButton:SetPoint("TOPRIGHT", -24, -255)
	frame.resumeButton:SetScript("OnClick", function() GuildPing:ShowConnectFourBoard() end)
	frame.inviteHeader = CreateLabel(frame, "Incoming challenges", "GameFontNormalLarge")
	frame.inviteHeader:SetPoint("TOPLEFT", 24, -310)
	frame.inviteRows = {}
	for index = 1, 3 do
		local row = CreateInviteRow(frame, 572)
		row:SetPoint("TOPLEFT", 24, -344 - ((index - 1) * 56))
		frame.inviteRows[index] = row
	end
	frame.emptyInvites = CreateLabel(frame, "No incoming Connect Four challenges.", "GameFontDisable")
	frame.emptyInvites:SetPoint("TOPLEFT", 30, -354)
	frame.gamesButton = CreateButton(frame, "All Games", 104, 25)
	frame.gamesButton:SetPoint("BOTTOMLEFT", 24, 16)
	frame.gamesButton:SetScript("OnClick", function() GuildPing:ShowGames() end)
	frame.closeButton = CreateButton(frame, "Close", 90, 25)
	frame.closeButton:SetPoint("BOTTOMRIGHT", -24, 16)
	frame.closeButton:SetScript("OnClick", function() frame:Hide() end)
	frame:SetScript("OnShow", function() GuildPing:RefreshConnectFourLobby() end)
	frame:Hide()
	self.connectFourLobby = frame
	return frame
end

function GuildPing:RefreshConnectFourLobby()
	local frame = self.connectFourLobby
	if not frame then return end
	local game = self.activeConnectFourGame
	if game then
		frame.activeText:SetText(game.practice and ("Local practice  •  " .. (game.status == "FINISHED" and "finished" or "in progress"))
			or string.format("You vs %s  •  %s", self:GetConnectFourOpponent(game) or "Unknown", game.status == "FINISHED" and "finished" or "in progress"))
		frame.resumeButton:Show()
	else
		frame.activeText:SetText("No Connect Four game saved for this guild.")
		frame.resumeButton:Hide()
	end
	local invites = self:GetConnectFourInvites()
	frame.emptyInvites:SetShown(#invites == 0)
	for index, row in ipairs(frame.inviteRows) do
		local invite = invites[#invites - index + 1]
		if invite then
			row.inviteID = invite.id
			row.senderText:SetText(invite.sender .. " challenged you  •  " .. FormatInviteAge(invite.receivedAt))
			row.acceptButton:SetEnabled(not game or game.status ~= "ACTIVE")
			row:Show()
		else row.inviteID = nil row:Hide() end
	end
end

local function EnsurePopups()
	if not StaticPopupDialogs[RESIGN_POPUP_KEY] then
		StaticPopupDialogs[RESIGN_POPUP_KEY] = {
			text = "Resign this Connect Four game? Your opponent will win.", button1 = "Resign", button2 = CANCEL,
			timeout = 0, whileDead = true, hideOnEscape = true, preferredIndex = 3,
			OnAccept = function() local ok, errorMessage = GuildPing:ResignConnectFourGame() if not ok then GuildPing:Print(errorMessage) end end,
		}
	end
	if not StaticPopupDialogs[END_PRACTICE_POPUP_KEY] then
		StaticPopupDialogs[END_PRACTICE_POPUP_KEY] = {
			text = "End this local Connect Four practice game?", button1 = "End Practice", button2 = CANCEL,
			timeout = 0, whileDead = true, hideOnEscape = true, preferredIndex = 3,
			OnAccept = function() local ok, errorMessage = GuildPing:ResignConnectFourGame() if not ok then GuildPing:Print(errorMessage) end end,
		}
	end
	if not StaticPopupDialogs[ABANDON_POPUP_KEY] then
		StaticPopupDialogs[ABANDON_POPUP_KEY] = {
			text = "Abandon this saved Connect Four game? It will be removed from this character.", button1 = "Abandon Game", button2 = CANCEL,
			timeout = 0, whileDead = true, hideOnEscape = true, preferredIndex = 3,
			OnAccept = function()
				local ok, errorMessage = GuildPing:AbandonConnectFourGame()
				if ok then GuildPing:ShowConnectFourLobby() else GuildPing:Print(errorMessage) end
			end,
		}
	end
end

function GuildPing:CreateConnectFourBoard()
	if self.connectFourBoard then return self.connectFourBoard end
	EnsurePopups()
	local frame = CreateWindow("GuildPingConnectFourBoardFrame")
	frame:SetSize(570, 620)
	frame:SetPoint("CENTER")
	frame:SetFrameStrata("DIALOG")
	MakeDraggable(frame, frame.header)
	SetTitle(frame, "Guild Ping — Connect Four")
	frame.opponentText = CreateLabel(frame, "", "GameFontHighlight")
	frame.opponentText:SetPoint("TOP", 0, -49)
	frame.statusText = CreateLabel(frame, "", "GameFontNormal")
	frame.statusText:SetPoint("TOPLEFT", 24, -77)
	frame.statusText:SetPoint("TOPRIGHT", -24, -77)
	frame.statusText:SetJustifyH("CENTER")
	frame.board = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	frame.board:SetSize(438, 376)
	frame.board:SetPoint("TOP", 0, -112)
	frame.board:SetBackdrop(ROW_BACKDROP)
	frame.board:SetBackdropColor(Unpack(BOARD_BLUE))
	frame.board:SetBackdropBorderColor(0.32, 0.55, 1, 1)
	frame.slots = {}
	for visualRow = 1, 6 do
		for column = 1, 7 do
			local slot = CreateFrame("Button", nil, frame.board, "BackdropTemplate")
			slot:SetSize(60, 60)
			slot:SetPoint("TOPLEFT", 8 + ((column - 1) * 60), -8 - ((visualRow - 1) * 60))
			slot:SetBackdrop(ROW_BACKDROP)
			slot:SetBackdropColor(Unpack(SLOT_DARK))
			slot:SetBackdropBorderColor(Unpack(BOARD_BLUE))
			slot.visualRow, slot.column = visualRow, column
			slot.disc = slot:CreateTexture(nil, "ARTWORK")
			slot.disc:SetTexture("Interface\\CHARACTERFRAME\\TempPortraitAlphaMask")
			slot.disc:SetSize(48, 48)
			slot.disc:SetPoint("CENTER")
			slot.disc:Hide()
			slot.winGlow = slot:CreateTexture(nil, "OVERLAY")
			slot.winGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
			slot.winGlow:SetSize(68, 68)
			slot.winGlow:SetPoint("CENTER")
			slot.winGlow:SetBlendMode("ADD")
			slot.winGlow:SetVertexColor(Unpack(WIN_GOLD))
			slot.winGlow:Hide()
			slot:SetScript("OnClick", function()
				local move, errorMessage = GuildPing:PlayConnectFourColumn(column)
				if not move then GuildPing:Print(errorMessage) end
			end)
			slot:SetScript("OnEnter", function() frame.hoverColumn = column GuildPing:RefreshConnectFourBoard() end)
			slot:SetScript("OnLeave", function() frame.hoverColumn = nil GuildPing:RefreshConnectFourBoard() end)
			frame.slots[((visualRow - 1) * 7) + column] = slot
		end
	end
	frame.effects = CreateFrame("Frame", nil, frame.board)
	frame.effects:SetAllPoints(frame.board)
	frame.effects:SetFrameLevel(frame.board:GetFrameLevel() + 10)
	frame.effects:EnableMouse(false)
	frame.fallingDisc = frame.effects:CreateTexture(nil, "OVERLAY")
	frame.fallingDisc:SetTexture("Interface\\CHARACTERFRAME\\TempPortraitAlphaMask")
	frame.fallingDisc:SetSize(48, 48)
	frame.fallingDisc:Hide()
	frame.moveText = CreateLabel(frame, "", "GameFontHighlightSmall")
	frame.moveText:SetPoint("TOP", frame.board, "BOTTOM", 0, -13)
	frame.moveText:SetTextColor(MUTED_COLOR:GetRGB())
	frame.pieceColorLabel = CreateLabel(frame, "My pieces: Default", "GameFontHighlightSmall")
	frame.pieceColorLabel:SetPoint("TOPLEFT", frame.board, "BOTTOMLEFT", 0, -34)
	frame.pieceColorLabel:SetSize(190, 24)
	frame.pieceColorLabel:SetJustifyH("RIGHT")
	frame.pieceColorButtons = {}
	for index, option in ipairs(PIECE_COLOR_OPTIONS) do
		local button = CreatePieceColorButton(frame, option)
		if index == 1 then button:SetPoint("LEFT", frame.pieceColorLabel, "RIGHT", 7, 0)
		else button:SetPoint("LEFT", frame.pieceColorButtons[index - 1], "RIGHT", 5, 0) end
		frame.pieceColorButtons[index] = button
	end
	frame.lobbyButton = CreateButton(frame, "Connect Four Lobby", 144, 25)
	frame.lobbyButton:SetPoint("BOTTOMLEFT", 24, 16)
	frame.lobbyButton:SetScript("OnClick", function() frame:Hide() GuildPing:ShowConnectFourLobby() end)
	frame.resignButton = CreateButton(frame, "Resign", 100, 25, "danger")
	frame.resignButton:SetPoint("BOTTOM", 0, 16)
	frame.resignButton:SetScript("OnClick", function()
		StaticPopup_Show(GuildPing.activeConnectFourGame and GuildPing.activeConnectFourGame.practice and END_PRACTICE_POPUP_KEY or RESIGN_POPUP_KEY)
	end)
	frame.playAgainButton = CreateButton(frame, "Play Again", 112, 25, "primary")
	frame.playAgainButton:SetPoint("BOTTOM", 0, 16)
	frame.playAgainButton:SetScript("OnClick", function()
		local challenge, errorMessage = GuildPing:PlayConnectFourAgain()
		if not challenge then GuildPing:Print(errorMessage) end
	end)
	frame.lifecycleResumeButton = CreateButton(frame, "Resume", 112, 25, "primary")
	frame.lifecycleResumeButton:SetPoint("BOTTOM", -66, 16)
	frame.lifecycleResumeButton:SetScript("OnClick", function() local ok, errorMessage = GuildPing:ResumeConnectFourGame() if not ok then GuildPing:Print(errorMessage) end end)
	frame.abandonButton = CreateButton(frame, "Abandon Game", 126, 25, "danger")
	frame.abandonButton:SetPoint("BOTTOM", 62, 16)
	frame.abandonButton:SetScript("OnClick", function() StaticPopup_Show(ABANDON_POPUP_KEY) end)
	frame.closeButton = CreateButton(frame, "Close", 90, 25)
	frame.closeButton:SetPoint("BOTTOMRIGHT", -24, 16)
	frame.closeButton:SetScript("OnClick", function() frame:Hide() end)
	frame:SetScript("OnShow", function() GuildPing:RefreshConnectFourBoard() end)
	frame:SetScript("OnHide", function()
		frame.dropAnimation = nil
		frame.winPulseElapsed = nil
		frame.fallingDisc:Hide()
		frame:SetScript("OnUpdate", nil)
	end)
	frame:Hide()
	self.connectFourBoard = frame
	return frame
end

function GuildPing:GetConnectFourDiscDisplayColor(piece, game)
	local defaultColor = piece == self.CONNECT_FOUR_YELLOW and YELLOW_DISC or RED_DISC
	if piece ~= self:GetLocalConnectFourColor(game) then return defaultColor end
	local colorKey = self.GetConnectFourPieceColorSetting and self:GetConnectFourPieceColorSetting() or "DEFAULT"
	return GetPieceColorOption(colorKey).color or defaultColor
end

local function PositionFallingDisc(frame, column, y)
	local x = 38 + ((column - 1) * 60)
	frame.fallingDisc:ClearAllPoints()
	frame.fallingDisc:SetPoint("CENTER", frame.board, "TOPLEFT", x, y)
end

function GuildPing:AnimateConnectFourMove(game, move)
	local frame = self.connectFourBoard
	if not frame or not frame:IsShown() or game ~= self.activeConnectFourGame or type(move) ~= "table" then return end
	local row, column = tonumber(move.row), tonumber(move.column)
	if not row or row < 1 or row > 6 or not column or column < 1 or column > 7 then return end
	local visualRow = 7 - row
	local duration = 0.22 + (visualRow * 0.055)
	frame.dropAnimation = {
		gameID = game.id, moveNumber = game.moveNumber, row = row, visualRow = visualRow,
		column = column, color = move.color, elapsed = 0, duration = duration, finishAt = duration + 0.16,
		targetY = -38 - ((visualRow - 1) * 60),
	}
	frame.fallingDisc:SetVertexColor(Unpack(self:GetConnectFourDiscDisplayColor(move.color, game)))
	PositionFallingDisc(frame, column, DROP_START_Y)
	frame.fallingDisc:Show()
	frame:SetScript("OnUpdate", function(_, elapsed) GuildPing:UpdateConnectFourEffects(elapsed) end)
end

function GuildPing:UpdateConnectFourEffects(elapsed)
	local frame = self.connectFourBoard
	if not frame or not frame:IsShown() then return end
	local animation = frame.dropAnimation
	if animation then
		local game = self.activeConnectFourGame
		if not game or game.id ~= animation.gameID or game.moveNumber ~= animation.moveNumber then
			frame.dropAnimation = nil
			frame.fallingDisc:Hide()
		else
			animation.elapsed = animation.elapsed + elapsed
			local y
			if animation.elapsed < animation.duration then
				local progress = math.min(1, animation.elapsed / animation.duration)
				y = DROP_START_Y + ((animation.targetY - DROP_START_Y) * progress * progress)
			else
				local impactProgress = math.min(1, (animation.elapsed - animation.duration) / (animation.finishAt - animation.duration))
				y = animation.targetY + (math.sin(impactProgress * math.pi) * 8 * (1 - impactProgress))
			end
			PositionFallingDisc(frame, animation.column, y)
			if animation.elapsed >= animation.finishAt then
				frame.dropAnimation = nil
				frame.fallingDisc:Hide()
				self:RefreshConnectFourBoard()
			end
		end
	end

	if frame.hasWinningLine and not frame.dropAnimation then
		frame.winPulseElapsed = (frame.winPulseElapsed or 0) + elapsed
		local alpha = 0.48 + (math.abs(math.sin(frame.winPulseElapsed * 3.2)) * 0.42)
		for _, slot in ipairs(frame.slots) do
			if slot.isWinning then slot.winGlow:SetAlpha(alpha) end
		end
	elseif not frame.dropAnimation then
		frame:SetScript("OnUpdate", nil)
	end
end

function GuildPing:RefreshConnectFourBoard()
	local frame, game = self.connectFourBoard, self.activeConnectFourGame
	if not frame then return end
	if not game then frame:Hide() return end
	local localColor = self:GetLocalConnectFourColor(game)
	local controllable = self:GetConnectFourControllableColor(game)
	local waiting = self:IsGameWaitingForResume(game)
	local currentGuild = self:IsConnectFourGameForCurrentGuild(game)
	local animation = frame.dropAnimation
	if animation and (animation.gameID ~= game.id or animation.moveNumber ~= game.moveNumber) then
		frame.dropAnimation = nil
		frame.fallingDisc:Hide()
		animation = nil
	end
	if animation then frame.fallingDisc:SetVertexColor(Unpack(self:GetConnectFourDiscDisplayColor(animation.color, game))) end
	frame.opponentText:SetText(game.practice and "Practice mode  •  You control Yellow and Red"
		or ("Opponent: " .. (self:GetConnectFourOpponent(game) or "Unknown")))
	if not currentGuild then
		frame.statusText:SetText("This game belongs to a different guild profile.") frame.statusText:SetTextColor(UPDATE_RED:GetRGB())
	elseif animation then
		local actor = game.practice and (animation.color == self.CONNECT_FOUR_YELLOW and "Yellow" or "Red")
			or (animation.color == localColor and "Your" or "Opponent's")
		frame.statusText:SetText(actor .. " disc is dropping…")
		frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif game.persistenceState == "RECONNECTING" then
		frame.statusText:SetText("Reconnecting to your opponent…") frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif game.persistenceState == "STALE" then
		frame.statusText:SetText("No response. Resume to retry or abandon this game.") frame.statusText:SetTextColor(UPDATE_RED:GetRGB())
	elseif game.persistenceState == "RESTORED" then
		frame.statusText:SetText("Practice game restored. Resume when you're ready.") frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	elseif game.status == "FINISHED" then
		if not game.winner then frame.statusText:SetText(game.practice and "Practice ended." or "Draw — the board is full.")
		elseif game.practice then frame.statusText:SetText((game.winner == self.CONNECT_FOUR_YELLOW and "Yellow" or "Red") .. " connected four!")
		elseif game.winner == localColor then frame.statusText:SetText("You won — four connected!")
		else frame.statusText:SetText("You lost — your opponent connected four.") end
		local resultColor = (game.practice or game.winner ~= nil and game.winner == localColor) and GUILD_GREEN or UPDATE_RED
		frame.statusText:SetTextColor(resultColor:GetRGB())
	elseif game.turn == controllable then
		frame.statusText:SetText((game.practice and (game.turn == self.CONNECT_FOUR_YELLOW and "Yellow" or "Red") or "Your") .. " turn — choose a column.")
		frame.statusText:SetTextColor(GUILD_GREEN:GetRGB())
	else
		frame.statusText:SetText("Waiting for your opponent to drop a disc.") frame.statusText:SetTextColor(WARNING_GOLD:GetRGB())
	end
	local winningCells, winningLookup = self:GetConnectFourWinningCells(game), {}
	for _, cellIndex in ipairs(winningCells) do winningLookup[cellIndex] = true end
	frame.hasWinningLine = next(winningLookup) ~= nil
	frame.moveText:SetText(frame.hasWinningLine and string.format("Winning line  •  Move %d", game.moveNumber or 0)
		or string.format("Move %d  •  connect four in any direction", game.moveNumber or 0))
	local pieceColorKey = self.GetConnectFourPieceColorSetting and self:GetConnectFourPieceColorSetting() or "DEFAULT"
	local pieceColorOption = GetPieceColorOption(pieceColorKey)
	frame.pieceColorLabel:SetText((game.practice and "Yellow pieces: " or "My pieces: ") .. pieceColorOption.label)
	for _, button in ipairs(frame.pieceColorButtons) do
		local option = GetPieceColorOption(button.colorKey)
		local swatchColor = option.color or (localColor == self.CONNECT_FOUR_RED and RED_DISC or YELLOW_DISC)
		button.swatch:SetVertexColor(Unpack(swatchColor))
		button:SetBackdropColor(Unpack(button.colorKey == pieceColorKey and { 0.12, 0.15, 0.23, 1 } or SLOT_DARK))
		button:SetBackdropBorderColor(Unpack(button.colorKey == pieceColorKey and WIN_GOLD or BOARD_BLUE))
	end
	local actionable = currentGuild and game.status == "ACTIVE" and not waiting and not animation and game.turn == controllable
	for _, slot in ipairs(frame.slots) do
		local row = 7 - slot.visualRow
		local cellIndex = ((row - 1) * 7) + slot.column
		local piece = game.board[cellIndex]
		local isFallingTarget = animation and animation.row == row and animation.column == slot.column
		slot.isWinning = winningLookup[cellIndex] and not animation or false
		slot:SetBackdropBorderColor(Unpack(slot.isWinning and WIN_GOLD
			or actionable and frame.hoverColumn == slot.column and BOARD_BLUE_HOVER or BOARD_BLUE))
		if piece and not isFallingTarget then slot.disc:SetVertexColor(Unpack(self:GetConnectFourDiscDisplayColor(piece, game))) slot.disc:Show()
		else slot.disc:Hide() end
		slot.winGlow:SetShown(slot.isWinning)
		if slot.isWinning then slot.winGlow:SetAlpha(0.75) end
		slot:EnableMouse(actionable)
	end
	frame.resignButton:SetText(game.practice and "End Practice" or "Resign")
	frame.resignButton:SetShown(game.status == "ACTIVE" and not waiting)
	frame.resignButton:SetEnabled(currentGuild and game.status == "ACTIVE" and not waiting)
	local rematchWaiting = self.outgoingConnectFourChallenge ~= nil
	frame.playAgainButton:SetShown(game.status == "FINISHED" and not waiting)
	frame.playAgainButton:SetText(rematchWaiting and "Challenge Sent" or "Play Again")
	frame.playAgainButton:SetEnabled(currentGuild and not rematchWaiting and not animation)
	frame.lifecycleResumeButton:SetShown(waiting)
	frame.lifecycleResumeButton:SetEnabled(currentGuild and game.persistenceState ~= "RECONNECTING")
	frame.abandonButton:SetShown(waiting)
	frame.abandonButton:SetEnabled(currentGuild)
	if frame.hasWinningLine or animation then
		frame:SetScript("OnUpdate", function(_, elapsed) GuildPing:UpdateConnectFourEffects(elapsed) end)
	else
		frame.winPulseElapsed = nil
		frame:SetScript("OnUpdate", nil)
	end
end

function GuildPing:ShowConnectFourLobby()
	if self.gamesHub then self.gamesHub:Hide() end
	if self.checkersLobby then self.checkersLobby:Hide() end
	if self.checkersBoard then self.checkersBoard:Hide() end
	if self.deathRollLobby then self.deathRollLobby:Hide() end
	if self.deathRollGameFrame then self.deathRollGameFrame:Hide() end
	if self.pokerLobby then self.pokerLobby:Hide() end
	if self.pokerTableFrame then self.pokerTableFrame:Hide() end
	if self.connectFourBoard then self.connectFourBoard:Hide() end
	local frame = self:CreateConnectFourLobby()
	frame:Show()
	self:RefreshConnectFourLobby()
end

function GuildPing:ShowConnectFourBoard()
	if not self.activeConnectFourGame then self:ShowConnectFourLobby() return end
	self:AcknowledgeGameTurn("connectFour")
	if self.gamesHub then self.gamesHub:Hide() end
	if self.checkersLobby then self.checkersLobby:Hide() end
	if self.checkersBoard then self.checkersBoard:Hide() end
	if self.deathRollLobby then self.deathRollLobby:Hide() end
	if self.deathRollGameFrame then self.deathRollGameFrame:Hide() end
	if self.pokerLobby then self.pokerLobby:Hide() end
	if self.pokerTableFrame then self.pokerTableFrame:Hide() end
	if self.connectFourLobby then self.connectFourLobby:Hide() end
	local frame = self:CreateConnectFourBoard()
	frame:Show()
	self:RefreshConnectFourBoard()
end

function GuildPing:ShowConnectFour()
	if self.fiveDiceFrame then self.fiveDiceFrame:Hide() end
	if self.runeMastermindFrame then self.runeMastermindFrame:Hide() end
	if self.activeConnectFourGame then self:ShowConnectFourBoard() else self:ShowConnectFourLobby() end
end
