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

local GAME_KEY = "runeMastermind"
local CODE_LENGTH = 4
local MAX_ATTEMPTS = 8
local RUNES = {
	{ id = "SUN", label = "Sun", glyph = "S" },
	{ id = "MOON", label = "Moon", glyph = "M" },
	{ id = "LEAF", label = "Leaf", glyph = "L" },
	{ id = "FLAME", label = "Flame", glyph = "F" },
	{ id = "TIDE", label = "Tide", glyph = "T" },
	{ id = "VOID", label = "Void", glyph = "V" },
}

local RUNE_LOOKUP = {}
for index, rune in ipairs(RUNES) do
	RUNE_LOOKUP[rune.id] = rune
	rune.index = index
end

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

local function CopyCode(code)
	local copy = {}
	for index = 1, CODE_LENGTH do copy[index] = code and code[index] or nil end
	return copy
end

local function CreateSecret()
	local secret = {}
	for index = 1, CODE_LENGTH do secret[index] = RUNES[math.random(1, #RUNES)].id end
	return secret
end

local function EvaluateGuess(secret, guess)
	local exact, misplaced, remaining = 0, 0, {}
	for index = 1, CODE_LENGTH do
		if secret[index] == guess[index] then exact = exact + 1
		else remaining[secret[index]] = (remaining[secret[index]] or 0) + 1 end
	end
	for index = 1, CODE_LENGTH do
		if secret[index] ~= guess[index] and (remaining[guess[index]] or 0) > 0 then
			misplaced = misplaced + 1
			remaining[guess[index]] = remaining[guess[index]] - 1
		end
	end
	return exact, misplaced
end

local function CreateGame(profileKey)
	return {
		id = CreateGameID(), profileKey = profileKey, status = "ACTIVE", secret = CreateSecret(),
		currentGuess = {}, attempts = {}, selectedSlot = 1, maxAttempts = MAX_ATTEMPTS,
		startedAt = GetServerTime and GetServerTime() or 0,
	}
end

local function IsValidCode(code, allowEmpty)
	if type(code) ~= "table" then return false end
	for index = 1, CODE_LENGTH do
		if code[index] ~= nil and not RUNE_LOOKUP[code[index]] then return false end
		if not allowEmpty and not RUNE_LOOKUP[code[index]] then return false end
	end
	return true
end

local function IsValidSavedGame(game, profileKey)
	if type(game) ~= "table" or type(game.id) ~= "string" or game.profileKey ~= profileKey
		or not (game.status == "ACTIVE" or game.status == "FINISHED") or not IsValidCode(game.secret, false)
		or not IsValidCode(game.currentGuess, true) or type(game.attempts) ~= "table" then return false end
	if #game.attempts > MAX_ATTEMPTS then return false end
	local cleanedAttempts, solved = {}, false
	for _, attempt in ipairs(game.attempts) do
		if type(attempt) ~= "table" or not IsValidCode(attempt.guess, false) then return false end
		local exact, misplaced = EvaluateGuess(game.secret, attempt.guess)
		if solved then return false end
		solved = exact == CODE_LENGTH
		table.insert(cleanedAttempts, { guess = CopyCode(attempt.guess), exact = exact, misplaced = misplaced })
	end
	game.attempts = cleanedAttempts
	game.selectedSlot = math.max(1, math.min(CODE_LENGTH, math.floor(tonumber(game.selectedSlot) or 1)))
	game.maxAttempts = MAX_ATTEMPTS
	game.status = (solved or #game.attempts >= MAX_ATTEMPTS) and "FINISHED" or "ACTIVE"
	game.won = game.status == "FINISHED" and solved or nil
	return true
end

function GuildPing:GetRuneMastermindRunes() return RUNES end
function GuildPing:EvaluateRuneMastermindGuess(secret, guess) return EvaluateGuess(secret, guess) end

function GuildPing:RefreshRuneMastermindUI()
	local game = self.activeRuneMastermindGame
	if game then
		if game.status == "FINISHED" then
			local attempts = #game.attempts
			self:RecordSoloGameResult(GAME_KEY, game, game.won, {
				attempts = attempts,
				reason = game.won and string.format("solved in %d attempt%s", attempts, attempts == 1 and "" or "s")
					or "rune lock held",
			})
		end
		self:PersistGameState(GAME_KEY, game)
	end
	if self.RefreshRuneMastermindFrame and self.runeMastermindFrame and self.runeMastermindFrame:IsShown() then
		self:RefreshRuneMastermindFrame()
	end
	if self.RefreshGamesIndicators then self:RefreshGamesIndicators() end
end

function GuildPing:StartRuneMastermindGame()
	local _, profileKey = self:GetProfile()
	self.activeRuneMastermindGame = CreateGame(profileKey)
	self:RefreshRuneMastermindUI()
	return self.activeRuneMastermindGame
end

function GuildPing:AbandonRuneMastermindGame()
	self.activeRuneMastermindGame = nil
	self:ClearSavedGameState(GAME_KEY)
	if self.RefreshRuneMastermindFrame and self.runeMastermindFrame and self.runeMastermindFrame:IsShown() then
		self:RefreshRuneMastermindFrame()
	end
	if self.RefreshGamesIndicators then self:RefreshGamesIndicators() end
	return true
end

function GuildPing:SetRuneMastermindGuess(slot, runeID)
	local game = self.activeRuneMastermindGame
	slot = math.floor(tonumber(slot) or 0)
	if not game or game.status ~= "ACTIVE" then return nil, "Start a new Rune Mastermind puzzle first." end
	if slot < 1 or slot > CODE_LENGTH or not RUNE_LOOKUP[runeID] then return nil, "Choose a valid rune slot and rune." end
	game.currentGuess[slot] = runeID
	game.selectedSlot = slot == CODE_LENGTH and 1 or slot + 1
	for offset = 1, CODE_LENGTH do
		local index = ((slot + offset - 1) % CODE_LENGTH) + 1
		if not game.currentGuess[index] then game.selectedSlot = index break end
	end
	self:RefreshRuneMastermindUI()
	return true
end

function GuildPing:SelectRuneMastermindSlot(slot)
	local game = self.activeRuneMastermindGame
	slot = math.floor(tonumber(slot) or 0)
	if not game or game.status ~= "ACTIVE" or slot < 1 or slot > CODE_LENGTH then return false end
	game.selectedSlot = slot
	self:RefreshRuneMastermindUI()
	return true
end

function GuildPing:CycleRuneMastermindSlot(slot)
	local game = self.activeRuneMastermindGame
	slot = math.floor(tonumber(slot) or 0)
	if not game or game.status ~= "ACTIVE" or slot < 1 or slot > CODE_LENGTH then return false end
	local current = game.currentGuess[slot]
	local nextIndex = current and ((RUNE_LOOKUP[current].index % #RUNES) + 1) or 1
	game.currentGuess[slot] = RUNES[nextIndex].id
	game.selectedSlot = slot
	self:RefreshRuneMastermindUI()
	return true
end

function GuildPing:ClearRuneMastermindGuess()
	local game = self.activeRuneMastermindGame
	if not game or game.status ~= "ACTIVE" then return false end
	game.currentGuess = {}
	game.selectedSlot = 1
	self:RefreshRuneMastermindUI()
	return true
end

function GuildPing:SubmitRuneMastermindGuess()
	local game = self.activeRuneMastermindGame
	if not game or game.status ~= "ACTIVE" then return nil, "There is no active Rune Mastermind puzzle." end
	if not IsValidCode(game.currentGuess, false) then return nil, "Fill all four rune slots before submitting." end
	local exact, misplaced = EvaluateGuess(game.secret, game.currentGuess)
	local attempt = { guess = CopyCode(game.currentGuess), exact = exact, misplaced = misplaced }
	table.insert(game.attempts, attempt)
	if exact == CODE_LENGTH then
		game.status, game.won = "FINISHED", true
	elseif #game.attempts >= MAX_ATTEMPTS then
		game.status, game.won = "FINISHED", false
	else
		game.currentGuess = {}
		game.selectedSlot = 1
	end
	self:RefreshRuneMastermindUI()
	return attempt
end

function GuildPing:InitializeRuneMastermind()
	local restored, reason = self:RestoreSavedGameState(GAME_KEY)
	local _, profileKey = self:GetProfile()
	if restored and IsValidSavedGame(restored, profileKey) then
		self.activeRuneMastermindGame = restored
		self:RefreshRuneMastermindUI()
	elseif restored then
		self:ClearSavedGameState(GAME_KEY)
	elseif reason == "EXPIRED" then
		self:Print("An unfinished Rune Mastermind puzzle expired and was removed.")
	end
end

GuildPing.RUNE_MASTERMIND_CODE_LENGTH = CODE_LENGTH
GuildPing.RUNE_MASTERMIND_MAX_ATTEMPTS = MAX_ATTEMPTS
