local addonName, GF = ...

-- RuntimeLifecycle is the sole owner of startup ordering and of the addon's
-- LFG event routing. Bootstrap owns the one WoW event frame and forwards each
-- event here; feature modules continue to own their own domain state.
GF.RuntimeLifecycle = GF.RuntimeLifecycle or {}
local Lifecycle = GF.RuntimeLifecycle

local function call(owner, methodName, ...)
	local method = owner and owner[methodName]
	if method then
		return method(owner, ...)
	end
end

local blizzardDependencies = {
	"Blizzard_SharedXML",
	"Blizzard_Menu",
	"Blizzard_GroupFinder",
	"Blizzard_UIPanelTemplates",
	"Blizzard_Settings_Shared",
}

function Lifecycle:EnsureBlizzardAddons()
	if GF._blizzardAddonsReady then
		return true
	end

	local loader = C_AddOns and C_AddOns.LoadAddOn or LoadAddOn
	if loader then
		for index = 1, #blizzardDependencies do
			loader(blizzardDependencies[index])
		end
	end

	GF._blizzardAddonsReady = true
	return true
end

-- Preserve the public bootstrap API used by Core and UI modules.
function GF.EnsureBlizzardAddons()
	return Lifecycle:EnsureBlizzardAddons()
end

local dispatcher
local receivesIncrementalSearchUpdates = false
local availabilityRecoveryInstalled = false
local handlingLfgAvailabilityUpdate = false

local incrementalSearchEvents = {
	"LFG_LIST_UPDATE_SEARCH_RESULTS",
}

function Lifecycle:SetLfgUpdateListening(shouldListen)
	shouldListen = shouldListen == true
	if receivesIncrementalSearchUpdates == shouldListen then
		return
	end

	receivesIncrementalSearchUpdates = shouldListen
	if not dispatcher then
		return
	end
	for index = 1, #incrementalSearchEvents do
		local eventName = incrementalSearchEvents[index]
		if shouldListen then
			dispatcher:RegisterEvent(eventName)
		else
			dispatcher:UnregisterEvent(eventName)
		end
	end
end

function GF.SetLfgUpdateListening(shouldListen)
	return Lifecycle:SetLfgUpdateListening(shouldListen)
end

local function lfgDispatchIsSuspended()
	local availability = GF.Availability
	return availability and availability.ShouldProcessLfgEvent
		and availability:ShouldProcessLfgEvent() ~= true
end

local function refreshAvailabilityProjection()
	-- Availability owns the chat-lockdown state and native-control release.
	-- RuntimeLifecycle owns the recovery edge so leaving a restricted scene
	-- cannot leave hidden UI projections stale while no LFG event is emitted.
	call(GF.MainFrame, "OnAvailabilityUpdate")
	call(GF.FloatButton, "RefreshAlert")
end

local function installAvailabilityRecovery()
	if availabilityRecoveryInstalled then
		return
	end
	local availability = GF.Availability
	if not (availability and availability.AddListener) then
		return
	end
	availabilityRecoveryInstalled = true
	availability:AddListener(function(_, reason)
		if reason == "restricted" or reason == "available" then
			-- LFG_LIST_AVAILABILITY_UPDATE completes the same projection in its
			-- authoritative handler. Only zone/world-driven chat transitions need
			-- the listener to supply the otherwise-missing recovery edge.
			if handlingLfgAvailabilityUpdate then
				call(GF.FloatButton, "RefreshAlert")
			else
				refreshAvailabilityProjection()
			end
		end
	end)
end

local handlers = {}

function handlers.ADDON_LOADED(loadedName)
	if loadedName ~= addonName then
		if GF._addonLoaded and GF.Hook and GF.Hook.Refresh then
			GF.Hook.Refresh()
		end
		return
	end

	-- The order is intentional. In particular, service/prefix initialization
	-- precedes Blizzard entry takeover, while the 12.1 censored handoff is
	-- installed before Hook.Refresh loads Blizzard_GroupFinder.
	call(GF.Availability, "Init")
	GF.InitDB()
	call(GF.VersionDiscoveryService, "Init")
	call(GF.MythicPlusServices, "Init")
	call(GF.Blocklist, "Init")
	call(GF.BlacklistMenu, "Init")
	call(GF.CensoredActiveEntryDialog, "Init")
	if GF.Hook and GF.Hook.Refresh then
		GF.Hook.Refresh()
	end
	call(GF.MinimapButton, "Init")
	call(GF.FloatButton, "Init")
	GF.NavData.RequestRefresh()
	GF._addonLoaded = true
	installAvailabilityRecovery()
end

function handlers.PLAYER_LOGIN()
	call(GF.BlacklistMenu, "Init")
	if GF.Hook and GF.Hook.Refresh then
		GF.Hook.Refresh()
	end
	if GF.ValidateLSMFontKeyAfterLogin then
		GF.ValidateLSMFontKeyAfterLogin()
	end
	call(GF.Blocklist, "ClearReadableTitleTokens")
	call(GF.Apply, "OnGroupRosterChanged")
	call(GF.Apply, "InitInviteDialogHooks")
	call(GF.Apply, "QueueAutoAcceptInvite")
	GF.NavData.Rebuild()
	call(GF.NavTree, "Refresh")
	call(GF.FloatButton, "RefreshAlert")
	call(GF.JoinAnnounce, "Init")
	call(GF.CensoredActiveEntryDialog, "Refresh", "PLAYER_LOGIN", {
		showPopup = true,
	})
	call(GF.MainFrame, "RequestPreload", 2)
end

function handlers.PLAYER_REGEN_ENABLED()
	call(GF.MainFrame, "ResumePreload")
end

function handlers.LFG_LIST_SEARCH_RESULTS_RECEIVED()
	if not lfgDispatchIsSuspended() then
		call(GF.Apply, "QueueAutoAcceptInvite")
		call(GF.MainFrame, "OnSearchResults")
	end
end

function handlers.LFG_LIST_SEARCH_FAILED()
	if not lfgDispatchIsSuspended() then
		call(GF.MainFrame, "OnSearchFailed")
	end
end

function handlers.LFG_LIST_SEARCH_RESULT_UPDATED(resultID)
	if not lfgDispatchIsSuspended() then
		-- Blizzard's native invite dialog also waits for this event before
		-- re-reading the stable invited/pendingStatus state.
		call(GF.Apply, "QueueAutoAcceptInvite")
		if receivesIncrementalSearchUpdates then
			call(GF.FindGroupTab, "OnSearchResultUpdated", resultID)
		end
	end
end

function handlers.LFG_LIST_UPDATE_SEARCH_RESULTS()
	if not lfgDispatchIsSuspended() then
		call(GF.FindGroupTab, "OnSearchResultsUpdated")
	end
end

function handlers.LFG_LIST_APPLICATION_STATUS_UPDATED(resultID, newStatus)
	if lfgDispatchIsSuspended() then
		return
	end
	if newStatus == "invited" then
		call(GF.Apply, "QueueAutoAcceptInvite")
	end
	call(GF.FindGroupTab, "OnApplicationStatusUpdated", resultID, newStatus)
	call(GF.JoinAnnounce, "OnApplicationStatusUpdated", resultID, newStatus)
	call(GF.FloatButton, "RefreshAlert")
end

function handlers.LFG_LIST_JOINED_GROUP(resultID)
	-- groupName is protected text; current-group identity consumes only the
	-- result ID and the separate GROUP_JOINED party GUID.
	call(GF.Apply, "OnJoinedGroup", resultID)
end

function handlers.LFG_LIST_AVAILABILITY_UPDATE()
	-- Navigation and the seasonal Mythic+ scope share the availability cache.
	-- Clear it first so a scheduled season rebuild cannot consume login data.
	if GF.NavData and GF.NavData.ClearBuildCaches then
		GF.NavData.ClearBuildCaches()
	end
	call(GF.MythicPlusSeason, "RequestRefresh", "LFG_LIST_AVAILABILITY_UPDATE")

	local availability = GF.Availability
	if availability and availability.UpdateRestrictedState then
		handlingLfgAvailabilityUpdate = true
		local ok, restricted = pcall(
			availability.UpdateRestrictedState,
			availability)
		handlingLfgAvailabilityUpdate = false
		if not ok then
			error(restricted, 0)
		end
		if restricted then
			-- EnterRestricted hides the main frame. Still mark its navigation
			-- dirty so leaving the restriction cannot restore old branches.
			call(GF.MainFrame, "OnAvailabilityUpdate")
			return
		end
	end
	call(GF.MainFrame, "OnAvailabilityUpdate")
end

local function handlePremadePermissionUpdate()
	-- Blizzard re-evaluates native Group Finder buttons on these events. Ask
	-- LFGList to publish its authoritative activity update before rebuilding.
	call(GF.NavData, "RequestRefresh")
	call(GF.MainFrame, "OnPremadePermissionUpdate")
end

handlers.PLAYER_LEVEL_CHANGED = handlePremadePermissionUpdate
handlers.TRIAL_STATUS_UPDATE = handlePremadePermissionUpdate

function handlers.LFG_LIST_ACTIVE_ENTRY_UPDATE(createdNew)
	-- The 12.1 payload is nullable. Relist consumes the original value; legacy
	-- projections continue to consume a strict boolean.
	local createdEventValue = createdNew
	createdNew = createdEventValue == true
	local listing = GF.RecruitmentSession
	local hasActive = listing and listing.HasActive
		and listing:HasActive() == true
	call(GF.CurrentGroupProjection, "OnActiveEntryUpdated")

	call(listing, "SyncEntryOwnership", hasActive)
	local questCreateOutcome = call(
		GF.QuestRecruitmentBridge,
		"HandleActiveEntryChanged",
		hasActive,
		createdNew
	)
	local bumpOutcome = call(
		listing,
		"HandleRelistEntryChanged",
		hasActive,
		createdEventValue)
	call(GF.InvitationScheduler, "HandleActiveEntryChanged", hasActive, createdNew)
	call(GF.CensoredActiveEntryDialog, "Refresh", "LFG_LIST_ACTIVE_ENTRY_UPDATE")
	if lfgDispatchIsSuspended() then
		return
	end

	call(GF.ApplicantAlertService, "SyncBaseline", hasActive, createdNew)
	call(GF.MainFrame, "OnActiveEntryUpdate", {
		hasActive = hasActive,
		createdNew = createdNew,
		questCreateOutcome = questCreateOutcome,
		bumpOutcome = bumpOutcome,
		bumpResolved = true,
		fromActiveEntryEvent = true,
	})
end

function handlers.LFG_LIST_CENSORED_ACTIVE_ENTRY_UPDATE(isCensored)
	-- Censorship changes the safe presentation boundary of the synthetic
	-- current-group row. Invalidate its generation before repainting so an
	-- opaque voice token can never survive into a censored row.
	call(GF.CurrentGroupProjection, "OnActiveEntryUpdated")
	if not lfgDispatchIsSuspended() then
		call(GF.FindGroupTab, "RefreshList", {
			preserveScroll = true,
		})
	end
	call(GF.CensoredActiveEntryDialog, "Refresh",
		"LFG_LIST_CENSORED_ACTIVE_ENTRY_UPDATE", {
			showPopup = isCensored == true,
		})
end

function handlers.LFG_LIST_REVEALED_CENSORED_ACTIVE_ENTRY()
	-- Revealing changes presentation only; the unresolved query remains the
	-- authority for whether updates and relists are blocked.
	call(GF.CurrentGroupProjection, "OnActiveEntryUpdated")
	if not lfgDispatchIsSuspended() then
		call(GF.FindGroupTab, "RefreshList", {
			preserveScroll = true,
		})
	end
	call(GF.CensoredActiveEntryDialog, "Refresh",
		"LFG_LIST_REVEALED_CENSORED_ACTIVE_ENTRY")
end

function handlers.LFG_LIST_ENTRY_CREATION_FAILED()
	local bumpOutcome = call(GF.RecruitmentSession, "HandleCreationFailed")
	local questCreateOutcome = call(GF.QuestRecruitmentBridge, "HandleCreationFailed")
	if not lfgDispatchIsSuspended() then
		call(GF.MainFrame, "OnActiveEntryUpdate", {
			creationFailed = true,
			questCreateOutcome = questCreateOutcome,
			bumpOutcome = bumpOutcome,
			bumpResolved = true,
		})
	end
end

function handlers.LFG_LIST_APPLICANT_LIST_UPDATED()
	if lfgDispatchIsSuspended() then
		return
	end
	-- The list-wide event is a wake edge only. The per-applicant event remains
	-- authoritative for releasing the scheduler's duplicate ledger.
	call(GF.InvitationScheduler, "Queue",
		nil, "LFG_LIST_APPLICANT_LIST_UPDATED")
	call(GF.MainFrame, "OnApplicantsUpdate",
		"LFG_LIST_APPLICANT_LIST_UPDATED")
	call(GF.MythicPlusCarpoolView, "OnApplicantRolesChanged",
		"LFG_LIST_APPLICANT_LIST_UPDATED")
end

function handlers.LFG_LIST_APPLICANT_UPDATED(applicantID)
	if lfgDispatchIsSuspended() then
		return
	end

	call(GF.InvitationScheduler, "Queue",
		applicantID, "LFG_LIST_APPLICANT_UPDATED")
	call(GF.ApplicantAlertService, "HandleApplicantChanged", applicantID)
	if GF.ApplicantsPanel and GF.ApplicantsPanel.OnApplicantUpdated then
		GF.ApplicantsPanel:OnApplicantUpdated(applicantID, true)
	else
		call(GF.MainFrame, "OnApplicantsUpdate")
	end
	call(GF.MythicPlusCarpoolView, "OnApplicantRolesChanged",
		"LFG_LIST_APPLICANT_UPDATED")
	call(GF.FloatButton, "RefreshAlert")
end

local function handleRosterChange(event)
	call(GF.CensoredActiveEntryDialog, "HandlePublishPermissionChanged")
	if lfgDispatchIsSuspended() then
		return
	end
	call(GF.Apply, "OnGroupRosterChanged")
	call(GF.MainFrame, "OnGroupRosterChanged", event)
	call(GF.JoinAnnounce, "OnGroupRosterChanged")
end

function handlers.PARTY_LEADER_CHANGED()
	handleRosterChange("PARTY_LEADER_CHANGED")
end

function handlers.GROUP_ROSTER_UPDATE()
	handleRosterChange("GROUP_ROSTER_UPDATE")
end

function handlers.GROUP_JOINED(category, partyGUID)
	call(GF.Apply, "OnGroupJoined", category, partyGUID)
end

function handlers.GROUP_LEFT(category, partyGUID)
	call(GF.ApplicantsPanel, "OnGroupLeft", category, partyGUID)
	call(GF.Apply, "OnGroupLeft", category, partyGUID)
end

local function refreshRoleSurfaces()
	call(GF.MainFrame, "RefreshRoleSelectionButtons")
	call(GF.ApplicantsPanel, "UpdateFromActiveRoleSummary")
	if lfgDispatchIsSuspended() then
		return
	end
	local currentGroupChanged = call(
		GF.CurrentGroupProjection, "OnRolesChanged") == true
	if currentGroupChanged then
		call(GF.Apply, "QueueCurrentGroupRefresh")
	end
end

handlers.LFG_ROLE_UPDATE = refreshRoleSurfaces
handlers.PLAYER_ROLES_ASSIGNED = refreshRoleSurfaces

local function handleRoleCheck()
	if not lfgDispatchIsSuspended() then
		call(GF.Apply, "QueueAutoConfirmLfgListRoleCheck")
	end
end

handlers.LFG_ROLE_CHECK_SHOW = handleRoleCheck
handlers.LFG_ROLE_CHECK_UPDATE = handleRoleCheck

function handlers.PLAYER_SPECIALIZATION_CHANGED(unit)
	if unit == "player" then
		refreshRoleSurfaces()
		call(GF.FindGroupTab, "OnPlayerSpecializationChanged")
	end
end

local alwaysRegisteredEvents = {
	"ADDON_LOADED",
	"PLAYER_LOGIN",
	"PLAYER_REGEN_ENABLED",
	"LFG_LIST_SEARCH_RESULTS_RECEIVED",
	"LFG_LIST_SEARCH_RESULT_UPDATED",
	"LFG_LIST_SEARCH_FAILED",
	"LFG_LIST_AVAILABILITY_UPDATE",
	"LFG_LIST_ACTIVE_ENTRY_UPDATE",
	"LFG_LIST_ENTRY_CREATION_FAILED",
	"LFG_LIST_APPLICANT_LIST_UPDATED",
	"LFG_LIST_APPLICANT_UPDATED",
	"LFG_LIST_APPLICATION_STATUS_UPDATED",
	"LFG_LIST_JOINED_GROUP",
	"PARTY_LEADER_CHANGED",
	"GROUP_ROSTER_UPDATE",
	"GROUP_JOINED",
	"GROUP_LEFT",
	"LFG_ROLE_UPDATE",
	"LFG_ROLE_CHECK_SHOW",
	"LFG_ROLE_CHECK_UPDATE",
	"PLAYER_ROLES_ASSIGNED",
	"PLAYER_SPECIALIZATION_CHANGED",
	"PLAYER_LEVEL_CHANGED",
	"TRIAL_STATUS_UPDATE",
}

local capabilityRegisteredEvents = {
	"LFG_LIST_CENSORED_ACTIVE_ENTRY_UPDATE",
	"LFG_LIST_REVEALED_CENSORED_ACTIVE_ENTRY",
}

function Lifecycle:AttachDispatcher(eventFrame)
	assert(eventFrame, "RuntimeLifecycle requires the Bootstrap dispatcher")
	if dispatcher == eventFrame then
		return
	end
	dispatcher = eventFrame
	for index = 1, #alwaysRegisteredEvents do
		dispatcher:RegisterEvent(alwaysRegisteredEvents[index])
	end

	local isEventValid = C_EventUtils and C_EventUtils.IsEventValid
	if type(isEventValid) == "function" then
		for index = 1, #capabilityRegisteredEvents do
			local eventName = capabilityRegisteredEvents[index]
			local ok, valid = pcall(isEventValid, eventName)
			if ok and valid == true then
				dispatcher:RegisterEvent(eventName)
			end
		end
	end

	if receivesIncrementalSearchUpdates then
		for index = 1, #incrementalSearchEvents do
			dispatcher:RegisterEvent(incrementalSearchEvents[index])
		end
	end
end

function Lifecycle:HandleEvent(eventName, ...)
	local handler = handlers[eventName]
	if handler then
		return handler(...)
	end
end

local function toggleMainWindow()
	call(GF.MainFrame, "Toggle")
end

function Lifecycle:InstallGlobalFacade()
	_G.SLASH_GROUPFINDER1 = "/gf"
	_G.SLASH_GROUPFINDER2 = "/groupfinder"
	_G.SLASH_GROUPFINDER3 = "/队伍查找器"
	SlashCmdList.GROUPFINDER = function(message)
		if not call(GF.DebugBridge, "HandleSlashCommand", message) then
			toggleMainWindow()
		end
	end

	-- Keep all three pre-2.0.10 command IDs so existing assignments survive.
	function GROUPFINDER_TOGGLE()
		call(GF.MainFrame, "ToggleRoute", {
			tabID = GF.TAB_BROWSE,
		})
	end

	function GROUPFINDER_CREATE()
		call(GF.MainFrame, "ToggleRoute", {
			tabID = GF.TAB_CREATE,
		})
	end

	function GROUPFINDER_MPLUS_CHARACTER()
		call(GF.MainFrame, "ToggleRoute", {
			workspaceID = GF.WORKSPACE_MYTHIC_PLUS,
			tabID = GF.TAB_MPLUS_CHARACTER,
		})
	end

	function GROUPFINDER_MPLUS_CARPOOL()
		call(GF.MainFrame, "ToggleRoute", {
			workspaceID = GF.WORKSPACE_MYTHIC_PLUS,
			tabID = GF.TAB_MPLUS_CARPOOL,
		})
	end

	function GROUPFINDER_MPLUS_TELEPORT()
		call(GF.MainFrame, "ToggleRoute", {
			workspaceID = GF.WORKSPACE_MYTHIC_PLUS,
			tabID = GF.TAB_MPLUS_DUNGEON,
		})
	end

	_G.GroupFinder = GF
end
