-- ApiVault Bug Reporter V2. Added in v1.3.8, hardened in v1.3.9.
-- Cockpit-native error console. Data-only UI: no runtime source file reads and no protected/secure execution.
local addonName, ns = ...
local ApiVault = ns

ApiVault.BugReporterPanelV2 = ApiVault.BugReporterPanelV2 or {}
local Panel = ApiVault.BugReporterPanelV2

Panel.rows = Panel.rows or {}
Panel.selectedId = Panel.selectedId or nil
Panel.startupMode = Panel.startupMode or nil
Panel.maxRows = 11
Panel.activeCategory = Panel.activeCategory or "all"
Panel.showNewOnly = Panel.showNewOnly or false

local CATEGORY_BUTTONS = {
    { key = "all", label = "All" },
    { key = "nil", label = "Nil/defaults" },
    { key = "badarg", label = "Bad arg" },
    { key = "missingapi", label = "Missing API" },
    { key = "taint", label = "Taint" },
    { key = "event", label = "Event" },
    { key = "unknown", label = "Unknown" },
}

local function trim(value)
    value = tostring(value or "")
    return (value:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function lower(value)
    return string.lower(tostring(value or ""))
end

local function createText(parent, template)
    local fs = parent:CreateFontString(nil, "OVERLAY", template or "GameFontNormal")
    fs:SetJustifyH("LEFT")
    fs:SetJustifyV("TOP")
    return fs
end

local function createBackdropFrame(name, parent)
    return CreateFrame("Frame", name, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
end

local function setBackdrop(frame, r, g, b, a)
    if not frame or not frame.SetBackdrop then return end
    frame:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    frame:SetBackdropColor(r or 0.02, g or 0.02, b or 0.03, a or 0.86)
end

local function createButton(parent, label, width, anchor, x, y, onClick)
    local button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    button:SetSize(width or 100, 22)
    if anchor and anchor.GetObjectType then
        button:SetPoint("LEFT", anchor, "RIGHT", x or 6, y or 0)
    else
        button:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 20, y or -20)
    end
    button:SetText(label)
    button:SetScript("OnClick", onClick)
    return button
end

local function shorten(value, limit)
    value = tostring(value or "")
    limit = limit or 90
    if string.len(value) > limit then
        return string.sub(value, 1, limit - 3) .. "..."
    end
    return value
end

local function categoryOf(record)
    return tostring(record and record.advice and record.advice.category or "uncategorized")
end

local function recordLabel(record)
    if not record then return "No error" end
    local state = record.reviewed and "reviewed" or "new"
    return string.format("#%s x%s [%s] %s - %s", tostring(record.id or "?"), tostring(record.count or 1), state, tostring(record.addon or "UNKNOWN"), shorten(record.message or "", 74))
end

local function getBugSettings()
    if ApiVault.GetBugReporterSettings then
        return ApiVault:GetBugReporterSettings()
    end
    return { autoRefreshBridge = true, showNewErrorBadge = true, includeStackInCopiedReports = false }
end

local function categoryMatches(record, key)
    key = tostring(key or "all")
    if key == "all" then return true end
    local category = lower(categoryOf(record))
    local text = lower(tostring(record and record.message or "") .. " " .. tostring(record and record.stack or ""))
    if key == "nil" then
        return string.find(category, "nil", 1, true) ~= nil or string.find(category, "default", 1, true) ~= nil or string.find(category, "savedvariables", 1, true) ~= nil or string.find(text, "nil value", 1, true) ~= nil
    elseif key == "badarg" then
        return string.find(category, "bad%-argument") ~= nil or string.find(category, "argument", 1, true) ~= nil or string.find(text, "bad argument", 1, true) ~= nil
    elseif key == "missingapi" then
        return string.find(category, "missing", 1, true) ~= nil or string.find(category, "call%-nil") ~= nil or string.find(text, "attempt to call", 1, true) ~= nil or string.find(text, "nil value", 1, true) ~= nil
    elseif key == "taint" then
        return string.find(category, "taint", 1, true) ~= nil or string.find(category, "secure", 1, true) ~= nil or string.find(category, "combat", 1, true) ~= nil or string.find(text, "protected", 1, true) ~= nil
    elseif key == "event" then
        return string.find(category, "event", 1, true) ~= nil or string.find(category, "payload", 1, true) ~= nil or string.find(text, "registerevent", 1, true) ~= nil
    elseif key == "unknown" then
        return category == "" or category == "uncategorized" or string.find(category, "unknown", 1, true) ~= nil or string.find(category, "uncertain", 1, true) ~= nil
    end
    return true
end

function Panel:Clear(parent)
    if parent.ApiVaultBugReporterChildren then
        for _, child in ipairs(parent.ApiVaultBugReporterChildren) do
            if child.Hide then child:Hide() end
            if child.SetParent then child:SetParent(nil) end
        end
    end
    parent.ApiVaultBugReporterChildren = {}
    if self.rows then
        if wipe then wipe(self.rows) else for k in pairs(self.rows) do self.rows[k] = nil end end
    else
        self.rows = {}
    end
end

function Panel:Add(parent, child)
    parent.ApiVaultBugReporterChildren = parent.ApiVaultBugReporterChildren or {}
    table.insert(parent.ApiVaultBugReporterChildren, child)
    return child
end

function Panel:GetFilter()
    return trim(self.filterBox and self.filterBox:GetText() or "")
end

function Panel:SetStartupMode(mode)
    self.startupMode = tostring(mode or "")
end

function Panel:SetOutput(text)
    if self.output then
        if ApiVault.CapOutputText then text = ApiVault:CapOutputText(text) end
        self.output:SetText(tostring(text or ""))
        self.output:SetCursorPosition(0)
    end
end

function Panel:SetStatus(text)
    if self.statusText then
        self.statusText:SetText(tostring(text or ""))
    end
end

function Panel:SetSummary(text)
    if self.summaryText then
        self.summaryText:SetText(tostring(text or ""))
    end
end

function Panel:GetFiltered()
    local records = {}
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.GetFilteredErrors then
        records = ApiVault.ErrorInbox:GetFilteredErrors(self:GetFilter(), not self.showNewOnly)
    elseif ApiVault.ErrorInbox and ApiVault.ErrorInbox.GetErrors then
        records = ApiVault.ErrorInbox:GetErrors()
    end
    local out = {}
    for _, record in ipairs(records or {}) do
        if categoryMatches(record, self.activeCategory) and (not self.showNewOnly or not record.reviewed) then
            table.insert(out, record)
        end
    end
    return out
end

function Panel:GetSelectedRecord()
    local id = tonumber(self.selectedId)
    if id and ApiVault.ErrorInbox and ApiVault.ErrorInbox.GetErrorById then
        local found = ApiVault.ErrorInbox:GetErrorById(id)
        if found then return found end
    end
    local records = self:GetFiltered()
    return records and records[1]
end

function Panel:BuildEmptyState()
    if ApiVault.EmptyState and ApiVault.EmptyState.Build then
        return ApiVault.EmptyState:Build("Bug Reporter V2", {
            "No captured errors are selected yet.",
            "This panel ingests ApiVault ErrorInbox, BugGrabber/BugSack bridge data and public API errors.",
            "It does not execute addon code, mutate BugGrabber state or read runtime source files.",
        }, {
            "No Lua error has occurred in this session.",
            "BugGrabber is not loaded or has no buffer entries.",
            "The current filter/category hides all entries.",
            "The bridge was loaded after the error occurred.",
        }, {
            "Click Refresh Bridge.",
            "Trigger or paste a known addon error during development.",
            "Use /av bugsack to open this panel on the bridge status view.",
        })
    end
    return "Bug Reporter V2\n\nNo captured errors selected."
end

function Panel:BuildDetail(record)
    if not record then
        return self:BuildEmptyState()
    end
    local lines = {}
    table.insert(lines, "Bug Reporter V2")
    table.insert(lines, "================")
    table.insert(lines, "")
    table.insert(lines, "Selected error")
    table.insert(lines, "id: " .. tostring(record.id or ""))
    table.insert(lines, "addon: " .. tostring(record.addon or "UNKNOWN"))
    table.insert(lines, "category: " .. categoryOf(record))
    table.insert(lines, "occurrences: " .. tostring(record.count or 1))
    table.insert(lines, "reviewed: " .. tostring(record.reviewed and true or false))
    table.insert(lines, "source: " .. tostring(record.source or ""))
    table.insert(lines, "firstSeen: " .. tostring(record.firstSeen or ""))
    table.insert(lines, "lastSeen: " .. tostring(record.lastSeen or ""))
    if record.file and record.file ~= "" then table.insert(lines, "file: " .. tostring(record.file)) end
    if record.line then table.insert(lines, "line: " .. tostring(record.line)) end
    table.insert(lines, "")
    table.insert(lines, "Message")
    table.insert(lines, "-------")
    table.insert(lines, tostring(record.message or ""))
    table.insert(lines, "")
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.Explain then
        table.insert(lines, ApiVault.ErrorInbox:Explain(record))
    else
        table.insert(lines, "ErrorInbox explanation unavailable.")
    end
    table.insert(lines, "")
    table.insert(lines, "Stack")
    table.insert(lines, "-----")
    table.insert(lines, tostring(record.stack or ""))
    return table.concat(lines, "\n")
end

function Panel:UpdateStatus(records)
    local stats = { total = 0, unread = 0, reviewed = 0, source = "ApiVault" }
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.GetSummaryStats then
        stats = ApiVault.ErrorInbox:GetSummaryStats(self:GetFilter())
    else
        records = records or self:GetFiltered()
        stats.total = #(records or {})
        for _, record in ipairs(records or {}) do
            if record.reviewed then stats.reviewed = stats.reviewed + 1 else stats.unread = stats.unread + 1 end
        end
    end
    self:SetSummary("Total: " .. tostring(stats.total or 0) .. "   New: " .. tostring(stats.unread or 0) .. "   Reviewed: " .. tostring(stats.reviewed or 0) .. "   Active source: " .. tostring(stats.source or "ApiVault"))
    local bridge = "bridge unavailable"
    if ApiVault.BugSackBridge and ApiVault.BugSackBridge.GetSummary then
        bridge = ApiVault.BugSackBridge:GetSummary()
    end
    self:SetStatus("filter=" .. tostring(self:GetFilter() ~= "" and self:GetFilter() or "<all>") .. "  category=" .. tostring(self.activeCategory or "all") .. "  newOnly=" .. tostring(self.showNewOnly and true or false) .. "  " .. tostring(bridge))
end

function Panel:UpdateCategoryButtons()
    for key, button in pairs(self.categoryButtons or {}) do
        if key == self.activeCategory then
            button:Disable()
        else
            button:Enable()
        end
    end
    if self.newOnlyButton then
        self.newOnlyButton:SetText(self.showNewOnly and "Show All" or "Show New Only")
    end
end

function Panel:SelectRecord(record)
    if record then
        self.selectedId = record.id
        self:SetOutput(self:BuildDetail(record))
    else
        self.selectedId = nil
        self:SetOutput(self:BuildEmptyState())
    end
    self:RefreshList()
end

function Panel:RefreshList()
    if not self.listFrame then return end
    local records = self:GetFiltered()
    self:UpdateStatus(records)
    self:UpdateCategoryButtons()
    for i = 1, self.maxRows do
        local row = self.rows and self.rows[i]
        local record = records and records[i]
        if row then
            row.recordId = record and record.id or nil
            if record then
                row:Show()
                row:SetText(recordLabel(record))
                if record.id == self.selectedId then
                    row:SetNormalFontObject("GameFontHighlight")
                elseif record.reviewed then
                    row:SetNormalFontObject("GameFontDisableSmall")
                else
                    row:SetNormalFontObject("GameFontNormalSmall")
                end
            else
                row:Hide()
            end
        end
    end
    if not records or #records == 0 then
        self:SetOutput(self:BuildEmptyState())
    elseif not self:GetSelectedRecord() then
        self.selectedId = records[1].id
        self:SetOutput(self:BuildDetail(records[1]))
    end
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
end

function Panel:RefreshBridge()
    local text
    if ApiVault.BugSackBridge and ApiVault.BugSackBridge.Refresh then
        local ok, msg = ApiVault.BugSackBridge:Refresh("BugReporterV2")
        text = (ApiVault.BugSackBridge.FormatStatus and ApiVault.BugSackBridge:FormatStatus() or tostring(msg)) .. "\n\nRefresh result: " .. tostring(ok) .. " - " .. tostring(msg)
    else
        text = "BugSack/BugGrabber bridge unavailable."
    end
    self:RefreshList()
    self:SetOutput(text)
end

function Panel:ShowBridgeStatus()
    if ApiVault.BugSackBridge and ApiVault.BugSackBridge.FormatStatus then
        self:SetOutput(ApiVault.BugSackBridge:FormatStatus())
    else
        self:SetOutput("BugSack/BugGrabber bridge unavailable.")
    end
    self:UpdateStatus()
end

function Panel:ShowLatest()
    local record = self:GetSelectedRecord()
    if not record then
        local records = self:GetFiltered()
        record = records and records[1]
    end
    self:SelectRecord(record)
end

function Panel:CopyReport(kind)
    local record = self:GetSelectedRecord()
    if not record then
        self:SetOutput("No error selected. Select an error row first or click Refresh Bridge.")
        return
    end
    kind = kind or "github"
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.CopyReport then
        self:SetOutput(ApiVault.ErrorInbox:CopyReport(record, kind))
    elseif ApiVault.ErrorInbox and ApiVault.ErrorInbox.CopyGitHubIssue then
        self:SetOutput(ApiVault.ErrorInbox:CopyGitHubIssue(record))
    else
        self:SetOutput(self:BuildDetail(record))
    end
end

function Panel:MarkReviewed()
    local record = self:GetSelectedRecord()
    if record and ApiVault.ErrorInbox and ApiVault.ErrorInbox.MarkReviewed then
        ApiVault.ErrorInbox:MarkReviewed(record.id, true)
        self:SetOutput("Marked reviewed.\n\n" .. self:BuildDetail(record))
    else
        self:SetOutput("No selected error to mark reviewed.")
    end
    self:RefreshList()
end

function Panel:MarkAllReviewed()
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.MarkAllReviewed then
        local count = ApiVault.ErrorInbox:MarkAllReviewed(self:GetFilter())
        self:SetOutput("Marked reviewed: " .. tostring(count) .. " error(s).")
    else
        self:SetOutput("ErrorInbox MarkAllReviewed unavailable.")
    end
    self:RefreshList()
end

function Panel:ClearReviewed()
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.ClearReviewed then
        local count = ApiVault.ErrorInbox:ClearReviewed()
        self.selectedId = nil
        self:SetOutput("Cleared reviewed ApiVault runtime ErrorInbox entries: " .. tostring(count) .. ".\n\nUnreviewed errors remain. BugGrabber/BugSack external buffers are not mutated.")
    else
        self:SetOutput("ErrorInbox ClearReviewed unavailable.")
    end
    self:RefreshList()
end

function Panel:ClearSession()
    if ApiVault.ErrorInbox and ApiVault.ErrorInbox.ClearSession then
        local count = ApiVault.ErrorInbox:ClearSession()
        self.selectedId = nil
        self:SetOutput("Cleared ApiVault runtime ErrorInbox entries: " .. tostring(count) .. ".\n\nBugGrabber/BugSack external buffers are not mutated.")
    else
        self:SetOutput("ErrorInbox ClearSession unavailable.")
    end
    self:RefreshList()
end

function Panel:OpenRelated(key)
    if ApiVault.AppShell and ApiVault.AppShell.Open then
        ApiVault.AppShell:Open(key)
        return
    end
    self:SetOutput("AppShell unavailable for related tool: " .. tostring(key))
end

function Panel:OpenLegacyAdvisor()
    if ApiVault.BugAdvisorPanel and ApiVault.BugAdvisorPanel.Open then
        ApiVault.BugAdvisorPanel:Open()
        self:SetOutput("[LEGACY PANEL]\nOpened standalone Bug Advisor.\n\nBug Reporter V2 remains the cockpit workflow for /av errors and /av bugsack.")
    else
        self:SetOutput("Legacy Bug Advisor panel unavailable.")
    end
end

function Panel:SelectOutput()
    if self.output then
        self.output:SetFocus()
        self.output:HighlightText()
    end
end

function Panel:SetCategory(key)
    self.activeCategory = tostring(key or "all")
    self.selectedId = nil
    self:RefreshList()
end

function Panel:ToggleNewOnly()
    self.showNewOnly = not self.showNewOnly
    self.selectedId = nil
    self:RefreshList()
end

function Panel:Render(parent)
    self:Clear(parent)

    if ApiVault.Header and ApiVault.Header.Render then
        ApiVault.Header:Render(self, parent, "Bug Reporter V2", "BugSack-style developer error console: capture, dedupe, filter, explain, review and copy reports without executing addon code.", {"LIVE", "READ-ONLY", "INSPECT-ONLY", "BUGGRABBER-BRIDGE", "NO-RUNTIME-FILE-READS"})
    else
        local title = self:Add(parent, createText(parent, "GameFontHighlightLarge"))
        title:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -18)
        title:SetText("Bug Reporter V2")
    end

    local summary = self:Add(parent, createText(parent, "GameFontNormalSmall"))
    self.summaryText = summary
    summary:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -76)
    summary:SetPoint("RIGHT", parent, "RIGHT", -20, 0)
    summary:SetText("Total: 0   New: 0   Reviewed: 0   Active source: ApiVault")

    local filter = self:Add(parent, CreateFrame("EditBox", nil, parent, "InputBoxTemplate"))
    self.filterBox = filter
    filter:SetSize(250, 24)
    filter:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -102)
    filter:SetAutoFocus(false)
    filter:SetText("")
    filter:SetScript("OnEnterPressed", function(box)
        box:ClearFocus()
        Panel.selectedId = nil
        Panel:RefreshList()
        Panel:ShowLatest()
    end)

    local refresh = self:Add(parent, createButton(parent, "Refresh Bridge", 116, filter, 10, 0, function() Panel:RefreshBridge() end))
    local latest = self:Add(parent, createButton(parent, "Latest", 68, refresh, 6, 0, function() Panel:ShowLatest() end))
    local bridge = self:Add(parent, createButton(parent, "Bridge Status", 106, latest, 6, 0, function() Panel:ShowBridgeStatus() end))
    local markdown = self:Add(parent, createButton(parent, "Copy MD", 82, bridge, 6, 0, function() Panel:CopyReport("markdown") end))
    local repro = self:Add(parent, createButton(parent, "Copy Repro", 96, markdown, 6, 0, function() Panel:CopyReport("repro") end))
    local github = self:Add(parent, createButton(parent, "GitHub", 72, repro, 6, 0, function() Panel:CopyReport("github") end))
    local curse = self:Add(parent, createButton(parent, "Curse", 68, github, 6, 0, function() Panel:CopyReport("curseforge") end))

    self.categoryButtons = {}
    local previous
    for index, item in ipairs(CATEGORY_BUTTONS) do
        local width = item.key == "nil" and 96 or (item.key == "missingapi" and 94 or 78)
        local button
        if index == 1 then
            button = self:Add(parent, createButton(parent, item.label, width, nil, 20, -134, function() Panel:SetCategory(item.key) end))
        else
            button = self:Add(parent, createButton(parent, item.label, width, previous, 6, 0, function() Panel:SetCategory(item.key) end))
        end
        self.categoryButtons[item.key] = button
        previous = button
    end
    self.newOnlyButton = self:Add(parent, createButton(parent, self.showNewOnly and "Show All" or "Show New Only", 112, previous, 6, 0, function() Panel:ToggleNewOnly() end))

    local mark = self:Add(parent, createButton(parent, "Mark Reviewed", 116, nil, 20, -166, function() Panel:MarkReviewed() end))
    local markAll = self:Add(parent, createButton(parent, "Mark All", 82, mark, 6, 0, function() Panel:MarkAllReviewed() end))
    local clearReviewed = self:Add(parent, createButton(parent, "Clear Reviewed", 120, markAll, 6, 0, function() Panel:ClearReviewed() end))
    local clear = self:Add(parent, createButton(parent, "Clear Session", 108, clearReviewed, 6, 0, function() Panel:ClearSession() end))
    local selectOut = self:Add(parent, createButton(parent, "Select Output", 108, clear, 6, 0, function() Panel:SelectOutput() end))
    self:Add(parent, createButton(parent, "Legacy Advisor", 112, selectOut, 6, 0, function() Panel:OpenLegacyAdvisor() end))

    local related = self:Add(parent, createText(parent, "GameFontDisableSmall"))
    related:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -198)
    related:SetText("Related tools:")
    local addonTool = self:Add(parent, createButton(parent, "Addon Inspector", 118, nil, 104, -194, function() Panel:OpenRelated("addons") end))
    local apiTool = self:Add(parent, createButton(parent, "API Search", 88, addonTool, 6, 0, function() Panel:OpenRelated("api") end))
    local secureTool = self:Add(parent, createButton(parent, "Secure/Taint", 104, apiTool, 6, 0, function() Panel:OpenRelated("secure") end))
    self:Add(parent, createButton(parent, "Reports", 78, secureTool, 6, 0, function() Panel:OpenRelated("reports") end))

    local status = self:Add(parent, createText(parent, "GameFontDisableSmall"))
    self.statusText = status
    status:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -226)
    status:SetPoint("RIGHT", parent, "RIGHT", -20, 0)
    status:SetText("filter=<all>  category=all  newOnly=false  bridge not refreshed")

    local listBg = self:Add(parent, createBackdropFrame(nil, parent))
    self.listFrame = listBg
    listBg:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -252)
    listBg:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 20, 20)
    listBg:SetWidth(326)
    setBackdrop(listBg, 0.015, 0.015, 0.02, 0.9)

    local listTitle = self:Add(parent, createText(listBg, "GameFontHighlightSmall"))
    listTitle:SetPoint("TOPLEFT", listBg, "TOPLEFT", 12, -10)
    listTitle:SetText("Captured errors")

    local previousRow
    for i = 1, self.maxRows do
        local row = self:Add(parent, CreateFrame("Button", nil, listBg, "UIPanelButtonTemplate"))
        row:SetHeight(24)
        row:SetPoint("LEFT", listBg, "LEFT", 10, 0)
        row:SetPoint("RIGHT", listBg, "RIGHT", -10, 0)
        if previousRow then
            row:SetPoint("TOP", previousRow, "BOTTOM", 0, -4)
        else
            row:SetPoint("TOP", listBg, "TOP", 0, -34)
        end
        row:SetText("")
        row:SetScript("OnClick", function(button)
            if button.recordId and ApiVault.ErrorInbox and ApiVault.ErrorInbox.GetErrorById then
                Panel:SelectRecord(ApiVault.ErrorInbox:GetErrorById(button.recordId))
            end
        end)
        row:Hide()
        self.rows[i] = row
        previousRow = row
    end

    local detailBg = self:Add(parent, createBackdropFrame(nil, parent))
    detailBg:SetPoint("TOPLEFT", listBg, "TOPRIGHT", 12, 0)
    detailBg:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -20, 20)
    setBackdrop(detailBg, 0.02, 0.02, 0.03, 0.86)

    local scroll = self:Add(parent, CreateFrame("ScrollFrame", nil, detailBg, "UIPanelScrollFrameTemplate"))
    scroll:SetPoint("TOPLEFT", detailBg, "TOPLEFT", 12, -12)
    scroll:SetPoint("BOTTOMRIGHT", detailBg, "BOTTOMRIGHT", -30, 12)

    local output = self:Add(parent, CreateFrame("EditBox", nil, scroll))
    self.output = output
    output:SetMultiLine(true)
    output:SetAutoFocus(false)
    output:SetFontObject(ChatFontNormal)
    output:SetWidth(580)
    output:SetText(self:BuildEmptyState())
    output:SetScript("OnEscapePressed", function(box) box:ClearFocus() end)
    scroll:SetScrollChild(output)

    parent:SetScript("OnSizeChanged", function()
        if output and detailBg then output:SetWidth(math.max(420, detailBg:GetWidth() - 50)) end
    end)

    local settings = getBugSettings()
    if self.startupMode == "bridge" then
        self.startupMode = nil
        self:RefreshBridge()
    elseif settings.autoRefreshBridge then
        self:RefreshBridge()
    else
        self:RefreshList()
    end
end
