GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.MythicPlusService = addon.MythicPlusService or {}

local DEFAULT_TARGET_KEY_LEVEL = 10
local DEFAULT_SEASON_ID = "current"
local DEFAULT_PORTAL_THRESHOLD = 10
local DEFAULT_CARRY_MILESTONE = 2000
local DEFAULT_CARRY_KEY_GOAL = 10
local AUTO_LIVE_LIGHT_REFRESH_COOLDOWN_SECONDS = 5
local AUTO_LIVE_REFRESH_COOLDOWN_SECONDS = 30
local AUTO_KEYSTONE_SLOT_COOLDOWN_SECONDS = 2
local PARTY_MEMBERS_CACHE_TTL_SECONDS = 1
local PARTY_ITEM_LEVEL_CACHE_TTL_SECONDS = 300
local PARTY_ITEM_LEVEL_REQUEST_COOLDOWN_SECONDS = 15
local MYTHIC_PLUS_OVERLAY_UPDATE_INTERVAL_SECONDS = 1.0
local MYTHIC_PLUS_OVERLAY_TRACKER_SCAN_INTERVAL_SECONDS = 1.0
local MYTHIC_PLUS_OVERLAY_STATE_REFRESH_INTERVAL_SECONDS = 2.0
local MYTHIC_PLUS_OVERLAY_ENABLE_TRACKER_FALLBACK = false
local MYTHIC_PLUS_OVERLAY_WIDTH = 468
local MYTHIC_PLUS_OVERLAY_HEIGHT = 304
local MYTHIC_PLUS_SUMMARY_LOOT_CAPTURE_SECONDS = 300
local MYTHIC_PLUS_SUMMARY_WIDTH = 1020
local MYTHIC_PLUS_SUMMARY_HEIGHT = 620
local GH3_MEDIA_ROOT = "Interface\\AddOns\\GH3\\Media\\"
local MEDIA_TEXTURES = {
    panelStandard = GH3_MEDIA_ROOT .. "Panels\\Panel_Standard_BG.png",
    panelTeal = GH3_MEDIA_ROOT .. "Panels\\Panel_Teal_BG.png",
    panelBlue = GH3_MEDIA_ROOT .. "Panels\\Panel_Blue_BG.png",
    panelPurple = GH3_MEDIA_ROOT .. "Panels\\Panel_Purple_BG.png",
    panelAmber = GH3_MEDIA_ROOT .. "Panels\\Panel_Amber_BG.png",
    headerGold = GH3_MEDIA_ROOT .. "Headers\\Header_Section_Gold.png",
    headerTeal = GH3_MEDIA_ROOT .. "Headers\\Header_Section_Teal.png",
    dividerGold = GH3_MEDIA_ROOT .. "Headers\\Divider_Gold.png",
    dividerTeal = GH3_MEDIA_ROOT .. "Headers\\Divider_Teal.png",
    subtitleStrip = GH3_MEDIA_ROOT .. "Headers\\Subtitle_strip.png",
    rowNormal = GH3_MEDIA_ROOT .. "Rows\\Row_Normal.png",
    rowSelected = GH3_MEDIA_ROOT .. "Rows\\Row_Selected.png",
    barFrame = GH3_MEDIA_ROOT .. "StatusBars\\Bar_Frame.png",
    fillBlue = GH3_MEDIA_ROOT .. "StatusBars\\Fill_Blue.png",
    fillGreen = GH3_MEDIA_ROOT .. "StatusBars\\Fill_Green.png",
    fillGold = GH3_MEDIA_ROOT .. "StatusBars\\Fill_Gold.png",
    fillPurple = GH3_MEDIA_ROOT .. "StatusBars\\Fill_Purple.png",
    buttonClose = GH3_MEDIA_ROOT .. "Buttons\\Button_Close.png",
    buttonCloseHover = GH3_MEDIA_ROOT .. "Buttons\\Button_Close_Hover.png",
    iconTimer = GH3_MEDIA_ROOT .. "Icons\\Timer.png",
    iconDeath = GH3_MEDIA_ROOT .. "Icons\\Death.png",
    iconMythicPlus = GH3_MEDIA_ROOT .. "Icons\\MythicPlus.png",
    iconLoot = GH3_MEDIA_ROOT .. "Icons\\Loot.png",
    iconReady = GH3_MEDIA_ROOT .. "Icons\\Ready.png",
    iconNotReady = GH3_MEDIA_ROOT .. "Icons\\NotReady.png",
    frameTopLeft = GH3_MEDIA_ROOT .. "Borders\\Frame_TopLeft.png",
    frameTopRight = GH3_MEDIA_ROOT .. "Borders\\Frame_TopRight.png",
    frameBottomLeft = GH3_MEDIA_ROOT .. "Borders\\Frame_BottomLeft.png",
    frameBottomRight = GH3_MEDIA_ROOT .. "Borders\\Frame_BottomRight.png",
    frameTopEdge = GH3_MEDIA_ROOT .. "Borders\\Frame_TopEdge.png",
    frameBottomEdge = GH3_MEDIA_ROOT .. "Borders\\Frame_BottomEdge.png",
    frameLeftEdge = GH3_MEDIA_ROOT .. "Borders\\Frame_LeftEdge.png",
    frameRightEdge = GH3_MEDIA_ROOT .. "Borders\\Frame_RightEdge.png",
}
local MEDIA_TEXCOORDS = {
    rowNormal = { 47 / 512, 463 / 512, 0, 1 },
    rowSelected = { 47 / 512, 463 / 512, 0, 1 },
    fillBlue = { 174 / 512, 336 / 512, 0, 1 },
    fillGreen = { 177 / 512, 334 / 512, 0, 1 },
    fillGold = { 183 / 512, 327 / 512, 0, 1 },
    fillPurple = { 190 / 512, 320 / 512, 0, 1 },
    fillDefault = { 184 / 512, 327 / 512, 0, 1 },
}
local CLASS_ICON_TEXTURE = "Interface\\GLUES\\CHARACTERCREATE\\UI-CharacterCreate-Classes"
local normalizeCarryMilestone
local getOverlayDeathCount
local function normalizeCarryKeyGoal(value)
    local numericValue = math.floor(tonumber(value) or DEFAULT_CARRY_KEY_GOAL)

    if numericValue >= 15 then
        return 15
    end

    if numericValue >= 12 then
        return 12
    end

    return 10
end

local function normalizeAnalysisMode(value)
    local normalizedValue = tostring(value or ""):match("^%s*(.-)%s*$") or ""

    if normalizedValue == "score-goal" or normalizedValue == "key-goal" then
        return normalizedValue
    end

    return "party-gains"
end

local function canGuildInviteMembers()
    if C_GuildInfo and C_GuildInfo.CanInvite then
        local ok, allowed = pcall(C_GuildInfo.CanInvite)

        if ok then
            return allowed and true or false
        end
    end

    if CanGuildInvite then
        local ok, allowed = pcall(CanGuildInvite)

        if ok then
            return allowed and true or false
        end
    end

    return false
end

local MIDNIGHT_BASE_SCORE_BY_LEVEL = {
    [2] = 155,
    [3] = 170,
    [4] = 200,
    [5] = 215,
    [6] = 230,
    [7] = 260,
    [8] = 275,
    [9] = 290,
    [10] = 320,
    [11] = 335,
    [12] = 365,
    [13] = 380,
    [14] = 395,
    [15] = 410,
    [16] = 425,
    [17] = 440,
    [18] = 455,
    [19] = 470,
    [20] = 485,
}
local MYTHIC_SCORE_TIME_MULTIPLIER = 37.5
local MYTHIC_SCORE_MAX_TIME_PERCENT = 0.4
local MYTHIC_SCORE_OVERTIME_PENALTY = 15
local SEASON_DEFINITIONS = {
    current = {
        id = DEFAULT_SEASON_ID,
        name = "Current Season",
        portalThreshold = DEFAULT_PORTAL_THRESHOLD,
        dungeons = {},
    },
}
local PORTAL_SHORT_NAMES = {
    ["Algeth'ar Academy"] = "AA",
    ["Seat of the Triumvirate"] = "SOTT",
    ["Skyreach"] = "SR",
    ["Pit of Saron"] = "POS",
    ["Magisters' Terrace"] = "MT",
    ["Nexus-Princess Ky'veza's Lair"] = "NPKL",
    ["Nexus-Princess Ky'veza"] = "NPK",
    ["Mists of Tirna Scithe"] = "MOTS",
    ["The MOTHERLODE!!"] = "ML",
}

local PORTAL_SPELL_IDS = {
    [161] = 159898,
    [168] = 159901,
    [198] = 424163,
    [199] = 424153,
    [244] = 424187,
    [247] = 467553,
    [248] = 424167,
    [353] = 445418,
    [370] = 373274,
    [375] = 354464,
    [376] = 354462,
    [378] = 354465,
    [382] = 354467,
    [391] = 367416,
    [392] = 367416,
    [399] = 393256,
    [400] = 393262,
    [401] = 393279,
    [402] = 393273,
    [403] = 393222,
    [404] = 393276,
    [405] = 393267,
    [406] = 393283,
    [456] = 424142,
    [463] = 424197,
    [464] = 424197,
    [499] = 445444,
    [500] = 445443,
    [501] = 445269,
    [502] = 445416,
    [503] = 445417,
    [504] = 445441,
    [505] = 445414,
    [506] = 445440,
    [507] = 445424,
    [525] = 1216786,
    [542] = 1237215,
    [239] = 1254551,
    [556] = 1254555,
    [557] = 1254400,
    [558] = 1254572,
    [559] = 1254563,
    [560] = 1254559,
}

local PORTAL_SPELL_IDS_HORDE = {
    [247] = 467555,
    [353] = 464256,
}

local FALLBACK_SEASON_DUNGEON_IDS = {
    525,
    542,
    239,
    556,
    557,
    558,
    559,
    560,
}

local ENCOUNTER_JOURNAL_TEXTURE_CACHE = {}

local function deepCopy(value)
    if type(value) ~= "table" then
        return value
    end

    local copy = {}

    for key, nestedValue in pairs(value) do
        copy[key] = deepCopy(nestedValue)
    end

    return copy
end

local function sortDungeonCatalogEntries(entries)
    table.sort(entries, function(left, right)
        local leftOrder = tonumber(left.displayOrder) or math.huge
        local rightOrder = tonumber(right.displayOrder) or math.huge
        local leftName = string.lower(tostring(left.name or left.id or ""):match("^%s*(.-)%s*$") or "")
        local rightName = string.lower(tostring(right.name or right.id or ""):match("^%s*(.-)%s*$") or "")

        if leftOrder == rightOrder then
            return leftName < rightName
        end

        return leftOrder < rightOrder
    end)
end

local function safeToString(value)
    if value == nil then
        return ""
    end

    if type(value) == "string" then
        return value
    end

    local ok, text = pcall(tostring, value)

    if ok then
        return text or ""
    end

    return ""
end

local function safeStringMatch(value, pattern)
    if value == nil then
        return nil
    end

    local ok, first, second, third = pcall(string.match, value, pattern)

    if ok then
        return first, second, third
    end

    if type(value) ~= "string" then
        local text = safeStringValue(value)

        if text ~= "" then
            ok, first, second, third = pcall(string.match, text, pattern)

            if ok then
                return first, second, third
            end
        end
    end

    return nil
end

local function trimString(value)
    return safeStringMatch(value, "^%s*(.-)%s*$") or ""
end

local function lowerText(value)
    return string.lower(trimString(value))
end

local function normalizePortalMatchText(value)
    local normalized = trimString(value)

    if normalized == "" then
        return ""
    end

    normalized = normalized:gsub("^[Tt]eleport:%s*", "")
    normalized = normalized:gsub("^[Pp]ortal:%s*", "")
    normalized = string.lower(normalized)
    normalized = normalized:gsub("^the%s+", "")
    normalized = normalized:gsub("[^%w%s]", " ")
    normalized = normalized:gsub("%s+", " ")

    return trimString(normalized)
end

local function stripPortalSpellPrefix(value)
    local normalized = trimString(value)

    if normalized == "" then
        return ""
    end

    normalized = normalized:gsub("^[Tt]eleport:%s*", "")
    normalized = normalized:gsub("^[Pp]ortal:%s*", "")

    return trimString(normalized)
end

local function safeInvoke(container, methodName, ...)
    if type(container) ~= "table" or type(container[methodName]) ~= "function" then
        return nil
    end

    local ok, result1, result2, result3, result4, result5 = pcall(container[methodName], ...)

    if not ok then
        return nil
    end

    return result1, result2, result3, result4, result5
end

local function safeCompressionCall(provider, methodName, ...)
    if type(provider) ~= "table" or type(provider[methodName]) ~= "function" then
        return nil
    end

    local ok, result = pcall(provider[methodName], provider, ...)

    if ok then
        return result
    end

    addon:Debug("Mythic+ OpenRaid", methodName, "failed", tostring(result))
    return nil
end

local builtinAddonChannelCodec

local function encodeForWoWAddonChannel(str)
    if type(str) ~= "string" then
        return nil
    end

    return string.gsub(str, "[\000\001]", {
        ["\000"] = "\001\002",
        ["\001"] = "\001\003",
    })
end

local function decodeForWoWAddonChannel(str)
    if type(str) ~= "string" then
        return nil
    end

    if string.find(str, "\000", 1, true) then
        return nil
    end

    local hadInvalidEscape = false
    local decoded = string.gsub(str, "\001(.)", function(suffix)
        if suffix == "\002" then
            return "\000"
        end

        if suffix == "\003" then
            return "\001"
        end

        hadInvalidEscape = true
        return ""
    end)

    if hadInvalidEscape or string.find(decoded, "\001", 1, true) then
        return nil
    end

    return decoded
end

local function getBuiltinCompressionProvider()
    if type(C_EncodingUtil) ~= "table"
        or type(C_EncodingUtil.CompressString) ~= "function"
        or type(C_EncodingUtil.DecompressString) ~= "function" then
        return nil
    end

    if builtinAddonChannelCodec then
        return builtinAddonChannelCodec
    end

    local compressionMethod = Enum and Enum.CompressionMethod and Enum.CompressionMethod.Deflate or 0
    builtinAddonChannelCodec = {
        __gh3Source = "builtin",
    }

    function builtinAddonChannelCodec:CompressDeflate(payload, options)
        local attempts = {
            { payload },
            { payload, compressionMethod },
        }

        if type(options) == "table" and options.level ~= nil then
            attempts[#attempts + 1] = { payload, tonumber(options.level) }
            attempts[#attempts + 1] = { payload, compressionMethod, tonumber(options.level) }
        end

        for _, args in ipairs(attempts) do
            local ok, compressed = pcall(C_EncodingUtil.CompressString, unpack(args))

            if ok and type(compressed) == "string" and compressed ~= "" then
                return compressed
            end
        end

        return nil
    end

    function builtinAddonChannelCodec:DecompressDeflate(payload)
        local attempts = {
            { payload },
            { payload, compressionMethod },
        }

        for _, args in ipairs(attempts) do
            local ok, decompressed = pcall(C_EncodingUtil.DecompressString, unpack(args))

            if ok and type(decompressed) == "string" and decompressed ~= "" then
                return decompressed
            end
        end

        return nil
    end

    function builtinAddonChannelCodec:EncodeForWoWAddonChannel(payload)
        return encodeForWoWAddonChannel(payload)
    end

    function builtinAddonChannelCodec:DecodeForWoWAddonChannel(payload)
        return decodeForWoWAddonChannel(payload)
    end

    return builtinAddonChannelCodec
end

local function getLibDeflate()
    if LibStub == nil then
        local builtinProvider = getBuiltinCompressionProvider()

        if builtinProvider then
            return builtinProvider
        end

        return nil
    end

    if type(LibStub.GetLibrary) == "function" then
        local getOk, getLibrary = pcall(LibStub.GetLibrary, LibStub, "LibDeflate", true)

        if getOk and type(getLibrary) == "table" then
            getLibrary.__gh3Source = getLibrary.__gh3Source or "libdeflate"
            return getLibrary
        end
    end

    if type(LibStub) == "function" then
        local ok, library = pcall(LibStub, "LibDeflate", true)

        if ok and type(library) == "table" then
            library.__gh3Source = library.__gh3Source or "libdeflate"
            return library
        end
    end

    local builtinProvider = getBuiltinCompressionProvider()

    if builtinProvider then
        return builtinProvider
    end

    return nil
end

local function sendAddonMessage(prefix, payload, channel, target)
    if trimString(prefix) == "" or type(payload) ~= "string" or payload == "" or trimString(channel) == "" then
        return false
    end

    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        local ok, result = pcall(C_ChatInfo.SendAddonMessage, prefix, payload, channel, target)

        if ok then
            return result
        end

        local parameterSets = {
            { prefix = prefix, message = payload, chatType = channel, target = target },
            { prefix = prefix, text = payload, chatType = channel, target = target },
            { prefix = prefix, message = payload, distribution = channel, target = target },
            { prefix = prefix, text = payload, distribution = channel, target = target },
        }

        for _, params in ipairs(parameterSets) do
            local tableOk, tableResult = pcall(C_ChatInfo.SendAddonMessage, params)

            if tableOk then
                return tableResult
            end
        end

        return false
    end

    if SendAddonMessage then
        return SendAddonMessage(prefix, payload, channel, target)
    end

    return false
end

local function requestLiveMythicData()
    safeInvoke(C_MythicPlus, "RequestCurrentAffixes")
    safeInvoke(C_MythicPlus, "RequestMapInfo")
    safeInvoke(C_MythicPlus, "RequestRewards")
end

local function clampKeyLevel(value)
    local level = tonumber(value) or DEFAULT_TARGET_KEY_LEVEL
    level = math.floor(level)

    if level < 2 then
        return 2
    end

    if level > 20 then
        return 20
    end

    return level
end

local function roundToNearestInteger(value)
    return math.floor((tonumber(value) or 0) + 0.5)
end

local function getClockSeconds()
    if type(GetTime) == "function" then
        return tonumber(GetTime()) or 0
    end

    return tonumber(time()) or 0
end

local function getWallClockSeconds()
    if type(GetServerTime) == "function" then
        local serverNow = tonumber(GetServerTime()) or 0

        if serverNow > 0 then
            return serverNow
        end
    end

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

    return getClockSeconds()
end

local function formatRunClock(value)
    local totalSeconds = math.max(0, math.floor((tonumber(value) or 0) + 0.5))
    local minutes = math.floor(totalSeconds / 60)
    local seconds = totalSeconds % 60

    return string.format("%d:%02d", minutes, seconds)
end

local function getMidnightBaseScoreForLevel(level)
    return MIDNIGHT_BASE_SCORE_BY_LEVEL[clampKeyLevel(level)] or MIDNIGHT_BASE_SCORE_BY_LEVEL[DEFAULT_TARGET_KEY_LEVEL]
end

local function calculateMythicRunScore(level, timePercent, isTimed)
    local baseScore = getMidnightBaseScoreForLevel(level)
    local resolvedTimePercent = tonumber(timePercent) or 0

    if isTimed then
        resolvedTimePercent = math.max(0, math.min(resolvedTimePercent, MYTHIC_SCORE_MAX_TIME_PERCENT))
        return math.max(0, baseScore + (resolvedTimePercent * MYTHIC_SCORE_TIME_MULTIPLIER))
    end

    resolvedTimePercent = math.min(resolvedTimePercent, 0)
    return math.max(0, baseScore + (resolvedTimePercent * MYTHIC_SCORE_TIME_MULTIPLIER) - MYTHIC_SCORE_OVERTIME_PENALTY)
end

local function normalizeDungeonId(value)
    local normalized = trimString(value)

    if normalized ~= "" then
        return normalized
    end

    return nil
end

local function countTableEntries(values)
    local count = 0

    for _ in pairs(values or {}) do
        count = count + 1
    end

    return count
end

local function getTrackedDungeonScore(dungeonRecord)
    local totalScore = tonumber(dungeonRecord and dungeonRecord.score) or 0

    if totalScore <= 0 and dungeonRecord then
        totalScore = (tonumber(dungeonRecord.fortifiedScore) or 0) + (tonumber(dungeonRecord.tyrannicalScore) or 0)
    end

    return math.max(0, totalScore)
end

local function buildGroupUnits()
    local units = {}

    if IsInGroup and IsInGroup() and not (IsInRaid and IsInRaid()) then
        units[#units + 1] = "player"

        for index = 1, (GetNumSubgroupMembers() or 0) do
            units[#units + 1] = "party" .. index
        end
    else
        units[#units + 1] = "player"
    end

    return units
end

local function calculateUnitItemLevel(unitId)
    local totalItemLevel = 0
    local itemCount = 0

    for slot = 1, 18 do
        if slot ~= 4 then
            local itemLink = GetInventoryItemLink and GetInventoryItemLink(unitId, slot) or nil

            if itemLink then
                local itemLevel = GetDetailedItemLevelInfo and GetDetailedItemLevelInfo(itemLink) or nil

                if itemLevel then
                    totalItemLevel = totalItemLevel + itemLevel
                    itemCount = itemCount + 1
                end
            end
        end
    end

    if itemCount <= 0 then
        return 0
    end

    return math.floor(((totalItemLevel / itemCount) * 10) + 0.5) / 10
end

local function refreshMythicPlusView()
    if type(addon.MainFrame) == "table" and type(addon.MainFrame.RefreshCurrentView) == "function" then
        addon.MainFrame:RefreshCurrentView(true)
    end
end

local function normalizePartyItemLevelMemberId(fullName)
    return addon.Store:NormalizeFullName(fullName, GetRealmName() or "Unknown")
end

local function clearPartyItemLevelInspect(runtime)
    if type(runtime.inspectFrame) == "table" and type(runtime.inspectFrame.UnregisterEvent) == "function" then
        runtime.inspectFrame:UnregisterEvent("INSPECT_READY")
    end

    runtime.pendingFullName = nil
    runtime.pendingGuid = nil
    runtime.pendingRequestedAt = 0
    runtime.pendingUnitId = nil

    if ClearInspectPlayer then
        ClearInspectPlayer()
    end
end

local function ensurePartyItemLevelInspectFrame(service, runtime)
    if type(runtime.inspectFrame) == "table" then
        return runtime.inspectFrame
    end

    local frame = CreateFrame("Frame")
    frame:SetScript("OnEvent", function(_, event, guid)
        if event ~= "INSPECT_READY" then
            return
        end

        if runtime.pendingGuid and guid ~= runtime.pendingGuid then
            return
        end

        local cacheKey = runtime.pendingFullName
        local unitId = runtime.pendingUnitId
        local itemLevel = 0

        if cacheKey and unitId and UnitExists and UnitExists(unitId) then
            itemLevel = calculateUnitItemLevel(unitId)
            runtime.cache[cacheKey] = {
                itemLevel = tonumber(itemLevel) or 0,
                updatedAt = time(),
            }
        end

        clearPartyItemLevelInspect(runtime)

        if cacheKey and (tonumber(itemLevel) or 0) > 0 then
            refreshMythicPlusView()
        end
    end)

    runtime.inspectFrame = frame
    return frame
end

local function requestPartyMemberItemLevel(service, unitId, fullName)
    if not (NotifyInspect and CanInspect and UnitGUID and UnitExists) then
        return
    end

    local cacheKey = normalizePartyItemLevelMemberId(fullName)

    if cacheKey == "" or unitId == "player" or not UnitExists(unitId) or not CanInspect(unitId) then
        return
    end

    local runtime = service:GetRuntimeState().partyItemLevels
    local now = time()
    local cached = runtime.cache[cacheKey]

    if cached and (tonumber(cached.updatedAt) or 0) + PARTY_ITEM_LEVEL_REQUEST_COOLDOWN_SECONDS > now then
        return
    end

    if runtime.pendingFullName and runtime.pendingFullName ~= cacheKey then
        if (tonumber(runtime.pendingRequestedAt) or 0) + PARTY_ITEM_LEVEL_REQUEST_COOLDOWN_SECONDS > now then
            return
        end

        clearPartyItemLevelInspect(runtime)
    elseif runtime.pendingFullName == cacheKey then
        return
    end

    local unitGuid = UnitGUID(unitId)

    if not unitGuid then
        return
    end

    local frame = ensurePartyItemLevelInspectFrame(service, runtime)
    runtime.pendingFullName = cacheKey
    runtime.pendingGuid = unitGuid
    runtime.pendingRequestedAt = now
    runtime.pendingUnitId = unitId
    frame:RegisterEvent("INSPECT_READY")

    local ok = pcall(NotifyInspect, unitId)

    if not ok then
        clearPartyItemLevelInspect(runtime)
    end
end

local function getPartyMemberItemLevel(service, unitId, fullName, fallbackItemLevel)
    local runtime = service:GetRuntimeState().partyItemLevels
    local cacheKey = normalizePartyItemLevelMemberId(fullName)
    local now = time()
    local fallbackValue = math.max(0, tonumber(fallbackItemLevel) or 0)
    local cached = cacheKey ~= "" and runtime.cache[cacheKey] or nil
    local cachedItemLevel = cached and math.max(0, tonumber(cached.itemLevel) or 0) or 0
    local cachedAt = cached and (tonumber(cached.updatedAt) or 0) or 0

    if unitId == "player" and GetAverageItemLevel then
        local averageItemLevel, equippedItemLevel = GetAverageItemLevel()
        local liveItemLevel = tonumber(equippedItemLevel) or tonumber(averageItemLevel) or 0

        if cacheKey ~= "" and liveItemLevel > 0 then
            runtime.cache[cacheKey] = {
                itemLevel = liveItemLevel,
                updatedAt = now,
            }
        end

        return math.max(fallbackValue, liveItemLevel, cachedItemLevel)
    end

    if cachedItemLevel > 0 then
        if cachedAt + PARTY_ITEM_LEVEL_CACHE_TTL_SECONDS <= now then
            requestPartyMemberItemLevel(service, unitId, cacheKey)
        end

        return math.max(fallbackValue, cachedItemLevel)
    end

    requestPartyMemberItemLevel(service, unitId, cacheKey)
    return fallbackValue
end

local function sortedPairsByName(values)
    local ordered = {}

    for key, value in pairs(values or {}) do
        ordered[#ordered + 1] = {
            key = key,
            value = value,
        }
    end

    table.sort(ordered, function(left, right)
        local leftName = lowerText(left.value and left.value.name or left.key)
        local rightName = lowerText(right.value and right.value.name or right.key)

        if leftName == rightName then
            return tostring(left.key) < tostring(right.key)
        end

        return leftName < rightName
    end)

    return ordered
end

local function getMapName(mapId)
    if not mapId then
        return "Unknown Dungeon"
    end

    local name = safeInvoke(C_ChallengeMode, "GetMapUIInfo", tonumber(mapId))

    if type(name) == "string" and name ~= "" then
        return name
    end

    return tostring(mapId)
end

local function findDungeonIdByName(dungeonName)
    local resolvedName = lowerText(dungeonName)

    if resolvedName == "" then
        return nil
    end

    local mapIds = safeInvoke(C_ChallengeMode, "GetMapTable")

    for _, mapId in ipairs(type(mapIds) == "table" and mapIds or {}) do
        local normalizedMapId = normalizeDungeonId(tostring(mapId))

        if normalizedMapId and lowerText(getMapName(normalizedMapId)) == resolvedName then
            return normalizedMapId
        end
    end

    return nil
end

local function tryGetDungeonTexture(mapId, fallbackName)
    local resolvedMapId = normalizeDungeonId(mapId)
    local resolvedName = trimString(fallbackName)

    if not resolvedMapId and resolvedName ~= "" then
        resolvedMapId = findDungeonIdByName(resolvedName)
    end

    if not resolvedMapId or type(C_ChallengeMode) ~= "table" or type(C_ChallengeMode.GetMapUIInfo) ~= "function" then
        resolvedMapId = resolvedMapId or nil
    end

    if resolvedMapId and type(C_ChallengeMode) == "table" and type(C_ChallengeMode.GetMapUIInfo) == "function" then
        local ok, _, _, _, texture, backgroundTexture = pcall(C_ChallengeMode.GetMapUIInfo, tonumber(resolvedMapId))

        if ok and (texture or backgroundTexture) then
            return texture or backgroundTexture
        end
    end

    if resolvedName == "" and resolvedMapId then
        resolvedName = getMapName(resolvedMapId)
    end

    local cacheKey = lowerText(resolvedName)

    if cacheKey ~= "" then
        local cachedTexture = ENCOUNTER_JOURNAL_TEXTURE_CACHE[cacheKey]

        if cachedTexture ~= nil then
            return cachedTexture ~= false and cachedTexture or nil
        end
    end

    if resolvedName ~= "" and type(EJ_GetInstanceByIndex) == "function" then
        local targetName = lowerText(resolvedName)

        for index = 1, 200 do
            local ok, _, journalName, _, bgImage, _, loreImage = pcall(EJ_GetInstanceByIndex, index, false)

            if not ok or journalName == nil then
                break
            end

            if lowerText(journalName) == targetName then
                local textureId = bgImage or loreImage or false

                if cacheKey ~= "" then
                    ENCOUNTER_JOURNAL_TEXTURE_CACHE[cacheKey] = textureId
                end

                return textureId ~= false and textureId or nil
            end
        end
    end

    if cacheKey ~= "" then
        ENCOUNTER_JOURNAL_TEXTURE_CACHE[cacheKey] = false
    end

    return nil
end

local function getDungeonCatalogEntry(service, dungeonId, seasonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId then
        return nil
    end

    local seasonDefinition = service:GetSeasonDungeonCatalog(seasonId)

    for _, dungeon in ipairs(seasonDefinition.dungeons or {}) do
        if dungeon.id == normalizedDungeonId then
            return dungeon
        end
    end

    return nil
end

local function buildDungeonAbbreviation(name)
    local resolvedName = trimString(name)

    if resolvedName == "" then
        return ""
    end

    local mapped = PORTAL_SHORT_NAMES[resolvedName]

    if mapped and mapped ~= "" then
        return mapped
    end

    local abbreviationParts = {}
    local connectorWords = {
        a = true,
        an = true,
        ["and"] = true,
        of = true,
        the = true,
        to = true,
    }

    for word in resolvedName:gmatch("[%a%d']+") do
        local normalizedWord = lowerText(word)

        if not connectorWords[normalizedWord] then
            abbreviationParts[#abbreviationParts + 1] = string.upper(string.sub(word, 1, 1))
        end
    end

    if #abbreviationParts <= 0 then
        return string.upper(string.sub(resolvedName, 1, math.min(4, #resolvedName)))
    end

    return table.concat(abbreviationParts, "")
end

local function getDungeonShortName(service, dungeonId, fallbackName, seasonId)
    local entry = getDungeonCatalogEntry(service, dungeonId, seasonId)

    if entry and trimString(entry.shortName) ~= "" then
        return trimString(entry.shortName)
    end

    local resolvedName = trimString(fallbackName) ~= "" and trimString(fallbackName) or getMapName(dungeonId)
    return buildDungeonAbbreviation(resolvedName)
end

function addon.MythicPlusService:GetDungeonShortLabel(dungeonId, dungeonName, seasonId)
    return getDungeonShortName(self, dungeonId, dungeonName, seasonId)
end

local function getLiveCurrentRating()
    local summary = safeInvoke(C_PlayerInfo, "GetPlayerMythicPlusRatingSummary", "player")

    if type(summary) == "table" then
        return tonumber(summary.currentSeasonScore)
            or tonumber(summary.rating)
            or tonumber(summary.score)
            or tonumber(summary.totalScore)
    end

    local rating = safeInvoke(C_MythicPlus, "GetCurrentMythicRating")
    return tonumber(rating)
end

local function getLiveCurrentKey()
    local mapId = safeInvoke(C_MythicPlus, "GetOwnedKeystoneChallengeMapID")

    if not mapId then
        mapId = safeInvoke(C_MythicPlus, "GetOwnedKeystoneMapID")
    end

    local level = safeInvoke(C_MythicPlus, "GetOwnedKeystoneLevel")

    return normalizeDungeonId(mapId and tostring(mapId) or nil), tonumber(level) or 0
end

local function getLiveCurrentKeyDetails()
    local challengeMapId = safeInvoke(C_MythicPlus, "GetOwnedKeystoneChallengeMapID")
    local mythicPlusMapId = safeInvoke(C_MythicPlus, "GetOwnedKeystoneMapID")
    local keyLevel = tonumber(safeInvoke(C_MythicPlus, "GetOwnedKeystoneLevel")) or 0
    local resolvedMapId = challengeMapId or mythicPlusMapId

    return {
        challengeMapId = normalizeDungeonId(challengeMapId and tostring(challengeMapId) or nil),
        keyLevel = keyLevel,
        mapName = resolvedMapId and getMapName(resolvedMapId) or nil,
        mythicPlusMapId = normalizeDungeonId(mythicPlusMapId and tostring(mythicPlusMapId) or nil),
    }
end

local function getUnitMythicPlusScore(unitId)
    local summary = safeInvoke(C_PlayerInfo, "GetPlayerMythicPlusRatingSummary", unitId)

    if type(summary) == "table" then
        return tonumber(summary.currentSeasonScore)
            or tonumber(summary.rating)
            or tonumber(summary.score)
            or tonumber(summary.totalScore)
    end

    return nil
end

local function getUnitFullName(unitId)
    if not UnitExists or not UnitExists(unitId) then
        return nil
    end

    local name, realm = UnitFullName and UnitFullName(unitId) or UnitName(unitId)

    if trimString(name) == "" then
        return nil
    end

    return addon.Store:NormalizeFullName(name, realm or GetRealmName() or "Unknown")
end

local function capturePartyScoreSnapshot()
    local snapshot = {
        classes = {},
        order = {},
        scores = {},
    }
    local seen = {}

    for _, unitId in ipairs(buildGroupUnits()) do
        local fullName = getUnitFullName(unitId)

        if fullName and not seen[fullName] then
            seen[fullName] = true
            snapshot.order[#snapshot.order + 1] = fullName
            snapshot.classes[fullName] = trimString(UnitClass and select(2, UnitClass(unitId)) or "")
            snapshot.scores[fullName] = getUnitMythicPlusScore(unitId)
        end
    end

    return snapshot
end

local function safeMessageText(value)
    return trimString(value)
end

local function extractSummaryItemLinks(messageText)
    local links = {}
    local text = safeMessageText(messageText)

    for itemLink in string.gmatch(text, "(|Hitem:.-|h%[.-%]|h)") do
        links[#links + 1] = itemLink
    end

    return links
end

local function extractLootOwnerFromMessage(messageText)
    local text = safeMessageText(messageText)
    local normalized = lowerText(string.gsub(string.gsub(text, "|c%x%x%x%x%x%x%x%x", ""), "|r", ""))

    if string.find(normalized, "you receive", 1, true) or string.find(normalized, "you create", 1, true) then
        return addon.Store:GetPlayerName()
    end

    local owner = string.match(text, "^([^:]+)%s+receives%s+loot:")
        or string.match(text, "^([^:]+)%s+receives%s+bonus%s+loot:")
        or string.match(text, "^([^:]+)%s+receives%s+item:")
        or string.match(text, "^([^:]+)%s+receives%s+")
        or string.match(text, "^([^:]+)%s+creates%s+")

    owner = trimString(owner)

    if owner == "" then
        return "Unknown"
    end

    owner = string.gsub(string.gsub(owner, "|c%x%x%x%x%x%x%x%x", ""), "|r", "")
    return addon.Store:NormalizeFullName(owner, GetRealmName() or "Unknown")
end

local function getItemIdFromLink(itemLink)
    local itemId = tonumber(safeStringMatch(itemLink, "item:(%d+):"))

    if itemId then
        return itemId
    end

    if type(GetItemInfoInstant) == "function" then
        return tonumber((select(1, GetItemInfoInstant(itemLink))))
    end

    return nil
end

local function isSummaryGearItem(itemLink)
    local itemId = getItemIdFromLink(itemLink)

    if itemId and type(C_Item) == "table" and type(C_Item.IsItemKeystoneByID) == "function" and C_Item.IsItemKeystoneByID(itemId) then
        return false
    end

    if type(IsEquippableItem) == "function" then
        local ok, equippable = pcall(IsEquippableItem, itemLink)

        if ok and equippable ~= nil then
            return equippable and true or false
        end
    end

    if type(GetItemInfoInstant) == "function" then
        local _, _, _, _, _, classId = GetItemInfoInstant(itemLink)

        if classId then
            return classId == 2 or classId == 4
        end
    end

    return true
end

local function getSummaryItemLevel(itemLink)
    if type(GetDetailedItemLevelInfo) ~= "function" then
        return 0
    end

    local ok, itemLevel = pcall(GetDetailedItemLevelInfo, itemLink)

    if not ok then
        return 0
    end

    return math.max(0, math.floor(tonumber(itemLevel) or 0))
end

local function getSummaryItemName(itemLink)
    local textName = safeStringMatch(itemLink, "%[(.-)%]")

    if trimString(textName) ~= "" then
        return trimString(textName)
    end

    if type(GetItemInfo) == "function" then
        local itemName = GetItemInfo(itemLink)

        if trimString(itemName) ~= "" then
            return trimString(itemName)
        end
    end

    return "Unknown Item"
end

local function findOwnedKeystoneBagSlot()
    if type(C_Container) ~= "table"
        or type(C_Container.GetContainerNumSlots) ~= "function"
        or type(C_Container.GetContainerItemID) ~= "function"
        or type(C_Item) ~= "table"
        or type(C_Item.IsItemKeystoneByID) ~= "function" then
        return nil, nil, nil
    end

    local maxBagSlots = tonumber(NUM_BAG_SLOTS) or 4

    for bagIndex = 0, maxBagSlots do
        local slotCount = math.max(0, math.floor(tonumber(C_Container.GetContainerNumSlots(bagIndex)) or 0))

        for slotIndex = 1, slotCount do
            local itemId = C_Container.GetContainerItemID(bagIndex, slotIndex)

            if itemId and C_Item.IsItemKeystoneByID(itemId) then
                return bagIndex, slotIndex, itemId
            end
        end
    end

    return nil, nil, nil
end

local function useContainerItemBySlot(bagIndex, slotIndex)
    if bagIndex == nil or slotIndex == nil then
        return false, "keystone bag slot was not found"
    end

    if type(C_Container) == "table" and type(C_Container.UseContainerItem) == "function" then
        return pcall(C_Container.UseContainerItem, bagIndex, slotIndex)
    end

    if type(UseContainerItem) == "function" then
        return pcall(UseContainerItem, bagIndex, slotIndex)
    end

    return false, "container item use is unavailable"
end

local function extractNumber(value, keys)
    if type(value) ~= "table" then
        return nil
    end

    for _, key in ipairs(keys or {}) do
        local candidate = value[key]

        if type(candidate) == "number" then
            return candidate
        end
    end

    return nil
end

local function extractBoolean(value, keys)
    if type(value) ~= "table" then
        return nil
    end

    for _, key in ipairs(keys or {}) do
        local candidate = value[key]

        if type(candidate) == "boolean" then
            return candidate
        end
    end

    return nil
end

local function extractString(value, keys)
    if type(value) ~= "table" then
        return nil
    end

    for _, key in ipairs(keys or {}) do
        local candidate = trimString(value[key])

        if candidate ~= "" then
            return candidate
        end
    end

    return nil
end

local function extractAffixName(value)
    if type(value) ~= "table" then
        return nil
    end

    local directName = extractString(value, { "name", "affixName", "keystoneAffixName" })

    if directName then
        return directName
    end

    local affixes = value.affixes or value.keystoneAffixes

    if type(affixes) == "table" then
        for _, affix in ipairs(affixes) do
            local nestedName = extractString(affix, { "name", "affixName", "description" })

            if nestedName then
                return nestedName
            end
        end
    end

    return nil
end

local function isFortifiedAffix(value)
    local name = lowerText(extractAffixName(value))
    return name ~= "" and string.find(name, "fortified", 1, true) ~= nil or false
end

local function isTyrannicalAffix(value)
    local name = lowerText(extractAffixName(value))
    return name ~= "" and string.find(name, "tyrannical", 1, true) ~= nil or false
end

local function extractMapId(value)
    local mapId = extractNumber(value, {
        "challengeModeID",
        "mapChallengeModeID",
        "challengeModeMapID",
        "challengeMapID",
        "mapID",
        "id",
        "mapId",
    })

    if mapId then
        return normalizeDungeonId(tostring(math.floor(mapId)))
    end

    return normalizeDungeonId(extractString(value, { "challengeModeID", "mapChallengeModeID", "challengeModeMapID", "mapID", "id", "mapId" }))
end

local function extractRunLevel(value)
    return extractNumber(value, {
        "bestRunLevel",
        "bestLevel",
        "level",
        "keystoneLevel",
        "mapChallengeLevel",
        "challengeLevel",
    })
end

local function extractRunScore(value)
    return extractNumber(value, {
        "bestRunScore",
        "mapScore",
        "score",
        "overallScore",
        "rating",
        "weightedScore",
        "currentSeasonScore",
    })
end

local function extractTimedFlag(value, allowCompletionFallback)
    local explicit = extractBoolean(value, {
        "onTime",
        "completedOnTime",
        "timed",
        "isCompletedWithinTime",
        "mapCompletedInTime",
    })

    if explicit ~= nil then
        return explicit
    end

    if allowCompletionFallback == false then
        return nil
    end

    return extractBoolean(value, { "completed", "finishedSuccess", "success" })
end

local function clampUpgradeLevels(value)
    local upgradeLevels = math.floor(tonumber(value) or 0)

    if upgradeLevels < 0 then
        return 0
    end

    if upgradeLevels > 3 then
        return 3
    end

    return upgradeLevels
end

local function extractUpgradeLevels(value)
    local upgradeLevels = extractNumber(value, {
        "keystoneUpgradeLevels",
        "upgradeLevels",
        "upgradeLevel",
        "levelIncrement",
        "keystoneLevelIncrement",
        "numChests",
        "chests",
    })

    if upgradeLevels == nil then
        return nil
    end

    return clampUpgradeLevels(upgradeLevels)
end

local function extractDurationSeconds(value)
    local seconds = extractNumber(value, {
        "durationSec",
        "durationSeconds",
        "time",
        "elapsedTime",
        "completionTime",
    })

    if seconds then
        return seconds
    end

    local milliseconds = extractNumber(value, {
        "bestRunDurationMS",
        "bestRunDurationMs",
        "durationMS",
        "durationMs",
        "elapsedTimeMS",
        "elapsedTimeMs",
        "completionTimeMS",
        "completionTimeMs",
    })

    if milliseconds then
        return milliseconds / 1000
    end

    return nil
end

local function getLiveMythicRatingSummaryRuns()
    local summary = safeInvoke(C_PlayerInfo, "GetPlayerMythicPlusRatingSummary", "player")

    if type(summary) ~= "table" then
        return nil, nil
    end

    if type(summary.runs) == "table" then
        return summary.runs, summary
    end

    if type(summary.mapSummary) == "table" then
        return summary.mapSummary, summary
    end

    if type(summary.maps) == "table" then
        return summary.maps, summary
    end

    return nil, summary
end

local function getDungeonTimeLimitSeconds(mapId)
    if type(C_ChallengeMode) ~= "table" or type(C_ChallengeMode.GetMapUIInfo) ~= "function" then
        return nil
    end

    local normalizedMapId = tonumber(mapId)

    if not normalizedMapId then
        return nil
    end

    local _, _, timeLimit = safeInvoke(C_ChallengeMode, "GetMapUIInfo", normalizedMapId)
    local numericTimeLimit = tonumber(timeLimit)

    if numericTimeLimit and numericTimeLimit > 0 then
        return numericTimeLimit
    end

    return nil
end

local function inferTimedFromDuration(mapId, durationSeconds, explicitTimed)
    if explicitTimed ~= nil then
        return explicitTimed and true or false
    end

    local numericDuration = tonumber(durationSeconds)
    local timeLimit = getDungeonTimeLimitSeconds(mapId)

    if not numericDuration or numericDuration <= 0 or not timeLimit then
        return nil
    end

    return numericDuration <= timeLimit
end

local function inferUpgradeLevelsFromDuration(mapId, durationSeconds, explicitUpgradeLevels, timed)
    if explicitUpgradeLevels ~= nil then
        return clampUpgradeLevels(explicitUpgradeLevels)
    end

    if timed == false then
        return 0
    end

    local numericDuration = tonumber(durationSeconds)
    local timeLimit = getDungeonTimeLimitSeconds(mapId)

    if not numericDuration or numericDuration <= 0 or not timeLimit or timeLimit <= 0 then
        return timed and 1 or nil
    end

    local ratio = numericDuration / timeLimit

    if ratio <= 0.60 then
        return 3
    end

    if ratio <= 0.80 then
        return 2
    end

    if ratio <= 1 then
        return 1
    end

    return 0
end

local function buildLiveDungeonUpdateFromRun(runEntry, fallbackMapId)
    if type(runEntry) ~= "table" then
        return nil, nil
    end

    local dungeonId = extractMapId(runEntry) or normalizeDungeonId(fallbackMapId and tostring(fallbackMapId) or nil)
    local bestLevel = extractRunLevel(runEntry)
    local score = extractRunScore(runEntry)

    if not dungeonId or not (bestLevel or score) then
        return nil, nil
    end

    local durationSeconds = extractDurationSeconds(runEntry)
    local timed = inferTimedFromDuration(dungeonId, durationSeconds, extractTimedFlag(runEntry, false))
    local upgradeLevels = inferUpgradeLevelsFromDuration(dungeonId, durationSeconds, extractUpgradeLevels(runEntry), timed)
    local update = {
        bestLevel = bestLevel,
        bestLevelUpgrades = timed ~= false and bestLevel and (upgradeLevels or 1) or 0,
        bestTimedLevel = timed ~= false and bestLevel or nil,
        name = extractString(runEntry, { "name", "mapName", "challengeModeName" }) or getMapName(dungeonId),
        score = score,
    }

    if isFortifiedAffix(runEntry) and score then
        update.fortifiedScore = score
    elseif isTyrannicalAffix(runEntry) and score then
        update.tyrannicalScore = score
    end

    return dungeonId, update
end

local function getChallengeCompletionInfo()
    local info = safeInvoke(C_ChallengeMode, "GetChallengeCompletionInfo")

    if type(info) == "table" then
        return info
    end

    if type(C_ChallengeMode) ~= "table" or type(C_ChallengeMode.GetCompletionInfo) ~= "function" then
        return nil
    end

    local ok, mapChallengeModeID, level, elapsedTime, onTime, keystoneUpgradeLevels, practiceRun = pcall(C_ChallengeMode.GetCompletionInfo)

    if not ok or not mapChallengeModeID then
        return nil
    end

    return {
        keystoneUpgradeLevels = keystoneUpgradeLevels,
        level = level,
        mapChallengeModeID = mapChallengeModeID,
        onTime = onTime,
        practiceRun = practiceRun,
        time = elapsedTime,
    }
end

local function buildResolvedChallengeCompletionInfo(runtimeRun, completionInfo)
    local activeRun = type(runtimeRun) == "table" and runtimeRun or nil
    local mapId = extractMapId(completionInfo) or (activeRun and normalizeDungeonId(activeRun.mapId) or nil)
    local level = math.max(0, math.floor(extractRunLevel(completionInfo) or tonumber(activeRun and activeRun.keyLevel) or 0))
    local duration = extractDurationSeconds(completionInfo)

    if not duration and activeRun then
        local startClock = tonumber(activeRun.startClock) or 0

        if startClock > 0 then
            duration = math.max(0, getClockSeconds() - startClock)
        end
    end

    local onTime = extractTimedFlag(completionInfo, false)

    if onTime == nil then
        onTime = inferTimedFromDuration(mapId, duration, nil)
    end

    local upgradeLevels = extractUpgradeLevels(completionInfo)

    if upgradeLevels == nil then
        upgradeLevels = inferUpgradeLevelsFromDuration(mapId, duration, nil, onTime)
    end

    if not mapId and level <= 0 then
        return nil
    end

    return {
        keystoneUpgradeLevels = upgradeLevels,
        level = level > 0 and level or nil,
        mapChallengeModeID = mapId and tonumber(mapId) or nil,
        onTime = onTime,
        practiceRun = type(completionInfo) == "table" and completionInfo.practiceRun and true or false,
        time = duration,
    }
end

local function buildCompletionInfoFromOverlaySnapshot(snapshot)
    if type(snapshot) ~= "table" then
        return nil
    end

    local mapId = normalizeDungeonId(snapshot.mapId and tostring(snapshot.mapId) or nil)
    local level = math.max(0, math.floor(tonumber(snapshot.keyLevel) or 0))
    local timeLimit = tonumber(snapshot.timeLimit) or 0
    local elapsed = tonumber(snapshot.elapsed)

    if elapsed == nil then
        local remaining = tonumber(snapshot.remaining)

        if timeLimit > 0 and remaining ~= nil then
            elapsed = math.max(0, timeLimit - remaining)
        end
    end

    if not mapId and level <= 0 then
        return nil
    end

    local onTime = timeLimit <= 0 or (tonumber(elapsed) or 0) <= timeLimit

    return {
        keystoneUpgradeLevels = inferUpgradeLevelsFromDuration(mapId, elapsed, nil, onTime),
        level = level > 0 and level or nil,
        mapChallengeModeID = mapId and tonumber(mapId) or nil,
        onTime = onTime,
        practiceRun = false,
        time = elapsed,
    }
end

local function buildCompletionInfoFromSavedActiveRun(savedRun)
    if type(savedRun) ~= "table" then
        return nil
    end

    local mapId = normalizeDungeonId(savedRun.mapId and tostring(savedRun.mapId) or nil)
    local level = math.max(0, math.floor(tonumber(savedRun.keyLevel) or 0))
    local timeLimit = tonumber(savedRun.timeLimit) or 0
    local startedAtWallClock = tonumber(savedRun.startedAtWallClock) or 0

    if not mapId and level <= 0 then
        return nil
    end

    local elapsed = nil

    if startedAtWallClock > 0 then
        elapsed = math.max(0, getWallClockSeconds() - startedAtWallClock)
    end

    local onTime = timeLimit <= 0 or (tonumber(elapsed) or 0) <= timeLimit

    return {
        keystoneUpgradeLevels = inferUpgradeLevelsFromDuration(mapId, elapsed, nil, onTime),
        level = level > 0 and level or nil,
        mapChallengeModeID = mapId and tonumber(mapId) or nil,
        onTime = onTime,
        practiceRun = false,
        time = elapsed,
    }
end

local function inferUpgradeLevels(bestLevel, bestTimedLevel, recordedUpgradeLevels)
    local highestLevel = math.max(tonumber(bestLevel) or 0, tonumber(bestTimedLevel) or 0)
    local upgradeLevels = clampUpgradeLevels(recordedUpgradeLevels)

    if highestLevel <= 0 then
        return 0, 0
    end

    if upgradeLevels <= 0 and (tonumber(bestTimedLevel) or 0) >= highestLevel then
        upgradeLevels = 1
    end

    return highestLevel, upgradeLevels
end

local function formatBestRunLabelFromValues(bestLevel, bestTimedLevel, recordedUpgradeLevels)
    local highestLevel, upgradeLevels = inferUpgradeLevels(bestLevel, bestTimedLevel, recordedUpgradeLevels)

    if highestLevel <= 0 then
        return "-"
    end

    if upgradeLevels > 0 then
        return tostring(highestLevel) .. string.rep("+", upgradeLevels)
    end

    return tostring(highestLevel)
end

local function formatBestRunLabel(record)
    if type(record) ~= "table" then
        return "-"
    end

    return formatBestRunLabelFromValues(record.bestLevel, record.bestTimedLevel, record.bestLevelUpgrades)
end

local function hasMeaningfulDungeonData(record)
    if type(record) ~= "table" then
        return false
    end

    return (tonumber(record.bestLevel) or 0) > 0
        or (tonumber(record.bestTimedLevel) or 0) > 0
        or (tonumber(record.score) or 0) > 0
        or (tonumber(record.fortifiedScore) or 0) > 0
        or (tonumber(record.tyrannicalScore) or 0) > 0
        or record.portalUnlocked == true
end

local function hasMeaningfulSeasonData(seasonProfile)
    if type(seasonProfile) ~= "table" then
        return false
    end

    if (tonumber(seasonProfile.overallRating) or 0) > 0 then
        return true
    end

    if type(seasonProfile.currentKey) == "table" and (tonumber(seasonProfile.currentKey.level) or 0) > 0 then
        return true
    end

    for _, record in pairs(seasonProfile.dungeons or {}) do
        if hasMeaningfulDungeonData(record) then
            return true
        end
    end

    return false
end

local PORTAL_SPELLBOOK_CACHE_SECONDS = 5
local portalSpellbookCache = {
    builtAt = 0,
    entries = nil,
}

local function getKnownPortalSpellbookEntries()
    local now = time()

    if type(portalSpellbookCache.entries) == "table" and (now - (tonumber(portalSpellbookCache.builtAt) or 0)) < PORTAL_SPELLBOOK_CACHE_SECONDS then
        return portalSpellbookCache.entries
    end

    local entries = {}
    local foundAny = false

    -- Modern TWW C_SpellBook API (11.0+)
    if C_SpellBook and C_SpellBook.GetNumSpellBookSkillLines and C_SpellBook.GetSpellBookSkillLineInfo then
        local playerBank = (Enum and Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player) or 0
        local numLines = C_SpellBook.GetNumSpellBookSkillLines() or 0

        for lineIndex = 1, numLines do
            local lineInfo = C_SpellBook.GetSpellBookSkillLineInfo(lineIndex)

            if type(lineInfo) == "table" then
                local offset = tonumber(lineInfo.itemIndexOffset) or 0
                local count = tonumber(lineInfo.numSpellBookItems) or 0

                for slotIndex = offset + 1, offset + count do
                    local spellId = nil
                    local spellName = ""

                    if C_SpellBook.GetSpellBookItemInfo then
                        local ok, info = pcall(C_SpellBook.GetSpellBookItemInfo, playerBank, slotIndex)
                        if ok and type(info) == "table" then
                            spellId = tonumber(info.spellID) or tonumber(info[2]) or nil
                        end
                    end

                    if spellId and C_SpellBook.GetSpellBookItemName then
                        local ok, name = pcall(C_SpellBook.GetSpellBookItemName, playerBank, slotIndex)
                        if ok and type(name) == "string" then
                            spellName = trimString(name)
                        end
                    end

                    if spellId and spellName == "" and C_Spell and C_Spell.GetSpellName then
                        spellName = trimString(C_Spell.GetSpellName(spellId))
                    end

                    if spellName ~= "" and spellId then
                        entries[#entries + 1] = {
                            spellId = spellId,
                            spellName = spellName,
                        }
                        foundAny = true
                    end
                end
            end
        end
    elseif GetNumSpellTabs and GetSpellTabInfo and GetSpellBookItemName then
        -- Legacy API fallback (pre-TWW 11.0)
        local bookType = BOOKTYPE_SPELL or "spell"

        for tabIndex = 1, (GetNumSpellTabs() or 0) do
            local _, _, offset, numSlots = GetSpellTabInfo(tabIndex)
            local firstSlot = (tonumber(offset) or 0) + 1
            local lastSlot = (tonumber(offset) or 0) + (tonumber(numSlots) or 0)

            for slotIndex = firstSlot, lastSlot do
                local spellName = trimString(GetSpellBookItemName(slotIndex, bookType))
                local spellType, spellId = nil, nil

                if GetSpellBookItemInfo then
                    spellType, spellId = GetSpellBookItemInfo(slotIndex, bookType)
                end

                if spellName == "" and tonumber(spellId) and C_Spell and C_Spell.GetSpellName then
                    spellName = trimString(C_Spell.GetSpellName(tonumber(spellId)))
                elseif spellName == "" and tonumber(spellId) and GetSpellInfo then
                    spellName = trimString(GetSpellInfo(tonumber(spellId)))
                end

                if spellName ~= "" and tonumber(spellId) then
                    entries[#entries + 1] = {
                        spellId = tonumber(spellId) or nil,
                        spellName = spellName,
                        spellType = spellType,
                    }
                    foundAny = true
                end
            end
        end
    end

    portalSpellbookCache.builtAt = now
    portalSpellbookCache.entries = foundAny and entries or nil
    return portalSpellbookCache.entries
end

local function getMappedPortalSpellInfo(dungeonId)
    local numericDungeonId = tonumber(dungeonId)

    if not numericDungeonId then
        return nil, nil, nil
    end

    local spellId = PORTAL_SPELL_IDS[numericDungeonId]
    local factionName = UnitFactionGroup and UnitFactionGroup("player") or nil

    if factionName == "Horde" and PORTAL_SPELL_IDS_HORDE[numericDungeonId] then
        spellId = PORTAL_SPELL_IDS_HORDE[numericDungeonId]
    end

    if not spellId then
        return nil, nil, nil
    end

    local spellName = nil

    if C_Spell and C_Spell.GetSpellName then
        spellName = trimString(C_Spell.GetSpellName(spellId))
    elseif GetSpellInfo then
        spellName = trimString(GetSpellInfo(spellId))
    end

    local spellKnown = true

    if IsSpellKnown and not IsSpellKnown(spellId) then
        -- IsPlayerSpell catches spells that IsSpellKnown may miss in TWW (e.g. unlocked via M+ teleports)
        if not (IsPlayerSpell and IsPlayerSpell(spellId)) then
            spellKnown = false
        end
    end

    return spellId, spellName ~= "" and spellName or nil, spellKnown
end

local function getPortalSpellInfoForDungeonName(dungeonName)
    local normalizedDungeonName = trimString(dungeonName)
    local normalizedPortalMatchName = normalizePortalMatchText(normalizedDungeonName)

    if normalizedDungeonName == "" then
        return nil, nil, nil
    end

    local knownSpellEntries = getKnownPortalSpellbookEntries()

    if type(knownSpellEntries) == "table" then
        for _, entry in ipairs(knownSpellEntries) do
            local spellName = trimString(entry and entry.spellName)
            local normalizedSpellName = normalizePortalMatchText(spellName)

            if spellName == normalizedDungeonName then
                return tonumber(entry.spellId) or nil, spellName, true
            end

            if #spellName > #normalizedDungeonName and string.sub(spellName, -#normalizedDungeonName) == normalizedDungeonName then
                return tonumber(entry.spellId) or nil, spellName, true
            end

            if normalizedPortalMatchName ~= "" and normalizedSpellName == normalizedPortalMatchName then
                return tonumber(entry.spellId) or nil, spellName, true
            end

            if normalizedPortalMatchName ~= "" and #normalizedSpellName > #normalizedPortalMatchName and string.sub(normalizedSpellName, -#normalizedPortalMatchName) == normalizedPortalMatchName then
                return tonumber(entry.spellId) or nil, spellName, true
            end
        end

        return nil, nil, false
    end

    return nil, nil, nil
end

local function resolvePartyMemberFullNameFromSender(sender)
    local normalizedSender = addon.Store:NormalizeFullName(sender, GetRealmName() or "Unknown")
    local senderName, senderRealm = addon.Store:SplitFullName(normalizedSender, GetRealmName() or "Unknown")
    local fallbackMatch = nil

    for _, unitId in ipairs(buildGroupUnits()) do
        if UnitExists and UnitExists(unitId) then
            local unitName, unitRealm = UnitFullName(unitId)

            if trimString(unitName) == "" then
                unitName = UnitName and UnitName(unitId) or nil
            end

            if trimString(unitName) ~= "" then
                local normalizedUnit = addon.Store:NormalizeFullName(unitName, unitRealm or GetRealmName() or "Unknown")

                if normalizedUnit == normalizedSender then
                    return normalizedUnit
                end

                local shortUnitName, shortUnitRealm = addon.Store:SplitFullName(normalizedUnit, GetRealmName() or "Unknown")

                if lowerText(shortUnitName) == lowerText(senderName) then
                    if lowerText(shortUnitRealm) == lowerText(senderRealm) then
                        return normalizedUnit
                    end

                    if not fallbackMatch then
                        fallbackMatch = normalizedUnit
                    end
                end
            end
        end
    end

    return fallbackMatch or normalizedSender
end

local function getPartyKeyForMember(state, fullName)
    if type(state) ~= "table" or type(state.partyKeys) ~= "table" then
        return nil, nil
    end

    local normalizedMemberName = addon.Store:NormalizeFullName(fullName, GetRealmName() or "Unknown")

    if normalizedMemberName == "" then
        return nil, nil
    end

    local exactMatch = state.partyKeys[normalizedMemberName]

    if type(exactMatch) == "table" then
        return exactMatch, normalizedMemberName
    end

    local memberShortName, memberRealm = addon.Store:SplitFullName(normalizedMemberName, GetRealmName() or "Unknown")
    local fallbackData = nil
    local fallbackKey = nil
    local fallbackCount = 0

    for candidateKey, candidateData in pairs(state.partyKeys) do
        if type(candidateData) == "table" then
            local normalizedCandidate = addon.Store:NormalizeFullName(candidateKey, GetRealmName() or "Unknown")
            local candidateShortName, candidateRealm = addon.Store:SplitFullName(normalizedCandidate, GetRealmName() or "Unknown")

            if lowerText(candidateShortName) == lowerText(memberShortName) then
                if lowerText(candidateRealm) == lowerText(memberRealm) then
                    return candidateData, normalizedCandidate
                end

                fallbackCount = fallbackCount + 1

                if fallbackCount == 1 then
                    fallbackData = candidateData
                    fallbackKey = normalizedCandidate
                end
            end
        end
    end

    if fallbackCount == 1 then
        return fallbackData, fallbackKey
    end

    return nil, nil
end

local function isPortalUnlockedInRecord(seasonProfile, dungeonRecord)
    if type(dungeonRecord) ~= "table" then
        return false
    end

    if dungeonRecord.portalUnlocked == true then
        return true
    end

    local threshold = tonumber(seasonProfile and seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD
    local bestTimedLevel = tonumber(dungeonRecord.bestTimedLevel) or 0
    local bestLevel = math.max(bestTimedLevel, tonumber(dungeonRecord.bestLevel) or 0)

    return bestTimedLevel >= threshold or bestLevel >= threshold
end

local function isLocalWarbandToon(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" then
        return false
    end

    if normalizedToonId == addon.Store:GetPlayerName() then
        return true
    end

    local knownCharacters = addon.Store:GetProfile().myCharacters or {}

    if knownCharacters[normalizedToonId] then
        return true
    end

    if addon.RosterService and addon.RosterService.IsMyToon then
        return addon.RosterService:IsMyToon(normalizedToonId) and true or false
    end

    return false
end

local function collectLocalWarbandToonIds(primaryToonId)
    local ids = {}
    local seen = {}

    local function addId(candidateId)
        local normalizedId = addon.Store:NormalizeFullName(candidateId, GetRealmName() or "Unknown")

        if normalizedId ~= "" and not seen[normalizedId] then
            seen[normalizedId] = true
            ids[#ids + 1] = normalizedId
        end
    end

    addId(primaryToonId)
    addId(addon.Store:GetPlayerName())

    for characterId in pairs(addon.Store:GetProfile().myCharacters or {}) do
        addId(characterId)
    end

    if addon.RosterService and addon.RosterService.ListMyToons then
        for _, member in ipairs(addon.RosterService:ListMyToons()) do
            addId(member and (member.id or member.fullName))
        end
    end

    return ids
end

local function isPortalUnlockedForWarbandToon(toonId, dungeonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId or not isLocalWarbandToon(toonId) then
        return false
    end

    for _, localToonId in ipairs(collectLocalWarbandToonIds(toonId)) do
        local seasonProfile = addon.MythicPlusService:GetStoredSeasonProfile(localToonId)
        local record = seasonProfile and seasonProfile.dungeons and seasonProfile.dungeons[normalizedDungeonId] or nil

        if isPortalUnlockedInRecord(seasonProfile, record) then
            return true
        end
    end

    return false
end

local function isPortalUnlockedForRecord(seasonProfile, dungeonRecord, dungeonId, dungeonName, toonId)
    local resolvedDungeonId = normalizeDungeonId(type(dungeonRecord) == "table" and dungeonRecord.id or dungeonId)
    local resolvedDungeonName = trimString(type(dungeonRecord) == "table" and dungeonRecord.name or dungeonName)

    local _, spellName, spellKnown = getMappedPortalSpellInfo(resolvedDungeonId)

    if spellKnown == nil then
        _, spellName, spellKnown = getPortalSpellInfoForDungeonName(resolvedDungeonName)
    end

    if spellKnown == true then
        return true
    end

    -- spellKnown == false means the spell ID is mapped but not currently learned by this character.
    -- Fall through to stored completion data so Warband/account progress still displays correctly.

    if isPortalUnlockedInRecord(seasonProfile, dungeonRecord) then
        return true
    end

    return isPortalUnlockedForWarbandToon(toonId, resolvedDungeonId)
end

function addon.MythicPlusService:GetPortalSpellName(dungeonId, dungeonName)
    local resolvedDungeonName = trimString(dungeonName)
    local spellId, mappedSpellName, spellKnown = getMappedPortalSpellInfo(dungeonId)

    if mappedSpellName then
        return mappedSpellName
    end

    if resolvedDungeonName == "" then
        resolvedDungeonName = getMapName(dungeonId)
    end

    local _, spellName, spellKnown = getPortalSpellInfoForDungeonName(resolvedDungeonName)

    if spellKnown then
        return spellName
    end

    if resolvedDungeonName == "" then
        return nil
    end

    return "Teleport: " .. resolvedDungeonName
end

function addon.MythicPlusService:GetPortalSpellId(dungeonId, dungeonName)
    local spellId, _, spellKnown = getMappedPortalSpellInfo(dungeonId)

    if spellKnown then
        return spellId
    end

    local resolvedDungeonName = trimString(dungeonName)

    if resolvedDungeonName == "" then
        resolvedDungeonName = getMapName(dungeonId)
    end

    spellId, _, spellKnown = getPortalSpellInfoForDungeonName(resolvedDungeonName)

    if spellKnown then
        return spellId
    end

    return nil
end

local function getFallbackSeasonDungeonName(dungeonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local mapName = trimString(getMapName(normalizedDungeonId))

    if mapName ~= "" and mapName ~= tostring(normalizedDungeonId) and mapName ~= "Unknown Dungeon" then
        return mapName
    end

    local _, spellName = getMappedPortalSpellInfo(normalizedDungeonId)
    local strippedSpellName = stripPortalSpellPrefix(spellName)

    if strippedSpellName ~= "" then
        return strippedSpellName
    end

    return mapName ~= "" and mapName or tostring(normalizedDungeonId or "Dungeon")
end

local function formatDurationLabel(totalSeconds)
    local seconds = math.max(0, math.floor(tonumber(totalSeconds) or 0))
    local hours = math.floor(seconds / 3600)
    local minutes = math.floor((seconds % 3600) / 60)

    if hours > 0 then
        return string.format("%dh %dm", hours, minutes)
    end

    if minutes > 0 then
        return string.format("%dm", minutes)
    end

    return string.format("%ds", seconds)
end

local function getSpellCooldownInfo(spellReference)
    local cooldownInfo = C_Spell and C_Spell.GetSpellCooldown and C_Spell.GetSpellCooldown(spellReference) or nil

    if type(cooldownInfo) ~= "table" then
        return nil
    end

    local remainingSeconds = math.max(
        tonumber(cooldownInfo.timeUntilEndOfCooldown) or 0,
        tonumber(cooldownInfo.timeUntilEndOfCharge) or 0,
        tonumber(cooldownInfo.timeUntilEndOfStartRecovery) or 0
    )

    if remainingSeconds <= 0 then
        local startTime = tonumber(cooldownInfo.startTime) or 0
        local duration = tonumber(cooldownInfo.duration) or 0

        if startTime > 0 and duration > 0 and GetTime then
            remainingSeconds = math.max(0, (startTime + duration) - GetTime())
        end
    end

    return {
        isOnCooldown = remainingSeconds > 0,
        label = remainingSeconds > 0 and formatDurationLabel(remainingSeconds) or "Ready",
        remainingSeconds = remainingSeconds,
    }
end

local function getSharedPortalCooldownInfo()
    local spellIds = {}

    for _, spellId in pairs(PORTAL_SPELL_IDS) do
        if tonumber(spellId) then
            spellIds[tonumber(spellId)] = true
        end
    end

    for _, spellId in pairs(PORTAL_SPELL_IDS_HORDE) do
        if tonumber(spellId) then
            spellIds[tonumber(spellId)] = true
        end
    end

    local bestInfo = nil

    for spellId in pairs(spellIds) do
        if not IsSpellKnown or IsSpellKnown(spellId) then
            local cooldownInfo = getSpellCooldownInfo(spellId)

            if cooldownInfo and cooldownInfo.isOnCooldown then
                if not bestInfo or (cooldownInfo.remainingSeconds or 0) > (bestInfo.remainingSeconds or 0) then
                    bestInfo = cooldownInfo
                end
            end
        end
    end

    return bestInfo
end

function addon.MythicPlusService:GetPortalCooldownInfo(dungeonId, dungeonName)
    local spellId = self:GetPortalSpellId(dungeonId, dungeonName)
    local spellName = self:GetPortalSpellName(dungeonId, dungeonName)

    if not spellId and not spellName then
        return nil
    end

    local cooldownInfo = getSpellCooldownInfo(spellId or spellName)

    if not cooldownInfo or not cooldownInfo.isOnCooldown then
        cooldownInfo = getSharedPortalCooldownInfo() or cooldownInfo
    end

    return {
        isOnCooldown = cooldownInfo and cooldownInfo.isOnCooldown or false,
        label = cooldownInfo and cooldownInfo.label or "Ready",
        remainingSeconds = cooldownInfo and cooldownInfo.remainingSeconds or 0,
        spellId = spellId,
        spellName = spellName,
    }
end

function addon.MythicPlusService:TrySlotCurrentKeystoneForMember(fullName, options)
    options = type(options) == "table" and options or {}

    local normalizedMember = addon.Store:NormalizeFullName(fullName, GetRealmName() or "Unknown")
    local playerName = addon.Store:GetPlayerName()

    if InCombatLockdown and InCombatLockdown() then
        return false, "cannot slot keystone during combat"
    end

    if normalizedMember == "" or normalizedMember ~= playerName then
        return false, "member is not the current player"
    end

    if C_ChallengeMode.HasSlottedKeystone and C_ChallengeMode.HasSlottedKeystone() then
        return false, "keystone is already slotted"
    end

    local liveKeyDetails = getLiveCurrentKeyDetails()
    local currentKeyLevel = math.max(0, math.floor(tonumber(liveKeyDetails.keyLevel) or 0))

    if currentKeyLevel <= 0 then
        local seasonProfile = self:GetStoredSeasonProfile(playerName)
        currentKeyLevel = math.max(0, math.floor(seasonProfile and seasonProfile.currentKey and tonumber(seasonProfile.currentKey.level) or 0))
    end

    if currentKeyLevel <= 0 then
        return false, "no current keystone is available"
    end

    local runtime = self:GetRuntimeState().keystoneSlotting
    local bagIndex, slotIndex = findOwnedKeystoneBagSlot()

    if bagIndex ~= nil and slotIndex ~= nil then
        local ok, errorMessage = useContainerItemBySlot(bagIndex, slotIndex)

        if not ok then
            return false, tostring(errorMessage or "failed to use keystone bag item")
        end

        runtime.lastAttemptAt = time()
        runtime.lastAttemptSource = trimString(options.source)
        runtime.lastAttemptMethod = "bag-item"

        return true
    end

    if not (C_ChallengeMode and C_ChallengeMode.SlotKeystone) then
        return false, "challenge mode keystone slotting is unavailable"
    end

    local ok, errorMessage = pcall(C_ChallengeMode.SlotKeystone)

    if not ok then
        return false, tostring(errorMessage or "failed to slot keystone")
    end

    runtime.lastAttemptAt = time()
    runtime.lastAttemptSource = trimString(options.source)
    runtime.lastAttemptMethod = "api"

    return true
end

function addon.MythicPlusService:HandleKeystoneReceptacleOpen()
    local runtime = self:GetRuntimeState().keystoneSlotting
    local now = time()
    local _, _, difficultyId = GetInstanceInfo()

    if InCombatLockdown and InCombatLockdown() then
        runtime.lastAutoSlotReason = "combat"
        return false
    end

    if difficultyId ~= 8 and difficultyId ~= 23 then
        return false
    end

    if (now - (tonumber(runtime.lastAutoSlotAt) or 0)) < AUTO_KEYSTONE_SLOT_COOLDOWN_SECONDS then
        return false
    end

    runtime.lastAutoSlotAt = now

    local ok, reason = self:TrySlotCurrentKeystoneForMember(addon.Store:GetPlayerName(), {
        source = "receptacle",
    })

    runtime.lastAutoSlotReason = ok and "slotted" or tostring(reason or "")
    return ok
end

local function setSeasonCaptureState(seasonProfile, source, attemptCount, reason, successful)
    if type(seasonProfile) ~= "table" then
        return
    end

    seasonProfile.lastCaptureSource = trimString(source) ~= "" and trimString(source) or "live"
    seasonProfile.lastCaptureAttemptAt = time()
    seasonProfile.lastCaptureCount = math.max(0, math.floor(tonumber(attemptCount) or 0))
    seasonProfile.lastCaptureReason = trimString(reason)

    if successful then
        seasonProfile.lastSuccessfulCaptureAt = seasonProfile.lastCaptureAttemptAt
    end
end

local function buildEmptySeasonProfile()
    return {
        currentKey = {
            dungeonId = nil,
            level = 0,
        },
        dungeons = {},
        lastCaptureAttemptAt = 0,
        lastCaptureCount = 0,
        lastCaptureReason = "",
        lastCaptureSource = nil,
        lastSuccessfulCaptureAt = 0,
        overallRating = 0,
        portalThreshold = DEFAULT_PORTAL_THRESHOLD,
        updatedAt = 0,
        updatedBy = nil,
    }
end

local function buildEmptyMythicProfile(toonId)
    return {
        id = toonId,
        owner = toonId,
        seasons = {
            current = buildEmptySeasonProfile(),
        },
        updatedAt = 0,
        updatedBy = nil,
        revision = 0,
    }
end

function addon.MythicPlusService:GetUIState()
    local profile = addon.Store:GetProfile()
    profile.mythics = profile.mythics or {}
    profile.mythics.overlay = type(profile.mythics.overlay) == "table" and profile.mythics.overlay or {}
    profile.mythics.planner = profile.mythics.planner or {}

    if profile.mythics.seasonId == nil or profile.mythics.seasonId == "" then
        profile.mythics.seasonId = DEFAULT_SEASON_ID
    end

    if profile.mythics.activePane ~= "party" and profile.mythics.activePane ~= "season" and profile.mythics.activePane ~= "leaderboard" then
        profile.mythics.activePane = "toons"
    end

    profile.mythics.planner.analysisMode = normalizeAnalysisMode(profile.mythics.planner.analysisMode)
    profile.mythics.planner.carryMilestone = normalizeCarryMilestone(profile.mythics.planner.carryMilestone or DEFAULT_CARRY_MILESTONE)
    profile.mythics.planner.carryKeyGoal = normalizeCarryKeyGoal(profile.mythics.planner.carryKeyGoal or DEFAULT_CARRY_KEY_GOAL)
    profile.mythics.planner.selectedCarryTargetId = addon.Store:NormalizeFullName(profile.mythics.planner.selectedCarryTargetId, GetRealmName() or "Unknown")
    profile.mythics.planner.targetKeyLevel = clampKeyLevel(profile.mythics.planner.targetKeyLevel or DEFAULT_TARGET_KEY_LEVEL)
    if profile.mythics.overlay.enabled == nil then
        profile.mythics.overlay.enabled = true
    else
        profile.mythics.overlay.enabled = profile.mythics.overlay.enabled and true or false
    end
    profile.mythics.overlay.locked = profile.mythics.overlay.locked and true or false
    profile.mythics.overlay.point = trimString(profile.mythics.overlay.point) ~= "" and trimString(profile.mythics.overlay.point) or "TOP"
    profile.mythics.overlay.relativePoint = trimString(profile.mythics.overlay.relativePoint) ~= "" and trimString(profile.mythics.overlay.relativePoint) or profile.mythics.overlay.point
    profile.mythics.overlay.x = tonumber(profile.mythics.overlay.x) or 0
    profile.mythics.overlay.y = tonumber(profile.mythics.overlay.y) or -130
    profile.mythics.overlay.activeRun = type(profile.mythics.overlay.activeRun) == "table" and profile.mythics.overlay.activeRun or {}
    profile.mythics.overlay.liveForces = type(profile.mythics.overlay.liveForces) == "table" and profile.mythics.overlay.liveForces or {}
    return profile.mythics
end

function addon.MythicPlusService:GetRuntimeState()
    self._runtimeState = self._runtimeState or {}
    self._runtimeState.keystoneSlotting = self._runtimeState.keystoneSlotting or {
        lastAttemptAt = 0,
        lastAttemptSource = nil,
        lastAutoSlotAt = 0,
        lastAutoSlotReason = nil,
    }
    self._runtimeState.libKeystone = self._runtimeState.libKeystone or {
        lastInboundAt = 0,
        lastInboundSender = nil,
        lastInboundType = nil,
        lastRequestAt = 0,
        lastRequestChannel = nil,
        lastResponseAt = 0,
        lastResponseChannel = nil,
        partyKeys = {},
    }
    self._runtimeState.openRaid = self._runtimeState.openRaid or {
        lastFailureReason = nil,
        lastInboundAt = 0,
        lastInboundSender = nil,
        lastInboundType = nil,
        lastRequestAt = 0,
        lastRequestChannel = nil,
        lastResponseAt = 0,
        lastResponseChannel = nil,
        partyKeys = {},
    }
    self._runtimeState.partyItemLevels = self._runtimeState.partyItemLevels or {
        cache = {},
        inspectFrame = nil,
        pendingFullName = nil,
        pendingGuid = nil,
        pendingRequestedAt = 0,
        pendingUnitId = nil,
    }
    self._runtimeState.partyMembers = self._runtimeState.partyMembers or {
        members = nil,
        updatedAt = 0,
    }
    self._runtimeState.overlay = self._runtimeState.overlay or {
        elapsedSinceRefresh = 0,
        eventFrame = nil,
        frame = nil,
        lastActive = false,
        lastSnapshot = nil,
        liveStateCache = nil,
        liveStateUpdatedAt = 0,
        previewSnapshot = nil,
        previewTrackerReadings = nil,
        startedAt = 0,
        trackerReadings = nil,
        trackerReadingsAt = 0,
    }
    self._runtimeState.summary = self._runtimeState.summary or {
        active = false,
        completedAt = 0,
        dismissedToken = 0,
        isPreview = false,
        loot = {},
        lootSeen = {},
        run = nil,
        summaryFrame = nil,
        token = 0,
    }
    self._runtimeState.keystoneSlotting.lastAttemptAt = tonumber(self._runtimeState.keystoneSlotting.lastAttemptAt) or 0
    self._runtimeState.keystoneSlotting.lastAutoSlotAt = tonumber(self._runtimeState.keystoneSlotting.lastAutoSlotAt) or 0
    self._runtimeState.libKeystone.partyKeys = type(self._runtimeState.libKeystone.partyKeys) == "table" and self._runtimeState.libKeystone.partyKeys or {}
    self._runtimeState.libKeystone.lastInboundAt = tonumber(self._runtimeState.libKeystone.lastInboundAt) or 0
    self._runtimeState.libKeystone.lastRequestAt = tonumber(self._runtimeState.libKeystone.lastRequestAt) or 0
    self._runtimeState.libKeystone.lastResponseAt = tonumber(self._runtimeState.libKeystone.lastResponseAt) or 0
    self._runtimeState.openRaid.partyKeys = type(self._runtimeState.openRaid.partyKeys) == "table" and self._runtimeState.openRaid.partyKeys or {}
    self._runtimeState.openRaid.lastInboundAt = tonumber(self._runtimeState.openRaid.lastInboundAt) or 0
    self._runtimeState.openRaid.lastRequestAt = tonumber(self._runtimeState.openRaid.lastRequestAt) or 0
    self._runtimeState.openRaid.lastResponseAt = tonumber(self._runtimeState.openRaid.lastResponseAt) or 0
    self._runtimeState.partyItemLevels.cache = type(self._runtimeState.partyItemLevels.cache) == "table" and self._runtimeState.partyItemLevels.cache or {}
    self._runtimeState.partyItemLevels.pendingRequestedAt = tonumber(self._runtimeState.partyItemLevels.pendingRequestedAt) or 0
    self._runtimeState.partyMembers.members = type(self._runtimeState.partyMembers.members) == "table" and self._runtimeState.partyMembers.members or nil
    self._runtimeState.partyMembers.updatedAt = tonumber(self._runtimeState.partyMembers.updatedAt) or 0
    self._runtimeState.overlay.elapsedSinceRefresh = tonumber(self._runtimeState.overlay.elapsedSinceRefresh) or 0
    self._runtimeState.overlay.liveStateCache = type(self._runtimeState.overlay.liveStateCache) == "table" and self._runtimeState.overlay.liveStateCache or nil
    self._runtimeState.overlay.liveStateUpdatedAt = tonumber(self._runtimeState.overlay.liveStateUpdatedAt) or 0
    self._runtimeState.overlay.previewSnapshot = type(self._runtimeState.overlay.previewSnapshot) == "table" and self._runtimeState.overlay.previewSnapshot or nil
    self._runtimeState.overlay.previewTrackerReadings = type(self._runtimeState.overlay.previewTrackerReadings) == "table" and self._runtimeState.overlay.previewTrackerReadings or nil
    self._runtimeState.overlay.startedAt = tonumber(self._runtimeState.overlay.startedAt) or 0
    self._runtimeState.overlay.trackerReadingsAt = tonumber(self._runtimeState.overlay.trackerReadingsAt) or 0
    self._runtimeState.overlay.trackerReadings = type(self._runtimeState.overlay.trackerReadings) == "table" and self._runtimeState.overlay.trackerReadings or nil
    self._runtimeState.summary.completedAt = tonumber(self._runtimeState.summary.completedAt) or 0
    self._runtimeState.summary.dismissedToken = math.max(0, math.floor(tonumber(self._runtimeState.summary.dismissedToken) or 0))
    self._runtimeState.summary.isPreview = self._runtimeState.summary.isPreview and true or false
    self._runtimeState.summary.loot = type(self._runtimeState.summary.loot) == "table" and self._runtimeState.summary.loot or {}
    self._runtimeState.summary.lootSeen = type(self._runtimeState.summary.lootSeen) == "table" and self._runtimeState.summary.lootSeen or {}
    self._runtimeState.summary.token = math.max(0, math.floor(tonumber(self._runtimeState.summary.token) or 0))
    return self._runtimeState
end

function addon.MythicPlusService:GetOverlaySettings()
    return self:GetUIState().overlay
end

function addon.MythicPlusService:IsOverlayEnabled()
    local settings = self:GetOverlaySettings()
    return settings.enabled and true or false
end

function addon.MythicPlusService:SetOverlayEnabled(enabled)
    local settings = self:GetOverlaySettings()
    settings.enabled = enabled and true or false

    if settings.enabled and self.InitializeOverlay then
        self:InitializeOverlay()
    end

    self:RefreshMythicPlusOverlay("settings")
    return settings.enabled
end

function addon.MythicPlusService:ToggleOverlayEnabled()
    return self:SetOverlayEnabled(not self:IsOverlayEnabled())
end

function addon.MythicPlusService:SetOverlayLocked(locked)
    local settings = self:GetOverlaySettings()
    settings.locked = locked and true or false
    local runtime = self:GetRuntimeState().overlay

    if runtime.frame then
        runtime.frame:EnableMouse(not settings.locked)
    end

    return settings.locked
end

function addon.MythicPlusService:ResetOverlayPosition()
    local settings = self:GetOverlaySettings()
    settings.point = "TOP"
    settings.relativePoint = "TOP"
    settings.x = 0
    settings.y = -130

    local runtime = self:GetRuntimeState().overlay

    if runtime.frame then
        runtime.frame:ClearAllPoints()
        runtime.frame:SetPoint(settings.point, UIParent, settings.relativePoint, settings.x, settings.y)
    end

    return true
end

local function getOverlayAffixNames()
    local names = {}
    local seen = {}
    local _, activeAffixIDs = safeInvoke(C_ChallengeMode, "GetActiveKeystoneInfo")

    local function addAffixName(affixValue)
        local affixId = tonumber(affixValue)
        local name = ""

        if type(affixValue) == "table" then
            name = trimString(affixValue.name or affixValue.affixName or affixValue.keystoneAffixName)
            affixId = tonumber(affixValue.id or affixValue.affixID or affixValue.affixId) or affixId
        end

        if name == "" and affixId and affixId > 0 then
            name = trimString(safeInvoke(C_ChallengeMode, "GetAffixInfo", affixId))

            if name == "" then
                name = trimString(safeInvoke(C_MythicPlus, "GetAffixInfo", affixId))
            end
        end

        if name ~= "" and not seen[lowerText(name)] then
            seen[lowerText(name)] = true
            names[#names + 1] = name
        end
    end

    if type(activeAffixIDs) == "table" then
        for _, affixId in ipairs(activeAffixIDs) do
            addAffixName(affixId)
        end
    end

    if #names == 0 then
        local currentAffixes = safeInvoke(C_MythicPlus, "GetCurrentAffixes")

        if type(currentAffixes) == "table" then
            for _, affixInfo in ipairs(currentAffixes) do
                addAffixName(affixInfo)
            end
        end
    end

    if #names == 0 then
        return "Affixes: Loading"
    end

    return "Affixes: " .. table.concat(names, ", ")
end

local function getOverlayScenarioCriteria()
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope("mythics:overlay-scenario-criteria") or nil
    local rows = {}
    local forcesRow = nil
    local objectiveTotal = 0
    local objectiveDone = 0

    local function getCriteriaCount()
        if type(C_ScenarioInfo) == "table" and type(C_ScenarioInfo.GetStepInfo) == "function" then
            local info = safeInvoke(C_ScenarioInfo, "GetStepInfo")

            if type(info) == "table" then
                return math.max(0, math.floor(tonumber(info.numCriteria) or 0))
            end
        end

        if type(C_Scenario) == "table" and type(C_Scenario.GetStepInfo) == "function" then
            local _, _, numCriteria = safeInvoke(C_Scenario, "GetStepInfo")
            return math.max(0, math.floor(tonumber(numCriteria) or 0))
        end

        return 0
    end

    local function mergeCriteriaInfo(primary, fallback)
        if type(primary) ~= "table" then
            return fallback
        end

        if type(fallback) ~= "table" then
            return primary
        end

        local primaryCurrent = tonumber(primary.current) or 0
        local primaryTotal = tonumber(primary.total) or 0
        local fallbackCurrent = tonumber(fallback.current) or 0
        local fallbackTotal = tonumber(fallback.total) or 0

        if fallbackTotal > 0 and (primaryTotal <= 0 or fallbackCurrent > primaryCurrent) then
            primary.current = fallback.current
            primary.total = fallback.total
        end

        local primaryDisplayPercent = extractDisplayPercent(primary.quantityLabel)
        local fallbackDisplayPercent = extractDisplayPercent(fallback.quantityLabel)

        if fallbackDisplayPercent ~= nil and (primaryDisplayPercent == nil or fallbackDisplayPercent > primaryDisplayPercent) then
            primary.quantityLabel = fallback.quantityLabel
        end

        if trimString(primary.label) == "" and trimString(fallback.label) ~= "" then
            primary.label = fallback.label
        end

        primary.completed = primary.completed or fallback.completed
        primary.failed = primary.failed or fallback.failed
        primary.isWeightedProgress = primary.isWeightedProgress or fallback.isWeightedProgress
        return primary
    end

    local function readCriteriaInfo(index)
        local scenarioInfo = nil

        if type(C_ScenarioInfo) == "table" and type(C_ScenarioInfo.GetCriteriaInfo) == "function" then
            local info = safeInvoke(C_ScenarioInfo, "GetCriteriaInfo", index)

            if type(info) == "table" then
                scenarioInfo = {
                    completed = info.completed,
                    current = info.quantity,
                    failed = info.failed,
                    isWeightedProgress = info.isWeightedProgress,
                    label = info.description or info.criteriaString or info.name,
                    quantityLabel = info.quantityString,
                    total = info.totalQuantity,
                }

                return scenarioInfo
            end
        end

        if type(C_Scenario) == "table" and type(C_Scenario.GetCriteriaInfo) == "function" then
            local criteriaString, _, completed, quantity, totalQuantity, _, _, quantityString, _, _, _, criteriaFailed, isWeightedProgress = safeInvoke(C_Scenario, "GetCriteriaInfo", index)

            if criteriaString ~= nil then
                return {
                    completed = completed,
                    current = quantity,
                    failed = criteriaFailed,
                    isWeightedProgress = isWeightedProgress,
                    label = criteriaString,
                    quantityLabel = quantityString,
                    total = totalQuantity,
                }
            end
        end

        return scenarioInfo
    end

    for index = 1, getCriteriaCount() do
        local criteriaInfo = readCriteriaInfo(index)
        local name = trimString(criteriaInfo and criteriaInfo.label or "")

        if name ~= "" then
            local current = tonumber(criteriaInfo.current) or 0
            local total = tonumber(criteriaInfo.total) or 0
            local quantityLabel = trimString(criteriaInfo.quantityLabel)
            local lowerName = lowerText(name)
            local done = criteriaInfo.completed or (total > 0 and current >= total) or false
            local isNamedForces = lowerName == "enemy forces" or lowerName == "forces" or lowerName:find("enemy forces", 1, true) ~= nil
            local forcesPercent = tonumber(string.match(quantityLabel, "([%d%.]+)%s*%%"))
            local row = {
                completed = done and true or false,
                current = current,
                failed = criteriaInfo.failed and true or false,
                isForces = isNamedForces,
                isTrustedForces = isNamedForces and (forcesPercent ~= nil or total > 0 or criteriaInfo.isWeightedProgress),
                isWeightedProgress = criteriaInfo.isWeightedProgress and true or false,
                label = name,
                quantityLabel = quantityLabel,
                total = total,
            }

            if row.isForces then
                if not forcesRow then
                    forcesRow = row
                elseif forcesRow.completed and not row.completed then
                    forcesRow = row
                elseif not forcesRow.completed and row.completed then
                    -- Keep the live in-progress forces row instead of replacing it with an older completed phase row.
                else
                    -- Prefer the newest forces row and only backfill missing fields from older rows.
                    forcesRow = mergeCriteriaInfo(row, forcesRow)
                end
            else
                objectiveTotal = objectiveTotal + 1

                if row.completed then
                    objectiveDone = objectiveDone + 1
                end

                rows[#rows + 1] = row
            end
        end
    end

    if perfStartedAt ~= nil and addon.EndPerfScope then
        addon:EndPerfScope("mythics:overlay-scenario-criteria", perfStartedAt)
    end

    return forcesRow, rows, objectiveDone, objectiveTotal
end

function getOverlayDeathCount()
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope("mythics:overlay-deaths") or nil
    local deathCount, timeLost = safeInvoke(C_ChallengeMode, "GetDeathCount")

    if deathCount == nil then
        deathCount, timeLost = safeInvoke(C_ChallengeMode, "GetDeathInfo")
    end

    local resolvedDeathCount = math.max(0, math.floor(tonumber(deathCount) or 0))
    local resolvedTimeLost = math.max(0, math.floor(tonumber(timeLost) or 0))

    if perfStartedAt ~= nil and addon.EndPerfScope then
        addon:EndPerfScope("mythics:overlay-deaths", perfStartedAt)
    end

    return resolvedDeathCount, resolvedTimeLost
end

local function getOverlayStartTime(runtime)
    local startTime = tonumber(safeInvoke(C_ChallengeMode, "GetStartTime")) or 0
    local now = getClockSeconds()

    if startTime > 0 then
        if startTime > now + 1000 and (startTime / 1000) <= now + 1000 then
            startTime = startTime / 1000
        end

        runtime.startedAt = startTime
        return startTime
    end

    if (tonumber(runtime.startedAt) or 0) > 0 then
        return runtime.startedAt
    end

    return nil
end

function addon.MythicPlusService:GetMythicPlusOverlaySnapshot(reason)
    local runtime = self:GetRuntimeState().overlay
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope("mythics:get-overlay-snapshot") or nil
    local active = self:IsChallengeModeActive()
    local activeMapId = tonumber(safeInvoke(C_ChallengeMode, "GetActiveChallengeMapID")) or 0
    local activeKeystoneLevel = safeInvoke(C_ChallengeMode, "GetActiveKeystoneInfo")
    local keyLevel = tonumber(activeKeystoneLevel) or 0
    local mapName = activeMapId > 0 and trimString(safeInvoke(C_ChallengeMode, "GetMapUIInfo", activeMapId)) or ""
    local timeLimit = activeMapId > 0 and tonumber(getDungeonTimeLimitSeconds(activeMapId)) or 0
    local now = getClockSeconds()
    local startTime = getOverlayStartTime(runtime)
    local elapsed = (active and startTime and startTime > 0) and math.max(0, now - startTime) or nil
    local remaining = (timeLimit > 0 and elapsed ~= nil) and (timeLimit - elapsed) or nil
    local plusTwoLimit = timeLimit > 0 and (timeLimit * 0.8) or nil
    local plusThreeLimit = timeLimit > 0 and (timeLimit * 0.6) or nil
    local cacheAge = now - (tonumber(runtime.liveStateUpdatedAt) or 0)
    local shouldRefreshLiveState = reason ~= "tick"
        or type(runtime.liveStateCache) ~= "table"
        or cacheAge >= MYTHIC_PLUS_OVERLAY_STATE_REFRESH_INTERVAL_SECONDS
    local liveState = shouldRefreshLiveState and {} or (runtime.liveStateCache or {})

    if shouldRefreshLiveState then
        local forcesRow, objectiveRows, objectiveDone, objectiveTotal = getOverlayScenarioCriteria()
        local deaths, deathTimeLost = getOverlayDeathCount()

        liveState = {
            affixes = getOverlayAffixNames(),
            deathTimeLost = deathTimeLost,
            deaths = deaths,
            forces = forcesRow,
            objectiveDone = objectiveDone,
            objectiveRows = objectiveRows,
            objectiveTotal = objectiveTotal,
        }
        runtime.liveStateCache = liveState
        runtime.liveStateUpdatedAt = now
    end

    if mapName == "" then
        mapName = "Mythic+"
    end

    local snapshot = {
        active = active,
        affixes = liveState.affixes or getOverlayAffixNames(),
        deathTimeLost = tonumber(liveState.deathTimeLost) or 0,
        deaths = tonumber(liveState.deaths) or 0,
        elapsed = elapsed,
        forces = liveState.forces,
        keyLevel = keyLevel,
        mapId = activeMapId,
        mapName = mapName,
        objectiveDone = tonumber(liveState.objectiveDone) or 0,
        objectiveRows = type(liveState.objectiveRows) == "table" and liveState.objectiveRows or {},
        objectiveTotal = tonumber(liveState.objectiveTotal) or 0,
        plusTwoRemaining = (plusTwoLimit and elapsed ~= nil) and (plusTwoLimit - elapsed) or nil,
        plusThreeRemaining = (plusThreeLimit and elapsed ~= nil) and (plusThreeLimit - elapsed) or nil,
        remaining = remaining,
        timeLimit = timeLimit,
    }

    if perfStartedAt ~= nil and addon.EndPerfScope then
        addon:EndPerfScope("mythics:get-overlay-snapshot", perfStartedAt)
    end

    return snapshot
end

local function setOverlayTextColor(fontString, color)
    if fontString and color then
        fontString:SetTextColor(color[1], color[2], color[3], color[4] or 1)
    end
end

local function applyDungeonArt(textureWidget, overlayWidget, mapId, fallbackName)
    if not textureWidget then
        return false
    end

    local texturePath = tryGetDungeonTexture(mapId, fallbackName)

    local function applyTextureSource(widget, source)
        if not widget or source == nil then
            return false
        end

        if type(source) == "string" then
            local looksLikeFilePath = source:find("\\", 1, true) ~= nil
                or source:find("/", 1, true) ~= nil
                or source:find("%.") ~= nil

            if not looksLikeFilePath and type(widget.SetAtlas) == "function" then
                local ok = pcall(widget.SetAtlas, widget, source)

                if ok then
                    return "atlas"
                end
            end
        end

        widget:SetTexture(source)
        return "texture"
    end

    if not texturePath then
        textureWidget:Hide()

        if overlayWidget then
            overlayWidget:Hide()
        end

        return false
    end

    local appliedAs = applyTextureSource(textureWidget, texturePath)

    if not appliedAs then
        textureWidget:Hide()

        if overlayWidget then
            overlayWidget:Hide()
        end

        return false
    end

    if appliedAs == "atlas" then
        textureWidget:SetTexCoord(0, 1, 0, 1)
    else
        textureWidget:SetTexCoord(0.06, 0.94, 0.12, 0.88)
    end

    textureWidget:Show()

    if overlayWidget then
        overlayWidget:Show()
    end

    return true
end

function GH3_MythicPlusHideDungeonArt(frame)
    if not frame then
        return
    end

    for _, key in ipairs({ "dungeonArt", "dungeonArtShade", "dungeonArtLeftShade", "dungeonArtRightShade" }) do
        local widget = frame[key]

        if widget and widget.Hide then
            widget:Hide()
        end
    end
end

local function setDungeonArtShade(textureWidget, topAlpha, bottomAlpha)
    if not textureWidget then
        return
    end

    textureWidget:SetTexture("Interface\\Buttons\\WHITE8X8")

    if type(textureWidget.SetGradientAlpha) == "function" then
        textureWidget:SetGradientAlpha("VERTICAL", 0, 0, 0, topAlpha or 0, 0, 0, 0, bottomAlpha or 0)
        return
    end

    textureWidget:SetColorTexture(0, 0, 0, math.max(topAlpha or 0, bottomAlpha or 0))
end

local function applyBracketChrome(frame, inset, length, color)
    if not frame then
        return
    end

    local chrome = frame._gh3BracketChrome or {}
    frame._gh3BracketChrome = chrome
    local resolvedInset = math.max(0, tonumber(inset) or 0)
    local resolvedLength = math.max(6, tonumber(length) or 14)
    local resolvedColor = color or { 0.78, 0.62, 0.24, 0.65 }

    local function ensureTexture(key)
        if chrome[key] then
            return chrome[key]
        end

        local texture = frame:CreateTexture(nil, "BORDER")
        chrome[key] = texture
        return texture
    end

    local function placeTexture(key, point, xOffset, yOffset, width, height)
        local texture = ensureTexture(key)
        texture:ClearAllPoints()
        texture:SetPoint(point, frame, point, xOffset, yOffset)
        texture:SetSize(width, height)
        texture:SetColorTexture(resolvedColor[1], resolvedColor[2], resolvedColor[3], resolvedColor[4] or 1)
        texture:Show()
    end

    placeTexture("topLeftHorizontal", "TOPLEFT", resolvedInset, -resolvedInset, resolvedLength, 1)
    placeTexture("topLeftVertical", "TOPLEFT", resolvedInset, -resolvedInset, 1, resolvedLength)
    placeTexture("topRightHorizontal", "TOPRIGHT", -resolvedInset, -resolvedInset, resolvedLength, 1)
    placeTexture("topRightVertical", "TOPRIGHT", -resolvedInset, -resolvedInset, 1, resolvedLength)
    placeTexture("bottomLeftHorizontal", "BOTTOMLEFT", resolvedInset, resolvedInset, resolvedLength, 1)
    placeTexture("bottomLeftVertical", "BOTTOMLEFT", resolvedInset, resolvedInset, 1, resolvedLength)
    placeTexture("bottomRightHorizontal", "BOTTOMRIGHT", -resolvedInset, resolvedInset, resolvedLength, 1)
    placeTexture("bottomRightVertical", "BOTTOMRIGHT", -resolvedInset, resolvedInset, 1, resolvedLength)
end

local function applyMythicSurfaceStyle(frame, palette)
    if not frame or type(frame.SetBackdrop) ~= "function" or type(palette) ~= "table" then
        return
    end

    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    frame:SetBackdropColor(unpack(palette.bg or { 0.02, 0.03, 0.05, 0.95 }))
    frame:SetBackdropBorderColor(unpack(palette.border or { 0.62, 0.48, 0.18, 1 }))

    if not frame._gh3Depth then
        local depth = {}
        depth.top = frame:CreateTexture(nil, "BORDER")
        depth.left = frame:CreateTexture(nil, "BORDER")
        depth.bottom = frame:CreateTexture(nil, "BORDER")
        depth.right = frame:CreateTexture(nil, "BORDER")
        depth.inner = frame:CreateTexture(nil, "ARTWORK")
        depth.glow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
        depth.glow:SetFrameLevel(math.max(frame:GetFrameLevel() - 1, 0))
        depth.glow:SetBackdrop({
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
        })
        depth.top:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
        depth.top:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
        depth.top:SetHeight(1)
        depth.left:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
        depth.left:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1)
        depth.left:SetWidth(1)
        depth.bottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1)
        depth.bottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
        depth.bottom:SetHeight(1)
        depth.right:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
        depth.right:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
        depth.right:SetWidth(1)
        depth.inner:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2)
        depth.inner:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2)
        depth.glow:SetPoint("TOPLEFT", frame, "TOPLEFT", -2, 2)
        depth.glow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 2, -2)
        frame._gh3Depth = depth
    end

    frame._gh3Depth.top:SetColorTexture(unpack(palette.highlight or { 0.78, 0.66, 0.30, 0.34 }))
    frame._gh3Depth.left:SetColorTexture(unpack(palette.highlight or { 0.78, 0.66, 0.30, 0.34 }))
    frame._gh3Depth.bottom:SetColorTexture(unpack(palette.shadow or { 0, 0, 0, 0.52 }))
    frame._gh3Depth.right:SetColorTexture(unpack(palette.shadow or { 0, 0, 0, 0.52 }))
    frame._gh3Depth.inner:SetColorTexture(1, 1, 1, 0.025)
    frame._gh3Depth.glow:SetBackdropBorderColor(unpack(palette.glow or palette.border or { 0.62, 0.48, 0.18, 0.5 }))
end

local function createMediaTexture(parent, layer, path, alpha)
    local texture = parent:CreateTexture(nil, layer or "ARTWORK")
    texture:SetTexture(path)
    texture:SetTexCoord(0, 1, 0, 1)
    texture:SetAlpha(alpha or 1)
    return texture
end

local function addMediaBackground(frame, path, inset, alpha)
    if not frame then
        return nil
    end

    local texture = createMediaTexture(frame, "BACKGROUND", path, alpha or 0.92)
    local resolvedInset = tonumber(inset) or 0
    texture:SetPoint("TOPLEFT", frame, "TOPLEFT", resolvedInset, -resolvedInset)
    texture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -resolvedInset, resolvedInset)
    frame._gh3MediaBackground = texture
    return texture
end

local function addMediaHeader(frame, path, height, leftInset, rightInset, topOffset, alpha)
    if not frame then
        return nil
    end

    local texture = createMediaTexture(frame, "BORDER", path, alpha or 0.96)
    texture:SetPoint("TOPLEFT", frame, "TOPLEFT", leftInset or 0, topOffset or 0)
    texture:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -(rightInset or leftInset or 0), topOffset or 0)
    texture:SetHeight(height or 32)
    return texture
end

local function addMediaDivider(frame, path, anchor, relativeTo, relativePoint, x, y, width, alpha)
    if not frame then
        return nil
    end

    local texture = createMediaTexture(frame, "ARTWORK", path, alpha or 0.92)
    texture:SetPoint(anchor, relativeTo or frame, relativePoint or anchor, x or 0, y or 0)
    texture:SetSize(width or 180, 6)
    return texture
end

local function addMediaFrameChrome(frame, cornerSize, edgeSize, alpha)
    if not frame then
        return nil
    end

    local chrome = frame._gh3MediaChrome or {}
    frame._gh3MediaChrome = chrome
    local resolvedCornerSize = math.max(16, tonumber(cornerSize) or 42)
    local resolvedEdgeSize = math.max(4, tonumber(edgeSize) or 10)
    local resolvedAlpha = alpha or 0.94

    local function ensureTexture(key, path)
        if not chrome[key] then
            chrome[key] = createMediaTexture(frame, "BORDER", path, resolvedAlpha)
        end

        chrome[key]:SetTexture(path)
        chrome[key]:SetTexCoord(0, 1, 0, 1)
        chrome[key]:SetAlpha(resolvedAlpha)
        chrome[key]:ClearAllPoints()
        chrome[key]:Show()
        return chrome[key]
    end

    local topLeft = ensureTexture("topLeft", MEDIA_TEXTURES.frameTopLeft)
    topLeft:SetPoint("TOPLEFT", frame, "TOPLEFT", -2, 2)
    topLeft:SetSize(resolvedCornerSize, resolvedCornerSize)

    local topRight = ensureTexture("topRight", MEDIA_TEXTURES.frameTopRight)
    topRight:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 2, 2)
    topRight:SetSize(resolvedCornerSize, resolvedCornerSize)

    local bottomLeft = ensureTexture("bottomLeft", MEDIA_TEXTURES.frameBottomLeft)
    bottomLeft:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", -2, -2)
    bottomLeft:SetSize(resolvedCornerSize, resolvedCornerSize)

    local bottomRight = ensureTexture("bottomRight", MEDIA_TEXTURES.frameBottomRight)
    bottomRight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 2, -2)
    bottomRight:SetSize(resolvedCornerSize, resolvedCornerSize)

    local topEdge = ensureTexture("topEdge", MEDIA_TEXTURES.frameTopEdge)
    topEdge:SetPoint("TOPLEFT", topLeft, "TOPRIGHT", -4, -2)
    topEdge:SetPoint("TOPRIGHT", topRight, "TOPLEFT", 4, -2)
    topEdge:SetHeight(resolvedEdgeSize)

    local bottomEdge = ensureTexture("bottomEdge", MEDIA_TEXTURES.frameBottomEdge)
    bottomEdge:SetPoint("BOTTOMLEFT", bottomLeft, "BOTTOMRIGHT", -4, 2)
    bottomEdge:SetPoint("BOTTOMRIGHT", bottomRight, "BOTTOMLEFT", 4, 2)
    bottomEdge:SetHeight(resolvedEdgeSize)

    local leftEdge = ensureTexture("leftEdge", MEDIA_TEXTURES.frameLeftEdge)
    leftEdge:SetPoint("TOPLEFT", topLeft, "BOTTOMLEFT", 2, 4)
    leftEdge:SetPoint("BOTTOMLEFT", bottomLeft, "TOPLEFT", 2, -4)
    leftEdge:SetWidth(resolvedEdgeSize)

    local rightEdge = ensureTexture("rightEdge", MEDIA_TEXTURES.frameRightEdge)
    rightEdge:SetPoint("TOPRIGHT", topRight, "BOTTOMRIGHT", -2, 4)
    rightEdge:SetPoint("BOTTOMRIGHT", bottomRight, "TOPRIGHT", -2, -4)
    rightEdge:SetWidth(resolvedEdgeSize)

    return chrome
end

function GH3_MythicPlusMakeMediaOnlyFrame(frame)
    if not frame then
        return
    end

    if frame.SetBackdropColor then
        frame:SetBackdropColor(0, 0, 0, 0)
    end

    if frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(0, 0, 0, 0)
    end

    if frame._gh3Depth then
        for _, texture in pairs(frame._gh3Depth) do
            if texture and texture.Hide then
                texture:Hide()
            end
        end
    end
end

function GH3_MythicPlusConfigureSingleLineText(fontString)
    if not fontString then
        return
    end

    if fontString.SetWordWrap then
        fontString:SetWordWrap(false)
    end

    if fontString.SetNonSpaceWrap then
        fontString:SetNonSpaceWrap(false)
    end

    if fontString.SetMaxLines then
        fontString:SetMaxLines(1)
    end
end

function GH3_MythicPlusAbbreviateText(text, maxCharacters)
    local resolvedText = tostring(text or "")
    local limit = math.max(4, tonumber(maxCharacters) or 16)

    if string.len(resolvedText) <= limit then
        return resolvedText
    end

    return string.sub(resolvedText, 1, limit - 3) .. "..."
end

function GH3_MythicPlusFormatObjectiveLine(row)
    if type(row) ~= "table" then
        return ""
    end

    local label = trimString(row.label)

    if label == "" then
        return ""
    end

    local current = tonumber(row.current) or 0
    local total = tonumber(row.total) or 0
    local quantityLabel = trimString(row.quantityLabel)

    if total > 0 then
        return string.format("Objective: %s %d/%d", GH3_MythicPlusAbbreviateText(label, 34), current, total)
    end

    if quantityLabel ~= "" then
        return string.format("Objective: %s %s", GH3_MythicPlusAbbreviateText(label, 34), quantityLabel)
    end

    return "Objective: " .. GH3_MythicPlusAbbreviateText(label, 42)
end

function GH3_MythicPlusFormatObjectiveProgressLine(row)
    local text = GH3_MythicPlusFormatObjectiveLine(row)

    if text == "" then
        return ""
    end

    return text
end

function GH3_MythicPlusFormatNextObjectiveLabel(label)
    local text = trimString(label)

    if text == "" then
        return ""
    end

    text = string.gsub(text, "%s+[Dd]efeated%s*$", "")
    text = string.gsub(text, "^[Dd]efeat%s+", "")
    text = trimString(text)

    return GH3_MythicPlusAbbreviateText(text, 42)
end

function GH3_MythicPlusIsBossObjectiveLabel(label)
    local text = lowerText(label)

    if text == "" then
        return false
    end

    return text:find("defeat", 1, true) ~= nil
        or text:find("defeated", 1, true) ~= nil
        or text:find("kill", 1, true) ~= nil
        or text:find("slay", 1, true) ~= nil
        or text:find("boss", 1, true) ~= nil
end

function GH3_MythicPlusIsUtilityObjectiveRow(row)
    if type(row) ~= "table" or row.completed then
        return false
    end

    local label = trimString(row.label)

    if label == "" or GH3_MythicPlusIsBossObjectiveLabel(label) then
        return false
    end

    local lowerLabel = lowerText(label)
    local total = tonumber(row.total) or 0
    local quantityLabel = trimString(row.quantityLabel)

    return total > 1
        or quantityLabel ~= ""
        or lowerLabel:find("camp", 1, true) ~= nil
        or lowerLabel:find("liberat", 1, true) ~= nil
        or lowerLabel:find("prison", 1, true) ~= nil
        or lowerLabel:find("free", 1, true) ~= nil
        or lowerLabel:find("rescue", 1, true) ~= nil
        or lowerLabel:find("captive", 1, true) ~= nil
        or lowerLabel:find("hostage", 1, true) ~= nil
end

function GH3_MythicPlusGetUtilityObjectiveProgress(rows)
    for _, row in ipairs(rows or {}) do
        if GH3_MythicPlusIsUtilityObjectiveRow(row) then
            return GH3_MythicPlusFormatObjectiveProgressLine(row), row
        end
    end

    return "", nil
end

function GH3_MythicPlusGetBossObjectiveProgress(rows)
    local done = 0
    local total = 0

    for _, row in ipairs(rows or {}) do
        if type(row) == "table" and GH3_MythicPlusIsBossObjectiveLabel(row.label) then
            total = total + 1

            if row.completed then
                done = done + 1
            end
        end
    end

    return done, total
end

function GH3_MythicPlusHasUtilityObjective(rows)
    for _, row in ipairs(rows or {}) do
        if GH3_MythicPlusIsUtilityObjectiveRow(row) then
            return true
        end
    end

    return false
end

function GH3_MythicPlusShouldSuppressForces(snapshot)
    return false
end

local function getClassColor(classFile)
    local color = RAID_CLASS_COLORS and RAID_CLASS_COLORS[tostring(classFile or "")]

    if color then
        return color.r, color.g, color.b
    end

    return 0.74, 0.79, 0.88
end

local function findGroupUnitIdByFullName(fullName)
    for _, unitId in ipairs(buildGroupUnits()) do
        if getUnitFullName(unitId) == fullName then
            return unitId
        end
    end

    return nil
end

local function ensureSummaryAvatar(parent)
    if parent.avatar then
        return parent.avatar
    end

    local avatar = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    avatar:SetSize(22, 22)
    avatar:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    avatar:SetBackdropColor(0.02, 0.03, 0.04, 0.88)
    avatar:SetBackdropBorderColor(0.22, 0.26, 0.34, 0.72)
    GH3_MythicPlusMakeMediaOnlyFrame(avatar)

    avatar.portrait = avatar:CreateTexture(nil, "ARTWORK")
    avatar.portrait:SetPoint("TOPLEFT", avatar, "TOPLEFT", 1, -1)
    avatar.portrait:SetPoint("BOTTOMRIGHT", avatar, "BOTTOMRIGHT", -1, 1)
    avatar.portrait:SetTexture("Interface\\Buttons\\WHITE8X8")
    avatar.portrait:SetTexCoord(0.07, 0.93, 0.07, 0.93)

    avatar.fallbackText = avatar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    avatar.fallbackText:SetAllPoints(avatar)
    avatar.fallbackText:SetJustifyH("CENTER")
    avatar.fallbackText:SetJustifyV("MIDDLE")
    avatar.fallbackText:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")

    parent.avatar = avatar
    return avatar
end

local function setSummaryAvatar(avatar, fullName, classFile)
    if not avatar then
        return
    end

    local resolvedClassFile = trimString(classFile)
    local unitId = type(fullName) == "string" and findGroupUnitIdByFullName(fullName) or nil
    local colorR, colorG, colorB = getClassColor(resolvedClassFile)

    if avatar.SetBackdropBorderColor then
        avatar:SetBackdropBorderColor(0, 0, 0, 0)
    end

    if unitId and type(SetPortraitTexture) == "function" then
        SetPortraitTexture(avatar.portrait, unitId)
        avatar.portrait:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        avatar.portrait:SetVertexColor(1, 1, 1, 0.95)
        avatar.fallbackText:SetText("")
        return
    end

    if resolvedClassFile ~= "" and type(CLASS_ICON_TCOORDS) == "table" and CLASS_ICON_TCOORDS[resolvedClassFile] then
        local coords = CLASS_ICON_TCOORDS[resolvedClassFile]
        avatar.portrait:SetTexture(CLASS_ICON_TEXTURE)
        avatar.portrait:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
        avatar.portrait:SetVertexColor(1, 1, 1, 0.92)
        avatar.fallbackText:SetText("")
        return
    end

    avatar.portrait:SetTexture("Interface\\Buttons\\WHITE8X8")
    avatar.portrait:SetTexCoord(0, 1, 0, 1)
    avatar.portrait:SetVertexColor(colorR, colorG, colorB, 0.35)
    avatar.fallbackText:SetText(string.upper(string.sub(addon.Store:SplitFullName(fullName or "?", GetRealmName() or "Unknown"), 1, 1)))
end

local function buildRunClassLookup(run)
    local classes = {}

    for _, snapshot in ipairs({ run and run.scoresStart or nil, run and run.scoresEnd or nil }) do
        for fullName, classFile in pairs(type(snapshot and snapshot.classes) == "table" and snapshot.classes or {}) do
            if trimString(classFile) ~= "" then
                classes[fullName] = trimString(classFile)
            end
        end
    end

    for fullName, classFile in pairs(type(run and run.playerClasses) == "table" and run.playerClasses or {}) do
        if trimString(classFile) ~= "" then
            classes[fullName] = trimString(classFile)
        end
    end

    return classes
end

local function setBlizzardMythicTimerSuppressed(suppressed)
    if type(_G) ~= "table" then
        return
    end

    local candidates = {
        _G.ObjectiveTrackerFrame,
        _G.ScenarioObjectiveTrackerBlock,
        _G.ScenarioChallengeModeBlock,
    }

    for _, frame in ipairs(candidates) do
        if frame and type(frame.SetAlpha) == "function" then
            if suppressed then
                if frame.__gh3OriginalAlpha == nil and type(frame.GetAlpha) == "function" then
                    frame.__gh3OriginalAlpha = frame:GetAlpha()
                end

                frame:SetAlpha(0)
            else
                local originalAlpha = frame.__gh3OriginalAlpha
                frame:SetAlpha(type(originalAlpha) == "number" and originalAlpha or 1)
            end
        end
    end
end

local function setBlizzardMythicSummarySuppressed(suppressed)
    if type(_G) ~= "table" then
        return
    end

    local candidates = {
        _G.UIWidgetTopCenterContainerFrame,
        _G.ChallengeModeCompleteBanner,
    }

    for _, frame in ipairs(candidates) do
        if frame and type(frame.SetAlpha) == "function" then
            if suppressed then
                if frame.__gh3OriginalAlpha == nil and type(frame.GetAlpha) == "function" then
                    frame.__gh3OriginalAlpha = frame:GetAlpha()
                end

                frame:SetAlpha(0)
            else
                local originalAlpha = frame.__gh3OriginalAlpha
                frame:SetAlpha(type(originalAlpha) == "number" and originalAlpha or 1)
            end
        end
    end
end

local function createOverlayStatusBar(parent, width, height)
    local bar = CreateFrame("Frame", nil, parent)
    bar:SetSize(width, height)
    bar._gh3MinValue = 0
    bar._gh3MaxValue = 1
    bar._gh3Value = 0

    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(bar)
    bg:SetColorTexture(0.03, 0.04, 0.06, 0.82)
    bar.bg = bg

    local fillClip = CreateFrame("Frame", nil, bar)
    fillClip:SetPoint("TOPLEFT", bar, "TOPLEFT", 1, -1)
    fillClip:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", 1, 1)
    fillClip:SetHeight(math.max(1, height - 2))
    fillClip:SetWidth(1)
    if fillClip.SetClipsChildren then
        fillClip:SetClipsChildren(true)
    end
    bar.fillClip = fillClip

    local fill = fillClip:CreateTexture(nil, "ARTWORK")
    fill:SetPoint("TOPLEFT", fillClip, "TOPLEFT", 0, 0)
    fill:SetPoint("BOTTOMLEFT", fillClip, "BOTTOMLEFT", 0, 0)
    fill:SetWidth(math.max(1, width - 2))
    fill:SetTexture(MEDIA_TEXTURES.fillBlue)
    fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillBlue))
    fill:SetVertexColor(1, 1, 1, 0.94)
    bar.fill = fill

    local bevel = bar:CreateTexture(nil, "OVERLAY")
    bevel:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
    bevel:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 0, 0)
    bevel:SetHeight(math.max(2, math.floor(height / 2)))
    bevel:SetColorTexture(1, 1, 1, 0.07)
    bar.bevel = bevel

    local border = CreateFrame("Frame", nil, bar, "BackdropTemplate")
    border:SetPoint("TOPLEFT", bar, "TOPLEFT", -1, 1)
    border:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 1, -1)
    border:SetBackdrop({
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    border:SetBackdropBorderColor(0.16, 0.18, 0.24, 0.90)
    bar.border = border

    function bar:SetMinMaxValues(minValue, maxValue)
        self._gh3MinValue = tonumber(minValue) or 0
        self._gh3MaxValue = tonumber(maxValue) or 1
        self:SetValue(self._gh3Value or self._gh3MinValue)
    end

    function bar:SetStatusBarTexture(texturePath)
        if self.fill then
            self.fill:SetTexture(texturePath)
            if texturePath == MEDIA_TEXTURES.fillBlue then
                self.fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillBlue))
            elseif texturePath == MEDIA_TEXTURES.fillGreen then
                self.fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillGreen))
            elseif texturePath == MEDIA_TEXTURES.fillGold then
                self.fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillGold))
            elseif texturePath == MEDIA_TEXTURES.fillPurple then
                self.fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillPurple))
            else
                self.fill:SetTexCoord(unpack(MEDIA_TEXCOORDS.fillDefault))
            end
        end
    end

    function bar:SetStatusBarColor(red, green, blue, alpha)
        if self.fill then
            self.fill:SetVertexColor(red or 1, green or 1, blue or 1, alpha or 1)
        end
    end

    function bar:SetValue(value)
        self._gh3Value = tonumber(value) or self._gh3MinValue or 0
        local minValue = tonumber(self._gh3MinValue) or 0
        local maxValue = tonumber(self._gh3MaxValue) or 1
        local range = math.max(maxValue - minValue, 0.0001)
        local progress = math.min(math.max((self._gh3Value - minValue) / range, 0), 1)
        local barWidth = type(self.GetWidth) == "function" and self:GetWidth() or width

        if self.fillClip then
            self.fillClip:SetWidth(math.max(1, math.floor((barWidth - 2) * progress)))
        end

        if self.fill then
            self.fill:SetWidth(math.max(1, math.floor((barWidth - 2) * progress)))

            if self.fillClip and self.fillClip.SetClipsChildren then
                self.fill:SetWidth(math.max(1, barWidth - 2))
            end
        end
    end

    bar:SetValue(0)

    return bar
end

function GH3_MythicPlusPreviewApplyRect(frame, parent, rect)
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", parent, "TOPLEFT", tonumber(rect.x) or 0, tonumber(rect.y) or 0)
    frame:SetSize(tonumber(rect.w) or 1, tonumber(rect.h) or 1)
end

function GH3_MythicPlusPreviewResolveTexturePath(textureKey)
    local textures = addon.NewUIAssets and addon.NewUIAssets.Textures or nil
    return textures and textures[textureKey] or nil
end

function GH3_MythicPlusPreviewResolveMetrics(textureKey)
    local metrics = addon.NewUIAssets and addon.NewUIAssets.Metrics or nil
    return metrics and metrics[textureKey] or nil
end

function GH3_MythicPlusPreviewApplyArtTexture(texture, textureKey, fallbackColor)
    local texturePath = GH3_MythicPlusPreviewResolveTexturePath(textureKey)
    local metrics = GH3_MythicPlusPreviewResolveMetrics(textureKey)

    if texturePath then
        texture:SetTexture(texturePath)
        texture:SetVertexColor(1, 1, 1, 0.98)

        if metrics and type(metrics.texCoord) == "table" and #metrics.texCoord == 4 then
            texture:SetTexCoord(metrics.texCoord[1], metrics.texCoord[3], metrics.texCoord[2], metrics.texCoord[4])
        else
            texture:SetTexCoord(0, 1, 0, 1)
        end

        return
    end

    texture:SetColorTexture(unpack(fallbackColor or { 0.10, 0.10, 0.12, 0.95 }))
end

function GH3_MythicPlusCreatePreviewSurface(parent, rect, textureKey)
    local panel = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    GH3_MythicPlusPreviewApplyRect(panel, parent, rect)

    local background = panel:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(panel)
    GH3_MythicPlusPreviewApplyArtTexture(background, textureKey or "MyToonsRowBackground", { 0.08, 0.09, 0.12, 0.96 })
    panel.Background = background
    return panel
end

function GH3_MythicPlusCreatePreviewTextureButton(parent, rect, textureKey, hoverTextureKey, onClick)
    local button = CreateFrame("Button", nil, parent)
    GH3_MythicPlusPreviewApplyRect(button, parent, rect)

    local texture = button:CreateTexture(nil, "ARTWORK")
    texture:SetAllPoints(button)
    GH3_MythicPlusPreviewApplyArtTexture(texture, textureKey, { 1, 1, 1, 1 })

    button:SetScript("OnEnter", function()
        GH3_MythicPlusPreviewApplyArtTexture(texture, hoverTextureKey or textureKey, { 1, 1, 1, 1 })
    end)
    button:SetScript("OnLeave", function()
        GH3_MythicPlusPreviewApplyArtTexture(texture, textureKey, { 1, 1, 1, 1 })
    end)
    button:SetScript("OnClick", function()
        if onClick then
            onClick(button)
        end
    end)

    return button
end

function GH3_MythicPlusCreatePreviewCard(parent, rect, labelText, valueText, accentColor, textureKey, iconTexturePath)
    local card = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    GH3_MythicPlusPreviewApplyRect(card, parent, rect)

    local tone = accentColor or { 0.98, 0.90, 0.64, 1 }
    local hasLabel = trimString(labelText) ~= ""
    local fill = card:CreateTexture(nil, "BACKGROUND")
    fill:SetAllPoints(card)
    fill:SetColorTexture(tone[1] * 0.12, tone[2] * 0.12, tone[3] * 0.12, 0.70)

    local tint = card:CreateTexture(nil, "ARTWORK")
    tint:SetAllPoints(card)
    GH3_MythicPlusPreviewApplyArtTexture(tint, textureKey or "MyToonsCardBackground", { 0.08, 0.09, 0.12, 0.96 })
    tint:SetVertexColor(1, 1, 1, 0.20)

    local frame = card:CreateTexture(nil, "OVERLAY")
    frame:SetAllPoints(card)
    GH3_MythicPlusPreviewApplyArtTexture(frame, textureKey or "MyToonsCardBackground", { 1, 1, 1, 1 })
    card.Fill = fill
    card.Tint = tint
    card.FrameTexture = frame

    local headerRule = card:CreateTexture(nil, "OVERLAY")
    headerRule:SetPoint("TOPLEFT", card, "TOPLEFT", 18, -32)
    headerRule:SetPoint("TOPRIGHT", card, "TOPRIGHT", -46, -32)
    headerRule:SetHeight(1)
    headerRule:SetColorTexture(tone[1], tone[2], tone[3], 0.40)

    local label = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("TOP", card, "TOP", 0, -14)
    label:SetWidth((tonumber(rect.w) or 1) - 58)
    label:SetJustifyH("CENTER")
    label:SetFont("Fonts\\FRIZQT__.TTF", 13, "")
    label:SetText(labelText or "")
    label:SetTextColor(tone[1], tone[2], tone[3], 1)

    if not hasLabel then
        label:Hide()
        headerRule:Hide()
    end

    if trimString(iconTexturePath or "") ~= "" then
        local icon = card:CreateTexture(nil, "OVERLAY")
        icon:SetTexture(iconTexturePath)
        icon:SetTexCoord(0.16, 0.84, 0.16, 0.84)
        icon:SetVertexColor(tone[1], tone[2], tone[3], 1)
        icon:SetPoint("TOPRIGHT", card, "TOPRIGHT", -14, -10)
        icon:SetSize(20, 20)
        card.Icon = icon
    end

    local value = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    if hasLabel then
        value:SetPoint("TOPLEFT", card, "TOPLEFT", 12, -34)
        value:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -12, 12)
    else
        value:SetPoint("CENTER", card, "CENTER", 0, -1)
    end
    value:SetWidth((tonumber(rect.w) or 1) - (hasLabel and 28 or 16))
    value:SetJustifyH("CENTER")
    value:SetJustifyV("MIDDLE")
    value:SetFont("Fonts\\FRIZQT__.TTF", 16, "")
    value:SetText(valueText or "-")
    value:SetTextColor(unpack(tone))

    card.Label = label
    card.Value = value
    card.HeaderRule = headerRule
    return card
end

function GH3_MythicPlusDecoratePreviewDialog(dialog)
    dialog._gh3Preview = dialog._gh3Preview or {}

    local function ensureTexture(key, layer, textureKey)
        if not dialog._gh3Preview[key] then
            dialog._gh3Preview[key] = dialog:CreateTexture(nil, layer or "ARTWORK")
        end

        local texture = dialog._gh3Preview[key]
        texture:ClearAllPoints()
        GH3_MythicPlusPreviewApplyArtTexture(texture, textureKey, { 0.08, 0.09, 0.12, 0.96 })
        texture:SetVertexColor(1, 1, 1, 0.98)
        texture:Show()
        return texture
    end

    ensureTexture("background", "BACKGROUND", "ShellFrameCenter"):SetAllPoints(dialog)
    ensureTexture("top", "OVERLAY", "ShellFrameTopEdge"):SetPoint("TOPLEFT", dialog, "TOPLEFT", 70, 0)
    dialog._gh3Preview.top:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -70, 0)
    dialog._gh3Preview.top:SetHeight(24)
    ensureTexture("bottom", "OVERLAY", "ShellFrameBottomEdge"):SetPoint("BOTTOMLEFT", dialog, "BOTTOMLEFT", 70, 0)
    dialog._gh3Preview.bottom:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -70, 0)
    dialog._gh3Preview.bottom:SetHeight(24)
    ensureTexture("left", "OVERLAY", "ShellFrameLeftEdge"):SetPoint("TOPLEFT", dialog, "TOPLEFT", 0, -70)
    dialog._gh3Preview.left:SetPoint("BOTTOMLEFT", dialog, "BOTTOMLEFT", 0, 70)
    dialog._gh3Preview.left:SetWidth(24)
    ensureTexture("right", "OVERLAY", "ShellFrameRightEdge"):SetPoint("TOPRIGHT", dialog, "TOPRIGHT", 0, -70)
    dialog._gh3Preview.right:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", 0, 70)
    dialog._gh3Preview.right:SetWidth(24)
    ensureTexture("topLeft", "OVERLAY", "ShellFrameTopLeft"):SetPoint("TOPLEFT", dialog, "TOPLEFT", 0, 0)
    dialog._gh3Preview.topLeft:SetSize(76, 76)
    ensureTexture("topRight", "OVERLAY", "ShellFrameTopRight"):SetPoint("TOPRIGHT", dialog, "TOPRIGHT", 0, 0)
    dialog._gh3Preview.topRight:SetSize(76, 76)
    ensureTexture("bottomLeft", "OVERLAY", "ShellFrameBottomLeft"):SetPoint("BOTTOMLEFT", dialog, "BOTTOMLEFT", 0, 0)
    dialog._gh3Preview.bottomLeft:SetSize(76, 76)
    ensureTexture("bottomRight", "OVERLAY", "ShellFrameBottomRight"):SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", 0, 0)
    dialog._gh3Preview.bottomRight:SetSize(76, 76)
end

local function applyMythicPreviewDialogTransparency(dialog, surfaceKey)
    if not dialog then
        return
    end

    local alpha = 1

    if addon.NewUISettings and addon.NewUISettings.GetConfiguredTransparencyAlpha then
        alpha = math.max(0.60, math.min(1, tonumber(addon.NewUISettings:GetConfiguredTransparencyAlpha(surfaceKey)) or 1))
    end

    local function applySurface(surface)
        if surface and surface.Background and surface.Background.SetAlpha then
            surface.Background:SetAlpha(alpha)
        end
    end

    local function applyCard(card)
        if not card then
            return
        end

        if card.Fill and card.Fill.SetAlpha then
            card.Fill:SetAlpha(0.70 * alpha)
        end

        if card.Tint and card.Tint.SetAlpha then
            card.Tint:SetAlpha(0.20 * alpha)
        end

        if card.FrameTexture and card.FrameTexture.SetAlpha then
            card.FrameTexture:SetAlpha(alpha)
        end
    end

    for _, texture in pairs(dialog._gh3Preview or {}) do
        if texture and texture.SetAlpha then
            texture:SetAlpha(alpha)
        end
    end

    applySurface(dialog.timerPanel)
    applySurface(dialog.infoPanel)
    applySurface(dialog.scorePanel)
    applySurface(dialog.lootPanel)
    applyCard(dialog.keyBadge)
    applyCard(dialog.resultCard)
    applyCard(dialog.timerCard)
    applyCard(dialog.deathsCard)
    applyCard(dialog.keyCard)
end

function GH3_MythicPlusCreatePreviewProgressBar(parent, width, height, fillColor)
    local bar = CreateFrame("Frame", nil, parent)
    bar:SetSize(width, height)
    bar._gh3FillColor = fillColor or { 0.12, 0.40, 0.92, 0.96 }
    bar._gh3Value = 0

    bar.track = bar:CreateTexture(nil, "BACKGROUND")
    bar.track:SetAllPoints(bar)
    bar.track:SetColorTexture(0.10, 0.12, 0.16, 0.94)

    bar.fill = bar:CreateTexture(nil, "ARTWORK")
    bar.fill:SetPoint("TOPLEFT", bar, "TOPLEFT", 3, -3)
    bar.fill:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", 3, 3)

    function bar:SetStatusBarColor(red, green, blue, alpha)
        self._gh3FillColor = { red or 1, green or 1, blue or 1, alpha or 0.96 }
        self.fill:SetColorTexture(unpack(self._gh3FillColor))
    end

    function bar:SetStatusBarTexture(texturePath)
        if texturePath == MEDIA_TEXTURES.fillGold then
            self:SetStatusBarColor(0.82, 0.64, 0.14, 0.96)
        elseif texturePath == MEDIA_TEXTURES.fillGreen then
            self:SetStatusBarColor(0.20, 0.72, 0.18, 0.96)
        else
            self:SetStatusBarColor(0.12, 0.40, 0.92, 0.96)
        end
    end

    function bar:SetValue(value)
        self._gh3Value = math.max(0, math.min(1, tonumber(value) or 0))
        local fillWidth = math.max(6, math.floor((self:GetWidth() - 6) * self._gh3Value))
        self.fill:SetWidth(fillWidth)
    end

    bar:SetStatusBarColor(unpack(bar._gh3FillColor))
    bar:SetValue(0)
    return bar
end

function GH3_MythicPlusLayoutPreviewOverlayFrame(frame)
    local titleWidth = math.ceil(frame.title and (frame.title:GetStringWidth() or 0) or 0)
    local dialogWidth = math.max(356, 66 + titleWidth + 14 + 68 + 10 + 44 + 18)
    local panelWidth = dialogWidth - 40

    frame:SetSize(dialogWidth, MYTHIC_PLUS_OVERLAY_HEIGHT)
    frame.keyBadge:ClearAllPoints()
    frame.keyBadge:SetPoint("LEFT", frame.title, "RIGHT", 14, -1)
    frame.closeButton:ClearAllPoints()
    frame.closeButton:SetPoint("LEFT", frame.keyBadge, "RIGHT", 10, 0)

    GH3_MythicPlusPreviewApplyRect(frame.timerPanel, frame, { x = 20, y = -68, w = panelWidth, h = 80 })
    GH3_MythicPlusPreviewApplyRect(frame.infoPanel, frame, { x = 20, y = -154, w = panelWidth, h = 130 })
    frame.timerBar:SetSize(panelWidth - 32, 18)
    frame.forcesBar:SetSize(panelWidth - 32, 18)
    frame.objective:SetWidth(panelWidth - 36)
    frame.threshold:SetWidth(panelWidth - 32)
end

function GH3_MythicPlusBuildPreviewOverlayFrame(service)
    local settings = service:GetOverlaySettings()
    local frame = CreateFrame("Frame", "GH3MythicPlusOverlayFrame", UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    frame:SetSize(MYTHIC_PLUS_OVERLAY_WIDTH, MYTHIC_PLUS_OVERLAY_HEIGHT)
    frame:SetFrameStrata("HIGH")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(not settings.locked)
    frame:RegisterForDrag("LeftButton")
    GH3_MythicPlusDecoratePreviewDialog(frame)
    frame:ClearAllPoints()
    frame:SetPoint(settings.point, UIParent, settings.relativePoint, settings.x, settings.y)

    frame:SetScript("OnDragStart", function(control)
        if not service:GetOverlaySettings().locked then
            control:StartMoving()
        end
    end)
    frame:SetScript("OnDragStop", function(control)
        control:StopMovingOrSizing()
        local overlaySettings = service:GetOverlaySettings()
        local point, _, relativePoint, x, y = control:GetPoint(1)
        overlaySettings.point = point or "TOP"
        overlaySettings.relativePoint = relativePoint or overlaySettings.point
        overlaySettings.x = tonumber(x) or 0
        overlaySettings.y = tonumber(y) or 0
    end)

    frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 66, -18)
    frame.title:SetFont("Fonts\\MORPHEUS.ttf", 22, "OUTLINE")
    frame.title:SetJustifyH("LEFT")
    frame.title:SetTextColor(0.98, 0.86, 0.44, 1)
    frame.titleIcon = frame:CreateTexture(nil, "OVERLAY")
    frame.titleIcon:SetTexture("Interface\\AddOns\\GH3\\Media\\Icons\\Icon_MythicPlus-l.png")
    frame.titleIcon:SetPoint("TOPLEFT", frame, "TOPLEFT", 28, -16)
    frame.titleIcon:SetSize(28, 28)
    frame.keyBadge = GH3_MythicPlusCreatePreviewCard(frame, { x = 0, y = 0, w = 68, h = 42 }, "", "+0", { 0.98, 0.86, 0.30, 1 }, "HomeRaidCardBackground", "")
    frame.closeButton = GH3_MythicPlusCreatePreviewTextureButton(frame, { x = 0, y = 0, w = 44, h = 44 }, "ShellCloseButton", "ShellCloseButtonHover", function()
        local runtime = service:GetRuntimeState().overlay
        if runtime.previewSnapshot then
            runtime.previewSnapshot = nil
            runtime.previewTrackerReadings = nil
            frame:Hide()
            return
        end

        service:SetOverlayEnabled(false)
        if addon.Print then
            addon:Print("Mythic+ overlay hidden. Use /gh3 mplushud on to show it again.")
        end
    end)
    frame.timerPanel = GH3_MythicPlusCreatePreviewSurface(frame, { x = 20, y = -68, w = MYTHIC_PLUS_OVERLAY_WIDTH - 40, h = 80 }, "MyToonsPanelBackground")
    frame.infoPanel = GH3_MythicPlusCreatePreviewSurface(frame, { x = 20, y = -154, w = MYTHIC_PLUS_OVERLAY_WIDTH - 40, h = 130 }, "MyToonsPanelBackground")

    frame.timerLabel = frame.timerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.timerLabel:SetPoint("TOPLEFT", frame.timerPanel, "TOPLEFT", 16, -7)
    frame.timerLabel:SetText("TIMER")
    frame.timerLabel:SetTextColor(0.78, 0.90, 1.00, 1)
    frame.timer = frame.timerPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    frame.timer:SetPoint("TOPLEFT", frame.timerPanel, "TOPLEFT", 16, -29)
    frame.timer:SetFont("Fonts\\FRIZQT__.TTF", 22, "")
    frame.timer:SetJustifyH("LEFT")
    frame.timerBar = GH3_MythicPlusCreatePreviewProgressBar(frame.timerPanel, MYTHIC_PLUS_OVERLAY_WIDTH - 72, 18, { 0.12, 0.40, 0.92, 0.96 })
    frame.timerBar:SetPoint("TOPLEFT", frame.timerPanel, "TOPLEFT", 16, -51)
    frame.threshold = frame.timerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.threshold:SetPoint("TOPLEFT", frame.timerPanel, "TOPLEFT", 16, -72)
    frame.threshold:SetTextColor(0.88, 0.86, 0.80, 1)

    frame.infoHeader = frame.infoPanel:CreateTexture(nil, "ARTWORK")
    frame.infoHeader:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 12, -8)
    frame.infoHeader:SetPoint("TOPRIGHT", frame.infoPanel, "TOPRIGHT", -12, -8)
    frame.infoHeader:SetHeight(24)
    GH3_MythicPlusPreviewApplyArtTexture(frame.infoHeader, "HomePanelHeader", { 1, 1, 1, 1 })
    frame.affixes = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.affixes:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 18, -13)
    frame.affixes:SetTextColor(0.92, 0.96, 1.00, 1)
    frame.forces = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.forces:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 18, -42)
    frame.forcesBar = GH3_MythicPlusCreatePreviewProgressBar(frame.infoPanel, MYTHIC_PLUS_OVERLAY_WIDTH - 72, 18, { 0.20, 0.72, 0.18, 0.96 })
    frame.forcesBar:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 16, -58)
    frame.bosses = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.bosses:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 18, -82)
    frame.objective = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.objective:SetPoint("TOPLEFT", frame.infoPanel, "TOPLEFT", 18, -102)
    frame.objective:SetJustifyH("LEFT")
    frame.objective:SetWordWrap(true)
    frame.objective:SetTextColor(0.84, 0.96, 0.74, 1)
    frame.nextObjective = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.nextObjective:SetPoint("TOPLEFT", frame.objective, "BOTTOMLEFT", 0, -4)
    frame.nextObjective:SetJustifyH("LEFT")
    frame.nextObjective:SetWordWrap(true)
    frame.nextObjective:SetTextColor(0.84, 0.96, 0.74, 1)
    frame.objectiveExtra = frame.infoPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.nextObjective:Hide()
    frame.objectiveExtra:Hide()
    GH3_MythicPlusLayoutPreviewOverlayFrame(frame)
    applyMythicPreviewDialogTransparency(frame, "mythicTimerPopup")
    return frame
end

function GH3_MythicPlusBuildPreviewSummaryFrame(service)
    local runtime = service:GetRuntimeState().summary
    local frame = CreateFrame("Frame", "GH3MythicPlusSummaryFrame", UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    frame:SetSize(MYTHIC_PLUS_SUMMARY_WIDTH, MYTHIC_PLUS_SUMMARY_HEIGHT)
    frame:SetFrameStrata("HIGH")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetPoint("CENTER", UIParent, "CENTER", 0, 60)
    GH3_MythicPlusDecoratePreviewDialog(frame)
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)

    frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    frame.title:SetPoint("TOP", frame, "TOP", 18, -18)
    frame.title:SetWidth(MYTHIC_PLUS_SUMMARY_WIDTH - 240)
    frame.title:SetFont("Fonts\\MORPHEUS.ttf", 28, "OUTLINE")
    frame.title:SetJustifyH("CENTER")
    frame.title:SetTextColor(0.98, 0.86, 0.44, 1)
    frame.titleIcon = frame:CreateTexture(nil, "OVERLAY")
    frame.titleIcon:SetTexture("Interface\\AddOns\\GH3\\Media\\Icons\\Icon_MythicPlus-l.png")
    frame.titleIcon:SetPoint("TOPLEFT", frame, "TOPLEFT", 274, -18)
    frame.titleIcon:SetSize(34, 34)
    frame.subtitle = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.subtitle:SetPoint("TOPLEFT", frame.title, "BOTTOMLEFT", 0, -4)
    frame.subtitle:Hide()
    frame.closeButton = GH3_MythicPlusCreatePreviewTextureButton(frame, { x = 966, y = -8, w = 44, h = 44 }, "ShellCloseButton", "ShellCloseButtonHover", function()
        runtime.dismissedToken = math.max(0, math.floor(tonumber(runtime.token) or 0))
        setBlizzardMythicSummarySuppressed(false)
        frame:Hide()
    end)
    frame:HookScript("OnHide", function()
        setBlizzardMythicSummarySuppressed(false)
    end)

    frame.resultCard = GH3_MythicPlusCreatePreviewCard(frame, { x = 36, y = -84, w = 222, h = 96 }, "RESULT", "-", { 0.78, 0.96, 0.72, 1 }, "HomeMembersCardBackground", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Ready-l.png")
    frame.timerCard = GH3_MythicPlusCreatePreviewCard(frame, { x = 274, y = -84, w = 222, h = 96 }, "TIMER", "-", { 0.82, 0.92, 1.00, 1 }, "HomeCardBackground", "Interface\\AddOns\\GH3\\Media\\Icons\\MythicBadge-l.png")
    frame.deathsCard = GH3_MythicPlusCreatePreviewCard(frame, { x = 512, y = -84, w = 222, h = 96 }, "DEATHS", "-", { 1.00, 0.84, 0.74, 1 }, "HomeRaidCardBackground", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png")
    frame.keyCard = GH3_MythicPlusCreatePreviewCard(frame, { x = 750, y = -84, w = 234, h = 96 }, "NEW KEY", "-", { 0.96, 0.88, 1.00, 1 }, "HomeMythicCardBackground", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Event_Mythic-l.png")
    frame.scorePanel = GH3_MythicPlusCreatePreviewSurface(frame, { x = 36, y = -198, w = 458, h = 368 }, "MyToonsPanelBackground")
    frame.lootPanel = GH3_MythicPlusCreatePreviewSurface(frame, { x = 526, y = -198, w = 458, h = 368 }, "MyToonsPanelBackground")

    local function decoratePanel(panel, titleText, iconTexture)
        panel.title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        panel.title:SetPoint("TOPLEFT", panel, "TOPLEFT", 44, -16)
        panel.title:SetFont("Fonts\\MORPHEUS.ttf", 22, "OUTLINE")
        panel.title:SetText(titleText)
        panel.title:SetTextColor(0.98, 0.90, 0.64, 1)
        panel.icon = panel:CreateTexture(nil, "OVERLAY")
        panel.icon:SetTexture(iconTexture)
        panel.icon:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -14)
        panel.icon:SetSize(20, 20)
        panel.header = GH3_MythicPlusCreatePreviewSurface(panel, { x = 16, y = -54, w = 426, h = 26 }, "HomePanelHeader")
        panel.tableHeader = CreateFrame("Frame", nil, panel)
        panel.tableHeader:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -58)
        panel.tableHeader:SetSize(426, 18)
        panel.rowsHost = CreateFrame("Frame", nil, panel)
        panel.rowsHost:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -82)
        panel.rowsHost:SetSize(426, 260)
        panel.body = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        panel.body:SetPoint("TOPLEFT", panel.rowsHost, "TOPLEFT", 12, -2)
        panel.body:SetWidth(402)
        panel.body:SetJustifyH("LEFT")
        panel.body:SetJustifyV("TOP")
        panel.body:Hide()
        panel.gridWidth = 426
        panel.gridContentInset = 12
        panel.rows = {}
    end

    decoratePanel(frame.scorePanel, "Party Score", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_MythicPlus-l.png")
    decoratePanel(frame.lootPanel, "Loot", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_BIS-l.png")
    applyMythicPreviewDialogTransparency(frame, "mythicSummaryPopup")
    return frame
end

function addon.MythicPlusService:ApplyConfiguredPopupTransparency()
    local runtime = self:GetRuntimeState()

    if runtime.overlay and runtime.overlay.frame then
        applyMythicPreviewDialogTransparency(runtime.overlay.frame, "mythicTimerPopup")
    end

    if runtime.summary and runtime.summary.summaryFrame then
        applyMythicPreviewDialogTransparency(runtime.summary.summaryFrame, "mythicSummaryPopup")
    end
end

function addon.MythicPlusService:EnsureMythicPlusOverlayFrame()
    local runtime = self:GetRuntimeState().overlay

    if runtime.frame then
        return runtime.frame
    end

    local frame = GH3_MythicPlusBuildPreviewOverlayFrame(self)
    frame:SetScript("OnUpdate", function(_, elapsedSeconds)
        runtime.elapsedSinceRefresh = (tonumber(runtime.elapsedSinceRefresh) or 0) + (tonumber(elapsedSeconds) or 0)

        if runtime.elapsedSinceRefresh >= MYTHIC_PLUS_OVERLAY_UPDATE_INTERVAL_SECONDS then
            runtime.elapsedSinceRefresh = 0
            self:RefreshMythicPlusOverlay("tick")
        end
    end)
    runtime.frame = frame
    return frame
end

local function formatThresholdLabel(label, remainingSeconds)
    local remaining = tonumber(remainingSeconds) or 0

    if remaining >= 0 then
        return string.format("%s %s", label, formatRunClock(remaining))
    end

    return string.format("%s +%s", label, formatRunClock(math.abs(remaining)))
end

local function extractDisplayPercent(value)
    local text = trimString(value)

    if text == "" then
        return nil
    end

    local percentValue = tonumber(string.match(text, "([%d%.]+)%s*%%"))

    if percentValue and percentValue >= 0 and percentValue <= 100 then
        return percentValue
    end

    return nil
end

local overlaySavedState = {}

function overlaySavedState.getSavedForces(service)
    local settings = service:GetOverlaySettings()
    settings.liveForces = type(settings.liveForces) == "table" and settings.liveForces or {}
    return settings.liveForces
end

function overlaySavedState.clearSavedForces(service)
    local settings = service:GetOverlaySettings()
    settings.liveForces = {}
end

function overlaySavedState.getSavedActiveRun(service)
    local settings = service:GetOverlaySettings()
    settings.activeRun = type(settings.activeRun) == "table" and settings.activeRun or {}
    return settings.activeRun
end

function overlaySavedState.clearSavedActiveRun(service)
    local settings = service:GetOverlaySettings()
    settings.activeRun = {}
end

function overlaySavedState.isSavedActiveRunMatch(savedRun, mapId, keyLevel)
    if type(savedRun) ~= "table" then
        return false
    end

    local savedMapId = normalizeDungeonId(savedRun.mapId and tostring(savedRun.mapId) or nil)
    local currentMapId = normalizeDungeonId(mapId and tostring(mapId) or nil)

    if not savedMapId or not currentMapId or savedMapId ~= currentMapId then
        return false
    end

    if math.floor(tonumber(savedRun.keyLevel) or 0) ~= math.floor(tonumber(keyLevel) or 0) then
        return false
    end

    return (tonumber(savedRun.startedAtWallClock) or 0) > 0
end

function overlaySavedState.getSavedTiming(service, snapshot)
    local savedRun = overlaySavedState.getSavedActiveRun(service)

    if not overlaySavedState.isSavedActiveRunMatch(savedRun, snapshot and snapshot.mapId, snapshot and snapshot.keyLevel) then
        return nil, nil
    end

    local wallNow = getWallClockSeconds()
    local startedAtWallClock = tonumber(savedRun.startedAtWallClock) or 0
    local timeLimit = math.max(0, math.floor(tonumber(snapshot and snapshot.timeLimit) or tonumber(savedRun.timeLimit) or 0))

    if startedAtWallClock <= 0 or timeLimit <= 0 then
        return nil, nil
    end

    local elapsed = math.max(0, wallNow - startedAtWallClock)
    return elapsed, timeLimit - elapsed
end

function overlaySavedState.saveActiveRun(service, snapshot, remaining)
    if not (snapshot and snapshot.active) then
        overlaySavedState.clearSavedActiveRun(service)
        return
    end

    local mapId = normalizeDungeonId(snapshot.mapId and tostring(snapshot.mapId) or nil)
    local keyLevel = math.floor(tonumber(snapshot.keyLevel) or 0)
    local timeLimit = math.max(0, math.floor(tonumber(snapshot.timeLimit) or 0))

    if not mapId or keyLevel <= 0 or timeLimit <= 0 then
        return
    end

    local savedRun = overlaySavedState.getSavedActiveRun(service)
    local wallNow = getWallClockSeconds()
    local resolvedRemaining = tonumber(remaining)
    local resolvedElapsed = tonumber(snapshot.elapsed)
    local startedAtWallClock = resolvedElapsed and (wallNow - resolvedElapsed)
        or (resolvedRemaining and (wallNow - (timeLimit - resolvedRemaining)))
        or tonumber(savedRun.startedAtWallClock)
    local savedMapId = normalizeDungeonId(savedRun.mapId and tostring(savedRun.mapId) or nil)
    local savedKeyLevel = math.floor(tonumber(savedRun.keyLevel) or 0)
    local savedStartedAtWallClock = math.floor(tonumber(savedRun.startedAtWallClock) or 0)
    local resolvedStartedAtWallClock = startedAtWallClock and math.floor(startedAtWallClock + 0.5) or 0
    local isDifferentRun = savedMapId ~= mapId
        or savedKeyLevel ~= keyLevel
        or (savedStartedAtWallClock > 0 and resolvedStartedAtWallClock > 0 and math.abs(savedStartedAtWallClock - resolvedStartedAtWallClock) > 10)

    if isDifferentRun then
        savedRun.forcesPercent = nil
        savedRun.forcesUpdatedAt = nil
        overlaySavedState.clearSavedForces(service)
    end

    savedRun.mapId = mapId
    savedRun.keyLevel = keyLevel
    savedRun.timeLimit = timeLimit
    savedRun.startedAtWallClock = resolvedStartedAtWallClock
    savedRun.updatedAt = wallNow
end

function overlaySavedState.seedActiveRun(service, mapId, keyLevel, timeLimit, startedAtWallClock)
    local normalizedMapId = normalizeDungeonId(mapId and tostring(mapId) or nil)
    local resolvedKeyLevel = math.floor(tonumber(keyLevel) or 0)
    local resolvedTimeLimit = math.max(0, math.floor(tonumber(timeLimit) or 0))
    local resolvedStartedAtWallClock = math.floor(tonumber(startedAtWallClock) or 0)

    if not normalizedMapId or resolvedKeyLevel <= 0 or resolvedTimeLimit <= 0 or resolvedStartedAtWallClock <= 0 then
        return
    end

    local savedRun = overlaySavedState.getSavedActiveRun(service)
    local savedMapId = normalizeDungeonId(savedRun.mapId and tostring(savedRun.mapId) or nil)
    local savedKeyLevel = math.floor(tonumber(savedRun.keyLevel) or 0)
    local savedStartedAtWallClock = math.floor(tonumber(savedRun.startedAtWallClock) or 0)
    local isDifferentRun = savedMapId ~= normalizedMapId
        or savedKeyLevel ~= resolvedKeyLevel
        or (savedStartedAtWallClock > 0 and math.abs(savedStartedAtWallClock - resolvedStartedAtWallClock) > 10)

    if isDifferentRun then
        savedRun.forcesPercent = nil
        savedRun.forcesUpdatedAt = nil
        overlaySavedState.clearSavedForces(service)
    end

    savedRun.mapId = normalizedMapId
    savedRun.keyLevel = resolvedKeyLevel
    savedRun.timeLimit = resolvedTimeLimit
    savedRun.startedAtWallClock = resolvedStartedAtWallClock
    savedRun.updatedAt = getWallClockSeconds()
end

function overlaySavedState.isSavedForcesMatch(savedForces, mapId, keyLevel)
    if type(savedForces) ~= "table" then
        return false
    end

    local savedMapId = normalizeDungeonId(savedForces.mapId and tostring(savedForces.mapId) or nil)
    local currentMapId = normalizeDungeonId(mapId and tostring(mapId) or nil)

    if not savedMapId or not currentMapId or savedMapId ~= currentMapId then
        return false
    end

    if math.floor(tonumber(savedForces.keyLevel) or 0) ~= math.floor(tonumber(keyLevel) or 0) then
        return false
    end

    local updatedAt = tonumber(savedForces.updatedAt) or 0

    if updatedAt > 0 and time and (time() - updatedAt) > 21600 then
        return false
    end

    return true
end

local function resolveOverlayForcesPercent(service, snapshot, forces, reason, trackerReadings)
    if GH3_MythicPlusShouldSuppressForces(snapshot) then
        overlaySavedState.clearSavedForces(service)
        return nil
    end

    if forces and forces.isTrustedForces == false then
        forces = nil
    end

    local current = forces and (tonumber(forces.current) or 0) or 0
    local total = forces and (tonumber(forces.total) or 0) or 0
    local isWeighted = forces and forces.isWeightedProgress and true or false
    local percent = nil
    local percentIsLive = false
    local displayPercent = forces and extractDisplayPercent(forces.quantityLabel) or nil
    local trackerPercent = tonumber(trackerReadings and trackerReadings.forcesPercent) or nil
    local ratioPercent = (not isWeighted and total > 0) and ((current / total) * 100) or nil

    local getTrackerReadings = addon.MythicPlusService and addon.MythicPlusService._getCachedObjectiveTrackerReadings or nil

    if trackerPercent == nil and snapshot and snapshot.active and getTrackerReadings then
        local runtime = service and service.GetRuntimeState and service:GetRuntimeState().overlay or nil
        local shouldForceTrackerScan = reason == "tick" or reason == "settings"
        local liveTrackerReadings = runtime and getTrackerReadings(runtime, reason or "tick", shouldForceTrackerScan) or nil
        local resolvedTrackerPercent = tonumber(liveTrackerReadings and liveTrackerReadings.forcesPercent)

        if resolvedTrackerPercent and resolvedTrackerPercent >= 0 and resolvedTrackerPercent <= 100 then
            trackerPercent = resolvedTrackerPercent
        end
    end

    local bestLive = trackerPercent or displayPercent

    if bestLive == nil and ratioPercent ~= nil and not isWeighted then
        bestLive = ratioPercent
    end

    if bestLive ~= nil then
        percent = bestLive
        percentIsLive = true
    end

    local savedForces = overlaySavedState.getSavedForces(service)
    local savedPercent = tonumber(savedForces.percent)
    local savedRun = overlaySavedState.getSavedActiveRun(service)
    local savedRunStartedAtWallClock = overlaySavedState.isSavedActiveRunMatch(savedRun, snapshot and snapshot.mapId, snapshot and snapshot.keyLevel)
        and math.floor(tonumber(savedRun.startedAtWallClock) or 0)
        or 0
    local savedRunPercent = (savedRunStartedAtWallClock > 0 and (tonumber(savedRun.forcesUpdatedAt) or 0) > 0)
        and tonumber(savedRun.forcesPercent)
        or nil
    local savedForcesStartedAtWallClock = math.floor(tonumber(savedForces.startedAtWallClock) or 0)
    local savedForcesMatchesRun = savedRunStartedAtWallClock <= 0
        or (savedForcesStartedAtWallClock > 0 and math.abs(savedForcesStartedAtWallClock - savedRunStartedAtWallClock) <= 10)

    if percent == nil and savedRunPercent and savedRunPercent >= 0 and savedRunPercent <= 100 then
        percent = savedRunPercent
    end

    if percent == nil
        and savedForcesMatchesRun
        and overlaySavedState.isSavedForcesMatch(savedForces, snapshot and snapshot.mapId, snapshot and snapshot.keyLevel)
        and savedPercent and savedPercent >= 0 and savedPercent <= 100 then
        percent = savedPercent
    end

    if percent ~= nil then
        percent = math.min(math.max(tonumber(percent) or 0, 0), 100)

        local elapsed = tonumber(snapshot and snapshot.elapsed)

        if elapsed ~= nil and elapsed >= 0 and elapsed < 300 and percent >= 80 then
            overlaySavedState.clearSavedForces(service)
            return nil
        end

        if percentIsLive and snapshot and snapshot.active and tonumber(snapshot.mapId) and tonumber(snapshot.mapId) > 0 and tonumber(snapshot.keyLevel) and tonumber(snapshot.keyLevel) > 0 then
            local shouldSave = not overlaySavedState.isSavedForcesMatch(savedForces, snapshot.mapId, snapshot.keyLevel)
                or percent >= (tonumber(savedForces.percent) or 0)

            if shouldSave then
                savedForces.mapId = normalizeDungeonId(tostring(snapshot.mapId))
                savedForces.keyLevel = math.floor(tonumber(snapshot.keyLevel) or 0)
                savedForces.percent = percent
                savedForces.startedAtWallClock = savedRunStartedAtWallClock
                savedForces.updatedAt = time and time() or 0
            end

            if overlaySavedState.isSavedActiveRunMatch(savedRun, snapshot.mapId, snapshot.keyLevel) then
                savedRun.forcesPercent = percent
                savedRun.forcesUpdatedAt = time and time() or 0
            end
        end
    end

    return percent
end

local function parseClockText(value)
    local text = trimString(value)
    local hours, minutes, seconds = string.match(text, "^(%d+):(%d%d):(%d%d)$")

    if hours then
        return ((tonumber(hours) or 0) * 3600) + ((tonumber(minutes) or 0) * 60) + (tonumber(seconds) or 0)
    end

    minutes, seconds = string.match(text, "^(%d+):(%d%d)$")

    if minutes then
        return ((tonumber(minutes) or 0) * 60) + (tonumber(seconds) or 0)
    end

    return nil
end

local function isInMythicOverlay(frame)
    local overlay = _G and _G.GH3MythicPlusOverlayFrame or nil
    local current = frame

    while current do
        if current == overlay then
            return true
        end

        local parent = nil

        if current.GetParent then
            local ok, resolvedParent = pcall(current.GetParent, current)

            if ok then
                parent = resolvedParent
            end
        end

        current = parent
    end

    return false
end

local function safeCallTruthy(callback)
    local ok, value = pcall(callback)

    if not ok then
        return false
    end

    local boolOk, boolValue = pcall(function()
        return value and true or false
    end)

    return boolOk and boolValue or false
end

local function safeCallNumber(callback)
    local ok, value = pcall(callback)

    if not ok then
        return nil
    end

    local numberOk, numberValue = pcall(tonumber, value)

    if numberOk then
        return numberValue
    end

    return nil
end

local function safeFrameName(frame)
    if not frame or type(frame.GetName) ~= "function" then
        return ""
    end

    local ok, name = pcall(frame.GetName, frame)

    if not ok then
        return ""
    end

    return trimString(name)
end

local function safeFrameParent(frame)
    if not frame or type(frame.GetParent) ~= "function" then
        return nil
    end

    local ok, parent = pcall(frame.GetParent, frame)

    if ok then
        return parent
    end

    return nil
end

local function isObjectiveTrackerRelatedFrame(frame)
    local current = frame
    local depth = 0

    while current and depth < 10 do
        local name = safeFrameName(current)

        if name ~= "" and (string.find(name, "ObjectiveTracker", 1, true) or string.find(name, "ScenarioObjective", 1, true)) then
            return true
        end

        current = safeFrameParent(current)
        depth = depth + 1
    end

    return false
end

local function collectRegionList(frame)
    if not frame or type(frame.GetRegions) ~= "function" then
        return {}
    end

    local values = { pcall(frame.GetRegions, frame) }

    if not values[1] then
        return {}
    end

    local regions = {}

    for index = 2, #values do
        regions[#regions + 1] = values[index]
    end

    return regions
end

local function getRegionObjectType(region)
    if not region or type(region.GetObjectType) ~= "function" then
        return ""
    end

    local ok, objectType = pcall(region.GetObjectType, region)

    if not ok then
        return ""
    end

    return trimString(objectType)
end

local function getRegionText(region)
    if not region or type(region.GetText) ~= "function" then
        return ""
    end

    local ok, text = pcall(region.GetText, region)

    if not ok then
        return ""
    end

    return trimString(text)
end

local function getRegionCenter(region)
    if not region or type(region.GetCenter) ~= "function" then
        return nil, nil
    end

    local ok, x, y = pcall(region.GetCenter, region)

    if not ok then
        return nil, nil
    end

    local numericX = safeCallNumber(function()
        return x
    end)
    local numericY = safeCallNumber(function()
        return y
    end)

    return numericX, numericY
end

local function getRegionStringHeight(region)
    if not region or type(region.GetStringHeight) ~= "function" then
        return 0
    end

    return safeCallNumber(function()
        return region:GetStringHeight()
    end) or 0
end

local function collectVisibleFontStrings()
    local rows = {}

    if type(EnumerateFrames) ~= "function" then
        return rows
    end

    local frame = EnumerateFrames()
    local scanned = 0

    while frame and scanned < 5000 do
        scanned = scanned + 1

        if isObjectiveTrackerRelatedFrame(frame)
            and safeCallTruthy(function() return frame:IsShown() end)
            and not isInMythicOverlay(frame) then
            local regions = collectRegionList(frame)

            for _, region in ipairs(regions) do
                if getRegionObjectType(region) == "FontString"
                    and safeCallTruthy(function() return region:IsShown() end) then
                    local text = getRegionText(region)

                    if text ~= "" then
                        local x, y = getRegionCenter(region)

                        if x ~= nil and y ~= nil then
                            rows[#rows + 1] = {
                                text = text,
                                lowerText = lowerText(text),
                                x = x,
                                y = y,
                                height = getRegionStringHeight(region),
                            }
                        end
                    end
                end
            end
        end

        frame = EnumerateFrames(frame)
    end

    return rows
end

local function getObjectiveTrackerReadings()
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope("mythics:overlay-tracker-scan") or nil
    local rows = collectVisibleFontStrings()
    local forcesAnchor = nil
    local timerAnchor = nil
    local bestTimer = nil
    local bestTimerScore = -math.huge
    local bestForcesPercent = nil
    local bestForcesScore = -math.huge

    for _, row in ipairs(rows) do
        if row.lowerText == "forces"
            or row.lowerText == "enemy forces"
            or string.find(row.lowerText, "enemy forces", 1, true)
            or string.find(row.lowerText, "forces", 1, true) then
            if not forcesAnchor or row.x > forcesAnchor.x then
                forcesAnchor = row
            end
        end

        if string.match(row.lowerText, "^level%s+%d+") then
            if not timerAnchor or row.x > timerAnchor.x then
                timerAnchor = row
            end
        end
    end

    if forcesAnchor then
        for _, row in ipairs(rows) do
            local percent = extractDisplayPercent(row.text)

            if percent then
                local xDistance = math.abs(row.x - forcesAnchor.x)
                local yDistance = math.abs(row.y - forcesAnchor.y)
                local score = 0

                if row.x >= forcesAnchor.x - 60 then
                    score = score + 100
                end

                if row.y <= forcesAnchor.y + 24 then
                    score = score + 100
                end

                score = score - xDistance - (yDistance * 2)

                if xDistance <= 420 and yDistance <= 130 and score > bestForcesScore then
                    bestForcesScore = score
                    bestForcesPercent = percent
                end
            end
        end
    end

    if timerAnchor then
        for _, row in ipairs(rows) do
            local seconds = parseClockText(row.text)

            if seconds then
                local xDistance = math.abs(row.x - timerAnchor.x)
                local yDistance = math.abs(row.y - timerAnchor.y)
                local score = 0

                if row.y <= timerAnchor.y + 12 then
                    score = score + 80
                end

                if row.x >= timerAnchor.x - 80 then
                    score = score + 80
                end

                score = score + row.height - xDistance - (yDistance * 1.5)

                if xDistance <= 360 and yDistance <= 120 and score > bestTimerScore then
                    bestTimerScore = score
                    bestTimer = seconds
                end
            end
        end
    end

    local result = {
        forcesPercent = bestForcesPercent,
        timerRemaining = bestTimer,
    }

    if perfStartedAt ~= nil and addon.EndPerfScope then
        addon:EndPerfScope("mythics:overlay-tracker-scan", perfStartedAt)
    end

    return result
end

addon.MythicPlusService._getCachedObjectiveTrackerReadings = function(runtime, reason, forceScan)
    local now = getClockSeconds()
    local cached = type(runtime.trackerReadings) == "table" and runtime.trackerReadings or nil

    if not forceScan and not MYTHIC_PLUS_OVERLAY_ENABLE_TRACKER_FALLBACK then
        return cached or {}
    end

    local age = now - (tonumber(runtime.trackerReadingsAt) or 0)
    local shouldRescan = reason ~= "tick"
        or not cached
        or age >= MYTHIC_PLUS_OVERLAY_TRACKER_SCAN_INTERVAL_SECONDS
        or (cached.forcesPercent == nil and cached.timerRemaining == nil)

    if not shouldRescan then
        return cached
    end

    local fresh = getObjectiveTrackerReadings()

    if cached then
        if fresh.forcesPercent == nil then
            fresh.forcesPercent = cached.forcesPercent
        end

        if fresh.timerRemaining == nil then
            fresh.timerRemaining = cached.timerRemaining
        end
    end

    runtime.trackerReadings = fresh
    runtime.trackerReadingsAt = now
    return fresh
end

function addon.MythicPlusService:RefreshMythicPlusOverlay(reason)
    local runtime = self:GetRuntimeState().overlay
    local perfStartedAt = addon.BeginPerfScope and addon:BeginPerfScope("mythics:refresh-overlay") or nil
    local previewSnapshot = type(runtime.previewSnapshot) == "table" and runtime.previewSnapshot or nil

    if reason == "CHALLENGE_MODE_START" or reason == "start" then
        overlaySavedState.clearSavedForces(self)
        overlaySavedState.clearSavedActiveRun(self)
        runtime.previewSnapshot = nil
        runtime.previewTrackerReadings = nil
        runtime.startedAt = getClockSeconds()
        runtime.liveStateCache = nil
        runtime.liveStateUpdatedAt = 0
        runtime.trackerReadings = nil
        runtime.trackerReadingsAt = 0
    elseif reason == "CHALLENGE_MODE_COMPLETED" or reason == "CHALLENGE_MODE_RESET" then
        overlaySavedState.clearSavedForces(self)
        runtime.previewSnapshot = nil
        runtime.previewTrackerReadings = nil
        runtime.startedAt = 0
        runtime.liveStateCache = nil
        runtime.liveStateUpdatedAt = 0
        runtime.trackerReadings = nil
        runtime.trackerReadingsAt = 0

        if reason == "CHALLENGE_MODE_RESET" then
            overlaySavedState.clearSavedActiveRun(self)
        end
    end

    if ((not self:IsOverlayEnabled()) and not previewSnapshot) or (addon.IsFeatureEnabled and not addon:IsFeatureEnabled("mythics")) then
        setBlizzardMythicTimerSuppressed(false)
        if runtime.frame then
            runtime.frame:Hide()
        end

        if perfStartedAt ~= nil and addon.EndPerfScope then
            addon:EndPerfScope("mythics:refresh-overlay", perfStartedAt)
        end

        return false
    end

    local snapshot = previewSnapshot or self:GetMythicPlusOverlaySnapshot(reason)

    if not snapshot.active then
        setBlizzardMythicTimerSuppressed(false)
        if runtime.frame then
            runtime.frame:Hide()
        end

        if perfStartedAt ~= nil and addon.EndPerfScope then
            addon:EndPerfScope("mythics:refresh-overlay", perfStartedAt)
        end

        return false
    end

    setBlizzardMythicTimerSuppressed(not previewSnapshot)

    local frame = self:EnsureMythicPlusOverlayFrame()
    local timeLimit = tonumber(snapshot.timeLimit) or 0
    local elapsed = tonumber(snapshot.elapsed)
    local remaining = tonumber(snapshot.remaining)
    local forces = snapshot.forces
    local titlePrefix = snapshot.keyLevel > 0 and string.format("+%d ", snapshot.keyLevel) or ""
    local trackerReadings = nil

    if not previewSnapshot and snapshot.active then
        local getTrackerReadings = addon.MythicPlusService and addon.MythicPlusService._getCachedObjectiveTrackerReadings or nil

        if getTrackerReadings then
            local shouldForceTrackerScan = reason == "tick" or reason == "settings"
            trackerReadings = getTrackerReadings(runtime, reason or "tick", shouldForceTrackerScan) or nil

            local trackerRemaining = tonumber(trackerReadings and trackerReadings.timerRemaining)

            if timeLimit > 0 and trackerRemaining and trackerRemaining >= 0 and trackerRemaining <= (timeLimit * 3) then
                remaining = trackerRemaining
                elapsed = math.max(0, timeLimit - trackerRemaining)
            end
        end
    end

    GH3_MythicPlusHideDungeonArt(frame)

    if timeLimit > 0 and (elapsed == nil or remaining == nil) then
        elapsed, remaining = overlaySavedState.getSavedTiming(self, snapshot)
    end

    snapshot.elapsed = elapsed
    snapshot.remaining = remaining
    overlaySavedState.saveActiveRun(self, snapshot, remaining)

    frame.title:SetText(trimString(snapshot.mapName) ~= "" and snapshot.mapName or "Mythic+")
    frame.keyBadge.Value:SetText(titlePrefix ~= "" and trimString(titlePrefix) or "+0")
    GH3_MythicPlusLayoutPreviewOverlayFrame(frame)

    if timeLimit > 0 and elapsed ~= nil and remaining ~= nil then
        local timerText = remaining >= 0
            and string.format("%s left / %s", formatRunClock(remaining), formatRunClock(timeLimit))
            or string.format("Overtime +%s / %s", formatRunClock(math.abs(remaining)), formatRunClock(timeLimit))
        frame.timer:SetText(timerText)
        setOverlayTextColor(frame.timer, remaining >= 0 and { 0.90, 0.96, 1.00, 1 } or { 1.00, 0.42, 0.32, 1 })
        frame.timerBar:SetStatusBarTexture(remaining >= 0 and MEDIA_TEXTURES.fillBlue or MEDIA_TEXTURES.fillGold)
        frame.timerBar:SetValue(math.min(math.max(elapsed / timeLimit, 0), 1))
        local plusTwoRemaining = (timeLimit * 0.8) - elapsed
        local plusThreeRemaining = (timeLimit * 0.6) - elapsed
        frame.threshold:SetText(table.concat({
            formatThresholdLabel("+3", plusThreeRemaining),
            formatThresholdLabel("+2", plusTwoRemaining),
            formatThresholdLabel("+1", remaining),
        }, "   "))
    else
        frame.timer:SetText("Timer loading")
        frame.timerBar:SetValue(0)
        frame.threshold:SetText("+3 --   +2 --   +1 --")
    end

    frame.affixes:SetText(trimString(snapshot.affixes) ~= "" and snapshot.affixes or "Affixes: loading")

    local objectiveLine, utilityObjectiveRow = GH3_MythicPlusGetUtilityObjectiveProgress(snapshot.objectiveRows)
    local bossDone, bossTotal = GH3_MythicPlusGetBossObjectiveProgress(snapshot.objectiveRows)
    local forcesPercent = resolveOverlayForcesPercent(self, snapshot, forces, reason, trackerReadings)

    if forcesPercent ~= nil then
        frame.forces:SetText(string.format("Enemy Forces  %d%% complete", math.floor(forcesPercent + 0.0001)))
        frame.forcesBar:SetValue(math.min(math.max(forcesPercent / 100, 0), 1))
    else
        frame.forces:SetText("Enemy Forces  loading")
        frame.forcesBar:SetValue(0)
    end

    frame.bosses:SetText(string.format("Bosses %d/%d killed   Deaths %d", bossDone, bossTotal, snapshot.deaths))

    local nextObjective = ""

    for _, row in ipairs(snapshot.objectiveRows or {}) do
        if not row.completed and row ~= utilityObjectiveRow then
            local nextLabel = GH3_MythicPlusFormatNextObjectiveLabel(row.label)

            if nextLabel ~= "" then
                nextObjective = "Next: " .. nextLabel
            end
            break
        end
    end

    if nextObjective == "" and snapshot.objectiveTotal > 0 then
        nextObjective = "Objectives complete"
    elseif nextObjective == "" then
        nextObjective = "Objectives loading"
    end

    local objectiveText = snapshot.objectiveTotal > 0
        and string.format("Objectives %d/%d", snapshot.objectiveDone, snapshot.objectiveTotal)
        or "Objectives loading"
    local objectiveDetail = utilityObjectiveRow and objectiveLine ~= "" and objectiveLine or nextObjective

    frame.objective:SetText(objectiveText)
    frame.nextObjective:SetText("")
    frame.objectiveExtra:SetText("")

    if objectiveDetail ~= "" and objectiveDetail ~= objectiveText then
        frame.nextObjective:SetText(objectiveDetail)
        frame.nextObjective:Show()
    else
        frame.nextObjective:Hide()
    end

    frame.objectiveExtra:Hide()
    frame.objective:Show()
    frame:Show()
    runtime.lastSnapshot = snapshot

    if perfStartedAt ~= nil and addon.EndPerfScope then
        addon:EndPerfScope("mythics:refresh-overlay", perfStartedAt)
    end

    return true
end

local function safeRegisterOverlayEvent(frame, eventName)
    if not frame or trimString(eventName) == "" then
        return
    end

    pcall(frame.RegisterEvent, frame, eventName)
end

function addon.MythicPlusService:InitializeOverlay()
    local runtime = self:GetRuntimeState().overlay

    if runtime.eventFrame then
        self:RefreshMythicPlusOverlay("init")
        return runtime.eventFrame
    end

    local eventFrame = CreateFrame("Frame")
    local service = self
    local events = {
        "CHALLENGE_MODE_COMPLETED",
        "CHALLENGE_MODE_RESET",
        "CHALLENGE_MODE_START",
        "CRITERIA_UPDATE",
        "MYTHIC_PLUS_CURRENT_AFFIX_UPDATE",
        "PLAYER_ENTERING_WORLD",
        "SCENARIO_CRITERIA_UPDATE",
        "SCENARIO_UPDATE",
        "ZONE_CHANGED_NEW_AREA",
    }

    for _, eventName in ipairs(events) do
        safeRegisterOverlayEvent(eventFrame, eventName)
    end

    eventFrame:SetScript("OnEvent", function(_, event)
        if event == "CHALLENGE_MODE_START" and service.HandleChallengeModeStarted then
            service:HandleChallengeModeStarted(event)
        elseif event == "CHALLENGE_MODE_COMPLETED" and service.HandleChallengeModeCompleted then
            service:HandleChallengeModeCompleted()

            if C_Timer and C_Timer.After then
                C_Timer.After(1, function()
                    service:HandleChallengeModeCompleted()
                end)
                C_Timer.After(4, function()
                    service:HandleChallengeModeCompleted()
                end)
            end
        end

        service:RefreshMythicPlusOverlay(event)
    end)

    runtime.eventFrame = eventFrame
    self:RefreshMythicPlusOverlay("init")
    return eventFrame
end

function addon.MythicPlusService:IsChallengeModeActive()
    if type(C_ChallengeMode) ~= "table" then
        return false
    end

    local active = safeInvoke(C_ChallengeMode, "IsChallengeModeActive")

    if active ~= nil then
        return active and true or false
    end

    local activeMapId = safeInvoke(C_ChallengeMode, "GetActiveChallengeMapID")
    return (tonumber(activeMapId) or 0) > 0
end

function addon.MythicPlusService:EnsureMythicPlusSummaryFrame()
    local runtime = self:GetRuntimeState().summary

    if runtime.summaryFrame then
        return runtime.summaryFrame
    end

    local frame = GH3_MythicPlusBuildPreviewSummaryFrame(self)
    runtime.summaryFrame = frame
    return frame
end

function addon.MythicPlusService:HandleChallengeModeStarted(reason)
    local runtime = self:GetRuntimeState().summary
    local activeMapId = tonumber(safeInvoke(C_ChallengeMode, "GetActiveChallengeMapID")) or 0
    local activeKeystoneLevel = safeInvoke(C_ChallengeMode, "GetActiveKeystoneInfo")
    local keyLevel = math.max(0, math.floor(tonumber(activeKeystoneLevel) or 0))
    local normalizedMapId = normalizeDungeonId(activeMapId > 0 and tostring(activeMapId) or nil)

    if not normalizedMapId and keyLevel <= 0 then
        return false
    end

    runtime.active = true
    runtime.completedAt = 0
    runtime.isPreview = false
    runtime.loot = {}
    runtime.lootSeen = {}
    runtime.token = runtime.token + 1
    runtime.run = {
        completedAt = 0,
        deaths = 0,
        duration = 0,
        keyLevel = keyLevel,
        mapId = normalizedMapId,
        mapName = normalizedMapId and getMapName(normalizedMapId) or "Mythic+",
        oldKey = getLiveCurrentKeyDetails(),
        reason = trimString(reason),
        scoresStart = capturePartyScoreSnapshot(),
        startedAt = time(),
        startClock = getClockSeconds(),
        timeLimit = normalizedMapId and (tonumber(getDungeonTimeLimitSeconds(normalizedMapId)) or 0) or 0,
    }

    overlaySavedState.seedActiveRun(
        self,
        runtime.run.mapId,
        runtime.run.keyLevel,
        runtime.run.timeLimit,
        runtime.run.startedAt
    )

    return true
end

function addon.MythicPlusService:ClearTestMythicPlusPreview(reason)
    local summaryRuntime = self:GetRuntimeState().summary
    local overlayRuntime = self:GetRuntimeState().overlay
    local changed = false

    if summaryRuntime.isPreview then
        summaryRuntime.active = false
        summaryRuntime.completedAt = 0
        summaryRuntime.isPreview = false
        summaryRuntime.loot = {}
        summaryRuntime.lootSeen = {}
        summaryRuntime.run = nil
        summaryRuntime.token = summaryRuntime.token + 1
        changed = true

        if summaryRuntime.summaryFrame then
            summaryRuntime.summaryFrame:Hide()
        end
    end

    if overlayRuntime.previewSnapshot then
        overlayRuntime.previewSnapshot = nil
        overlayRuntime.previewTrackerReadings = nil
        changed = true

        if overlayRuntime.frame and not self:IsChallengeModeActive() then
            overlayRuntime.frame:Hide()
        end
    end

    return changed
end

function addon.MythicPlusService:HandleWorldStateChanged(reason)
    local summaryRuntime = self:GetRuntimeState().summary

    if summaryRuntime.isPreview then
        return self:ClearTestMythicPlusPreview(reason)
    end

    return false
end

function addon.MythicPlusService:HandleLootMessage(message)
    local runtime = self:GetRuntimeState().summary
    local now = time()
    local active = self:IsChallengeModeActive()
    local recentlyCompleted = (tonumber(runtime.completedAt) or 0) > 0
        and (now - (tonumber(runtime.completedAt) or 0)) <= MYTHIC_PLUS_SUMMARY_LOOT_CAPTURE_SECONDS

    if not active and not recentlyCompleted then
        return false
    end

    if active and not runtime.active then
        self:HandleChallengeModeStarted("loot")
    end

    local itemLinks = extractSummaryItemLinks(message)

    if #itemLinks <= 0 then
        return false
    end

    local owner = extractLootOwnerFromMessage(message)
    local changed = false

    for _, itemLink in ipairs(itemLinks) do
        if isSummaryGearItem(itemLink) then
            local key = owner .. "\n" .. itemLink

            if not runtime.lootSeen[key] then
                runtime.lootSeen[key] = true
                runtime.loot[#runtime.loot + 1] = {
                    itemLevel = getSummaryItemLevel(itemLink),
                    itemLink = itemLink,
                    itemName = getSummaryItemName(itemLink),
                    lootedAt = now,
                    owner = owner,
                }
                changed = true
            end
        end
    end

    if changed and recentlyCompleted then
        self:RefreshMythicPlusCompletionSummary("loot")
    end

    return changed
end

local function formatSummaryKey(keyData, compact)
    local dungeonId = keyData and (keyData.challengeMapId or keyData.mythicPlusMapId) or nil
    local dungeonName = trimString(keyData and (keyData.mapName or keyData.name) or "")
    local level = math.max(0, math.floor(tonumber(keyData and keyData.keyLevel) or 0))

    if level <= 0 then
        return "waiting for bag update"
    end

    if dungeonId then
        dungeonName = getMapName(dungeonId)
    end

    if dungeonName ~= "" then
        if compact then
            return string.format("%s +%d", buildDungeonAbbreviation(dungeonName), level)
        end

        return string.format("%s +%d", dungeonName, level)
    end

    return string.format("+%d", level)
end

local function colorizeSummaryText(colorHex, text)
    local resolvedText = tostring(text or "")

    if resolvedText == "" then
        return ""
    end

    return string.format("|cff%s%s|r", colorHex, resolvedText)
end

local function setSummaryCardTheme(card, style)
    if not card or type(style) ~= "table" then
        return
    end

    local background = style.background or { 0.03, 0.04, 0.07, 0.95 }
    local border = style.border or { 0.16, 0.20, 0.30, 0.95 }
    local accent = style.accent or { 0.28, 0.63, 0.84, 0.95 }
    local glow = style.glow or accent
    local label = style.label or { 0.74, 0.79, 0.88, 1 }
    local value = style.value or { 0.96, 0.92, 0.82, 1 }

    applyMythicSurfaceStyle(card, {
        bg = { background[1], background[2], background[3], (background[4] or 0.95) * 0.78 },
        border = { border[1], border[2], border[3], (border[4] or 0.95) * 0.52 },
        highlight = { label[1], label[2], label[3], 0.06 },
        shadow = { 0, 0, 0, 0.78 },
        glow = { glow[1], glow[2], glow[3], (glow[4] or 0.22) * 0.75 },
    })
    GH3_MythicPlusMakeMediaOnlyFrame(card)
    if card.innerFrame then
        applyMythicSurfaceStyle(card.innerFrame, {
            bg = { background[1] * 0.45, background[2] * 0.45, background[3] * 0.45, 0.18 },
            border = { border[1], border[2], border[3], 0.16 },
            highlight = { value[1], value[2], value[3], 0.04 },
            shadow = { 0, 0, 0, 0.82 },
            glow = { border[1], border[2], border[3], 0.08 },
        })
        GH3_MythicPlusMakeMediaOnlyFrame(card.innerFrame)
    end
    card.accent:SetColorTexture(accent[1], accent[2], accent[3], accent[4] or 0.95)
    card.glow:SetColorTexture(glow[1], glow[2], glow[3], glow[4] or 0.22)
    card.corner:SetColorTexture(accent[1], accent[2], accent[3], 0.12)
    card.label:SetTextColor(label[1], label[2], label[3], label[4] or 1)
    card.value:SetTextColor(value[1], value[2], value[3], value[4] or 1)

    if card.icon then
        card.icon:SetVertexColor(value[1], value[2], value[3], 0.96)
    end
end

local function setSummaryPanelTheme(panel, style)
    if not panel or type(style) ~= "table" then
        return
    end

    local background = style.background or { 0.035, 0.05, 0.08, 0.94 }
    local border = style.border or { 0.18, 0.24, 0.34, 0.94 }
    local accent = style.accent or { 0.28, 0.63, 0.84, 0.95 }
    local glow = style.glow or accent
    local heading = style.heading or { 0.95, 0.84, 0.58, 1 }
    local body = style.body or { 0.90, 0.92, 0.96, 1 }

    applyMythicSurfaceStyle(panel, {
        bg = { background[1], background[2], background[3], (background[4] or 0.94) * 0.74 },
        border = { border[1], border[2], border[3], (border[4] or 0.94) * 0.48 },
        highlight = { heading[1], heading[2], heading[3], 0.05 },
        shadow = { 0, 0, 0, 0.80 },
        glow = { glow[1], glow[2], glow[3], (glow[4] or 0.26) * 0.65 },
    })
    GH3_MythicPlusMakeMediaOnlyFrame(panel)
    if panel.innerFrame then
        applyMythicSurfaceStyle(panel.innerFrame, {
            bg = { background[1] * 0.35, background[2] * 0.35, background[3] * 0.35, 0.16 },
            border = { border[1], border[2], border[3], 0.14 },
            highlight = { heading[1], heading[2], heading[3], 0.04 },
            shadow = { 0, 0, 0, 0.84 },
            glow = { border[1], border[2], border[3], 0.08 },
        })
        GH3_MythicPlusMakeMediaOnlyFrame(panel.innerFrame)
    end
    panel.accent:SetColorTexture(accent[1], accent[2], accent[3], accent[4] or 0.95)
    panel.headerGlow:SetColorTexture(glow[1], glow[2], glow[3], glow[4] or 0.26)
    panel.divider:SetColorTexture(border[1], border[2], border[3], 0.62)
    panel.heading:SetTextColor(heading[1], heading[2], heading[3], heading[4] or 1)
    panel.body:SetTextColor(body[1], body[2], body[3], body[4] or 1)

    for _, label in ipairs(panel.headerLabels or {}) do
        label:SetTextColor(body[1], body[2], body[3], 0.90)
    end
end

local function resetSummaryTable(panel)
    if not panel then
        return
    end

    for _, label in ipairs(panel.headerLabels or {}) do
        label:Hide()
    end

    for _, rowFrame in ipairs(panel.rowFrames or {}) do
        rowFrame:Hide()
    end
end

local function ensureSummaryHeaderLabel(panel, index)
    panel.headerLabels = panel.headerLabels or {}

    if panel.headerLabels[index] then
        return panel.headerLabels[index]
    end

    local label = panel.tableHeader:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
    label:SetJustifyH("LEFT")
    label:SetTextColor(0.74, 0.79, 0.88, 0.92)
    panel.headerLabels[index] = label
    return label
end

local function ensureSummaryRowFrame(panel, index)
    panel.rowFrames = panel.rowFrames or {}

    if panel.rowFrames[index] then
        return panel.rowFrames[index]
    end

    local rowFrame = CreateFrame("Frame", nil, panel.rowsHost, "BackdropTemplate")
    rowFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    rowFrame.mediaRow = createMediaTexture(rowFrame, "BACKGROUND", MEDIA_TEXTURES.rowNormal, 0.72)
    rowFrame.mediaRow:SetAllPoints(rowFrame)
    rowFrame.mediaRow:SetTexCoord(unpack(MEDIA_TEXCOORDS.rowNormal))
    GH3_MythicPlusMakeMediaOnlyFrame(rowFrame)
    panel.rowFrames[index] = rowFrame
    return rowFrame
end

local function ensureSummaryText(parent, key, fontPath, fontSize, fontFlags)
    if parent[key] then
        return parent[key]
    end

    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetFont(fontPath or "Fonts\\FRIZQT__.TTF", fontSize or 12, fontFlags or "")
    label:SetJustifyH("LEFT")
    label:SetJustifyV("MIDDLE")
    GH3_MythicPlusConfigureSingleLineText(label)
    parent[key] = label
    return label
end

local function resetSummaryRowFrame(rowFrame)
    if not rowFrame then
        return
    end

    if rowFrame.avatar then
        rowFrame.avatar:Hide()
    end

    for _, key in ipairs({ "nameText", "startText", "endText", "gainText", "itemText", "levelText" }) do
        if rowFrame[key] then
            rowFrame[key]:SetText("")
            rowFrame[key]:Hide()
        end
    end
end

local function renderSummaryTable(panel, headers, rows, columnWidths, rowHeight, rowBuilder)
    resetSummaryTable(panel)
    panel.body:SetText("")
    panel.body:Hide()

    local totalWidth = math.max(1, tonumber(panel.gridWidth) or (panel:GetWidth() - 40))
    local contentInset = math.max(0, tonumber(panel.gridContentInset) or 0)
    local headerOffset = 0

    for index, headerText in ipairs(headers or {}) do
        local width = columnWidths[index] or math.floor(totalWidth / math.max(#headers, 1))
        local label = ensureSummaryHeaderLabel(panel, index)
        label:ClearAllPoints()
        label:SetPoint("TOPLEFT", panel.tableHeader, "TOPLEFT", contentInset + headerOffset, 0)
        label:SetWidth(math.max(width - 4, 10))
        label:SetText(headerText)
        label:Show()
        headerOffset = headerOffset + width
    end

    for rowIndex, rowData in ipairs(rows or {}) do
        local rowFrame = ensureSummaryRowFrame(panel, rowIndex)
        rowFrame:ClearAllPoints()
        rowFrame:SetPoint("TOPLEFT", panel.rowsHost, "TOPLEFT", 0, -((rowIndex - 1) * rowHeight))
        rowFrame:SetSize(totalWidth, rowHeight)
        if rowFrame.SetBackdropColor then
            rowFrame:SetBackdropColor(0, 0, 0, 0)
        end
        if rowFrame.SetBackdropBorderColor then
            rowFrame:SetBackdropBorderColor(0, 0, 0, 0)
        end
        if rowFrame.mediaRow then
            rowFrame.mediaRow:SetTexture(MEDIA_TEXTURES.rowNormal)
            rowFrame.mediaRow:SetTexCoord(unpack(MEDIA_TEXCOORDS.rowNormal))
            rowFrame.mediaRow:SetAlpha(0.90)
        end
        resetSummaryRowFrame(rowFrame)
        rowFrame:Show()
        rowBuilder(rowFrame, rowData, columnWidths, contentInset)
    end
end

local function buildSummaryScoreRows(run)
    local rows = {}
    local seen = {}
    local startSnapshot = run and run.scoresStart or {}
    local endSnapshot = run and run.scoresEnd or {}
    local classes = buildRunClassLookup(run)

    for _, fullName in ipairs(startSnapshot.order or {}) do
        seen[fullName] = true
        rows[#rows + 1] = {
            classFile = classes[fullName],
            delta = (tonumber(startSnapshot.scores and startSnapshot.scores[fullName]) and tonumber(endSnapshot.scores and endSnapshot.scores[fullName]))
                and math.floor((tonumber(endSnapshot.scores[fullName]) or 0) - (tonumber(startSnapshot.scores[fullName]) or 0) + 0.5)
                or nil,
            endScore = tonumber(endSnapshot.scores and endSnapshot.scores[fullName]),
            fullName = fullName,
            shortName = addon.Store:SplitFullName(fullName, GetRealmName() or "Unknown"),
            startScore = tonumber(startSnapshot.scores and startSnapshot.scores[fullName]),
        }
    end

    for _, fullName in ipairs(endSnapshot.order or {}) do
        if not seen[fullName] then
            rows[#rows + 1] = {
                classFile = classes[fullName],
                delta = nil,
                endScore = tonumber(endSnapshot.scores and endSnapshot.scores[fullName]),
                fullName = fullName,
                shortName = addon.Store:SplitFullName(fullName, GetRealmName() or "Unknown"),
                startScore = nil,
            }
        end
    end

    return rows
end

local function buildSummaryLootRows(runtime, run)
    local rows = {}
    local classes = buildRunClassLookup(run)

    for _, lootRow in ipairs(runtime.loot or {}) do
        local fullName = lootRow.owner ~= "Unknown" and lootRow.owner or nil
        rows[#rows + 1] = {
            classFile = fullName and classes[fullName] or nil,
            fullName = fullName,
            itemLevel = math.max(0, math.floor(tonumber(lootRow.itemLevel) or 0)),
            itemName = trimString(lootRow.itemName) ~= "" and trimString(lootRow.itemName) or trimString(lootRow.itemLink),
            shortName = lootRow.owner ~= "Unknown" and addon.Store:SplitFullName(lootRow.owner, GetRealmName() or "Unknown") or "Unknown",
        }
    end

    return rows
end

function GH3_MythicPlusEnsurePreviewPanelColumns(panel, columns)
    panel.headerLabels = panel.headerLabels or {}

    for index, column in ipairs(columns) do
        local label = panel.headerLabels[index]
        if not label then
            label = panel.header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            panel.headerLabels[index] = label
        end
        label:ClearAllPoints()
        label:SetPoint("LEFT", panel.header, "LEFT", column.x, 0)
        label:SetText(column.text)
        label:SetTextColor(0.98, 0.90, 0.64, 1)
        label:Show()
    end
end

function GH3_MythicPlusRenderPreviewSummaryScorePanel(panel, run)
    local rows = buildSummaryScoreRows(run)
    GH3_MythicPlusEnsurePreviewPanelColumns(panel, {
        { x = 12, text = "Player" },
        { x = 212, text = "Start" },
        { x = 296, text = "End" },
        { x = 374, text = "Gain" },
    })

    for index, rowFrame in ipairs(panel.rows or {}) do
        rowFrame:Hide()
    end

    for index = 1, math.min(#rows, 5) do
        local rowData = rows[index]
        local rowFrame = panel.rows[index]
        if not rowFrame then
            rowFrame = GH3_MythicPlusCreatePreviewSurface(panel, { x = 16, y = -80 - ((index - 1) * 42), w = 426, h = 42 }, "MyToonsRowBackground")
            panel.rows[index] = rowFrame
            rowFrame.name = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.name:SetPoint("LEFT", rowFrame, "LEFT", 12, 0)
            rowFrame.name:SetWidth(170)
            rowFrame.name:SetJustifyH("LEFT")
            rowFrame.start = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.start:SetPoint("LEFT", rowFrame, "LEFT", 210, 0)
            rowFrame["end"] = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame["end"]:SetPoint("LEFT", rowFrame, "LEFT", 294, 0)
            rowFrame.gain = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.gain:SetPoint("LEFT", rowFrame, "LEFT", 370, 0)
        end

        local colorR, colorG, colorB = getClassColor(rowData.classFile)
        rowFrame.name:SetText(rowData.shortName or "Unknown")
        rowFrame.name:SetTextColor(colorR, colorG, colorB, 1)
        rowFrame.start:SetText(rowData.startScore and tostring(math.floor(rowData.startScore + 0.5)) or "-")
        rowFrame["end"]:SetText(rowData.endScore and tostring(math.floor(rowData.endScore + 0.5)) or "-")

        if type(rowData.delta) == "number" then
            rowFrame.gain:SetText(string.format("%+d", rowData.delta))
            rowFrame.gain:SetTextColor(rowData.delta > 0 and 0.78 or 0.96, rowData.delta > 0 and 0.96 or 0.90, rowData.delta > 0 and 0.72 or 0.82, 1)
        else
            rowFrame.gain:SetText("-")
            rowFrame.gain:SetTextColor(0.96, 0.90, 0.82, 1)
        end

        rowFrame:Show()
    end
end

function GH3_MythicPlusRenderPreviewSummaryLootPanel(panel, runtime, run)
    local rows = buildSummaryLootRows(runtime, run)
    GH3_MythicPlusEnsurePreviewPanelColumns(panel, {
        { x = 12, text = "Player" },
        { x = 150, text = "Item" },
        { x = 374, text = "iLvl" },
    })

    for index, rowFrame in ipairs(panel.rows or {}) do
        rowFrame:Hide()
    end

    for index, rowData in ipairs(rows) do
        local rowFrame = panel.rows[index]
        if not rowFrame then
            rowFrame = GH3_MythicPlusCreatePreviewSurface(panel, { x = 16, y = -80 - ((index - 1) * 42), w = 426, h = 42 }, "MyToonsRowBackground")
            panel.rows[index] = rowFrame
        end

        if not rowFrame.name then
            rowFrame.name = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.name:SetPoint("LEFT", rowFrame, "LEFT", 12, 0)
            rowFrame.name:SetWidth(120)
            rowFrame.name:SetJustifyH("LEFT")
            GH3_MythicPlusConfigureSingleLineText(rowFrame.name)
        end

        if not rowFrame.item then
            rowFrame.item = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.item:SetPoint("LEFT", rowFrame, "LEFT", 150, 0)
            rowFrame.item:SetWidth(196)
            rowFrame.item:SetJustifyH("LEFT")
            GH3_MythicPlusConfigureSingleLineText(rowFrame.item)
        end

        if not rowFrame.level then
            rowFrame.level = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.level:SetPoint("LEFT", rowFrame, "LEFT", 374, 0)
            GH3_MythicPlusConfigureSingleLineText(rowFrame.level)
        end

        local colorR, colorG, colorB = getClassColor(rowData.classFile)
        rowFrame.name:SetText(GH3_MythicPlusAbbreviateText(rowData.shortName or "Unknown", 12))
        rowFrame.name:SetTextColor(colorR, colorG, colorB, 1)
        rowFrame.item:SetText(GH3_MythicPlusAbbreviateText(trimString(rowData.itemName) ~= "" and trimString(rowData.itemName) or "Item", 32))
        rowFrame.level:SetText((tonumber(rowData.itemLevel) or 0) > 0 and tostring(tonumber(rowData.itemLevel) or 0) or "-")
        rowFrame:Show()
    end

    if #rows <= 0 then
        local rowFrame = panel.rows[1]
        if not rowFrame then
            rowFrame = GH3_MythicPlusCreatePreviewSurface(panel, { x = 16, y = -80, w = 426, h = 42 }, "MyToonsRowBackground")
            panel.rows[1] = rowFrame
            rowFrame.item = rowFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            rowFrame.item:SetPoint("LEFT", rowFrame, "LEFT", 12, 0)
            rowFrame.item:SetWidth(400)
            rowFrame.item:SetJustifyH("LEFT")
        end
        rowFrame.item:SetText("No gear captured yet.")
        rowFrame:Show()
    end
end

local function renderSummaryScoreTable(panel, run)
    local rows = buildSummaryScoreRows(run)

    resetSummaryTable(panel)

    if #rows <= 0 then
        panel.body:Show()
        panel.body:SetText(colorizeSummaryText("9ba8b8", "Score data unavailable."))
        return
    end

    renderSummaryTable(panel, { "Player", "Start", "End", "Gain" }, rows, { 200, 84, 78, 52 }, 42, function(rowFrame, rowData, widths, contentInset)
        local avatar = ensureSummaryAvatar(rowFrame)
        avatar:ClearAllPoints()
        avatar:SetSize(24, 24)
        avatar:SetPoint("LEFT", rowFrame, "LEFT", contentInset, 0)
        setSummaryAvatar(avatar, rowData.fullName, rowData.classFile)
        avatar:Show()

        local nameText = ensureSummaryText(rowFrame, "nameText", "Fonts\\FRIZQT__.TTF", 12, "")
        nameText:ClearAllPoints()
        nameText:SetPoint("LEFT", avatar, "RIGHT", 8, 0)
        nameText:SetWidth(math.max(widths[1] - 40, 68))
        nameText:SetText(GH3_MythicPlusAbbreviateText(rowData.shortName, 15))
        nameText:SetTextColor(getClassColor(rowData.classFile))
        nameText:Show()

        local startText = ensureSummaryText(rowFrame, "startText", "Fonts\\FRIZQT__.TTF", 11, "")
        startText:ClearAllPoints()
        startText:SetPoint("LEFT", rowFrame, "LEFT", contentInset + widths[1], 0)
        startText:SetWidth(widths[2] - 4)
        startText:SetText(rowData.startScore and tostring(math.floor(rowData.startScore + 0.5)) or "-")
        startText:SetTextColor(0.80, 0.78, 0.74, 0.92)
        startText:Show()

        local endText = ensureSummaryText(rowFrame, "endText", "Fonts\\FRIZQT__.TTF", 11, "")
        endText:ClearAllPoints()
        endText:SetPoint("LEFT", rowFrame, "LEFT", contentInset + widths[1] + widths[2], 0)
        endText:SetWidth(widths[3] - 4)
        endText:SetText(rowData.endScore and tostring(math.floor(rowData.endScore + 0.5)) or "-")
        endText:SetTextColor(0.94, 0.92, 0.86, 1)
        endText:Show()

        local gainText = ensureSummaryText(rowFrame, "gainText", "Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
        gainText:ClearAllPoints()
        gainText:SetPoint("LEFT", rowFrame, "LEFT", contentInset + widths[1] + widths[2] + widths[3], 0)
        gainText:SetWidth(widths[4] - 4)

        if type(rowData.delta) == "number" then
            gainText:SetText(string.format("%+d", rowData.delta))

            if rowData.delta > 0 then
                gainText:SetTextColor(0.48, 0.94, 0.54, 1)
            elseif rowData.delta < 0 then
                gainText:SetTextColor(1, 0.46, 0.42, 1)
            else
                gainText:SetTextColor(0.90, 0.80, 0.56, 1)
            end
        else
            gainText:SetText("-")
            gainText:SetTextColor(0.62, 0.66, 0.74, 0.9)
        end

        gainText:Show()
    end)
end

local function renderSummaryLootTable(panel, runtime, run)
    local rows = buildSummaryLootRows(runtime, run)

    resetSummaryTable(panel)

    if #rows <= 0 then
        panel.body:Show()
        panel.body:SetText(colorizeSummaryText("9ba8b8", "No gear captured yet."))
        return
    end

    renderSummaryTable(panel, { "Player", "Item", "iLvl" }, rows, { 138, 224, 52 }, 42, function(rowFrame, rowData, widths, contentInset)
        local avatar = ensureSummaryAvatar(rowFrame)
        avatar:ClearAllPoints()
        avatar:SetSize(24, 24)
        avatar:SetPoint("LEFT", rowFrame, "LEFT", contentInset, 0)
        setSummaryAvatar(avatar, rowData.fullName or rowData.shortName, rowData.classFile)
        avatar:Show()

        local nameText = ensureSummaryText(rowFrame, "nameText", "Fonts\\FRIZQT__.TTF", 11, "")
        nameText:ClearAllPoints()
        nameText:SetPoint("LEFT", avatar, "RIGHT", 8, 0)
        nameText:SetWidth(math.max(widths[1] - 40, 44))
        nameText:SetText(GH3_MythicPlusAbbreviateText(rowData.shortName, 8))
        nameText:SetTextColor(getClassColor(rowData.classFile))
        nameText:Show()

        local itemText = ensureSummaryText(rowFrame, "itemText", "Fonts\\FRIZQT__.TTF", 11, "")
        itemText:ClearAllPoints()
        itemText:SetPoint("LEFT", rowFrame, "LEFT", contentInset + widths[1], 0)
        itemText:SetWidth(widths[2] - 6)
        GH3_MythicPlusConfigureSingleLineText(itemText)
        itemText:SetText(GH3_MythicPlusAbbreviateText(rowData.itemName, 34))
        itemText:SetJustifyV("MIDDLE")
        itemText:SetTextColor(0.92, 0.92, 0.88, 1)
        itemText:Show()

        local levelText = ensureSummaryText(rowFrame, "levelText", "Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
        levelText:ClearAllPoints()
        levelText:SetPoint("LEFT", rowFrame, "LEFT", contentInset + widths[1] + widths[2], 0)
        levelText:SetWidth(widths[3] - 4)
        levelText:SetText(rowData.itemLevel > 0 and tostring(rowData.itemLevel) or "-")
        levelText:SetTextColor(0.46, 0.78, 1, 1)
        levelText:Show()
    end)
end

local function buildSummaryScoreLines(run)
    local lines = {}
    local seen = {}
    local startSnapshot = run and run.scoresStart or {}
    local endSnapshot = run and run.scoresEnd or {}

    for _, fullName in ipairs(startSnapshot.order or {}) do
        seen[fullName] = true
        local shortName = addon.Store:SplitFullName(fullName, GetRealmName() or "Unknown")
        local startScore = tonumber(startSnapshot.scores and startSnapshot.scores[fullName])
        local endScore = tonumber(endSnapshot.scores and endSnapshot.scores[fullName])
        local displayName = colorizeSummaryText("9fd8ff", shortName)

        if startScore and endScore then
            local delta = math.floor(endScore - startScore + 0.5)
            local deltaHex = delta > 0 and "78ef87" or (delta < 0 and "ff8d8d" or "f0d39a")
            lines[#lines + 1] = string.format("%s  %s -> %s  %s", displayName, colorizeSummaryText("d5c195", math.floor(startScore + 0.5)), colorizeSummaryText("ffffff", math.floor(endScore + 0.5)), colorizeSummaryText(deltaHex, string.format("%+d", delta)))
        elseif endScore then
            lines[#lines + 1] = string.format("%s  %s", displayName, colorizeSummaryText("ffffff", math.floor(endScore + 0.5)))
        elseif startScore then
            lines[#lines + 1] = string.format("%s  %s -> %s", displayName, colorizeSummaryText("d5c195", math.floor(startScore + 0.5)), colorizeSummaryText("9ba8b8", "pending"))
        else
            lines[#lines + 1] = string.format("%s  %s", displayName, colorizeSummaryText("9ba8b8", "pending"))
        end
    end

    for _, fullName in ipairs(endSnapshot.order or {}) do
        if not seen[fullName] then
            local shortName = addon.Store:SplitFullName(fullName, GetRealmName() or "Unknown")
            local endScore = tonumber(endSnapshot.scores and endSnapshot.scores[fullName])
            local displayName = colorizeSummaryText("9fd8ff", shortName)
            lines[#lines + 1] = endScore
                and string.format("%s  %s", displayName, colorizeSummaryText("ffffff", math.floor(endScore + 0.5)))
                or string.format("%s  %s", displayName, colorizeSummaryText("9ba8b8", "pending"))
        end
    end

    if #lines <= 0 then
        lines[#lines + 1] = colorizeSummaryText("9ba8b8", "Score data unavailable.")
    end

    return lines
end

function addon.MythicPlusService:BuildMythicPlusCompletionSummaryLines()
    local runtime = self:GetRuntimeState().summary
    local run = runtime.run or {}
    local mapName = trimString(run.mapName) ~= "" and trimString(run.mapName) or "Mythic+"
    local level = math.max(0, math.floor(tonumber(run.keyLevel) or 0))
    local timeLimit = tonumber(run.timeLimit) or 0
    local duration = tonumber(run.duration) or 0
    local upgradeLevels = clampUpgradeLevels(run.upgradeLevels)
    local timed = run.onTime and true or false
    local resultText = timed and string.format("Timed +%d", math.max(1, upgradeLevels)) or "Not timed"
    local lines = {}

    lines[#lines + 1] = string.format("Run result  %s", resultText)

    if duration > 0 and timeLimit > 0 then
        lines[#lines + 1] = string.format("Timer       %s / %s", formatRunClock(duration), formatRunClock(timeLimit))
    elseif duration > 0 then
        lines[#lines + 1] = string.format("Timer       %s", formatRunClock(duration))
    end

    lines[#lines + 1] = string.format("Deaths      %d", math.max(0, math.floor(tonumber(run.deaths) or 0)))
    lines[#lines + 1] = string.format("New key     %s", formatSummaryKey(run.newKey))
    lines[#lines + 1] = ""
    lines[#lines + 1] = "Party score"
    lines[#lines + 1] = "------------------------------"

    for _, line in ipairs(buildSummaryScoreLines(run)) do
        lines[#lines + 1] = line
    end

    lines[#lines + 1] = ""
    lines[#lines + 1] = "Loot"
    lines[#lines + 1] = "------------------------------"

    if #runtime.loot <= 0 then
        lines[#lines + 1] = "  No gear captured yet."
    else
        for _, lootRow in ipairs(runtime.loot) do
            local ownerName = lootRow.owner ~= "Unknown" and addon.Store:SplitFullName(lootRow.owner, GetRealmName() or "Unknown") or "Unknown"
            local itemText = trimString(lootRow.itemLink) ~= "" and trimString(lootRow.itemLink) or trimString(lootRow.itemName)
            local itemLevel = math.max(0, math.floor(tonumber(lootRow.itemLevel) or 0))

            if itemLevel > 0 then
                lines[#lines + 1] = string.format("  %s: %s (iLvl %d)", ownerName, itemText, itemLevel)
            else
                lines[#lines + 1] = string.format("  %s: %s", ownerName, itemText)
            end
        end
    end

    return lines, string.format("+%d %s Summary", level, mapName)
end

function addon.MythicPlusService:RenderMythicPlusCompletionSummary()
    local frame = self:EnsureMythicPlusSummaryFrame()
    local runtime = self:GetRuntimeState().summary
    local run = runtime.run or {}
    local _, title = self:BuildMythicPlusCompletionSummaryLines()
    local level = math.max(0, math.floor(tonumber(run.keyLevel) or 0))
    local upgradeLevels = clampUpgradeLevels(run.upgradeLevels)
    local timed = run.onTime and true or false
    local duration = tonumber(run.duration) or 0
    local timeLimit = tonumber(run.timeLimit) or 0
    local resultText = timed and string.format("Timed +%d", math.max(1, upgradeLevels)) or "Depleted"
    local subtitleTimer = duration > 0 and formatRunClock(duration) or "Timer pending"
    local deaths = math.max(0, math.floor(tonumber(run.deaths) or 0))

    frame.title:SetText(title)
    frame.subtitle:SetText(string.format("%s / %s / %d deaths / next key %s", resultText, subtitleTimer, deaths, formatSummaryKey(run.newKey, false)))
    frame.subtitle:Hide()
    GH3_MythicPlusHideDungeonArt(frame)

    frame.title:SetTextColor(timed and 0.98 or 1, timed and 0.86 or 0.80, timed and 0.44 or 0.76, 1)

    frame.resultCard.Label:SetText("RESULT")
    frame.resultCard.Value:SetText(resultText)
    if frame.resultCard.Icon then
        frame.resultCard.Icon:SetTexture(timed and MEDIA_TEXTURES.iconReady or MEDIA_TEXTURES.iconNotReady)
    end

    frame.timerCard.Label:SetText("TIMER")
    if frame.timerCard.Icon then
        frame.timerCard.Icon:SetTexture(MEDIA_TEXTURES.iconTimer)
    end
    if duration > 0 and timeLimit > 0 then
        frame.timerCard.Value:SetText(string.format("%s / %s", formatRunClock(duration), formatRunClock(timeLimit)))
    elseif duration > 0 then
        frame.timerCard.Value:SetText(formatRunClock(duration))
    else
        frame.timerCard.Value:SetText("Pending")
    end

    frame.deathsCard.Label:SetText("DEATHS")
    frame.deathsCard.Value:SetText(tostring(deaths))
    if frame.deathsCard.Icon then
        frame.deathsCard.Icon:SetTexture(MEDIA_TEXTURES.iconDeath)
    end

    frame.keyCard.Label:SetText("NEW KEY")
    frame.keyCard.Value:SetText(formatSummaryKey(run.newKey, false))
    if frame.keyCard.Icon then
        frame.keyCard.Icon:SetTexture(MEDIA_TEXTURES.iconMythicPlus)
    end

    for _, rowFrame in ipairs(frame.scorePanel.rows or {}) do
        rowFrame:Hide()
    end

    for _, rowFrame in ipairs(frame.lootPanel.rows or {}) do
        rowFrame:Hide()
    end

    renderSummaryScoreTable(frame.scorePanel, run)
    renderSummaryLootTable(frame.lootPanel, runtime, run)

    setBlizzardMythicSummarySuppressed(true)
    frame:Show()
    return true
end

function addon.MythicPlusService:RefreshMythicPlusCompletionSummary(reason)
    local runtime = self:GetRuntimeState().summary
    local run = runtime.run

    if runtime.isPreview then
        return false
    end

    if type(run) ~= "table" or (tonumber(runtime.completedAt) or 0) <= 0 then
        return false
    end

    if math.max(0, math.floor(tonumber(runtime.dismissedToken) or 0)) == math.max(0, math.floor(tonumber(runtime.token) or 0)) then
        return false
    end

    local deaths = getOverlayDeathCount()
    run.deaths = math.max(tonumber(run.deaths) or 0, deaths or 0)
    run.newKey = getLiveCurrentKeyDetails()
    run.scoresEnd = capturePartyScoreSnapshot()
    run.lastRefreshReason = trimString(reason)
    return self:RenderMythicPlusCompletionSummary()
end

function addon.MythicPlusService:QueueMythicPlusCompletionSummary(completionInfo)
    local runtime = self:GetRuntimeState().summary

    if not runtime.active or type(runtime.run) ~= "table" then
        self:HandleChallengeModeStarted("completion")
    end

    local run = runtime.run or {}
    local mapId = extractMapId(completionInfo) or run.mapId
    local normalizedMapId = normalizeDungeonId(mapId and tostring(mapId) or nil)
    local completedLevel = math.max(0, math.floor(extractRunLevel(completionInfo) or tonumber(run.keyLevel) or 0))
    local duration = extractDurationSeconds(completionInfo) or ((tonumber(run.startClock) or 0) > 0 and (getClockSeconds() - (tonumber(run.startClock) or 0)) or 0)
    local onTime = extractTimedFlag(completionInfo, false)
    local timeLimit = normalizedMapId and (tonumber(getDungeonTimeLimitSeconds(normalizedMapId)) or tonumber(run.timeLimit) or 0) or (tonumber(run.timeLimit) or 0)

    if (tonumber(duration) or 0) > 10000 and (timeLimit <= 0 or (tonumber(duration) or 0) > (timeLimit * 5)) then
        duration = (tonumber(duration) or 0) / 1000
    end

    if onTime == nil then
        onTime = inferTimedFromDuration(normalizedMapId, duration, nil)
    end

    local upgradeLevels = extractUpgradeLevels(completionInfo) or inferUpgradeLevelsFromDuration(normalizedMapId, duration, nil, onTime)
    local deaths = getOverlayDeathCount()

    run.completedAt = time()
    run.completionInfo = completionInfo
    run.deaths = deaths
    run.duration = math.max(0, tonumber(duration) or 0)
    run.keyLevel = completedLevel
    run.mapId = normalizedMapId or run.mapId
    run.mapName = normalizedMapId and getMapName(normalizedMapId) or trimString(run.mapName) ~= "" and run.mapName or "Mythic+"
    run.newKey = getLiveCurrentKeyDetails()
    run.onTime = onTime and true or false
    run.scoresEnd = capturePartyScoreSnapshot()
    run.timeLimit = timeLimit
    run.upgradeLevels = clampUpgradeLevels(upgradeLevels or (onTime and 1 or 0))
    runtime.active = false
    runtime.completedAt = run.completedAt
    runtime.dismissedToken = 0
    runtime.run = run
    runtime.token = runtime.token + 1

    local token = runtime.token
    self:RenderMythicPlusCompletionSummary()

    if C_Timer and C_Timer.After then
        C_Timer.After(1.5, function()
            if self:GetRuntimeState().summary.token == token then
                self:RefreshMythicPlusCompletionSummary("completion-delay-1")
            end
        end)
        C_Timer.After(5, function()
            if self:GetRuntimeState().summary.token == token then
                self:RefreshMythicPlusCompletionSummary("completion-delay-2")
            end
        end)
        C_Timer.After(12, function()
            if self:GetRuntimeState().summary.token == token then
                self:RefreshMythicPlusCompletionSummary("completion-delay-3")
            end
        end)
    end

    return true
end

function addon.MythicPlusService:HandleBagUpdateDelayed()
    if self:GetRuntimeState().summary.isPreview then
        return false
    end

    if self:IsChallengeModeActive() then
        return false
    end

    local changed = self:RefreshCurrentCharacter() and true or false

    if self:RefreshMythicPlusCompletionSummary("bag") then
        changed = true
    end

    return changed
end

function addon.MythicPlusService:GetSeasonId()
    return self:GetUIState().seasonId or DEFAULT_SEASON_ID
end

function addon.MythicPlusService:GetSeasonDefinition(seasonId)
    local resolvedSeasonId = trimString(seasonId or self:GetSeasonId())

    if resolvedSeasonId == "" then
        resolvedSeasonId = DEFAULT_SEASON_ID
    end

    local definition = deepCopy(SEASON_DEFINITIONS[resolvedSeasonId] or SEASON_DEFINITIONS[DEFAULT_SEASON_ID] or {})
    definition.id = trimString(definition.id) ~= "" and trimString(definition.id) or resolvedSeasonId
    definition.name = trimString(definition.name) ~= "" and trimString(definition.name) or "Current Season"
    definition.portalThreshold = tonumber(definition.portalThreshold) or DEFAULT_PORTAL_THRESHOLD
    definition.dungeons = type(definition.dungeons) == "table" and definition.dungeons or {}
    return definition
end

function addon.MythicPlusService:GetSeasonDungeonCatalog(seasonId)
    local definition = self:GetSeasonDefinition(seasonId)
    local entries = {}
    local seen = {}

    local function addEntry(dungeonId, name, shortName, displayOrder)
        local normalizedDungeonId = normalizeDungeonId(dungeonId)

        if not normalizedDungeonId or seen[normalizedDungeonId] then
            return
        end

        seen[normalizedDungeonId] = true
        entries[#entries + 1] = {
            displayOrder = tonumber(displayOrder) or (#entries + 1),
            id = normalizedDungeonId,
            name = trimString(name) ~= "" and trimString(name) or getMapName(normalizedDungeonId),
            shortName = trimString(shortName) ~= "" and trimString(shortName) or nil,
        }
    end

    for index, dungeon in ipairs(definition.dungeons) do
        if type(dungeon) == "table" then
            addEntry(dungeon.id, dungeon.name, dungeon.shortName, dungeon.displayOrder or index)
        end
    end

    local liveMapIds = safeInvoke(C_ChallengeMode, "GetMapTable")

    if type(liveMapIds) == "table" then
        for index, mapId in ipairs(liveMapIds) do
            addEntry(tostring(mapId), getMapName(mapId), nil, 1000 + index)
        end
    end

    if #entries == 0 then
        for index, dungeonId in ipairs(FALLBACK_SEASON_DUNGEON_IDS) do
            addEntry(dungeonId, getFallbackSeasonDungeonName(dungeonId), nil, 2000 + index)
        end
    end

    sortDungeonCatalogEntries(entries)
    definition.dungeons = entries
    return definition
end

function addon.MythicPlusService:GetStore()
    return addon.Store:GetMythics()
end

function addon.MythicPlusService:GetProfile(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" then
        return nil
    end

    local store = self:GetStore()

    if type(store[normalizedToonId]) ~= "table" then
        store[normalizedToonId] = buildEmptyMythicProfile(normalizedToonId)
    end

    local profile = store[normalizedToonId]
    profile.id = normalizedToonId
    profile.owner = profile.owner or normalizedToonId
    profile.seasons = profile.seasons or {}
    profile.updatedAt = tonumber(profile.updatedAt) or 0
    profile.revision = tonumber(profile.revision) or 0

    return profile
end

function addon.MythicPlusService:FindStoredProfileKey(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" then
        return nil
    end

    local store = self:GetStore()

    if type(store[normalizedToonId]) == "table" then
        return normalizedToonId
    end

    local toonName, toonRealm = addon.Store:SplitFullName(normalizedToonId, GetRealmName() or "Unknown")
    local fallbackKey = nil
    local fallbackCount = 0

    for candidateKey, profile in pairs(store or {}) do
        if type(profile) == "table" then
            local normalizedCandidate = addon.Store:NormalizeFullName(candidateKey, GetRealmName() or "Unknown")
            local candidateName, candidateRealm = addon.Store:SplitFullName(normalizedCandidate, GetRealmName() or "Unknown")

            if lowerText(candidateName) == lowerText(toonName) then
                if lowerText(candidateRealm) == lowerText(toonRealm) then
                    return candidateKey
                end

                fallbackCount = fallbackCount + 1

                if fallbackCount == 1 then
                    fallbackKey = candidateKey
                end
            end
        end
    end

    if fallbackCount == 1 then
        return fallbackKey
    end

    return normalizedToonId
end

function addon.MythicPlusService:GetStoredProfile(toonId)
    local normalizedToonId = self:FindStoredProfileKey(toonId)

    if not normalizedToonId or normalizedToonId == "" then
        return nil
    end

    local store = self:GetStore()
    local profile = store[normalizedToonId]

    if type(profile) ~= "table" then
        return nil
    end

    return profile
end

function addon.MythicPlusService:GetSeasonProfile(toonId, seasonId)
    local profile = self:GetProfile(toonId)

    if not profile then
        return nil
    end

    local resolvedSeasonId = trimString(seasonId or self:GetSeasonId())

    if resolvedSeasonId == "" then
        resolvedSeasonId = "current"
    end

    if type(profile.seasons[resolvedSeasonId]) ~= "table" then
        profile.seasons[resolvedSeasonId] = buildEmptySeasonProfile()
    end

    local seasonProfile = profile.seasons[resolvedSeasonId]
    seasonProfile.currentKey = seasonProfile.currentKey or { dungeonId = nil, level = 0 }
    seasonProfile.dungeons = seasonProfile.dungeons or {}
    seasonProfile.lastCaptureAttemptAt = tonumber(seasonProfile.lastCaptureAttemptAt) or 0
    seasonProfile.lastCaptureCount = tonumber(seasonProfile.lastCaptureCount) or 0
    seasonProfile.lastCaptureReason = trimString(seasonProfile.lastCaptureReason)
    seasonProfile.lastCaptureSource = trimString(seasonProfile.lastCaptureSource)
    seasonProfile.lastSuccessfulCaptureAt = tonumber(seasonProfile.lastSuccessfulCaptureAt) or 0
    seasonProfile.overallRating = tonumber(seasonProfile.overallRating) or 0
    seasonProfile.portalThreshold = tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD
    seasonProfile.updatedAt = tonumber(seasonProfile.updatedAt) or 0
    return seasonProfile
end

function addon.MythicPlusService:GetStoredSeasonProfile(toonId, seasonId)
    local profile = self:GetStoredProfile(toonId)

    if type(profile) ~= "table" or type(profile.seasons) ~= "table" then
        return nil
    end

    local resolvedSeasonId = trimString(seasonId or self:GetSeasonId())

    if resolvedSeasonId == "" then
        resolvedSeasonId = "current"
    end

    local seasonProfile = profile.seasons[resolvedSeasonId]

    if type(seasonProfile) ~= "table" then
        return nil
    end

    seasonProfile.currentKey = seasonProfile.currentKey or { dungeonId = nil, level = 0 }
    seasonProfile.dungeons = seasonProfile.dungeons or {}
    seasonProfile.lastCaptureAttemptAt = tonumber(seasonProfile.lastCaptureAttemptAt) or 0
    seasonProfile.lastCaptureCount = tonumber(seasonProfile.lastCaptureCount) or 0
    seasonProfile.lastCaptureReason = trimString(seasonProfile.lastCaptureReason)
    seasonProfile.lastCaptureSource = trimString(seasonProfile.lastCaptureSource)
    seasonProfile.lastSuccessfulCaptureAt = tonumber(seasonProfile.lastSuccessfulCaptureAt) or 0
    seasonProfile.portalThreshold = tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD
    return seasonProfile
end

function addon.MythicPlusService:GetStoredDungeonRecord(toonId, dungeonId)
    local seasonProfile = self:GetStoredSeasonProfile(toonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not seasonProfile or not normalizedDungeonId then
        return nil
    end

    local record = seasonProfile.dungeons and seasonProfile.dungeons[normalizedDungeonId] or nil

    if type(record) ~= "table" then
        return nil
    end

    record.name = trimString(record.name) ~= "" and record.name or getMapName(normalizedDungeonId)
    record.bestLevel = tonumber(record.bestLevel) or 0
    record.bestTimedLevel = tonumber(record.bestTimedLevel) or 0
    record.bestLevelUpgrades = clampUpgradeLevels(record.bestLevelUpgrades)
    record.fortifiedScore = tonumber(record.fortifiedScore) or 0
    record.tyrannicalScore = tonumber(record.tyrannicalScore) or 0
    record.score = tonumber(record.score) or 0
    record.portalUnlocked = record.portalUnlocked and true or false

    if record.bestTimedLevel > record.bestLevel then
        record.bestLevel = record.bestTimedLevel
    end

    return record
end

function addon.MythicPlusService:GetDungeonRecord(toonId, dungeonId)
    local seasonProfile = self:GetSeasonProfile(toonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not seasonProfile or not normalizedDungeonId then
        return nil
    end

    if type(seasonProfile.dungeons[normalizedDungeonId]) ~= "table" then
        seasonProfile.dungeons[normalizedDungeonId] = {
            bestLevel = 0,
            bestTimedLevel = 0,
            bestLevelUpgrades = 0,
            fortifiedScore = 0,
            name = getMapName(normalizedDungeonId),
            portalUnlocked = false,
            score = 0,
            tyrannicalScore = 0,
        }
    end

    local record = seasonProfile.dungeons[normalizedDungeonId]
    record.name = trimString(record.name) ~= "" and record.name or getMapName(normalizedDungeonId)
    record.bestLevel = tonumber(record.bestLevel) or 0
    record.bestTimedLevel = tonumber(record.bestTimedLevel) or 0
    record.bestLevelUpgrades = clampUpgradeLevels(record.bestLevelUpgrades)
    record.fortifiedScore = tonumber(record.fortifiedScore) or 0
    record.tyrannicalScore = tonumber(record.tyrannicalScore) or 0
    record.score = tonumber(record.score) or 0
    record.portalUnlocked = record.portalUnlocked and true or false

    if record.bestTimedLevel > record.bestLevel then
        record.bestLevel = record.bestTimedLevel
    end

    return record
end

function addon.MythicPlusService:ApplyDungeonUpdate(toonId, dungeonId, payload)
    toonId = toonId or addon.Store:GetPlayerName()
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId or type(payload) ~= "table" then
        return false
    end

    local profile = self:GetProfile(toonId)
    local seasonProfile = self:GetSeasonProfile(toonId)

    if not profile or not seasonProfile then
        return false
    end

    local record = self:GetDungeonRecord(toonId, normalizedDungeonId)
    local changed = false

    if payload.name ~= nil then
        local nextName = trimString(payload.name)

        if nextName ~= "" and record.name ~= nextName then
            record.name = nextName
            changed = true
        end
    end

    if payload.bestLevel ~= nil then
        local bestLevel = math.max(0, math.floor(tonumber(payload.bestLevel) or 0))
        local inferredUpgrades = nil

        if payload.bestLevelUpgrades ~= nil then
            inferredUpgrades = clampUpgradeLevels(payload.bestLevelUpgrades)
        elseif bestLevel > 0 and payload.bestTimedLevel ~= nil and math.max(0, math.floor(tonumber(payload.bestTimedLevel) or 0)) >= bestLevel then
            inferredUpgrades = 1
        elseif bestLevel > 0 then
            inferredUpgrades = 0
        end

        if bestLevel > record.bestLevel then
            record.bestLevel = bestLevel
            changed = true

            if inferredUpgrades ~= nil and record.bestLevelUpgrades ~= inferredUpgrades then
                record.bestLevelUpgrades = inferredUpgrades
                changed = true
            end
        elseif bestLevel == record.bestLevel and inferredUpgrades ~= nil then
            if inferredUpgrades == 0 and record.bestLevelUpgrades ~= 0 and payload.bestTimedLevel ~= nil and math.max(0, math.floor(tonumber(payload.bestTimedLevel) or 0)) < bestLevel then
                record.bestLevelUpgrades = 0
                changed = true
            elseif inferredUpgrades > record.bestLevelUpgrades then
                record.bestLevelUpgrades = inferredUpgrades
                changed = true
            end
        end
    end

    if payload.bestTimedLevel ~= nil then
        local bestTimedLevel = math.max(0, math.floor(tonumber(payload.bestTimedLevel) or 0))

        if bestTimedLevel > record.bestTimedLevel then
            record.bestTimedLevel = bestTimedLevel
            changed = true
        end
    end

    if payload.bestLevelUpgrades ~= nil and payload.bestLevel == nil then
        local bestLevelUpgrades = clampUpgradeLevels(payload.bestLevelUpgrades)

        if bestLevelUpgrades > record.bestLevelUpgrades then
            record.bestLevelUpgrades = bestLevelUpgrades
            changed = true
        end
    end

    if payload.score ~= nil then
        local totalScore = math.max(0, tonumber(payload.score) or 0)

        if totalScore > record.score then
            record.score = totalScore
            changed = true
        end
    end

    if payload.fortifiedScore ~= nil then
        local fortifiedScore = math.max(0, tonumber(payload.fortifiedScore) or 0)

        if fortifiedScore > record.fortifiedScore then
            record.fortifiedScore = fortifiedScore
            changed = true
        end
    end

    if payload.tyrannicalScore ~= nil then
        local tyrannicalScore = math.max(0, tonumber(payload.tyrannicalScore) or 0)

        if tyrannicalScore > record.tyrannicalScore then
            record.tyrannicalScore = tyrannicalScore
            changed = true
        end
    end

    if payload.portalUnlocked ~= nil then
        local portalUnlocked = payload.portalUnlocked and true or false

        if record.portalUnlocked ~= portalUnlocked then
            record.portalUnlocked = portalUnlocked
            changed = true
        end
    end

    if record.score <= 0 then
        local combinedScore = (tonumber(record.fortifiedScore) or 0) + (tonumber(record.tyrannicalScore) or 0)

        if combinedScore > 0 then
            record.score = combinedScore
            changed = true
        end
    end

    if record.bestTimedLevel >= (tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD) and not record.portalUnlocked then
        record.portalUnlocked = true
        changed = true
    end

    if changed then
        self:TouchProfile(profile, seasonProfile)
    end

    return changed
end

function addon.MythicPlusService:ReplaceLiveDungeonRecord(toonId, dungeonId, payload)
    toonId = toonId or addon.Store:GetPlayerName()
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId or type(payload) ~= "table" then
        return false
    end

    local profile = self:GetProfile(toonId)
    local seasonProfile = self:GetSeasonProfile(toonId)

    if not profile or not seasonProfile then
        return false
    end

    local record = self:GetDungeonRecord(toonId, normalizedDungeonId)
    local nextName = trimString(payload.name) ~= "" and trimString(payload.name) or getMapName(normalizedDungeonId)
    local nextBestLevel = math.max(0, math.floor(tonumber(payload.bestLevel) or 0))
    local nextBestTimedLevel = math.max(0, math.floor(tonumber(payload.bestTimedLevel) or 0))
    local nextBestLevelUpgrades = clampUpgradeLevels(payload.bestLevelUpgrades)
    local nextScore = math.max(0, tonumber(payload.score) or 0)
    local nextFortifiedScore = math.max(0, tonumber(payload.fortifiedScore) or 0)
    local nextTyrannicalScore = math.max(0, tonumber(payload.tyrannicalScore) or 0)
    local nextPortalUnlocked = payload.portalUnlocked and true or false

    if nextBestTimedLevel >= (tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD) then
        nextPortalUnlocked = true
    end

    local changed = false

    if record.name ~= nextName then
        record.name = nextName
        changed = true
    end

    if record.bestLevel ~= nextBestLevel then
        record.bestLevel = nextBestLevel
        changed = true
    end

    if record.bestTimedLevel ~= nextBestTimedLevel then
        record.bestTimedLevel = nextBestTimedLevel
        changed = true
    end

    if record.bestLevelUpgrades ~= nextBestLevelUpgrades then
        record.bestLevelUpgrades = nextBestLevelUpgrades
        changed = true
    end

    if record.score ~= nextScore then
        record.score = nextScore
        changed = true
    end

    if record.fortifiedScore ~= nextFortifiedScore then
        record.fortifiedScore = nextFortifiedScore
        changed = true
    end

    if record.tyrannicalScore ~= nextTyrannicalScore then
        record.tyrannicalScore = nextTyrannicalScore
        changed = true
    end

    if record.portalUnlocked ~= nextPortalUnlocked then
        record.portalUnlocked = nextPortalUnlocked
        changed = true
    end

    if changed then
        self:TouchProfile(profile, seasonProfile)
    end

    return changed
end

function addon.MythicPlusService:ClearLiveDungeonRecord(toonId, dungeonId)
    toonId = toonId or addon.Store:GetPlayerName()
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId then
        return false
    end

    local profile = self:GetProfile(toonId)
    local seasonProfile = self:GetSeasonProfile(toonId)

    if not profile or not seasonProfile or type(seasonProfile.dungeons) ~= "table" or type(seasonProfile.dungeons[normalizedDungeonId]) ~= "table" then
        return false
    end

    seasonProfile.dungeons[normalizedDungeonId] = nil
    self:TouchProfile(profile, seasonProfile)
    return true
end

function addon.MythicPlusService:TrackCurrentKey(toonId)
    toonId = toonId or addon.Store:GetPlayerName()
    local currentDungeonId = select(1, getLiveCurrentKey())

    if not currentDungeonId then
        return false, "No current keystone was found."
    end

    self:ApplyDungeonUpdate(toonId, currentDungeonId, {
        name = getMapName(currentDungeonId),
    })
    self:SetSelectedPlannerDungeonId(currentDungeonId)
    return true, currentDungeonId
end

function addon.MythicPlusService:DeleteDungeonRecord(toonId, dungeonId)
    toonId = toonId or addon.Store:GetPlayerName()
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local profile = self:GetProfile(toonId)
    local seasonProfile = self:GetSeasonProfile(toonId)

    if not normalizedDungeonId or not profile or not seasonProfile then
        return false
    end

    if type(seasonProfile.dungeons[normalizedDungeonId]) ~= "table" then
        return false
    end

    seasonProfile.dungeons[normalizedDungeonId] = nil
    self:TouchProfile(profile, seasonProfile)

    local state = self:GetUIState()

    if state.planner.selectedDungeonId == normalizedDungeonId then
        state.planner.selectedDungeonId = nil
    end

    return true
end

function addon.MythicPlusService:GetDungeonEditorState(toonId, dungeonId)
    toonId = toonId or addon.Store:GetPlayerName()
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId then
        return nil
    end

    local record = self:GetDungeonRecord(toonId, normalizedDungeonId)

    if not record then
        return nil
    end

    return {
        bestLevel = tonumber(record.bestLevel) or 0,
        bestLevelUpgrades = clampUpgradeLevels(record.bestLevelUpgrades),
        fortifiedScore = tonumber(record.fortifiedScore) or 0,
        id = normalizedDungeonId,
        name = record.name or getMapName(normalizedDungeonId),
        portalUnlocked = record.portalUnlocked and true or false,
        tyrannicalScore = tonumber(record.tyrannicalScore) or 0,
    }
end

function addon.MythicPlusService:CaptureLiveSeasonData(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" or normalizedToonId ~= addon.Store:GetPlayerName() then
        return false, 0
    end

    local capturedDungeons = {}
    local changed = false
    requestLiveMythicData()
    local ratingSummaryRuns = getLiveMythicRatingSummaryRuns()
    local hasCharacterRatingSummaryRuns = type(ratingSummaryRuns) == "table"
    local runHistory = safeInvoke(C_MythicPlus, "GetRunHistory")
    local hadRunHistoryEntries = false

    if hasCharacterRatingSummaryRuns then
        for _, runEntry in ipairs(ratingSummaryRuns) do
            local dungeonId, update = buildLiveDungeonUpdateFromRun(runEntry)

            if dungeonId and update then
                if self:ReplaceLiveDungeonRecord(normalizedToonId, dungeonId, update) then
                    changed = true
                end

                capturedDungeons[dungeonId] = true
            end
        end
    elseif type(runHistory) == "table" then
        for _, runEntry in ipairs(runHistory) do
            if type(runEntry) == "table" then
                hadRunHistoryEntries = true
            end

            local dungeonId, update = buildLiveDungeonUpdateFromRun(runEntry)

            if dungeonId and update then
                if self:ApplyDungeonUpdate(normalizedToonId, dungeonId, update) then
                    changed = true
                end

                capturedDungeons[dungeonId] = true
            end
        end
    end

    local mapIds = safeInvoke(C_ChallengeMode, "GetMapTable")
    local hadSeasonMapTable = type(mapIds) == "table" and #mapIds > 0
    local seasonMapIds = {}
    local hadAuthoritativeSeasonResponse = hasCharacterRatingSummaryRuns

    if type(mapIds) == "table" then
        for _, mapId in ipairs(mapIds) do
            local normalizedMapId = normalizeDungeonId(tostring(mapId))

            if normalizedMapId then
                seasonMapIds[normalizedMapId] = true
            end

            if not hasCharacterRatingSummaryRuns then
                local bestInfo = safeInvoke(C_MythicPlus, "GetSeasonBestForMap", mapId)
                local affixScores, bestOverallScore = safeInvoke(C_MythicPlus, "GetSeasonBestAffixScoreInfoForMap", mapId)
                local update = {
                    name = getMapName(mapId),
                }
                local hasMeaningfulValue = false

                if (type(bestInfo) == "table" and countTableEntries(bestInfo) > 0)
                    or (type(affixScores) == "table" and countTableEntries(affixScores) > 0)
                    or tonumber(bestOverallScore) ~= nil then
                    hadAuthoritativeSeasonResponse = true
                end

                if type(bestInfo) == "table" then
                    local bestLevel = extractRunLevel(bestInfo)
                    local score = extractRunScore(bestInfo)
                    local durationSeconds = extractDurationSeconds(bestInfo)
                    local timed = inferTimedFromDuration(mapId, durationSeconds, extractTimedFlag(bestInfo, false))
                    local upgradeLevels = inferUpgradeLevelsFromDuration(mapId, durationSeconds, extractUpgradeLevels(bestInfo), timed)

                    if bestLevel or score then
                        update.bestLevel = bestLevel
                        update.bestLevelUpgrades = timed ~= false and bestLevel and (upgradeLevels or 1) or 0
                        update.bestTimedLevel = timed ~= false and bestLevel or nil
                        update.score = score
                        hasMeaningfulValue = true
                    end
                end

                if type(affixScores) == "table" then
                    for _, affixInfo in ipairs(affixScores) do
                        if type(affixInfo) == "table" then
                            local affixScore = tonumber(affixInfo.score) or extractRunScore(affixInfo)
                            local affixLevel = tonumber(affixInfo.level) or extractRunLevel(affixInfo)
                            local durationSeconds = extractDurationSeconds(affixInfo)
                            local timed = affixInfo.overTime == nil
                                and inferTimedFromDuration(mapId, durationSeconds, extractTimedFlag(affixInfo, false))
                                or not affixInfo.overTime
                            local upgradeLevels = inferUpgradeLevelsFromDuration(mapId, durationSeconds, extractUpgradeLevels(affixInfo), timed)

                            if affixScore and affixScore > (tonumber(update.score) or 0) then
                                update.score = affixScore
                            end

                            if affixLevel and affixLevel > (tonumber(update.bestLevel) or 0) then
                                update.bestLevel = affixLevel
                                update.bestLevelUpgrades = timed and (upgradeLevels or 1) or 0
                            end

                            if timed and affixLevel and affixLevel > (tonumber(update.bestTimedLevel) or 0) then
                                update.bestTimedLevel = affixLevel
                            end

                            hasMeaningfulValue = hasMeaningfulValue or affixScore ~= nil or affixLevel ~= nil
                        end
                    end

                    if tonumber(bestOverallScore) and tonumber(bestOverallScore) > (tonumber(update.score) or 0) then
                        update.score = tonumber(bestOverallScore)
                        hasMeaningfulValue = true
                    end
                end

                if normalizedMapId and hasMeaningfulValue then
                    if self:ReplaceLiveDungeonRecord(normalizedToonId, normalizedMapId, update) then
                        changed = true
                    end

                    capturedDungeons[normalizedMapId] = true
                end
            end
        end
    end

    if hadSeasonMapTable and hadAuthoritativeSeasonResponse then
        for seasonMapId in pairs(seasonMapIds) do
            if not capturedDungeons[seasonMapId] and self:ClearLiveDungeonRecord(normalizedToonId, seasonMapId) then
                changed = true
            end
        end
    end

    local seasonProfile = self:GetSeasonProfile(normalizedToonId)
    local captureCount = countTableEntries(capturedDungeons)
    local captureReason = ""

    if captureCount <= 0 then
        if not hadSeasonMapTable and not hadRunHistoryEntries then
            captureReason = "Season data is not available from the game client yet."
        elseif not hadRunHistoryEntries and not hasMeaningfulSeasonData(seasonProfile) then
            captureReason = "No Mythic+ runs have been recorded for this toon yet."
        else
            captureReason = "No new per-dungeon Mythic+ records were returned on this refresh."
        end
    end

    setSeasonCaptureState(seasonProfile, "live", captureCount, captureReason, captureCount > 0)

    return changed, captureCount, captureReason
end

function addon.MythicPlusService:HandleChallengeModeCompleted()
    local playerId = addon.Store:GetPlayerName()
    local runtime = self:GetRuntimeState().summary
    local overlayRuntime = self:GetRuntimeState().overlay
    local completionInfo = getChallengeCompletionInfo()
    local resolvedCompletionInfo = buildResolvedChallengeCompletionInfo(runtime.run, completionInfo)

    if type(resolvedCompletionInfo) ~= "table" then
        resolvedCompletionInfo = buildCompletionInfoFromOverlaySnapshot(overlayRuntime and overlayRuntime.lastSnapshot)
    end

    if type(resolvedCompletionInfo) ~= "table" then
        resolvedCompletionInfo = buildCompletionInfoFromSavedActiveRun(overlaySavedState.getSavedActiveRun(self))
    end

    if type(resolvedCompletionInfo) ~= "table" or resolvedCompletionInfo.practiceRun then
        return false
    end

    local mapId = extractMapId(resolvedCompletionInfo)
    local completedLevel = math.max(0, math.floor(extractRunLevel(resolvedCompletionInfo) or 0))
    local onTime = extractTimedFlag(resolvedCompletionInfo, false) and true or false
    local keystoneUpgradeLevels = extractUpgradeLevels(resolvedCompletionInfo)
    local normalizedMapId = normalizeDungeonId(tostring(mapId))
    local changed = false

    if playerId and trimString(playerId) ~= "" and normalizedMapId and completedLevel > 0 then
        changed = self:ApplyDungeonUpdate(playerId, normalizedMapId, {
            bestLevel = completedLevel,
            bestLevelUpgrades = onTime and clampUpgradeLevels(keystoneUpgradeLevels or 1) or 0,
            bestTimedLevel = onTime and completedLevel or nil,
            name = getMapName(normalizedMapId),
            portalUnlocked = onTime and completedLevel >= (tonumber(self:GetSeasonProfile(playerId).portalThreshold) or DEFAULT_PORTAL_THRESHOLD) or nil,
        }) and true or false
    end

    local liveChanged = playerId and trimString(playerId) ~= "" and self:CaptureLiveSeasonData(playerId) or false
    local summaryQueued = self:QueueMythicPlusCompletionSummary(resolvedCompletionInfo) and true or false
    return changed or (liveChanged and true or false) or summaryQueued
end

function addon.MythicPlusService:TouchProfile(profile, seasonProfile)
    local now = time()
    local updatedBy = addon.Store:GetPlayerName()

    if type(seasonProfile) == "table" then
        seasonProfile.updatedAt = now
        seasonProfile.updatedBy = updatedBy
    end

    if type(profile) == "table" then
        profile.updatedAt = now
        profile.updatedBy = updatedBy
        profile.revision = (profile.revision or 0) + 1
    end
end

function addon.MythicPlusService:RefreshCurrentCharacter(force)
    local playerId = addon.Store:GetPlayerName()
    local profile = self:GetProfile(playerId)
    local seasonProfile = self:GetSeasonProfile(playerId)

    if not profile or not seasonProfile then
        return false
    end

    local changed = false
    local liveRating = getLiveCurrentRating()
    local currentDungeonId, currentKeyLevel = getLiveCurrentKey()

    if type(liveRating) == "number" and liveRating >= 0 and seasonProfile.overallRating ~= liveRating then
        seasonProfile.overallRating = liveRating
        changed = true
    end

    if currentDungeonId ~= seasonProfile.currentKey.dungeonId then
        seasonProfile.currentKey.dungeonId = currentDungeonId
        changed = true
    end

    if (tonumber(seasonProfile.currentKey.level) or 0) ~= (tonumber(currentKeyLevel) or 0) then
        seasonProfile.currentKey.level = tonumber(currentKeyLevel) or 0
        changed = true
    end

    if currentDungeonId then
        local dungeonRecord = self:GetDungeonRecord(playerId, currentDungeonId)

        if dungeonRecord and trimString(dungeonRecord.name) == "" then
            dungeonRecord.name = getMapName(currentDungeonId)
            changed = true
        end
    end

    local liveChanged = false
    local liveCaptureCount = 0
    local liveCaptureReason = ""

    if force then
        liveChanged, liveCaptureCount, liveCaptureReason = self:CaptureLiveSeasonData(playerId)
        changed = changed or liveChanged
    else
        local existingSeasonProfile = self:GetSeasonProfile(playerId)

        if existingSeasonProfile and existingSeasonProfile.lastCaptureAttemptAt <= 0 and not hasMeaningfulSeasonData(existingSeasonProfile) then
            setSeasonCaptureState(existingSeasonProfile, "live", 0, "Waiting for the next live Mythic+ refresh window.", false)
        end
    end

    if force and (profile.updatedAt or 0) <= 0 then
        changed = true
    end

    if changed then
        self:TouchProfile(profile, seasonProfile)
    end

    return changed, liveCaptureCount, liveCaptureReason
end

function addon.MythicPlusService:AutoRefreshLiveData()
    local state = self:GetUIState()
    local now = time()
    local lastRefreshAt = tonumber(state.lastLiveRefreshAt) or 0
    local lastLightRefreshAt = tonumber(state.lastLiveLightRefreshAt) or 0
    local shouldForce = (now - lastRefreshAt) >= AUTO_LIVE_REFRESH_COOLDOWN_SECONDS

    if not shouldForce and (now - lastLightRefreshAt) < AUTO_LIVE_LIGHT_REFRESH_COOLDOWN_SECONDS then
        return false, 0, false
    end

    local changed, liveCaptureCount = self:RefreshCurrentCharacter(shouldForce)

    state.lastLiveLightRefreshAt = now

    if shouldForce then
        state.lastLiveRefreshAt = now
    end

    return changed, liveCaptureCount, shouldForce
end

function addon.MythicPlusService:RequestOpenRaidPartyKeystoneData()
    local runtimeState = self:GetRuntimeState()
    local runtime = self:GetRuntimeState().openRaid
    local libKeystoneRuntime = runtimeState.libKeystone
    local now = time()
    runtime.lastFailureReason = nil

    if not (IsInGroup and IsInGroup()) or (IsInRaid and IsInRaid()) then
        runtime.lastFailureReason = "not-in-party"
        return false
    end

    if (now - runtime.lastRequestAt) < 5 then
        runtime.lastFailureReason = "request-cooldown"
        return false
    end

    local libDeflate = getLibDeflate()

    if not libDeflate then
        runtime.lastFailureReason = "no-codec"
        return false
    end

    local compressedPayload = safeCompressionCall(libDeflate, "CompressDeflate", "J", { level = 9 })
    local payload = nil

    if type(compressedPayload) == "string" and compressedPayload ~= "" then
        payload = safeCompressionCall(libDeflate, "EncodeForWoWAddonChannel", compressedPayload)
    end

    if type(payload) ~= "string" or payload == "" then
        payload = "J"
    end

    local channel = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY"
    runtime.lastRequestAt = now
    runtime.lastRequestChannel = channel
    if sendAddonMessage("LRS", payload, channel) then
        runtime.lastFailureReason = nil
    else
        runtime.lastFailureReason = "send-request-failed"
    end

    if (now - libKeystoneRuntime.lastRequestAt) >= 3 and sendAddonMessage("LibKS", "R", "PARTY") then
        libKeystoneRuntime.lastRequestAt = now
        libKeystoneRuntime.lastRequestChannel = "PARTY"
    end

    return true
end

function addon.MythicPlusService:SendLibKeystoneResponse(channel)
    local runtime = self:GetRuntimeState().libKeystone
    local keyData = getLiveCurrentKeyDetails()
    local rating = math.max(0, math.floor(tonumber(getLiveCurrentRating()) or 0))
    local keyLevel = math.max(0, math.floor(tonumber(keyData.keyLevel) or 0))
    local challengeMapId = math.max(0, math.floor(tonumber(keyData.challengeMapId or keyData.mythicPlusMapId) or 0))
    local response = string.format("%d,%d,%d", keyLevel, challengeMapId, rating)
    local resolvedChannel = channel == "GUILD" and "GUILD" or "PARTY"

    if sendAddonMessage("LibKS", response, resolvedChannel) then
        runtime.lastResponseAt = time()
        runtime.lastResponseChannel = resolvedChannel
        return true
    end

    return false
end

function addon.MythicPlusService:SendOpenRaidKeystoneResponse(channel)
    local runtime = self:GetRuntimeState().openRaid
    local libDeflate = getLibDeflate()

    if not libDeflate then
        return false
    end

    local keyData = getLiveCurrentKeyDetails()
    local classId = select(3, UnitClass and UnitClass("player") or nil) or 0
    local rating = math.max(0, tonumber(getLiveCurrentRating()) or 0)
    local challengeMapId = keyData.challengeMapId or keyData.mythicPlusMapId or "0"
    local mythicPlusMapId = keyData.mythicPlusMapId or keyData.challengeMapId or "0"
    local payloadText = string.format(
        "K,%d,%s,%s,%d,%d,%s",
        math.max(0, math.floor(tonumber(keyData.keyLevel) or 0)),
        tostring(mythicPlusMapId or "0"),
        tostring(challengeMapId or "0"),
        math.max(0, math.floor(tonumber(classId) or 0)),
        math.max(0, math.floor(rating)),
        tostring(mythicPlusMapId or "0")
    )
    local compressedPayload = safeCompressionCall(libDeflate, "CompressDeflate", payloadText, { level = 9 })
    local encodedPayload = nil

    if type(compressedPayload) == "string" and compressedPayload ~= "" then
        encodedPayload = safeCompressionCall(libDeflate, "EncodeForWoWAddonChannel", compressedPayload)
    end

    if type(encodedPayload) ~= "string" or encodedPayload == "" then
        encodedPayload = payloadText
    end

    local resolvedChannel = channel == "INSTANCE_CHAT" and "INSTANCE_CHAT" or "PARTY"
    local sent = sendAddonMessage("LRS", encodedPayload, resolvedChannel)

    if sent then
        runtime.lastResponseAt = time()
        runtime.lastResponseChannel = resolvedChannel
    end

    return sent
end

function addon.MythicPlusService:HandleOpenRaidKeystoneMessage(prefix, message, distribution, sender)
    if trimString(message) == "" or trimString(sender) == "" then
        return false
    end

    if prefix == "LibKS" then
        local runtime = self:GetRuntimeState().libKeystone
        runtime.lastInboundAt = time()
        runtime.lastInboundSender = sender
        runtime.lastInboundType = trimString(message)

        if trimString(message) == "R" then
            if resolvePartyMemberFullNameFromSender(sender) == addon.Store:GetPlayerName() then
                return false
            end

            self:SendLibKeystoneResponse(distribution)
            return false
        end

        local keyLevelText, challengeMapIdText, ratingText = string.match(trimString(message), "^(-?%d+),(-?%d+),(%d+)$")

        if not keyLevelText or not challengeMapIdText or not ratingText then
            return false
        end

        local normalizedSender = resolvePartyMemberFullNameFromSender(sender)

        if normalizedSender == "" or normalizedSender == addon.Store:GetPlayerName() then
            return false
        end

        local keyLevel = math.max(0, math.floor(tonumber(keyLevelText) or 0))
        local challengeMapId = math.max(0, math.floor(tonumber(challengeMapIdText) or 0))
        local nextDungeonId = challengeMapId > 0 and normalizeDungeonId(tostring(challengeMapId)) or nil
        local nextRating = math.max(0, tonumber(ratingText) or 0)
        local existing = runtime.partyKeys[normalizedSender]
        local changed = type(existing) ~= "table"
            or existing.currentKeyDungeonId ~= nextDungeonId
            or (tonumber(existing.currentKeyLevel) or 0) ~= keyLevel
            or (tonumber(existing.overallRating) or 0) ~= nextRating

        if not changed then
            existing.updatedAt = time()
            return false
        end

        runtime.partyKeys[normalizedSender] = {
            currentKeyDungeonId = nextDungeonId,
            currentKeyLevel = keyLevel,
            overallRating = nextRating,
            source = "libks",
            updatedAt = time(),
        }

        return true
    end

    if prefix ~= "LRS" then
        return false
    end

    local libDeflate = getLibDeflate()

    if not libDeflate then
        return false
    end

    local decompressed = nil

    if string.sub(message, 1, 1) == "J" or string.sub(message, 1, 1) == "K" then
        decompressed = message
    else
        local decoded = safeCompressionCall(libDeflate, "DecodeForWoWAddonChannel", message)

        if type(decoded) ~= "string" or decoded == "" then
            return false
        end

        decompressed = safeCompressionCall(libDeflate, "DecompressDeflate", decoded)
    end

    if type(decompressed) ~= "string" or decompressed == "" then
        return false
    end

    local runtime = self:GetRuntimeState().openRaid
    runtime.lastInboundAt = time()
    runtime.lastInboundSender = sender
    runtime.lastInboundType = string.sub(decompressed, 1, 1)

    if runtime.lastInboundType == "J" then
        if resolvePartyMemberFullNameFromSender(sender) == addon.Store:GetPlayerName() then
            return false
        end

        self:SendOpenRaidKeystoneResponse(distribution)
        return false
    end

    if string.sub(decompressed, 1, 1) ~= "K" then
        return false
    end

    local data = { strsplit(",", decompressed) }

    if #data < 6 then
        return false
    end

    table.remove(data, 1)

    local normalizedSender = resolvePartyMemberFullNameFromSender(sender)

    if normalizedSender == "" or normalizedSender == addon.Store:GetPlayerName() then
        return false
    end

    local nextDungeonId = normalizeDungeonId(tostring(data[3] or ""))
    local nextKeyLevel = math.max(0, math.floor(tonumber(data[1]) or 0))
    local nextRating = math.max(0, tonumber(data[5]) or 0)
    local existing = runtime.partyKeys[normalizedSender]
    local changed = type(existing) ~= "table"
        or existing.currentKeyDungeonId ~= nextDungeonId
        or (tonumber(existing.currentKeyLevel) or 0) ~= nextKeyLevel
        or (tonumber(existing.overallRating) or 0) ~= nextRating

    if not changed then
        existing.updatedAt = time()
        return false
    end

    runtime.partyKeys[normalizedSender] = {
        currentKeyDungeonId = nextDungeonId,
        currentKeyLevel = nextKeyLevel,
        overallRating = nextRating,
        source = "openraid",
        updatedAt = time(),
    }

    return true
end

function addon.MythicPlusService:IsShowingUnratedAlts()
    return self:GetUIState().showUnratedAlts and true or false
end

function addon.MythicPlusService:ToggleShowUnratedAlts()
    local state = self:GetUIState()
    state.showUnratedAlts = not state.showUnratedAlts
    return state.showUnratedAlts
end

function addon.MythicPlusService:SetSelectedToonId(toonId)
    local state = self:GetUIState()
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId ~= "" then
        state.selectedToonId = normalizedToonId
    else
        state.selectedToonId = nil
    end
end

function addon.MythicPlusService:GetSelectedToonId()
    local state = self:GetUIState()
    addon.RosterService:EnsurePlayerMemberRecord()

    if state.selectedToonId and addon.RosterService:IsMyToon(state.selectedToonId) then
        return state.selectedToonId
    end

    local currentPlayerId = addon.Store:GetPlayerName()

    if currentPlayerId and addon.RosterService:IsMyToon(currentPlayerId) then
        state.selectedToonId = currentPlayerId
        return currentPlayerId
    end

    state.selectedToonId = addon.RosterService:GetFirstMyToonId()
    return state.selectedToonId
end

function addon.MythicPlusService:SetSelectedPlannerDungeonId(dungeonId)
    local state = self:GetUIState()
    state.planner.selectedDungeonId = normalizeDungeonId(dungeonId)
end

function addon.MythicPlusService:GetActivePane()
    return self:GetUIState().activePane or "toons"
end

function addon.MythicPlusService:SetActivePane(value)
    local state = self:GetUIState()
    state.activePane = value == "party" and "party"
        or (value == "season" and "season"
        or (value == "leaderboard" and "leaderboard" or "toons"))
    return state.activePane
end

function addon.MythicPlusService:SetPlannerTargetLevel(value)
    local state = self:GetUIState()
    state.planner.targetKeyLevel = clampKeyLevel(value)
    return state.planner.targetKeyLevel
end

function addon.MythicPlusService:GetPlannerTargetLevel()
    return clampKeyLevel(self:GetUIState().planner.targetKeyLevel)
end

function addon.MythicPlusService:SetSelectedCarryTargetId(toonId)
    local state = self:GetUIState()
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    state.planner.selectedCarryTargetId = normalizedToonId ~= "" and normalizedToonId or nil
    return state.planner.selectedCarryTargetId
end

function addon.MythicPlusService:GetSelectedCarryTargetId()
    local state = self:GetUIState()
    local partyMembers = self:ListCurrentPartyMembers()

    for _, member in ipairs(partyMembers) do
        if member.fullName == state.planner.selectedCarryTargetId and member.hasMythicData then
            return state.planner.selectedCarryTargetId
        end
    end

    for _, member in ipairs(partyMembers) do
        if member.hasMythicData then
            state.planner.selectedCarryTargetId = member.fullName
            return member.fullName
        end
    end

    state.planner.selectedCarryTargetId = nil
    return nil
end

function addon.MythicPlusService:SetCarryMilestone(value)
    local state = self:GetUIState()
    state.planner.carryMilestone = normalizeCarryMilestone(value)
    return state.planner.carryMilestone
end

function addon.MythicPlusService:GetCarryMilestone()
    return normalizeCarryMilestone(self:GetUIState().planner.carryMilestone)
end

function addon.MythicPlusService:SetCarryKeyGoal(value)
    local state = self:GetUIState()
    state.planner.carryKeyGoal = normalizeCarryKeyGoal(value)
    return state.planner.carryKeyGoal
end

function addon.MythicPlusService:GetCarryKeyGoal()
    return normalizeCarryKeyGoal(self:GetUIState().planner.carryKeyGoal)
end

function addon.MythicPlusService:SetAnalysisMode(value)
    local state = self:GetUIState()
    state.planner.analysisMode = normalizeAnalysisMode(value)
    return state.planner.analysisMode
end

function addon.MythicPlusService:GetAnalysisMode()
    return normalizeAnalysisMode(self:GetUIState().planner.analysisMode)
end

function addon.MythicPlusService:GetPlannerDungeonOptions(toonId)
    return self:GetAvailableDungeonOptions(toonId)
end

function addon.MythicPlusService:GetAvailableDungeonOptions(toonId)
    local options = {}
    local seen = {}
    local seasonProfile = self:GetSeasonProfile(toonId)
    local currentDungeonId = seasonProfile and seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil
    local seasonDefinition = self:GetSeasonDungeonCatalog()
    local plannerState = self:GetUIState().planner or {}
    local selectedDungeonId = normalizeDungeonId(plannerState.selectedDungeonId)

    local function addOption(dungeonId, dungeonName, dungeonShortName)
        local normalizedDungeonId = normalizeDungeonId(dungeonId)

        if not normalizedDungeonId or seen[normalizedDungeonId] then
            return
        end

        local resolvedName = trimString(dungeonName) ~= "" and trimString(dungeonName) or getMapName(normalizedDungeonId)

        seen[normalizedDungeonId] = true
        options[#options + 1] = {
            id = normalizedDungeonId,
            name = resolvedName,
            shortName = trimString(dungeonShortName) ~= "" and trimString(dungeonShortName) or getDungeonShortName(self, normalizedDungeonId, resolvedName),
        }
    end

    for _, dungeon in ipairs(seasonDefinition.dungeons or {}) do
        if type(dungeon) == "table" then
            addOption(dungeon.id, dungeon.name, dungeon.shortName)
        end
    end

    for _, row in ipairs(self:ListDungeonRows(toonId)) do
        addOption(row.id, row.name, row.shortName)
    end

    if currentDungeonId then
        addOption(currentDungeonId, getMapName(currentDungeonId))
    end

    if selectedDungeonId then
        addOption(selectedDungeonId, getMapName(selectedDungeonId))
    end

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        if member.currentKeyDungeonId then
            addOption(member.currentKeyDungeonId, getMapName(member.currentKeyDungeonId))
        end
    end

    table.sort(options, function(left, right)
        return lowerText(left.name) < lowerText(right.name)
    end)

    return options
end

function addon.MythicPlusService:GetPlannerState(toonId)
    local state = self:GetUIState()
    local options = self:GetAvailableDungeonOptions(toonId)
    local selectedDungeonId = normalizeDungeonId(state.planner.selectedDungeonId)
    local selectedDungeonName
    local selectedDungeonShortName
    local seasonProfile = self:GetSeasonProfile(toonId)
    local currentDungeonId = seasonProfile and seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil

    for _, option in ipairs(options) do
        if option.id == selectedDungeonId then
            selectedDungeonName = option.name
            selectedDungeonShortName = option.shortName
            break
        end
    end

    if not selectedDungeonName and currentDungeonId then
        for _, option in ipairs(options) do
            if option.id == currentDungeonId then
                selectedDungeonId = option.id
                selectedDungeonName = option.name
                selectedDungeonShortName = option.shortName
                state.planner.selectedDungeonId = selectedDungeonId
                break
            end
        end
    end

    if not selectedDungeonName and options[1] then
        selectedDungeonId = options[1].id
        selectedDungeonName = options[1].name
        selectedDungeonShortName = options[1].shortName
        state.planner.selectedDungeonId = selectedDungeonId
    end

    return {
        options = options,
        selectedDungeonId = selectedDungeonId,
        selectedDungeonName = selectedDungeonName,
        selectedDungeonShortName = selectedDungeonShortName,
        targetKeyLevel = clampKeyLevel(state.planner.targetKeyLevel),
    }
end

function addon.MythicPlusService:GetToonSummary(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" then
        return nil
    end

    if normalizedToonId == addon.Store:GetPlayerName() then
        addon.RosterService:RefreshLivePlayerMember()
        self:RefreshCurrentCharacter()
    end

    local member = addon.RosterService:GetMyToonMember(normalizedToonId) or addon.RosterService:GetMember(normalizedToonId)
    local seasonProfile = self:GetSeasonProfile(normalizedToonId)

    if not member or not seasonProfile then
        return nil
    end

    local currentKeyLevel = tonumber(seasonProfile.currentKey and seasonProfile.currentKey.level) or 0
    local currentKeyDungeonId = seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil
    local trackedDungeons = countTableEntries(seasonProfile.dungeons)
    local portalCount = 0
    local seasonDefinition = self:GetSeasonDungeonCatalog()

    for _, dungeon in ipairs(seasonDefinition.dungeons or {}) do
        local dungeonRecord = seasonProfile.dungeons and seasonProfile.dungeons[dungeon.id] or nil

        if isPortalUnlockedForRecord(seasonProfile, dungeonRecord, dungeon.id, dungeon.name, normalizedToonId) then
            portalCount = portalCount + 1
        end
    end

    local currentKeyText = "No Key"
    local currentKeyShortText = "No Key"

    if currentKeyDungeonId and currentKeyLevel > 0 then
        currentKeyText = string.format("%s +%d", getMapName(currentKeyDungeonId), currentKeyLevel)
        currentKeyShortText = string.format("%s +%d", getDungeonShortName(self, currentKeyDungeonId), currentKeyLevel)
    elseif currentKeyLevel > 0 then
        currentKeyText = string.format("+%d", currentKeyLevel)
        currentKeyShortText = currentKeyText
    end

    local metaParts = {}

    if member.className and member.className ~= "" then
        metaParts[#metaParts + 1] = member.className
    end

    if member.spec and member.spec ~= "" then
        metaParts[#metaParts + 1] = member.spec
    end

    metaParts[#metaParts + 1] = string.format("ilvl %d", tonumber(member.itemLevel) or 0)
    metaParts[#metaParts + 1] = seasonDefinition.name or "Current Season"

    return {
        id = normalizedToonId,
        classFile = trimString(member.classFile or ""),
        fullName = member.fullName or normalizedToonId,
        itemLevel = tonumber(member.itemLevel) or 0,
        metaLine = table.concat(metaParts, " | "),
        overallRating = tonumber(seasonProfile.overallRating) or 0,
        currentKeyShortText = currentKeyShortText,
        currentKeyText = currentKeyText,
        portalCount = portalCount,
        ratingText = string.format("%d", math.floor(tonumber(seasonProfile.overallRating) or 0)),
        trackedDungeons = trackedDungeons,
        summaryLine = trackedDungeons > 0
            and string.format("%d dungeon rows tracked this season. Portal threshold defaults to +%d until seasonal metadata is expanded.", trackedDungeons, tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD)
            or (seasonProfile.lastCaptureReason ~= "" and seasonProfile.lastCaptureReason)
            or "Current key and overall score can refresh from live data. Per-dungeon rows will become more useful as seasonal capture expands.",
        lastCaptureAttemptAt = tonumber(seasonProfile.lastCaptureAttemptAt) or 0,
        lastCaptureCount = tonumber(seasonProfile.lastCaptureCount) or 0,
        lastCaptureReason = seasonProfile.lastCaptureReason,
        lastCaptureSource = seasonProfile.lastCaptureSource,
        lastSuccessfulCaptureAt = tonumber(seasonProfile.lastSuccessfulCaptureAt) or 0,
    }
end

function addon.MythicPlusService:ListToons()
    local members = addon.RosterService:ListMyToons()
    local records = {}
    local showUnrated = self:IsShowingUnratedAlts()
    local selectedToonId = self:GetSelectedToonId()
    local currentPlayerId = addon.Store:GetPlayerName()

    for _, member in ipairs(members) do
        local summary = self:GetToonSummary(member.id)

        if summary then
            local hasTrackedData = summary.overallRating > 0 or summary.trackedDungeons > 0 or summary.currentKeyText ~= "No Key"

            if showUnrated or hasTrackedData or member.id == selectedToonId or member.id == currentPlayerId then
                records[#records + 1] = summary
            end
        end
    end

    table.sort(records, function(left, right)
        if left.id == selectedToonId then
            return true
        end

        if right.id == selectedToonId then
            return false
        end

        if left.overallRating == right.overallRating then
            return lowerText(left.fullName) < lowerText(right.fullName)
        end

        return left.overallRating > right.overallRating
    end)

    return records
end

function addon.MythicPlusService:ListLeaderboardRows()
    local rows = {}
    local playerId = addon.Store:GetPlayerName()

    for toonId, profile in pairs(self:GetStore() or {}) do
        if type(profile) == "table" then
            local seasonProfile = self:GetStoredSeasonProfile(toonId)

            if seasonProfile and hasMeaningfulSeasonData(seasonProfile) then
                local member = addon.RosterService:GetMyToonMember(toonId) or addon.RosterService:GetMember(toonId) or {}
                local currentKeyLevel = tonumber(seasonProfile.currentKey and seasonProfile.currentKey.level) or 0
                local currentKeyDungeonId = seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil
                local currentKeyText = "No Key"

                if currentKeyDungeonId and currentKeyLevel > 0 then
                    currentKeyText = string.format("%s +%d", getMapName(currentKeyDungeonId), currentKeyLevel)
                elseif currentKeyLevel > 0 then
                    currentKeyText = string.format("+%d", currentKeyLevel)
                end

                rows[#rows + 1] = {
                    classFile = trimString(member.classFile),
                    currentKeyLevel = currentKeyLevel,
                    currentKeyText = currentKeyText,
                    fullName = member.fullName or toonId,
                    guildName = trimString(member.guildName or member.guild or member.homeGuild or ""),
                    id = toonId,
                    isPlayer = toonId == playerId,
                    itemLevel = tonumber(member.itemLevel) or 0,
                    overallRating = tonumber(seasonProfile.overallRating) or 0,
                    shortName = addon.Store:SplitFullName(toonId, GetRealmName() or "Unknown"),
                    spec = trimString(member.spec or ""),
                    trackedDungeons = countTableEntries(seasonProfile.dungeons),
                    updatedAt = tonumber(seasonProfile.updatedAt) or tonumber(profile.updatedAt) or 0,
                }
            end
        end
    end

    table.sort(rows, function(left, right)
        if left.overallRating == right.overallRating then
            if left.currentKeyLevel == right.currentKeyLevel then
                if left.itemLevel == right.itemLevel then
                    return lowerText(left.fullName) < lowerText(right.fullName)
                end

                return left.itemLevel > right.itemLevel
            end

            return left.currentKeyLevel > right.currentKeyLevel
        end

        return left.overallRating > right.overallRating
    end)

    return rows
end

function addon.MythicPlusService:GetLeaderboardSummary()
    local rows = self:ListLeaderboardRows()
    local keyHolders = 0

    for _, row in ipairs(rows) do
        if (tonumber(row.currentKeyLevel) or 0) > 0 then
            keyHolders = keyHolders + 1
        end
    end

    return {
        keyHolders = keyHolders,
        rowCount = #rows,
        topScore = rows[1] and math.floor(tonumber(rows[1].overallRating) or 0) or 0,
    }
end

function addon.MythicPlusService:ListDungeonRows(toonId, targetKeyLevel)
    local seasonProfile = self:GetSeasonProfile(toonId)
    local rows = {}
    local desiredLevel = clampKeyLevel(targetKeyLevel)

    if not seasonProfile then
        return rows
    end

    for dungeonId, dungeonRecord in pairs(seasonProfile.dungeons or {}) do
        local bestLevel = math.max(tonumber(dungeonRecord.bestTimedLevel) or 0, tonumber(dungeonRecord.bestLevel) or 0)
        local totalScore = tonumber(dungeonRecord.score) or 0
        local portalUnlocked = isPortalUnlockedForRecord(seasonProfile, dungeonRecord, dungeonId, dungeonRecord.name, toonId)

        if totalScore <= 0 then
            totalScore = (tonumber(dungeonRecord.fortifiedScore) or 0) + (tonumber(dungeonRecord.tyrannicalScore) or 0)
        end

        totalScore = math.floor(totalScore)

        local estimate = self:EstimateScoreGain(toonId, dungeonId, desiredLevel)
        local gainLabel = "-"
        local gainValue = 0

        if estimate then
            gainValue = math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0))

            if gainValue > 0 then
                gainLabel = string.format("+%d", gainValue)
            elseif (tonumber(estimate.bestTimedGain) or 0) > 0 then
                gainLabel = string.format("~+%d", tonumber(estimate.bestTimedGain) or 0)
            else
                gainLabel = "0"
            end
        end

        rows[#rows + 1] = {
            id = dungeonId,
            name = trimString(dungeonRecord.name) ~= "" and dungeonRecord.name or getMapName(dungeonId),
            bestLabel = formatBestRunLabel(dungeonRecord),
            fortifiedLabel = tostring(math.floor(tonumber(dungeonRecord.fortifiedScore) or 0)),
            gainValue = gainValue,
            gainLabel = gainLabel,
            projectedLabel = tostring(totalScore + gainValue),
            projectedValue = totalScore + gainValue,
            tyrannicalLabel = tostring(math.floor(tonumber(dungeonRecord.tyrannicalScore) or 0)),
            portalLabel = portalUnlocked and "Yes" or "No",
            scoreValue = totalScore,
            scoreLabel = tostring(math.floor(totalScore)),
        }
    end

    table.sort(rows, function(left, right)
        return lowerText(left.name) < lowerText(right.name)
    end)

    return rows
end

function addon.MythicPlusService:GetDungeonTableSummary(toonId, targetKeyLevel)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local desiredLevel = clampKeyLevel(targetKeyLevel)
    local currentTotal = 0
    local gainTotal = 0
    local rowCount = 0

    if normalizedToonId == "" then
        return nil
    end

    for _, row in ipairs(self:ListDungeonRows(normalizedToonId, desiredLevel)) do
        currentTotal = currentTotal + math.max(0, math.floor(tonumber(row.scoreValue) or 0))
        gainTotal = gainTotal + math.max(0, math.floor(tonumber(row.gainValue) or 0))
        rowCount = rowCount + 1
    end

    return {
        currentTotal = currentTotal,
        gainTotal = gainTotal,
        projectedTotal = currentTotal + gainTotal,
        rowCount = rowCount,
        targetLevel = desiredLevel,
    }
end

function addon.MythicPlusService:GetDungeonTableScoreGoalSummary(toonId, milestone)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local targetMilestone = normalizeCarryMilestone(milestone)

    if normalizedToonId == "" then
        return nil
    end

    local currentSummary = self:GetDungeonTableSummary(normalizedToonId, self:GetPlannerTargetLevel())

    if not currentSummary or currentSummary.rowCount <= 0 then
        return nil
    end

    if currentSummary.currentTotal >= targetMilestone then
        return {
            currentTotal = currentSummary.currentTotal,
            gainTotal = 0,
            milestone = targetMilestone,
            projectedTotal = currentSummary.currentTotal,
            reachesMilestone = true,
            recommendedKeyLevel = nil,
            remaining = 0,
            summaryLine = string.format("Current total %d already meets the %d score goal.", currentSummary.currentTotal, targetMilestone),
        }
    end

    local bestAttempt = nil

    for keyLevel = 2, 20 do
        local summary = self:GetDungeonTableSummary(normalizedToonId, keyLevel)

        if summary and summary.rowCount > 0 then
            bestAttempt = summary

            if summary.projectedTotal >= targetMilestone then
                return {
                    currentTotal = currentSummary.currentTotal,
                    gainTotal = summary.gainTotal,
                    milestone = targetMilestone,
                    projectedTotal = summary.projectedTotal,
                    reachesMilestone = true,
                    recommendedKeyLevel = keyLevel,
                    remaining = 0,
                    summaryLine = string.format("Timing every shown dungeon at %d projects %d total score (+%d), enough for %d.", keyLevel, summary.projectedTotal, summary.gainTotal, targetMilestone),
                }
            end
        end
    end

    if not bestAttempt then
        return nil
    end

    return {
        currentTotal = currentSummary.currentTotal,
        gainTotal = bestAttempt.gainTotal,
        milestone = targetMilestone,
        projectedTotal = bestAttempt.projectedTotal,
        reachesMilestone = false,
        recommendedKeyLevel = 20,
        remaining = math.max(targetMilestone - bestAttempt.projectedTotal, 0),
        summaryLine = string.format("Even timing every shown dungeon at 20 only projects %d total score (+%d), leaving %d short of %d.", bestAttempt.projectedTotal, bestAttempt.gainTotal, math.max(targetMilestone - bestAttempt.projectedTotal, 0), targetMilestone),
    }
end

function addon.MythicPlusService:EnsureDungeonTracked(toonId, dungeonId)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if not normalizedDungeonId then
        return false
    end

    local seasonProfile = self:GetSeasonProfile(toonId)

    if seasonProfile and type(seasonProfile.dungeons[normalizedDungeonId]) == "table" then
        self:SetSelectedPlannerDungeonId(normalizedDungeonId)
        return false
    end

    local changed = self:ApplyDungeonUpdate(toonId, normalizedDungeonId, {
        name = getMapName(normalizedDungeonId),
    })
    self:SetSelectedPlannerDungeonId(normalizedDungeonId)
    return changed
end

function addon.MythicPlusService:GetTrackedDungeonCount(toonId)
    local seasonProfile = self:GetSeasonProfile(toonId)
    return seasonProfile and countTableEntries(seasonProfile.dungeons) or 0
end

function addon.MythicPlusService:GetAvailableDungeonCount(toonId)
    return #self:GetAvailableDungeonOptions(toonId)
end

function addon.MythicPlusService:GetDungeonCatalogText(toonId)
    local trackedCount = self:GetTrackedDungeonCount(toonId)
    local seasonDefinition = self:GetSeasonDungeonCatalog()
    local availableCount = #(seasonDefinition.dungeons or {})

    if trackedCount <= 0 and availableCount <= 0 then
        return "No season dungeon catalog is available yet."
    end

    return string.format("%d tracked | %d dungeons in %s", trackedCount, availableCount, seasonDefinition.name or "the current season")
end

function addon.MythicPlusService:GetSeasonOverviewSummary(toonId)
    local seasonDefinition = self:GetSeasonDungeonCatalog()
    local seasonProfile = self:GetSeasonProfile(toonId)
    local seasonDungeons = seasonDefinition.dungeons or {}

    if #seasonDungeons == 0 then
        seasonDungeons = self:GetAvailableDungeonOptions(toonId)
    end

    local summary = {
        highestKey = 0,
        missingCount = #seasonDungeons,
        overallRating = 0,
        portalCount = 0,
        seasonName = seasonDefinition.name or "Current Season",
        timedCount = 0,
        totalDungeons = #seasonDungeons,
    }

    if not seasonProfile then
        return summary
    end

    summary.overallRating = math.floor(tonumber(seasonProfile.overallRating) or 0)
    local missingCount = 0

    for _, dungeon in ipairs(seasonDungeons) do
        local record = seasonProfile.dungeons and seasonProfile.dungeons[dungeon.id] or nil
        local bestTimedLevel = record and (tonumber(record.bestTimedLevel) or 0) or 0
        local bestLevel = record and math.max(bestTimedLevel, tonumber(record.bestLevel) or 0) or 0
        local portalUnlocked = isPortalUnlockedForRecord(seasonProfile, record, dungeon.id, dungeon.name, toonId)

        if bestTimedLevel > 0 then
            summary.timedCount = summary.timedCount + 1
        end

        if portalUnlocked then
            summary.portalCount = summary.portalCount + 1
        end

        if bestLevel > summary.highestKey then
            summary.highestKey = bestLevel
        end

        if bestLevel <= 0 then
            missingCount = missingCount + 1
        end
    end

    summary.missingCount = missingCount
    return summary
end

function addon.MythicPlusService:ListSeasonOverviewRows(toonId)
    local seasonDefinition = self:GetSeasonDungeonCatalog()
    local seasonProfile = self:GetSeasonProfile(toonId)
    local seasonDungeons = seasonDefinition.dungeons or {}
    local rows = {}

    if #seasonDungeons == 0 then
        seasonDungeons = self:GetAvailableDungeonOptions(toonId)
    end

    for _, dungeon in ipairs(seasonDungeons) do
        local record = seasonProfile and seasonProfile.dungeons and seasonProfile.dungeons[dungeon.id] or nil
        local bestTimedLevel = record and (tonumber(record.bestTimedLevel) or 0) or 0
        local bestLevel = record and math.max(bestTimedLevel, tonumber(record.bestLevel) or 0) or 0
        local totalScore = record and (tonumber(record.score) or 0) or 0
        local portalUnlocked = isPortalUnlockedForRecord(seasonProfile, record, dungeon.id, dungeon.name, toonId)

        if totalScore <= 0 and record then
            totalScore = math.floor((tonumber(record.fortifiedScore) or 0) + (tonumber(record.tyrannicalScore) or 0))
        end

        rows[#rows + 1] = {
            bestLabel = formatBestRunLabel(record),
            id = dungeon.id,
            name = dungeon.name,
            portalLabel = portalUnlocked and "Yes" or "No",
            scoreLabel = totalScore > 0 and tostring(math.floor(totalScore)) or "0",
            shortName = dungeon.shortName,
            timedLabel = bestTimedLevel > 0 and "Timed" or (bestLevel > 0 and "Untimed" or "Missing"),
        }
    end

    return rows
end

function addon.MythicPlusService:EstimateScoreGain(toonId, dungeonId, targetLevel)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if normalizedToonId == "" or not normalizedDungeonId then
        return nil
    end

    local seasonProfile = self:GetStoredSeasonProfile(normalizedToonId)
    local dungeonRecord = self:GetStoredDungeonRecord(normalizedToonId, normalizedDungeonId)

    if not seasonProfile then
        return nil
    end

    if not dungeonRecord then
        dungeonRecord = {
            bestLevel = 0,
            bestTimedLevel = 0,
            bestLevelUpgrades = 0,
            fortifiedScore = 0,
            name = getMapName(normalizedDungeonId),
            portalUnlocked = false,
            score = 0,
            tyrannicalScore = 0,
        }
    end

    local desiredLevel = clampKeyLevel(targetLevel)
    local bestLevel = math.max(tonumber(dungeonRecord.bestTimedLevel) or 0, tonumber(dungeonRecord.bestLevel) or 0)
    local currentScore = getTrackedDungeonScore(dungeonRecord)
    local onTimeScore = roundToNearestInteger(calculateMythicRunScore(desiredLevel, 0, true))
    local bestTimedScore = roundToNearestInteger(calculateMythicRunScore(desiredLevel, MYTHIC_SCORE_MAX_TIME_PERCENT, true))
    local overtimeScore = roundToNearestInteger(calculateMythicRunScore(desiredLevel, 0, false))
    local estimatedGain = math.max(onTimeScore - currentScore, 0)
    local bestTimedGain = math.max(bestTimedScore - currentScore, 0)
    local overtimeGain = math.max(overtimeScore - currentScore, 0)
    local portalAlreadyUnlocked = isPortalUnlockedForRecord(seasonProfile, dungeonRecord, normalizedDungeonId, dungeonRecord.name, toonId)
    local portalUnlock = desiredLevel >= (tonumber(seasonProfile.portalThreshold) or DEFAULT_PORTAL_THRESHOLD) and not portalAlreadyUnlocked

    local titleLine = string.format("%s at +%d", dungeonRecord.name or getMapName(normalizedDungeonId), desiredLevel)
    local summaryLine

    if estimatedGain > 0 then
        summaryLine = string.format(
            "Approximate on-time gain: +%d score. Strongly timed ceiling: +%d. Slight overtime: +%d. Current saved dungeon score is %d from a best level of +%d. %s",
            estimatedGain,
            bestTimedGain,
            overtimeGain,
            roundToNearestInteger(currentScore),
            bestLevel,
            portalUnlock and "This key also lines up with a portal unlock if the run is completed." or "No portal unlock change is predicted."
        )
    elseif bestTimedGain > 0 then
        summaryLine = string.format(
            "An on-time clear is roughly flat against the current saved dungeon score of %d, but a strong timer beat could still add about +%d. Current best level is +%d. %s",
            roundToNearestInteger(currentScore),
            bestTimedGain,
            bestLevel,
            portalUnlock and "This key also lines up with a portal unlock if the run is completed." or "No portal unlock change is predicted."
        )
    else
        summaryLine = string.format(
            "No clear score increase is predicted from the current saved dungeon score of %d at a best level of +%d. Raise the target key level or refresh the saved run history.",
            roundToNearestInteger(currentScore),
            bestLevel
        )
    end

    return {
        bestLevel = bestLevel,
        bestTimedGain = bestTimedGain,
        bestTimedScore = bestTimedScore,
        currentScore = roundToNearestInteger(currentScore),
        estimatedGain = estimatedGain,
        estimatedScore = onTimeScore,
        overtimeGain = overtimeGain,
        overtimeScore = overtimeScore,
        portalUnlock = portalUnlock,
        summaryLine = summaryLine,
        targetLevel = desiredLevel,
        titleLine = titleLine,
    }
end

function addon.MythicPlusService:GetPartyDungeonDetailLabel(toonId, dungeonId, targetLevel)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local normalizedDungeonId = normalizeDungeonId(dungeonId)

    if normalizedToonId == "" or not normalizedDungeonId then
        return "-\n-"
    end

    local dungeonRecord = self:GetStoredDungeonRecord(normalizedToonId, normalizedDungeonId)
    local estimate = self:EstimateScoreGain(normalizedToonId, normalizedDungeonId, targetLevel)
    local bestLabel = "-"
    local scoreLabel = "0"
    local gainLabel = "-"

    if dungeonRecord then
        local trackedScore = math.floor(getTrackedDungeonScore(dungeonRecord))

        bestLabel = formatBestRunLabel(dungeonRecord)

        scoreLabel = tostring(trackedScore)
    end

    if estimate then
        local estimatedGain = math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0))
        local bestTimedGain = math.max(0, math.floor(tonumber(estimate.bestTimedGain) or 0))

        if estimatedGain > 0 then
            gainLabel = "+" .. tostring(estimatedGain)
        elseif bestTimedGain > 0 then
            gainLabel = "~+" .. tostring(bestTimedGain)
        else
            gainLabel = "+0"
        end
    end

    return string.format("%s\n%s (%s)", bestLabel, scoreLabel, gainLabel)
end

function addon.MythicPlusService:ListCurrentPartyMembers()
    local runtimeState = self:GetRuntimeState()
    local partyMembersCache = runtimeState.partyMembers
    local now = time()

    if type(partyMembersCache.members) == "table" and (now - (tonumber(partyMembersCache.updatedAt) or 0)) <= PARTY_MEMBERS_CACHE_TTL_SECONDS then
        return partyMembersCache.members
    end

    local members = {}
    local seen = {}
    local openRaidState = runtimeState.openRaid
    local libKeystoneState = runtimeState.libKeystone
    local canInvite = canGuildInviteMembers()

    self:RequestOpenRaidPartyKeystoneData()

    for _, unitId in ipairs(buildGroupUnits()) do
        if UnitExists and UnitExists(unitId) then
            local unitName, unitRealm = UnitFullName(unitId)
            local fullName = addon.Store:NormalizeFullName(unitName, unitRealm or GetRealmName() or "Unknown")

            if fullName ~= "" and not seen[fullName] then
                seen[fullName] = true

                local rosterMember = addon.RosterService:GetMyToonMember(fullName) or addon.RosterService:GetMember(fullName) or {}
                local classFile = UnitClass and select(2, UnitClass(unitId)) or nil
                local seasonProfile = self:GetStoredSeasonProfile(fullName)
                local openRaidData = getPartyKeyForMember(openRaidState, fullName)
                local libKeystoneData = getPartyKeyForMember(libKeystoneState, fullName)
                local currentKeyLevel = seasonProfile and seasonProfile.currentKey and tonumber(seasonProfile.currentKey.level) or 0
                local currentKeyDungeonId = seasonProfile and seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil
                local currentKeyText = "No Key"
                local itemLevel = getPartyMemberItemLevel(self, unitId, fullName, rosterMember.itemLevel)
                local roleValue = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unitId) or nil
                local role = roleValue == "TANK" and "tank"
                    or (roleValue == "HEALER" and "healer")
                    or (roleValue == "DAMAGER" and "dps")
                    or (rosterMember.primaryRole or "")
                local guildName = GetGuildInfo and GetGuildInfo(unitId) or nil

                if type(openRaidData) == "table" then
                    local openRaidLevel = math.max(0, tonumber(openRaidData.currentKeyLevel) or 0)
                    local openRaidDungeonId = normalizeDungeonId(openRaidData.currentKeyDungeonId)

                    if openRaidLevel > currentKeyLevel then
                        currentKeyLevel = openRaidLevel
                    end

                    if currentKeyLevel > 0 and openRaidDungeonId then
                        currentKeyDungeonId = openRaidDungeonId
                    end
                end

                if type(libKeystoneData) == "table" then
                    local libKeystoneLevel = math.max(0, tonumber(libKeystoneData.currentKeyLevel) or 0)
                    local libKeystoneDungeonId = normalizeDungeonId(libKeystoneData.currentKeyDungeonId)

                    if libKeystoneLevel > currentKeyLevel then
                        currentKeyLevel = libKeystoneLevel
                    end

                    if currentKeyLevel > 0 and libKeystoneDungeonId then
                        currentKeyDungeonId = libKeystoneDungeonId
                    end
                end

                if currentKeyDungeonId and currentKeyLevel > 0 then
                    currentKeyText = string.format("%s +%d", getMapName(currentKeyDungeonId), currentKeyLevel)
                elseif currentKeyLevel > 0 then
                    currentKeyText = string.format("+%d", currentKeyLevel)
                end

                members[#members + 1] = {
                    canGuildInvite = canInvite,
                    classFile = trimString(classFile) ~= "" and trimString(classFile) or trimString(rosterMember.classFile),
                    currentKeyDungeonId = currentKeyDungeonId,
                    currentKeyLevel = currentKeyLevel,
                    currentKeyText = currentKeyText,
                    fullName = fullName,
                    guildInviteEligible = canInvite and not guildName and fullName ~= addon.Store:GetPlayerName(),
                    guildName = guildName,
                    hasCurrentKey = currentKeyLevel > 0,
                    hasMythicData = seasonProfile ~= nil or type(openRaidData) == "table" or type(libKeystoneData) == "table",
                    isPlayer = fullName == addon.Store:GetPlayerName(),
                    itemLevel = itemLevel,
                    overallRating = math.max(
                        seasonProfile and (tonumber(seasonProfile.overallRating) or 0) or 0,
                        type(openRaidData) == "table" and (tonumber(openRaidData.overallRating) or 0) or 0,
                        type(libKeystoneData) == "table" and (tonumber(libKeystoneData.overallRating) or 0) or 0
                    ),
                    role = role,
                }
            end
        end
    end

    table.sort(members, function(left, right)
        return lowerText(left.fullName) < lowerText(right.fullName)
    end)

    partyMembersCache.members = members
    partyMembersCache.updatedAt = now

    return members
end

function addon.MythicPlusService:SendGuildInviteToPartyMember(toonId)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")

    if normalizedToonId == "" then
        return false, "invalid party member"
    end

    if normalizedToonId == addon.Store:GetPlayerName() then
        return false, "you cannot invite yourself"
    end

    if not canGuildInviteMembers() then
        return false, "you do not have permission to send guild invites"
    end

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        if member.fullName == normalizedToonId then
            if trimString(member.guildName) ~= "" then
                return false, "that player already appears to be in a guild"
            end

            local ok, inviteError

            if C_GuildInfo and C_GuildInfo.Invite then
                ok, inviteError = pcall(C_GuildInfo.Invite, normalizedToonId)
            elseif GuildInvite then
                ok, inviteError = pcall(GuildInvite, normalizedToonId)
            else
                return false, "guild invite API is unavailable"
            end

            if ok then
                local shortName = addon.Store:SplitFullName(normalizedToonId, GetRealmName() or "Unknown")
                return true, "sent guild invite to " .. shortName
            end

            return false, tostring(inviteError or "unable to send the guild invite")
        end
    end

    return false, "that player is no longer in your party"
end

function addon.MythicPlusService:GetPartyPlannerSummary(dungeonId, targetLevel)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local desiredLevel = clampKeyLevel(targetLevel)
    local partyMembers = self:ListCurrentPartyMembers()

    if #partyMembers <= 1 then
        return "Solo view active. Group gain summary will appear when you are in a party."
    end

    if not normalizedDungeonId then
        return string.format("Party detected: %d members. Select a dungeon to estimate shared gains.", #partyMembers)
    end

    local knownMembers = 0
    local totalKnownGain = 0

    for _, member in ipairs(partyMembers) do
        if member.hasMythicData then
            local estimate = self:EstimateScoreGain(member.fullName, normalizedDungeonId, desiredLevel)

            if estimate then
                knownMembers = knownMembers + 1
                totalKnownGain = totalKnownGain + (estimate.estimatedGain or 0)
            end
        end
    end

    if knownMembers <= 0 then
        return string.format("Party detected: %d members. GH3 has no synced Mythic+ records for this group yet.", #partyMembers)
    end

    return string.format(
        "Party detected: %d members. Known tracked players: %d. Estimated combined gain for %s +%d is +%d.",
        #partyMembers,
        knownMembers,
        getMapName(normalizedDungeonId),
        desiredLevel,
        totalKnownGain
    )
end

function addon.MythicPlusService:ListPartyPlannerRows(dungeonId, targetLevel)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local desiredLevel = clampKeyLevel(targetLevel)
    local rows = {}

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        local displayName = addon.Store:SplitFullName(member.fullName, GetRealmName() or "Unknown")
        local gainLabel = "-"
        local statusLabel = member.hasMythicData and "Tracked" or "No Data"

        if normalizedDungeonId and member.hasMythicData then
            local estimate = self:EstimateScoreGain(member.fullName, normalizedDungeonId, desiredLevel)

            if estimate then
                gainLabel = string.format("+%d", estimate.estimatedGain or 0)
                statusLabel = estimate.portalUnlock and "Portal" or ((estimate.estimatedGain or 0) > 0 and "Upgrade" or "Flat")
            end
        elseif not normalizedDungeonId and member.hasMythicData then
            statusLabel = "Ready"
        end

        rows[#rows + 1] = {
            classFile = member.classFile,
            fullName = member.fullName,
            gain = gainLabel,
            itemLevel = tonumber(member.itemLevel) or 0,
            name = displayName,
            rating = tostring(math.floor(tonumber(member.overallRating) or 0)),
            role = member.role ~= "" and member.role or "-",
            status = statusLabel,
        }
    end

    table.sort(rows, function(left, right)
        return lowerText(left.name) < lowerText(right.name)
    end)

    return rows
end

function addon.MythicPlusService:GetBestKnownPartyTarget(toonId, targetLevel)
    local desiredLevel = clampKeyLevel(targetLevel)
    local options = self:GetAvailableDungeonOptions(toonId)
    local partyMembers = self:ListCurrentPartyMembers()
    local bestOption

    if #partyMembers <= 1 or #options == 0 then
        return nil
    end

    for _, option in ipairs(options) do
        local totalKnownGain = 0
        local knownMembers = 0

        for _, member in ipairs(partyMembers) do
            if member.hasMythicData then
                local estimate = self:EstimateScoreGain(member.fullName, option.id, desiredLevel)

                if estimate then
                    knownMembers = knownMembers + 1
                    totalKnownGain = totalKnownGain + (estimate.estimatedGain or 0)
                end
            end
        end

        if knownMembers > 0 then
            if not bestOption or totalKnownGain > bestOption.totalKnownGain or (totalKnownGain == bestOption.totalKnownGain and lowerText(option.name) < lowerText(bestOption.name)) then
                bestOption = {
                    id = option.id,
                    knownMembers = knownMembers,
                    name = option.name,
                    shortName = option.shortName or getDungeonShortName(self, option.id, option.name),
                    targetLevel = desiredLevel,
                    totalKnownGain = totalKnownGain,
                }
            end
        end
    end

    return bestOption
end

local function getRecommendedKeyLevelForItemLevel(itemLevel)
    local level = math.floor(tonumber(itemLevel) or 0)

    if level >= 330 then
        return 20
    end

    if level >= 320 then
        return 18
    end

    if level >= 310 then
        return 16
    end

    if level >= 300 then
        return 14
    end

    if level >= 290 then
        return 13
    end

    if level >= 280 then
        return 12
    end

    if level >= 270 then
        return 11
    end

    if level >= 260 then
        return 10
    end

    if level >= 250 then
        return 8
    end

    if level >= 240 then
        return 6
    end

    return 4
end

function addon.MythicPlusService:GetPartyKeyLevelGuide()
    local knownCount = 0
    local lowestItemLevel = nil
    local totalItemLevel = 0
    local recommendedLevel = 20

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        local itemLevel = math.floor(tonumber(member.itemLevel) or 0)

        if itemLevel > 0 then
            knownCount = knownCount + 1
            totalItemLevel = totalItemLevel + itemLevel
            lowestItemLevel = lowestItemLevel and math.min(lowestItemLevel, itemLevel) or itemLevel
            recommendedLevel = math.min(recommendedLevel, getRecommendedKeyLevelForItemLevel(itemLevel))
        end
    end

    if knownCount <= 0 then
        return {
            averageItemLevel = 0,
            knownCount = 0,
            lowestItemLevel = 0,
            recommendedLevel = 10,
            summaryLine = "Party gear check needs synced item levels before GH3 can cap the suggested key level.",
        }
    end

    local averageItemLevel = math.floor(totalItemLevel / knownCount)

    return {
        averageItemLevel = averageItemLevel,
        knownCount = knownCount,
        lowestItemLevel = lowestItemLevel or 0,
        recommendedLevel = recommendedLevel,
        summaryLine = string.format(
            "Party gear check: lowest tracked iLvl %d, average %d. GH3 will avoid suggesting keys much above +%d for this group.",
            lowestItemLevel or 0,
            averageItemLevel,
            recommendedLevel
        ),
    }
end

function addon.MythicPlusService:ListPartyGoalRecommendationRows(targetMemberId, analysisMode, milestone, keyGoal)
    local normalizedToonId = addon.Store:NormalizeFullName(targetMemberId, GetRealmName() or "Unknown")
    local mode = normalizeAnalysisMode(analysisMode)
    local partyGuide = self:GetPartyKeyLevelGuide()
    local partyKeyRows = self:ListPartyKeystoneRows()
    local rows = {}
    local targetMilestone = normalizeCarryMilestone(milestone)
    local targetKeyGoal = normalizeCarryKeyGoal(keyGoal)
    local seasonProfile = normalizedToonId ~= "" and self:GetSeasonProfile(normalizedToonId) or nil
    local currentRating = math.floor(tonumber(seasonProfile and seasonProfile.overallRating) or 0)

    for _, row in ipairs(partyKeyRows) do
        local keyLevel = math.max(0, math.floor(tonumber(row.keyLevel) or 0))

        if row.dungeonId and keyLevel > 0 then
            local estimatedGain = 0
            local reachesGoal = false
            local state = row.state or "Tracked"
            local note = ""
            local tooHigh = keyLevel > (tonumber(partyGuide and partyGuide.recommendedLevel) or 20)

            if tooHigh then
                state = "Too High"
                note = string.format("Above the current party comfort cap of +%d.", tonumber(partyGuide and partyGuide.recommendedLevel) or 20)
            elseif mode == "score-goal" and normalizedToonId ~= "" then
                local estimate = self:EstimateScoreGain(normalizedToonId, row.dungeonId, keyLevel)
                estimatedGain = estimate and math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0)) or 0
                reachesGoal = (currentRating + estimatedGain) >= targetMilestone
                state = reachesGoal and "Goal" or (estimatedGain > 0 and "Upgrade" or "Flat")
                note = reachesGoal
                    and string.format("Gets %s to %d with this one run.", addon.Store:SplitFullName(normalizedToonId, GetRealmName() or "Unknown"), targetMilestone)
                    or string.format("Projects +%d for %s.", estimatedGain, addon.Store:SplitFullName(normalizedToonId, GetRealmName() or "Unknown"))
            elseif mode == "key-goal" and normalizedToonId ~= "" then
                local record = self:GetStoredDungeonRecord(normalizedToonId, row.dungeonId)
                local currentBestLevel = math.max(tonumber(record and record.bestTimedLevel) or 0, tonumber(record and record.bestLevel) or 0)
                local portalUnlocked = isPortalUnlockedForRecord(seasonProfile, record, row.dungeonId, row.dungeonName, normalizedToonId)
                local estimate = self:EstimateScoreGain(normalizedToonId, row.dungeonId, keyLevel)
                estimatedGain = estimate and math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0)) or 0
                reachesGoal = keyLevel >= targetKeyGoal and currentBestLevel < targetKeyGoal and not portalUnlocked
                state = reachesGoal and "Portal" or (keyLevel >= targetKeyGoal and "Goal" or "Below")
                note = reachesGoal
                    and string.format("High enough to unlock this portal if timed for %s.", addon.Store:SplitFullName(normalizedToonId, GetRealmName() or "Unknown"))
                    or string.format("Current best here is %s.", currentBestLevel > 0 and tostring(currentBestLevel) or "-")
            else
                local totalKnownGain = 0
                local knownMembers = 0

                for _, member in ipairs(self:ListCurrentPartyMembers()) do
                    if member.hasMythicData then
                        local estimate = self:EstimateScoreGain(member.fullName, row.dungeonId, keyLevel)

                        if estimate then
                            knownMembers = knownMembers + 1
                            totalKnownGain = totalKnownGain + math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0))
                        end
                    end
                end

                estimatedGain = totalKnownGain
                state = estimatedGain > 0 and "Upgrade" or "Flat"
                note = string.format("Combined projected gain across %d tracked party members.", knownMembers)
            end

            rows[#rows + 1] = {
                dungeonId = row.dungeonId,
                gain = estimatedGain,
                gainLabel = string.format("+%d", estimatedGain),
                holderName = row.name,
                keyLabel = row.key,
                keyLevel = keyLevel,
                note = note,
                reachesGoal = reachesGoal,
                selected = row.selected,
                state = state,
                tooHigh = tooHigh,
            }
        end
    end

    table.sort(rows, function(left, right)
        if left.tooHigh ~= right.tooHigh then
            return not left.tooHigh
        end

        if left.reachesGoal ~= right.reachesGoal then
            return left.reachesGoal
        end

        if left.gain == right.gain then
            if left.keyLevel == right.keyLevel then
                return lowerText(left.holderName) < lowerText(right.holderName)
            end

            return left.keyLevel < right.keyLevel
        end

        return left.gain > right.gain
    end)

    return rows
end

function addon.MythicPlusService:GetBestPartyGoalRecommendation(targetMemberId, analysisMode, milestone, keyGoal)
    local rows = self:ListPartyGoalRecommendationRows(targetMemberId, analysisMode, milestone, keyGoal)
    local partyGuide = self:GetPartyKeyLevelGuide()

    if #rows <= 0 then
        return nil
    end

    local bestRow = rows[1]

    return {
        gain = bestRow.gain,
        holderName = bestRow.holderName,
        keyLabel = bestRow.keyLabel,
        note = bestRow.note,
        recommendedLevel = tonumber(partyGuide and partyGuide.recommendedLevel) or 10,
        state = bestRow.state,
        tooHigh = bestRow.tooHigh,
    }
end

function addon.MythicPlusService:ExportProfiles()
    return deepCopy(self:GetStore())
end

function addon.MythicPlusService:HandleSyncedProfiles()
    if addon.MainFrame and addon.MainFrame.MarkBackgroundRefreshPending then
        addon.MainFrame:MarkBackgroundRefreshPending()
    end

    return true
end

function addon.MythicPlusService:GetDebugReportLines()
    local lines = {}
    local playerId = addon.Store:GetPlayerName()
    local seasonProfile = self:GetStoredSeasonProfile(playerId)
    local runtime = self:GetRuntimeState().openRaid
    local peers = addon.Store:GetSync() and addon.Store:GetSync().peers or {}
    local compressionProvider = getLibDeflate()
    local trackedDungeons = seasonProfile and countTableEntries(seasonProfile.dungeons) or 0
    local currentKeyLevel = seasonProfile and seasonProfile.currentKey and tonumber(seasonProfile.currentKey.level) or 0
    local currentKeyDungeonId = seasonProfile and seasonProfile.currentKey and seasonProfile.currentKey.dungeonId or nil

    lines[#lines + 1] = string.format("Local Mythics: rating=%d key=%s trackedDungeons=%d lastCaptureCount=%d lastCaptureReason=%s",
        seasonProfile and math.floor(tonumber(seasonProfile.overallRating) or 0) or 0,
        currentKeyLevel > 0 and string.format("%s +%d", getMapName(currentKeyDungeonId), currentKeyLevel) or "No Key",
        trackedDungeons,
        seasonProfile and math.floor(tonumber(seasonProfile.lastCaptureCount) or 0) or 0,
        seasonProfile and trimString(seasonProfile.lastCaptureReason) ~= "" and trimString(seasonProfile.lastCaptureReason) or "none")

    lines[#lines + 1] = string.format("OpenRaid: lastRequest=%d channel=%s lastInbound=%s from=%s at=%d lastResponse=%d channel=%s",
        math.floor(tonumber(runtime.lastRequestAt) or 0),
        tostring(runtime.lastRequestChannel or "none"),
        tostring(runtime.lastInboundType or "none"),
        tostring(runtime.lastInboundSender or "none"),
        math.floor(tonumber(runtime.lastInboundAt) or 0),
        math.floor(tonumber(runtime.lastResponseAt) or 0),
        tostring(runtime.lastResponseChannel or "none"))
    lines[#lines + 1] = string.format("OpenRaid State: inGroup=%s inRaid=%s codec=%s partyMembers=%d",
        tostring(IsInGroup and IsInGroup() and true or false),
        tostring(IsInRaid and IsInRaid() and true or false),
        tostring(compressionProvider and compressionProvider.__gh3Source or "none"),
        #buildGroupUnits())
    lines[#lines + 1] = string.format("OpenRaid Failure: %s", tostring(runtime.lastFailureReason or "none"))

    local libKeystoneRuntime = self:GetRuntimeState().libKeystone
    lines[#lines + 1] = string.format("LibKS: lastRequest=%d channel=%s lastInbound=%s from=%s at=%d lastResponse=%d channel=%s",
        math.floor(tonumber(libKeystoneRuntime.lastRequestAt) or 0),
        tostring(libKeystoneRuntime.lastRequestChannel or "none"),
        tostring(libKeystoneRuntime.lastInboundType or "none"),
        tostring(libKeystoneRuntime.lastInboundSender or "none"),
        math.floor(tonumber(libKeystoneRuntime.lastInboundAt) or 0),
        math.floor(tonumber(libKeystoneRuntime.lastResponseAt) or 0),
        tostring(libKeystoneRuntime.lastResponseChannel or "none"))

    local partyKeys = {}

    for fullName, keyData in pairs(runtime.partyKeys or {}) do
        partyKeys[#partyKeys + 1] = string.format("Party Key: %s -> %s +%d | rating=%d | source=%s",
            fullName,
            getMapName(keyData.currentKeyDungeonId),
            math.floor(tonumber(keyData.currentKeyLevel) or 0),
            math.floor(tonumber(keyData.overallRating) or 0),
            tostring(keyData.source or "unknown"))
    end

    for fullName, keyData in pairs(libKeystoneRuntime.partyKeys or {}) do
        partyKeys[#partyKeys + 1] = string.format("Party Key: %s -> %s +%d | rating=%d | source=%s",
            fullName,
            getMapName(keyData.currentKeyDungeonId),
            math.floor(tonumber(keyData.currentKeyLevel) or 0),
            math.floor(tonumber(keyData.overallRating) or 0),
            tostring(keyData.source or "unknown"))
    end

    table.sort(partyKeys)

    if #partyKeys == 0 then
        lines[#lines + 1] = "Party Key: none received"
    else
        for _, line in ipairs(partyKeys) do
            lines[#lines + 1] = line
        end
    end

    local peerLines = {}

    for peerName, peerRecord in pairs(peers or {}) do
        if type(peerRecord) == "table" then
            local mythicsStatus = peerRecord.tables and peerRecord.tables.mythics or nil
            peerLines[#peerLines + 1] = string.format(
                "GH3 Peer: %s pending=%s timedOut=%s mythicsCount=%d mythicsUpdatedAt=%d",
                tostring(peerName),
                tostring(peerRecord.pending and true or false),
                tostring(peerRecord.probeTimedOut and true or false),
                math.floor(tonumber(mythicsStatus and mythicsStatus.count) or 0),
                math.floor(tonumber(mythicsStatus and mythicsStatus.updatedAt) or 0)
            )
        end
    end

    table.sort(peerLines)

    if #peerLines == 0 then
        lines[#lines + 1] = "GH3 Peer: none known"
    else
        for _, line in ipairs(peerLines) do
            lines[#lines + 1] = line
        end
    end

    return lines
end

function addon.MythicPlusService:ListPortalRows(toonId)
    local rows = {}

    for _, row in ipairs(self:ListDungeonRows(toonId)) do
        if row.portalLabel == "Yes" then
            rows[#rows + 1] = row
        end
    end

    return rows
end

function addon.MythicPlusService:ListPartyKeystoneRows(dungeonId, targetLevel)
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local desiredLevel = clampKeyLevel(targetLevel)
    local rows = {}

    local function formatRoleLabel(role)
        local normalizedRole = lowerText(role)

        if normalizedRole == "tank" then
            return "Tank"
        end

        if normalizedRole == "healer" then
            return "Healer"
        end

        if normalizedRole == "dps" then
            return "DPS"
        end

        return "-"
    end

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        local displayName = addon.Store:SplitFullName(member.fullName, GetRealmName() or "Unknown")
        local keyState = "No Data"
        local selectedGainLabel = "-"
        local selectedGainValue = 0

        if member.hasCurrentKey then
            keyState = member.hasMythicData and "Tracked" or "Key Only"
        elseif member.hasMythicData then
            keyState = "Tracked"
        end

        if normalizedDungeonId and member.hasMythicData then
            local estimate = self:EstimateScoreGain(member.fullName, normalizedDungeonId, desiredLevel)

            if estimate then
                selectedGainValue = math.max(0, math.floor(tonumber(estimate.estimatedGain) or 0))
                selectedGainLabel = string.format("+%d", selectedGainValue)
            end
        end

        rows[#rows + 1] = {
            classFile = member.classFile,
            dungeonId = member.currentKeyDungeonId,
            dungeonName = member.currentKeyDungeonId and getMapName(member.currentKeyDungeonId) or nil,
            fullName = member.fullName,
            isPlayer = member.isPlayer and true or false,
            itemLevel = tonumber(member.itemLevel) or 0,
            key = member.currentKeyText,
            keyLevel = tonumber(member.currentKeyLevel) or 0,
            name = displayName,
            selectedGain = selectedGainValue,
            selectedGainLabel = selectedGainLabel,
            roleLabel = formatRoleLabel(member.role),
            selected = member.currentKeyDungeonId == normalizedDungeonId and (tonumber(member.currentKeyLevel) or 0) == desiredLevel,
            state = keyState,
            guildInviteEligible = member.guildInviteEligible and true or false,
            guildName = member.guildName,
        }
    end

    table.sort(rows, function(left, right)
        local leftLevel = tonumber(left.keyLevel) or 0
        local rightLevel = tonumber(right.keyLevel) or 0

        if leftLevel == rightLevel then
            return lowerText(left.name) < lowerText(right.name)
        end

        return leftLevel > rightLevel
    end)

    return rows
end

function addon.MythicPlusService:GetCarryTargetSummary(targetMemberId, milestone)
    local normalizedToonId = addon.Store:NormalizeFullName(targetMemberId, GetRealmName() or "Unknown")
    local targetMilestone = normalizeCarryMilestone(milestone)

    if normalizedToonId == "" then
        return nil
    end

    for _, member in ipairs(self:ListCurrentPartyMembers()) do
        if member.fullName == normalizedToonId then
            local shortName = addon.Store:SplitFullName(member.fullName, GetRealmName() or "Unknown")
            local currentRating = math.floor(tonumber(member.overallRating) or 0)
            local remaining = math.max(targetMilestone - currentRating, 0)

            return {
                currentKeyText = member.currentKeyText,
                currentRating = currentRating,
                fullName = member.fullName,
                milestone = targetMilestone,
                remaining = remaining,
                shortName = shortName,
                summaryLine = remaining > 0
                    and string.format("Needs about %d more score to hit %d.", remaining, targetMilestone)
                    or string.format("Already at or above %d.", targetMilestone),
            }
        end
    end

    return nil
end

function addon.MythicPlusService:GetScoreGoalRecommendationForDungeon(toonId, dungeonId, milestone)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local targetMilestone = normalizeCarryMilestone(milestone)
    local seasonProfile = self:GetSeasonProfile(normalizedToonId)
    local currentRating = math.floor(tonumber(seasonProfile and seasonProfile.overallRating) or 0)
    local lowestUpgradeLevel = nil
    local lowestMilestoneLevel = nil
    local bestGain = 0

    if normalizedToonId == "" or not normalizedDungeonId or not seasonProfile then
        return nil
    end

    for keyLevel = 2, 20 do
        local estimate = self:EstimateScoreGain(normalizedToonId, normalizedDungeonId, keyLevel)
        local gain = estimate and math.max(tonumber(estimate.estimatedGain) or 0, 0) or 0

        if gain > bestGain then
            bestGain = gain
        end

        if gain > 0 and not lowestUpgradeLevel then
            lowestUpgradeLevel = keyLevel
        end

        if gain > 0 and (currentRating + gain) >= targetMilestone and not lowestMilestoneLevel then
            lowestMilestoneLevel = keyLevel
        end
    end

    local recommendedLevel = lowestMilestoneLevel or lowestUpgradeLevel

    if not recommendedLevel then
        return nil
    end

    local estimate = self:EstimateScoreGain(normalizedToonId, normalizedDungeonId, recommendedLevel)
    local recommendedGain = estimate and math.max(tonumber(estimate.estimatedGain) or 0, 0) or 0

    return {
        currentRating = currentRating,
        gain = recommendedGain,
        keyLevel = recommendedLevel,
        reachesMilestone = lowestMilestoneLevel ~= nil,
        remainingAfterRun = math.max(targetMilestone - (currentRating + recommendedGain), 0),
    }
end

function addon.MythicPlusService:GetPartyBreakdownCellLabel(toonId, dungeonId, analysisMode, targetLevel, milestone, keyGoal)
    local normalizedToonId = addon.Store:NormalizeFullName(toonId, GetRealmName() or "Unknown")
    local normalizedDungeonId = normalizeDungeonId(dungeonId)
    local mode = normalizeAnalysisMode(analysisMode)

    if normalizedToonId == "" or not normalizedDungeonId then
        return "-"
    end

    if mode == "score-goal" then
        local targetMilestone = normalizeCarryMilestone(milestone)
        local seasonProfile = self:GetSeasonProfile(normalizedToonId)
        local currentRating = math.floor(tonumber(seasonProfile and seasonProfile.overallRating) or 0)
        local recommendation = self:GetScoreGoalRecommendationForDungeon(normalizedToonId, normalizedDungeonId, targetMilestone)

        if recommendation then
            if recommendation.reachesMilestone then
                return string.format("+%d", recommendation.keyLevel)
            end

            return string.format("~+%d", recommendation.keyLevel)
        end

        if currentRating >= targetMilestone then
            return "Done"
        end

        return "-"
    end

    if mode == "key-goal" then
        local targetKeyGoal = normalizeCarryKeyGoal(keyGoal)
        local dungeonRecord = self:GetStoredDungeonRecord(normalizedToonId, normalizedDungeonId)
        local currentBestLevel = math.max(tonumber(dungeonRecord and dungeonRecord.bestTimedLevel) or 0, tonumber(dungeonRecord and dungeonRecord.bestLevel) or 0)

        if currentBestLevel >= targetKeyGoal then
            return "OK"
        end

        if currentBestLevel > 0 then
            return formatBestRunLabel(dungeonRecord)
        end

        return "-"
    end

    return self:GetPartyDungeonDetailLabel(normalizedToonId, normalizedDungeonId, targetLevel)
end

function addon.MythicPlusService:ListCarryTargetRows(targetMemberId, milestone)
    local normalizedToonId = addon.Store:NormalizeFullName(targetMemberId, GetRealmName() or "Unknown")
    local targetMilestone = normalizeCarryMilestone(milestone)
    local summary = self:GetCarryTargetSummary(normalizedToonId, targetMilestone)
    local rows = {}

    if not summary then
        return rows
    end

    for _, option in ipairs(self:GetAvailableDungeonOptions(normalizedToonId)) do
        local recommendation = self:GetScoreGoalRecommendationForDungeon(normalizedToonId, option.id, targetMilestone)

        if recommendation then
            rows[#rows + 1] = {
                dungeonId = option.id,
                gain = recommendation.gain,
                gainLabel = string.format("+%d", recommendation.gain),
                keyLevel = recommendation.keyLevel,
                keyLabel = string.format("+%d", recommendation.keyLevel),
                name = option.name,
                reachesMilestone = recommendation.reachesMilestone,
                remainingAfterRun = recommendation.remainingAfterRun,
                state = recommendation.reachesMilestone and "Milestone" or "Upgrade",
            }
        end
    end

    table.sort(rows, function(left, right)
        if left.reachesMilestone ~= right.reachesMilestone then
            return left.reachesMilestone
        end

        if left.keyLevel == right.keyLevel then
            if left.gain == right.gain then
                return lowerText(left.name) < lowerText(right.name)
            end

            return left.gain > right.gain
        end

        return left.keyLevel < right.keyLevel
    end)

    return rows
end

function addon.MythicPlusService:ListCarryKeyGoalRows(targetMemberId, keyGoal)
    local normalizedToonId = addon.Store:NormalizeFullName(targetMemberId, GetRealmName() or "Unknown")
    local targetKeyGoal = normalizeCarryKeyGoal(keyGoal)
    local rows = {}

    if normalizedToonId == "" then
        return rows
    end

    for _, option in ipairs(self:GetAvailableDungeonOptions(normalizedToonId)) do
        local dungeonRecord = self:GetStoredDungeonRecord(normalizedToonId, option.id)
        local currentBestLevel = math.max(tonumber(dungeonRecord and dungeonRecord.bestTimedLevel) or 0, tonumber(dungeonRecord and dungeonRecord.bestLevel) or 0)

        if currentBestLevel < targetKeyGoal then
            local estimate = self:EstimateScoreGain(normalizedToonId, option.id, targetKeyGoal)
            local estimatedGain = estimate and math.max(tonumber(estimate.estimatedGain) or 0, 0) or 0

            rows[#rows + 1] = {
                currentBestLabel = formatBestRunLabel(dungeonRecord),
                dungeonId = option.id,
                gain = estimatedGain,
                gainLabel = string.format("+%d", estimatedGain),
                keyLevel = targetKeyGoal,
                keyLabel = string.format("+%d", targetKeyGoal),
                missingLevels = math.max(targetKeyGoal - currentBestLevel, 0),
                name = option.name,
                state = currentBestLevel > 0 and "Below Goal" or "Missing",
            }
        end
    end

    table.sort(rows, function(left, right)
        if left.missingLevels == right.missingLevels then
            if left.gain == right.gain then
                return lowerText(left.name) < lowerText(right.name)
            end

            return left.gain > right.gain
        end

        return left.missingLevels < right.missingLevels
    end)

    return rows
end

normalizeCarryMilestone = function(value)
    local numericValue = math.floor(tonumber(value) or DEFAULT_CARRY_MILESTONE)

    if numericValue >= 3000 then
        return 3000
    end

    if numericValue >= 2500 then
        return 2500
    end

    return 2000
end