AddonDiagnostics = AddonDiagnostics or {}
AddonDiagnostics.UniversalObserver = {}
local UniversalObserver = AddonDiagnostics.UniversalObserver

local MAX_SESSION_LOADED_ADDONS = 200
local MAX_SESSION_ENVIRONMENT_CHARACTERS = 12000
local MAX_ADDON_NAME_CHARACTERS = 80
local MAX_ADDON_VERSION_CHARACTERS = 64
local MAX_BUILD_FIELD_CHARACTERS = 64
local MAX_CAPTURED_LUA_ERRORS = 20
local MAX_LUA_ERROR_MESSAGE_CHARACTERS = 1200
local MAX_LUA_ERROR_STACK_CHARACTERS = 5000
local MAX_LUA_ERROR_EVIDENCE_CHARACTERS = 24000
local MAX_PERFORMANCE_SNAPSHOT_CHARACTERS = 240

local capturedLuaErrors = {}
local capturedLuaErrorCount = 0
local droppedLuaErrorCount = 0
local luaErrorEvidenceCharacters = 0
local luaErrorEvidenceTruncated = false
local lastLuaErrorElapsed = nil
local lastLuaErrorHadStack = false
local errorCaptureStartedAt = nil
local errorCaptureActive = false
local errorCaptureAvailable = false
local errorHandlerInstalled = false
local errorHandlerChanged = false
local errorForwardFailed = false
local previousErrorHandler = nil
local inErrorHandler = false

local function BoundText(value, maximumCharacters, fallback)
    local text = fallback
    if value ~= nil and value ~= "" then
        text = tostring(value)
    end

    if string.len(text) > maximumCharacters then
        return string.sub(text, 1, maximumCharacters)
    end

    return text
end

local function BoundLuaErrorText(value, maximumCharacters, fallback)
    local text = fallback
    if type(value) == "string" and value ~= "" then
        text = value
    end

    if string.len(text) > maximumCharacters then
        return string.sub(text, 1, maximumCharacters), true
    end

    return text, false
end

local function CaptureLuaError(message, rawStack)
    if not errorCaptureActive then
        return
    end

    if capturedLuaErrorCount >= MAX_CAPTURED_LUA_ERRORS then
        droppedLuaErrorCount = droppedLuaErrorCount + 1
        luaErrorEvidenceTruncated = true
        return
    end

    local safeMessage, messageTruncated = BoundLuaErrorText(
        message,
        MAX_LUA_ERROR_MESSAGE_CHARACTERS,
        "(non-string Lua error)"
    )
    local safeStack = ""
    local stackTruncated = false

    if type(rawStack) == "string" and rawStack ~= "" then
        local boundedStack, boundedStackTruncated = BoundLuaErrorText(
            rawStack,
            MAX_LUA_ERROR_STACK_CHARACTERS,
            ""
        )
        safeStack = boundedStack
        stackTruncated = boundedStackTruncated
    end

    local elapsed = 0
    if errorCaptureStartedAt then
        elapsed = math.max(0, GetTime() - errorCaptureStartedAt)
    end

    local stackForEvidence = safeStack
    if stackForEvidence == "" then
        stackForEvidence = "unavailable"
    end

    local evidence = string.format(
        "Time: %.1fs\nMessage: %s\nStack:\n%s",
        elapsed,
        safeMessage,
        stackForEvidence
    )

    if luaErrorEvidenceCharacters + string.len(evidence) > MAX_LUA_ERROR_EVIDENCE_CHARACTERS then
        droppedLuaErrorCount = droppedLuaErrorCount + 1
        luaErrorEvidenceTruncated = true
        return
    end

    table.insert(capturedLuaErrors, evidence)
    capturedLuaErrorCount = capturedLuaErrorCount + 1
    luaErrorEvidenceCharacters = luaErrorEvidenceCharacters + string.len(evidence)
    lastLuaErrorElapsed = elapsed
    lastLuaErrorHadStack = safeStack ~= ""

    if messageTruncated or stackTruncated then
        luaErrorEvidenceTruncated = true
    end
end

local function ReadCurrentErrorStack()
    if type(debugstack) == "function" then
        local okay, stack = pcall(debugstack)
        if okay and type(stack) == "string" then return stack end
    end
    return ""
end

local function HideBlizzardErrorPresentation()
    local frame = _G and _G.ScriptErrorsFrame or nil
    if not frame or type(frame.Hide) ~= "function" then return nil end
    local shown = true
    if type(frame.IsShown) == "function" then
        local okay, value = pcall(frame.IsShown, frame)
        if okay then shown = value == true end
    end
    if not shown then return nil end
    local hidden = pcall(frame.Hide, frame)
    return hidden == true
end

local function AddonDiagnosticsErrorHandler(message)
    if inErrorHandler then return end
    inErrorHandler = true

    local rawStack = ReadCurrentErrorStack()
    local incidentID = nil
    local store = AddonDiagnostics.IncidentStore
    if store and type(store.CaptureIncident) == "function" then
        incidentID = store:CaptureIncident(message, rawStack, errorCaptureActive)
    end
    CaptureLuaError(message, rawStack)

    local handler = previousErrorHandler
    if type(handler) == "function" and handler ~= AddonDiagnosticsErrorHandler then
        local forwarded = pcall(handler, message)
        if not forwarded then errorForwardFailed = true end
    end

    local suppressed = HideBlizzardErrorPresentation()
    if incidentID and store and type(store.MarkPresentationSuppressed) == "function" then
        store:MarkPresentationSuppressed(incidentID, suppressed)
    end
    inErrorHandler = false
end

local function InstallPassiveErrorHandler()
    if type(geterrorhandler) ~= "function" or type(seterrorhandler) ~= "function" then
        errorHandlerInstalled = false
        return false
    end

    if errorHandlerInstalled then
        if geterrorhandler() ~= AddonDiagnosticsErrorHandler then
            errorHandlerChanged = true
        end
        return true
    end

    local currentHandler = geterrorhandler()
    if currentHandler ~= AddonDiagnosticsErrorHandler then
        if type(currentHandler) ~= "function" then
            return false
        end
        previousErrorHandler = currentHandler
        local installed = pcall(seterrorhandler, AddonDiagnosticsErrorHandler)
        if not installed or geterrorhandler() ~= AddonDiagnosticsErrorHandler then
            previousErrorHandler = nil
            return false
        end
    end
    errorHandlerInstalled = true
    return true
end

local function CheckErrorHandlerOwnership()
    if not errorCaptureActive or not errorHandlerInstalled then
        return
    end

    if type(geterrorhandler) ~= "function" then
        errorHandlerChanged = true
        return
    end

    if geterrorhandler() ~= AddonDiagnosticsErrorHandler then
        errorHandlerChanged = true
    end
end

local function ResetLuaErrorEvidence()
    capturedLuaErrors = {}
    capturedLuaErrorCount = 0
    droppedLuaErrorCount = 0
    luaErrorEvidenceCharacters = 0
    luaErrorEvidenceTruncated = false
    lastLuaErrorElapsed = nil
    lastLuaErrorHadStack = false
    errorCaptureAvailable = false
    errorHandlerChanged = false
    errorForwardFailed = false
end

local function GetInstalledAddonCount()
    if C_AddOns and C_AddOns.GetNumAddOns then
        return tonumber(C_AddOns.GetNumAddOns()) or 0
    end

    if type(GetNumAddOns) == "function" then
        return tonumber(GetNumAddOns()) or 0
    end

    return 0
end

local function GetInstalledAddonName(index)
    if C_AddOns and C_AddOns.GetAddOnInfo then
        local name = C_AddOns.GetAddOnInfo(index)
        return name
    end

    if type(GetAddOnInfo) == "function" then
        local name = GetAddOnInfo(index)
        return name
    end

    return nil
end

local function GetInstalledAddonVersion(name)
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata(name, "Version")
    end

    if type(GetAddOnMetadata) == "function" then
        return GetAddOnMetadata(name, "Version")
    end

    return nil
end

local function IsInstalledAddonLoaded(name)
    if C_AddOns and C_AddOns.IsAddOnLoaded then
        return C_AddOns.IsAddOnLoaded(name) == true
    end

    if type(IsAddOnLoaded) == "function" then
        return IsAddOnLoaded(name) == true
    end

    return false
end

local function GetClientBuildSummary()
    if type(GetBuildInfo) == "function" then
        local version, build, buildDate, interfaceVersion = GetBuildInfo()
        return string.format(
            "Client: %s | Build: %s | Date: %s | Interface: %s",
            BoundText(version, MAX_BUILD_FIELD_CHARACTERS, "unknown"),
            BoundText(build, MAX_BUILD_FIELD_CHARACTERS, "unknown"),
            BoundText(buildDate, MAX_BUILD_FIELD_CHARACTERS, "unknown"),
            BoundText(interfaceVersion, MAX_BUILD_FIELD_CHARACTERS, "unknown")
        )
    end

    return "Client build metadata is unavailable."
end

local function GetLoadedAddonEvidence()
    local count = GetInstalledAddonCount()
    local capturedCount = 0
    local droppedCount = 0
    local listing = ""

    for index = 1, count do
        local name = GetInstalledAddonName(index)
        if name and IsInstalledAddonLoaded(name) then
            if capturedCount < MAX_SESSION_LOADED_ADDONS then
                local version = GetInstalledAddonVersion(name)
                local safeName = BoundText(name, MAX_ADDON_NAME_CHARACTERS, "unknown addon")
                local safeVersion = BoundText(
                    version,
                    MAX_ADDON_VERSION_CHARACTERS,
                    "version not declared"
                )
                listing = string.format("%s%s - %s\n", listing, safeName, safeVersion)
                capturedCount = capturedCount + 1
            else
                droppedCount = droppedCount + 1
            end
        end
    end

    if listing == "" then
        listing = "No loaded addon metadata is available.\n"
    end

    return capturedCount, droppedCount, listing
end

function UniversalObserver:GetInstalledAddonInventory()
    local count = GetInstalledAddonCount()
    local listing = ""

    for index = 1, count do
        local name = GetInstalledAddonName(index)
        if name then
            local version = GetInstalledAddonVersion(name)
            if not version or version == "" then
                version = "version not declared"
            end
            listing = string.format("%s%s - %s\n", listing, name, version)
        end
    end

    if listing == "" then
        listing = "No installed addon metadata is available."
    end

    return count, listing
end

function UniversalObserver:GetSessionEnvironmentEvidence()
    local capturedCount, droppedCount, listing = GetLoadedAddonEvidence()
    local addonEvidence = string.format(
        "Loaded Addons Captured: %d\nLoaded Addons Dropped By Entry Limit: %d\n%s",
        capturedCount,
        droppedCount,
        listing
    )
    local evidence = string.format("%s\n%s", GetClientBuildSummary(), addonEvidence)
    local truncated = droppedCount > 0

    if string.len(evidence) > MAX_SESSION_ENVIRONMENT_CHARACTERS then
        evidence = string.sub(evidence, 1, MAX_SESSION_ENVIRONMENT_CHARACTERS)
        truncated = true
    end

    return evidence, truncated, capturedCount, droppedCount
end

function UniversalObserver:GetPerformanceSnapshot(label)
    local safeLabel = "Start"
    if label == "Stop" then
        safeLabel = "Stop"
    end

    local fpsText = "unavailable"
    if type(GetFramerate) == "function" then
        local fps = tonumber(GetFramerate())
        if fps then
            fpsText = string.format("%.1f", math.max(0, fps))
        end
    end

    local homeText = "unavailable"
    local worldText = "unavailable"
    if type(GetNetStats) == "function" then
        local _, _, latencyHome, latencyWorld = GetNetStats()
        local home = tonumber(latencyHome)
        local world = tonumber(latencyWorld)
        if home then
            homeText = string.format("%.0f", math.max(0, home))
        end
        if world then
            worldText = string.format("%.0f", math.max(0, world))
        end
    end

    local memoryText = "unavailable"
    if type(collectgarbage) == "function" then
        local memoryOkay, rawMemory = pcall(collectgarbage, "count")
        if memoryOkay then
            local memory = tonumber(rawMemory)
            if memory then
                memoryText = string.format("%.1f", math.max(0, memory))
            end
        end
    end

    local snapshot = string.format(
        "%s: FPS %s | H %s ms | W %s ms | Lua %s KB",
        safeLabel,
        fpsText,
        homeText,
        worldText,
        memoryText
    )
    local truncated = false

    if string.len(snapshot) > MAX_PERFORMANCE_SNAPSHOT_CHARACTERS then
        snapshot = string.sub(snapshot, 1, MAX_PERFORMANCE_SNAPSHOT_CHARACTERS)
        truncated = true
    end

    return snapshot, truncated
end

function UniversalObserver:StartLuaErrorCapture(sessionStartedAt)
    ResetLuaErrorEvidence()
    errorCaptureStartedAt = tonumber(sessionStartedAt) or GetTime()
    errorCaptureActive = false

    if not InstallPassiveErrorHandler() then
        errorCaptureStartedAt = nil
        return false
    end

    errorCaptureAvailable = true
    errorCaptureActive = true
    return true
end

function UniversalObserver:StopLuaErrorCapture()
    CheckErrorHandlerOwnership()
    errorCaptureActive = false
    errorCaptureStartedAt = nil
    return true
end

function UniversalObserver:GetCapturedLuaErrors()
    return capturedLuaErrors
end

function UniversalObserver:GetLuaErrorCaptureStats()
    CheckErrorHandlerOwnership()
    return capturedLuaErrorCount,
        droppedLuaErrorCount,
        lastLuaErrorElapsed,
        lastLuaErrorHadStack,
        luaErrorEvidenceTruncated,
        errorCaptureAvailable,
        errorHandlerChanged,
        errorForwardFailed
end

function UniversalObserver:IsLuaErrorCaptureActive()
    CheckErrorHandlerOwnership()
    return errorCaptureActive and not errorHandlerChanged
end

local passiveHandlerLifecycleFrame = CreateFrame("Frame")
passiveHandlerLifecycleFrame:RegisterEvent("PLAYER_LOGIN")
local function OnPassiveHandlerLifecycleEvent(self, event, reportedContext, protectedFunction)
    if event == "PLAYER_LOGIN" then
        InstallPassiveErrorHandler()
        self:RegisterEvent("ADDON_ACTION_BLOCKED")
        self:RegisterEvent("ADDON_ACTION_FORBIDDEN")
        return
    end
    if event ~= "ADDON_ACTION_BLOCKED" and event ~= "ADDON_ACTION_FORBIDDEN" then return end
    local store = AddonDiagnostics.IncidentStore
    if store and type(store.CaptureProtectedAction) == "function" then
        store:CaptureProtectedAction(event, reportedContext, protectedFunction, errorCaptureActive)
    end
end
passiveHandlerLifecycleFrame:SetScript("OnEvent", OnPassiveHandlerLifecycleEvent)
