local _, namespace = ...
local GuildPing = namespace.GuildPing

local MESSAGE_PREFIX = "GuildPingFour"
local PROTOCOL_VERSION = "1"
local CHALLENGE_TIMEOUT = 300
local MAX_INVITES = 3
local YELLOW = "YELLOW"
local RED = "RED"

local function Trim(value)
	return type(value) == "string" and value:gsub("^%s+", ""):gsub("%s+$", "") or ""
end

local function NormalizePlayerName(name)
	return Trim(tostring(name or "")):lower():gsub("%s+", "")
end

local function PlayerBaseName(name)
	return NormalizePlayerName(name):match("^([^-]+)") or ""
end

local function NamesMatch(left, right)
	local normalizedLeft, normalizedRight = NormalizePlayerName(left), NormalizePlayerName(right)
	if normalizedLeft == "" or normalizedRight == "" then return false end
	if normalizedLeft == normalizedRight then return true end
	return PlayerBaseName(normalizedLeft) == PlayerBaseName(normalizedRight)
		and (not normalizedLeft:find("-", 1, true) or not normalizedRight:find("-", 1, true))
end

local function GetLocalPlayerName()
	if GetUnitName then
		local name = GetUnitName("player", true)
		if name and name ~= "" then return name end
	end
	if UnitFullName then
		local name, realm = UnitFullName("player")
		if name and realm and realm ~= "" then return name .. "-" .. realm end
		if name then return name end
	end
	return UnitName and UnitName("player") or "player"
end

local function OtherColor(color)
	return color == YELLOW and RED or YELLOW
end

local function Index(row, column)
	return ((row - 1) * 7) + column
end

local function HashString(value)
	local hash = 17
	for index = 1, #value do hash = ((hash * 131) + string.byte(value, index)) % 2147483647 end
	return hash
end

local function SerializeBoard(board)
	local values = {}
	for index = 1, 42 do
		values[index] = board[index] == YELLOW and "Y" or board[index] == RED and "R" or "."
	end
	return table.concat(values)
end

local function DeserializeBoard(value)
	if type(value) ~= "string" or #value ~= 42 or value:find("[^%.YR]") then return nil end
	local board = {}
	for index = 1, 42 do
		local piece = value:sub(index, index)
		if piece == "Y" then board[index] = YELLOW
		elseif piece == "R" then board[index] = RED end
	end
	return board
end

local function StateChecksum(game)
	return HashString(table.concat({
		SerializeBoard(game.board), game.turn or "", tostring(game.moveNumber or 0),
		game.status or "ACTIVE", game.winner or "",
	}, ":"))
end

local function FindWinningCells(board, color)
	local cells, seen = {}, {}
	for row = 1, 6 do
		for column = 1, 7 do
			if board[Index(row, column)] == color then
				for _, direction in ipairs({ { 0, 1 }, { 1, 0 }, { 1, 1 }, { 1, -1 } }) do
					local matched = true
					for offset = 1, 3 do
						local nextRow = row + (direction[1] * offset)
						local nextColumn = column + (direction[2] * offset)
						if nextRow < 1 or nextRow > 6 or nextColumn < 1 or nextColumn > 7
							or board[Index(nextRow, nextColumn)] ~= color then
							matched = false
							break
						end
					end
					if matched then
						for offset = 0, 3 do
							local cellIndex = Index(row + (direction[1] * offset), column + (direction[2] * offset))
							if not seen[cellIndex] then
								seen[cellIndex] = true
								table.insert(cells, cellIndex)
							end
						end
					end
				end
			end
		end
	end
	return cells
end

local function ApplyDrop(game, column, color)
	column = math.floor(tonumber(column) or 0)
	if game.status ~= "ACTIVE" then return nil, "That Connect Four game has ended." end
	if color ~= game.turn then return nil, "It is not that player's turn." end
	if column < 1 or column > 7 then return nil, "Choose a column from 1 to 7." end
	local row
	for candidate = 1, 6 do
		if not game.board[Index(candidate, column)] then row = candidate break end
	end
	if not row then return nil, "That column is full." end
	game.board[Index(row, column)] = color
	game.moveNumber = (game.moveNumber or 0) + 1
	game.lastMove = { row = row, column = column, color = color }
	game.history = game.history or {}
	table.insert(game.history, game.lastMove)
	local winningCells = FindWinningCells(game.board, color)
	if #winningCells > 0 then
		game.status = "FINISHED"
		game.winner = color
		game.finishReason = "connected four"
		game.winningCells = winningCells
	elseif game.moveNumber >= 42 then
		game.status = "FINISHED"
		game.winner = nil
		game.finishReason = "board full"
		game.winningCells = nil
	else
		game.winningCells = nil
		game.turn = OtherColor(color)
	end
	return game.lastMove
end

local function CreateGame(gameID, challenger, opponent, profileKey)
	return {
		id = gameID, challenger = challenger, opponent = opponent,
		yellowPlayer = challenger, redPlayer = opponent, profileKey = profileKey,
		board = {}, turn = YELLOW, moveNumber = 0, status = "ACTIVE", history = {},
		startedAt = GetServerTime and GetServerTime() or 0,
	}
end

local function SnapshotGame(game)
	local snapshot = {
		board = {}, turn = game.turn, moveNumber = game.moveNumber, status = game.status,
		winner = game.winner, finishReason = game.finishReason, lastMove = game.lastMove,
		historyLength = #(game.history or {}), winningCells = game.winningCells and {} or nil,
	}
	for index = 1, 42 do snapshot.board[index] = game.board[index] end
	for index, cellIndex in ipairs(game.winningCells or {}) do snapshot.winningCells[index] = cellIndex end
	return snapshot
end

local function RestoreGame(game, snapshot)
	game.board, game.turn, game.moveNumber = snapshot.board, snapshot.turn, snapshot.moveNumber
	game.status, game.winner, game.finishReason = snapshot.status, snapshot.winner, snapshot.finishReason
	game.lastMove, game.winningCells = snapshot.lastMove, snapshot.winningCells
	while #(game.history or {}) > snapshot.historyLength do table.remove(game.history) end
end

local function CreateGameID()
	local serverTime = GetServerTime and GetServerTime() or math.floor(GetTime and GetTime() or 0)
	return string.format("f%x%x", serverTime, math.random(0, 0xFFFF))
end

local function IsValidSavedGame(game, profileKey)
	if type(game) ~= "table" or type(game.id) ~= "string" or not game.id:match("^[%w%-]+$")
		or game.profileKey ~= profileKey or type(game.board) ~= "table"
		or not (game.status == "ACTIVE" or game.status == "FINISHED")
		or not (game.turn == YELLOW or game.turn == RED)
		or not NamesMatch(game.yellowPlayer, GetLocalPlayerName())
			and not NamesMatch(game.redPlayer, GetLocalPlayerName()) then return false end
	for index = 1, 42 do
		if game.board[index] ~= nil and game.board[index] ~= YELLOW and game.board[index] ~= RED then return false end
	end
	game.moveNumber = math.max(0, math.floor(tonumber(game.moveNumber) or 0))
	game.history = type(game.history) == "table" and game.history or {}
	game.winningCells = game.status == "FINISHED" and game.winner and FindWinningCells(game.board, game.winner) or nil
	return true
end

function GuildPing:GetLocalConnectFourPlayerName() return GetLocalPlayerName() end

function GuildPing:GetLocalConnectFourColor(game)
	game = game or self.activeConnectFourGame
	if not game then return nil end
	if NamesMatch(game.yellowPlayer, GetLocalPlayerName()) then return YELLOW end
	if NamesMatch(game.redPlayer, GetLocalPlayerName()) then return RED end
	return nil
end

function GuildPing:GetConnectFourControllableColor(game)
	game = game or self.activeConnectFourGame
	return game and game.practice and game.turn or self:GetLocalConnectFourColor(game)
end

function GuildPing:GetConnectFourWinningCells(game)
	game = game or self.activeConnectFourGame
	if not game or game.status ~= "FINISHED" or not game.winner then return {} end
	return FindWinningCells(game.board, game.winner)
end

function GuildPing:GetConnectFourOpponent(game)
	game = game or self.activeConnectFourGame
	if not game then return nil end
	if game.practice then return "Practice board" end
	local color = self:GetLocalConnectFourColor(game)
	return color == YELLOW and game.redPlayer or color == RED and game.yellowPlayer or nil
end

function GuildPing:IsConnectFourGameForCurrentGuild(game)
	game = game or self.activeConnectFourGame
	local _, profileKey = self:GetProfile()
	return game and game.profileKey == profileKey or false
end

function GuildPing:RefreshConnectFourUI()
	local game = self.activeConnectFourGame
	if game then
		local localColor = self:GetLocalConnectFourColor(game)
		if game.status == "FINISHED" then
			local result = game.winner == nil and "DRAW" or game.winner == localColor and "WIN" or "LOSS"
			self:RecordGameResult("connectFour", game, result, self:GetConnectFourOpponent(game), {
				reason = game.finishReason or "game complete", resigned = game.finishReason == "resignation",
			})
		end
		self:PersistGameState("connectFour", game)
		if localColor and game.turn == localColor then
			self:NotifyGameTurn("connectFour", game, tostring(game.moveNumber) .. ":" .. game.turn,
				"Your turn in Guild Ping Connect Four",
				self.connectFourBoard and self.connectFourBoard:IsShown())
		end
	end
	if self.RefreshConnectFourBoard and self.connectFourBoard and self.connectFourBoard:IsShown() then self:RefreshConnectFourBoard() end
	if self.RefreshConnectFourLobby and self.connectFourLobby and self.connectFourLobby:IsShown() then self:RefreshConnectFourLobby() end
	if self.RefreshGamesIndicators then self:RefreshGamesIndicators() end
end

function GuildPing:SendConnectFourMessage(message)
	if not self.connectFourInitialized then return nil, "Connect Four messaging is not available." end
	if not IsInGuild or not IsInGuild() then return nil, "You must be in a guild to play Connect Four." end
	local succeeded, result = pcall(C_ChatInfo.SendAddonMessage, MESSAGE_PREFIX, message, "GUILD")
	if not succeeded or result == false then return nil, "WoW could not send the Connect Four update." end
	return true
end

function GuildPing:ResolveConnectFourGuildMember(input)
	input = Trim(input):gsub("^@", "")
	if input == "" then return nil, "Enter the character name of a guild member." end
	local normalizedInput, inputBase = NormalizePlayerName(input), PlayerBaseName(input)
	local baseMatches = {}
	for index = 1, tonumber(GetNumGuildMembers and GetNumGuildMembers() or 0) do
		local memberName, _, _, _, _, _, _, _, online = GetGuildRosterInfo(index)
		if memberName then
			if NormalizePlayerName(memberName) == normalizedInput then
				if online == false then return nil, memberName .. " is currently offline." end
				return memberName
			elseif PlayerBaseName(memberName) == inputBase then
				table.insert(baseMatches, { name = memberName, online = online ~= false })
			end
		end
	end
	if #baseMatches == 1 then
		return baseMatches[1].online and baseMatches[1].name or nil,
			baseMatches[1].online and nil or (baseMatches[1].name .. " is currently offline.")
	end
	if #baseMatches > 1 then return nil, "More than one guild member has that name. Include the realm name." end
	return nil, "That character was not found in the current guild roster."
end

function GuildPing:CleanupConnectFourInvites()
	local currentTime = GetTime and GetTime() or 0
	self.connectFourInvites = self.connectFourInvites or {}
	for index = #self.connectFourInvites, 1, -1 do
		if currentTime - (self.connectFourInvites[index].receivedAt or 0) > CHALLENGE_TIMEOUT then table.remove(self.connectFourInvites, index) end
	end
	if self.outgoingConnectFourChallenge and currentTime - (self.outgoingConnectFourChallenge.sentAt or 0) > CHALLENGE_TIMEOUT then
		self.outgoingConnectFourChallenge = nil
	end
end

function GuildPing:GetConnectFourInvites() self:CleanupConnectFourInvites() return self.connectFourInvites end
function GuildPing:GetConnectFourInviteCount() return #self:GetConnectFourInvites() end

function GuildPing:StartConnectFourPractice()
	local localName = GetLocalPlayerName()
	local _, profileKey = self:GetProfile()
	local game = CreateGame(CreateGameID(), localName, localName, profileKey)
	game.practice = true
	self.activeConnectFourGame = game
	self.outgoingConnectFourChallenge = nil
	self:RefreshConnectFourUI()
	return game
end

function GuildPing:ChallengeConnectFourPlayer(targetInput)
	self:CleanupConnectFourInvites()
	if self.activeConnectFourGame and self.activeConnectFourGame.status == "ACTIVE" then return nil, "Finish your current Connect Four game first." end
	if self.outgoingConnectFourChallenge then return nil, "You already have a Connect Four challenge waiting." end
	local requested = Trim(targetInput):gsub("^@", "")
	if NamesMatch(requested, GetLocalPlayerName()) then
		local game = self:StartConnectFourPractice()
		return { practice = true, game = game, target = GetLocalPlayerName() }
	end
	local target, errorMessage = self:ResolveConnectFourGuildMember(targetInput)
	if not target then return nil, errorMessage end
	local gameID = CreateGameID()
	local sent, sendError = self:SendConnectFourMessage(table.concat({ "I", PROTOCOL_VERSION, gameID, target }, "|"))
	if not sent then return nil, sendError end
	self.outgoingConnectFourChallenge = { id = gameID, target = target, sentAt = GetTime and GetTime() or 0 }
	self:RefreshConnectFourUI()
	return self.outgoingConnectFourChallenge
end

function GuildPing:PlayConnectFourAgain()
	local game = self.activeConnectFourGame
	if not game or game.status ~= "FINISHED" then return nil, "Finish the current Connect Four game first." end
	return self:ChallengeConnectFourPlayer(game.practice and GetLocalPlayerName() or self:GetConnectFourOpponent(game))
end

function GuildPing:GetConnectFourInvite(inviteID)
	for index, invite in ipairs(self:GetConnectFourInvites()) do if invite.id == inviteID then return invite, index end end
end

function GuildPing:AcceptConnectFourChallenge(inviteID)
	if self.activeConnectFourGame and self.activeConnectFourGame.status == "ACTIVE" then return nil, "Finish your current Connect Four game first." end
	local invite, index = self:GetConnectFourInvite(inviteID)
	if not invite then return nil, "That Connect Four challenge is no longer available." end
	local sent, errorMessage = self:SendConnectFourMessage(table.concat({ "A", PROTOCOL_VERSION, invite.id, invite.sender }, "|"))
	if not sent then return nil, errorMessage end
	local _, profileKey = self:GetProfile()
	self.activeConnectFourGame = CreateGame(invite.id, invite.sender, GetLocalPlayerName(), profileKey)
	table.remove(self.connectFourInvites, index)
	self:RefreshConnectFourUI()
	return self.activeConnectFourGame
end

function GuildPing:DeclineConnectFourChallenge(inviteID)
	local invite, index = self:GetConnectFourInvite(inviteID)
	if not invite then return false end
	self:SendConnectFourMessage(table.concat({ "D", PROTOCOL_VERSION, invite.id, invite.sender }, "|"))
	table.remove(self.connectFourInvites, index)
	self:RefreshConnectFourUI()
	return true
end

function GuildPing:PlayConnectFourColumn(column)
	local game = self.activeConnectFourGame
	if not game or game.status ~= "ACTIVE" then return nil, "There is no active Connect Four game." end
	if not self:IsConnectFourGameForCurrentGuild(game) then return nil, "This game belongs to a different guild." end
	if self:IsGameWaitingForResume(game) then return nil, "Resume this Connect Four game before playing." end
	local color = self:GetConnectFourControllableColor(game)
	if not color or color ~= game.turn then return nil, "Wait for your opponent's move." end
	local snapshot = SnapshotGame(game)
	local move, errorMessage = ApplyDrop(game, column, color)
	if not move then return nil, errorMessage end
	if not game.practice then
		local sent, sendError = self:SendConnectFourMessage(table.concat({
			"M", PROTOCOL_VERSION, game.id, self:GetConnectFourOpponent(game), tostring(game.moveNumber),
			tostring(column), tostring(StateChecksum(game)),
		}, "|"))
		if not sent then
			RestoreGame(game, snapshot)
			return nil, sendError
		end
	end
	if self.AnimateConnectFourMove then self:AnimateConnectFourMove(game, move) end
	self:RefreshConnectFourUI()
	return move
end

function GuildPing:RequestConnectFourSync(game, target)
	game = game or self.activeConnectFourGame
	if not game then return nil, "There is no Connect Four game to synchronize." end
	return self:SendConnectFourMessage(table.concat({ "Q", PROTOCOL_VERSION, game.id, target or self:GetConnectFourOpponent(game) }, "|"))
end

function GuildPing:SendConnectFourSync(game, target)
	return self:SendConnectFourMessage(table.concat({
		"S", PROTOCOL_VERSION, game.id, target, tostring(game.moveNumber), game.turn == YELLOW and "Y" or "R",
		game.status == "FINISHED" and "F" or "A", game.winner == YELLOW and "Y" or game.winner == RED and "R" or "N",
		SerializeBoard(game.board), tostring(StateChecksum(game)),
	}, "|"))
end

function GuildPing:ResumeConnectFourGame(delay)
	local game = self.activeConnectFourGame
	if not game then return nil, "There is no saved Connect Four game to resume." end
	if game.status ~= "ACTIVE" or game.practice then
		game.persistenceState = nil self:PersistGameState("connectFour", game) self:RefreshConnectFourUI() return true
	end
	local opponent = self:GetConnectFourOpponent(game)
	self:BeginGameReconnect("connectFour", game, function()
		local sent, errorMessage = self:RequestConnectFourSync(game, opponent)
		if not sent then
			game.persistenceState = "STALE"
			self:PersistGameState("connectFour", game)
			if errorMessage then self:Print(errorMessage) end
			self:RefreshConnectFourUI()
		end
	end, delay)
	self:RefreshConnectFourUI()
	return true
end

function GuildPing:AbandonConnectFourGame()
	local game = self.activeConnectFourGame
	if not game then return nil, "There is no Connect Four game to abandon." end
	if game.status == "ACTIVE" and not game.practice then
		self:SendConnectFourMessage(table.concat({ "R", PROTOCOL_VERSION, game.id, self:GetConnectFourOpponent(game) }, "|"))
		game.status, game.winner, game.finishReason = "FINISHED", OtherColor(self:GetLocalConnectFourColor(game)), "resignation"
		self:RecordGameResult("connectFour", game, "LOSS", self:GetConnectFourOpponent(game), { reason = "abandoned", resigned = true })
	end
	self.activeConnectFourGame = nil
	self:ClearSavedGameState("connectFour")
	self:RefreshConnectFourUI()
	return true
end

function GuildPing:ResignConnectFourGame()
	local game = self.activeConnectFourGame
	if not game or game.status ~= "ACTIVE" then return nil, "There is no active Connect Four game." end
	if game.practice then
		game.status, game.winner, game.finishReason = "FINISHED", nil, "practice ended"
	else
		local sent, errorMessage = self:SendConnectFourMessage(table.concat({ "R", PROTOCOL_VERSION, game.id, self:GetConnectFourOpponent(game) }, "|"))
		if not sent then return nil, errorMessage end
		game.status, game.winner, game.finishReason = "FINISHED", OtherColor(self:GetLocalConnectFourColor(game)), "resignation"
	end
	self:RefreshConnectFourUI()
	return true
end

function GuildPing:HandleConnectFourMessage(prefix, message, channel, sender)
	if not self.connectFourInitialized or prefix ~= MESSAGE_PREFIX or channel ~= "GUILD" or type(message) ~= "string"
		or type(sender) ~= "string" or NamesMatch(sender, GetLocalPlayerName()) then return end
	local messageType, protocol = message:match("^([^|]+)|([^|]+)|")
	if protocol ~= PROTOCOL_VERSION then return end
	local localName = GetLocalPlayerName()
	self:CleanupConnectFourInvites()
	if messageType == "I" then
		local gameID, target = message:match("^I|1|([%w%-]+)|([^|]+)$")
		if not gameID or not NamesMatch(target, localName) then return end
		for _, invite in ipairs(self.connectFourInvites) do if invite.id == gameID then return end end
		if #self.connectFourInvites >= MAX_INVITES then table.remove(self.connectFourInvites, 1) end
		table.insert(self.connectFourInvites, { id = gameID, sender = sender, receivedAt = GetTime and GetTime() or 0 })
		self:Print(sender .. " challenged you to Connect Four. Open /gp games to respond.")
		self:RefreshConnectFourUI()
	elseif messageType == "A" then
		local gameID, target = message:match("^A|1|([%w%-]+)|([^|]+)$")
		local outgoing = self.outgoingConnectFourChallenge
		if not gameID or not NamesMatch(target, localName) or not outgoing or outgoing.id ~= gameID or not NamesMatch(outgoing.target, sender) then return end
		local _, profileKey = self:GetProfile()
		self.activeConnectFourGame = CreateGame(gameID, localName, sender, profileKey)
		self.outgoingConnectFourChallenge = nil
		self:Print(sender .. " accepted your Connect Four challenge. You play Yellow and move first.")
		self:RefreshConnectFourUI()
	elseif messageType == "D" then
		local gameID, target = message:match("^D|1|([%w%-]+)|([^|]+)$")
		if self.outgoingConnectFourChallenge and gameID == self.outgoingConnectFourChallenge.id and NamesMatch(target, localName) then
			self.outgoingConnectFourChallenge = nil self:Print(sender .. " declined your Connect Four challenge.") self:RefreshConnectFourUI()
		end
	elseif messageType == "M" then
		local gameID, target, sequence, column, checksum = message:match("^M|1|([%w%-]+)|([^|]+)|(%d+)|(%d+)|(%d+)$")
		local game = self.activeConnectFourGame
		sequence, column, checksum = tonumber(sequence), tonumber(column), tonumber(checksum)
		if not gameID or not game or game.id ~= gameID or not NamesMatch(target, localName)
			or not NamesMatch(sender, self:GetConnectFourOpponent(game)) or sequence ~= game.moveNumber + 1 then return end
		self:CompleteGameReconnect("connectFour", game)
		local senderColor = NamesMatch(sender, game.yellowPlayer) and YELLOW or RED
		local snapshot = SnapshotGame(game)
		local move = ApplyDrop(game, column, senderColor)
		if not move or StateChecksum(game) ~= checksum then
			RestoreGame(game, snapshot)
			self:Print("Connect Four state mismatch; requesting synchronization.")
			self:RequestConnectFourSync(game, sender)
			return
		end
		if self.AnimateConnectFourMove then self:AnimateConnectFourMove(game, move) end
		self:RefreshConnectFourUI()
	elseif messageType == "Q" then
		local gameID, target = message:match("^Q|1|([%w%-]+)|([^|]+)$")
		local game = self.activeConnectFourGame
		if gameID and game and game.id == gameID and NamesMatch(target, localName) and NamesMatch(sender, self:GetConnectFourOpponent(game)) then
			self:CompleteGameReconnect("connectFour", game) self:SendConnectFourSync(game, sender)
		end
	elseif messageType == "S" then
		local gameID, target, sequence, turnCode, statusCode, winnerCode, boardValue, checksum = message:match(
			"^S|1|([%w%-]+)|([^|]+)|(%d+)|([YR])|([AF])|([YRN])|([%.YR]+)|(%d+)$")
		local game = self.activeConnectFourGame
		sequence, checksum = tonumber(sequence), tonumber(checksum)
		local board = DeserializeBoard(boardValue)
		if not gameID or not game or game.id ~= gameID or not board or not NamesMatch(target, localName)
			or not NamesMatch(sender, self:GetConnectFourOpponent(game)) or sequence < game.moveNumber then return end
		local synchronized = { board = board, moveNumber = sequence, turn = turnCode == "Y" and YELLOW or RED,
			status = statusCode == "F" and "FINISHED" or "ACTIVE", winner = winnerCode == "Y" and YELLOW or winnerCode == "R" and RED or nil }
		if StateChecksum(synchronized) ~= checksum then return end
		game.board, game.moveNumber, game.turn, game.status, game.winner = board, sequence, synchronized.turn, synchronized.status, synchronized.winner
		game.winningCells = game.status == "FINISHED" and game.winner and FindWinningCells(game.board, game.winner) or nil
		game.finishReason = game.winner and "connected four" or game.status == "FINISHED" and "board full" or nil
		game.history, game.lastMove = {}, nil
		self:CompleteGameReconnect("connectFour", game)
		self:Print("The Connect Four board was synchronized with " .. sender .. ".")
		self:RefreshConnectFourUI()
	elseif messageType == "R" then
		local gameID, target = message:match("^R|1|([%w%-]+)|([^|]+)$")
		local game = self.activeConnectFourGame
		if gameID and game and game.id == gameID and game.status == "ACTIVE" and NamesMatch(target, localName)
			and NamesMatch(sender, self:GetConnectFourOpponent(game)) then
			self:CompleteGameReconnect("connectFour", game)
			game.status, game.winner, game.finishReason = "FINISHED", self:GetLocalConnectFourColor(game), "opponent resigned"
			self:RefreshConnectFourUI()
		end
	end
end

function GuildPing:InitializeConnectFour()
	if self.connectFourInitialized or not C_ChatInfo or not C_ChatInfo.RegisterAddonMessagePrefix or not C_ChatInfo.SendAddonMessage then return end
	if not C_ChatInfo.RegisterAddonMessagePrefix(MESSAGE_PREFIX) then return end
	self.connectFourInitialized = true
	self.connectFourInvites = self.connectFourInvites or {}
	local restored, reason = self:RestoreSavedGameState("connectFour")
	local _, profileKey = self:GetProfile()
	if restored and IsValidSavedGame(restored, profileKey) then
		self.activeConnectFourGame = restored
		if restored.status == "ACTIVE" then
			if restored.practice then restored.persistenceState = "RESTORED" self:PersistGameState("connectFour", restored)
			else self:ResumeConnectFourGame(1.75) end
		end
	elseif restored then self:ClearSavedGameState("connectFour")
	elseif reason == "EXPIRED" then self:Print("An abandoned saved Connect Four game expired and was removed.") end
	if C_GuildInfo and C_GuildInfo.GuildRoster then C_GuildInfo.GuildRoster() end
end

GuildPing.CONNECT_FOUR_YELLOW = YELLOW
GuildPing.CONNECT_FOUR_RED = RED
