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

local MESSAGE_PREFIX = "GuildPingSpect"
local PROTOCOL_VERSION = "1"
local LISTING_TIMEOUT = 150
local ADVERTISE_INTERVAL = 60
local CHECKERS = "C"
local POKER = "P"

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

local function NamesMatch(left, right)
	local normalizedLeft = NormalizePlayerName(left)
	local normalizedRight = NormalizePlayerName(right)
	if normalizedLeft == normalizedRight and normalizedLeft ~= "" then return true end
	local leftBase = normalizedLeft:match("^([^-]+)")
	local rightBase = normalizedRight:match("^([^-]+)")
	return leftBase and leftBase == rightBase
		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 SplitMessage(message)
	local fields = {}
	for value in (message .. "|"):gmatch("(.-)|") do table.insert(fields, value) end
	return fields
end

local function SeatCode(seat)
	return seat == GuildPing.POKER_CHALLENGER and "C"
		or seat == GuildPing.POKER_OPPONENT and "O" or "N"
end

local function CodeSeat(code)
	return code == "C" and GuildPing.POKER_CHALLENGER
		or code == "O" and GuildPing.POKER_OPPONENT or nil
end

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

local function DeserializeCheckersBoard(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 board[index] = piece end
	end
	return board
end

local function SendMessage(message, channel, target)
	if #message > 255 then return nil, "The spectator update was too large to send." end
	local succeeded, result = pcall(C_ChatInfo.SendAddonMessage, MESSAGE_PREFIX, message, channel, target)
	if not succeeded or result == false then return nil, "WoW could not send the spectator update." end
	return true
end

function GuildPing:CleanupSpectatorListings()
	local currentTime = GetTime and GetTime() or 0
	self.spectatorListings = self.spectatorListings or {}
	for key, listing in pairs(self.spectatorListings) do
		if currentTime - (tonumber(listing.lastSeen) or 0) > LISTING_TIMEOUT then
			self.spectatorListings[key] = nil
		end
	end
end

function GuildPing:GetSpectatorListings()
	self:CleanupSpectatorListings()
	local listings = {}
	for _, listing in pairs(self.spectatorListings or {}) do table.insert(listings, listing) end
	table.sort(listings, function(left, right)
		if left.gameType ~= right.gameType then return left.gameType < right.gameType end
		return NormalizePlayerName(left.host) < NormalizePlayerName(right.host)
	end)
	return listings
end

function GuildPing:BroadcastSpectatableGames()
	if not self.spectatorInitialized or not IsInGuild or not IsInGuild() then return end
	local checkers = self.activeCheckersGame
	if checkers and not checkers.practice and checkers.status == "ACTIVE"
		and self:GetLocalCheckersColor(checkers) == self.CHECKERS_BLACK then
		SendMessage(table.concat({ "A", PROTOCOL_VERSION, CHECKERS, checkers.id, checkers.opponent }, "|"), "GUILD")
	end
	local poker = self.activePokerGame
	if poker and not poker.practice and poker.status == "ACTIVE" and self:IsPokerAuthority(poker) then
		SendMessage(table.concat({ "A", PROTOCOL_VERSION, POKER, poker.id, poker.opponent }, "|"), "GUILD")
	end
end

function GuildPing:MaybeBroadcastSpectatableGames()
	local currentTime = GetTime and GetTime() or 0
	if self.lastSpectatorAdvertisementAt and currentTime - self.lastSpectatorAdvertisementAt < 20 then return end
	self.lastSpectatorAdvertisementAt = currentTime
	self:BroadcastSpectatableGames()
end

local function BuildCheckersSnapshot(game)
	local winner = game.winner == GuildPing.CHECKERS_BLACK and "B"
		or game.winner == GuildPing.CHECKERS_RED and "R" or "N"
	return table.concat({
		"S", PROTOCOL_VERSION, CHECKERS, game.id, tostring(game.moveNumber or 0),
		game.turn == GuildPing.CHECKERS_BLACK and "B" or "R",
		game.status == "FINISHED" and "F" or "A", winner,
		SerializeCheckersBoard(game.board), game.challenger, game.opponent,
	}, "|")
end

local function EncodeCards(cards)
	return cards and #cards > 0 and table.concat(cards, ",") or "-"
end

local function BuildPokerSnapshot(game)
	return table.concat({
		"S", PROTOCOL_VERSION, POKER, game.id, tostring(game.sequence or 0), tostring(game.handNumber or 0),
		SeatCode(game.button), game.round or "PREFLOP", SeatCode(game.turn),
		game.status == "FINISHED" and "F" or "A", game.handStatus or "WAITING",
		tostring(game.stacks[GuildPing.POKER_CHALLENGER] or 0),
		tostring(game.stacks[GuildPing.POKER_OPPONENT] or 0),
		tostring(game.pot or 0), tostring(game.currentBet or 0),
		tostring(game.roundBets[GuildPing.POKER_CHALLENGER] or 0),
		tostring(game.roundBets[GuildPing.POKER_OPPONENT] or 0),
		EncodeCards(game.board), SeatCode(game.winnerSeat), game.tie and "T" or "N",
		tostring(game.resultPot or 0), game.finishReason or "N", SeatCode(game.lastActor),
		game.lastAction or "N", tostring(game.lastAmount or 0), SeatCode(game.matchWinnerSeat),
		game.challenger, game.opponent,
	}, "|")
end

function GuildPing:SendSpectatorSnapshot(gameType, game, target)
	if not game or not target then return nil end
	local message = gameType == CHECKERS and BuildCheckersSnapshot(game)
		or gameType == POKER and BuildPokerSnapshot(game) or nil
	return message and SendMessage(message, "WHISPER", target) or nil
end

function GuildPing:PublishSpectatorState(gameType, game)
	if not self.spectatorInitialized or not game or game.practice then return end
	local isHost = gameType == CHECKERS and self:GetLocalCheckersColor(game) == self.CHECKERS_BLACK
		or gameType == POKER and self:IsPokerAuthority(game)
	if not isHost then return end
	game.spectators = type(game.spectators) == "table" and game.spectators or {}
	local stateToken = tostring(game.moveNumber or game.sequence or 0) .. ":" .. tostring(game.status)
	if game.lastSpectatorStateToken == stateToken then return end
	game.lastSpectatorStateToken = stateToken
	for _, spectator in pairs(game.spectators) do
		self:SendSpectatorSnapshot(gameType, game, spectator)
	end
end

function GuildPing:WatchSpectatorGame(listing)
	if type(listing) == "string" then
		for _, candidate in ipairs(self:GetSpectatorListings()) do
			if candidate.key == listing then listing = candidate break end
		end
	end
	if type(listing) ~= "table" or not listing.host or not listing.gameID then
		return nil, "That game is no longer available to watch."
	end
	local sent, errorMessage = SendMessage(
		table.concat({ "W", PROTOCOL_VERSION, listing.gameType, listing.gameID }, "|"),
		"WHISPER",
		listing.host
	)
	if not sent then return nil, errorMessage end
	self.activeSpectatorSession = {
		gameType = listing.gameType,
		gameID = listing.gameID,
		host = listing.host,
		status = "CONNECTING",
	}
	if self.ShowSpectatorGame then self:ShowSpectatorGame() end
	return self.activeSpectatorSession
end

function GuildPing:LeaveSpectatorGame()
	local session = self.activeSpectatorSession
	if not session then return false end
	SendMessage(table.concat({ "X", PROTOCOL_VERSION, session.gameType, session.gameID }, "|"), "WHISPER", session.host)
	self.activeSpectatorSession = nil
	if self.RefreshSpectatorUI then self:RefreshSpectatorUI() end
	return true
end

local function ApplyCheckersSpectatorSnapshot(session, fields)
	if #fields ~= 11 then return false end
	local moveNumber = tonumber(fields[5])
	local board = DeserializeCheckersBoard(fields[9])
	if not moveNumber or not board or not (fields[6] == "B" or fields[6] == "R")
		or not (fields[7] == "A" or fields[7] == "F") then return false end
	session.state = {
		moveNumber = moveNumber,
		turn = fields[6],
		status = fields[7] == "F" and "FINISHED" or "ACTIVE",
		winner = fields[8],
		board = board,
		challenger = fields[10],
		opponent = fields[11],
	}
	return true
end

local function DecodeBoard(value)
	if value == "-" then return {} end
	local board = {}
	for card in tostring(value):gmatch("[^,]+") do
		if not card:match("^[2-9TJQKA][cdhs]$") or #board >= 5 then return nil end
		table.insert(board, card)
	end
	return board
end

local function ApplyPokerSpectatorSnapshot(session, fields)
	if #fields ~= 28 then return false end
	local sequence, handNumber = tonumber(fields[5]), tonumber(fields[6])
	local challengerStack, opponentStack = tonumber(fields[12]), tonumber(fields[13])
	local pot, currentBet = tonumber(fields[14]), tonumber(fields[15])
	local challengerBet, opponentBet = tonumber(fields[16]), tonumber(fields[17])
	local resultPot, lastAmount = tonumber(fields[21]), tonumber(fields[25])
	local board = DecodeBoard(fields[18])
	if not sequence or not handNumber or not challengerStack or challengerStack < 0
		or not opponentStack or opponentStack < 0 or not pot or pot < 0 or not currentBet or currentBet < 0
		or not challengerBet or challengerBet < 0 or not opponentBet or opponentBet < 0
		or not resultPot or resultPot < 0 or not lastAmount or lastAmount < 0 or not board then return false end
	session.state = {
		sequence = sequence, handNumber = handNumber, button = CodeSeat(fields[7]), round = fields[8],
		turn = CodeSeat(fields[9]), status = fields[10] == "F" and "FINISHED" or "ACTIVE",
		handStatus = fields[11], stacks = {
			[GuildPing.POKER_CHALLENGER] = challengerStack,
			[GuildPing.POKER_OPPONENT] = opponentStack,
		}, pot = pot, currentBet = currentBet, roundBets = {
			[GuildPing.POKER_CHALLENGER] = challengerBet,
			[GuildPing.POKER_OPPONENT] = opponentBet,
		}, board = board, winnerSeat = CodeSeat(fields[19]), tie = fields[20] == "T",
		resultPot = resultPot, finishReason = fields[22] ~= "N" and fields[22] or nil,
		lastActor = CodeSeat(fields[23]), lastAction = fields[24] ~= "N" and fields[24] or nil,
		lastAmount = lastAmount, matchWinnerSeat = CodeSeat(fields[26]),
		challenger = fields[27], opponent = fields[28],
	}
	return true
end

function GuildPing:HandleSpectatorMessage(prefix, message, channel, sender)
	if not self.spectatorInitialized or prefix ~= MESSAGE_PREFIX or type(message) ~= "string"
		or type(sender) ~= "string" or NamesMatch(sender, GetLocalPlayerName()) then return end
	local fields = SplitMessage(message)
	if fields[2] ~= PROTOCOL_VERSION then return end
	local messageType, gameType, gameID = fields[1], fields[3], fields[4]
	if not (gameType == CHECKERS or gameType == POKER) or not gameID or not gameID:match("^[%w%-]+$") then return end

	if messageType == "A" and channel == "GUILD" and #fields == 5 then
		local key = gameType .. ":" .. gameID
		self.spectatorListings = self.spectatorListings or {}
		self.spectatorListings[key] = {
			key = key, gameType = gameType, gameID = gameID, host = sender,
			opponent = fields[5], lastSeen = GetTime and GetTime() or 0,
		}
		if self.RefreshSpectatorUI then self:RefreshSpectatorUI() end
	elseif messageType == "W" and channel == "WHISPER" and #fields == 4 then
		local game = gameType == CHECKERS and self.activeCheckersGame or self.activePokerGame
		local isHost = game and not game.practice and game.id == gameID and (
			gameType == CHECKERS and self:GetLocalCheckersColor(game) == self.CHECKERS_BLACK
			or gameType == POKER and self:IsPokerAuthority(game)
		)
		if isHost then
			game.spectators = type(game.spectators) == "table" and game.spectators or {}
			game.spectators[NormalizePlayerName(sender)] = sender
			self:SendSpectatorSnapshot(gameType, game, sender)
		end
	elseif messageType == "X" and channel == "WHISPER" and #fields == 4 then
		local game = gameType == CHECKERS and self.activeCheckersGame or self.activePokerGame
		if game and game.id == gameID and game.spectators then
			game.spectators[NormalizePlayerName(sender)] = nil
		end
	elseif messageType == "S" and channel == "WHISPER" then
		local session = self.activeSpectatorSession
		if not session or session.gameType ~= gameType or session.gameID ~= gameID
			or not NamesMatch(sender, session.host) then return end
		local applied = gameType == CHECKERS and ApplyCheckersSpectatorSnapshot(session, fields)
			or ApplyPokerSpectatorSnapshot(session, fields)
		if applied then
			session.status = "WATCHING"
			if self.RefreshSpectatorUI then self:RefreshSpectatorUI() end
		end
	end
end

function GuildPing:InitializeSpectatorMode()
	if self.spectatorInitialized 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.spectatorInitialized = true
	self.spectatorListings = self.spectatorListings or {}
	local function advertise() self:BroadcastSpectatableGames() end
	if C_Timer and C_Timer.After then C_Timer.After(6, advertise) end
	if C_Timer and C_Timer.NewTicker then
		self.spectatorTicker = C_Timer.NewTicker(ADVERTISE_INTERVAL, advertise)
	end
end

GuildPing.SPECTATOR_CHECKERS = CHECKERS
GuildPing.SPECTATOR_POKER = POKER
