--[[
    Core/Debug.lua

    Diagnostics channel. Disabled by default: nothing reaches the chat frame
    unless the user turns it on with "/shiny debug".

    Messages are always kept in a small ring buffer even while debug output is
    off, so "/shiny debug dump" can show what happened during start-up after the
    fact -- which is exactly when the user cannot have had debug enabled yet.
]]

local _, ns = ...

local Debug = ns.NewModule("Debug")
ns.Debug = Debug

local RING_CAPACITY = 250

local ring = {}
local ringCount = 0
local ringNext = 1

-- The last message printed to chat for each category, so a line that repeats
-- itself does not repeat itself. Keyed by category, not by whole line, so a
-- changed value in the same category still gets through.
local lastPrinted = {}

-- Start-up says the same thirteen lines every login: which modules woke up,
-- how many records loaded, which character was resolved. None of it is news
-- to a player, and with diagnostics left on it greeted them at every reload.
--
-- Those lines are still recorded, so "/shiny debug dump" answers "what
-- happened while the addon was starting" -- which is exactly the question a
-- player cannot have had diagnostics enabled in time to see anyway. Chat only
-- starts receiving once the addon is up and something is actually happening.
local booted = false

Debug.enabled = false

--- Records a diagnostic line, and prints it when debug output is enabled.
-- @param category string Short tag, e.g. "DB", "UI", "DATA".
-- @param message string Message or format string.
-- @param ... any Optional string.format arguments.
function Debug:Log(category, message, ...)
    if select("#", ...) > 0 then
        message = message:format(...)
    end

    local line = ("[%s] %s"):format(category, message)

    ring[ringNext] = line
    ringNext = ringNext % RING_CAPACITY + 1
    if ringCount < RING_CAPACITY then
        ringCount = ringCount + 1
    end

    if not self.enabled or not booted then
        return
    end

    -- Chat only gets a line when it says something new for its category.
    --
    -- Opening the world map refreshes its pins several times, and each refresh
    -- reported the same counts, so a few minutes of ordinary play buried every
    -- useful line under dozens of identical ones. The ring buffer above still
    -- keeps every repeat, so "/shiny debug dump" remains complete: this
    -- suppresses the noise, not the record.
    if lastPrinted[category] == message then
        return
    end
    lastPrinted[category] = message

    ns.Print("|cff888888" .. line .. "|r")
end

--- Records a problem. Always kept, printed only in debug mode.
function Debug:Warn(category, message, ...)
    self:Log(category, "|cffff8800WARN|r " .. message, ...)
end

--- Turns debug output on or off.
-- @param enabled boolean
--- Called once the enable sequence is over. Until then diagnostics are kept
--- but not printed, so a reload does not greet the player with a wall of text.
function Debug:MarkBooted()
    booted = true
end

function Debug:SetEnabled(enabled)
    self.enabled = enabled and true or false

    -- Turning diagnostics on should show the current state of things, even if
    -- the last line of a category is the same one suppressed earlier.
    wipe(lastPrinted)
end

--- @return boolean Whether debug output currently reaches the chat frame.
function Debug:IsEnabled()
    return self.enabled
end

--- Prints the buffered lines, oldest first.
-- @param limit number|nil How many lines to show (default: all buffered).
function Debug:Dump(limit)
    if ringCount == 0 then
        ns.Print(ns.L["DEBUG_DUMP_EMPTY"])
        return
    end

    limit = math.min(limit or ringCount, ringCount)

    -- The newest `limit` entries end just before the write cursor.
    local first = ringNext - limit

    ns.Print(ns.Plural("DEBUG_DUMP_HEADER", limit):format(limit))
    for offset = 0, limit - 1 do
        local index = (first + offset - 1) % RING_CAPACITY + 1
        ns.Print("|cff888888" .. ring[index] .. "|r")
    end
end

--- Reports an unexpected Lua error without hiding it from the user.
-- Used by the start-up sequence so one broken module cannot silently take the
-- whole addon down.
-- @param context string Where the failure happened.
-- @param errorMessage string
function Debug:ReportError(context, errorMessage)
    local fullMessage = tostring(errorMessage)
    local firstLine = fullMessage:match("^[^\r\n]+") or fullMessage

    ns.Print("|cffff2020" .. ns.L["ERROR_IN_CONTEXT"]:format(context) .. "|r")
    ns.Print("|cffff2020" .. fullMessage .. "|r")

    -- A multiline stack trace scrolls its useful first line out of a small
    -- chat window. Repeat a compact summary last so screenshots always retain
    -- the actual failure instead of only the tail of the call stack.
    if fullMessage:find("[\r\n]") then
        ns.Print("|cffff6060ERROR: " .. firstLine .. "|r")
    end

    self:Log("ERROR", "%s: %s", context, fullMessage)
end

-- Blizzard's stock dialogs omit the function name. Keep a compact, safe
-- diagnostic in chat so a client restriction can be fixed from evidence
-- instead of guesswork.
--
-- There are TWO distinct client reactions and they are not interchangeable:
--
--   ADDON_ACTION_BLOCKED    tainted execution reached a protected function.
--                           Reported as a chat message, no dialog.
--   ADDON_ACTION_FORBIDDEN  an addon called something addons may never call.
--                           This is the one that raises the "You can disable
--                           this AddOn and reload the UI" pop-up.
local function ReportRestriction(tag, addonName, actionName)
    if addonName ~= ns.addonName then return end

    local action = type(actionName) == "string" and actionName
        or "unknown restricted action"

    Debug:Log(tag, "%s", action)
    ns.Print(("|cffff6060[%s]|r %s"):format(tag, action))
end

function Debug:OnEnable()
    ns.Events:RegisterEvent("ADDON_ACTION_BLOCKED", self,
        function(_, addonName, actionName)
            ReportRestriction("BLOCKED", addonName, actionName)
        end)

    ns.Events:RegisterEvent("ADDON_ACTION_FORBIDDEN", self,
        function(_, addonName, actionName)
            ReportRestriction("FORBIDDEN", addonName, actionName)
        end)
end
