AddonDiagnostics = AddonDiagnostics or {}
AddonDiagnostics.Sanitizer = {}
local Sanitizer = AddonDiagnostics.Sanitizer

local MAX_SANITIZER_LINES = 1024
local MAX_SANITIZER_LINE_CHARACTERS = 2048
local MAX_REDACTIONS_PER_LINE = 24
local MAX_SANITIZED_TOTAL_CHARACTERS = 184000
local MAX_ENVIRONMENT_CHARACTERS = 12000
local MAX_TIMELINE_CHARACTERS = 64000
local MAX_LUA_ERROR_CHARACTERS = 24000
local MAX_LUA_ERROR_ENTRY_CHARACTERS = 7000
local MAX_PROVIDER_SUMMARY_CHARACTERS = 12000
local MAX_BREADCRUMB_CHARACTERS = 24000
local MAX_SPAN_CHARACTERS = 24000
local MAX_SNAPSHOT_CHARACTERS = 24000
local MAX_LUA_ERROR_EVENTS = 20

local sanitizedEnvironmentEvidence = ""
local sanitizedTimelineEvidence = ""
local sanitizedLuaErrorEvidence = ""
local sanitizedProviderSummary = ""
local sanitizedBreadcrumbEvidence = ""
local sanitizedSpanEvidence = ""
local sanitizedSnapshotEvidence = ""
local sanitizedSectionCount = 0
local redactedValueCount = 0
local droppedValueCount = 0
local sanitizedTotalCharacters = 0
local sanitizedEvidenceTruncated = false
local sanitizationReady = false
local lastSanitizationReason = nil
local originalSanitizerSessionStart = nil
local originalSanitizerSessionStop = nil

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 ResetSanitization()
    sanitizedEnvironmentEvidence = ""
    sanitizedTimelineEvidence = ""
    sanitizedLuaErrorEvidence = ""
    sanitizedProviderSummary = ""
    sanitizedBreadcrumbEvidence = ""
    sanitizedSpanEvidence = ""
    sanitizedSnapshotEvidence = ""
    sanitizedSectionCount = 0
    redactedValueCount = 0
    droppedValueCount = 0
    sanitizedTotalCharacters = 0
    sanitizedEvidenceTruncated = false
    sanitizationReady = false
    lastSanitizationReason = nil
end

local function RecordDrop(reason)
    droppedValueCount = droppedValueCount + 1
    lastSanitizationReason = reason
end

local function RecordRedaction(reason)
    redactedValueCount = redactedValueCount + 1
    lastSanitizationReason = reason
end

local function ReplacePlain(text, needle, replacement, maximumReplacements)
    local output = text
    local searchStart = 1

    for index = 1, maximumReplacements do
        local startIndex, endIndex = string.find(output, needle, searchStart, true)
        if not startIndex then
            return output
        end

        output = string.format(
            "%s%s%s",
            string.sub(output, 1, startIndex - 1),
            replacement,
            string.sub(output, endIndex + 1)
        )
        searchStart = startIndex + string.len(replacement)
    end

    sanitizedEvidenceTruncated = true
    return output
end

local function IsTokenDelimiter(character)
    return character == " "
        or character == "\t"
        or character == "|"
        or character == ","
        or character == ";"
        or character == "<"
        or character == ">"
        or character == "("
        or character == ")"
        or character == "["
        or character == "]"
        or character == '"'
        or character == "'"
end

local function FindTokenStart(text, position)
    for index = position, 1, -1 do
        local character = string.sub(text, index, index)
        if IsTokenDelimiter(character) then
            return index + 1
        end
    end

    return 1
end

local function FindTokenEnd(text, position)
    local length = string.len(text)
    for index = position, length do
        local character = string.sub(text, index, index)
        if IsTokenDelimiter(character) then
            return index
        end
    end

    return length + 1
end

local function RedactAssignmentMarker(text, marker)
    local output = text
    local searchStart = 1

    for index = 1, MAX_REDACTIONS_PER_LINE do
        local lowered = string.lower(output)
        local markerStart = string.find(lowered, marker, searchStart, true)
        if not markerStart then
            return output
        end

        local valueStart = markerStart + string.len(marker)
        local segmentEnd = string.find(output, " | ", valueStart, true)
        local suffix = ""
        if segmentEnd then
            suffix = string.sub(output, segmentEnd)
        end

        output = string.format(
            "%s[redacted]%s",
            string.sub(output, 1, valueStart - 1),
            suffix
        )
        RecordRedaction("sensitive diagnostic field redacted")
        searchStart = valueStart + string.len("[redacted]")
    end

    sanitizedEvidenceTruncated = true
    return output
end

local SENSITIVE_ASSIGNMENT_MARKERS = "password=|passwd=|credential=|credentials=|token=|secret=|authorization=|auth=|email=|emailaddress=|account=|accountid=|account_id=|userid=|user_id=|username=|user_name=|externalaccountid=|external_account_id=|player=|playername=|player_name=|character=|charactername=|character_name=|guild=|guildname=|guild_name=|bnet=|battlenet=|realid=|real_id=|battletag=|battle_tag=|apikey=|api_key="
local MAX_SENSITIVE_ASSIGNMENT_MARKERS = 36

local function RedactSensitiveAssignments(text)
    local output = text
    local markerStart = 1

    for index = 1, MAX_SENSITIVE_ASSIGNMENT_MARKERS do
        local markerEnd = string.find(SENSITIVE_ASSIGNMENT_MARKERS, "|", markerStart, true)
        local marker = nil
        if markerEnd then
            marker = string.sub(SENSITIVE_ASSIGNMENT_MARKERS, markerStart, markerEnd - 1)
        else
            marker = string.sub(SENSITIVE_ASSIGNMENT_MARKERS, markerStart)
        end

        if marker ~= "" then
            output = RedactAssignmentMarker(output, marker)
        end

        if not markerEnd then
            return output
        end

        markerStart = markerEnd + 1
    end

    sanitizedEvidenceTruncated = true
    return output
end

local function RedactEmailTokens(text)
    local output = text
    local searchStart = 1

    for index = 1, MAX_REDACTIONS_PER_LINE do
        local atIndex = string.find(output, "@", searchStart, true)
        if not atIndex then
            return output
        end

        local tokenStart = FindTokenStart(output, atIndex)
        local tokenEnd = FindTokenEnd(output, atIndex + 1)
        if tokenStart < atIndex and tokenEnd > atIndex + 1 then
            output = string.format(
                "%s[email redacted]%s",
                string.sub(output, 1, tokenStart - 1),
                string.sub(output, tokenEnd)
            )
            RecordRedaction("email address redacted")
            searchStart = tokenStart + string.len("[email redacted]")
        else
            searchStart = atIndex + 1
        end
    end

    sanitizedEvidenceTruncated = true
    return output
end

local function RedactMarkerTokens(text, marker, replacement, reason)
    local output = text
    local searchStart = 1

    for index = 1, MAX_REDACTIONS_PER_LINE do
        local lowered = string.lower(output)
        local markerStart = string.find(lowered, marker, searchStart, true)
        if not markerStart then
            return output
        end

        local tokenStart = FindTokenStart(output, markerStart)
        local tokenEnd = FindTokenEnd(output, markerStart + string.len(marker))
        output = string.format(
            "%s%s%s",
            string.sub(output, 1, tokenStart - 1),
            replacement,
            string.sub(output, tokenEnd)
        )
        RecordRedaction(reason)
        searchStart = tokenStart + string.len(replacement)
    end

    sanitizedEvidenceTruncated = true
    return output
end

local function RedactIdentityLinkMarker(text, marker)
    local output = text
    local searchStart = 1

    for index = 1, MAX_REDACTIONS_PER_LINE do
        local lowered = string.lower(output)
        local linkStart = string.find(lowered, marker, searchStart, true)
        if not linkStart then
            return output
        end

        local firstLinkEnd = string.find(lowered, "|h", linkStart + string.len(marker), true)
        if not firstLinkEnd then
            return RedactMarkerTokens(output, marker, "[identity redacted]", "identity link redacted")
        end

        local secondLinkEnd = string.find(lowered, "|h", firstLinkEnd + 2, true)
        local replaceEnd = firstLinkEnd + 1
        if secondLinkEnd then
            replaceEnd = secondLinkEnd + 1
        end

        output = string.format(
            "%s[identity redacted]%s",
            string.sub(output, 1, linkStart - 1),
            string.sub(output, replaceEnd + 1)
        )
        RecordRedaction("identity link redacted")
        searchStart = linkStart + string.len("[identity redacted]")
    end

    sanitizedEvidenceTruncated = true
    return output
end

local function RedactIdentityLinks(text)
    local output = text
    output = RedactIdentityLinkMarker(output, "|hplayer:")
    output = RedactIdentityLinkMarker(output, "|hbnplayer:")
    output = RedactIdentityLinkMarker(output, "|hguild:")
    return output
end

local function RedactBearerTokens(text)
    return RedactMarkerTokens(text, "bearer ", "[credential redacted]", "bearer credential redacted")
end

local function SanitizeLine(line)
    local output = ReplacePlain(line, "\r", "", MAX_REDACTIONS_PER_LINE)
    output = ReplacePlain(output, "\t", " ", MAX_REDACTIONS_PER_LINE)

    if string.len(output) > MAX_SANITIZER_LINE_CHARACTERS then
        output = string.sub(output, 1, MAX_SANITIZER_LINE_CHARACTERS)
        sanitizedEvidenceTruncated = true
        lastSanitizationReason = "sanitized line character limit reached"
    end

    output = RedactSensitiveAssignments(output)
    output = RedactEmailTokens(output)
    output = RedactIdentityLinks(output)
    output = RedactMarkerTokens(output, "player-", "[player id redacted]", "player identifier redacted")
    output = RedactBearerTokens(output)
    return output
end

local function SanitizeEvidence(value, maximumInputCharacters, maximumOutputCharacters)
    if value == nil or value == "" then
        return ""
    end

    if not IsAccessibleValue(value) or type(value) ~= "string" then
        RecordDrop("protected, secret, or non-string evidence rejected")
        return ""
    end

    local input = value
    if string.len(input) > maximumInputCharacters then
        input = string.sub(input, 1, maximumInputCharacters)
        sanitizedEvidenceTruncated = true
        lastSanitizationReason = "sanitizer input character limit reached"
    end

    local output = ""
    local startIndex = 1

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

        local sanitizedLine = SanitizeLine(line)
        local nextOutput = sanitizedLine
        if output ~= "" then
            nextOutput = string.format("%s\n%s", output, sanitizedLine)
        end

        if string.len(nextOutput) > maximumOutputCharacters then
            sanitizedEvidenceTruncated = true
            lastSanitizationReason = "sanitizer output character limit reached"
            return output
        end

        output = nextOutput
        if not endIndex then
            return output
        end

        startIndex = endIndex + 1
    end

    if startIndex <= string.len(input) then
        sanitizedEvidenceTruncated = true
        lastSanitizationReason = "sanitizer line limit reached"
    end

    return output
end

local function CommitSanitizedSection(value)
    local output = value or ""
    local remaining = MAX_SANITIZED_TOTAL_CHARACTERS - sanitizedTotalCharacters
    if remaining <= 0 then
        sanitizedEvidenceTruncated = true
        lastSanitizationReason = "sanitized session evidence limit reached"
        return ""
    end

    if string.len(output) > remaining then
        output = string.sub(output, 1, remaining)
        sanitizedEvidenceTruncated = true
        lastSanitizationReason = "sanitized session evidence limit reached"
    end

    sanitizedSectionCount = sanitizedSectionCount + 1
    sanitizedTotalCharacters = sanitizedTotalCharacters + string.len(output)
    return output
end

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

    local errors = observer:GetCapturedLuaErrors()
    if not IsAccessibleValue(errors) or type(errors) ~= "table" then
        RecordDrop("Lua error evidence container rejected")
        return ""
    end

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

        local sanitized = SanitizeEvidence(
            evidence,
            MAX_LUA_ERROR_ENTRY_CHARACTERS,
            MAX_LUA_ERROR_ENTRY_CHARACTERS
        )
        local nextOutput = sanitized
        if output ~= "" and sanitized ~= "" then
            nextOutput = string.format("%s\n---\n%s", output, sanitized)
        elseif output ~= "" then
            nextOutput = output
        end

        if string.len(nextOutput) > MAX_LUA_ERROR_CHARACTERS then
            sanitizedEvidenceTruncated = true
            lastSanitizationReason = "sanitized Lua error evidence limit reached"
            return CommitSanitizedSection(output)
        end

        output = nextOutput
    end

    return CommitSanitizedSection(output)
end

local function BuildSanitizedSessionEvidence()
    local controller = AddonDiagnostics.SessionController
    local timeline = AddonDiagnostics.SessionTimeline
    local observer = AddonDiagnostics.UniversalObserver
    local broker = AddonDiagnostics.EnhancedDiagnosticsBroker
    local snapshots = AddonDiagnostics.EnhancedDiagnosticsSnapshots

    if controller and type(controller.GetSessionEnvironmentEvidence) == "function" then
        sanitizedEnvironmentEvidence = CommitSanitizedSection(
            SanitizeEvidence(
                controller:GetSessionEnvironmentEvidence(),
                MAX_ENVIRONMENT_CHARACTERS,
                MAX_ENVIRONMENT_CHARACTERS
            )
        )
    else
        sanitizedEnvironmentEvidence = CommitSanitizedSection("")
    end

    if timeline and type(timeline.GetTimelineEvidence) == "function" then
        sanitizedTimelineEvidence = CommitSanitizedSection(
            SanitizeEvidence(
                timeline:GetTimelineEvidence(),
                MAX_TIMELINE_CHARACTERS,
                MAX_TIMELINE_CHARACTERS
            )
        )
    else
        sanitizedTimelineEvidence = CommitSanitizedSection("")
    end

    sanitizedLuaErrorEvidence = BuildSanitizedLuaErrors()

    if broker and type(broker.GetProviderSummary) == "function" then
        sanitizedProviderSummary = CommitSanitizedSection(
            SanitizeEvidence(
                broker:GetProviderSummary(),
                MAX_PROVIDER_SUMMARY_CHARACTERS,
                MAX_PROVIDER_SUMMARY_CHARACTERS
            )
        )
    else
        sanitizedProviderSummary = CommitSanitizedSection("")
    end

    if broker and type(broker.GetBreadcrumbEvidence) == "function" then
        sanitizedBreadcrumbEvidence = CommitSanitizedSection(
            SanitizeEvidence(
                broker:GetBreadcrumbEvidence(),
                MAX_BREADCRUMB_CHARACTERS,
                MAX_BREADCRUMB_CHARACTERS
            )
        )
    else
        sanitizedBreadcrumbEvidence = CommitSanitizedSection("")
    end

    if broker and type(broker.GetSpanEvidence) == "function" then
        sanitizedSpanEvidence = CommitSanitizedSection(
            SanitizeEvidence(
                broker:GetSpanEvidence(),
                MAX_SPAN_CHARACTERS,
                MAX_SPAN_CHARACTERS
            )
        )
    else
        sanitizedSpanEvidence = CommitSanitizedSection("")
    end

    if snapshots and type(snapshots.GetSnapshotEvidence) == "function" then
        sanitizedSnapshotEvidence = CommitSanitizedSection(
            SanitizeEvidence(
                snapshots:GetSnapshotEvidence(),
                MAX_SNAPSHOT_CHARACTERS,
                MAX_SNAPSHOT_CHARACTERS
            )
        )
    else
        sanitizedSnapshotEvidence = CommitSanitizedSection("")
    end

    sanitizationReady = true
end

function Sanitizer:IsSanitizationReady()
    return sanitizationReady
end

function Sanitizer:GetSanitizedEnvironmentEvidence()
    return sanitizedEnvironmentEvidence
end

function Sanitizer:GetSanitizedTimelineEvidence()
    return sanitizedTimelineEvidence
end

function Sanitizer:GetSanitizedLuaErrorEvidence()
    return sanitizedLuaErrorEvidence
end

function Sanitizer:GetSanitizedProviderSummary()
    return sanitizedProviderSummary
end

function Sanitizer:GetSanitizedBreadcrumbEvidence()
    return sanitizedBreadcrumbEvidence
end

function Sanitizer:GetSanitizedSpanEvidence()
    return sanitizedSpanEvidence
end

function Sanitizer:GetSanitizedSnapshotEvidence()
    return sanitizedSnapshotEvidence
end

function Sanitizer:GetSanitizationStats()
    return sanitizedSectionCount,
        redactedValueCount,
        droppedValueCount,
        sanitizedTotalCharacters,
        sanitizedEvidenceTruncated,
        sanitizationReady,
        lastSanitizationReason
end

local function WrappedSanitizerSessionStart(self)
    local started = originalSanitizerSessionStart(self)
    if started then
        ResetSanitization()
    end
    return started
end

local function WrappedSanitizerSessionStop(self)
    local stopped, elapsed = originalSanitizerSessionStop(self)
    if stopped then
        BuildSanitizedSessionEvidence()
    end
    return stopped, elapsed
end

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

    if originalSanitizerSessionStart ~= nil or originalSanitizerSessionStop ~= nil then
        return
    end

    originalSanitizerSessionStart = controller.Start
    originalSanitizerSessionStop = controller.Stop
    controller.Start = WrappedSanitizerSessionStart
    controller.Stop = WrappedSanitizerSessionStop
end

AttachSanitizerLifecycle()
