AddonDiagnostics = AddonDiagnostics or {}
AddonDiagnostics.SessionTimeline = {}
local SessionTimeline = AddonDiagnostics.SessionTimeline

local MAX_TIMELINE_EVENTS = 640
local MAX_TIMELINE_DETAIL_CHARACTERS = 320
local MAX_TIMELINE_EVIDENCE_CHARACTERS = 64000
local MAX_MARKER_EVENTS = 25
local MAX_LUA_ERROR_EVENTS = 20
local MAX_BREADCRUMB_EVENTS = 200
local MAX_SPAN_EVENTS = 100
local MAX_SNAPSHOT_EVENTS = 128

local timelineEvents = {}
local timelineEventCount = 0
local droppedTimelineEventCount = 0
local timelineEvidence = ""
local timelineEvidenceTruncated = false
local timelineSequence = 0
local timelineSessionElapsed = 0
local originalTimelineSessionStart = nil
local originalTimelineSessionStop = nil

local function ResetTimeline()
    timelineEvents = {}
    timelineEventCount = 0
    droppedTimelineEventCount = 0
    timelineEvidence = ""
    timelineEvidenceTruncated = false
    timelineSequence = 0
    timelineSessionElapsed = 0
end

local function BoundTimelineDetail(detail)
    if type(detail) ~= "string" then
        return "", false
    end

    if string.len(detail) > MAX_TIMELINE_DETAIL_CHARACTERS then
        return string.sub(detail, 1, MAX_TIMELINE_DETAIL_CHARACTERS), true
    end

    return detail, false
end

local function AddTimelineEvent(elapsed, eventType, detail)
    if timelineEventCount >= MAX_TIMELINE_EVENTS then
        droppedTimelineEventCount = droppedTimelineEventCount + 1
        timelineEvidenceTruncated = true
        return false
    end

    local safeElapsed = tonumber(elapsed) or 0
    safeElapsed = math.max(0, safeElapsed)
    local safeDetail, detailTruncated = BoundTimelineDetail(detail)
    if detailTruncated then
        timelineEvidenceTruncated = true
    end

    timelineSequence = timelineSequence + 1
    timelineEventCount = timelineEventCount + 1
    local event = {}
    event.elapsed = safeElapsed
    event.sequence = timelineSequence
    event.eventType = eventType
    event.detail = safeDetail
    table.insert(timelineEvents, event)
    return true
end

local function ParseElapsedPrefix(line)
    if type(line) ~= "string" then
        return nil
    end

    local separator = string.find(line, "s | ", 1, true)
    if not separator then
        return nil
    end

    return tonumber(string.sub(line, 1, separator - 1))
end

local function StripElapsedPrefix(line)
    local separator = string.find(line, "s | ", 1, true)
    if not separator then
        return line
    end

    return string.sub(line, separator + 4)
end

local function AddDelimitedEvidence(evidence, eventType, maximumLines)
    if type(evidence) ~= "string" or evidence == "" then
        return
    end

    local startIndex = 1
    for index = 1, maximumLines do
        local endIndex = string.find(evidence, "\n", startIndex, true)
        local line = nil
        if endIndex then
            line = string.sub(evidence, startIndex, endIndex - 1)
        else
            line = string.sub(evidence, startIndex)
        end

        if line ~= "" then
            local elapsed = ParseElapsedPrefix(line)
            if elapsed ~= nil then
                AddTimelineEvent(elapsed, eventType, StripElapsedPrefix(line))
            end
        end

        if not endIndex then
            return
        end

        startIndex = endIndex + 1
    end

    if startIndex <= string.len(evidence) then
        droppedTimelineEventCount = droppedTimelineEventCount + 1
        timelineEvidenceTruncated = true
    end
end

local function ParseSpanDuration(line)
    local durationMarker = string.find(line, "duration=", 1, true)
    if not durationMarker then
        return nil
    end

    local durationStart = durationMarker + string.len("duration=")
    local durationEnd = string.find(line, "s", durationStart, true)
    if not durationEnd then
        return nil
    end

    return tonumber(string.sub(line, durationStart, durationEnd - 1))
end

local function AddSpanEvidence(evidence)
    if type(evidence) ~= "string" or evidence == "" then
        return
    end

    local startIndex = 1
    for index = 1, MAX_SPAN_EVENTS do
        local endIndex = string.find(evidence, "\n", startIndex, true)
        local line = nil
        if endIndex then
            line = string.sub(evidence, startIndex, endIndex - 1)
        else
            line = string.sub(evidence, startIndex)
        end

        if line ~= "" then
            local endedAt = ParseElapsedPrefix(line)
            if endedAt ~= nil then
                local detail = StripElapsedPrefix(line)
                local duration = ParseSpanDuration(line)
                if duration ~= nil then
                    AddTimelineEvent(math.max(0, endedAt - duration), "SPAN_START", detail)
                end
                AddTimelineEvent(endedAt, "SPAN_END", detail)
            end
        end

        if not endIndex then
            return
        end

        startIndex = endIndex + 1
    end

    if startIndex <= string.len(evidence) then
        droppedTimelineEventCount = droppedTimelineEventCount + 1
        timelineEvidenceTruncated = true
    end
end

local function ParseLuaErrorElapsed(evidence)
    if type(evidence) ~= "string" then
        return nil
    end

    local timeStart = string.find(evidence, "Time: ", 1, true)
    if timeStart ~= 1 then
        return nil
    end

    local timeEnd = string.find(evidence, "s\n", 1, true)
    if not timeEnd then
        return nil
    end

    return tonumber(string.sub(evidence, 7, timeEnd - 1))
end

local function ParseLuaErrorMessage(evidence)
    local messageMarker = string.find(evidence, "Message: ", 1, true)
    if not messageMarker then
        return "captured Lua error"
    end

    local messageStart = messageMarker + string.len("Message: ")
    local messageEnd = string.find(evidence, "\nStack:", messageStart, true)
    if not messageEnd then
        return string.sub(evidence, messageStart)
    end

    return string.sub(evidence, messageStart, messageEnd - 1)
end

local function AddMarkerEvents()
    local controller = AddonDiagnostics.SessionController
    if not controller or type(controller.GetMarkerTimes) ~= "function" then
        return
    end

    local markers = controller:GetMarkerTimes()
    if type(markers) ~= "table" then
        return
    end

    for index = 1, MAX_MARKER_EVENTS do
        local elapsed = rawget(markers, index)
        if elapsed == nil then
            return
        end
        AddTimelineEvent(elapsed, "MARKER", "Problem marked")
    end
end

local function AddLuaErrorEvents()
    local observer = AddonDiagnostics.UniversalObserver
    if not observer or type(observer.GetCapturedLuaErrors) ~= "function" then
        return
    end

    local errors = observer:GetCapturedLuaErrors()
    if type(errors) ~= "table" then
        return
    end

    for index = 1, MAX_LUA_ERROR_EVENTS do
        local evidence = rawget(errors, index)
        if evidence == nil then
            return
        end

        local elapsed = ParseLuaErrorElapsed(evidence)
        if elapsed ~= nil then
            AddTimelineEvent(elapsed, "LUA_ERROR", ParseLuaErrorMessage(evidence))
        end
    end
end

local function AddPerformanceEvents(sessionElapsed)
    local controller = AddonDiagnostics.SessionController
    if not controller or type(controller.GetPerformanceSnapshots) ~= "function" then
        return
    end

    local startSnapshot, stopSnapshot = controller:GetPerformanceSnapshots()
    if type(startSnapshot) == "string" and startSnapshot ~= "" then
        AddTimelineEvent(0, "PERFORMANCE", startSnapshot)
    end
    if type(stopSnapshot) == "string" and stopSnapshot ~= "" then
        AddTimelineEvent(sessionElapsed, "PERFORMANCE", stopSnapshot)
    end
end

local function AddEnhancedDiagnosticsEvents()
    local broker = AddonDiagnostics.EnhancedDiagnosticsBroker
    if broker and type(broker.GetBreadcrumbEvidence) == "function" then
        AddDelimitedEvidence(broker:GetBreadcrumbEvidence(), "BREADCRUMB", MAX_BREADCRUMB_EVENTS)
    end

    if broker and type(broker.GetSpanEvidence) == "function" then
        AddSpanEvidence(broker:GetSpanEvidence())
    end

    local snapshots = AddonDiagnostics.EnhancedDiagnosticsSnapshots
    if snapshots and type(snapshots.GetSnapshotEvidence) == "function" then
        AddDelimitedEvidence(snapshots:GetSnapshotEvidence(), "SNAPSHOT", MAX_SNAPSHOT_EVENTS)
    end
end

local function CompareTimelineEvents(left, right)
    if left.elapsed == right.elapsed then
        return left.sequence < right.sequence
    end
    return left.elapsed < right.elapsed
end

local function RenderTimelineEvidence()
    table.sort(timelineEvents, CompareTimelineEvents)
    timelineEvidence = ""

    for index = 1, timelineEventCount do
        local event = rawget(timelineEvents, index)
        local line = string.format("%.3fs | %s", event.elapsed, event.eventType)
        if event.detail ~= "" then
            line = string.format("%s | %s", line, event.detail)
        end

        local nextEvidence = line
        if timelineEvidence ~= "" then
            nextEvidence = string.format("%s\n%s", timelineEvidence, line)
        end

        if string.len(nextEvidence) > MAX_TIMELINE_EVIDENCE_CHARACTERS then
            droppedTimelineEventCount = droppedTimelineEventCount + (timelineEventCount - index + 1)
            timelineEventCount = index - 1
            timelineEvidenceTruncated = true
            return
        end

        timelineEvidence = nextEvidence
    end
end

local function BuildTimeline(sessionElapsed)
    timelineSessionElapsed = math.max(0, tonumber(sessionElapsed) or 0)
    timelineEvents = {}
    timelineEventCount = 0
    timelineSequence = 0
    timelineEvidence = ""

    AddTimelineEvent(0, "SESSION", "recording_started")
    AddPerformanceEvents(timelineSessionElapsed)
    AddMarkerEvents()
    AddLuaErrorEvents()
    AddEnhancedDiagnosticsEvents()
    AddTimelineEvent(timelineSessionElapsed, "SESSION", "recording_stopped")
    RenderTimelineEvidence()
end

function SessionTimeline:GetTimelineCount()
    return timelineEventCount
end

function SessionTimeline:GetDroppedTimelineCount()
    return droppedTimelineEventCount
end

function SessionTimeline:IsTimelineTruncated()
    return timelineEvidenceTruncated
end

function SessionTimeline:GetTimelineEvidence()
    return timelineEvidence
end

function SessionTimeline:GetTimelineStats()
    return timelineEventCount, droppedTimelineEventCount, timelineEvidenceTruncated, timelineSessionElapsed
end

local function WrappedTimelineSessionStart(self)
    local started = originalTimelineSessionStart(self)
    if started then
        ResetTimeline()
    end
    return started
end

local function WrappedTimelineSessionStop(self)
    local stopped, elapsed = originalTimelineSessionStop(self)
    if stopped then
        BuildTimeline(elapsed)
    end
    return stopped, elapsed
end

local function AttachTimelineLifecycle()
    local controller = AddonDiagnostics.SessionController
    if not controller or type(controller.Start) ~= "function" or type(controller.Stop) ~= "function" then
        return
    end

    if originalTimelineSessionStart ~= nil or originalTimelineSessionStop ~= nil then
        return
    end

    originalTimelineSessionStart = controller.Start
    originalTimelineSessionStop = controller.Stop
    controller.Start = WrappedTimelineSessionStart
    controller.Stop = WrappedTimelineSessionStop
end

AttachTimelineLifecycle()
