local addonName, SinsToolkit = ...

local BugCatcher = {}
SinsToolkit.BugCatcher = BugCatcher

local originalErrorHandler = geterrorhandler and geterrorhandler()
local installedErrorHandler = 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 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 BuildErrorText(err)
    if not err then
        return "No Lua errors captured."
    end

    local lines = {}
    lines[#lines + 1] = ("Sin's Toolkit 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] = 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 then return end
    if SinsToolkitDB and SinsToolkitDB.bugCatcherEnabled and SinsToolkitDB.bugCatcherMinimap and not BugCatcher:IsBlockedByExternalAddon() then
        minimapButton:Show()
    else
        minimapButton:Hide()
    end
    if minimapButton.count then
        local count = CountErrors()
        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()
    else
        errors[#errors + 1] = {
            message = message,
            stack = stack or (debugstack and debugstack(3)) or "",
            time = time(),
            session = sessionId,
            counter = 1,
        }
    end

    while #errors > (SinsToolkitDB.bugCatcherMaxErrors or 100) do
        table.remove(errors, 1)
    end
    PlayErrorSound()
    currentIndex = #errors
    self:UpdateDisplay()
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

    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()
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

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
        scrollChild:SetHeight(math.max(1800, textArea:GetStringHeight() + 24))
    end
    if frame and frame.titleText then
        frame.titleText:SetText(("Sin's Toolkit 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:SetBackdropColor(0.02, 0.015, 0.025, 0.98)
    copyFrame:SetBackdropBorderColor(0.46, 0.28, 0.58, 0.95)
    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)

    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()
    self:CreateCopyWindow()

    copyBox:SetText(currentDisplayText ~= "" and currentDisplayText or "No Lua errors captured.")
    copyBox:HighlightText()
    copyBox:SetFocus()
    copyFrame:Show()
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

local function TryWindToolsSkin()
    if not ownsBugSackGlobals or _G.BugSackFrame ~= frame then return end
    local skin = ElvUI_WindTools and ElvUI_WindTools.Modules and ElvUI_WindTools.Modules.Skins
    if skin and skin.BugSack_OpenSack and not frame.__windSkin then
        pcall(function() skin:BugSack_OpenSack() end)
    end
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()
    frame:Show()
    currentIndex = #GetVisibleErrors()
    if currentIndex < 1 then currentIndex = 1 end
    self:UpdateDisplay()
    TryWindToolsSkin()
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: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:SetBackdropColor(0.02, 0.015, 0.025, 0.94)
    frame:SetBackdropBorderColor(0.46, 0.28, 0.58, 0.9)
    frame:Hide()
    tinsert(UISpecialFrames, frame:GetName())

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

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

    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 = scrollChild:CreateFontString(prefix .. "ScrollText", "ARTWORK", "GameFontHighlightSmall")
    textArea:SetTextColor(1, 1, 1, 1)
    textArea:SetJustifyH("LEFT")
    textArea:SetJustifyV("TOP")
    if textArea.SetWordWrap then textArea:SetWordWrap(true) end
    if textArea.SetNonSpaceWrap then textArea:SetNonSpaceWrap(true) end
    textArea:SetWidth(650)
    textArea:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0)

    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
    minimapButton = CreateFrame("Button", addonName .. "BugCatcherMinimapButton", Minimap)
    minimapButton:SetSize(31, 31)
    minimapButton:SetFrameStrata("MEDIUM")
    minimapButton:SetPoint("TOPLEFT", Minimap, "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 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)
        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))
            if originalErrorHandler then
                return originalErrorHandler(message)
            end
        end)
    end
end

function BugCatcher:Init()
    EnsureDB()
    if WantsBugSackCompatibility() and not _G.BugSack then
        -- Optional compatibility mode lets WindTools treat this window like BugSack for skinning.
        -- 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
        PrintConflictNotice(false)
        UpdateMinimapButton()
        return
    end

    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
    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
        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
