-- ApiVault Error Inbox + Bug Reporter data model.
-- Data-only; does not execute external code and does not read source files at runtime.
local addonName, ns = ...
local ApiVault = ns

ApiVault.ErrorInbox = ApiVault.ErrorInbox or {}
local Inbox = ApiVault.ErrorInbox

Inbox.errors = Inbox.errors or {}
Inbox.byFingerprint = Inbox.byFingerprint or {}
Inbox.nextId = Inbox.nextId or 1
Inbox.maxErrors = 250
Inbox.reviewedCount = Inbox.reviewedCount or 0

local function now()
    return GetTime and GetTime() or 0
end

local function wallTime()
    return date and date("%Y-%m-%d %H:%M:%S") or tostring(now())
end

local function clamp(value, limit)
    value = tostring(value or "")
    limit = limit or 12000
    if string.len(value) > limit then
        return string.sub(value, 1, limit) .. "\n<truncated by ApiVault ErrorInbox>"
    end
    return value
end

local function clearTable(tbl)
    if type(tbl) ~= "table" then return end
    if wipe then
        wipe(tbl)
        return
    end
    for key in pairs(tbl) do
        tbl[key] = nil
    end
end

local function normalize(message)
    message = tostring(message or "")
    message = string.gsub(message, "%d%d?:%d%d?:%d%d?", "<time>")
    message = string.gsub(message, "line %d+", "line <n>")
    message = string.gsub(message, ":%d+:", ":<n>:")
    message = string.gsub(message, "%.lua:%d+", ".lua:<n>")
    message = string.gsub(message, "0x%x+", "0x<hex>")
    return message
end

local function compactOneLine(value, limit)
    value = tostring(value or "")
    value = value:gsub("\r", " "):gsub("\n", " "):gsub("%s+", " ")
    limit = limit or 240
    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 ensureHistoryDB(create)
    if type(ApiVaultDB) ~= "table" then
        if not create then return nil end
        ApiVaultDB = {}
    end
    ApiVaultDB.errorHistory = type(ApiVaultDB.errorHistory) == "table" and ApiVaultDB.errorHistory or {}
    local history = ApiVaultDB.errorHistory
    if history.enabled == nil then history.enabled = false end
    if history.maxEntries == nil then history.maxEntries = 100 end
    if history.entries == nil or type(history.entries) ~= "table" then history.entries = {} end
    return history
end

local function getBugSettings()
    if ApiVault.GetBugReporterSettings then
        return ApiVault:GetBugReporterSettings()
    end
    local settings = ensureHistoryDB(true) or {}
    settings.autoRefreshBridge = settings.autoRefreshBridge ~= false
    settings.showNewErrorBadge = settings.showNewErrorBadge ~= false
    settings.includeStackInCopiedReports = settings.includeStackInCopiedReports and true or false
    return settings
end

function Inbox:StableKey(record)
    record = record or {}
    return table.concat({
        tostring(record.addon or "UNKNOWN_ADDON"),
        normalize(record.message or ""),
        normalize(record.file or ""),
        tostring(record.line or ""),
    }, "|")
end

function Inbox:Fingerprint(record)
    return self:StableKey(record)
end

function Inbox:BuildSummary(record)
    if not record then return nil end
    return {
        key = tostring(record.fingerprint or self:StableKey(record)),
        addon = compactOneLine(record.addon or "UNKNOWN_ADDON", 256),
        message = compactOneLine(record.message or "unknown error", 2000),
        category = compactOneLine(categoryOf(record), 256),
        count = tonumber(record.count) or 1,
        firstSeen = tostring(record.firstSeen or wallTime()),
        lastSeen = tostring(record.lastSeen or wallTime()),
        reviewed = record.reviewed and true or false,
        source = compactOneLine(record.source or "", 256),
    }
end

function Inbox:PersistSummary(record)
    local history = ensureHistoryDB(false)
    if not history or not history.enabled then return false end
    local entries = history.entries or {}
    history.entries = entries
    local summary = self:BuildSummary(record)
    if not summary then return false end
    for index, existing in ipairs(entries) do
        if existing and existing.key == summary.key then
            existing.addon = summary.addon
            existing.message = summary.message
            existing.category = summary.category
            existing.count = summary.count
            existing.lastSeen = summary.lastSeen
            existing.reviewed = summary.reviewed
            existing.source = summary.source
            if index > 1 then
                table.remove(entries, index)
                table.insert(entries, 1, existing)
            end
            return true
        end
    end
    table.insert(entries, 1, summary)
    local maxEntries = tonumber(history.maxEntries) or 100
    maxEntries = math.max(10, math.min(500, math.floor(maxEntries)))
    while #entries > maxEntries do
        table.remove(entries)
    end
    return true
end

function Inbox:UpdateHistoryReviewed(record)
    local history = ensureHistoryDB(false)
    if not history or type(history.entries) ~= "table" or not record then return end
    local key = tostring(record.fingerprint or self:StableKey(record))
    for _, entry in ipairs(history.entries) do
        if entry and entry.key == key then
            entry.reviewed = record.reviewed and true or false
            entry.count = tonumber(record.count) or entry.count or 1
            entry.lastSeen = tostring(record.lastSeen or entry.lastSeen or "")
            return
        end
    end
end

function Inbox:Ingest(record)
    record = record or {}
    local item = {
        addon = clamp(record.addon or record.addonName or record.owner or "UNKNOWN_ADDON", 256),
        message = clamp(record.message or record.error or "unknown error", 12000),
        stack = clamp(record.stack or "", 24000),
        source = clamp(record.source or "ErrorInbox", 256),
        file = clamp(record.file or "", 1024),
        line = tonumber(record.line) or nil,
        context = clamp(record.context or "", 2000),
        firstSeen = wallTime(),
        lastSeen = wallTime(),
        count = tonumber(record.count) or 1,
        reviewed = false,
        reviewedAt = nil,
    }
    item.fingerprint = self:Fingerprint(item)

    local existing = self.byFingerprint[item.fingerprint]
    if existing then
        existing.count = (existing.count or 1) + (item.count or 1)
        existing.lastSeen = wallTime()
        existing.stack = item.stack ~= "" and item.stack or existing.stack
        existing.source = item.source
        existing.context = item.context
        existing.reviewed = false
        existing.reviewedAt = nil
        if ApiVault.BugAdvisor and ApiVault.BugAdvisor.Analyze then
            existing.advice = ApiVault.BugAdvisor:Analyze(existing)
        end
        if ApiVault.BugAdvisor and ApiVault.BugAdvisor.ResolveLinks then
            existing.links = ApiVault.BugAdvisor:ResolveLinks(existing, existing.advice)
        end
        if ApiVault.BugAdvisor and ApiVault.BugAdvisor.BuildFixChecklist and existing.advice then
            existing.fixChecklist = ApiVault.BugAdvisor:BuildFixChecklist(existing.advice.category, existing.links)
        end
        self:PersistSummary(existing)
        if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
        return existing, "deduplicated"
    end

    item.id = self.nextId or 1
    self.nextId = item.id + 1

    if ApiVault.BugAdvisor and ApiVault.BugAdvisor.Analyze then
        item.advice = ApiVault.BugAdvisor:Analyze(item)
    end
    if ApiVault.BugAdvisor and ApiVault.BugAdvisor.ResolveLinks then
        item.links = ApiVault.BugAdvisor:ResolveLinks(item, item.advice)
    end
    if ApiVault.BugAdvisor and ApiVault.BugAdvisor.BuildFixChecklist and item.advice then
        item.fixChecklist = ApiVault.BugAdvisor:BuildFixChecklist(item.advice.category, item.links)
    end

    table.insert(self.errors, 1, item)
    self.byFingerprint[item.fingerprint] = item
    while #self.errors > (self.maxErrors or 250) do
        local old = table.remove(self.errors)
        if old and old.fingerprint then
            self.byFingerprint[old.fingerprint] = nil
        end
    end
    self:PersistSummary(item)
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
    return item, "ingested"
end

function Inbox:Explain(record)
    if not record then
        return "No error selected."
    end
    local category = categoryOf(record)
    local lines = {}
    table.insert(lines, "Explain This Error")
    table.insert(lines, "id: " .. tostring(record.id or ""))
    table.insert(lines, "addon: " .. tostring(record.addon or ""))
    table.insert(lines, "category: " .. tostring(category))
    table.insert(lines, "occurrences: " .. tostring(record.count or 1))
    table.insert(lines, "firstSeen: " .. tostring(record.firstSeen or ""))
    table.insert(lines, "lastSeen: " .. tostring(record.lastSeen or ""))
    table.insert(lines, "")
    table.insert(lines, "Message:")
    table.insert(lines, tostring(record.message or ""))
    table.insert(lines, "")
    table.insert(lines, "Likely meaning:")
    if category == "nil-value/default-initialization" then
        table.insert(lines, "A table or field is nil before defaults were initialized. Check SavedVariables, module load order and nested table creation.")
    elseif category == "call-nil/missing-method-or-load-order" then
        table.insert(lines, "The code called a nil function. Check TOC order, missing modules, API availability and optional dependencies.")
    elseif category == "bad-argument/api-signature" then
        table.insert(lines, "An API/function was called with an unexpected argument type/count. Compare the call with ApiVault API metadata.")
    elseif category == "protected/taint" or category == "secure-ui/combat-sensitive" then
        table.insert(lines, "The error is secure/protected/taint-sensitive. Do not fix by forcing execution. Separate secure paths and test OOC/combat.")
    elseif category == "xml-template/missing-template" then
        table.insert(lines, "A template or XML frame reference may be missing or unavailable. Check XML Template Catalog and TOC dependencies.")
    elseif category == "savedvariables/schema" then
        table.insert(lines, "SavedVariables schema/defaults/migration may be inconsistent. Check declarations and defensive default creation.")
    elseif string.find(category, "event", 1, true) or string.find(category, "payload", 1, true) then
        table.insert(lines, "An event or payload shape likely changed or was registered incorrectly. Verify event name, payload count and event timing.")
    else
        table.insert(lines, "Category is uncertain. Use stack, linked symbols and source context before changing code.")
    end
    table.insert(lines, "")
    table.insert(lines, "Checklist:")
    local checklist = record.fixChecklist or (ApiVault.BugAdvisor and ApiVault.BugAdvisor.BuildFixChecklist and ApiVault.BugAdvisor:BuildFixChecklist(category, record.links)) or {}
    if #checklist == 0 and ApiVault.BugAdvisor and ApiVault.BugAdvisor.BuildChecklist then
        checklist = ApiVault.BugAdvisor:BuildChecklist(category)
    end
    for _, item in ipairs(checklist or {}) do
        table.insert(lines, tostring(item))
    end
    if #(checklist or {}) == 0 then
        table.insert(lines, "- Reproduce once after /reload.")
        table.insert(lines, "- Identify addon, file, line and event/click path.")
        table.insert(lines, "- Confirm whether combat lockdown or SavedVariables timing is involved.")
    end
    table.insert(lines, "")
    table.insert(lines, "Linked metadata:")
    local links = record.links or {}
    local anyLink = false
    if links.apis and #links.apis > 0 then anyLink = true; table.insert(lines, "APIs: " .. table.concat(links.apis, ", ")) end
    if links.events and #links.events > 0 then anyLink = true; table.insert(lines, "Events: " .. table.concat(links.events, ", ")) end
    if links.templates and #links.templates > 0 then anyLink = true; table.insert(lines, "Templates: " .. table.concat(links.templates, ", ")) end
    if links.modules and #links.modules > 0 then anyLink = true; table.insert(lines, "Modules: " .. table.concat(links.modules, ", ")) end
    if links.securePatterns and #links.securePatterns > 0 then anyLink = true; table.insert(lines, "Secure: " .. table.concat(links.securePatterns, ", ")) end
    if not anyLink then
        table.insert(lines, "No direct metadata links detected.")
    end
    table.insert(lines, "")
    table.insert(lines, "UNCERTAIN: Explanation is generated from error text/stack metadata. Validate against addon source and live behavior.")
    return table.concat(lines, "\n")
end

function Inbox:GetErrors()
    return self.errors or {}
end

function Inbox:GetErrorById(id)
    id = tonumber(id)
    for _, record in ipairs(self.errors or {}) do
        if record.id == id then return record end
    end
end

function Inbox:GetLatestMatch(filter)
    filter = string.lower(tostring(filter or ""))
    for _, record in ipairs(self.errors or {}) do
        local hay = string.lower(tostring(record.addon or "") .. " " .. tostring(record.message or "") .. " " .. tostring(record.source or "") .. " " .. tostring(record.stack or "") .. " " .. categoryOf(record))
        if filter == "" or string.find(hay, filter, 1, true) then
            return record
        end
    end
end

function Inbox:GetStats()
    return #(self.errors or {}), self.nextId or 1
end

function Inbox:Matches(record, filter, includeReviewed)
    if not record then return false end
    if not includeReviewed and record.reviewed then return false end
    filter = string.lower(tostring(filter or ""))
    if filter == "" then return true end
    local hay = string.lower(table.concat({
        tostring(record.addon or ""),
        tostring(record.message or ""),
        tostring(record.source or ""),
        tostring(record.file or ""),
        tostring(record.stack or ""),
        categoryOf(record),
    }, " "))
    return string.find(hay, filter, 1, true) ~= nil
end

function Inbox:GetFilteredErrors(filter, includeReviewed)
    local out = {}
    for _, record in ipairs(self.errors or {}) do
        if self:Matches(record, filter, includeReviewed ~= false) then
            table.insert(out, record)
        end
    end
    return out
end

function Inbox:GetCounts(filter)
    local total, unread = 0, 0
    for _, record in ipairs(self.errors or {}) do
        if self:Matches(record, filter, true) then
            total = total + 1
            if not record.reviewed then unread = unread + 1 end
        end
    end
    return total, unread
end

function Inbox:GetReviewedCount(filter)
    local reviewed = 0
    for _, record in ipairs(self.errors or {}) do
        if self:Matches(record, filter, true) and record.reviewed then
            reviewed = reviewed + 1
        end
    end
    return reviewed
end

function Inbox:GetUnreadCount(filter)
    local _, unread = self:GetCounts(filter)
    return unread
end

function Inbox:GetSummaryStats(filter)
    local total, unread = self:GetCounts(filter)
    local reviewed = self:GetReviewedCount(filter)
    local source = "ApiVault"
    if ApiVault.BugSackBridge and ApiVault.BugSackBridge.GetActiveSource then
        source = ApiVault.BugSackBridge:GetActiveSource()
    elseif ApiVault.BugSackBridge and ApiVault.BugSackBridge.GetSummary then
        source = ApiVault.BugSackBridge:GetSummary()
    end
    return {
        total = total,
        unread = unread,
        reviewed = reviewed,
        source = source,
    }
end

function Inbox:IsReviewed(id)
    local record = type(id) == "table" and id or self:GetErrorById(id)
    return record and record.reviewed and true or false
end

function Inbox:MarkReviewed(id, reviewed)
    local record = type(id) == "table" and id or self:GetErrorById(id)
    if not record then return false end
    local wasReviewed = record.reviewed and true or false
    record.reviewed = reviewed ~= false
    record.reviewedAt = record.reviewed and wallTime() or nil
    if record.reviewed and not wasReviewed then
        self.reviewedCount = (self.reviewedCount or 0) + 1
    end
    self:UpdateHistoryReviewed(record)
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
    return true, record
end

function Inbox:MarkAllReviewed(filter)
    local count = 0
    for _, record in ipairs(self.errors or {}) do
        if self:Matches(record, filter, true) and not record.reviewed then
            record.reviewed = true
            record.reviewedAt = wallTime()
            self:UpdateHistoryReviewed(record)
            count = count + 1
        end
    end
    self.reviewedCount = (self.reviewedCount or 0) + count
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
    return count
end

function Inbox:ClearReviewed()
    local kept = {}
    local removed = 0
    clearTable(self.byFingerprint)
    for _, record in ipairs(self.errors or {}) do
        if record.reviewed then
            removed = removed + 1
        else
            table.insert(kept, record)
            if record.fingerprint then self.byFingerprint[record.fingerprint] = record end
        end
    end
    clearTable(self.errors)
    for _, record in ipairs(kept) do table.insert(self.errors, record) end
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
    return removed
end

function Inbox:ClearSession()
    local count = #(self.errors or {})
    clearTable(self.errors)
    clearTable(self.byFingerprint)
    self.nextId = 1
    if ApiVault.AppShell and ApiVault.AppShell.UpdateNavBadges then ApiVault.AppShell:UpdateNavBadges() end
    return count
end

function Inbox:GetReportStack(record)
    local settings = getBugSettings()
    if settings.includeStackInCopiedReports then
        return tostring(record and record.stack or "")
    end
    return "<stack omitted by ApiVault setting: includeStackInCopiedReports=false>"
end

function Inbox:CopyGitHubIssue(record)
    if not record then return "No error selected." end
    return table.concat({
        "## Bug report",
        "",
        "**Addon:** " .. tostring(record.addon or ""),
        "**ApiVault category:** " .. tostring(categoryOf(record)),
        "**Occurrences:** " .. tostring(record.count or 1),
        "**Reviewed:** " .. tostring(record.reviewed and true or false),
        "**Source:** " .. tostring(record.source or ""),
        "",
        "### Error",
        "```text",
        tostring(record.message or ""),
        "```",
        "",
        "### Stack",
        "```text",
        self:GetReportStack(record),
        "```",
        "",
        "### ApiVault explanation",
        "```text",
        self:Explain(record),
        "```",
    }, "\n")
end

function Inbox:CopyCurseForgeReport(record)
    if not record then return "No error selected." end
    return table.concat({
        "ApiVault CurseForge Bug Report",
        "",
        "Addon: " .. tostring(record.addon or "UNKNOWN"),
        "ApiVault category: " .. tostring(categoryOf(record)),
        "Occurrences this session: " .. tostring(record.count or 1),
        "First seen: " .. tostring(record.firstSeen or ""),
        "Last seen: " .. tostring(record.lastSeen or ""),
        "Source: " .. tostring(record.source or ""),
        "Reviewed: " .. tostring(record.reviewed and true or false),
        "",
        "Error message:",
        tostring(record.message or ""),
        "",
        "Stack trace:",
        self:GetReportStack(record),
        "",
        "ApiVault analysis:",
        self:Explain(record),
        "",
        "Note: generated by ApiVault Bug Reporter V2 from captured error text and metadata. Validate against addon source before applying a fix.",
    }, "\n")
end

function Inbox:CopyMarkdownReport(record)
    if not record then return "No error selected." end
    return table.concat({
        "# ApiVault Error Analysis",
        "",
        "| Field | Value |",
        "|---|---|",
        "| Addon | " .. tostring(record.addon or "UNKNOWN") .. " |",
        "| Category | " .. tostring(categoryOf(record)) .. " |",
        "| Occurrences | " .. tostring(record.count or 1) .. " |",
        "| Reviewed | " .. tostring(record.reviewed and true or false) .. " |",
        "| Source | " .. tostring(record.source or "") .. " |",
        "| First seen | " .. tostring(record.firstSeen or "") .. " |",
        "| Last seen | " .. tostring(record.lastSeen or "") .. " |",
        "",
        "## Error message",
        "```text",
        tostring(record.message or ""),
        "```",
        "",
        "## Stack",
        "```text",
        self:GetReportStack(record),
        "```",
        "",
        "## ApiVault explanation",
        "```text",
        self:Explain(record),
        "```",
    }, "\n")
end

function Inbox:CopyMinimalReproChecklist(record)
    local addon = record and tostring(record.addon or "UNKNOWN") or "UNKNOWN"
    local message = record and tostring(record.message or "") or ""
    return table.concat({
        "# Minimal Reproduction Checklist",
        "",
        "Addon: " .. addon,
        "Category: " .. tostring(record and categoryOf(record) or "uncategorized"),
        "",
        "## Repro steps",
        "1. Start from `/reload` with BugGrabber/BugSack enabled if available.",
        "2. Open the relevant addon UI or trigger the feature path.",
        "3. Record whether the error happens out of combat or during combat.",
        "4. Reproduce once with only the target addon plus ApiVault/BugGrabber enabled.",
        "5. Capture the exact click path, slash command or event sequence.",
        "",
        "## Expected",
        "- No Lua error and the feature completes its documented behavior.",
        "",
        "## Actual",
        "```text",
        message,
        "```",
        "",
        "## Data to attach",
        "- ApiVault Copy Markdown Report output.",
        "- Addon version and TOC Interface.",
        "- Whether SavedVariables were freshly reset.",
        "- Whether this occurred in combat, in an instance, or after reload/login.",
    }, "\n")
end

function Inbox:CopyPlainReport(record)
    if not record then return "No error selected." end
    return table.concat({
        "ApiVault Error Report",
        "id=" .. tostring(record.id or ""),
        "addon=" .. tostring(record.addon or "UNKNOWN"),
        "category=" .. tostring(categoryOf(record)),
        "count=" .. tostring(record.count or 1),
        "reviewed=" .. tostring(record.reviewed and true or false),
        "source=" .. tostring(record.source or ""),
        "message=" .. tostring(record.message or ""),
        "stack=" .. self:GetReportStack(record),
    }, "\n")
end

function Inbox:CopyReport(record, formatName)
    formatName = string.lower(tostring(formatName or "github"))
    if formatName == "curse" or formatName == "curseforge" then
        return self:CopyCurseForgeReport(record)
    elseif formatName == "plain" or formatName == "text" then
        return self:CopyPlainReport(record)
    elseif formatName == "markdown" or formatName == "md" then
        return self:CopyMarkdownReport(record)
    elseif formatName == "repro" or formatName == "checklist" then
        return self:CopyMinimalReproChecklist(record)
    end
    return self:CopyGitHubIssue(record)
end

function Inbox:FormatList(filter)
    filter = string.lower(tostring(filter or ""))
    local lines = {}
    table.insert(lines, "ApiVault Error Inbox")
    table.insert(lines, "errors: " .. tostring(#(self.errors or {})))
    table.insert(lines, "")
    local shown = 0
    for _, record in ipairs(self.errors or {}) do
        local hay = string.lower(tostring(record.addon or "") .. " " .. tostring(record.message or "") .. " " .. tostring(record.source or "") .. " " .. categoryOf(record))
        if filter == "" or string.find(hay, filter, 1, true) then
            table.insert(lines, "#" .. tostring(record.id) .. " x" .. tostring(record.count or 1) .. " [" .. categoryOf(record) .. "] " .. tostring(record.addon) .. " - " .. tostring(record.message))
            shown = shown + 1
        end
    end
    if #(self.errors or {}) == 0 then
        table.insert(lines, "No errors ingested yet. Use ApiVault.ErrorInbox:Ingest({...}) or Public Developer API ReportError.")
    elseif shown == 0 then
        table.insert(lines, "No matching errors for filter: " .. tostring(filter))
        table.insert(lines, "Tip: use the Addon Lookup tool if you typed an addon name such as StockVault.")
    end
    return table.concat(lines, "\n")
end

function Inbox:FormatBugReporterList(filter)
    local lines = {}
    local total, unread = self:GetCounts(filter)
    local reviewed = self:GetReviewedCount(filter)
    table.insert(lines, "ApiVault Bug Reporter V2")
    table.insert(lines, "errors: " .. tostring(total) .. " new: " .. tostring(unread) .. " reviewed: " .. tostring(reviewed))
    table.insert(lines, "")
    local shown = 0
    for _, record in ipairs(self.errors or {}) do
        if self:Matches(record, filter, true) then
            local state = record.reviewed and "reviewed" or "new"
            table.insert(lines, "#" .. tostring(record.id) .. " x" .. tostring(record.count or 1) .. " [" .. state .. "] [" .. categoryOf(record) .. "] " .. tostring(record.addon) .. " - " .. tostring(record.message))
            shown = shown + 1
        end
    end
    if shown == 0 then
        table.insert(lines, "No matching errors. Refresh the BugGrabber bridge or clear the filter.")
    end
    return table.concat(lines, "\n")
end

function Inbox:FormatBugReporterSummary(filter)
    local stats = self:GetSummaryStats(filter)
    local lines = {}
    table.insert(lines, "ApiVault Bug Reporter Summary")
    table.insert(lines, "=============================")
    table.insert(lines, "Total errors: " .. tostring(stats.total or 0))
    table.insert(lines, "New/unreviewed: " .. tostring(stats.unread or 0))
    table.insert(lines, "Reviewed: " .. tostring(stats.reviewed or 0))
    table.insert(lines, "Active source: " .. tostring(stats.source or "ApiVault"))
    table.insert(lines, "")
    table.insert(lines, self:FormatBugReporterList(filter))
    local history = ensureHistoryDB(false)
    if history then
        table.insert(lines, "")
        table.insert(lines, "History persistence: " .. tostring(history.enabled and true or false) .. "; maxEntries=" .. tostring(history.maxEntries or 100) .. "; stored=" .. tostring(type(history.entries) == "table" and #history.entries or 0))
    end
    return table.concat(lines, "\n")
end

function ApiVault:IngestError(record)
    return ApiVault.ErrorInbox:Ingest(record)
end
