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

local MESSAGE_PREFIX = "GuildPingGame"
local PROTOCOL_VERSION = "1"
local CHALLENGE_TIMEOUT = 300
local MAX_INVITES = 3
local BLACK = "BLACK"
local RED = "RED"

local function Trim(value)
	if type(value) ~= "string" then
		return ""
	end
	return (value:gsub("^%s+", ""):gsub("%s+$", ""))
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 = NormalizePlayerName(left)
	local normalizedRight = NormalizePlayerName(right)
	if normalizedLeft == "" or normalizedRight == "" then
		return false
	end
	if normalizedLeft == normalizedRight then
		return true
	end
	local leftHasRealm = normalizedLeft:find("-", 1, true) ~= nil
	local rightHasRealm = normalizedRight:find("-", 1, true) ~= nil
	return (not leftHasRealm or not rightHasRealm)
		and PlayerBaseName(normalizedLeft) == PlayerBaseName(normalizedRight)
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 == BLACK and RED or BLACK
end

local function PieceColor(piece)
	if piece == "b" or piece == "B" then
		return BLACK
	elseif piece == "r" or piece == "R" then
		return RED
	end
	return nil
end

local function IsKing(piece)
	return piece == "B" or piece == "R"
end

local function IsBoardPosition(row, column)
	return row >= 1 and row <= 8 and column >= 1 and column <= 8
end

local function PositionIndex(row, column)
	return ((row - 1) * 8) + column
end

local function PositionCoordinates(index)
	index = tonumber(index)
	if not index or index < 1 or index > 64 then
		return nil
	end
	return math.floor((index - 1) / 8) + 1, ((index - 1) % 8) + 1
end

local function IsPlayableSquare(row, column)
	return IsBoardPosition(row, column) and (row + column) % 2 == 1
end

local function CopyBoard(board)
	local copy = {}
	for index = 1, 64 do
		copy[index] = board[index]
	end
	return copy
end

local function CreateInitialBoard()
	local board = {}
	for row = 1, 8 do
		for column = 1, 8 do
			if IsPlayableSquare(row, column) then
				local index = PositionIndex(row, column)
				if row <= 3 then
					board[index] = "b"
				elseif row >= 6 then
					board[index] = "r"
				end
			end
		end
	end
	return board
end

local function GetDirections(piece)
	if IsKing(piece) then
		return { { 1, -1 }, { 1, 1 }, { -1, -1 }, { -1, 1 } }
	end
	if PieceColor(piece) == BLACK then
		return { { 1, -1 }, { 1, 1 } }
	end
	return { { -1, -1 }, { -1, 1 } }
end

local function GetCapturesForPiece(board, fromIndex)
	local piece = board[fromIndex]
	local color = PieceColor(piece)
	local row, column = PositionCoordinates(fromIndex)
	local captures = {}
	if not color or not row then
		return captures
	end

	for _, direction in ipairs(GetDirections(piece)) do
		local middleRow = row + direction[1]
		local middleColumn = column + direction[2]
		local targetRow = row + (direction[1] * 2)
		local targetColumn = column + (direction[2] * 2)
		if IsPlayableSquare(targetRow, targetColumn) and IsBoardPosition(middleRow, middleColumn) then
			local middleIndex = PositionIndex(middleRow, middleColumn)
			local targetIndex = PositionIndex(targetRow, targetColumn)
			local middlePiece = board[middleIndex]
			if middlePiece and PieceColor(middlePiece) == OtherColor(color) and not board[targetIndex] then
				table.insert(captures, {
					from = fromIndex,
					to = targetIndex,
					capture = middleIndex,
				})
			end
		end
	end
	return captures
end

local function GetSimpleMovesForPiece(board, fromIndex)
	local piece = board[fromIndex]
	local row, column = PositionCoordinates(fromIndex)
	local moves = {}
	if not PieceColor(piece) or not row then
		return moves
	end

	for _, direction in ipairs(GetDirections(piece)) do
		local targetRow = row + direction[1]
		local targetColumn = column + direction[2]
		if IsPlayableSquare(targetRow, targetColumn) then
			local targetIndex = PositionIndex(targetRow, targetColumn)
			if not board[targetIndex] then
				table.insert(moves, { from = fromIndex, to = targetIndex })
			end
		end
	end
	return moves
end

local function GetLegalMoves(game, color)
	local board = game.board
	local captures = {}
	local simpleMoves = {}
	local firstIndex = 1
	local lastIndex = 64
	if game.forcedPiece and game.turn == color then
		firstIndex = game.forcedPiece
		lastIndex = game.forcedPiece
	end

	for index = firstIndex, lastIndex do
		if PieceColor(board[index]) == color then
			for _, move in ipairs(GetCapturesForPiece(board, index)) do
				table.insert(captures, move)
			end
			if not game.forcedPiece then
				for _, move in ipairs(GetSimpleMovesForPiece(board, index)) do
					table.insert(simpleMoves, move)
				end
			end
		end
	end

	local moves = {}
	for _, move in ipairs(captures) do
		table.insert(moves, move)
	end
	for _, move in ipairs(simpleMoves) do
		table.insert(moves, move)
	end
	table.sort(moves, function(left, right)
		if left.from == right.from then
			return left.to < right.to
		end
		return left.from < right.from
	end)
	return moves
end

local function FindLegalMove(game, color, fromIndex, toIndex)
	for _, move in ipairs(GetLegalMoves(game, color)) do
		if move.from == fromIndex and move.to == toIndex then
			return move
		end
	end
	return nil
end

local function CountPieces(board, color)
	local count = 0
	for index = 1, 64 do
		if PieceColor(board[index]) == color then
			count = count + 1
		end
	end
	return count
end

local function SerializeBoard(board)
	local parts = {}
	for index = 1, 64 do
		parts[index] = board[index] or "."
	end
	return table.concat(parts)
end

local function DeserializeBoard(value)
	if type(value) ~= "string" or #value ~= 64 or value:find("[^%.bBrR]") then
		return nil
	end
	local board = {}
	for index = 1, 64 do
		local piece = value:sub(index, index)
		if piece ~= "." then
			local row, column = PositionCoordinates(index)
			if not IsPlayableSquare(row, column) then
				return nil
			end
			board[index] = piece
		end
	end
	return board
end

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

local function StateChecksum(game)
	local value = GameStateString(game)
	local checksum = 0
	for index = 1, #value do
		checksum = (checksum + (string.byte(value, index) * index)) % 1000000007
	end
	return checksum
end

local function SnapshotGame(game)
	return {
		board = CopyBoard(game.board),
		turn = game.turn,
		forcedPiece = game.forcedPiece,
		moveNumber = game.moveNumber,
		status = game.status,
		winner = game.winner,
		finishReason = game.finishReason,
		lastMove = game.lastMove,
		historyLength = #(game.history or {}),
	}
end

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

local function ApplyMove(game, fromIndex, toIndex, color)
	fromIndex = tonumber(fromIndex)
	toIndex = tonumber(toIndex)
	if game.status ~= "ACTIVE" then
		return nil, "That checkers game has already ended."
	end
	if color ~= game.turn then
		return nil, "It is not that player's turn."
	end

	local move = FindLegalMove(game, color, fromIndex, toIndex)
	if not move then
		return nil, "That is not a legal checkers move."
	end

	local piece = game.board[fromIndex]
	game.board[fromIndex] = nil
	if move.capture then
		game.board[move.capture] = nil
	end

	local targetRow = PositionCoordinates(toIndex)
	local promoted = false
	if piece == "b" and targetRow == 8 then
		piece = "B"
		promoted = true
	elseif piece == "r" and targetRow == 1 then
		piece = "R"
		promoted = true
	end
	game.board[toIndex] = piece
	game.moveNumber = (game.moveNumber or 0) + 1
	game.lastMove = { from = fromIndex, to = toIndex, capture = move.capture }
	game.history = game.history or {}
	table.insert(game.history, game.lastMove)

	local opponentColor = OtherColor(color)
	if CountPieces(game.board, opponentColor) == 0 then
		game.status = "FINISHED"
		game.winner = color
		game.finishReason = "all pieces captured"
		game.forcedPiece = nil
		return move
	end

	if move.capture and not promoted and #GetCapturesForPiece(game.board, toIndex) > 0 then
		game.forcedPiece = toIndex
	else
		game.forcedPiece = nil
		game.turn = opponentColor
		if #GetLegalMoves(game, opponentColor) == 0 then
			game.status = "FINISHED"
			game.winner = color
			game.finishReason = "opponent has no legal moves"
		end
	end
	return move
end

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

local function IsValidSavedCheckersGame(game, profileKey)
	if type(game) ~= "table" or type(game.id) ~= "string" or not game.id:match("^[%w%-]+$")
		or type(game.board) ~= "table" or game.profileKey ~= profileKey
		or not (game.status == "ACTIVE" or game.status == "FINISHED")
		or not (game.turn == BLACK or game.turn == RED)
		or not NamesMatch(game.blackPlayer, GetLocalPlayerName())
			and not NamesMatch(game.redPlayer, GetLocalPlayerName()) then
		return false
	end
	for index = 1, 64 do
		local piece = game.board[index]
		if piece ~= nil and piece ~= "b" and piece ~= "B" and piece ~= "r" and piece ~= "R" 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 {}
	return true
end

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

function GuildPing:GetLocalCheckersPlayerName()
	return GetLocalPlayerName()
end

function GuildPing:GetCheckersPlayerColor(game, playerName)
	game = game or self.activeCheckersGame
	if not game then
		return nil
	end
	if NamesMatch(game.blackPlayer, playerName) then
		return BLACK
	elseif NamesMatch(game.redPlayer, playerName) then
		return RED
	end
	return nil
end

function GuildPing:GetLocalCheckersColor(game)
	return self:GetCheckersPlayerColor(game, GetLocalPlayerName())
end

function GuildPing:GetCheckersControllableColor(game)
	game = game or self.activeCheckersGame
	if not game then
		return nil
	end
	if game.practice then
		return game.turn
	end
	return self:GetLocalCheckersColor(game)
end

function GuildPing:IsCheckersGameForCurrentGuild(game)
	game = game or self.activeCheckersGame
	if not game then
		return false
	end
	local _, profileKey = self:GetProfile()
	return game.profileKey == profileKey
end

function GuildPing:GetCheckersOpponent(game)
	game = game or self.activeCheckersGame
	if game and game.practice then
		return "Practice board"
	end
	local localColor = self:GetLocalCheckersColor(game)
	if not game or not localColor then
		return nil
	end
	return localColor == BLACK and game.redPlayer or game.blackPlayer
end

function GuildPing:GetCheckersLegalMoves(game, color)
	game = game or self.activeCheckersGame
	if not game then
		return {}
	end
	return GetLegalMoves(game, color or game.turn)
end

function GuildPing:GetCheckersStateChecksum(game)
	return StateChecksum(game or self.activeCheckersGame)
end

function GuildPing:ApplyCheckersMove(game, fromIndex, toIndex, color)
	return ApplyMove(game, fromIndex, toIndex, color)
end

function GuildPing:RefreshCheckersUI()
	if self.activeCheckersGame then
		local game = self.activeCheckersGame
		local localColor = self:GetLocalCheckersColor(game)
		if game.status == "FINISHED" then
			local result = game.winner == nil and "DRAW" or game.winner == localColor and "WIN" or "LOSS"
			self:RecordGameResult("checkers", game, result, self:GetCheckersOpponent(game), {
				reason = game.finishReason or "game complete",
				resigned = game.finishReason == "resignation",
			})
		end
		self:PersistGameState("checkers", game)
		if self.MaybeBroadcastSpectatableGames and not game.practice and game.status == "ACTIVE"
			and localColor == self.CHECKERS_BLACK then self:MaybeBroadcastSpectatableGames() end
		if self.PublishSpectatorState then self:PublishSpectatorState("C", game) end
		if localColor and game.turn == localColor then
			self:NotifyGameTurn(
				"checkers",
				game,
				tostring(game.moveNumber or 0) .. ":" .. tostring(game.turn),
				"Your turn in Guild Ping Checkers",
				self.checkersBoard and self.checkersBoard:IsShown()
			)
		end
	end
	if self.RefreshCheckersBoard and self.checkersBoard and self.checkersBoard:IsShown() then
		self:RefreshCheckersBoard()
	end
	if self.RefreshCheckersLobby and self.checkersLobby and self.checkersLobby:IsShown() then
		self:RefreshCheckersLobby()
	end
	if self.RefreshCheckersIndicators then
		self:RefreshCheckersIndicators()
	end
end

function GuildPing:SendCheckersAddonMessage(message)
	if not self.checkersInitialized then
		return nil, "Checkers messaging is not available."
	end
	if not IsInGuild or not IsInGuild() then
		return nil, "You must be in a guild to play Guild Ping Checkers."
	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 checkers message."
	end
	return true
end

function GuildPing:ResolveCheckersGuildMember(input)
	input = Trim(input):gsub("^@", "")
	if input == "" then
		return nil, "Enter the character name of a guild member."
	end
	if not GetNumGuildMembers or not GetGuildRosterInfo then
		return nil, "The guild roster is not available yet."
	end

	local normalizedInput = NormalizePlayerName(input)
	local inputBase = PlayerBaseName(input)
	local baseMatches = {}
	for index = 1, tonumber(GetNumGuildMembers()) or 0 do
		local memberName, _, _, _, _, _, _, _, isOnline = GetGuildRosterInfo(index)
		if memberName then
			local normalizedMember = NormalizePlayerName(memberName)
			if normalizedMember == normalizedInput then
				if isOnline == false then
					return nil, memberName .. " is currently offline."
				end
				return memberName
			elseif PlayerBaseName(memberName) == inputBase then
				table.insert(baseMatches, { name = memberName, online = isOnline ~= false })
			end
		end
	end

	if #baseMatches == 1 then
		if not baseMatches[1].online then
			return nil, baseMatches[1].name .. " is currently offline."
		end
		return baseMatches[1].name
	elseif #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:CleanupCheckersInvites()
	local now = GetTime and GetTime() or 0
	local invites = self.checkersInvites or {}
	for index = #invites, 1, -1 do
		if now - (invites[index].receivedAt or 0) > CHALLENGE_TIMEOUT then
			table.remove(invites, index)
		end
	end
	if self.outgoingCheckersChallenge
		and now - (self.outgoingCheckersChallenge.sentAt or 0) > CHALLENGE_TIMEOUT then
		self.outgoingCheckersChallenge = nil
	end
end

function GuildPing:GetCheckersInvites()
	self:CleanupCheckersInvites()
	self.checkersInvites = self.checkersInvites or {}
	return self.checkersInvites
end

function GuildPing:GetCheckersInviteCount()
	return #self:GetCheckersInvites()
end

function GuildPing:StartCheckersPractice()
	local localName = GetLocalPlayerName()
	local _, profileKey = self:GetProfile()
	local game = CreateGame("practice" .. CreateGameID(), localName, localName, profileKey)
	game.practice = true
	self.activeCheckersGame = game
	self.outgoingCheckersChallenge = nil
	self:RefreshCheckersUI()
	return game
end

function GuildPing:ChallengeCheckersPlayer(targetInput)
	self:CleanupCheckersInvites()
	if self.activeCheckersGame and self.activeCheckersGame.status == "ACTIVE" then
		return nil, "Finish or resign from your current checkers game first."
	end
	if self.outgoingCheckersChallenge then
		return nil, "You already have a checkers challenge waiting for a response."
	end

	local requestedName = Trim(targetInput):gsub("^@", "")
	if NamesMatch(requestedName, GetLocalPlayerName()) then
		local game = self:StartCheckersPractice()
		return {
			practice = true,
			game = game,
			target = GetLocalPlayerName(),
		}
	end

	local target, errorMessage = self:ResolveCheckersGuildMember(targetInput)
	if not target then
		return nil, errorMessage
	end
	if NamesMatch(target, GetLocalPlayerName()) then
		local game = self:StartCheckersPractice()
		return {
			practice = true,
			game = game,
			target = GetLocalPlayerName(),
		}
	end

	local gameID = CreateGameID()
	local sent, sendError = self:SendCheckersAddonMessage(table.concat({
		"I",
		PROTOCOL_VERSION,
		gameID,
		target,
	}, "|"))
	if not sent then
		return nil, sendError
	end

	self.outgoingCheckersChallenge = {
		id = gameID,
		target = target,
		sentAt = GetTime and GetTime() or 0,
	}
	self:RefreshCheckersUI()
	return self.outgoingCheckersChallenge
end

function GuildPing:PlayCheckersAgain()
	local game = self.activeCheckersGame
	if not game or game.status ~= "FINISHED" then
		return nil, "Finish the current checkers game before starting a rematch."
	end
	if not self:IsCheckersGameForCurrentGuild(game) then
		return nil, "This checkers game belongs to a different guild."
	end
	local target = game.practice and self:GetLocalCheckersPlayerName() or self:GetCheckersOpponent(game)
	if not target then return nil, "The previous checkers opponent could not be found." end
	return self:ChallengeCheckersPlayer(target)
end

function GuildPing:ResumeCheckersGame(delay)
	local game = self.activeCheckersGame
	if not game then
		return nil, "There is no saved checkers game to resume."
	end
	if not self:IsCheckersGameForCurrentGuild(game) then
		return nil, "This checkers game belongs to a different guild."
	end
	if game.status ~= "ACTIVE" or game.practice then
		game.persistenceState = nil
		self:PersistGameState("checkers", game)
		self:RefreshCheckersUI()
		return true
	end
	local opponent = self:GetCheckersOpponent(game)
	if not opponent then
		return nil, "The previous checkers opponent could not be found."
	end
	self:BeginGameReconnect("checkers", game, function()
		local sent, errorMessage = self:RequestCheckersSync(game, opponent)
		if not sent and game.persistenceState == "RECONNECTING" then
			game.persistenceState = "STALE"
			self:PersistGameState("checkers", game)
			if errorMessage then self:Print(errorMessage) end
			self:RefreshCheckersUI()
		end
	end, delay)
	self:RefreshCheckersUI()
	return true
end

function GuildPing:AbandonCheckersGame()
	local game = self.activeCheckersGame
	if not game then
		return nil, "There is no checkers game to abandon."
	end
	if game.status == "ACTIVE" and not game.practice then
		self:SendCheckersAddonMessage(table.concat({
			"R", PROTOCOL_VERSION, game.id, self:GetCheckersOpponent(game),
		}, "|"))
		game.status = "FINISHED"
		game.winner = OtherColor(self:GetLocalCheckersColor(game))
		game.finishReason = "resignation"
		self:RecordGameResult("checkers", game, "LOSS", self:GetCheckersOpponent(game), {
			reason = "abandoned",
			resigned = true,
		})
	end
	self.activeCheckersGame = nil
	self:ClearSavedGameState("checkers")
	self:RefreshCheckersUI()
	return true
end

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

function GuildPing:AcceptCheckersChallenge(inviteID)
	if self.activeCheckersGame and self.activeCheckersGame.status == "ACTIVE" then
		return nil, "Finish or resign from your current checkers game first."
	end
	local invite, index = self:GetCheckersInvite(inviteID)
	if not invite then
		return nil, "That checkers challenge is no longer available."
	end

	local _, profileKey = self:GetProfile()
	local sent, errorMessage = self:SendCheckersAddonMessage(table.concat({
		"A",
		PROTOCOL_VERSION,
		invite.id,
		invite.sender,
	}, "|"))
	if not sent then
		return nil, errorMessage
	end

	self.activeCheckersGame = CreateGame(invite.id, invite.sender, GetLocalPlayerName(), profileKey)
	table.remove(self.checkersInvites, index)
	self.outgoingCheckersChallenge = nil
	self:RefreshCheckersUI()
	return self.activeCheckersGame
end

function GuildPing:DeclineCheckersChallenge(inviteID)
	local invite, index = self:GetCheckersInvite(inviteID)
	if not invite then
		return false
	end
	self:SendCheckersAddonMessage(table.concat({
		"D",
		PROTOCOL_VERSION,
		invite.id,
		invite.sender,
	}, "|"))
	table.remove(self.checkersInvites, index)
	self:RefreshCheckersUI()
	return true
end

function GuildPing:PlayCheckersMove(fromIndex, toIndex)
	local game = self.activeCheckersGame
	if not game then
		return nil, "There is no active checkers game."
	end
	local _, profileKey = self:GetProfile()
	if game.profileKey ~= profileKey then
		return nil, "This checkers game belongs to a different guild."
	end
	if self:IsGameWaitingForResume(game) then
		return nil, "Resume this checkers game before making a move."
	end

	local controllableColor = self:GetCheckersControllableColor(game)
	if not controllableColor or game.turn ~= controllableColor then
		return nil, "Wait for your opponent's move."
	end
	local snapshot = SnapshotGame(game)
	local move, moveError = ApplyMove(game, fromIndex, toIndex, controllableColor)
	if not move then
		return nil, moveError
	end
	if game.practice then
		self:RefreshCheckersUI()
		return move
	end

	local opponent = self:GetCheckersOpponent(game)
	local sent, sendError = self:SendCheckersAddonMessage(table.concat({
		"M",
		PROTOCOL_VERSION,
		game.id,
		opponent,
		tostring(game.moveNumber),
		tostring(fromIndex),
		tostring(toIndex),
		tostring(StateChecksum(game)),
	}, "|"))
	if not sent then
		RestoreGame(game, snapshot)
		return nil, sendError
	end
	self:RefreshCheckersUI()
	return move
end

function GuildPing:RequestCheckersSync(game, target)
	game = game or self.activeCheckersGame
	if not game then
		return nil, "There is no checkers game to synchronize."
	end
	return self:SendCheckersAddonMessage(table.concat({
		"Q",
		PROTOCOL_VERSION,
		game.id,
		target or self:GetCheckersOpponent(game),
	}, "|"))
end

function GuildPing:SendCheckersSync(game, target)
	local statusCode = game.status == "FINISHED" and "F" or "A"
	local winnerCode = game.winner == BLACK and "B" or game.winner == RED and "R" or "N"
	self:SendCheckersAddonMessage(table.concat({
		"S",
		PROTOCOL_VERSION,
		game.id,
		target,
		tostring(game.moveNumber or 0),
		game.turn == BLACK and "B" or "R",
		tostring(game.forcedPiece or 0),
		statusCode,
		winnerCode,
		SerializeBoard(game.board),
		tostring(StateChecksum(game)),
	}, "|"))
end

function GuildPing:ResignCheckersGame()
	local game = self.activeCheckersGame
	if not game or game.status ~= "ACTIVE" then
		return nil, "There is no active checkers game to resign from."
	end
	if game.practice then
		game.status = "FINISHED"
		game.winner = nil
		game.finishReason = "practice ended"
		game.forcedPiece = nil
		self:RefreshCheckersUI()
		return true
	end
	local localColor = self:GetLocalCheckersColor(game)
	local sent, errorMessage = self:SendCheckersAddonMessage(table.concat({
		"R",
		PROTOCOL_VERSION,
		game.id,
		self:GetCheckersOpponent(game),
	}, "|"))
	if not sent then
		return nil, errorMessage
	end
	game.status = "FINISHED"
	game.winner = OtherColor(localColor)
	game.finishReason = "resignation"
	game.forcedPiece = nil
	self:RefreshCheckersUI()
	return true
end

function GuildPing:HandleCheckersMessage(prefix, message, channel, sender)
	if not self.checkersInitialized
		or prefix ~= MESSAGE_PREFIX
		or channel ~= "GUILD"
		or type(message) ~= "string"
		or type(sender) ~= "string"
		or NamesMatch(sender, GetLocalPlayerName()) then
		return
	end
	self:CleanupCheckersInvites()

	local messageType, protocol = message:match("^([^|]+)|([^|]+)|")
	if protocol ~= PROTOCOL_VERSION then
		return
	end
	local localName = GetLocalPlayerName()

	if messageType == "I" then
		local gameID, target = message:match("^I|1|([%w%-]+)|([^|]+)$")
		if not gameID or not NamesMatch(target, localName) then
			return
		end
		local invites = self:GetCheckersInvites()
		local now = GetTime and GetTime() or 0
		self.checkersInviteSenderCooldowns = self.checkersInviteSenderCooldowns or {}
		local senderKey = NormalizePlayerName(sender)
		local lastInviteAt = self.checkersInviteSenderCooldowns[senderKey]
		if lastInviteAt and now - lastInviteAt < 15 then
			return
		end
		for _, invite in ipairs(invites) do
			if invite.id == gameID then
				return
			end
		end
		if #invites >= MAX_INVITES then
			table.remove(invites, 1)
		end
		self.checkersInviteSenderCooldowns[senderKey] = now
		table.insert(invites, {
			id = gameID,
			sender = sender,
			receivedAt = GetTime and GetTime() or 0,
		})
		self:Print(sender .. " challenged you to Guild Ping Checkers. Open /gp games to respond.")
		self:RefreshCheckersUI()
	elseif messageType == "A" then
		local gameID, target = message:match("^A|1|([%w%-]+)|([^|]+)$")
		local outgoing = self.outgoingCheckersChallenge
		if not gameID or not NamesMatch(target, localName) or not outgoing
			or outgoing.id ~= gameID or not NamesMatch(outgoing.target, sender) then
			return
		end
		if self.activeCheckersGame and self.activeCheckersGame.status == "ACTIVE" then
			self.outgoingCheckersChallenge = nil
			return
		end
		local _, profileKey = self:GetProfile()
		self.activeCheckersGame = CreateGame(gameID, localName, sender, profileKey)
		self.outgoingCheckersChallenge = nil
		self:Print(sender .. " accepted your checkers challenge. You move first.")
		self:RefreshCheckersUI()
	elseif messageType == "D" then
		local gameID, target = message:match("^D|1|([%w%-]+)|([^|]+)$")
		local outgoing = self.outgoingCheckersChallenge
		if gameID and NamesMatch(target, localName) and outgoing and outgoing.id == gameID then
			self.outgoingCheckersChallenge = nil
			self:Print(sender .. " declined your checkers challenge.")
			self:RefreshCheckersUI()
		end
	elseif messageType == "M" then
		local gameID, target, sequence, fromIndex, toIndex, checksum = message:match(
			"^M|1|([%w%-]+)|([^|]+)|(%d+)|(%d+)|(%d+)|(%d+)$"
		)
		local game = self.activeCheckersGame
		sequence, fromIndex, toIndex, checksum = tonumber(sequence), tonumber(fromIndex), tonumber(toIndex), tonumber(checksum)
		if not gameID or not game or game.id ~= gameID or not NamesMatch(target, localName)
			or not NamesMatch(sender, self:GetCheckersOpponent(game)) then
			return
		end
		local senderColor = self:GetCheckersPlayerColor(game, sender)
		if sequence ~= (game.moveNumber or 0) + 1 or senderColor ~= game.turn then
			self:Print("Checkers move order was out of sync; requesting the current board.")
			self:RequestCheckersSync(game, sender)
			return
		end
		self:CompleteGameReconnect("checkers", game)
		local snapshot = SnapshotGame(game)
		local move = ApplyMove(game, fromIndex, toIndex, senderColor)
		if not move or StateChecksum(game) ~= checksum then
			RestoreGame(game, snapshot)
			self:Print("Checkers board verification failed; requesting a synchronized board.")
			self:RequestCheckersSync(game, sender)
			return
		end
		if game.status == "FINISHED" then
			self:Print(sender .. " won your Guild Ping Checkers game.")
		end
		self:RefreshCheckersUI()
	elseif messageType == "Q" then
		local gameID, target = message:match("^Q|1|([%w%-]+)|([^|]+)$")
		local game = self.activeCheckersGame
		if gameID and game and game.id == gameID and NamesMatch(target, localName)
			and NamesMatch(sender, self:GetCheckersOpponent(game)) then
			self:CompleteGameReconnect("checkers", game)
			self:SendCheckersSync(game, sender)
		end
	elseif messageType == "S" then
		local gameID, target, sequence, turnCode, forcedPiece, statusCode, winnerCode, boardValue, checksum = message:match(
			"^S|1|([%w%-]+)|([^|]+)|(%d+)|([BR])|(%d+)|([AF])|([BRN])|([%.bBrR]+)|(%d+)$"
		)
		local game = self.activeCheckersGame
		sequence, forcedPiece, checksum = tonumber(sequence), tonumber(forcedPiece), tonumber(checksum)
		if not gameID or not game or game.id ~= gameID or not NamesMatch(target, localName)
			or not NamesMatch(sender, self:GetCheckersOpponent(game)) or sequence < (game.moveNumber or 0) then
			return
		end
		local board = DeserializeBoard(boardValue)
		if not board then
			return
		end
		local synchronized = {
			board = board,
			turn = turnCode == "B" and BLACK or RED,
			forcedPiece = forcedPiece ~= 0 and forcedPiece or nil,
			moveNumber = sequence,
			status = statusCode == "F" and "FINISHED" or "ACTIVE",
			winner = winnerCode == "B" and BLACK or winnerCode == "R" and RED or nil,
		}
		if StateChecksum(synchronized) ~= checksum then
			return
		end
		if synchronized.forcedPiece then
			local forcedPiece = synchronized.board[synchronized.forcedPiece]
			if PieceColor(forcedPiece) ~= synchronized.turn
				or #GetCapturesForPiece(synchronized.board, synchronized.forcedPiece) == 0 then
				return
			end
		end
		game.board = synchronized.board
		game.turn = synchronized.turn
		game.forcedPiece = synchronized.forcedPiece
		game.moveNumber = synchronized.moveNumber
		game.status = synchronized.status
		game.winner = synchronized.winner
		game.lastMove = nil
		game.history = {}
		self:CompleteGameReconnect("checkers", game)
		self:Print("The checkers board was synchronized with " .. sender .. ".")
		self:RefreshCheckersUI()
	elseif messageType == "R" then
		local gameID, target = message:match("^R|1|([%w%-]+)|([^|]+)$")
		local game = self.activeCheckersGame
		if gameID and game and game.id == gameID and game.status == "ACTIVE"
			and NamesMatch(target, localName) and NamesMatch(sender, self:GetCheckersOpponent(game)) then
			self:CompleteGameReconnect("checkers", game)
			game.status = "FINISHED"
			game.winner = self:GetLocalCheckersColor(game)
			game.finishReason = "opponent resigned"
			game.forcedPiece = nil
			self:Print(sender .. " resigned from your checkers game.")
			self:RefreshCheckersUI()
		end
	end
end

function GuildPing:InitializeCheckers()
	if self.checkersInitialized
		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.checkersInitialized = true
	self.checkersInvites = self.checkersInvites or {}
	local restoredGame, restoreReason = self:RestoreSavedGameState("checkers")
	local _, profileKey = self:GetProfile()
	if restoredGame and IsValidSavedCheckersGame(restoredGame, profileKey) then
		self.activeCheckersGame = restoredGame
		restoredGame.spectators = {}
		restoredGame.lastSpectatorStateToken = nil
		if restoredGame.status == "ACTIVE" then
			if restoredGame.practice then
				restoredGame.persistenceState = "RESTORED"
				self:PersistGameState("checkers", restoredGame)
			else
				self:ResumeCheckersGame(1)
			end
		end
	elseif restoredGame then
		self:ClearSavedGameState("checkers")
	elseif restoreReason == "EXPIRED" then
		self:Print("An abandoned saved checkers game expired and was removed.")
	end
	if C_GuildInfo and C_GuildInfo.GuildRoster then
		C_GuildInfo.GuildRoster()
	end
end

GuildPing.CHECKERS_BLACK = BLACK
GuildPing.CHECKERS_RED = RED
