-- ApiVault UI Stability and Navigation helpers. Added in v1.1.6.
-- UI-only; no runtime source reads and no protected/secure execution.
local addonName, ApiVault = ...

ApiVault.UIStability = ApiVault.UIStability or {}
local Stable = ApiVault.UIStability

Stable.windows = Stable.windows or {}
Stable.metrics = Stable.metrics or {
	registered = 0,
	clamped = 0,
	raised = 0,
	wrapped = 0,
	cappedText = 0,
	normalizedScrolls = 0,
	queuedOOC = 0,
	flushedOOC = 0,
}
Stable.defaultMinWidth = 760
Stable.defaultMinHeight = 480
Stable.defaultStrata = "FULLSCREEN_DIALOG"
Stable.defaultMaxLines = 500
Stable.defaultMaxChars = 65000

local function safeGetParentSize()
	local w = (UIParent and UIParent.GetWidth and UIParent:GetWidth()) or 1280
	local h = (UIParent and UIParent.GetHeight and UIParent:GetHeight()) or 720
	return w, h
end

local function getSettings()
	ApiVaultDB = ApiVaultDB or {}
	ApiVaultDB.settings = ApiVaultDB.settings or {}
	ApiVaultDB.settings.ui = ApiVaultDB.settings.ui or {}
	local ui = ApiVaultDB.settings.ui
	if ui.compactMode == nil then ui.compactMode = false end
	if ui.maxOutputLines == nil then ui.maxOutputLines = Stable.defaultMaxLines end
	if ui.maxOutputChars == nil then ui.maxOutputChars = Stable.defaultMaxChars end
	return ui
end

function Stable:IsCompactMode()
	return getSettings().compactMode and true or false
end

function Stable:SetCompactMode(enabled)
	getSettings().compactMode = enabled and true or false
	if InCombatLockdown and InCombatLockdown() and self.RunAfterCombat then
		self:RunAfterCombat(function()
			ApiVault.UIStability:ApplyCompactModeToAll()
		end, "UIStability.ApplyCompactModeToAll")
	else
		self:ApplyCompactModeToAll()
	end
	return getSettings().compactMode
end

function Stable:ToggleCompactMode()
	return self:SetCompactMode(not self:IsCompactMode())
end

function Stable:GetWindowScale()
	return self:IsCompactMode() and 0.92 or 1.0
end

function Stable:GetDefaultSize(width, height)
	if self:IsCompactMode() then
		return math.max(720, math.floor((width or 1000) * 0.92)), math.max(460, math.floor((height or 640) * 0.92))
	end
	return width or 1000, height or 640
end

function Stable:ClampFrame(frame)
	if not frame or not frame.GetLeft then return end
	local parentW, parentH = safeGetParentSize()
	local left, right, top, bottom = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
	if not left or not right or not top or not bottom then
		if frame.ClearAllPoints and frame.SetPoint then
			frame:ClearAllPoints()
			frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
			self.metrics.clamped = (self.metrics.clamped or 0) + 1
		end
		return
	end

	local width = frame:GetWidth() or 0
	local height = frame:GetHeight() or 0
	local x, y = 0, 0
	if left < 0 then x = -left + 12 end
	if right > parentW then x = x - (right - parentW) - 12 end
	if top > parentH then y = y - (top - parentH) - 12 end
	if bottom < 0 then y = y - bottom + 12 end
	if x ~= 0 or y ~= 0 or width > parentW or height > parentH then
		if frame.ClearAllPoints and frame.SetPoint then
			frame:ClearAllPoints()
			frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
			self.metrics.clamped = (self.metrics.clamped or 0) + 1
		end
	end
end

function Stable:NormalizeScrollFrames(frame)
	if not frame or not frame.GetChildren then return end
	local baseLevel = frame.GetFrameLevel and frame:GetFrameLevel() or 100
	local children = { frame:GetChildren() }
	for _, child in ipairs(children) do
		if child and child.GetObjectType and child:GetObjectType() == "ScrollFrame" then
			if child.SetFrameLevel then child:SetFrameLevel(baseLevel + 2) end
			if child.SetClipsChildren then child:SetClipsChildren(true) end
			self.metrics.normalizedScrolls = (self.metrics.normalizedScrolls or 0) + 1
		elseif child and child.GetChildren then
			-- Avoid deep recursion explosion; normalize one extra level for common panel holders.
			local grandchildren = { child:GetChildren() }
			for _, grandchild in ipairs(grandchildren) do
				if grandchild and grandchild.GetObjectType and grandchild:GetObjectType() == "ScrollFrame" then
					if grandchild.SetFrameLevel then grandchild:SetFrameLevel(baseLevel + 2) end
					if grandchild.SetClipsChildren then grandchild:SetClipsChildren(true) end
					self.metrics.normalizedScrolls = (self.metrics.normalizedScrolls or 0) + 1
				end
			end
		end
	end
end

local function buttonText(button)
	if not button or not button.GetText then return nil end
	local ok, text = pcall(button.GetText, button)
	if ok and text and text ~= "" then return tostring(text) end
end

function Stable:WrapTopButtons(frame)
	if not frame or not frame.GetChildren or not frame.GetWidth then return end
	if not frame.ApiVaultAutoWrapTopButtons then return end
	local buttons = {}
	local frameTop = frame.GetTop and frame:GetTop()
	if not frameTop then return end
	for _, child in ipairs({ frame:GetChildren() }) do
		local text = buttonText(child)
		if text and child:IsShown() and child.GetTop and child.GetLeft and child.GetWidth then
			local top = child:GetTop()
			local left = child:GetLeft()
			if top and left and (frameTop - top) < 96 and child:GetWidth() <= 190 then
				table.insert(buttons, child)
			end
		end
	end
	if #buttons < 4 then return end
	table.sort(buttons, function(a, b) return (a:GetLeft() or 0) < (b:GetLeft() or 0) end)

	local maxWidth = math.max(360, (frame:GetWidth() or 900) - 80)
	local x, y = 18, -62
	local rowHeight = 26
	local used = 0
	for _, button in ipairs(buttons) do
		local bw = math.max(70, math.min(170, button:GetWidth() or 90))
		if used > 0 and used + bw + 6 > maxWidth then
			used = 0
			y = y - rowHeight
		end
		button:ClearAllPoints()
		button:SetPoint("TOPLEFT", frame, "TOPLEFT", x + used, y)
		used = used + bw + 6
		if button.SetFrameLevel and frame.GetFrameLevel then
			button:SetFrameLevel((frame:GetFrameLevel() or 100) + 8)
		end
	end
	self.metrics.wrapped = (self.metrics.wrapped or 0) + 1
end

function Stable:ApplyCompactMode(frame)
	if not frame then return end
	if frame.SetScale then
		frame:SetScale(self:GetWindowScale())
	end
end

function Stable:RegisterPanel(frame, name, opts)
	if not frame then return frame end
	opts = opts or {}
	name = name or frame:GetName() or "ApiVaultWindow"
	self.windows[frame] = {
		name = name,
		opts = opts,
	}
	self.metrics.registered = (self.metrics.registered or 0) + 1

	if frame.SetClampedToScreen then frame:SetClampedToScreen(true) end
	if frame.SetToplevel then frame:SetToplevel(true) end
	if frame.SetFrameStrata then frame:SetFrameStrata(self.defaultStrata) end
	if frame.SetResizable then
		frame:SetResizable(true)
		local minW = opts.minWidth or self.defaultMinWidth
		local minH = opts.minHeight or self.defaultMinHeight
		if frame.SetResizeBounds then
			pcall(frame.SetResizeBounds, frame, minW, minH)
		elseif frame.SetMinResize then
			pcall(frame.SetMinResize, frame, minW, minH)
		end
	end

	if not frame.ApiVaultUIStabilityHooked then
		frame.ApiVaultUIStabilityHooked = true
		frame:HookScript("OnShow", function(selfFrame)
			ApiVault.UIStability:ApplyCompactMode(selfFrame)
			ApiVault.UIStability:ClampFrame(selfFrame)
			ApiVault.UIStability:NormalizeScrollFrames(selfFrame)
			ApiVault.UIStability:WrapTopButtons(selfFrame)
		end)
		frame:HookScript("OnMouseDown", function(selfFrame)
			if ApiVault.RaiseWindow then ApiVault:RaiseWindow(selfFrame) end
		end)
		frame:HookScript("OnSizeChanged", function(selfFrame)
			ApiVault.UIStability:ClampFrame(selfFrame)
			ApiVault.UIStability:NormalizeScrollFrames(selfFrame)
			ApiVault.UIStability:WrapTopButtons(selfFrame)
		end)
	end

	self:ApplyCompactMode(frame)
	self:ClampFrame(frame)
	self:NormalizeScrollFrames(frame)
	return frame
end

function Stable:ApplyCompactModeToAll()
	for frame in pairs(self.windows or {}) do
		if frame then
			self:ApplyCompactMode(frame)
			if frame.IsShown and frame:IsShown() then
				self:ClampFrame(frame)
				self:WrapTopButtons(frame)
			end
		end
	end
end

function Stable:EnsureOOCFrame()
	if self.oocFrame then return self.oocFrame end
	local frame = CreateFrame("Frame")
	self.oocFrame = frame
	frame:SetScript("OnEvent", function()
		ApiVault.UIStability:FlushAfterCombat()
	end)
	return frame
end

function Stable:RunAfterCombat(callback, label)
	if type(callback) ~= "function" then return true end
	if InCombatLockdown and InCombatLockdown() then
		self.oocQueue = self.oocQueue or {}
		table.insert(self.oocQueue, { callback = callback, label = tostring(label or "ui") })
		self.metrics.queuedOOC = (self.metrics.queuedOOC or 0) + 1
		self:EnsureOOCFrame():RegisterEvent("PLAYER_REGEN_ENABLED")
		return false
	end
	local ok, err = pcall(callback)
	if not ok and ApiVault.Print then
		ApiVault:Print("queued UI operation failed: " .. tostring(err))
	end
	return true
end

function Stable:FlushAfterCombat()
	if InCombatLockdown and InCombatLockdown() then return end
	if self.oocFrame then self.oocFrame:UnregisterEvent("PLAYER_REGEN_ENABLED") end
	local queue = self.oocQueue or {}
	self.oocQueue = {}
	for _, entry in ipairs(queue) do
		if type(entry.callback) == "function" then
			local ok, err = pcall(entry.callback)
			if not ok and ApiVault.Print then
				ApiVault:Print("queued UI operation failed: " .. tostring(entry.label or "ui") .. " - " .. tostring(err))
			else
				self.metrics.flushedOOC = (self.metrics.flushedOOC or 0) + 1
			end
		end
	end
end

function Stable:ClampAllWindows()
	for frame in pairs(self.windows or {}) do
		self:ClampFrame(frame)
	end
end

function Stable:CapText(text, maxLines, maxChars)
	text = tostring(text or "")
	local settings = getSettings()
	maxLines = maxLines or settings.maxOutputLines or self.defaultMaxLines
	maxChars = maxChars or settings.maxOutputChars or self.defaultMaxChars
	local truncated = false
	if #text > maxChars then
		text = string.sub(text, 1, maxChars) .. "\n<truncated by ApiVault UIStability: char cap " .. tostring(maxChars) .. ">"
		truncated = true
	end
	local lines, count = {}, 0
	for line in string.gmatch(text .. "\n", "(.-)\n") do
		count = count + 1
		if count <= maxLines then
			table.insert(lines, line)
		else
			truncated = true
			break
		end
	end
	if truncated and count > maxLines then
		table.insert(lines, "<truncated by ApiVault UIStability: line cap " .. tostring(maxLines) .. ">")
	end
	if truncated then
		self.metrics.cappedText = (self.metrics.cappedText or 0) + 1
	end
	return table.concat(lines, "\n"), truncated
end

local METHODS_TO_CAP = {
	{ "LoadedAddonInspector", "FormatList" },
	{ "EventRecorder", "FormatLog" },
	{ "BuildDiff", "FormatDiff" },
	{ "BuildDiff", "FormatManifest" },
	{ "DeveloperAPI", "FormatAllReports" },
	{ "DeveloperAPI", "FormatAddonReport" },
	{ "BugAdvisor", "FormatBugReport" },
	{ "BugAdvisor", "FormatErrorList" },
	{ "ExternalErrorBridge", "FormatSources" },
	{ "DeveloperDebugDashboard", "FormatOverview" },
	{ "DeveloperDebugDashboard", "FormatTimeline" },
	{ "DeveloperDebugDashboard", "FormatReport" },
}

function Stable:WrapMethodForCap(objectName, methodName)
	local object = ApiVault and ApiVault[objectName]
	if type(object) ~= "table" or type(object[methodName]) ~= "function" then return false end
	local key = "_ApiVaultCapped_" .. methodName
	if object[key] then return true end
	local original = object[methodName]
	object[methodName] = function(self, ...)
		local result = original(self, ...)
		if type(result) == "string" then
			return ApiVault.UIStability:CapText(result)
		end
		return result
	end
	object[key] = true
	return true
end

function Stable:InstallOutputCaps()
	local wrapped = 0
	for _, entry in ipairs(METHODS_TO_CAP) do
		if self:WrapMethodForCap(entry[1], entry[2]) then
			wrapped = wrapped + 1
		end
	end
	return wrapped
end

function Stable:Initialize()
	getSettings()
	local wrapped = self:InstallOutputCaps()
	self.metrics.wrappedOutputMethods = wrapped
end

function Stable:GetStats()
	local settings = getSettings()
	local windowCount = 0
	for _ in pairs(self.windows or {}) do windowCount = windowCount + 1 end
	return {
		windows = windowCount,
		compactMode = settings.compactMode,
		maxOutputLines = settings.maxOutputLines,
		maxOutputChars = settings.maxOutputChars,
		registered = self.metrics.registered or 0,
		clamped = self.metrics.clamped or 0,
		wrapped = self.metrics.wrapped or 0,
		normalizedScrolls = self.metrics.normalizedScrolls or 0,
		cappedText = self.metrics.cappedText or 0,
		wrappedOutputMethods = self.metrics.wrappedOutputMethods or 0,
		queuedOOC = self.metrics.queuedOOC or 0,
		flushedOOC = self.metrics.flushedOOC or 0,
		queuedOOCPending = #(self.oocQueue or {}),
	}
end

function Stable:FormatStatus()
	local s = self:GetStats()
	return table.concat({
		"ApiVault UI Stability",
		"windows=" .. tostring(s.windows),
		"compactMode=" .. tostring(s.compactMode),
		"maxOutputLines=" .. tostring(s.maxOutputLines),
		"maxOutputChars=" .. tostring(s.maxOutputChars),
		"registered=" .. tostring(s.registered),
		"clamped=" .. tostring(s.clamped),
		"wrappedButtonRows=" .. tostring(s.wrapped),
		"normalizedScrolls=" .. tostring(s.normalizedScrolls),
		"cappedTextOutputs=" .. tostring(s.cappedText),
		"wrappedOutputMethods=" .. tostring(s.wrappedOutputMethods),
		"queuedOOC=" .. tostring(s.queuedOOC),
		"flushedOOC=" .. tostring(s.flushedOOC),
		"pendingOOC=" .. tostring(s.queuedOOCPending),
		"",
		"Policy: UI-only stabilization. No runtime source reads. No protected/secure execution.",
	}, "\n")
end

function ApiVault:CapOutputText(text, maxLines, maxChars)
	return ApiVault.UIStability:CapText(text, maxLines, maxChars)
end
