local _, ns = ...
local SlickUI = ns.SlickUI
local Theme = SlickUI.Theme

local ErrorCatcher = {
	Installed = false,
	Capturing = false,
	LastAlert = 0,
	MaximumErrors = 30,
	PendingMessages = {},
	RecentMessages = {},
	PendingScheduled = false,
	BurstWindowStarted = 0,
	BurstCount = 0,
}
SlickUI.ErrorCatcher = ErrorCatcher

local ERROR_ALERT_SOUND = "Interface\\AddOns\\SlickUI\\Media\\Sounds\\SlickUI_Error.wav"

local function GetSettings()
	return SlickUI.DB and SlickUI.DB.errorCatcher
end

local function GetHistory()
	local settings = GetSettings()
	if not settings then
		return nil
	end
	if type(settings.history) ~= "table" then
		settings.history = {}
	end
	return settings.history
end

local function FormatRecord(record, index)
	return table.concat({
		string.format("========== Error %d - %s ==========", index, record.time or "Unknown time"),
		record.message or "Unknown Lua error",
		"",
		"Stack:",
		record.stack or "No stack was available.",
	}, "\n")
end

function ErrorCatcher:GetCopyText()
	local history = GetHistory() or {}
	if #history == 0 then
		return "SlickUI has not captured any Lua errors."
	end

	local output = {
		string.format("SlickUI Error Report - version %s - %d captured error(s)", SlickUI.Version, #history),
		"",
	}
	for index, record in ipairs(history) do
		output[#output + 1] = FormatRecord(record, index)
		output[#output + 1] = ""
	end
	return table.concat(output, "\n")
end

function ErrorCatcher:GetCounts()
	local settings = GetSettings()
	local history = GetHistory() or {}
	return #history, settings and tonumber(settings.unread) or 0
end

function ErrorCatcher:Acknowledge()
	local settings = GetSettings()
	if settings then
		settings.unread = 0
	end
	if SlickUI.MinimapButton and SlickUI.MinimapButton.RefreshErrorAlert then
		SlickUI.MinimapButton:RefreshErrorAlert()
	end
end

function ErrorCatcher:RefreshWindow(selectAll)
	if not self.Window then
		return
	end
	local editBox = self.Window.EditBox
	local report = self:GetCopyText()
	editBox:SetText(report)
	editBox:SetCursorPosition(0)
	local _, lineBreaks = string.gsub(report, "\n", "\n")
	editBox:SetHeight(math.max(330, (lineBreaks + 2) * 15))
	if selectAll then
		editBox:SetFocus()
		editBox:HighlightText()
	end
end

function ErrorCatcher:CreateWindow()
	if self.Window then
		return self.Window
	end

	local window = CreateFrame("Frame", "SlickUIErrorCatcherFrame", UIParent, "BackdropTemplate")
	window:SetSize(760, 480)
	window:SetPoint("CENTER")
	window:SetFrameStrata("DIALOG")
	window:SetClampedToScreen(true)
	window:SetMovable(true)
	window:EnableMouse(true)
	window:RegisterForDrag("LeftButton")
	window:SetScript("OnDragStart", window.StartMoving)
	window:SetScript("OnDragStop", window.StopMovingOrSizing)
	Theme:CreatePanel(window)
	window:Hide()
	table.insert(UISpecialFrames, window:GetName())

	Theme:CreateHeaderIcon(window, 34, 14, -9)
	local title = Theme:CreateText(window, 17)
	title:SetPoint("TOPLEFT", 54, -16)
	title:SetText("SlickUI Error Catcher")
	title:SetTextColor(unpack(Theme.Colors.goldText))

	local help = Theme:CreateText(window, 10)
	help:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
	help:SetText("Click Select All, press Ctrl+C, then paste the report wherever you need it.")
	help:SetTextColor(unpack(Theme.Colors.mutedText))

	local close = CreateFrame("Button", nil, window, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -4, -4)

	local scroll = CreateFrame("ScrollFrame", nil, window, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 18, -68)
	scroll:SetPoint("BOTTOMRIGHT", -38, 52)

	local editBox = CreateFrame("EditBox", nil, scroll)
	editBox:SetMultiLine(true)
	editBox:SetAutoFocus(false)
	editBox:SetWidth(688)
	editBox:SetHeight(330)
	editBox:SetTextInsets(6, 6, 6, 6)
	Theme:SetFont(editBox, 11, "")
	editBox:SetTextColor(unpack(Theme.Colors.text))
	editBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
	scroll:SetScrollChild(editBox)
	window.EditBox = editBox

	local selectAll = CreateFrame("Button", nil, window, "UIPanelButtonTemplate")
	selectAll:SetSize(180, 24)
	selectAll:SetPoint("BOTTOMLEFT", 18, 16)
	selectAll:SetText("Select All for Copy")
	selectAll:SetScript("OnClick", function()
		ErrorCatcher:RefreshWindow(true)
	end)

	local clear = CreateFrame("Button", nil, window, "UIPanelButtonTemplate")
	clear:SetSize(130, 24)
	clear:SetPoint("LEFT", selectAll, "RIGHT", 10, 0)
	clear:SetText("Clear Errors")
	clear:SetScript("OnClick", function()
		ErrorCatcher:Clear()
	end)

	self.Window = window
	return window
end

function ErrorCatcher:Show()
	local window = self:CreateWindow()
	self:RefreshWindow(false)
	window:Show()
	window:Raise()
	self:Acknowledge()
end

function ErrorCatcher:Clear()
	local history = GetHistory()
	if history then
		wipe(history)
	end
	local settings = GetSettings()
	if settings then
		settings.unread = 0
	end
	self:RefreshWindow(false)
	if SlickUI.MinimapButton and SlickUI.MinimapButton.RefreshErrorAlert then
		SlickUI.MinimapButton:RefreshErrorAlert()
	end
end

function ErrorCatcher:Alert()
	local now = GetTime()
	if now - self.LastAlert < 3 then
		return
	end
	self.LastAlert = now

	local settings = GetSettings()
	if settings and settings.sound then
		-- Current clients do not guarantee the old SOUNDKIT identifiers used here.
		-- Play SlickUI's packaged file directly so a missing identifier cannot make
		-- the configured error alert fail silently.
		PlaySoundFile(ERROR_ALERT_SOUND, "Master")
	end
	SlickUI:Print("A Lua error was captured. Type /slick errors to copy it.")
end

function ErrorCatcher:ProcessCapture(message, capturedStack)
	if self.Capturing then
		return
	end
	self.Capturing = true
	if SlickUI.DiagnosticLog then
		SlickUI:DiagnosticLog("ErrorCatcher", "lua-error", tostring(message):sub(1, 1000))
	end

	local succeeded = pcall(function()
		local history = GetHistory()
		if not history then
			return
		end
		local record = {
			time = date("%Y-%m-%d %H:%M:%S"),
			message = tostring(message):sub(1, 12000),
			stack = (capturedStack or debugstack(3, 16, 16)):sub(1, 12000),
		}
		table.insert(history, 1, record)
		while #history > self.MaximumErrors do
			table.remove(history)
		end
		local settings = GetSettings()
		settings.unread = math.min(self.MaximumErrors, (tonumber(settings.unread) or 0) + 1)
		if SlickUI.MinimapButton and SlickUI.MinimapButton.RefreshErrorAlert then
			SlickUI.MinimapButton:RefreshErrorAlert()
		end
		self:Alert()
		if self.Window and self.Window:IsShown() then
			self:RefreshWindow(false)
		end
	end)

	self.Capturing = false
	if not succeeded and self.PreviousHandler then
		pcall(self.PreviousHandler, message)
	end
end

-- Error handlers run inside the failing execution path. Keep that path tiny:
-- queue one copy of each error and build stacks/update UI on a later frame.
-- This prevents a rapidly repeating client or addon fault from exhausting
-- WoW's per-addon execution budget and being attributed to SlickUI itself.
function ErrorCatcher:FlushQueue()
	self.PendingScheduled = false
	local pending = table.remove(self.PendingMessages, 1)
	if pending ~= nil then
		if type(pending) == "table" then
			self:ProcessCapture(pending.message, pending.stack)
		else
			self:ProcessCapture(pending)
		end
	end
	if #self.PendingMessages > 0 then
		self.PendingScheduled = true
		C_Timer.After(0.10, function() ErrorCatcher:FlushQueue() end)
	end
end

function ErrorCatcher:Queue(message, capturedStack)
	if self.Capturing then
		return
	end
	if #self.PendingMessages >= 5 then return end
	-- WoW can invoke the active error handler for an execution-budget warning.
	-- Never enqueue a warning attributed to this catcher itself; doing so feeds
	-- the budget failure back into the same addon and produces a recursive storm.
	if type(message) == "string" and message:find("insecure scripts exceeded execution limit", 1, true) then return end

	local now = GetTime()
	if now - self.BurstWindowStarted >= 2 then
		self.BurstWindowStarted = now
		self.BurstCount = 0
	end

	-- Five distinct records in two seconds is enough to diagnose a burst.
	-- Drop the rest instead of letting an error storm stall the client.
	if self.BurstCount >= 5 then
		return
	end

	local succeeded, key = pcall(function()
		return tostring(message):sub(1, 1000)
	end)
	if not succeeded then
		key = "An unreadable Lua error occurred."
		message = key
	end

	local previous = self.RecentMessages[key]
	if previous and now - previous < 10 then
		return
	end
	self.RecentMessages[key] = now
	self.BurstCount = self.BurstCount + 1
	table.insert(self.PendingMessages, {
		message = message,
		stack = capturedStack,
	})

	if not self.PendingScheduled then
		self.PendingScheduled = true
		C_Timer.After(0.05, function() ErrorCatcher:FlushQueue() end)
	end
end

function ErrorCatcher:Capture(message)
	self:Queue(message)
end

function ErrorCatcher:SetEnabled(enabled)
	local settings = GetSettings()
	if settings then
		settings.enabled = enabled == true
	end

	if enabled and not self.Installed then
		self.PreviousHandler = geterrorhandler()
		self.Handler = self.Handler or function(message)
			ErrorCatcher:Queue(message, debugstack(2, 16, 16))
		end
		seterrorhandler(self.Handler)
		self.Installed = true
	elseif not enabled and self.Installed then
		if geterrorhandler() == self.Handler and self.PreviousHandler then
			seterrorhandler(self.PreviousHandler)
		end
		self.Installed = false
	end
end

-- Register the protected-action diagnostics while files are loading, before
-- SlickUI enters its ADDON_LOADED initialization callback. Registering these
-- diagnostic events from inside that callback can itself be rejected by WoW,
-- producing an unhelpful self-referential Frame:RegisterEvent report.
local ProtectedActionFrame = CreateFrame("Frame")
ProtectedActionFrame:RegisterEvent("ADDON_ACTION_BLOCKED")
ProtectedActionFrame:RegisterEvent("ADDON_ACTION_FORBIDDEN")
ProtectedActionFrame:SetScript("OnEvent", function(_, event, addon, action)
	if addon ~= SlickUI.Name or type(action) ~= "string" or action == ""
		or action == "UNKNOWN" or action == "UNKNOWN()" then return end
	local moduleName = SlickUI.InitializingModule
	local context = moduleName and (" during module initialization: " .. moduleName) or " outside module initialization"
	local combat = InCombatLockdown() and "yes" or "no"
	if SlickUI.DiagnosticLog then
		SlickUI:DiagnosticLog("ProtectedAction", event, string.format("action=%s module=%s combat=%s", tostring(action), tostring(moduleName or "none"), combat))
	end
	ErrorCatcher:Queue(
		string.format("%s: SlickUI attempted %s\nContext:%s\nIn combat: %s", event, tostring(action), context, combat),
		debugstack(2, 16, 16)
	)
end)

function ErrorCatcher:Initialize()
	local settings = GetSettings()
	self:SetEnabled(not settings or settings.enabled ~= false)
	self.ProtectedActionFrame = ProtectedActionFrame
end

SlickUI:RegisterModule("ErrorCatcher", ErrorCatcher)
