local addonName, SinsToolkit = ...

local BugCatcher = {}
SinsToolkit.BugCatcher = BugCatcher

local installedErrorHandler = false
local scriptErrorsSuppressed = false
local savedScriptErrorsValue
local scriptErrorsFrameHooked = false
local frame, textArea, minimapButton, copyFrame, copyBox
local currentIndex = 1
local currentView = "all"
local currentDisplayText = ""
local sessionId = 1
local ownsBugSackGlobals = false
local conflictNoticePrinted = false
local registeredBugGrabberCallback = false
local mirroredBugGrabberErrors = {}

local function Print(message)
    print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. message)
end

local function IsEnabled()
    return SinsToolkit.IsFeatureActive and SinsToolkit.IsFeatureActive("bugCatcherEnabled") and not BugCatcher:IsBlockedByExternalAddon()
end

local function EnsureDB()
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
end

local function IsAddonLoaded(addon)
    if C_AddOns and C_AddOns.IsAddOnLoaded then
        return C_AddOns.IsAddOnLoaded(addon)
    end
    return IsAddOnLoaded and IsAddOnLoaded(addon)
end

local function GetConflictText()
    local conflicts = {}
    if IsAddonLoaded("BugSack") or (_G.BugSack and _G.BugSack ~= BugCatcher) then
        conflicts[#conflicts + 1] = "BugSack"
    end
    return table.concat(conflicts, " and ")
end

local function WantsBugSackCompatibility()
    EnsureDB()
    return SinsToolkitDB.bugCatcherBugSackCompatibility == true
end

local function ClearBugSackCompatibilityGlobals()
    if _G.BugSack == BugCatcher then _G.BugSack = nil end
    local names = {
        "BugSackFrame",
        "BugSackScroll",
        "BugSackScrollText",
        "BugSackScrollChild",
        "BugSackPrevButton",
        "BugSackNextButton",
        "BugSackSendButton",
        "BugSackTabAll",
        "BugSackTabSession",
        "BugSackTabLast",
    }
    for _, name in ipairs(names) do
        if _G[name] then _G[name] = nil end
    end
    ownsBugSackGlobals = false
end

local function PrintConflictNotice(force)
    local conflicts = GetConflictText()
    if conflicts == "" then return false end
    if force or not conflictNoticePrinted then
        conflictNoticePrinted = true
        Print("|cffff4040Bug Catcher is disabled.|r Remove or disable " .. conflicts .. " to use the Sin's Toolkit plugin version. !BugGrabber may stay enabled for testing.")
    end
    return true
end

local function GetErrors()
    EnsureDB()
    return SinsToolkitDB.bugCatcherErrors
end

local function CountErrors()
    return #GetErrors()
end

local function GetBugGrabberCount()
    if not _G.BugGrabber or not BugGrabber.GetDB then return nil end
    local db = BugGrabber:GetDB()
    return type(db) == "table" and #db or nil
end

local function ExtractBugGrabberTableID(...)
    for i = 1, select("#", ...) do
        local value = select(i, ...)
        if type(value) == "string" and value:match("^table: ") then
            return value
        end
    end
    return select(2, ...) or select(1, ...)
end

local BUG_CATCHER_SOUNDS = {
    RAID_WARNING = function() return SOUNDKIT and SOUNDKIT.RAID_WARNING end,
    ALARM = function() return SOUNDKIT and SOUNDKIT.ALARM_CLOCK_WARNING_3 end,
    WHISPER = function() return SOUNDKIT and SOUNDKIT.TELL_MESSAGE end,
    READY_CHECK = function() return SOUNDKIT and SOUNDKIT.READY_CHECK end,
    BUGSACK_FATALITY = function()
        if LibStub then
            local ok, media = pcall(LibStub, "LibSharedMedia-3.0", true)
            if ok and media and media.Fetch then
                local sound = media:Fetch("sound", "BugSack: Fatality", true)
                if sound then return sound end
            end
        end
        return "Interface\\AddOns\\SinsToolkit\\Media\\BugSackFatality.ogg", SOUNDKIT and SOUNDKIT.RAID_WARNING
    end,
}

local function PlayErrorSound()
    local soundKey = SinsToolkitDB and SinsToolkitDB.bugCatcherSound or "NONE"
    if not soundKey or soundKey == "NONE" then return false end

    local resolver = BUG_CATCHER_SOUNDS[soundKey]
    local soundID, fallbackSoundID = resolver and resolver()
    local function TryPlay(sound)
        if type(sound) == "string" and PlaySoundFile then
            local ok, willPlay = pcall(PlaySoundFile, sound, "Master")
            return ok and willPlay ~= false
        elseif sound and PlaySound then
            local ok, willPlay = pcall(PlaySound, sound, "Master")
            return ok and willPlay ~= false
        end
        return false
    end

    return TryPlay(soundID) or TryPlay(fallbackSoundID) or TryPlay(SOUNDKIT and SOUNDKIT.RAID_WARNING)
end

function BugCatcher:TestSound()
    return PlayErrorSound()
end

function BugCatcher:RunSelfTest()
    self:StoreError("Sin's Toolkit: QOL Bug Catcher self-test captured successfully.", "Self-test only: no Lua exception was generated.")
    self:ShowLast()
end

local function SetBagColor(hasErrors)
    if not minimapButton or not minimapButton.bagIcon then return end

    if hasErrors then
        minimapButton.bagIcon:SetVertexColor(0.95, 0.12, 0.10, 1)
    else
        minimapButton.bagIcon:SetVertexColor(0.74, 0.34, 1.00, 1)
    end
    if minimapButton.alertGlow then minimapButton.alertGlow:Hide() end
end

local function GetVisibleErrors()
    local errors = GetErrors()
    if currentView == "last" then
        return errors[#errors] and { errors[#errors] } or {}
    end
    if currentView ~= "session" then
        return errors
    end

    local filtered = {}
    for _, err in ipairs(errors) do
        if err.session == sessionId then
            filtered[#filtered + 1] = err
        end
    end
    return filtered
end

local function GetAddonDisplayName(addon)
    addon = tostring(addon or ""):match("^%s*(.-)%s*$")
    if addon == "" then return "Unknown" end
    if addon == "SinsToolkit" then return "Sin's Toolkit" end
    if addon:match("^Blizzard_") then
        addon = "Blizzard " .. addon:gsub("^Blizzard_", "")
    end
    return (addon:gsub("_", " "))
end

local function FindFirstAddon(text)
    text = tostring(text or "")
    for addon in text:gmatch("[Aa]dd[Oo]ns[/\\]([^/\\%]%s:]+)") do
        if addon and addon ~= "" then return addon end
    end
    return text:match("AddOn '([^']+)'")
end

local function TextContainsAddon(text, addon)
    text = tostring(text or "")
    return text:find("AddOns/" .. addon, 1, true)
        or text:find("AddOns\\" .. addon, 1, true)
        or text:find(addon .. "_", 1, true)
end

local function ExtractTaintOwner(text)
    text = tostring(text or "")
    return text:match("tainted by '([^']+)'")
        or text:match('tainted by "([^"]+)"')
end

local function GetCategoryBadge(category)
    if category == "toolkit" then return "|cff2ecc71🟢 Sin's Toolkit|r" end
    if category == "external" then return "|cffffd54f🟡 External Addon|r" end
    if category == "blizzard" then return "|cff4aa3ff🔵 Blizzard|r" end
    if category == "taint" then return "|cffff9f40🟠 UI Taint|r" end
    if category == "protected" then return "|cffff4d4d🔴 Protected Function|r" end
    return "|cff888888⚫ Unknown|r"
end

local function ClassifyError(message, stack)
    message = tostring(message or "")
    stack = tostring(stack or "")
    local combined = message .. "\n" .. stack
    local protectedAddon = message:match("AddOn '([^']+)' tried to call the protected function")
    local sourceAddon = FindFirstAddon(message) or FindFirstAddon(stack) or protectedAddon
    local taintOwner = ExtractTaintOwner(combined)
    local isWarning = message:find("LUA_WARNING:", 1, true) == 1
    local isProtected = message:find("ADDON_ACTION_FORBIDDEN", 1, true) or message:find("ADDON_ACTION_BLOCKED", 1, true)
    local hasTaint = taintOwner or combined:lower():find("taint", 1, true)
    local toolkitInStack = TextContainsAddon(combined, "SinsToolkit")
    local blizzardSource = sourceAddon and sourceAddon:match("^Blizzard_")

    local errorType = "Lua Error"
    if isProtected then
        errorType = "Protected Function"
    elseif hasTaint then
        errorType = "UI Taint"
    elseif isWarning then
        errorType = "Warning"
    end

    local severity = isWarning and "Warning" or "Error"
    if isProtected then severity = "Protected Action" end

    local origin = "Unknown"
    local originKind = "unknown"
    if toolkitInStack then
        origin = "Sin's Toolkit"
        originKind = "toolkit"
    elseif taintOwner then
        origin = "External (" .. GetAddonDisplayName(taintOwner) .. ")"
        originKind = "external"
    elseif blizzardSource then
        origin = "Blizzard"
        originKind = "blizzard"
    elseif sourceAddon or protectedAddon then
        origin = "External (" .. GetAddonDisplayName(sourceAddon or protectedAddon) .. ")"
        originKind = "external"
    end

    local category = originKind
    if isProtected then
        category = "protected"
    elseif hasTaint then
        category = "taint"
    end

    local source = sourceAddon and GetAddonDisplayName(sourceAddon) or "Unknown"
    local likelyCause = "The source could not be identified from the message or stack."
    local nextStep = "Keep the entry for context and watch for repeats or gameplay impact."
    local confidence = "Low"
    local summary = "Bug Catcher captured this entry, but there was not enough path or taint context to identify a likely owner."

    if toolkitInStack then
        likelyCause = "Sin's Toolkit appears in the execution stack."
        nextStep = "Report this with the raw log and steps that triggered it."
        confidence = "High"
        summary = "This entry includes Sin's Toolkit in the execution stack, so it may need a Toolkit-side fix."
    elseif isProtected then
        likelyCause = "An addon attempted to call a protected Blizzard function."
        nextStep = "Reload the UI, then update or disable the named addon if the protected action repeats."
        confidence = protectedAddon and "High" or "Medium"
        summary = ("This protected-action error points at %s. Sin's Toolkit captured it as part of the complete UI error log."):format(GetAddonDisplayName(protectedAddon or sourceAddon or "Unknown"))
    elseif taintOwner then
        likelyCause = "External addon/UI taint."
        nextStep = "No action is required unless gameplay is affected; update the taint owner and affected UI addons if it repeats."
        confidence = sourceAddon and "High" or "Medium"
        summary = ("This %s appears to originate from %s while the UI was tainted by %s. Sin's Toolkit captured it but does not appear to be the origin."):format(isWarning and "warning" or "error", source, GetAddonDisplayName(taintOwner))
    elseif blizzardSource then
        likelyCause = "Blizzard UI code raised the entry."
        nextStep = "Keep the entry for visibility; check whether the issue persists after Blizzard or addon updates."
        confidence = "Medium"
        summary = ("This entry appears to originate from %s. Sin's Toolkit captured it but does not appear in the execution stack."):format(source)
    elseif sourceAddon then
        likelyCause = "External addon code raised the entry."
        nextStep = "Update or test without the named addon if the entry repeats or affects gameplay."
        confidence = "Medium"
        summary = ("This entry appears to originate from %s. Sin's Toolkit captured it but does not appear in the execution stack."):format(source)
    end

    return {
        severity = severity,
        source = source,
        origin = origin,
        errorType = errorType,
        confidence = confidence,
        category = category,
        badge = GetCategoryBadge(category),
        taintOwner = taintOwner and GetAddonDisplayName(taintOwner) or nil,
        likelyCause = likelyCause,
        nextStep = nextStep,
        summary = summary,
    }
end

local function BuildErrorText(err)
    if not err then
        return "No Lua errors captured."
    end

    local analysis = err.analysis or ClassifyError(err.message, err.stack)
    err.analysis = analysis

    local lines = {}
    lines[#lines + 1] = ("Sin's Toolkit: QOL Bug Catcher")
    lines[#lines + 1] = ("Captured: %s"):format(err.time and date("%Y-%m-%d %H:%M:%S", err.time) or "unknown")
    if err.counter and err.counter > 1 then
        lines[#lines + 1] = ("Repeated: %d times"):format(err.counter)
    end
    lines[#lines + 1] = ""
    lines[#lines + 1] = "Analysis"
    lines[#lines + 1] = ("Severity: %s"):format(analysis.severity)
    lines[#lines + 1] = ("Category: %s"):format(analysis.badge)
    lines[#lines + 1] = ("Source Addon: %s"):format(analysis.source)
    lines[#lines + 1] = ("Origin Addon: %s"):format(analysis.origin)
    if analysis.taintOwner then
        lines[#lines + 1] = ("Taint Owner: %s"):format(analysis.taintOwner)
    end
    lines[#lines + 1] = ("Error Type: %s"):format(analysis.errorType)
    lines[#lines + 1] = "Captured By: Sin's Toolkit Bug Catcher"
    lines[#lines + 1] = ("Likely Cause: %s"):format(analysis.likelyCause)
    lines[#lines + 1] = ("Confidence: %s"):format(analysis.confidence)
    lines[#lines + 1] = ("Suggested Next Step: %s"):format(analysis.nextStep)
    lines[#lines + 1] = ""
    lines[#lines + 1] = analysis.summary
    lines[#lines + 1] = ""
    lines[#lines + 1] = "Raw Log:"
    lines[#lines + 1] = tostring(err.message or "Unknown error")
    if err.stack and err.stack ~= "" then
        lines[#lines + 1] = ""
        lines[#lines + 1] = "Stack:"
        lines[#lines + 1] = tostring(err.stack)
    end
    if err.locals and err.locals ~= "" then
        lines[#lines + 1] = ""
        lines[#lines + 1] = "Locals:"
        lines[#lines + 1] = tostring(err.locals)
    end
    return table.concat(lines, "\n")
end

local function UpdateMinimapButton()
    if not minimapButton and SinsToolkitDB and SinsToolkitDB.bugCatcherMinimap then
        BugCatcher:CreateMinimapButton()
    end
    if not minimapButton then return end
    minimapButton:SetParent(Minimap or UIParent)
    minimapButton:SetFrameLevel(900)
    local count = CountErrors()
    local showOnlyWithErrors = SinsToolkitDB and SinsToolkitDB.bugCatcherMinimapOnlyErrors
    if SinsToolkitDB
        and SinsToolkitDB.bugCatcherEnabled
        and SinsToolkitDB.bugCatcherMinimap
        and not BugCatcher:IsBlockedByExternalAddon()
        and (not showOnlyWithErrors or count > 0) then
        minimapButton:Show()
    else
        minimapButton:Hide()
    end
    if minimapButton.count then
        minimapButton.count:SetText(count > 0 and tostring(count) or "")
        SetBagColor(count > 0)
    end
end

function BugCatcher:StoreError(message, stack)
    EnsureDB()
    if not IsEnabled() then return end
    message = tostring(message or "Unknown error")
    if message:find("SinsToolkit_BugCatcher", 1, true) then return end

    local errors = SinsToolkitDB.bugCatcherErrors
    local last = errors[#errors]
    if last and last.message == message then
        last.counter = (last.counter or 1) + 1
        last.time = time()
        last.analysis = ClassifyError(last.message, last.stack)
    else
        local capturedStack = stack or (debugstack and debugstack(3)) or ""
        errors[#errors + 1] = {
            message = message,
            stack = capturedStack,
            time = time(),
            session = sessionId,
            counter = 1,
            analysis = ClassifyError(message, capturedStack),
        }
    end

    while #errors > (SinsToolkitDB.bugCatcherMaxErrors or 100) do
        table.remove(errors, 1)
    end
    PlayErrorSound()
    currentIndex = #errors
    self:UpdateDisplay()
    UpdateMinimapButton()
    HideScriptErrorsFrame()
    if C_Timer then C_Timer.After(0, HideScriptErrorsFrame) end
end

function BugCatcher:MirrorBugGrabberError(tableID, silent)
    EnsureDB()
    if not IsEnabled() or not _G.BugGrabber then return end
    tableID = tostring(tableID or "")
    if tableID == "" then return end

    local source = BugGrabber.GetErrorByID and BugGrabber:GetErrorByID(tableID)
    if not source and BugGrabber.GetDB then
        local db = BugGrabber:GetDB()
        if type(db) == "table" then
            for _, err in ipairs(db) do
                if tostring(err) == tableID then
                    source = err
                    break
                end
            end
        end
    end
    if type(source) ~= "table" then return end

    local errors = SinsToolkitDB.bugCatcherErrors
    local existingIndex = mirroredBugGrabberErrors[tableID]
    local entry = existingIndex and errors[existingIndex]

    if not entry then
        entry = {}
        errors[#errors + 1] = entry
        mirroredBugGrabberErrors[tableID] = #errors
    end

    entry.message = tostring(source.message or "Unknown error")
    entry.stack = source.stack or ""
    entry.locals = source.locals or ""
    entry.time = source.time or time()
    entry.session = source.session or sessionId
    entry.counter = source.counter or 1
    entry.bugGrabberID = tableID
    entry.analysis = ClassifyError(entry.message, entry.stack)

    while #errors > (SinsToolkitDB.bugCatcherMaxErrors or 100) do
        local removed = table.remove(errors, 1)
        if removed and removed.bugGrabberID then
            mirroredBugGrabberErrors[removed.bugGrabberID] = nil
        end
        for id, index in pairs(mirroredBugGrabberErrors) do
            mirroredBugGrabberErrors[id] = index - 1
        end
    end

    if not silent then PlayErrorSound() end
    currentIndex = #GetVisibleErrors()
    if currentIndex < 1 then currentIndex = 1 end
    self:UpdateDisplay()
    UpdateMinimapButton()
end

function BugCatcher:MirrorBugGrabberDB()
    if not _G.BugGrabber or not BugGrabber.GetDB then return end
    local db = BugGrabber:GetDB()
    if type(db) ~= "table" then return end
    for _, err in ipairs(db) do
        self:MirrorBugGrabberError(tostring(err), true)
    end
    self:UpdateDisplay()
end

function BugCatcher:ScheduleBugGrabberSync()
    if not C_Timer or not C_Timer.After then return end
    C_Timer.After(0.5, function() BugCatcher:MirrorBugGrabberDB() end)
    C_Timer.After(2.0, function() BugCatcher:MirrorBugGrabberDB() end)
    C_Timer.After(5.0, function() BugCatcher:MirrorBugGrabberDB() end)
end

function BugCatcher:Reset()
    if self:IsBlockedByExternalAddon() then
        PrintConflictNotice(true)
        return
    end
    if _G.BugGrabber and BugGrabber.Reset then
        pcall(function() BugGrabber:Reset() end)
    end
    EnsureDB()
    SinsToolkitDB.bugCatcherErrors = {}
    mirroredBugGrabberErrors = {}
    currentIndex = 1
    currentDisplayText = ""
    self:UpdateDisplay()
end

function BugCatcher:GetErrors()
    return GetErrors()
end

function BugCatcher:GetSessionId()
    return sessionId
end

function BugCatcher:IsBlockedByExternalAddon()
    return GetConflictText() ~= ""
end

local function HideScriptErrorsFrame()
    if scriptErrorsSuppressed and _G.ScriptErrorsFrame and _G.ScriptErrorsFrame:IsShown() then
        _G.ScriptErrorsFrame:Hide()
    end
end

local function SetScriptErrorsSuppressed(suppressed)
    if suppressed then
        if not scriptErrorsSuppressed then
            savedScriptErrorsValue = GetCVar and GetCVar("scriptErrors") or nil
            scriptErrorsSuppressed = true
        end
        if C_CVar and C_CVar.SetCVar then
            pcall(C_CVar.SetCVar, "scriptErrors", "0")
        elseif SetCVar then
            pcall(SetCVar, "scriptErrors", "0")
        end
        if _G.ScriptErrorsFrame then
            if not scriptErrorsFrameHooked then
                scriptErrorsFrameHooked = true
                _G.ScriptErrorsFrame:HookScript("OnShow", HideScriptErrorsFrame)
            end
            HideScriptErrorsFrame()
        end
        return
    end

    if not scriptErrorsSuppressed then return end
    scriptErrorsSuppressed = false
    if savedScriptErrorsValue ~= nil then
        if C_CVar and C_CVar.SetCVar then
            pcall(C_CVar.SetCVar, "scriptErrors", savedScriptErrorsValue)
        elseif SetCVar then
            pcall(SetCVar, "scriptErrors", savedScriptErrorsValue)
        end
    end
    savedScriptErrorsValue = nil
end

function BugCatcher:UpdateDisplay()
    if not textArea then
        UpdateMinimapButton()
        return
    end
    local errors = GetVisibleErrors()
    local count = #errors
    if count == 0 then
        currentIndex = 1
        currentDisplayText = ("No Lua errors captured in this view.\n\nStored Toolkit errors: %d\nSelected view: %s\n\nIf !BugGrabber is enabled and shows errors, run /stkbug sync and switch to the All tab."):format(CountErrors(), currentView)
    else
        if currentIndex < 1 then currentIndex = 1 end
        if currentIndex > count then currentIndex = count end
        local err = errors[currentIndex]
        currentDisplayText = ("[%d/%d]\n\n%s"):format(currentIndex, count, BuildErrorText(err))
    end
    textArea:SetText(currentDisplayText)
    local scrollChild = textArea:GetParent()
    if scrollChild and textArea.GetStringHeight then
        local height = math.max(1800, textArea:GetStringHeight() + 24)
        textArea:SetHeight(height)
        scrollChild:SetHeight(height)
    end
    if frame and frame.titleText then
        frame.titleText:SetText(("Sin's Toolkit: QOL Bug Catcher (%d)"):format(count))
    end
    UpdateMinimapButton()
end

function BugCatcher:CreateCopyWindow()
    if copyFrame then return end

    copyFrame = CreateFrame("Frame", "SinsToolkitBugCatcherCopyFrame", UIParent, "BackdropTemplate")
    copyFrame:SetSize(640, 360)
    copyFrame:SetPoint("CENTER")
    copyFrame:SetFrameStrata("FULLSCREEN_DIALOG")
    copyFrame:SetClampedToScreen(true)
    copyFrame:SetMovable(true)
    copyFrame:EnableMouse(true)
    copyFrame:RegisterForDrag("LeftButton")
    copyFrame:SetScript("OnDragStart", copyFrame.StartMoving)
    copyFrame:SetScript("OnDragStop", copyFrame.StopMovingOrSizing)
    copyFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    copyFrame:SetBackdropBorderColor(0.46, 0.28, 0.58, 0.95)
    if SinsToolkit.RegisterOpacityFrame then
        SinsToolkit.RegisterOpacityFrame(copyFrame, 0.02, 0.015, 0.025, 0.46, 0.28, 0.58, 0.95)
    else
        copyFrame:SetBackdropColor(0.02, 0.015, 0.025, 0.98)
    end
    copyFrame:Hide()
    tinsert(UISpecialFrames, copyFrame:GetName())

    local title = copyFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -14)
    title:SetText("Copy Bug Catcher Error")

    local close = CreateFrame("Button", nil, copyFrame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -4, -4)
    close:SetFrameLevel((copyFrame:GetFrameLevel() or 1) + 10)

    local scroll = CreateFrame("ScrollFrame", "SinsToolkitBugCatcherCopyScroll", copyFrame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 16, -46)
    scroll:SetPoint("BOTTOMRIGHT", -32, 46)

    copyBox = CreateFrame("EditBox", "SinsToolkitBugCatcherCopyText", scroll)
    copyBox:SetMultiLine(true)
    copyBox:SetAutoFocus(false)
    copyBox:SetFontObject(GameFontHighlightSmall)
    copyBox:SetTextColor(1, 1, 1, 1)
    copyBox:SetJustifyH("LEFT")
    copyBox:SetJustifyV("TOP")
    copyBox:SetSize(570, 1200)
    copyBox:SetScript("OnEscapePressed", function() copyFrame:Hide() end)
    scroll:SetScrollChild(copyBox)

    local done = CreateFrame("Button", nil, copyFrame, "UIPanelButtonTemplate")
    done:SetSize(100, 24)
    done:SetPoint("BOTTOMRIGHT", -18, 14)
    done:SetText("Close")
    done:SetScript("OnClick", function() copyFrame:Hide() end)
end

function BugCatcher:CopyCurrentError()
    self:CreateWindow()
    self:UpdateDisplay()

    if textArea and textArea.HighlightText then
        textArea:SetText(currentDisplayText ~= "" and currentDisplayText or "No Lua errors captured.")
        textArea:HighlightText()
        textArea:SetFocus()
    end
end

function BugCatcher:SetView(view)
    currentView = view or "all"
    currentIndex = currentView == "last" and 1 or #GetVisibleErrors()
    if currentIndex < 1 then currentIndex = 1 end
    self:UpdateDisplay()
end

function BugCatcher:OpenSack()
    if self:IsBlockedByExternalAddon() then
        PrintConflictNotice(true)
        return
    end
    if not IsEnabled() then
        Print("Bug Catcher is disabled.")
        return
    end
    self:MirrorBugGrabberDB()
    self:CreateWindow()
    if frame then
        frame:SetFrameStrata("DIALOG")
        frame:SetFrameLevel(1000)
        frame:SetToplevel(true)
        frame:Raise()
    end
    frame:Show()
    currentIndex = #GetVisibleErrors()
    if currentIndex < 1 then currentIndex = 1 end
    self:UpdateDisplay()
end

function BugCatcher:CloseSack()
    if frame then frame:Hide() end
end

function BugCatcher:ToggleSack()
    if frame and frame:IsShown() then self:CloseSack() else self:OpenSack() end
end

function BugCatcher:ShowLast()
    currentView = "last"
    currentIndex = 1
    self:OpenSack()
end

function BugCatcher:CreateWindow()
    if frame and textArea then return end

    local prefix = ownsBugSackGlobals and "BugSack" or "SinsToolkitBugCatcher"

    frame = frame or CreateFrame("Frame", prefix .. "Frame", UIParent, "BackdropTemplate")
    frame:SetSize(720, 460)
    frame:SetPoint("CENTER")
    frame:SetFrameStrata("DIALOG")
    frame:SetFrameLevel(1000)
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    frame:SetBackdropBorderColor(0.46, 0.28, 0.58, 0.9)
    if SinsToolkit.RegisterOpacityFrame then
        SinsToolkit.RegisterOpacityFrame(frame, 0.02, 0.015, 0.025, 0.46, 0.28, 0.58, 0.9)
    else
        frame:SetBackdropColor(0.02, 0.015, 0.025, 0.94)
    end
    frame:Hide()
    tinsert(UISpecialFrames, frame:GetName())

    local title = frame:CreateFontString(prefix .. "FrameTitleText", "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 18, -14)
    title:SetText("Sin's Toolkit: QOL Bug Catcher")
    frame.titleText = title

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -4, -4)
    close:SetFrameLevel((frame:GetFrameLevel() or 1) + 10)
    close:SetScript("OnClick", function() frame:Hide() end)

    local prev = CreateFrame("Button", prefix .. "PrevButton", frame, "UIPanelButtonTemplate")
    prev:SetSize(90, 24)
    prev:SetPoint("TOPLEFT", 18, -44)
    prev:SetText("Previous")
    prev:SetScript("OnClick", function()
        currentIndex = currentIndex - 1
        BugCatcher:UpdateDisplay()
    end)

    local clear = CreateFrame("Button", prefix .. "SendButton", frame, "UIPanelButtonTemplate")
    clear:SetSize(100, 24)
    clear:SetPoint("LEFT", prev, "RIGHT", 8, 0)
    clear:SetText("Clear")
    clear:SetScript("OnClick", function() BugCatcher:Reset() end)

    local next = CreateFrame("Button", prefix .. "NextButton", frame, "UIPanelButtonTemplate")
    next:SetSize(90, 24)
    next:SetPoint("LEFT", clear, "RIGHT", 8, 0)
    next:SetText("Next")
    next:SetScript("OnClick", function()
        currentIndex = currentIndex + 1
        BugCatcher:UpdateDisplay()
    end)

    local reload = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    reload:SetSize(90, 24)
    reload:SetPoint("LEFT", next, "RIGHT", 8, 0)
    reload:SetText("Reload")
    reload:SetScript("OnClick", ReloadUI)

    local copy = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    copy:SetSize(90, 24)
    copy:SetPoint("LEFT", reload, "RIGHT", 8, 0)
    copy:SetText("Copy")
    copy:SetScript("OnClick", function() BugCatcher:CopyCurrentError() end)

    local all = CreateFrame("Button", prefix .. "TabAll", frame, "UIPanelButtonTemplate")
    all:SetSize(86, 24)
    all:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 18, 10)
    all:SetText("All")
    all:SetID(1)
    all:SetScript("OnClick", function() BugCatcher:SetView("all") end)

    local session = CreateFrame("Button", prefix .. "TabSession", frame, "UIPanelButtonTemplate")
    session:SetSize(86, 24)
    session:SetPoint("LEFT", all, "RIGHT", 8, 0)
    session:SetText("Session")
    session:SetID(2)
    session:SetScript("OnClick", function() BugCatcher:SetView("session") end)

    local last = CreateFrame("Button", prefix .. "TabLast", frame, "UIPanelButtonTemplate")
    last:SetSize(86, 24)
    last:SetPoint("LEFT", session, "RIGHT", 8, 0)
    last:SetText("Last")
    last:SetID(3)
    last:SetScript("OnClick", function() BugCatcher:SetView("last") end)

    local scroll = CreateFrame("ScrollFrame", prefix .. "Scroll", frame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 18, -76)
    scroll:SetPoint("BOTTOMRIGHT", -32, 44)

    local scrollChild = CreateFrame("Frame", prefix .. "ScrollChild", scroll)
    scrollChild:SetSize(650, 1800)
    scroll:SetScrollChild(scrollChild)

    textArea = CreateFrame("EditBox", prefix .. "ScrollText", scrollChild)
    textArea:SetMultiLine(true)
    textArea:SetAutoFocus(false)
    textArea:SetFontObject(GameFontHighlightSmall)
    textArea:SetTextColor(1, 1, 1, 1)
    textArea:SetJustifyH("LEFT")
    textArea:SetJustifyV("TOP")
    textArea:EnableMouse(true)
    if textArea.SetWordWrap then textArea:SetWordWrap(true) end
    if textArea.SetNonSpaceWrap then textArea:SetNonSpaceWrap(true) end
    textArea:SetSize(650, 1800)
    textArea:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0)
    textArea:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
        self:HighlightText(0, 0)
    end)
    textArea:SetScript("OnEditFocusGained", function(self)
        self:SetCursorPosition(0)
    end)

    if ownsBugSackGlobals then
        _G.BugSackFrame = frame
        _G.BugSackScroll = scroll
        _G.BugSackScrollText = textArea
        _G.BugSackScrollChild = scrollChild
        _G.BugSackPrevButton = prev
        _G.BugSackNextButton = next
        _G.BugSackSendButton = clear
        _G.BugSackTabAll = all
        _G.BugSackTabSession = session
        _G.BugSackTabLast = last
    end

    frame.__sinToolkitBugCatcherComplete = true
end

function BugCatcher:CreateMinimapButton()
    if minimapButton then return end
    local parent = Minimap or UIParent
    minimapButton = CreateFrame("Button", addonName .. "BugCatcherMinimapButton", parent)
    minimapButton:SetSize(31, 31)
    minimapButton:SetFrameStrata("FULLSCREEN")
    minimapButton:SetFrameLevel(900)
    minimapButton:SetToplevel(true)
    minimapButton:SetPoint("TOPLEFT", parent, "TOPLEFT", 32, 0)
    minimapButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    minimapButton.alertGlow = minimapButton:CreateTexture(nil, "BACKGROUND")
    minimapButton.alertGlow:SetTexture("Interface\\Buttons\\UI-Quickslot-Depress")
    minimapButton.alertGlow:SetVertexColor(1, 0, 0, 0.85)
    minimapButton.alertGlow:SetSize(30, 30)
    minimapButton.alertGlow:SetPoint("CENTER")
    minimapButton.alertGlow:Hide()
    minimapButton.bagIcon = minimapButton:CreateTexture(nil, "ARTWORK")
    minimapButton.bagIcon:SetTexture("Interface\\Icons\\INV_Misc_Bag_10_Blue")
    minimapButton.bagIcon:SetSize(24, 24)
    minimapButton.bagIcon:SetPoint("CENTER", 0, -1)
    SetBagColor(false)
    minimapButton.border = minimapButton:CreateTexture(nil, "OVERLAY")
    minimapButton.border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    minimapButton.border:SetSize(54, 54)
    minimapButton.border:SetPoint("TOPLEFT")
    minimapButton.count = minimapButton:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
    minimapButton.count:SetPoint("BOTTOMRIGHT", 0, 2)
    minimapButton:SetScript("OnClick", function(_, button)
        if IsShiftKeyDown() then
            ReloadUI()
        elseif IsAltKeyDown() then
            BugCatcher:Reset()
            Print("Bug Catcher errors cleared.")
        elseif button == "RightButton" then
            SinsToolkitDB.bugCatcherMinimap = false
            UpdateMinimapButton()
            Print("Bug Catcher minimap button hidden. Use /stkbug minimap to show it again.")
        else
            BugCatcher:ShowLast()
        end
    end)
    minimapButton:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:AddLine("Sin's Toolkit: QOL Bug Catcher", 0.74, 0.45, 0.95)
        GameTooltip:AddLine("Click: show last error", 1, 1, 1)
        GameTooltip:AddLine("Shift-click: reload UI", 1, 1, 1)
        GameTooltip:AddLine("Alt-click: clear all errors", 1, 1, 1)
        GameTooltip:AddLine("Right-click: hide minimap button", 0.85, 0.66, 0.30)
        if SinsToolkitDB and SinsToolkitDB.bugCatcherMinimapOnlyErrors then
            GameTooltip:AddLine("Hidden automatically when no errors are stored.", 0.74, 0.45, 0.95)
        end
        GameTooltip:Show()
    end)
    minimapButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
end

local function InstallErrorHandler()
    if installedErrorHandler or BugCatcher:IsBlockedByExternalAddon() then return end
    installedErrorHandler = true
    if seterrorhandler then
        seterrorhandler(function(message)
            BugCatcher:StoreError(message, debugstack and debugstack(3))
        end)
    end
end

function BugCatcher:Init()
    EnsureDB()
    if WantsBugSackCompatibility() and not _G.BugSack then
        -- Optional compatibility mode lets compatible skinning addons treat this window like BugSack.
        -- It is opt-in for new installs to avoid affecting addons that detect the real BugSack addon.
        _G.BugSack = self
        ownsBugSackGlobals = true
    elseif not WantsBugSackCompatibility() and ownsBugSackGlobals then
        ClearBugSackCompatibilityGlobals()
    end

    if self:IsBlockedByExternalAddon() then
        SetScriptErrorsSuppressed(false)
        PrintConflictNotice(false)
        UpdateMinimapButton()
        return
    end

    SetScriptErrorsSuppressed(IsEnabled())
    self:CreateMinimapButton()
    UpdateMinimapButton()

    if _G.BugGrabber and EventRegistry and not registeredBugGrabberCallback then
        registeredBugGrabberCallback = true
        EventRegistry:RegisterCallback("BugGrabber.BugGrabbed", function(...)
            local tableID = ExtractBugGrabberTableID(...)
            BugCatcher:MirrorBugGrabberError(tableID)
        end, self)
        EventRegistry:TriggerEvent("BugGrabber.DisplayRegistered")
        self:MirrorBugGrabberDB()
        self:ScheduleBugGrabberSync()
    end

    InstallErrorHandler()
end

function SinsToolkit.ApplyBugCatcher()
    if BugCatcher.Init then BugCatcher:Init() end
    SetScriptErrorsSuppressed(IsEnabled())
    UpdateMinimapButton()
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("ADDON_ACTION_FORBIDDEN")
eventFrame:RegisterEvent("ADDON_ACTION_BLOCKED")
eventFrame:RegisterEvent("LUA_WARNING")
eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "PLAYER_LOGIN" then
        sessionId = time()
        BugCatcher:Init()
        BugCatcher:ScheduleBugGrabberSync()
    elseif event == "ADDON_LOADED" then
        local loadedAddon = ...
        if loadedAddon == "!BugGrabber" then
            BugCatcher:Init()
            BugCatcher:ScheduleBugGrabberSync()
        end
    elseif event == "ADDON_ACTION_FORBIDDEN" or event == "ADDON_ACTION_BLOCKED" then
        if BugCatcher:IsBlockedByExternalAddon() then return end
        if _G.BugGrabber then return end
        local addon, func = ...
        BugCatcher:StoreError(("[%s] AddOn '%s' tried to call the protected function '%s'."):format(event, tostring(addon or "<name>"), tostring(func or "<func>")))
    elseif event == "LUA_WARNING" then
        if BugCatcher:IsBlockedByExternalAddon() then return end
        if _G.BugGrabber then return end
        local warning = ...
        BugCatcher:StoreError("LUA_WARNING: " .. tostring(warning))
    end
end)

SLASH_SINSTOOLKITBUGCATCHER1 = "/stkbug"
SLASH_SINSTOOLKITBUGCATCHER2 = "/sinbugs"
SlashCmdList.SINSTOOLKITBUGCATCHER = function(msg)
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
    msg = (msg or ""):lower():match("^%s*(.-)%s*$")
    if msg == "" or msg == "open" or msg == "last" then
        BugCatcher:ShowLast()
    elseif msg == "clear" or msg == "reset" then
        if BugCatcher:IsBlockedByExternalAddon() then
            PrintConflictNotice(true)
            return
        end
        BugCatcher:Reset()
        Print("Bug Catcher errors cleared.")
    elseif msg == "minimap" then
        if BugCatcher:IsBlockedByExternalAddon() then
            PrintConflictNotice(true)
            return
        end
        SinsToolkitDB.bugCatcherMinimap = not SinsToolkitDB.bugCatcherMinimap
        if SinsToolkitDB.bugCatcherMinimap then BugCatcher:CreateMinimapButton() end
        UpdateMinimapButton()
        Print("Bug Catcher minimap button " .. (SinsToolkitDB.bugCatcherMinimap and "|cff00ff00shown|r." or "|cffff4040hidden|r."))
    elseif msg == "on" or msg == "enable" then
        if BugCatcher:IsBlockedByExternalAddon() then
            PrintConflictNotice(true)
            return
        end
        SinsToolkitDB.bugCatcherEnabled = true
        SinsToolkit.ApplyBugCatcher()
        Print("Bug Catcher enabled.")
    elseif msg == "off" or msg == "disable" then
        SinsToolkitDB.bugCatcherEnabled = false
        SinsToolkit.ApplyBugCatcher()
        Print("Bug Catcher disabled.")
    elseif msg == "reload" then
        ReloadUI()
    elseif msg == "sync" then
        BugCatcher:MirrorBugGrabberDB()
        local bugGrabberCount = GetBugGrabberCount()
        Print(("Bug Catcher synced. Toolkit errors: %d%s"):format(CountErrors(), bugGrabberCount and (", BugGrabber errors: " .. tostring(bugGrabberCount)) or ""))
    elseif msg == "copy" then
        BugCatcher:CopyCurrentError()
    elseif msg == "sound" or msg == "testsound" then
        local soundKey = SinsToolkitDB and SinsToolkitDB.bugCatcherSound or "NONE"
        if soundKey == "NONE" then
            Print("Bug Catcher sound is set to |cffff4040None|r.")
        elseif BugCatcher:TestSound() then
            Print("Played Bug Catcher sound: |cff00ff00" .. tostring(soundKey) .. "|r.")
        else
            Print("Could not play Bug Catcher sound: |cffff4040" .. tostring(soundKey) .. "|r.")
        end
    elseif msg == "test" then
        if BugCatcher:IsBlockedByExternalAddon() then
            PrintConflictNotice(true)
            return
        end
        if not IsEnabled() then
            Print("Bug Catcher is disabled.")
            return
        end
        BugCatcher:RunSelfTest()
        Print("Bug Catcher self-test captured a test entry.")
    elseif msg == "status" then
        local conflicts = GetConflictText()
        if conflicts ~= "" then
            Print("Bug Catcher: |cffff4040disabled by external addon|r (" .. conflicts .. "). Remove or disable those addons to use the toolkit version.")
        else
            Print(("Bug Catcher: %s, errors: %d"):format(IsEnabled() and "|cff00ff00enabled|r" or "|cffff4040disabled|r", CountErrors()))
        end
    else
        Print("commands:")
        print("  |cffffff00/stkbug|r - show the last captured error")
        print("  |cffffff00/stkbug clear|r - clear all errors")
        print("  |cffffff00/stkbug minimap|r - show or hide the minimap button")
        print("  |cffffff00/stkbug sync|r - sync captured errors from !BugGrabber")
        print("  |cffffff00/stkbug copy|r - copy the currently selected error")
        print("  |cffffff00/stkbug sound|r - test the selected Bug Catcher sound")
        print("  |cffffff00/stkbug test|r - capture a safe self-test entry")
        print("  |cffffff00/stkbug on|r / |cffffff00off|r - enable or disable Bug Catcher")
        print("  |cffffff00/stkbug reload|r - reload the UI")
    end
end
