AddonDiagnostics = AddonDiagnostics or {}
local EnhancedDiagnosticsBroker = AddonDiagnostics.EnhancedDiagnosticsBroker

local MAX_SPANS_PER_SESSION = 100
local MAX_SPANS_PER_PROVIDER = 50
local MAX_ACTIVE_SPANS = 32
local MAX_SPANS_PER_PROVIDER_PER_SECOND = 10
local MAX_SPANS_GLOBAL_PER_SECOND = 40
local MAX_SPAN_FIELDS = 8
local MAX_SPAN_NAME_CHARACTERS = 96
local MAX_SPAN_FIELD_NAME_CHARACTERS = 48
local MAX_SPAN_STRING_CHARACTERS = 160
local MAX_OPERATION_TOKEN_CHARACTERS = 64
local MAX_SPAN_EVIDENCE_CHARACTERS = 24000

local spanSessionActive = false
local spanSessionStartedAt = nil
local spanSessionSerial = 0
local spanNextToken = 0
local spanStartedCount = 0
local spanCompletedCount = 0
local spanActiveCount = 0
local droppedSpanCount = 0
local rejectedSpanCount = 0
local spanEvidenceTruncated = false
local lastSpanReason = nil
local spanEvidence = ""
local spanProviderSessionCounts = {}
local spanProviderWindowStarts = {}
local spanProviderWindowCounts = {}
local spanGlobalWindowStart = nil
local spanGlobalWindowCount = 0
local spanTokenProviderIDs = {}
local spanTokenNames = {}
local spanTokenStartedAt = {}
local spanTokenStartFields = {}
local originalSpanSessionStart = nil
local originalSpanSessionStop = nil

local function RejectSpan(reason)
    rejectedSpanCount = rejectedSpanCount + 1
    lastSpanReason = reason
    return false, reason
end

local function DropSpan(reason)
    droppedSpanCount = droppedSpanCount + 1
    spanEvidenceTruncated = true
    lastSpanReason = reason
    return false, reason
end

local function IsSafeSpanText(value, maximumCharacters, allowEmpty)
    if type(value) ~= "string" then
        return false
    end

    if not allowEmpty and value == "" then
        return false
    end

    if string.len(value) > maximumCharacters then
        return false
    end

    if string.find(value, "\n", 1, true) then
        return false
    end

    if string.find(value, "\r", 1, true) then
        return false
    end

    if string.find(value, "|", 1, true) then
        return false
    end

    return true
end

local function IsAccessibleValue(value)
    if type(issecretvalue) == "function" and issecretvalue(value) then
        return false
    end

    if type(canaccessvalue) == "function" and not canaccessvalue(value) then
        return false
    end

    return true
end

local function FormatSpanValue(value)
    if not IsAccessibleValue(value) then
        return nil, "protected or secret span value"
    end

    local valueType = type(value)

    if valueType == "boolean" then
        if value then
            return "true", nil
        end
        return "false", nil
    end

    if valueType == "number" then
        if value ~= value or value == math.huge or value == -math.huge then
            return nil, "invalid span number"
        end
        return string.format("%.17g", value), nil
    end

    if valueType == "string" then
        if not IsSafeSpanText(value, MAX_SPAN_STRING_CHARACTERS, true) then
            return nil, "invalid span string"
        end
        return value, nil
    end

    return nil, "unsupported span value type"
end

local function BuildSpanFields(fields)
    if fields == nil then
        return "", nil
    end

    if not IsAccessibleValue(fields) or type(fields) ~= "table" then
        return nil, "span fields must be a table"
    end

    local formattedFields = {}
    local fieldName = nil

    for index = 1, MAX_SPAN_FIELDS + 1 do
        fieldName = next(fields, fieldName)

        if fieldName == nil then
            table.sort(formattedFields)
            return table.concat(formattedFields, ", "), nil
        end

        if index > MAX_SPAN_FIELDS then
            return nil, "span field limit exceeded"
        end

        if not IsAccessibleValue(fieldName) then
            return nil, "protected or secret span field name"
        end

        if not IsSafeSpanText(fieldName, MAX_SPAN_FIELD_NAME_CHARACTERS, false) then
            return nil, "invalid span field name"
        end

        if string.find(fieldName, "=", 1, true) then
            return nil, "invalid span field-name delimiter"
        end

        local valueText, valueReason = FormatSpanValue(rawget(fields, fieldName))
        if valueText == nil then
            return nil, valueReason
        end

        table.insert(formattedFields, string.format("%s=%s", fieldName, valueText))
    end

    return nil, "span field limit exceeded"
end

local function IsRecording()
    if not AddonDiagnostics.SessionController or not AddonDiagnostics.SessionController.IsRecording then
        return false
    end

    return AddonDiagnostics.SessionController:IsRecording() == true
end

local function ResetSpanSession()
    spanSessionActive = true
    spanSessionStartedAt = GetTime()
    spanSessionSerial = spanSessionSerial + 1
    spanNextToken = 0
    spanStartedCount = 0
    spanCompletedCount = 0
    spanActiveCount = 0
    droppedSpanCount = 0
    rejectedSpanCount = 0
    spanEvidenceTruncated = false
    lastSpanReason = nil
    spanEvidence = ""
    spanProviderSessionCounts = {}
    spanProviderWindowStarts = {}
    spanProviderWindowCounts = {}
    spanGlobalWindowStart = nil
    spanGlobalWindowCount = 0
    spanTokenProviderIDs = {}
    spanTokenNames = {}
    spanTokenStartedAt = {}
    spanTokenStartFields = {}
end

local function StopSpanSession()
    if spanActiveCount > 0 then
        droppedSpanCount = droppedSpanCount + spanActiveCount
        spanEvidenceTruncated = true
        lastSpanReason = "recording stopped with active spans"
    end

    spanSessionActive = false
    spanSessionStartedAt = nil
    spanActiveCount = 0
    spanTokenProviderIDs = {}
    spanTokenNames = {}
    spanTokenStartedAt = {}
    spanTokenStartFields = {}
end

local function GetSpanElapsed()
    if not spanSessionStartedAt then
        return 0
    end

    return math.max(0, GetTime() - spanSessionStartedAt)
end

local function CheckSpanRate(providerID, elapsed)
    local providerSessionCount = rawget(spanProviderSessionCounts, providerID) or 0
    if providerSessionCount >= MAX_SPANS_PER_PROVIDER then
        return false, "provider span session limit reached"
    end

    if spanStartedCount >= MAX_SPANS_PER_SESSION then
        return false, "span session limit reached"
    end

    if spanActiveCount >= MAX_ACTIVE_SPANS then
        return false, "active span limit reached"
    end

    if spanGlobalWindowStart == nil or elapsed - spanGlobalWindowStart >= 1 then
        spanGlobalWindowStart = elapsed
        spanGlobalWindowCount = 0
    end

    local providerWindowStart = rawget(spanProviderWindowStarts, providerID)
    if providerWindowStart == nil or elapsed - providerWindowStart >= 1 then
        rawset(spanProviderWindowStarts, providerID, elapsed)
        rawset(spanProviderWindowCounts, providerID, 0)
    end

    local providerWindowCount = rawget(spanProviderWindowCounts, providerID) or 0
    if providerWindowCount >= MAX_SPANS_PER_PROVIDER_PER_SECOND then
        return false, "provider span rate limit reached"
    end

    if spanGlobalWindowCount >= MAX_SPANS_GLOBAL_PER_SECOND then
        return false, "global span rate limit reached"
    end

    return true, nil
end

local function CommitSpanBegin(providerID)
    rawset(
        spanProviderSessionCounts,
        providerID,
        (rawget(spanProviderSessionCounts, providerID) or 0) + 1
    )
    rawset(
        spanProviderWindowCounts,
        providerID,
        (rawget(spanProviderWindowCounts, providerID) or 0) + 1
    )
    spanGlobalWindowCount = spanGlobalWindowCount + 1
    spanStartedCount = spanStartedCount + 1
    spanActiveCount = spanActiveCount + 1
end

local function ClearActiveSpan(operationToken)
    rawset(spanTokenProviderIDs, operationToken, nil)
    rawset(spanTokenNames, operationToken, nil)
    rawset(spanTokenStartedAt, operationToken, nil)
    rawset(spanTokenStartFields, operationToken, nil)
    spanActiveCount = math.max(0, spanActiveCount - 1)
end

local function AppendSpanEvidence(line)
    local nextEvidence = line

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

    if string.len(nextEvidence) > MAX_SPAN_EVIDENCE_CHARACTERS then
        return false
    end

    spanEvidence = nextEvidence
    return true
end

local function IsValidOutcome(outcome)
    return outcome == "ok" or outcome == "warning" or outcome == "failed" or outcome == "cancelled"
end

function EnhancedDiagnosticsBroker:BeginOperation(providerID, operationName, fields)
    if not IsRecording() then
        return false, "not recording"
    end

    if not spanSessionActive then
        ResetSpanSession()
    end

    if type(providerID) ~= "string" or not EnhancedDiagnosticsBroker:SupportsCapability(providerID, "spans") then
        return RejectSpan("provider does not advertise spans")
    end

    if not IsSafeSpanText(operationName, MAX_SPAN_NAME_CHARACTERS, false) then
        return RejectSpan("invalid operation name")
    end

    local fieldText, fieldReason = BuildSpanFields(fields)
    if fieldText == nil then
        return RejectSpan(fieldReason)
    end

    local elapsed = GetSpanElapsed()
    local withinRate, rateReason = CheckSpanRate(providerID, elapsed)
    if not withinRate then
        return DropSpan(rateReason)
    end

    spanNextToken = spanNextToken + 1
    local operationToken = string.format("S%d-%d", spanSessionSerial, spanNextToken)

    if string.len(operationToken) > MAX_OPERATION_TOKEN_CHARACTERS then
        return DropSpan("operation token limit exceeded")
    end

    rawset(spanTokenProviderIDs, operationToken, providerID)
    rawset(spanTokenNames, operationToken, operationName)
    rawset(spanTokenStartedAt, operationToken, elapsed)
    rawset(spanTokenStartFields, operationToken, fieldText)
    CommitSpanBegin(providerID)
    lastSpanReason = nil
    return operationToken, "started"
end

function EnhancedDiagnosticsBroker:EndOperation(operationToken, outcome, fields)
    if not IsRecording() then
        return false, "not recording"
    end

    if not spanSessionActive then
        return RejectSpan("operation token is not active")
    end

    if not IsSafeSpanText(operationToken, MAX_OPERATION_TOKEN_CHARACTERS, false) then
        return RejectSpan("invalid operation token")
    end

    local providerID = rawget(spanTokenProviderIDs, operationToken)
    if providerID == nil then
        return RejectSpan("operation token is not active")
    end

    if not IsValidOutcome(outcome) then
        return RejectSpan("invalid operation outcome")
    end

    local fieldText, fieldReason = BuildSpanFields(fields)
    if fieldText == nil then
        return RejectSpan(fieldReason)
    end

    local operationName = rawget(spanTokenNames, operationToken)
    local startedAt = rawget(spanTokenStartedAt, operationToken) or 0
    local startFields = rawget(spanTokenStartFields, operationToken) or ""
    local elapsed = GetSpanElapsed()
    local duration = math.max(0, elapsed - startedAt)
    local line = string.format(
        "%.3fs | %s | SPAN | %s | %s | duration=%.3fs",
        elapsed,
        providerID,
        operationName,
        outcome,
        duration
    )

    if startFields ~= "" then
        line = string.format("%s | begin:%s", line, startFields)
    end

    if fieldText ~= "" then
        line = string.format("%s | end:%s", line, fieldText)
    end

    if not AppendSpanEvidence(line) then
        ClearActiveSpan(operationToken)
        return DropSpan("span evidence character limit reached")
    end

    ClearActiveSpan(operationToken)
    spanCompletedCount = spanCompletedCount + 1
    lastSpanReason = nil
    return true, "completed"
end

function EnhancedDiagnosticsBroker:GetSpanCount()
    return spanCompletedCount
end

function EnhancedDiagnosticsBroker:GetActiveSpanCount()
    return spanActiveCount
end

function EnhancedDiagnosticsBroker:GetSpanEvidence()
    return spanEvidence
end

function EnhancedDiagnosticsBroker:GetSpanStats()
    return spanCompletedCount,
        spanActiveCount,
        droppedSpanCount,
        rejectedSpanCount,
        spanEvidenceTruncated,
        lastSpanReason
end

local function WrappedSpanSessionStart(self)
    local started = originalSpanSessionStart(self)
    if started then
        ResetSpanSession()
    end
    return started
end

local function WrappedSpanSessionStop(self)
    local stopped, elapsed = originalSpanSessionStop(self)
    if stopped then
        StopSpanSession()
    end
    return stopped, elapsed
end

local function AttachSpanSessionLifecycle()
    local controller = AddonDiagnostics.SessionController
    if not controller then
        return
    end

    if type(controller.Start) ~= "function" or type(controller.Stop) ~= "function" then
        return
    end

    if originalSpanSessionStart ~= nil or originalSpanSessionStop ~= nil then
        return
    end

    originalSpanSessionStart = controller.Start
    originalSpanSessionStop = controller.Stop
    controller.Start = WrappedSpanSessionStart
    controller.Stop = WrappedSpanSessionStop
end

function AddonDiagnostics_EnhancedDiagnosticsBeginOperation(providerID, operationName, fields)
    return EnhancedDiagnosticsBroker:BeginOperation(providerID, operationName, fields)
end

function AddonDiagnostics_EnhancedDiagnosticsEndOperation(operationToken, outcome, fields)
    return EnhancedDiagnosticsBroker:EndOperation(operationToken, outcome, fields)
end

EnhancedDiagnostics.BeginOperation = AddonDiagnostics_EnhancedDiagnosticsBeginOperation
EnhancedDiagnostics.EndOperation = AddonDiagnostics_EnhancedDiagnosticsEndOperation

AttachSpanSessionLifecycle()
