local _, GF = ...

GF.MythicPlusKeystoneRotationReminderService =
	GF.MythicPlusKeystoneRotationReminderService or {}
local Service = GF.MythicPlusKeystoneRotationReminderService
local Util = GF.MythicPlusServiceUtil

local PLAYER_START_GRACE_SECONDS = 10

local function notify(owner, reason)
	if Util and Util.Notify then
		Util.Notify(owner, reason)
		return
	end
	for _, callback in ipairs(owner.listeners or {}) do
		pcall(callback, owner, reason)
	end
end

local function canAccessValue(value)
	if type(canaccessvalue) == "function" then
		local ok, accessible = pcall(canaccessvalue, value)
		if not ok or accessible ~= true then
			return false
		end
	end
	if type(issecretvalue) == "function" then
		local ok, secret = pcall(issecretvalue, value)
		if not ok or secret == true then
			return false
		end
	end
	return true
end

local function positiveNumber(value)
	if not canAccessValue(value) then
		return nil
	end
	local ok, number = pcall(tonumber, value)
	if not ok or not number or number <= 0 then
		return nil
	end
	return number
end

local function readableString(value)
	if not canAccessValue(value) or type(value) ~= "string" or value == "" then
		return nil
	end
	return value
end

local function getAccessibleField(owner, field)
	if not canAccessValue(owner) or type(owner) ~= "table" then
		return nil, false
	end
	local ok, value = pcall(function()
		return owner[field]
	end)
	if not ok or not canAccessValue(value) then
		return nil, false
	end
	return value, true
end

local function getMythicPlusSettings()
	local mythicPlus
	if GF.GetMythicPlusDB then
		local ok, value = pcall(GF.GetMythicPlusDB)
		if ok and type(value) == "table" then
			mythicPlus = value
		end
	end
	if not mythicPlus then
		local db = GF.GetDB and GF.GetDB() or GF.db
		if type(db) ~= "table" then
			return nil
		end
		db.mythicPlus = type(db.mythicPlus) == "table"
			and db.mythicPlus or {}
		mythicPlus = db.mythicPlus
	end
	mythicPlus.settings = type(mythicPlus.settings) == "table"
		and mythicPlus.settings or {}
	local settings = mythicPlus.settings
	if settings.keystoneRotationReminderEnabled == nil then
		settings.keystoneRotationReminderEnabled = false
	else
		settings.keystoneRotationReminderEnabled =
			settings.keystoneRotationReminderEnabled == true
	end
	return settings
end

local function getCurrentTime()
	if type(GetTime) == "function" then
		local ok, value = pcall(GetTime)
		value = ok and tonumber(value) or nil
		if value then
			return value
		end
	end
	return type(time) == "function" and time() or 0
end

local function getActiveKeystoneLevel()
	if not (C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo) then
		return nil
	end
	local ok, level = pcall(C_ChallengeMode.GetActiveKeystoneInfo)
	return ok and positiveNumber(level) or nil
end

local function getCompletionInfo()
	if not (C_ChallengeMode and C_ChallengeMode.GetChallengeCompletionInfo) then
		return nil
	end
	local ok, info = pcall(C_ChallengeMode.GetChallengeCompletionInfo)
	if not ok or not canAccessValue(info) or type(info) ~= "table" then
		return nil
	end
	return info
end

local function getOwnedKeystoneLevel()
	if not (C_MythicPlus and C_MythicPlus.GetOwnedKeystoneLevel) then
		return nil
	end
	local ok, level = pcall(C_MythicPlus.GetOwnedKeystoneLevel)
	return ok and positiveNumber(level) or nil
end

local function getOwnedChallengeModeID()
	if not (C_MythicPlus
		and C_MythicPlus.GetOwnedKeystoneChallengeMapID)
	then
		return nil
	end
	local ok, challengeModeID = pcall(
		C_MythicPlus.GetOwnedKeystoneChallengeMapID)
	return ok and positiveNumber(challengeModeID) or nil
end

local function getDungeonName(challengeModeID)
	if not challengeModeID then
		return nil
	end
	local dungeon = GF.MythicPlusSeason
		and GF.MythicPlusSeason.GetByChallengeModeID
		and GF.MythicPlusSeason:GetByChallengeModeID(challengeModeID) or nil
	local name = dungeon and readableString(dungeon.name) or nil
	if name then
		return name
	end
	if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
		local ok, value = pcall(
			C_ChallengeMode.GetMapUIInfo,
			challengeModeID)
		return ok and readableString(value) or nil
	end
	return nil
end

local function getOwnedKeystoneSnapshot()
	local cache = GF.MythicPlusKeystoneCache
	local snapshot = cache and cache.GetSnapshot and cache:GetSnapshot() or nil
	local ownedChallengeModeID
	local ownedLevel
	local ownedDungeonName
	if snapshot and canAccessValue(snapshot) and type(snapshot) == "table" then
		ownedChallengeModeID = positiveNumber(snapshot.challengeModeID)
		ownedLevel = positiveNumber(snapshot.level)
		ownedDungeonName = readableString(snapshot.dungeonName)
	end
	ownedChallengeModeID = ownedChallengeModeID or getOwnedChallengeModeID()
	ownedLevel = ownedLevel or getOwnedKeystoneLevel()
	ownedDungeonName = ownedDungeonName
		or getDungeonName(ownedChallengeModeID)
	return ownedChallengeModeID, ownedLevel, ownedDungeonName
end

local function classifyCompletion(session, info, enabled)
	if not session or not session.armed
		or session.starterEvidence ~= "other"
		or enabled ~= true
	then
		return "ineligible"
	end
	local practiceRun, practiceReadable = getAccessibleField(
		info, "practiceRun")
	local onTime, onTimeReadable = getAccessibleField(info, "onTime")
	local completedMapID, mapReadable = getAccessibleField(
		info, "mapChallengeModeID")
	local completedLevel, levelReadable = getAccessibleField(info, "level")
	completedMapID = positiveNumber(completedMapID)
	completedLevel = positiveNumber(completedLevel)
	if not info or not practiceReadable or not onTimeReadable or not mapReadable
		or not levelReadable or not completedMapID or not completedLevel
	then
		return "unreadable"
	end
	if practiceRun ~= false or onTime ~= true
		or completedMapID ~= session.challengeModeID
		or completedLevel ~= session.runLevel
		or session.runLevel < session.ownedLevel
	then
		return "ineligible"
	end
	return "eligible"
end

local function nextToken(owner)
	owner.presentationSerial = (tonumber(owner.presentationSerial) or 0) + 1
	return owner.presentationSerial
end

local function setProjection(owner, session, kind)
	owner.presentation = {
		token = nextToken(owner),
		kind = kind,
		runLevel = session.runLevel,
		ownedLevel = session.ownedLevel,
		ownedDungeonName = session.ownedDungeonName,
	}
end

local function hookStartButton()
	local frame = _G.ChallengesKeystoneFrame
	local button = frame and frame.StartButton
	if not button or type(button.HookScript) ~= "function"
		or Service.startButtonHooked
	then
		return false
	end
	button:HookScript("OnClick", function()
		Service:MarkRunStartedByPlayer()
	end)
	Service.startButtonHooked = true
	return true
end

local function scheduleStartButtonHook()
	hookStartButton()
	if C_Timer and type(C_Timer.After) == "function" then
		C_Timer.After(0, hookStartButton)
		C_Timer.After(0.5, hookStartButton)
	end
end

function Service:AddListener(callback)
	if Util and Util.AddListener then
		Util.AddListener(self, callback)
	elseif type(callback) == "function" then
		self.listeners = self.listeners or {}
		self.listeners[#self.listeners + 1] = callback
	end
end

function Service:IsEnabled()
	local settings = getMythicPlusSettings()
	return settings and settings.keystoneRotationReminderEnabled == true or false
end

function Service:SetEnabled(enabled)
	local settings = getMythicPlusSettings()
	if not settings then
		return false
	end
	enabled = enabled == true
	if settings.keystoneRotationReminderEnabled == enabled then
		return true
	end
	settings.keystoneRotationReminderEnabled = enabled
	if not enabled and self.presentation then
		self.presentation = nil
		notify(self, "setting-disabled")
	else
		notify(self, "setting")
	end
	return true
end

function Service:GetProjection()
	local source = self.presentation
	if type(source) ~= "table" then
		return nil
	end
	return {
		token = source.token,
		kind = source.kind,
		runLevel = source.runLevel,
		ownedLevel = source.ownedLevel,
		ownedDungeonName = source.ownedDungeonName,
	}
end

function Service:GetSessionSnapshot()
	local source = self.session
	if type(source) ~= "table" then
		return nil
	end
	return {
		challengeModeID = source.challengeModeID,
		runLevel = source.runLevel,
		ownedChallengeModeID = source.ownedChallengeModeID,
		ownedLevel = source.ownedLevel,
		ownedDungeonName = source.ownedDungeonName,
		starterEvidence = source.starterEvidence,
		armed = source.armed == true,
		completionObserved = source.completionObserved == true,
	}
end

function Service:DismissPresentation(token)
	if not self.presentation
		or token ~= nil and token ~= self.presentation.token
	then
		return false
	end
	self.presentation = nil
	notify(self, "dismiss")
	return true
end

function Service:RequestPreview()
	if self.presentation and self.presentation.kind == "actual" then
		return false
	end
	local _, _, ownedDungeonName = getOwnedKeystoneSnapshot()
	self.presentation = {
		token = nextToken(self),
		kind = "preview",
		runLevel = 12,
		ownedLevel = 10,
		ownedDungeonName = ownedDungeonName,
	}
	notify(self, "preview")
	return true
end

function Service:Clear(reason, clearPresentation)
	self.session = nil
	if clearPresentation == true then
		self.presentation = nil
	end
	notify(self, reason or "clear")
end

function Service:MarkRunStartedByPlayer()
	self.playerStartTime = getCurrentTime()
	self.session = nil
	notify(self, "player-start-intent")
	return true
end

function Service:WasRecentlyStartedByPlayer()
	local startedAt = tonumber(self.playerStartTime)
	if not startedAt then
		return false
	end
	local elapsed = getCurrentTime() - startedAt
	return elapsed >= 0 and elapsed <= PLAYER_START_GRACE_SECONDS
end

function Service:BeginSession(challengeModeID)
	self:Clear("challenge-start-clear", true)
	local startedByPlayer = self:WasRecentlyStartedByPlayer()
	self.playerStartTime = nil
	if startedByPlayer then
		notify(self, "challenge-start-player")
		return false
	end

	local ownedChallengeModeID, ownedLevel, ownedDungeonName =
		getOwnedKeystoneSnapshot()
	local runMapID = positiveNumber(challengeModeID)
	local runLevel = getActiveKeystoneLevel()
	if not ownedLevel or not runMapID or not runLevel
		or runLevel < ownedLevel
	then
		notify(self, "challenge-start-ineligible")
		return false
	end

	self.session = {
		challengeModeID = runMapID,
		runLevel = runLevel,
		ownedChallengeModeID = ownedChallengeModeID,
		ownedLevel = ownedLevel,
		ownedDungeonName = ownedDungeonName,
		starterEvidence = "other",
		armed = true,
	}
	notify(self, "challenge-start-other")
	return true
end

function Service:CompleteSession()
	local session = self.session
	local outcome = classifyCompletion(
		session,
		getCompletionInfo(),
		self:IsEnabled())
	if outcome == "ineligible" then
		self.session = nil
		return false
	end
	if outcome == "unreadable" then
		session.completionObserved = true
		return false
	end
	self.session = nil
	setProjection(self, session, "actual")
	notify(self, "completed")
	return true
end

function Service:HandleEvent(event, ...)
	if event == "ADDON_LOADED" then
		if (...) == "Blizzard_ChallengesUI" then
			scheduleStartButtonHook()
		end
	elseif event == "PLAYER_ENTERING_WORLD" then
		scheduleStartButtonHook()
	elseif event == "CHALLENGE_MODE_KEYSTONE_RECEPTABLE_OPEN"
		or event == "CHALLENGE_MODE_KEYSTONE_SLOTTED"
	then
		scheduleStartButtonHook()
	elseif event == "CHALLENGE_MODE_START" then
		return self:BeginSession(...)
	elseif event == "CHALLENGE_MODE_COMPLETED" then
		return self:CompleteSession()
	elseif event == "CHALLENGE_MODE_COMPLETED_REWARDS" then
		return self:CompleteSession()
	elseif event == "CHALLENGE_MODE_RESET" then
		self:Clear(event, true)
	end
	return false
end

function Service:Init()
	if self.initialized then
		return
	end
	self.initialized = true
	getMythicPlusSettings()
	scheduleStartButtonHook()
end
