local _, GF = ...

-- Runtime eligibility is deliberately kept separate from feature policy.
-- This module reads Blizzard's gates, projects user-facing reasons, and
-- announces transitions. Navigation and page owners decide how to render the
-- projection; they do not need to duplicate protected API reads.
local Availability = {}
GF.Availability = Availability

local function protectedCall(callback, ...)
	if type(callback) ~= "function" then
		return false
	end
	return pcall(callback, ...)
end

local function accessible(value)
	if value == nil then
		return true
	end
	if type(canaccessvalue) == "function" then
		local ok, canAccess = pcall(canaccessvalue, value)
		if ok and canAccess == false then
			return false
		end
	end
	if type(issecretvalue) == "function" then
		local ok, secret = pcall(issecretvalue, value)
		if ok and secret == true then
			return false
		end
	end
	return true
end

local function readableText(value)
	if type(value) ~= "string" or not accessible(value) then
		return nil
	end
	local ok, text = pcall(string.match, value, "^%s*(.-)%s*$")
	return ok and text ~= "" and text or nil
end

local function displayReason(value)
	local text = readableText(value)
	if not text then
		return nil
	end
	-- Blizzard failure reasons are sentence copy. Inline page labels in GF do
	-- not end with sentence punctuation, while navigation keeps native copy.
	local ok, trimmed = pcall(string.gsub, text, "[%.!。！]+$", "")
	return ok and trimmed ~= "" and trimmed or text
end

local function fallbackPremadeReason()
	local locale = GF.L or {}
	return readableText(locale.UNAVAILABLE_PREMADE)
		or "Premade Groups are currently unavailable."
end

local function premadeProjection()
	local api = C_LFGInfo
	local reader = type(api) == "table" and api.CanPlayerUsePremadeGroup or nil
	local ok, canUse, nativeReason = protectedCall(reader)
	if not ok or canUse ~= false then
		return { allowed = true }
	end

	local level
	local levelOK, rawLevel = protectedCall(UnitLevel, "player")
	if levelOK and accessible(rawLevel) then
		local numberOK, numericLevel = pcall(tonumber, rawLevel)
		if numberOK then
			level = numericLevel
		end
	end

	local nativeText = readableText(nativeReason)
	local fallback = fallbackPremadeReason()
	local locale = GF.L or {}
	local belowFindLevel = level ~= nil and level < 50
	return {
		allowed = false,
		nativeReason = nativeText or fallback,
		findReason = belowFindLevel
			and (readableText(locale.PREMADE_FIND_LEVEL_REQUIRED)
				or "You must reach level 50 to find a group")
			or displayReason(nativeText or fallback),
		createReason = belowFindLevel
			and (readableText(locale.PREMADE_CREATE_LEVEL_REQUIRED)
				or "You must reach level 50 to create a premade group")
			or displayReason(nativeText or fallback),
	}
end

local function chatLockdownProjection()
	local api = C_ChatInfo
	local reader = type(api) == "table" and api.InChatMessagingLockdown or nil
	local ok, locked = protectedCall(reader)
	if not ok or locked ~= true then
		return { restricted = false, lfgPaused = false }
	end
	local locale = GF.L or {}
	return {
		restricted = true,
		lfgPaused = true,
		message = readableText(locale.UNAVAILABLE_RESTRICTED)
			or "The current scene is restricted by game APIs, so GroupFinder is temporarily unavailable.",
	}
end

function Availability:GetSnapshot()
	local runtime = chatLockdownProjection()
	local premade = premadeProjection()
	return {
		restricted = runtime.restricted,
		lfgPaused = runtime.lfgPaused,
		runtimeMessage = runtime.message,
		canUsePremadeGroup = premade.allowed,
		premadeNavigationReason = premade.nativeReason,
		premadeFindReason = premade.findReason,
		premadeCreateReason = premade.createReason,
	}
end

function Availability:CanUsePremadeGroup()
	return premadeProjection().allowed == true
end

function Availability:GetPremadeBlockMessage()
	return premadeProjection().createReason
end

function Availability:GetPremadeFindBlockMessage()
	return premadeProjection().findReason
end

function Availability:GetPremadeNavigationRestriction()
	return premadeProjection().nativeReason
end

function Availability:GetRuntimeRestrictionMessage()
	return chatLockdownProjection().message
end

function Availability:IsRestricted()
	return chatLockdownProjection().restricted == true
end

function Availability:IsLfgPaused()
	return chatLockdownProjection().lfgPaused == true
end

function Availability:GetBlockMessage()
	return self:GetRuntimeRestrictionMessage()
end

function Availability:ShouldProcessLfgEvent()
	return not self:IsLfgPaused()
end

local function invoke(owner, methodName, ...)
	local method = owner and owner[methodName]
	if type(method) ~= "function" then
		return nil
	end
	local ok, value = pcall(method, owner, ...)
	return ok and value or nil
end

function Availability:ReleaseBlizzardHandoff()
	-- Borrowed native controls have their own ownership implementations. The
	-- runtime gate asks those owners to release; it never reparents fields.
	invoke(GF.SubtitleBar, "ReleaseBlizzardSearchBox")
	invoke(GF.CreatePanel, "ReleaseCreateFields", "restricted")
	if GF.BlizzardBorrow and type(GF.BlizzardBorrow.SetActiveOwner) == "function" then
		pcall(GF.BlizzardBorrow.SetActiveOwner, "blizzard")
	end
end

function Availability:NotifyBlocked(message)
	message = readableText(message) or self:GetRuntimeRestrictionMessage()
	if not message then
		return false
	end
	if type(GF.ShowWarningMessage) == "function" then
		local ok = pcall(GF.ShowWarningMessage, message)
		if ok then
			return true
		end
	end
	local errorFrame = UIErrorsFrame
	if errorFrame and type(errorFrame.AddMessage) == "function" then
		local red = RED_FONT_COLOR
		local r = red and red.r or 1
		local g = red and red.g or 0.2
		local b = red and red.b or 0.2
		local ok = pcall(errorFrame.AddMessage, errorFrame, message, r, g, b, 1)
		if ok then
			return true
		end
	end
	return false
end

function Availability:AddListener(callback)
	if type(callback) ~= "function" then
		return nil
	end
	self.listeners = self.listeners or {}
	self.listeners[#self.listeners + 1] = callback
	return callback
end

local function publish(owner, snapshot, reason)
	owner.revision = (owner.revision or 0) + 1
	for _, callback in ipairs(owner.listeners or {}) do
		pcall(callback, snapshot, reason, owner.revision)
	end
end

function Availability:OnEnterRestricted(snapshot)
	self:ReleaseBlizzardHandoff()
	invoke(GF.MainFrame, "HideFrame")
	publish(self, snapshot or self:GetSnapshot(), "restricted")
end

function Availability:OnLeaveRestricted(snapshot)
	publish(self, snapshot or self:GetSnapshot(), "available")
end

function Availability:UpdateRestrictedState()
	local snapshot = self:GetSnapshot()
	local restricted = snapshot.restricted == true
	local previous = self.lastRestricted
	self.lastRestricted = restricted
	self.snapshot = snapshot
	if previous == restricted then
		return restricted
	end
	if restricted then
		self:OnEnterRestricted(snapshot)
	elseif previous ~= nil then
		self:OnLeaveRestricted(snapshot)
	else
		publish(self, snapshot, "initial")
	end
	return restricted
end

function Availability:GetRevision()
	return self.revision or 0
end

function Availability:Init()
	if self.initialized then
		return self:UpdateRestrictedState()
	end
	self.initialized = true
	if type(CreateFrame) == "function" then
		local ok, frame = pcall(CreateFrame, "Frame")
		if ok and frame then
			self.eventFrame = frame
			for _, eventName in ipairs({
				"PLAYER_ENTERING_WORLD",
				"ZONE_CHANGED_NEW_AREA",
			}) do
				pcall(frame.RegisterEvent, frame, eventName)
			end
			frame:SetScript("OnEvent", function()
				Availability:UpdateRestrictedState()
			end)
		end
	end
	return self:UpdateRestrictedState()
end
