local addonName, ns = ...

-- Resolve the NomTools namespace when loaded from a sub-addon TOC.
if addonName ~= "NomTools" then
    ns = _G and _G["NomTools"]
    addonName = ns and ns.ADDON_NAME or "NomTools"
end

if not ns then
    return
end

-- =============================================
-- Cached globals and numeric constants
-- =============================================
-- Consolidated into WOW/K tables so the main chunk stays under Lua 5.1's
-- 200 locals-per-function limit. Each table is a single upvalue; per-call
-- field lookups cost one extra table get, imperceptible at addon scale
-- (well-below the cost of any C API call that follows).
local WOW = {
    C_Timer                      = C_Timer,
    C_TaskQuest                  = C_TaskQuest,
    C_QuestLog                   = C_QuestLog,
    C_Map                        = C_Map,
    C_SuperTrack                 = C_SuperTrack,
    C_Item                       = C_Item,
    C_Reputation                 = C_Reputation,
    C_UnitAuras                  = C_UnitAuras,
    C_Spell                      = C_Spell,
    CreateFrame                  = CreateFrame,
    GameTooltip                  = GameTooltip,
    WorldMapFrame                = WorldMapFrame,
    InCombatLockdown             = InCombatLockdown,
    HaveQuestData                = HaveQuestData,
    HaveQuestRewardData          = HaveQuestRewardData,
    GetQuestObjectiveInfo        = GetQuestObjectiveInfo,
    GetQuestLogQuestText         = GetQuestLogQuestText,
    -- Compat shim: legacy global, referenced via _G so the file still loads
    -- cleanly if Blizzard ever removes it outright on a future patch.
    GetQuestUiMapIDCompat        = _G["GetQuestUiMapID"],
    GetTime                      = GetTime,
    GetServerTime                = GetServerTime,
    QuestUtils_IsQuestWorldQuest = QuestUtils_IsQuestWorldQuest,
    GetQuestLogRewardInfo        = GetQuestLogRewardInfo,
    GetQuestLogRewardMoney       = GetQuestLogRewardMoney,
    -- Legacy globals (GetNumQuestLogRewardCurrencies, GetQuestLogRewardCurrencyInfo,
    -- GetFactionInfoByID, etc.) removed -- replaced with WOW.C_QuestLog / WOW.C_Reputation
    -- questID-specific equivalents for Midnight 12.0.
    math_floor                   = math.floor,
    string_find                  = string.find,
    string_format                = string.format,
    string_gsub                  = string.gsub,
    string_lower                 = string.lower,
    string_sub                   = string.sub,
    table_sort                   = table.sort,
}

local K = {
    -- Display mode key for QuestMapFrame tab registration
    DISPLAY_MODE                           = "NOMTOOLS_WorldQuests",
    ROW_HEIGHT                             = 64,
    LOCKED_ROW_HEIGHT                      = 82,
    ZONE_HEADER_HEIGHT                     = 22,
    DOT_SIZE                               = 10,
    POI_BUTTON_SIZE                        = 22,
    CONTRACT_ICON_SIZE                     = 10,
    REWARD_ICON_SIZE                       = 22,
    REWARD_ICON_SPACING                    = 3,
    MAX_REWARD_ICONS                       = 4,
    TIME_RED                               = 3600,    -- < 1 hour
    TIME_ORANGE                            = 21600,   -- < 6 hours
    TIME_YELLOW                            = 43200,   -- < 12 hours
    QUEST_DATA_RETRY_COOLDOWN              = 30,
    QUEST_CORE_DATA_REQUEST_STALE_TIMEOUT  = 10,
    -- UI map type values (Enum.UIMapType) used for overview query scheduling.
    MAP_TYPE_WORLD                         = (Enum and Enum.UIMapType and Enum.UIMapType.World) or 1,
}

-- Color palette stored as { r, g, b [,a] } tables.
local COL = {
    dotRed       = { 1.0,  0.15, 0.15 },
    dotOrange    = { 1.0,  0.55, 0.15 },
    dotYellow    = { 1.0,  0.85, 0.0  },
    dotOk        = { 0.35, 0.75, 0.35 },
    zoneHeaderBg = { 0.06, 0.07, 0.10, 0.85 },
    zoneLabel    = { 0.78, 0.78, 0.82 },
    rowTitle     = { 1.0,  1.0,  1.0  },
    rowTitleLocked = { 0.62, 0.62, 0.68 },
    rowFaction   = { 0.70, 0.78, 0.88 },
    rowFactionActiveContract = { 0.52, 0.84, 1.0 },
    rowLockedRequirement = { 0.90, 0.68, 0.48 },
    rowTime      = { 0.72, 0.72, 0.76 },
    rewardCount  = { 0.75, 0.75, 0.75 },
    rowHover     = { 1,    1,    1,    0.06 },
    separator    = { 0.15, 0.17, 0.21, 1    },
    noQuests     = { 0.60, 0.60, 0.65 },
    superTrackOn = { 1.0,  0.85, 0.20 },
    superTrackOff= { 0.55, 0.58, 0.64 },
    contractNotice = { 0.62, 0.88, 1.0 },
    contractNoticeNone = { 1.0, 0.32, 0.32 },
}

-- =============================================
-- Scan hierarchy registry
-- Instead of walking every descendant zone from the top-level map, the
-- hierarchy defines specific "scan root" zones whose WOW.C_Map descendants
-- contain world quests.  Intermediate nodes (with children) are waypoints
-- the scanner skips through; leaf nodes (no children) are scan roots whose
-- actual map descendants are walked via WOW.C_Map.GetMapChildrenInfo.
-- =============================================
local hierarchyNodeSet   = {}   -- [mapID] = true for all hierarchy nodes
local hierarchyScanRoots = {}   -- [mapID] = { scanRootID, ... } for waypoints only

do
    ---@param node table
    ---@return number[]
    local function BuildScanHierarchyLookups(node)
        if not node or type(node.mapID) ~= "number" then
            return {}
        end

        hierarchyNodeSet[node.mapID] = true

        if not node.children or #node.children == 0 then
            return { node.mapID }
        end

        local leafRoots = {}
        for childIndex = 1, #node.children do
            local childLeaves = BuildScanHierarchyLookups(node.children[childIndex])
            for leafIndex = 1, #childLeaves do
                leafRoots[#leafRoots + 1] = childLeaves[leafIndex]
            end
        end

        hierarchyScanRoots[node.mapID] = leafRoots
        return leafRoots
    end

    local scanHierarchy = ns.WORLD_QUEST_SCAN_HIERARCHY
    if type(scanHierarchy) == "table" then
        BuildScanHierarchyLookups(scanHierarchy)
    end
end

-- =============================================
-- Module state
-- =============================================
local questMapPanel          -- content frame parented to QuestMapFrame
local questMapTab            -- QuestLogTabButtonTemplate tab button
local worldQuestSearchBox    -- world quests search edit box
local RebuildTabAnchor       -- forward declaration; defined after module state
local ScheduleRefresh        -- forward declaration; defined after layout functions
local EnsureMinuteAlignedTimeUpdates
local StopMinuteAlignedTimeUpdates
local EnsureQuestDataRetryRefresh
local StopQuestDataRetryRefresh
local scrollFrame
local scrollChild
local noQuestsLabel
local contractNoticeFrame
local contractNoticeLabel
local UpdateContractNoticeLayout
local activeTooltipAnchor
local activeTooltipQuestID
local activeTooltipShowTrackHint = false

local activeHoverState = {
    pinMisses = 0,
    surfaceMisses = 0,
    stopToken = 0,
    fxPin = nil,
}
local questRowPool       = {}  -- inactive (recycled) quest row frames
local zoneHeaderPool     = {}  -- inactive zone header frames
local activeContent      = {}  -- ordered list of { type="zone"|"row", frame=f }
local collapseState      = {}  -- collapseState[zoneName] = true when zone is collapsed
local refreshPending         = false
local refreshPendingAnimateRows = true
local displayHookRegistered  = false
local eventFrame             = WOW.CreateFrame("Frame")
eventFrame._activeWorldQuestRawIDs = {}
eventFrame._descendantGatherGeneration = 0
eventFrame._descendantGatherSession = nil
eventFrame._currentQuestEntriesByID = {}
eventFrame._sessionScannedQueryMapIDs = {}
eventFrame._sessionRawEntries = {}
eventFrame._sessionRawEntriesSeen = {}
eventFrame._sessionEnrichedEntries = {}
eventFrame._sessionEnrichedEntriesSeen = {}
eventFrame._sessionQueryStateCache = {}
eventFrame._excludedMaps = {}
eventFrame._typographyVersion = 0
eventFrame._DebugHoverTrace = function(tag, fmt, ...)
    if not ns.IsDebugEnabled() then
        return
    end

    local message
    local count = select("#", ...)

    if type(fmt) == "string" then
        if count > 0 then
            local ok, formatted = pcall(WOW.string_format, fmt, ...)
            if ok then
                message = formatted
            else
                local parts = { fmt }
                for index = 1, count do
                    parts[#parts + 1] = tostring(select(index, ...))
                end
                message = table.concat(parts, " ")
            end
        else
            message = fmt
        end
    elseif fmt == nil then
        if count > 0 then
            local parts = {}
            for index = 1, count do
                parts[index] = tostring(select(index, ...))
            end
            message = table.concat(parts, " ")
        else
            message = ""
        end
    elseif count > 0 then
        local parts = { tostring(fmt) }
        for index = 1, count do
            parts[#parts + 1] = tostring(select(index, ...))
        end
        message = table.concat(parts, " ")
    else
        message = tostring(fmt)
    end

    if message and message ~= "" then
        ns.DebugPrint(WOW.string_format("WQ Hover: %s %s", tostring(tag), message))
    else
        ns.DebugPrint("WQ Hover: " .. tostring(tag))
    end
end

---@param row Frame?
---@return string
eventFrame._GetHoverRowIdentity = function(row)
    if not ns.IsDebugEnabled() then
        return ""
    end

    if not row then
        return "nil"
    end

    local rowQuestID = rawget(row, "questID")
    return WOW.string_format("%s(q=%s)", tostring(row), tostring(rowQuestID))
end
local currentRelevantContract
local currentQuestEntries    = {}
local minuteUpdateGeneration = 0
local minuteUpdateActive     = false
local retryRefreshGeneration = 0
local retryRefreshDueAt
local pendingDisplayModeSource
local pendingBuiltinDisplayModeFallback

-- â”€â”€ Performance caches â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- mapDescendantsCache: stable per mapID â€” the map hierarchy never changes
-- at runtime, so we compute each entry once and keep it for the session.
local mapDescendantsCache = {}
-- Incremented each LayoutScrollContent call.  Batch callbacks capture their
-- generation at creation time and self-cancel if it no longer matches.
local layoutGeneration  = 0

-- â”€â”€ Pending quest data tracking â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Set of questIDs visible in the current panel whose data has NOT fully loaded.
-- Each entry tracks quest-data readiness separately from reward-data readiness
-- so the first non-animated rebuild can happen as soon as ordering inputs are
-- usable, while still allowing one later rebuild if reward-derived state lands.
local pendingQuestIDs   = {}   -- [questID] = { needsQuestData, needsRewardData, questDataRefreshDone, searchRefreshOnRewardReady }
-- Tracks which questIDs have been requested via RequestLoadQuestByID this
-- session to avoid redundant server requests.
local requestedQuestData = {}  -- [questID] = requestedAt timestamp
local questDataRetrySuppressedUntil = {} -- [questID] = WOW.GetTime() + cooldown after load failure
local rewardPreloadState = {
    queuedQuestIDs = {},
    requestedQuestIDs = {},
    recordedRequestTime = {},  -- Track request timestamps for timeout-based re-requests
    requestRetryCooldown = 5,
    queue = {},
    queueHead = 1,
    queueTail = 0,
    drainScheduled = false,
    drainGeneration = 0,
    pollGeneration = 0,
    pollActive = false,
    drainCycleCounter = 0,  -- Anti-starvation counter: cycles through [0-4], pop from tail when 4
    itemLoadState = {
        pending = {},
        completed = {},
    },
}

-- â”€â”€ Filter / sort state â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- These are runtime copies of  ns.db.worldQuests.filter* / sort* values,
-- kept in locals so the hot path (GatherQuestsForCurrentMap) does not call
-- GetSettings() on every refresh.
local filterSearch      = ""   -- text typed in the search bar (session-only)
local filterTypes       = {}   -- set of quest-type strings to EXCLUDE (empty = show all)
local filterRewards     = {}   -- set of reward-type strings to EXCLUDE (empty = show all)
local sortMode          = "time"  -- quest sort: "time" | "alpha" | "reward" | "faction"
local zoneSortMode      = "time"  -- zone sort: "time" | "alpha"

-- Called once at startup and whenever settings are loaded/changed.
local function SyncFilterState(clearSearch)
    local s = ns.GetWorldQuestsSettings and ns.GetWorldQuestsSettings() or {}
    if clearSearch then
        filterSearch = ""  -- never persisted; always starts empty
        if worldQuestSearchBox then
            local currentText = worldQuestSearchBox:GetText()
            if currentText and currentText ~= "" then
                worldQuestSearchBox:SetText("")
            end
        end
    end
    -- filterVersion guards against loading saved data that was written by the
    -- old inclusion-set logic (version < 2).  If absent or stale, clear all
    -- filter sets so the exclusion-set defaults (empty = show all) take effect.
    if not s.filterVersion or s.filterVersion < 2 then
        filterTypes   = {}
        filterRewards = {}
        sortMode      = "time"
        zoneSortMode  = "time"
        -- Initialize excluded maps from defaults
        wipe(eventFrame._excludedMaps)
        local defaults = ns.WORLD_QUEST_DEFAULT_EXCLUDED_MAPS
        if defaults then
            for mapID, excluded in pairs(defaults) do
                if excluded then
                    eventFrame._excludedMaps[mapID] = true
                end
            end
        end
        if ns.SetWorldQuestsSetting then
            ns.SetWorldQuestsSetting("filterVersion", 2)
            ns.SetWorldQuestsSetting("filterTypes",   filterTypes)
            ns.SetWorldQuestsSetting("filterRewards", filterRewards)
            ns.SetWorldQuestsSetting("sortMode",      sortMode)
            ns.SetWorldQuestsSetting("zoneSortMode",  zoneSortMode)
            local excludedMapsCopy = {}
            for mapID, excluded in pairs(eventFrame._excludedMaps) do
                excludedMapsCopy[mapID] = excluded
            end
            ns.SetWorldQuestsSetting("excludedMaps", excludedMapsCopy)
        end
    else
        local normalizedFilters = false
        if type(s.filterTypes) == "table" then
            filterTypes = s.filterTypes
        else
            filterTypes = {}
            normalizedFilters = s.filterTypes ~= nil
        end

        if type(s.filterRewards) == "table" then
            filterRewards = s.filterRewards
        else
            filterRewards = {}
            normalizedFilters = normalizedFilters or s.filterRewards ~= nil
        end

        sortMode      = s.sortMode      or "time"
        zoneSortMode  = s.zoneSortMode  or "time"

        if normalizedFilters and ns.SetWorldQuestsSetting then
            ns.SetWorldQuestsSetting("filterTypes", filterTypes)
            ns.SetWorldQuestsSetting("filterRewards", filterRewards)
        end

        -- Load excluded maps from settings, falling back to defaults
        if type(s.excludedMaps) == "table" then
            wipe(eventFrame._excludedMaps)
            for mapID, excluded in pairs(s.excludedMaps) do
                if excluded then
                    eventFrame._excludedMaps[mapID] = true
                end
            end
        else
            wipe(eventFrame._excludedMaps)
            local defaults = ns.WORLD_QUEST_DEFAULT_EXCLUDED_MAPS
            if defaults then
                for mapID, excluded in pairs(defaults) do
                    if excluded then
                        eventFrame._excludedMaps[mapID] = true
                    end
                end
            end
            -- Persist the defaults so they show up correctly in the options UI
            if ns.SetWorldQuestsSetting then
                local toSave = {}
                for mapID, excluded in pairs(eventFrame._excludedMaps) do
                    toSave[mapID] = excluded
                end
                ns.SetWorldQuestsSetting("excludedMaps", toSave)
            end
        end
    end
end

local function SaveFilterState()
    if not ns.SetWorldQuestsSetting then return end
    -- filterSearch is intentionally not saved (session-only)
    ns.SetWorldQuestsSetting("filterTypes",   filterTypes)
    ns.SetWorldQuestsSetting("filterRewards", filterRewards)
    ns.SetWorldQuestsSetting("sortMode",      sortMode)
    ns.SetWorldQuestsSetting("zoneSortMode",  zoneSortMode)
    local excludedMapsCopy = {}
    for mapID, excluded in pairs(eventFrame._excludedMaps) do
        if excluded then
            excludedMapsCopy[mapID] = true
        end
    end
    ns.SetWorldQuestsSetting("excludedMaps", excludedMapsCopy)
end

function eventFrame:IsWQFilterDefault()
    if next(filterTypes) then return false end
    if next(filterRewards) then return false end
    if sortMode ~= "time" then return false end
    if zoneSortMode ~= "time" then return false end
    return true
end

function eventFrame:UpdateWQFilterResetState()
    -- No-op: reset is now inside the dropdown menu
end

---@param mapID number
---@return boolean
function eventFrame:IsWorldQuestMapExcluded(mapID)
    return self._excludedMaps[mapID] == true
end

---@param mapID number
---@param excluded boolean|nil
function eventFrame:SetWorldQuestMapExcluded(mapID, excluded)
    if excluded then
        self._excludedMaps[mapID] = true
    else
        self._excludedMaps[mapID] = nil
    end
    SaveFilterState()
    wipe(self._sessionQueryStateCache)
    wipe(self._sessionScannedQueryMapIDs)
    self._sessionEnrichedEntries = {}
    self._sessionEnrichedEntriesSeen = {}
    rewardPreloadState.StopPoll()
    ScheduleRefresh(false, "Excluded maps changed")
end

ns.IsWorldQuestMapExcluded = function(mapID)
    return eventFrame:IsWorldQuestMapExcluded(mapID)
end

ns.SetWorldQuestMapExcluded = function(mapID, excluded)
    eventFrame:SetWorldQuestMapExcluded(mapID, excluded)
end

local function UpdateQuestLogUpdateRegistration()
    local shouldRegister = false
    local hasVisibleLockedAreaPOI = false
    local isRefreshContextActive = ns.IsWorldQuestsRefreshContextActive()
    local visibleLockedAreaPOIWidgetSetIDs = eventFrame._visibleLockedAreaPOIWidgetSetIDs or {}
    local visibleLockedAreaPOIWidgetSetCount = 0

    eventFrame._visibleLockedAreaPOIWidgetSetIDs = visibleLockedAreaPOIWidgetSetIDs
    wipe(visibleLockedAreaPOIWidgetSetIDs)

    if isRefreshContextActive then
        shouldRegister = next(pendingQuestIDs) ~= nil
        if currentQuestEntries and #currentQuestEntries > 0 then
            for _, entry in ipairs(currentQuestEntries) do
                local questID = entry.questID
                if entry.isAreaPOI and entry.isLocked then
                    hasVisibleLockedAreaPOI = true

                    local widgetSetID = entry.tooltipWidgetSet
                    if widgetSetID and widgetSetID > 0
                        and not visibleLockedAreaPOIWidgetSetIDs[widgetSetID]
                    then
                        visibleLockedAreaPOIWidgetSetIDs[widgetSetID] = true
                        visibleLockedAreaPOIWidgetSetCount =
                            visibleLockedAreaPOIWidgetSetCount + 1
                    end

                    if not shouldRegister then
                        shouldRegister = true
                    end
                elseif not shouldRegister and questID and questID > 0
                    and not entry.isAreaPOI
                then
                    shouldRegister = true
                end
            end
        end
    end

    eventFrame._visibleLockedAreaPOIWidgetSetCount = visibleLockedAreaPOIWidgetSetCount

    if not hasVisibleLockedAreaPOI then
        eventFrame._visibleLockedAreaPOISnapshot = nil
    end

    local shouldRegisterWidgetUpdates = isRefreshContextActive
        and (visibleLockedAreaPOIWidgetSetCount > 0
            or (eventFrame._liveRelevantAreaPOIWidgetSetCount or 0) > 0)

    if shouldRegister then
        if not eventFrame._questLogUpdateRegistered then
            eventFrame:RegisterEvent("QUEST_LOG_UPDATE")
            eventFrame._questLogUpdateRegistered = true
        end
        if not eventFrame._itemDataLoadResultRegistered then
            eventFrame:RegisterEvent("ITEM_DATA_LOAD_RESULT")
            eventFrame._itemDataLoadResultRegistered = true
        end
        if not eventFrame._getItemInfoReceivedRegistered then
            eventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
            eventFrame._getItemInfoReceivedRegistered = true
        end
    else
        if eventFrame._questLogUpdateRegistered then
            eventFrame:UnregisterEvent("QUEST_LOG_UPDATE")
            eventFrame._questLogUpdateRegistered = false
        end
        if eventFrame._itemDataLoadResultRegistered then
            eventFrame:UnregisterEvent("ITEM_DATA_LOAD_RESULT")
            eventFrame._itemDataLoadResultRegistered = false
        end
        if eventFrame._getItemInfoReceivedRegistered then
            eventFrame:UnregisterEvent("GET_ITEM_INFO_RECEIVED")
            eventFrame._getItemInfoReceivedRegistered = false
        end
    end

    if shouldRegisterWidgetUpdates then
        if not eventFrame._uiWidgetUpdateRegistered then
            eventFrame:RegisterEvent("UPDATE_UI_WIDGET")
            eventFrame._uiWidgetUpdateRegistered = true
        end
        if not eventFrame._allUiWidgetUpdatesRegistered then
            eventFrame:RegisterEvent("UPDATE_ALL_UI_WIDGETS")
            eventFrame._allUiWidgetUpdatesRegistered = true
        end
    else
        if eventFrame._uiWidgetUpdateRegistered then
            eventFrame:UnregisterEvent("UPDATE_UI_WIDGET")
            eventFrame._uiWidgetUpdateRegistered = false
        end
        if eventFrame._allUiWidgetUpdatesRegistered then
            eventFrame:UnregisterEvent("UPDATE_ALL_UI_WIDGETS")
            eventFrame._allUiWidgetUpdatesRegistered = false
        end
    end
end

local function PruneQuestRequestBookkeeping(activeQuestIDs)
    local activeRawQuestIDs = eventFrame._activeWorldQuestRawIDs
    local completedItemLoads = rewardPreloadState.itemLoadState.completed

    if not activeQuestIDs then
        rewardPreloadState.CancelDrain()
        wipe(activeRawQuestIDs)
        wipe(pendingQuestIDs)
        wipe(requestedQuestData)
        wipe(questDataRetrySuppressedUntil)
        wipe(rewardPreloadState.requestedQuestIDs)
        wipe(rewardPreloadState.queuedQuestIDs)
        wipe(rewardPreloadState.recordedRequestTime)
        wipe(rewardPreloadState.queue)
        wipe(completedItemLoads)
        rewardPreloadState.queueHead = 1
        rewardPreloadState.queueTail = 0
        return
    end

    for questID in pairs(activeRawQuestIDs) do
        if not activeQuestIDs[questID] then
            activeRawQuestIDs[questID] = nil
        end
    end

    for questID in pairs(activeQuestIDs) do
        activeRawQuestIDs[questID] = true
    end

    for questID in pairs(pendingQuestIDs) do
        if not activeQuestIDs[questID] then
            pendingQuestIDs[questID] = nil
        end
    end

    for questID in pairs(requestedQuestData) do
        if not activeQuestIDs[questID] then
            requestedQuestData[questID] = nil
        end
    end

    for questID in pairs(questDataRetrySuppressedUntil) do
        if not activeQuestIDs[questID] then
            questDataRetrySuppressedUntil[questID] = nil
        end
    end

    for questID in pairs(rewardPreloadState.requestedQuestIDs) do
        if not activeQuestIDs[questID] then
            rewardPreloadState.requestedQuestIDs[questID] = nil
            rewardPreloadState.queuedQuestIDs[questID] = nil
            rewardPreloadState.recordedRequestTime[questID] = nil
        end
    end

    for questID in pairs(rewardPreloadState.queuedQuestIDs) do
        if not activeQuestIDs[questID] then
            rewardPreloadState.queuedQuestIDs[questID] = nil
            rewardPreloadState.recordedRequestTime[questID] = nil
        end
    end

    for key in pairs(completedItemLoads) do
        local questID = tonumber(key:match("^(%d+):"))
        if not questID or not activeQuestIDs[questID] then
            completedItemLoads[key] = nil
        end
    end

    -- Clean up any orphaned recordedRequestTime entries for quests no longer active
    for questID in pairs(rewardPreloadState.recordedRequestTime) do
        if not activeQuestIDs[questID] then
            rewardPreloadState.recordedRequestTime[questID] = nil
        end
    end
end

local function ResolvePendingQuestDataState(questID)
    local state = pendingQuestIDs[questID]
    if not state then
        return false, false, false
    end

    local hasQuestData = eventFrame:IsQuestCoreDataReady(questID)
    local hasRewardData = not WOW.HaveQuestRewardData or WOW.HaveQuestRewardData(questID)
    if hasRewardData and WOW.HaveQuestRewardData and WOW.HaveQuestRewardData(questID) then
        -- HaveQuestRewardData can be true before C_Item finishes hydrating reward item rows
        -- (ITEM_DATA_LOAD_RESULT may follow QUEST_DATA_LOAD_RESULT by tens of ms).
        hasRewardData = rewardPreloadState.AreQuestRewardItemClientCachesReady(questID)
    end
    local rowNeedsUpdate = false
    local needsFullRefresh = false
    local searchRefreshOnRewardReady = false

    if state.needsQuestData and hasQuestData then
        state.needsQuestData = false
        rowNeedsUpdate = true
        if not state.questDataRefreshDone then
            state.questDataRefreshDone = true
            needsFullRefresh = true
        end
    end

    if hasQuestData then
        requestedQuestData[questID] = nil
        questDataRetrySuppressedUntil[questID] = nil
    end

    if state.needsRewardData and hasRewardData then
        state.needsRewardData = false
        searchRefreshOnRewardReady = state.searchRefreshOnRewardReady == true
        state.searchRefreshOnRewardReady = nil
        rewardPreloadState.requestedQuestIDs[questID] = nil
        rewardPreloadState.recordedRequestTime[questID] = nil
        rewardPreloadState.queuedQuestIDs[questID] = nil
        rowNeedsUpdate = true
    end

    if not state.needsRewardData then
        state.searchRefreshOnRewardReady = nil
    end

    if not state.needsQuestData and not state.needsRewardData then
        pendingQuestIDs[questID] = nil
    end

    return rowNeedsUpdate, needsFullRefresh, searchRefreshOnRewardReady
end

function rewardPreloadState.IsQuestRewardDisplayReady(questOrEntry)
    local questID

    if type(questOrEntry) == "table" then
        -- Locked special assignments on the map are Area POI rows; bounty lock quest
        -- rewards (and item client cache) must be ready, not only the synthetic row.
        if questOrEntry.isAreaPOI == true and questOrEntry.isLocked then
            local bountyID = ResolveBountyLockQuestIDFromQuestEntry(questOrEntry)
            if not bountyID then
                return true
            end
            return rewardPreloadState.IsQuestRewardDisplayReady(bountyID)
        end
        if questOrEntry.isAreaPOI == true or questOrEntry.isLocked then
            return true
        end
        questID = questOrEntry.questID
    else
        questID = questOrEntry
    end

    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    if not WOW.HaveQuestRewardData then
        return true
    end

    if not WOW.HaveQuestRewardData(questID) then
        return false
    end

    return rewardPreloadState.AreQuestRewardItemClientCachesReady(questID)
end

function rewardPreloadState.HasFreshRequest(questID, now)
    local requestedAt = rewardPreloadState.requestedQuestIDs[questID]
    if not requestedAt then
        return false
    end

    if type(requestedAt) ~= "number" then
        rewardPreloadState.requestedQuestIDs[questID] = nil
        rewardPreloadState.recordedRequestTime[questID] = nil
        return false
    end

    local currentTime = now or WOW.GetTime()
    local retryWindow = requestedAt + rewardPreloadState.requestRetryCooldown
    
    -- Check cooldown: this is the primary retry gate.
    if retryWindow > currentTime then
        return true
    end
    
    -- Defensive timeout for stale metadata: if a request lingers unusually long,
    -- clear tracking explicitly even if the cooldown gate has already elapsed.
    local recordedTime = rewardPreloadState.recordedRequestTime[questID]
    if recordedTime and recordedTime + 10 <= currentTime then
        -- Timeout exceeded, clear and allow retry
        rewardPreloadState.requestedQuestIDs[questID] = nil
        rewardPreloadState.recordedRequestTime[questID] = nil
        return false
    end

    rewardPreloadState.requestedQuestIDs[questID] = nil
    rewardPreloadState.recordedRequestTime[questID] = nil
    return false
end

function rewardPreloadState.ResetQueue()
    wipe(rewardPreloadState.queue)
    rewardPreloadState.queueHead = 1
    rewardPreloadState.queueTail = 0
    rewardPreloadState.drainCycleCounter = 0
end

function rewardPreloadState.CancelDrain()
    rewardPreloadState.drainGeneration = rewardPreloadState.drainGeneration + 1
    rewardPreloadState.drainScheduled = false
    rewardPreloadState.drainCycleCounter = 0
    -- Explicit queue cleanup to prevent zombie queue state persisting
    rewardPreloadState.queue = {}
    rewardPreloadState.queueHead = 1
    rewardPreloadState.queueTail = 0
end

function rewardPreloadState.CompactQueue()
    local head = rewardPreloadState.queueHead
    local tail = rewardPreloadState.queueTail

    if head <= 1 then
        return
    end

    if head > tail then
        rewardPreloadState.ResetQueue()
        return
    end

    local newTail = tail - head + 1
    for index = 1, newTail do
        rewardPreloadState.queue[index] = rewardPreloadState.queue[head + index - 1]
    end

    for index = newTail + 1, tail do
        rewardPreloadState.queue[index] = nil
    end

    rewardPreloadState.queueHead = 1
    rewardPreloadState.queueTail = newTail
end

function rewardPreloadState.PopQueuedQuestID()
    local head = rewardPreloadState.queueHead
    local tail = rewardPreloadState.queueTail

    if head > tail then
        return nil
    end

    local questID = rewardPreloadState.queue[head]
    rewardPreloadState.queue[head] = nil
    rewardPreloadState.queueHead = head + 1

    if rewardPreloadState.queueHead > tail then
        rewardPreloadState.ResetQueue()
    else
        local remainingCount = tail - rewardPreloadState.queueHead + 1
        if rewardPreloadState.queueHead > 32
            and rewardPreloadState.queueHead > remainingCount
        then
            rewardPreloadState.CompactQueue()
        end
    end

    return questID
end

function rewardPreloadState.PopQuestFromTail()
    local head = rewardPreloadState.queueHead
    local tail = rewardPreloadState.queueTail

    if head > tail then
        return nil
    end

    local questID = rewardPreloadState.queue[tail]
    rewardPreloadState.queue[tail] = nil
    rewardPreloadState.queueTail = tail - 1

    if rewardPreloadState.queueHead > rewardPreloadState.queueTail then
        rewardPreloadState.ResetQueue()
    end

    return questID
end

function rewardPreloadState.ScheduleDrain()
    if rewardPreloadState.drainScheduled
        or not WOW.C_TaskQuest
        or not WOW.C_TaskQuest.RequestPreloadRewardData
    then
        return
    end

    local myGeneration = rewardPreloadState.drainGeneration
    rewardPreloadState.drainScheduled = true
    WOW.C_Timer.After(0.02, function()
        if rewardPreloadState.drainGeneration ~= myGeneration then
            return
        end

        rewardPreloadState.drainScheduled = false

        if not ns.IsWorldQuestsRefreshContextActive() then
            return
        end

        local now = WOW.GetTime()
        local DRAIN_BATCH_SIZE = 5
        for _ = 1, DRAIN_BATCH_SIZE do
            -- Anti-starvation: every 5th item, pop from tail (low-priority)
            -- Otherwise pop from head (high-priority)
            local questID
            if rewardPreloadState.drainCycleCounter == 4 then
                questID = rewardPreloadState.PopQuestFromTail()
                rewardPreloadState.drainCycleCounter = 0
            else
                questID = rewardPreloadState.PopQueuedQuestID()
                rewardPreloadState.drainCycleCounter = rewardPreloadState.drainCycleCounter + 1
            end

            if not questID then
                rewardPreloadState.drainCycleCounter = 0  -- Reset counter when queue empties
                break
            end

            rewardPreloadState.queuedQuestIDs[questID] = nil

            local retrySuppressedUntil = questDataRetrySuppressedUntil[questID]
            local questDataSuppressed = retrySuppressedUntil
                and retrySuppressedUntil > now

            if not rewardPreloadState.IsQuestRewardDisplayReady(questID)
                and not rewardPreloadState.HasFreshRequest(questID, now)
                and not (questDataSuppressed and not eventFrame:IsQuestCoreDataReady(questID))
            then
                rewardPreloadState.requestedQuestIDs[questID] = now
                rewardPreloadState.recordedRequestTime[questID] = now
                WOW.C_TaskQuest.RequestPreloadRewardData(questID)
                EnsureQuestDataRetryRefresh()
            end
        end

        if rewardPreloadState.queueHead <= rewardPreloadState.queueTail then
            rewardPreloadState.ScheduleDrain()
        end
    end)
end

function rewardPreloadState.DrainSyncPass(maxItems)
    -- Synchronously process UP TO maxItems queued items immediately
    -- Used in RefreshPanel to drain what was queued before async schedule fires,
    -- closing the race window where quests can get stuck.
    local processed = 0
    local generation = rewardPreloadState.drainGeneration
    
    while processed < maxItems and rewardPreloadState.queueHead <= rewardPreloadState.queueTail do
        local questID = rewardPreloadState.queue[rewardPreloadState.queueHead]
        rewardPreloadState.queueHead = rewardPreloadState.queueHead + 1
        processed = processed + 1

        if questID then
            rewardPreloadState.queuedQuestIDs[questID] = nil
        end
        
        if questID and rewardPreloadState.drainGeneration == generation then
            local now = WOW.GetTime()
            local retrySuppressedUntil = questDataRetrySuppressedUntil[questID]
            local questDataSuppressed = retrySuppressedUntil and retrySuppressedUntil > now

            -- Check if reward data ready now or if we need to retry request
            if rewardPreloadState.IsQuestRewardDisplayReady(questID) then
                ResolvePendingQuestDataState(questID)
            elseif not rewardPreloadState.HasFreshRequest(questID, now)
                and not (questDataSuppressed and not eventFrame:IsQuestCoreDataReady(questID))
            then
                -- Re-request if old request hasn't been fulfilled
                WOW.C_TaskQuest.RequestPreloadRewardData(questID)
                rewardPreloadState.requestedQuestIDs[questID] = now
                rewardPreloadState.recordedRequestTime[questID] = now
            end
        elseif rewardPreloadState.drainGeneration ~= generation then
            break  -- Generation changed, abort
        end
    end
    
    if rewardPreloadState.queueHead > rewardPreloadState.queueTail then
        rewardPreloadState.CompactQueue()
    end
    
    return processed
end

function rewardPreloadState.ScheduleFallbackGuarantee()
    -- Schedule a safety check: if pending quests exist, force immediate poll
    -- after 1.1 seconds to catch any quests that slipped through the drain/poll chain
    
    -- Only schedule if we have pending quests
    if not next(pendingQuestIDs or {}) then
        return
    end
    
    -- Use generation token to abort previous scheduled fallback if any
    if not rewardPreloadState.fallbackGuaranteeGeneration then
        rewardPreloadState.fallbackGuaranteeGeneration = 0
    end
    rewardPreloadState.fallbackGuaranteeGeneration = rewardPreloadState.fallbackGuaranteeGeneration + 1
    local myGeneration = rewardPreloadState.fallbackGuaranteeGeneration
    
    WOW.C_Timer.After(1.1, function()
        -- If generation changed, abort
        if rewardPreloadState.fallbackGuaranteeGeneration ~= myGeneration then
            return
        end
        
        -- If quests are STILL pending after >1s, force poll immediately
        if next(pendingQuestIDs or {}) then
            rewardPreloadState.PollPendingRewardData()
        end
    end)
end

function rewardPreloadState.QueueQuestRewardPreload(questID, prioritize)
    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    local now = WOW.GetTime()

    if not WOW.HaveQuestRewardData
        or not WOW.C_TaskQuest
        or not WOW.C_TaskQuest.RequestPreloadRewardData
    then
        return true
    end

    if rewardPreloadState.IsQuestRewardDisplayReady(questID) then
        rewardPreloadState.queuedQuestIDs[questID] = nil
        rewardPreloadState.requestedQuestIDs[questID] = nil
        rewardPreloadState.recordedRequestTime[questID] = nil
        return true
    end

    local retrySuppressedUntil = questDataRetrySuppressedUntil[questID]
    if retrySuppressedUntil and retrySuppressedUntil > now
        and not eventFrame:IsQuestCoreDataReady(questID)
    then
        return false
    end

    if rewardPreloadState.HasFreshRequest(questID, now) then
        return false
    end

    if rewardPreloadState.queuedQuestIDs[questID] then
        if prioritize then
            local head = rewardPreloadState.queueHead
            for index = head, rewardPreloadState.queueTail do
                if rewardPreloadState.queue[index] == questID then
                    if index > head then
                        for shiftIndex = index, head + 1, -1 do
                            rewardPreloadState.queue[shiftIndex] = rewardPreloadState.queue[shiftIndex - 1]
                        end
                        rewardPreloadState.queue[head] = questID
                    end
                    break
                end
            end
        end

        rewardPreloadState.ScheduleDrain()
        return false
    end

    rewardPreloadState.queuedQuestIDs[questID] = true
    if prioritize then
        local head = rewardPreloadState.queueHead
        local tail = rewardPreloadState.queueTail
        if head > tail then
            rewardPreloadState.queueHead = 1
            rewardPreloadState.queueTail = 1
            rewardPreloadState.queue[1] = questID
        elseif head > 1 then
            head = head - 1
            rewardPreloadState.queueHead = head
            rewardPreloadState.queue[head] = questID
        else
            for index = tail, head, -1 do
                rewardPreloadState.queue[index + 1] = rewardPreloadState.queue[index]
            end
            rewardPreloadState.queue[head] = questID
            rewardPreloadState.queueTail = tail + 1
        end
    else
        local nextTail = rewardPreloadState.queueTail + 1
        rewardPreloadState.queueTail = nextTail
        rewardPreloadState.queue[nextTail] = questID
    end

    rewardPreloadState.ScheduleDrain()
    return false
end

function rewardPreloadState.StartPoll()
    if rewardPreloadState.pollActive then return end
    rewardPreloadState.pollActive = true
    rewardPreloadState.pollGeneration = rewardPreloadState.pollGeneration + 1
    local myGeneration = rewardPreloadState.pollGeneration

    local function tick()
        if rewardPreloadState.pollGeneration ~= myGeneration then return end
        if not ns.IsWorldQuestsRefreshContextActive() then
            rewardPreloadState.pollActive = false
            return
        end
        if rewardPreloadState.PollPendingRewardData() then
            WOW.C_Timer.After(1.0, tick)
        else
            rewardPreloadState.pollActive = false
        end
    end

    WOW.C_Timer.After(1.0, tick)
end

function rewardPreloadState.StopPoll()
    rewardPreloadState.pollGeneration = rewardPreloadState.pollGeneration + 1
    rewardPreloadState.pollActive = false
end

local function SuppressPendingQuestDataAfterFailure(questID)
    requestedQuestData[questID] = nil

    if not ns.IsWorldQuestsRefreshContextActive() then
        return
    end

    if not eventFrame._activeWorldQuestRawIDs[questID] then
        return
    end

    questDataRetrySuppressedUntil[questID] = WOW.GetTime() + K.QUEST_DATA_RETRY_COOLDOWN
    local pendingState = pendingQuestIDs[questID]
    if pendingState then
        pendingState.needsQuestData = true
        pendingState.questDataRefreshDone = false
    else
        pendingQuestIDs[questID] = {
            needsQuestData = true,
            needsRewardData = false,
            questDataRefreshDone = false,
        }
    end
end

-- Repositions questMapTab below the last visible tab that isn't ours.
-- Defined at module scope so both CreateQuestMapTab and the OnShow hook
-- (in a different function scope) can reference it.
RebuildTabAnchor = function()
    if not questMapTab then return end
    if WOW.InCombatLockdown() then
        eventFrame._pendingTabAnchorRebuild = true
        if not eventFrame._displayModeRetryRegistered then
            eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
            eventFrame._displayModeRetryRegistered = true
        end
        return
    end

    eventFrame._pendingTabAnchorRebuild = nil
    local anchor
    if QuestMapFrame and QuestMapFrame.TabButtons then
        for i = #QuestMapFrame.TabButtons, 1, -1 do
            local b = QuestMapFrame.TabButtons[i]
            if b and b ~= questMapTab and b:IsShown() then
                anchor = b
                break
            end
        end
    end
    anchor = anchor
             or (QuestMapFrame and QuestMapFrame.MapLegendTab)
             or (QuestMapFrame and QuestMapFrame.QuestsTab)
    questMapTab:ClearAllPoints()
    if anchor then
        questMapTab:SetPoint("TOP", anchor, "BOTTOM", 0, -3)
    else
        questMapTab:SetPoint("TOPRIGHT", QuestMapFrame, "TOPRIGHT", -6, -100)
    end
end



-- =============================================
-- Settings access
-- =============================================
local function GetSettings()
    return ns.GetWorldQuestsSettings and ns.GetWorldQuestsSettings() or {}
end

local function IsModuleEnabled(settings)
    local resolvedSettings = settings or GetSettings()
    local enabled = resolvedSettings and resolvedSettings.enabled
    if ns.IsModuleRuntimeEnabled then
        return ns.IsModuleRuntimeEnabled("worldQuests", enabled)
    end

    return enabled ~= false
end

local function HasOpenQuestDetailsFrame()
    if not QuestMapFrame then
        return false
    end

    local detailsFrame = QuestMapFrame.DetailsFrame
    return detailsFrame and detailsFrame.IsShown and detailsFrame:IsShown() or false
end

local function UpdateDisplayModeRetryRegistration()
    if pendingDisplayModeSource
        or pendingBuiltinDisplayModeFallback
        or eventFrame._pendingTabAnchorRebuild
    then
        if not eventFrame._displayModeRetryRegistered then
            eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
            eventFrame._displayModeRetryRegistered = true
        end
    elseif eventFrame._displayModeRetryRegistered then
        eventFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
        eventFrame._displayModeRetryRegistered = false
    end
end

local function ClearPendingDisplayModeRequest()
    pendingDisplayModeSource = nil
    UpdateDisplayModeRetryRegistration()
end

local function ClearPendingBuiltinDisplayModeFallback()
    pendingBuiltinDisplayModeFallback = nil
    UpdateDisplayModeRetryRegistration()
end

local function IsCustomWorldQuestsDisplayModeActive()
    return eventFrame.isWorldQuestsDisplayActive == true
end

function ns.IsWorldQuestsRefreshContextActive()
    return (WOW.WorldMapFrame and WOW.WorldMapFrame:IsShown() or false)
        and (questMapPanel and questMapPanel:IsShown() or false)
        and IsCustomWorldQuestsDisplayModeActive()
end

eventFrame._WithQuestPOIMapContext = function(mapID, callback)
    if type(callback) ~= "function" then
        return nil
    end

    if not WOW.C_QuestLog or not WOW.C_QuestLog.SetMapForQuestPOIs then
        return callback()
    end

    local previousMapID
    if WOW.C_QuestLog.GetMapForQuestPOIs then
        previousMapID = WOW.C_QuestLog.GetMapForQuestPOIs()
    end

    local targetMapID = type(mapID) == "number" and mapID > 0 and mapID or nil
    local restoreMapID = type(previousMapID) == "number" and previousMapID > 0 and previousMapID or nil
    local switchedContext = false

    if targetMapID and restoreMapID and targetMapID ~= restoreMapID then
        WOW.C_QuestLog.SetMapForQuestPOIs(targetMapID)
        switchedContext = true
    elseif targetMapID and not restoreMapID and ns.IsDebugEnabled and ns.IsDebugEnabled() then
        ns.DebugPrint("WorldQuests: skipped temporary quest POI map context change due to non-restorable prior map state")
    end

    local ok, first, second, third, fourth = pcall(callback)

    if switchedContext then
        WOW.C_QuestLog.SetMapForQuestPOIs(restoreMapID)
    end

    if not ok then
        if ns.IsDebugEnabled and ns.IsDebugEnabled() then
            ns.DebugPrint("WorldQuests: quest POI map context callback failed: " .. tostring(first))
        end
        return nil
    end

    return first, second, third, fourth
end

function eventFrame:ResetWorldQuestsRefreshGateState()
    self:CancelWorldQuestDescendantGather()
    self._lastScheduledWorldQuestsMapID = nil
    self._lastLiveRelevantAreaPOISnapshotCount = nil
    self._lastLiveRelevantAreaPOISnapshotParts = nil
    self._liveRelevantAreaPOIWidgetSetCount = nil
    self._visibleLockedAreaPOIWidgetSetCount = nil
    self._activeRelevantWorldQuestMapID = nil
    self._activeRelevantWorldQuestQuerySignature = nil
    self._visibleLockedAreaPOISnapshot = nil
    if self._liveRelevantAreaPOIWidgetSetIDs then
        wipe(self._liveRelevantAreaPOIWidgetSetIDs)
    end
    if self._visibleLockedAreaPOIWidgetSetIDs then
        wipe(self._visibleLockedAreaPOIWidgetSetIDs)
    end
    wipe(self._sessionScannedQueryMapIDs)
    wipe(self._sessionRawEntries)
    wipe(self._sessionRawEntriesSeen)
    wipe(self._sessionEnrichedEntries)
    wipe(self._sessionEnrichedEntriesSeen)
    wipe(self._sessionQueryStateCache)
    self:InvalidateActiveWorldQuestsLayout()
    rewardPreloadState.StopPoll()
    PruneQuestRequestBookkeeping()
end

function eventFrame:DebugWorldQuestsRefreshTrace(message, detail)
    if not ns.IsDebugEnabled() then
        return
    end

    local safeMessage = tostring(message)
    local isWorldMapShown = WOW.WorldMapFrame and WOW.WorldMapFrame:IsShown() or false
    local isQuestMapPanelShown = questMapPanel and questMapPanel:IsShown() or false
    local isDisplayActive = IsCustomWorldQuestsDisplayModeActive()
    local suffix = detail and detail ~= "" and (" " .. detail) or ""

    ns.DebugPrint(WOW.string_format(
        "WorldQuests: %s%s worldMapShown=%s panelShown=%s displayActive=%s",
        safeMessage,
        suffix,
        tostring(isWorldMapShown),
        tostring(isQuestMapPanelShown),
        tostring(isDisplayActive)))
end

local function ShouldKeepCustomWorldQuestsVisible()
    return pendingBuiltinDisplayModeFallback and IsCustomWorldQuestsDisplayModeActive()
end

local function ResolveFallbackDisplayMode()
    if not QuestMapFrame then
        return nil
    end

    local questsTab = QuestMapFrame.QuestsTab
    if questsTab then
        return questsTab.displayMode or "Quests"
    end

    local mapLegendTab = QuestMapFrame.MapLegendTab
    if mapLegendTab then
        return mapLegendTab.displayMode or "MapLegend"
    end

    local eventsTab = QuestMapFrame.EventsTab
    if eventsTab and eventsTab.IsShown and eventsTab:IsShown() then
        return eventsTab.displayMode or "Events"
    end

    return nil
end

local function RestoreBuiltinDisplayModeIfNeeded()
    if not QuestMapFrame or not QuestMapFrame.SetDisplayMode then
        return false
    end
    if not IsCustomWorldQuestsDisplayModeActive() then
        ClearPendingBuiltinDisplayModeFallback()
        return false
    end

    local fallbackMode = ResolveFallbackDisplayMode()
    if not fallbackMode or fallbackMode == K.DISPLAY_MODE then
        ClearPendingBuiltinDisplayModeFallback()
        return false
    end

    if WOW.InCombatLockdown() then
        pendingBuiltinDisplayModeFallback = true
        UpdateDisplayModeRetryRegistration()
        return false
    end

    ClearPendingBuiltinDisplayModeFallback()
    QuestMapFrame:SetDisplayMode(fallbackMode)
    return true
end

local function CanRequestWorldQuestsDisplayMode(source)
    if not WOW.WorldMapFrame or not WOW.WorldMapFrame:IsShown() then
        return false
    end
    if not QuestMapFrame or not QuestMapFrame.SetDisplayMode then
        return false
    end
    local settings = GetSettings()
    if not IsModuleEnabled(settings) then
        return false
    end
    if source == "auto" then
        if settings.openOnWorldQuestsTab ~= true then
            return false
        end
        if HasOpenQuestDetailsFrame() then
            return false
        end
    end
    return true
end

local function RequestWorldQuestsDisplayMode(source)
    source = source == "auto" and "auto" or "manual"

    if not CanRequestWorldQuestsDisplayMode(source) then
        ClearPendingDisplayModeRequest()
        return false
    end

    if QuestMapFrame.GetDisplayMode and QuestMapFrame:GetDisplayMode() == K.DISPLAY_MODE then
        ClearPendingDisplayModeRequest()
        return true
    end

    if WOW.InCombatLockdown() then
        pendingDisplayModeSource = source
        UpdateDisplayModeRetryRegistration()
        return false
    end

    local settings = GetSettings()
    if not IsModuleEnabled(settings) then
        ClearPendingDisplayModeRequest()
        return false
    end

    ClearPendingDisplayModeRequest()
    QuestMapFrame:SetDisplayMode(K.DISPLAY_MODE)
    return true
end

local function GetCurrentServerTime()
    if WOW.GetServerTime then
        return WOW.GetServerTime()
    end
    return 0
end

local function ClampWorldQuestFontSize(value, fallback)
    local size = tonumber(value) or fallback
    if not size then
        return 12
    end
    return math.max(8, math.min(30, WOW.math_floor(size + 0.5)))
end

local function GetWorldQuestFontConfig()
    local settings = GetSettings()
    local defaults = ns.DEFAULTS and ns.DEFAULTS.worldQuests or {}
    return {
        font = settings.font or defaults.font or ns.GLOBAL_CHOICE_KEY,
        fontOutline = settings.fontOutline or defaults.fontOutline or ns.GLOBAL_CHOICE_KEY,
        titleFontSize = ClampWorldQuestFontSize(settings.titleFontSize, defaults.titleFontSize or 14),
        detailFontSize = ClampWorldQuestFontSize(settings.detailFontSize, defaults.detailFontSize or 11),
        rewardFontSize = ClampWorldQuestFontSize(settings.rewardFontSize, defaults.rewardFontSize or 10),
    }
end

local function ApplyWorldQuestFont(fontString, fontPath, fontSize, fontOutline)
    if fontString and fontString.SetFont then
        fontString:SetFont(fontPath, fontSize, fontOutline)
    end
end

local function ApplyWorldQuestTypographyToRow(row)
    if not row then return end
    if row._typographyVersion == eventFrame._typographyVersion
        and eventFrame._typographyVersion > 0 then
        return
    end
    local config = GetWorldQuestFontConfig()
    local fontPath = ns.GetFontPath and ns.GetFontPath(config.font) or STANDARD_TEXT_FONT
    local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(config.fontOutline) or "OUTLINE"

    ApplyWorldQuestFont(row.titleText, fontPath, config.titleFontSize, fontOutline)
    ApplyWorldQuestFont(row.timeText, fontPath, config.detailFontSize, fontOutline)
    ApplyWorldQuestFont(row.factionText, fontPath, config.detailFontSize, fontOutline)
    ApplyWorldQuestFont(row.moneyText, fontPath, config.detailFontSize, fontOutline)

    for _, rewardButton in ipairs(row.rewardIcons or {}) do
        ApplyWorldQuestFont(rewardButton.countText, fontPath, config.rewardFontSize, fontOutline)
    end
    row._typographyVersion = eventFrame._typographyVersion
end

local function ApplyWorldQuestTypographyToHeader(header)
    if not header then return end
    local config = GetWorldQuestFontConfig()
    local fontPath = ns.GetFontPath and ns.GetFontPath(config.font) or STANDARD_TEXT_FONT
    local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(config.fontOutline) or "OUTLINE"

    ApplyWorldQuestFont(header.label, fontPath, config.titleFontSize, fontOutline)
    ApplyWorldQuestFont(header.countLabel, fontPath, config.detailFontSize, fontOutline)
end

local function ApplyWorldQuestTypography()
    eventFrame._typographyVersion = (eventFrame._typographyVersion or 0) + 1
    local config = GetWorldQuestFontConfig()
    local fontPath = ns.GetFontPath and ns.GetFontPath(config.font) or STANDARD_TEXT_FONT
    local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(config.fontOutline) or "OUTLINE"

    ApplyWorldQuestFont(noQuestsLabel, fontPath, config.detailFontSize, fontOutline)
    ApplyWorldQuestFont(contractNoticeLabel, fontPath, config.detailFontSize, fontOutline)

    for _, row in ipairs(questRowPool) do
        ApplyWorldQuestTypographyToRow(row)
    end
    for _, header in ipairs(zoneHeaderPool) do
        ApplyWorldQuestTypographyToHeader(header)
    end
    for _, item in ipairs(activeContent) do
        if item.type == "row" and item.frame then
            ApplyWorldQuestTypographyToRow(item.frame)
        elseif item.type == "zone" then
            ApplyWorldQuestTypographyToHeader(item.frame)
        end
    end
end

-- =============================================
-- Time utilities
-- =============================================

local function GetQuestExpirySnapshot(questID, serverTime, fallbackEntry)
    local now = serverTime or GetCurrentServerTime()
    local timeLeft
    if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestTimeLeftSeconds then
        timeLeft = WOW.C_TaskQuest.GetQuestTimeLeftSeconds(questID)
    else
        local legacyGetQuestTimeLeftMinutes = _G and _G["GetQuestTimeLeftMinutes"] or nil
        local mins = legacyGetQuestTimeLeftMinutes and legacyGetQuestTimeLeftMinutes(questID)
        timeLeft = mins and (mins * 60) or nil
    end
    if timeLeft and timeLeft > 0 then
        return timeLeft, now + timeLeft
    end
    if fallbackEntry then
        local fallbackExpiresAt = fallbackEntry.expiresAt
        if fallbackEntry.rawTimeLeftSeconds and fallbackExpiresAt and fallbackExpiresAt > 0 then
            fallbackEntry.rawTimeLeftSecondsConsumed = true
        end
        if fallbackExpiresAt and fallbackExpiresAt > now then
            return fallbackExpiresAt - now, fallbackExpiresAt
        end

        if fallbackEntry.rawTimeLeftSecondsConsumed ~= true
            and (not fallbackExpiresAt or fallbackExpiresAt <= 0)
        then
            local fallbackTimeLeft = fallbackEntry.rawTimeLeftSeconds
            if fallbackTimeLeft and fallbackTimeLeft > 0 then
                local derivedExpiresAt = now + fallbackTimeLeft
                fallbackEntry.rawTimeLeftSecondsConsumed = true
                fallbackEntry.expiresAt = derivedExpiresAt
                return fallbackTimeLeft, derivedExpiresAt
            end
        end
    end
    return timeLeft, nil
end

-- World quest / special-assignment row time: exactly one tier — "Xd Yh", "Xh Ym", or "Xm"
-- (never days + hours + minutes together).
local function FormatTimeLeft(seconds)
    if not seconds or seconds <= 0 then
        return "---"
    end
    local s = WOW.math_floor(seconds)
    if s < 3600 then
        return WOW.math_floor(s / 60) .. "m"
    elseif s < 86400 then
        local h = WOW.math_floor(s / 3600)
        local m = WOW.math_floor((s % 3600) / 60)
        return m > 0 and (h .. "h " .. m .. "m") or (h .. "h")
    else
        local d = WOW.math_floor(s / 86400)
        local h = WOW.math_floor((s % 86400) / 3600)
        return h > 0 and (d .. "d " .. h .. "h") or (d .. "d")
    end
end

local function GetRemainingTimeLeft(timeLeft, expiresAt, now)
    if timeLeft and timeLeft > 0 then
        return timeLeft
    end
    if expiresAt and expiresAt > now then
        return expiresAt - now
    end
    return nil
end

local function TryGetAreaPOISecondsLeft(poiID)
    if not poiID or not C_AreaPoiInfo or not C_AreaPoiInfo.IsAreaPOITimed
        or not C_AreaPoiInfo.GetAreaPOISecondsLeft then
        return nil
    end
    if not C_AreaPoiInfo.IsAreaPOITimed(poiID) then
        return nil
    end
    local sec = C_AreaPoiInfo.GetAreaPOISecondsLeft(poiID)
    if sec and sec > 0 then
        return sec
    end
    return nil
end

local function ClearActiveQuestTooltip(anchorFrame)
    if anchorFrame and activeTooltipAnchor ~= anchorFrame then
        return
    end

    activeTooltipAnchor = nil
    activeTooltipQuestID = nil
    activeTooltipShowTrackHint = false
    WOW.GameTooltip:Hide()
end

do
local HIGHLIGHT_STATUS_OK = 1
local HIGHLIGHT_STATUS_TRANSIENT_MISS = 2
local HIGHLIGHT_STATUS_INVALID = 3

---@param pin table?
---@return string
local function GetHoverPinIdentity(pin)
    if not ns.IsDebugEnabled() then
        return ""
    end

    if not pin then
        return "nil"
    end

    local pinQuestID = rawget(pin, "questID")
    return WOW.string_format("%s(q=%s)", tostring(pin), tostring(pinQuestID))
end

---@param status number
---@return string
local function GetHoverHighlightStatusName(status)
    if type(status) == "string" then
        return status
    end

    if status == HIGHLIGHT_STATUS_OK then
        return "ok"
    elseif status == HIGHLIGHT_STATUS_TRANSIENT_MISS then
        return "miss"
    elseif status == HIGHLIGHT_STATUS_INVALID then
        return "invalid"
    end

    return tostring(status)
end

local function DebugRebindTransition(success, source, questID, row, rowQuestID, reason, hasPOI)
    local rowIdentity = eventFrame._GetHoverRowIdentity(row)
    if activeHoverState._dbgLastRebindResult == success
        and activeHoverState._dbgLastRebindSource == source
        and activeHoverState._dbgLastRebindRowIdentity == rowIdentity
    then
        return
    end

    eventFrame._DebugHoverTrace(
        "Rebind",
        "result=%s source=%s questID=%s row=%s rowQuestID=%s reason=%s poi=%s",
        success and "success" or "failure",
        tostring(source),
        tostring(questID),
        rowIdentity,
        tostring(rowQuestID),
        tostring(reason or ""),
        tostring(hasPOI == true))

    activeHoverState._dbgLastRebindResult = success
    activeHoverState._dbgLastRebindSource = source
    activeHoverState._dbgLastRebindRowIdentity = rowIdentity
end

---@param questID number?
---@return table?
local function FindWorldQuestPinByQuestID(questID)
    if not questID
        or questID <= 0
        or not WOW.WorldMapFrame
        or not WOW.WorldMapFrame.IsVisible
        or not WOW.WorldMapFrame:IsVisible()
        or not WOW.WorldMapFrame.EnumeratePinsByTemplate
    then
        return nil
    end

    for pin in WOW.WorldMapFrame:EnumeratePinsByTemplate("WorldMap_WorldQuestPinTemplate") do
        if pin and pin.questID == questID then
            return pin
        end
    end

    return nil
end

---@param questID number?
---@return table?
local function FindAnyQuestPinByQuestID(questID)
    if not questID
        or questID <= 0
        or not WOW.WorldMapFrame
        or not WOW.WorldMapFrame.IsVisible
        or not WOW.WorldMapFrame:IsVisible()
        or not WOW.WorldMapFrame.EnumeratePins
    then
        return nil
    end

    for pin in WOW.WorldMapFrame:EnumeratePins() do
        if pin then
            local pinQuestID = rawget(pin, "questID") or pin.questID
            if pinQuestID == questID then
                return pin
            end
            if pin.GetQuestID then
                local ok, discoveredQuestID = pcall(pin.GetQuestID, pin)
                if ok and discoveredQuestID == questID then
                    return pin
                end
            end
        end
    end

    return nil
end

local _areaPOIPinDiagDone = false

---@param areaPOIID number?
---@return table?
local function FindAreaPOIPinByAreaPOIID(areaPOIID)
    if not areaPOIID or areaPOIID <= 0 or not WOW.WorldMapFrame then
        return nil
    end

    local function GetPinAreaPOIID(pin)
        if not pin then return nil end
        -- Direct fields
        local id = rawget(pin, "areaPoiID")
            or rawget(pin, "areaPOIID")
        if id then return id end
        -- Nested areaPOIInfo (used by Blizzard's AreaPOIPinMixin)
        local info = rawget(pin, "areaPOIInfo")
        if info then
            id = info.areaPoiID or info.areaPOIID or info.poiID
            if id then return id end
        end
        -- Fallback indexing (may trigger __index)
        id = pin.areaPoiID or pin.areaPOIID or pin.poiID
        if id then return id end
        -- Method
        if pin.GetAreaPoiID then
            local ok, mid = pcall(pin.GetAreaPoiID, pin)
            if ok and mid then return mid end
        end
        return nil
    end

    local debugEnabled = ns.IsDebugEnabled()
    local _diagDumped = false
    local _diagPinCount = 0

    local function CheckPin(pin)
        if not pin then return false end
        local id = GetPinAreaPOIID(pin)
        if debugEnabled and not _diagDumped then
            _diagPinCount = _diagPinCount + 1
        end
        return id == areaPOIID
    end

    if WOW.WorldMapFrame.EnumeratePinsByTemplate then
        local templates = {
            "AreaPOIPinTemplate",
            "WorldMap_AreaPOIPinTemplate",
            "WorldMap_LockedQuestPOIPinTemplate",
        }
        for t = 1, #templates do
            for pin in WOW.WorldMapFrame:EnumeratePinsByTemplate(templates[t]) do
                if CheckPin(pin) then return pin end
            end
        end
    end

    if WOW.WorldMapFrame.EnumeratePins then
        for pin in WOW.WorldMapFrame:EnumeratePins() do
            if CheckPin(pin) then return pin end
        end
    end

    -- One-shot diagnostic: log what AreaPOI-related pins actually look like
    if debugEnabled and not _areaPOIPinDiagDone then
        _areaPOIPinDiagDone = true
        local pinLog = {}
        if WOW.WorldMapFrame.EnumeratePins then
            for pin in WOW.WorldMapFrame:EnumeratePins() do
                if pin then
                    local id = GetPinAreaPOIID(pin)
                    if id or (pin.areaPOIInfo) then
                        local tmpl = rawget(pin, "_pinTemplate") or rawget(pin, "pinTemplate") or "?"
                        local directID = rawget(pin, "areaPoiID") or rawget(pin, "areaPOIID") or rawget(pin, "poiID")
                        local infoID = pin.areaPOIInfo and (pin.areaPOIInfo.areaPoiID or pin.areaPOIInfo.areaPOIID or pin.areaPOIInfo.poiID) or nil
                        pinLog[#pinLog + 1] = WOW.string_format(
                            "tmpl=%s directID=%s infoID=%s resolvedID=%s",
                            tostring(tmpl), tostring(directID), tostring(infoID), tostring(id))
                    end
                end
            end
        end
        eventFrame._DebugHoverTrace(
            "PinDiag",
            "areaPOIID=%s mapVisible=%s pinCount=%s areaPOIPins=%s list=%s",
            tostring(areaPOIID),
            tostring(WOW.WorldMapFrame.IsVisible and WOW.WorldMapFrame:IsVisible() or false),
            tostring(_diagPinCount),
            tostring(#pinLog),
            table.concat(pinLog, " | "))
    end

    return nil
end

---@param button Button?
---@return boolean
local function IsChildHoverButtonActuallyHovered(button)
    if not button
        or not button.IsShown
        or not button:IsShown()
        or not button.IsMouseOver
    then
        return false
    end

    return button:IsMouseOver()
end

---@param rewardIcons Button[]?
---@return boolean
local function IsAnyQuestRewardButtonActuallyHovered(rewardIcons)
    if not rewardIcons then
        return false
    end

    for iconIndex = 1, #rewardIcons do
        if IsChildHoverButtonActuallyHovered(rewardIcons[iconIndex]) then
            return true
        end
    end

    return false
end

---@param poiBtn Button?
---@return boolean
local function IsPOIButtonActuallyHovered(poiBtn)
    if not poiBtn then
        return false
    end

    return IsChildHoverButtonActuallyHovered(poiBtn)
end

---@param row Frame?
---@return boolean
local function IsQuestRowActuallyHovered(row)
    if not row
        or not row.IsShown
        or not row:IsShown()
    then
        return false
    end

    if row.IsMouseOver and row:IsMouseOver() then
        return true
    end

    if IsPOIButtonActuallyHovered(rawget(row, "poiBtn")) then
        return true
    end

    if IsChildHoverButtonActuallyHovered(rawget(row, "contractBtn")) then
        return true
    end

    return IsAnyQuestRewardButtonActuallyHovered(rawget(row, "rewardIcons"))
end

local function ResetActiveWorldQuestHoverPinMisses()
    activeHoverState.pinMisses = 0
    activeHoverState.pinMissStart = nil
end

local function ResetActiveWorldQuestHoverSurfaceMisses()
    activeHoverState.surfaceMisses = 0
    activeHoverState.surfaceMissStart = nil
end

---@param row Frame?
local function SetActiveWorldQuestHoverRow(row)
    local previousRow = activeHoverState.row
    local previousHoverBg = previousRow and rawget(previousRow, "hoverBg") or nil
    if previousRow and previousRow ~= row and previousHoverBg then
        previousHoverBg:Hide()
    end

    activeHoverState.row = row
    activeHoverState.poiBtn = row and rawget(row, "poiBtn") or nil

    local hoverBg = row and rawget(row, "hoverBg") or nil
    if hoverBg then
        hoverBg:Show()
    end
end

---@param pin table?
local function ClearNomToolsPinHoverFX(pin)
    if not pin then
        return
    end

    local fx = rawget(pin, "_nomToolsHoverFX")
    if not fx then
        return
    end

    local pulse = fx.pulse
    if pulse and pulse.IsPlaying and pulse:IsPlaying() then
        pulse:Stop()
    end

    local frame = fx.frame
    if frame and frame.Hide then
        frame:Hide()
    end
end

---@param pin table?
---@return table?
local function EnsureNomToolsPinHoverFX(pin)
    if not pin or not pin.CreateTexture then
        return nil
    end

    local fx = rawget(pin, "_nomToolsHoverFX")
    if fx
        and fx.frame
        and fx.innerTexture
        and fx.outerTexture
        and fx.pulse
        and fx.frame.GetParent
        and fx.frame:GetParent() == pin
    then
        return fx
    end

    local frame = WOW.CreateFrame("Frame", nil, pin)
    if not frame then
        return nil
    end

    frame:SetAllPoints(pin)
    if frame.SetIgnoreParentAlpha then
        frame:SetIgnoreParentAlpha(true)
    end
    if frame.SetFrameStrata then
        frame:SetFrameStrata("HIGH")
    end
    if frame.SetFrameLevel and pin.GetFrameLevel then
        frame:SetFrameLevel(pin:GetFrameLevel() + 6)
    end

    local outerTexture = frame:CreateTexture(nil, "OVERLAY", nil, 1)
    outerTexture:SetPoint("TOPLEFT", frame, "TOPLEFT", -6, 6)
    outerTexture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 6, -6)
    outerTexture:SetBlendMode("ADD")
    outerTexture:SetVertexColor(1.00, 0.82, 0.28, 0.75)
    outerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")

    local innerTexture = frame:CreateTexture(nil, "OVERLAY", nil, 2)
    innerTexture:SetAllPoints(frame)
    innerTexture:SetBlendMode("ADD")
    innerTexture:SetVertexColor(1.00, 0.95, 0.55, 1.0)
    innerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")

    local fxStyle = rawget(pin, "_nomToolsHoverFXStyle")
    if fxStyle == "soft-circle" then
        outerTexture:ClearAllPoints()
        outerTexture:SetPoint("TOPLEFT", frame, "TOPLEFT", -12, 12)
        outerTexture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 12, -12)
        outerTexture:SetVertexColor(1.00, 0.84, 0.30, 0.22)

        innerTexture:SetVertexColor(1.00, 0.92, 0.58, 0.34)

        local circleMask = frame:CreateMaskTexture(nil, "OVERLAY")
        circleMask:SetAllPoints(frame)
        circleMask:SetTexture(
            "Interface\\CharacterFrame\\TempPortraitAlphaMask",
            "CLAMPTOBLACKADDITIVE",
            "CLAMPTOBLACKADDITIVE")
        outerTexture:AddMaskTexture(circleMask)
        innerTexture:AddMaskTexture(circleMask)
    end

    local pulse = frame:CreateAnimationGroup()
    pulse:SetLooping("REPEAT")

    local fadeOut = pulse:CreateAnimation("Alpha")
    fadeOut:SetOrder(1)
    fadeOut:SetDuration(0.34)
    fadeOut:SetFromAlpha(0.95)
    fadeOut:SetToAlpha(0.42)
    if fadeOut.SetSmoothing then
        fadeOut:SetSmoothing("IN_OUT")
    end

    local fadeIn = pulse:CreateAnimation("Alpha")
    fadeIn:SetOrder(2)
    fadeIn:SetDuration(0.24)
    fadeIn:SetFromAlpha(0.42)
    fadeIn:SetToAlpha(0.95)
    if fadeIn.SetSmoothing then
        fadeIn:SetSmoothing("IN_OUT")
    end

    if fxStyle == "soft-circle" then
        fadeOut:SetDuration(0.44)
        fadeOut:SetFromAlpha(0.58)
        fadeOut:SetToAlpha(0.22)
        fadeIn:SetDuration(0.34)
        fadeIn:SetFromAlpha(0.22)
        fadeIn:SetToAlpha(0.58)
    end

    frame:Hide()
    fx = {
        frame = frame,
        outerTexture = outerTexture,
        innerTexture = innerTexture,
        pulse = pulse,
    }
    pin._nomToolsHoverFX = fx
    return fx
end

---@param pin table?
---@return boolean fxReady
local function ApplyNomToolsPinHoverFX(pin)
    if not pin then
        return false
    end

    local fx = EnsureNomToolsPinHoverFX(pin)
    if not fx then
        return false
    end

    local innerTexture = fx.innerTexture
    local outerTexture = fx.outerTexture
    local normalTexture = rawget(pin, "NormalTexture")
    local fxStyle = rawget(pin, "_nomToolsHoverFXStyle")
    local textureApplied = false

    if fxStyle == "soft-circle" and innerTexture and outerTexture then
        innerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")
        outerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")
        textureApplied = true
    end

    if not textureApplied and innerTexture and outerTexture and normalTexture and normalTexture.GetAtlas then
        local atlas = normalTexture:GetAtlas()
        if atlas and atlas ~= "" then
            innerTexture:SetAtlas(atlas, true)
            outerTexture:SetAtlas(atlas, false)
            textureApplied = true
        end
    end

    if not textureApplied and innerTexture and outerTexture and normalTexture and normalTexture.GetTexture then
        local sourceTexture = normalTexture:GetTexture()
        if sourceTexture then
            innerTexture:SetTexture(sourceTexture)
            outerTexture:SetTexture(sourceTexture)
            textureApplied = true
        end
    end

    if not textureApplied and innerTexture and outerTexture then
        innerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")
        outerTexture:SetTexture("Interface\\Buttons\\WHITE8x8")
    end

    if activeHoverState.fxPin and activeHoverState.fxPin ~= pin then
        ClearNomToolsPinHoverFX(activeHoverState.fxPin)
    end

    activeHoverState.fxPin = pin
    if fx.frame and fx.frame.Show then
        fx.frame:Show()
    end
    if fx.pulse and fx.pulse.IsPlaying and not fx.pulse:IsPlaying() then
        fx.pulse:Play()
    end

    return true
end

---@param reason string?
local function StopActiveWorldQuestHover(reason)
    local debugEnabled = ns.IsDebugEnabled()
    local stopToken = (activeHoverState.stopToken or 0) + 1
    if debugEnabled then
        local row = activeHoverState.row
        local rowQuestID = row and rawget(row, "questID") or nil
        eventFrame._DebugHoverTrace(
            "Stop",
            "reason=%s questID=%s row=%s rowQuestID=%s ticker=%s stopToken=%s pinMisses=%s surfaceMisses=%s",
            tostring(reason or "unspecified"),
            tostring(activeHoverState.questID),
            eventFrame._GetHoverRowIdentity(row),
            tostring(rowQuestID),
            tostring(activeHoverState.ticker ~= nil),
            tostring(stopToken),
            tostring(activeHoverState.pinMisses or 0),
            tostring(activeHoverState.surfaceMisses or 0))
    end

    activeHoverState.stopToken = (activeHoverState.stopToken or 0) + 1

    local ticker = activeHoverState.ticker
    if ticker then
        ticker:Cancel()
        activeHoverState.ticker = nil
    end

    local row = activeHoverState.row
    local hoverBg = row and rawget(row, "hoverBg") or nil
    if hoverBg then
        hoverBg:Hide()
    end

    activeHoverState.questID = nil
    activeHoverState.row = nil
    activeHoverState.poiBtn = nil
    if activeHoverState.fxPin then
        ClearNomToolsPinHoverFX(activeHoverState.fxPin)
    end
    activeHoverState.fxPin = nil
    local canvasOverlay = eventFrame._areaPOICanvasOverlay
    if canvasOverlay and canvasOverlay.IsShown and canvasOverlay:IsShown() then
        canvasOverlay:Hide()
    end
    ResetActiveWorldQuestHoverPinMisses()
    ResetActiveWorldQuestHoverSurfaceMisses()

    if debugEnabled then
        activeHoverState._dbgLastHovered = nil
        activeHoverState._dbgLastHighlightStatus = nil
        activeHoverState._dbgLastPinFound = nil
        activeHoverState._dbgLastPinIdentity = nil
        activeHoverState._dbgLastTickLogTime = nil
        activeHoverState._dbgLastRebindResult = nil
        activeHoverState._dbgLastRebindSource = nil
        activeHoverState._dbgLastRebindRowIdentity = nil
    end
end

---@return boolean hovered
---@return string source
---@return Frame? row
local function TryRebindActiveWorldQuestHoverRow()
    local questID = activeHoverState.questID
    if not questID or questID == 0 then
        DebugRebindTransition(false, "none", questID, nil, nil, "invalid-quest", false)
        return false, "none", nil
    end

    local row = activeHoverState.row
    local rowQuestID = row and rawget(row, "questID") or nil
    if row and rowQuestID == questID and IsQuestRowActuallyHovered(row) then
        activeHoverState.poiBtn = rawget(row, "poiBtn")
        DebugRebindTransition(true, "active-row", questID, row, rowQuestID, nil, true)
        return true, "active-row", row
    end

    local poiBtn = activeHoverState.poiBtn
    if poiBtn and IsPOIButtonActuallyHovered(poiBtn) then
        local parentRow = poiBtn.GetParent and poiBtn:GetParent() or nil
        local parentQuestID = parentRow and rawget(parentRow, "questID") or nil
        if parentRow and parentQuestID == questID then
            SetActiveWorldQuestHoverRow(parentRow)
            DebugRebindTransition(true, "poi-parent", questID, parentRow, parentQuestID, nil, true)
            return true, "poi-parent", parentRow
        end
    end

    for itemIndex = 1, #activeContent do
        local item = activeContent[itemIndex]
        local itemRow = item and item.type == "row" and item.frame or nil
        local itemQuestID = itemRow and rawget(itemRow, "questID") or nil
        if itemRow and itemQuestID == questID and IsQuestRowActuallyHovered(itemRow) then
            SetActiveWorldQuestHoverRow(itemRow)
            DebugRebindTransition(true, "activeContent-scan", questID, itemRow, itemQuestID, nil, true)
            return true, "activeContent-scan", itemRow
        end
    end

    if row and rowQuestID ~= questID then
        activeHoverState.row = nil
    end
    if poiBtn then
        local parentRow = poiBtn.GetParent and poiBtn:GetParent() or nil
        local parentQuestID = parentRow and rawget(parentRow, "questID") or nil
        if not parentRow or parentQuestID ~= questID then
            activeHoverState.poiBtn = nil
        end
    end

    DebugRebindTransition(false, "none", questID, row, rowQuestID, "no-hovered-surface", poiBtn ~= nil)
    return false, "none", nil
end

---@param questID number?
---@param forceReplay boolean?
---@return number status
---@return boolean countsAsPinMiss
local function UpdateWorldQuestPinHighlightState(questID, forceReplay)
    if not questID then
        return HIGHLIGHT_STATUS_INVALID, false
    end

    local row = activeHoverState.row
    local rowEntry = row and rawget(row, "questEntry") or nil
    local pin = nil
    if questID > 0 then
        pin = FindWorldQuestPinByQuestID(questID)
        if not pin then
            pin = FindAnyQuestPinByQuestID(questID)
        end
    end
    if not pin and rowEntry and rowEntry.poiID then
        pin = FindAreaPOIPinByAreaPOIID(rowEntry.poiID)
    end
    local pinFound = pin ~= nil
    local pinIdentity = GetHoverPinIdentity(pin)
    if not pin then
        -- Canvas overlay for locked AreaPOI when no map pin exists.
        -- areaPOIInfo.position is in poiQueryMapID's coordinate space; only
        -- apply when the WOW.WorldMapFrame is currently showing that same map.
        local areaPOIInfo = rowEntry and rowEntry.areaPOIInfo or nil
        local poiQueryMapID = rowEntry and rowEntry.poiQueryMapID or nil
        local poiPos = areaPOIInfo and areaPOIInfo.position or nil
        if poiPos and poiPos.x and poiPos.y and poiQueryMapID
            and WOW.WorldMapFrame
            and WOW.WorldMapFrame.GetMapID
            and WOW.WorldMapFrame:GetMapID() == poiQueryMapID
            and WOW.WorldMapFrame.GetCanvas
        then
            local canvas = WOW.WorldMapFrame:GetCanvas()
            if canvas then
                local canvasOverlay = eventFrame._areaPOICanvasOverlay
                if not canvasOverlay then
                    canvasOverlay = WOW.CreateFrame("Frame", nil, canvas)
                    canvasOverlay:SetSize(100, 100)
                    canvasOverlay._nomToolsHoverFXStyle = "soft-circle"
                    if canvasOverlay.SetFrameLevel and canvas.GetFrameLevel then
                        canvasOverlay:SetFrameLevel(canvas:GetFrameLevel() + 50)
                    end
                    canvasOverlay:Hide()
                    eventFrame._areaPOICanvasOverlay = canvasOverlay
                end
                local cw = canvas:GetWidth()
                local ch = canvas:GetHeight()
                canvasOverlay:ClearAllPoints()
                canvasOverlay:SetPoint("CENTER", canvas, "BOTTOMLEFT",
                    poiPos.x * cw, (1 - poiPos.y) * ch + 25)
                canvasOverlay:Show()
                local fxReady = ApplyNomToolsPinHoverFX(canvasOverlay)
                if fxReady then
                    local statusName = "canvas-overlay-fx"
                    if activeHoverState._dbgLastHighlightStatus ~= statusName
                        or activeHoverState._dbgLastPinFound ~= pinFound
                        or activeHoverState._dbgLastPinIdentity ~= pinIdentity
                    then
                        eventFrame._DebugHoverTrace(
                            "PinHighlight",
                            "status=%s questID=%s poiQueryMapID=%s poiX=%.3f poiY=%.3f countsAsPinMiss=false",
                            statusName,
                            tostring(questID),
                            tostring(poiQueryMapID),
                            poiPos.x, poiPos.y)
                        activeHoverState._dbgLastHighlightStatus = statusName
                        activeHoverState._dbgLastPinFound = pinFound
                        activeHoverState._dbgLastPinIdentity = pinIdentity
                    end
                    ResetActiveWorldQuestHoverPinMisses()
                    return HIGHLIGHT_STATUS_OK, false
                else
                    canvasOverlay:Hide()
                end
            end
        end

        if activeHoverState.fxPin then
            ClearNomToolsPinHoverFX(activeHoverState.fxPin)
            activeHoverState.fxPin = nil
        end

        activeHoverState.pinMisses = (activeHoverState.pinMisses or 0) + 1
        if not activeHoverState.pinMissStart then
            activeHoverState.pinMissStart = WOW.GetTime and WOW.GetTime() or 0
        end
        local statusName = GetHoverHighlightStatusName(HIGHLIGHT_STATUS_TRANSIENT_MISS)
        if activeHoverState._dbgLastHighlightStatus ~= statusName
            or activeHoverState._dbgLastPinFound ~= pinFound
            or activeHoverState._dbgLastPinIdentity ~= pinIdentity
        then
            eventFrame._DebugHoverTrace(
                "PinHighlight",
                "status=%s questID=%s pinFound=%s pin=%s pinMisses=%s forceReplay=%s countsAsPinMiss=true",
                statusName,
                tostring(questID),
                tostring(pinFound),
                pinIdentity,
                tostring(activeHoverState.pinMisses or 0),
                tostring(forceReplay == true))
            activeHoverState._dbgLastHighlightStatus = statusName
            activeHoverState._dbgLastPinFound = pinFound
            activeHoverState._dbgLastPinIdentity = pinIdentity
        end
        return HIGHLIGHT_STATUS_TRANSIENT_MISS, true
    end

    ResetActiveWorldQuestHoverPinMisses()

    local pinChanged = activeHoverState.fxPin ~= pin
    local fxReady = ApplyNomToolsPinHoverFX(pin)
    local statusName
    if not fxReady then
        statusName = GetHoverHighlightStatusName(HIGHLIGHT_STATUS_TRANSIENT_MISS)
    elseif pinChanged then
        statusName = "fx-pin-changed"
    elseif forceReplay then
        statusName = "fx-applied"
    else
        statusName = "fx-running"
    end

    if activeHoverState._dbgLastHighlightStatus ~= statusName
        or activeHoverState._dbgLastPinFound ~= pinFound
        or activeHoverState._dbgLastPinIdentity ~= pinIdentity
    then
        local fx = rawget(pin, "_nomToolsHoverFX")
        local isPlaying = fx and fx.pulse and fx.pulse.IsPlaying and fx.pulse:IsPlaying() or false
        eventFrame._DebugHoverTrace(
            "PinHighlight",
            "status=%s questID=%s pinFound=%s pin=%s pinChanged=%s forceReplay=%s isPlaying=%s countsAsPinMiss=false",
            statusName,
            tostring(questID),
            tostring(pinFound),
            pinIdentity,
            tostring(pinChanged),
            tostring(forceReplay == true),
            tostring(isPlaying))
        activeHoverState._dbgLastHighlightStatus = statusName
        activeHoverState._dbgLastPinFound = pinFound
        activeHoverState._dbgLastPinIdentity = pinIdentity
    end

    if not fxReady then
        return HIGHLIGHT_STATUS_TRANSIENT_MISS, false
    end

    return HIGHLIGHT_STATUS_OK, false
end

---@param forceReplay boolean?
local function TickActiveWorldQuestHover(forceReplay)
    local questID = activeHoverState.questID
    if not questID then
        eventFrame._DebugHoverTrace("Tick", "stop-trigger=invalid-quest")
        StopActiveWorldQuestHover("invalid-quest")
        return
    end

    local hovered, rebindSource, rebindRow = TryRebindActiveWorldQuestHoverRow()
    if hovered ~= activeHoverState._dbgLastHovered then
        eventFrame._DebugHoverTrace(
            "Tick",
            "hovered=%s questID=%s source=%s row=%s",
            tostring(hovered),
            tostring(questID),
            tostring(rebindSource),
            eventFrame._GetHoverRowIdentity(rebindRow))
        activeHoverState._dbgLastHovered = hovered
    end

    if hovered then
        ResetActiveWorldQuestHoverSurfaceMisses()
    else
        local now = WOW.GetTime and WOW.GetTime() or 0
        activeHoverState.surfaceMisses = (activeHoverState.surfaceMisses or 0) + 1
        if activeHoverState.surfaceMisses == 1
            or activeHoverState.surfaceMisses == 4
            or activeHoverState.surfaceMisses == 8
        then
            eventFrame._DebugHoverTrace(
                "Tick",
                "surface-miss milestone=%s questID=%s start=%.2f",
                tostring(activeHoverState.surfaceMisses),
                tostring(questID),
                tonumber(activeHoverState.surfaceMissStart or now) or 0)
        end
        if not activeHoverState.surfaceMissStart then
            activeHoverState.surfaceMissStart = now
        end
        if (now - activeHoverState.surfaceMissStart) > 1.0
            or activeHoverState.surfaceMisses >= 8
        then
            eventFrame._DebugHoverTrace(
                "Tick",
                "stop-trigger=hover-miss questID=%s surfaceMisses=%s elapsed=%.2f",
                tostring(questID),
                tostring(activeHoverState.surfaceMisses or 0),
                now - (activeHoverState.surfaceMissStart or now))
            StopActiveWorldQuestHover("hover-miss")
            return
        end
    end

    local status, countsAsPinMiss = UpdateWorldQuestPinHighlightState(questID, forceReplay)
    if status == HIGHLIGHT_STATUS_INVALID then
        eventFrame._DebugHoverTrace("Tick", "stop-trigger=invalid-highlight")
        StopActiveWorldQuestHover("invalid-highlight")
        return
    end

    if status == HIGHLIGHT_STATUS_TRANSIENT_MISS and countsAsPinMiss then
        local now = WOW.GetTime and WOW.GetTime() or 0
        local missStart = activeHoverState.pinMissStart or now
        local pinMisses = activeHoverState.pinMisses or 0
        if pinMisses == 1 or pinMisses == 4 or pinMisses == 8 then
            eventFrame._DebugHoverTrace(
                "Tick",
                "pin-miss milestone=%s questID=%s start=%.2f",
                tostring(pinMisses),
                tostring(questID),
                tonumber(missStart) or 0)
        end
        if (now - missStart) > 1.0 or activeHoverState.pinMisses >= 8 then
            if hovered then
                -- Keep list-row hover active while the row/surfaces remain hovered.
                eventFrame._DebugHoverTrace(
                    "Tick",
                    "pin-miss tolerated while-hovered questID=%s pinMisses=%s elapsed=%.2f",
                    tostring(questID),
                    tostring(activeHoverState.pinMisses or 0),
                    now - missStart)
                return
            end
            eventFrame._DebugHoverTrace(
                "Tick",
                "stop-trigger=pin-miss questID=%s pinMisses=%s elapsed=%.2f",
                tostring(questID),
                tostring(activeHoverState.pinMisses or 0),
                now - missStart)
            StopActiveWorldQuestHover("pin-miss")
        end
    end

    local now = WOW.GetTime and WOW.GetTime() or 0
    if not activeHoverState._dbgLastTickLogTime
        or (now - activeHoverState._dbgLastTickLogTime) >= 1.0
    then
        eventFrame._DebugHoverTrace(
            "TickHeartbeat",
            "questID=%s hovered=%s status=%s pinMisses=%s surfaceMisses=%s",
            tostring(questID),
            tostring(hovered),
            tostring(activeHoverState._dbgLastHighlightStatus or "none"),
            tostring(activeHoverState.pinMisses or 0),
            tostring(activeHoverState.surfaceMisses or 0))
        activeHoverState._dbgLastTickLogTime = now
    end
end

local function EnsureActiveWorldQuestHoverTicker()
    local questID = activeHoverState.questID
    if activeHoverState.ticker
        or not questID
        or not WOW.C_Timer
        or not WOW.C_Timer.NewTicker
    then
        if activeHoverState.ticker and questID then
            eventFrame._DebugHoverTrace("Ticker", "already-running questID=%s", tostring(questID))
        end
        return
    end

    eventFrame._DebugHoverTrace("Ticker", "create questID=%s", tostring(questID))
    activeHoverState.ticker = WOW.C_Timer.NewTicker(0.15, function()
        TickActiveWorldQuestHover(false)
    end)
end

---@param row Frame?
---@param questID number?
---@param forceReplay boolean?
activeHoverState.StartOrResume = function(row, questID, forceReplay, source, context)
    if not questID or questID == 0 then
        return
    end

    local previousQuestID = activeHoverState.questID
    local rowQuestID = row and rawget(row, "questID") or nil
    local rowHovered = IsQuestRowActuallyHovered(row)
    eventFrame._DebugHoverTrace(
        "StartOrResume",
        "source=%s context=%s questID=%s previousQuestID=%s forceReplay=%s questSwitch=%s row=%s rowQuestID=%s rowHovered=%s",
        tostring(source or "unknown"),
        tostring(context or ""),
        tostring(questID),
        tostring(previousQuestID),
        tostring(forceReplay == true),
        tostring(previousQuestID ~= questID),
        eventFrame._GetHoverRowIdentity(row),
        tostring(rowQuestID),
        tostring(rowHovered))

    activeHoverState.stopToken = (activeHoverState.stopToken or 0) + 1

    if activeHoverState.questID ~= questID then
        StopActiveWorldQuestHover("quest-changed")
        activeHoverState.questID = questID
    end

    if row and rowQuestID == questID then
        SetActiveWorldQuestHoverRow(row)
    else
        local activeRow = activeHoverState.row
        local activeRowQuestID = activeRow and rawget(activeRow, "questID") or nil
        if activeRow and activeRowQuestID ~= questID then
            activeHoverState.row = nil
            activeHoverState.poiBtn = nil
        end
    end

    ResetActiveWorldQuestHoverSurfaceMisses()

    EnsureActiveWorldQuestHoverTicker()
    TickActiveWorldQuestHover(forceReplay)
end

---@param questID number?
activeHoverState.DeferStop = function(questID, reason)
    if not questID or questID == 0 then
        return
    end

    local stopToken = (activeHoverState.stopToken or 0) + 1
    activeHoverState.stopToken = stopToken
    eventFrame._DebugHoverTrace(
        "DeferStop",
        "scheduled token=%s reason=%s questID=%s",
        tostring(stopToken),
        tostring(reason or "leave"),
        tostring(questID))

    if not WOW.C_Timer or not WOW.C_Timer.After then
        if activeHoverState.questID == questID and not TryRebindActiveWorldQuestHoverRow() then
            eventFrame._DebugHoverTrace("DeferStop", "callback outcome=stopped reason=no-timer")
            StopActiveWorldQuestHover("leave")
        else
            eventFrame._DebugHoverTrace("DeferStop", "callback outcome=kept reason=rebind-or-quest-mismatch")
        end
        return
    end

    WOW.C_Timer.After(0, function()
        if activeHoverState.stopToken ~= stopToken or activeHoverState.questID ~= questID then
            eventFrame._DebugHoverTrace(
                "DeferStop",
                "callback outcome=kept reason=token-or-quest-changed token=%s liveToken=%s liveQuestID=%s",
                tostring(stopToken),
                tostring(activeHoverState.stopToken),
                tostring(activeHoverState.questID))
            return
        end
        if TryRebindActiveWorldQuestHoverRow() then
            ResetActiveWorldQuestHoverSurfaceMisses()
            eventFrame._DebugHoverTrace("DeferStop", "callback outcome=kept reason=rebind-hovered")
            return
        end
        eventFrame._DebugHoverTrace("DeferStop", "callback outcome=stopped reason=leave")
        StopActiveWorldQuestHover("leave")
    end)
end

---@param row Frame?
activeHoverState.ResumeForShownRow = function(row, source)
    local rowQuestID = row and rawget(row, "questID") or nil
    if not row
        or not activeHoverState.questID
        or rowQuestID ~= activeHoverState.questID
    then
        return
    end

    local isHovered = IsQuestRowActuallyHovered(row)
    eventFrame._DebugHoverTrace(
        "ResumeForShownRow",
        "source=%s questID=%s row=%s hovered=%s",
        tostring(source or "unknown"),
        tostring(rowQuestID),
        eventFrame._GetHoverRowIdentity(row),
        tostring(isHovered))

    if isHovered then
        activeHoverState.StartOrResume(row, rowQuestID, false, "resume-for-shown-row", source)
    end
end
end

local function BuildContractAuraFactionMap()
    local auraMap = {}
    local contractConfigs = ns.WORLD_QUEST_CONTRACTS or {}

    for _, contract in ipairs(contractConfigs) do
        if type(contract) == "table" and type(contract.label) == "string" then
            for _, spellID in ipairs(contract.buffIDs or {}) do
                if type(spellID) == "number" and spellID > 0 then
                    auraMap[spellID] = contract.label
                end
            end
        end
    end

    return auraMap
end

local CONTRACT_AURA_FACTIONS = BuildContractAuraFactionMap()

local function GetContractDisplayName(name)
    if type(name) ~= "string" then
        return ""
    end
    return (WOW.string_gsub(name, "^The%s+", ""))
end

local function NormalizeFactionName(name)
    if type(name) ~= "string" or name == "" then
        return nil
    end

    local normalized = WOW.string_lower(name)
    normalized = WOW.string_gsub(normalized, "^the%s+", "")
    normalized = WOW.string_gsub(normalized, "[%s%'%-]", "")
    return normalized ~= "" and normalized or nil
end

local function DoesFactionMatchContract(contractInfo, factionName)
    return contractInfo
        and contractInfo.normalizedFactionName
        and NormalizeFactionName(factionName) == contractInfo.normalizedFactionName
end

local function GetActiveContractAura()
    if not WOW.C_UnitAuras then
        return nil
    end

    for spellID, factionName in pairs(CONTRACT_AURA_FACTIONS) do
        local aura = WOW.C_UnitAuras.GetPlayerAuraBySpellID
            and WOW.C_UnitAuras.GetPlayerAuraBySpellID(spellID)
        if not aura and AuraUtil and AuraUtil.FindAuraBySpellID then
            aura = AuraUtil.FindAuraBySpellID(spellID, "player", "HELPFUL")
        end
        if aura then
            return {
                aura = aura,
                spellID = spellID,
                factionName = factionName,
                displayName = GetContractDisplayName(factionName),
                normalizedFactionName = NormalizeFactionName(factionName),
            }
        end
    end

    return nil
end

local function GetAuraRemainingSeconds(aura)
    if type(aura) ~= "table" then
        return nil
    end

    local expirationTime = aura.expirationTime or aura.expirationTimeMs
    if type(expirationTime) ~= "number" or expirationTime <= 0 then
        return nil
    end

    if aura.expirationTimeMs and not aura.expirationTime then
        expirationTime = expirationTime / 1000
    end

    return math.max(0, expirationTime - WOW.GetTime())
end

local function FindRelevantActiveContract()
    local activeContract = GetActiveContractAura()
    if not activeContract or not activeContract.normalizedFactionName then
        return nil
    end

    return {
        factionName = activeContract.factionName,
        displayName = activeContract.displayName,
        normalizedFactionName = activeContract.normalizedFactionName,
        auraName = activeContract.aura.name,
        spellID = activeContract.spellID,
        auraInstanceID = activeContract.aura.auraInstanceID,
        remainingSeconds = GetAuraRemainingSeconds(activeContract.aura),
    }
end

local function RefreshContractNotice()
    currentRelevantContract = FindRelevantActiveContract()

    if not contractNoticeFrame or not contractNoticeLabel
        or not UpdateContractNoticeLayout
    then
        return
    end

    if currentRelevantContract then
        local message = "Active Contract: " .. currentRelevantContract.displayName
        if currentRelevantContract.remainingSeconds
            and currentRelevantContract.remainingSeconds > 0
        then
            message = message .. " (" .. FormatTimeLeft(currentRelevantContract.remainingSeconds) .. " remaining)"
        end
        message = message .. "."
        contractNoticeLabel:SetText(message)
        contractNoticeLabel:SetTextColor(
            COL.contractNotice[1], COL.contractNotice[2], COL.contractNotice[3])
    else
        contractNoticeLabel:SetText("No active contract.")
        contractNoticeLabel:SetTextColor(
            COL.contractNoticeNone[1], COL.contractNoticeNone[2], COL.contractNoticeNone[3])
    end
end

-- Returns r, g, b values for the expiry dot based on seconds remaining.
local function GetDotColor(seconds)
    local dotColor
    if not seconds then
        dotColor = COL.dotOk
    elseif seconds < K.TIME_RED then
        dotColor = COL.dotRed
    elseif seconds < K.TIME_ORANGE then
        dotColor = COL.dotOrange
    elseif seconds < K.TIME_YELLOW then
        dotColor = COL.dotYellow
    else
        dotColor = COL.dotOk
    end
    return dotColor[1], dotColor[2], dotColor[3]
end

-- =============================================
-- Quest classification helpers  (tag, expansion, reward type)
-- =============================================

-- World quest activity type constants from Enum.QuestTagType.
-- info.worldQuestType (NOT info.tagID) is the correct discriminator for world
-- quest activity type.  Values from Blizzard's Enum.QuestTagType global at
-- addon load; numeric fallbacks verified from WorldQuestTracker's in-game dump:
--   TAG=0, PROF=1, NORMAL=2, PVP=3, PET_BATTLE=4, BOUNTY=5, DUNGEON=6,
--   INVASION=7, RAID=8, INVASION_WRAPPER=11, CAPSTONE=17
local WQT_PVP, WQT_PET, WQT_DUNGEON, WQT_RAID, WQT_DELVE, WQT_CAPSTONE
do
    local e = Enum.QuestTagType
    WQT_PVP     = (e and e.PvP)       or 3
    WQT_PET     = (e and e.PetBattle) or 4
    WQT_DUNGEON = (e and e.Dungeon)   or 6
    WQT_RAID    = (e and e.Raid)      or 8
    WQT_DELVE   = e and rawget(e, "Delve") -- nil if not defined in this client version
    WQT_CAPSTONE = (e and e.Capstone) or 17
end
-- Dragon-racing / skyriding uses tagID not worldQuestType (confirmed: WQT IsRacingQuest).
-- Keep the legacy special-assignment tagID as a fallback, but prefer capstone
-- worldQuestType / questTagType for both classification and gather-time admission.
local TAG_ID_SPECIAL_ASSIGNMENT = 286
local TAG_ID_SPECIAL_ASSIGNMENT_BLOCKER = 287
local TAG_ID_RACING = 281

local function IsSpecialAssignmentTagID(rawTagID)
    return rawTagID == TAG_ID_SPECIAL_ASSIGNMENT
        or rawTagID == TAG_ID_SPECIAL_ASSIGNMENT_BLOCKER
end

local function IsSpecialAssignmentFromMetadata(rawQuestTagType, rawTagID, questType)
    return questType == "special_assignment"
        or rawQuestTagType == WQT_CAPSTONE
        or IsSpecialAssignmentTagID(rawTagID)
end

local function IsSpecialAssignmentQuest(questID, rawQuestTagType, rawTagID, questType, returnConfirmationState)
    if IsSpecialAssignmentFromMetadata(rawQuestTagType, rawTagID, questType) then
        if returnConfirmationState then
            return true, true
        end
        return true
    end

    if questID and WOW.C_QuestLog and WOW.C_QuestLog.GetQuestTagInfo then
        local info = WOW.C_QuestLog.GetQuestTagInfo(questID)
        if info then
            local isSpecialAssignment = IsSpecialAssignmentFromMetadata(
                info.worldQuestType,
                info.tagID,
                questType)
            if returnConfirmationState then
                return isSpecialAssignment, true
            end
            return isSpecialAssignment
        end
    end

    if returnConfirmationState then
        return false, false
    end

    return false
end

local function ResolveQuestLockedState(questID, baseEntry)
    local isLocked = baseEntry and baseEntry.isLocked == true or false
    if not isLocked then
        return false
    end

    if baseEntry.isAreaPOI then
        return true
    end

    if not IsSpecialAssignmentFromMetadata(
        baseEntry.rawQuestTagType,
        baseEntry.rawTagID,
        baseEntry.questType)
    then
        return true
    end

    if WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(questID) then
        return false
    end

    return true
end

local function GetPersistedLockedSpecialAssignmentExpiresAt(
    questID,
    isLocked,
    rawQuestTagType,
    rawTagID,
    questType,
    timeLeft,
    expiresAt)
    if expiresAt or timeLeft ~= nil then
        return expiresAt
    end

    if not isLocked or not currentQuestEntries or #currentQuestEntries == 0 then
        return nil
    end

    if not IsSpecialAssignmentFromMetadata(rawQuestTagType, rawTagID, questType) then
        return nil
    end

    local currentServerTime
    for _, existingEntry in ipairs(currentQuestEntries) do
        if existingEntry.questID == questID and existingEntry.expiresAt then
            currentServerTime = currentServerTime or GetCurrentServerTime()
            if existingEntry.expiresAt > currentServerTime then
                return existingEntry.expiresAt
            end
        end
    end

    return nil
end

-- Human-readable labels kept in display order (used by the filter UI).
local QUEST_TYPE_ORDER = {
    "account_wide", "special_assignment", "normal", "raid", "dungeon",
    "delve", "pvp", "skyriding", "pet_battle",
}
local QUEST_TYPE_LABEL = {
    account_wide = "Account-Wide",
    special_assignment = "Special Assignment",
    normal       = "Regular",
    raid         = "Raid",
    dungeon      = "Dungeon",
    delve        = "Delve",
    pvp          = "PvP",
    skyriding    = "Skyriding / Race",
    pet_battle   = "Pet Battle",
}

-- Cache: cleared per-quest by QUEST_DATA_LOAD_RESULT so stale "normal"
-- defaults from unloaded data don't persist.
local questTypeCache = {}

local function GetQuestType(questID, rawQuestTagType, rawTagID)
    if questTypeCache[questID] then return questTypeCache[questID] end

    -- Guard: if quest data hasn't loaded, tag info will be nil and we'd
    -- permanently mis-classify the quest as "normal".  Return nil to signal
    -- "unknown" rather than poison the cache.
    if not eventFrame:IsQuestCoreDataReady(questID) then
        if IsSpecialAssignmentQuest(questID, rawQuestTagType, rawTagID) then
            return "special_assignment"
        end
        return nil
    end

    local t = "normal"
    if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestTagInfo then
        local info = WOW.C_QuestLog.GetQuestTagInfo(questID)
        if info then
            local wqt = info.worldQuestType
            if     wqt == WQT_CAPSTONE then t = "special_assignment"
            elseif wqt == WQT_PVP     then t = "pvp"
            elseif wqt == WQT_PET     then t = "pet_battle"
            elseif wqt == WQT_DUNGEON then t = "dungeon"
            elseif wqt == WQT_RAID    then t = "raid"
            elseif WQT_DELVE and wqt == WQT_DELVE then t = "delve"
            end
            if t == "normal" then
                if IsSpecialAssignmentTagID(info.tagID) then
                    t = "special_assignment"
                elseif info.tagID == TAG_ID_RACING then
                    t = "skyriding"
                end
            end
        end
    end
    if t == "normal" and IsSpecialAssignmentQuest(questID, rawQuestTagType, rawTagID) then
        t = "special_assignment"
    end
    if t == "normal" and WOW.C_QuestLog and WOW.C_QuestLog.IsAccountQuest then
        if WOW.C_QuestLog.IsAccountQuest(questID) then t = "account_wide" end
    end
    questTypeCache[questID] = t
    return t
end

-- Reward type priority order (highest first) for sort-by-reward.
-- "currency" covers both general currency and progression resources.
local REWARD_TYPE_PRIORITY = {
    gear = 1, currency = 2, rep = 3,
    pet  = 4, gold = 5, other = 6,
}
local REWARD_TYPE_ORDER = {
    "gear", "currency", "rep", "pet", "gold", "other",
}
local REWARD_TYPE_LABEL = {
    gear     = "Gear",
    currency = "Currency / Resources",
    rep      = "Reputation",
    pet      = "Pet",
    gold     = "Gold",
    other    = "Other Items",
}

do
    local function IsActualGearRewardClass(classID, subclassID)
        local itemClass = Enum and Enum.ItemClass
        if not itemClass then
            return false
        end

        if classID == itemClass.Weapon or classID == itemClass.Armor then
            return true
        end

        local itemGemSubclass = Enum and Enum.ItemGemSubclass
        return itemGemSubclass
            and itemClass.Gem
            and itemGemSubclass.Artifactrelic
            and classID == itemClass.Gem
            and subclassID == itemGemSubclass.Artifactrelic
    end

    ns._WorldQuestsIsActualGearRewardItem = function(itemID)
        if not itemID or not WOW.C_Item or not WOW.C_Item.GetItemInfoInstant then
            return false
        end

        local _, _, _, _, _, classID, subclassID = WOW.C_Item.GetItemInfoInstant(itemID)
        return IsActualGearRewardClass(classID, subclassID)
    end

    ns._WorldQuestsShouldShowRewardItemLevel = function(rewardData)
        return rewardData
            and rewardData.rewardType == "item"
            and rewardData.ilvl and rewardData.ilvl > 0
            and ns._WorldQuestsIsActualGearRewardItem(rewardData.itemID)
    end
end

-- Maps a single reward entry to a canonical reward-type string.
local function ClassifyRewardEntry(rd)
    if rd.rewardType == "gold" then return "gold" end
    if rd.rewardType == "rep"  then return "rep"  end
    if rd.rewardType == "currency" then
        -- Currency and resource currencies are combined into a single filter type.
        return "currency"
    end
    if rd.rewardType == "item" then
        if rd.itemID then
            local _, _, itemSubType = WOW.C_Item.GetItemInfoInstant(rd.itemID)
            if ns._WorldQuestsIsActualGearRewardItem(rd.itemID) then
                return "gear"
            end
            if itemSubType == "Battle Pets" or itemSubType == "Companion Pets" then
                return "pet"
            end
        end
        return "other"
    end
    return "other"
end

-- Returns the single highest-priority reward type for a quest's reward list.
-- Used for sorting and display filtering.
local function GetPrimaryRewardType(rewards)
    local best = "other"
    local bestPrio = REWARD_TYPE_PRIORITY["other"]
    for _, rd in ipairs(rewards) do
        local rtype = ClassifyRewardEntry(rd)
        local prio = REWARD_TYPE_PRIORITY[rtype] or 99
        if prio < bestPrio then
            best = rtype
            bestPrio = prio
        end
    end
    return best
end

-- Tier ladder sort key for weekly cache / special-assignment item displays.
-- English-only checks miss localized names (e.g. French "Vétéran"); add common
-- locale fragments. Mirrors World Quest Tracker / Blizzard widget best-pick
-- logic (quality, then tier, then itemID).
local function GetAssignmentCacheTierSortKey(name)
    if not name or name == "" then
        return 0
    end
    local lower = WOW.string_lower(name)
    if WOW.string_find(lower, "mythic") or WOW.string_find(lower, "mythique")
        or WOW.string_find(lower, "mythisch")
    then
        return 500
    end
    -- Do not match bare "hero" (too easy to false-positive); "heroic" / locale only.
    if WOW.string_find(lower, "heroic")
        or WOW.string_find(lower, "héro") or WOW.string_find(lower, "héroe")
        or WOW.string_find(lower, "heroisch")
    then
        return 400
    end
    if WOW.string_find(lower, "champion") then
        return 300
    end
    if WOW.string_find(lower, "veteran") or WOW.string_find(lower, "vétéran")
        or WOW.string_find(lower, "veterano")
    then
        return 200
    end
    if WOW.string_find(lower, "adventurer") or WOW.string_find(lower, "aventur")
        or WOW.string_find(lower, "abenteur")
    then
        return 100
    end
    return 150
end

local GetSpecialAssignmentDisplayTierRank = GetAssignmentCacheTierSortKey

-- Parallel weekly cache tiers (Adventurer vs Veteran): compare **name tier first**.
-- GetQuestLogRewardInfo's scaled ilevel can list a *lower* tier with a higher number
-- than your actual Veteran reward, so sorting by rewardItemLevel first picked Adventurer.
local function WeeklyParallelRewardItemIsBetterForDisplay(a, b)
    local function labelOf(r)
        if not r then
            return nil
        end
        if r.label and r.label ~= "" then
            return r.label
        end
        if r.text and r.text ~= "" then
            return r.text
        end
        if r.itemID and WOW.C_Item and WOW.C_Item.GetItemNameByID then
            return WOW.C_Item.GetItemNameByID(r.itemID)
        end
        return nil
    end

    local ra = GetSpecialAssignmentDisplayTierRank(labelOf(a))
    local rb = GetSpecialAssignmentDisplayTierRank(labelOf(b))
    if ra ~= rb then
        return ra > rb
    end

    local la = type(a.rewardItemLevel) == "number" and a.rewardItemLevel or 0
    local lb = type(b.rewardItemLevel) == "number" and b.rewardItemLevel or 0
    if la > 0 and lb == 0 then
        return true
    end
    if lb > 0 and la == 0 then
        return false
    end
    if la > 0 and lb > 0 and la ~= lb then
        return la > lb
    end

    local qa = a.quality or 0
    local qb = b.quality or 0
    if qa ~= qb then
        return qa > qb
    end
    return (a.itemID or 0) > (b.itemID or 0)
end

-- Locked Area POI tooltips walk every item-display widget and pick the best tier
-- (AppendSingleBestItemDisplayFromWidgetSet). Row parsing used to stop after the
-- first reward block, so only the first widget (often lowest tier) was shown.
-- Collapse consecutive parallel item rows to a single best, like the tooltip.
local function ShouldCollapseLockedAreaPOIItemCluster(cluster)
    if #cluster < 2 then
        return false
    end
    -- Parallel weekly cache tiers (Adventurer / Veteran / …) often differ in item
    -- quality; still only one row reward. Do not require matching quality.
    local anyTierKeyword = false
    local icon0 = cluster[1].icon
    local allSameIcon = icon0 and icon0 ~= ""
    for i = 1, #cluster do
        if cluster[i].icon ~= icon0 then
            allSameIcon = false
        end
        local lab = cluster[i].label
        if lab and lab ~= "" then
            local rk = GetAssignmentCacheTierSortKey(lab)
            if rk >= 100 and rk ~= 150 then
                anyTierKeyword = true
            end
            local low = WOW.string_lower(lab)
            if WOW.string_find(low, "cach") then
                anyTierKeyword = true
            end
        end
    end
    -- Multiple item-display widgets with the same art are almost always tier rungs.
    if allSameIcon and icon0 then
        return true
    end
    return anyTierKeyword
end

local function CollapseLockedAreaPOIParallelItemRewards(rewards)
    if not rewards or #rewards == 0 then
        return rewards
    end
    local out = {}
    local i = 1
    while i <= #rewards do
        local r = rewards[i]
        if not r or r.rewardType ~= "item" then
            out[#out + 1] = r
            i = i + 1
        else
            local j = i
            local cluster = {}
            while j <= #rewards and rewards[j] and rewards[j].rewardType == "item" do
                cluster[#cluster + 1] = rewards[j]
                j = j + 1
            end
            if ShouldCollapseLockedAreaPOIItemCluster(cluster) then
                WOW.table_sort(cluster, function(a, b)
                    return WeeklyParallelRewardItemIsBetterForDisplay(a, b)
                end)
                out[#out + 1] = cluster[1]
            else
                for k = 1, #cluster do
                    out[#out + 1] = cluster[k]
                end
            end
            i = j
        end
    end
    return out
end

-- â”€â”€ Expansion detection â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Uses QuestUtils_GetQuestExpansion when available (Retail 10.1+).
-- Falls back to WOW.C_QuestLog.GetQuestExpansion.
local function GetQuestExpansionID(questID)
    local getQuestExpansion = _G and _G["QuestUtils_GetQuestExpansion"]
    if getQuestExpansion then
        return getQuestExpansion(questID)
    end
    if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestExpansion then
        return WOW.C_QuestLog.GetQuestExpansion(questID)
    end
    return nil
end

-- Human-readable expansion names keyed by Blizzard's LE_EXPANSION_* values.
local EXPANSION_LABELS = {}
do
    -- Populate from Blizzard's own LE_EXPANSION_* globals if present.
    local map = {
        [0]  = "Classic",
        [1]  = "The Burning Crusade",
        [2]  = "Wrath of the Lich King",
        [3]  = "Cataclysm",
        [4]  = "Mists of Pandaria",
        [5]  = "Warlords of Draenor",
        [6]  = "Legion",
        [7]  = "Battle for Azeroth",
        [8]  = "Shadowlands",
        [9]  = "Dragonflight",
        [10] = "The War Within",
        [11] = "Midnight",
    }
    for k, v in pairs(map) do EXPANSION_LABELS[k] = v end
end

-- =============================================
-- Quest data gathering
-- =============================================

-- Returns the display title for a quest, or nil when quest data has not
-- loaded yet.  Intentionally never caches â€” pre-data-load fallbacks like
-- "Quest #12345" would persist permanently in the old cache and never be
-- replaced by the real title when it arrived.
-- questID: number
local function GetQuestTitle(questID)
    -- Primary: WOW.C_QuestLog is the authoritative title source in 12.0.
    if WOW.C_QuestLog and WOW.C_QuestLog.GetTitleForQuestID then
        local title = WOW.C_QuestLog.GetTitleForQuestID(questID)
        if title and title ~= "" then return title end
    end
    -- Secondary: WOW.C_TaskQuest variant (task-quest specific, returns title
    -- as first return value).  Only useful once the server has sent data.
    if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestInfoByQuestID then
        local title = WOW.C_TaskQuest.GetQuestInfoByQuestID(questID)
        if title and title ~= "" then return title end
    end
    -- No data yet â€” return nil so callers know the title is pending.
    return nil
end

function eventFrame:IsQuestCoreDataReady(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    if WOW.HaveQuestData and not WOW.HaveQuestData(questID) then
        return false
    end

    local title = GetQuestTitle(questID)
    return title ~= nil and title ~= ""
end

function eventFrame:QueueQuestCoreDataLoad(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    if self:IsQuestCoreDataReady(questID) then
        requestedQuestData[questID] = nil
        questDataRetrySuppressedUntil[questID] = nil
        return true
    end

    if not WOW.C_QuestLog or not WOW.C_QuestLog.RequestLoadQuestByID then
        return false
    end

    local now = WOW.GetTime()
    local retryAt = questDataRetrySuppressedUntil[questID]
    if retryAt and retryAt <= now then
        questDataRetrySuppressedUntil[questID] = nil
        retryAt = nil
    end

    if retryAt and retryAt > now then
        EnsureQuestDataRetryRefresh()
        return false
    end

    local requestedAt = requestedQuestData[questID]
    if requestedAt then
        if type(requestedAt) ~= "number" then
            requestedQuestData[questID] = nil
            requestedAt = nil
        end

        if requestedAt and requestedAt + K.QUEST_CORE_DATA_REQUEST_STALE_TIMEOUT > now then
            return true
        end

        if requestedAt then
            requestedQuestData[questID] = nil
        end
    end

    WOW.C_QuestLog.RequestLoadQuestByID(questID)
    requestedQuestData[questID] = now
    return false
end

local function NormalizeInlineQuestText(text)
    if not text or text == "" then
        return nil
    end

    local normalized = WOW.string_gsub(text, "[\r\n]+", " ")
    normalized = WOW.string_gsub(normalized, "%s+", " ")
    normalized = WOW.string_gsub(normalized, "^%s+", "")
    normalized = WOW.string_gsub(normalized, "%s+$", "")
    if normalized == "" then
        return nil
    end

    return normalized
end

do
    function eventFrame:IsAreaPOITimeText(text)
        if not text or text == "" or not WOW.string_find(text, "%d") then
            return false
        end

        -- Strip common WoW tokens so patterns can match Blizzard widget strings.
        text = WOW.string_gsub(text, "|c%x%x%x%x%x%x%x%x", "")
        text = WOW.string_gsub(text, "|r", "")
        text = WOW.string_gsub(text, "|T[^|]-|t", " ")

        local lowerText = WOW.string_lower(text)
        return WOW.string_find(lowerText, "^%d+:%d%d$") ~= nil
            or WOW.string_find(lowerText, "^%d+:%d%d:%d%d$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*[dhms]$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*[dhms]%s+%d+%s*[dhms]$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*[dhms]%s+%d+%s*[dhms]%s+%d+%s*[dhms]$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*day[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*hour[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*hr[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*min[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*minute[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*sec[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*second[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*day[s]?%s+%d+%s*hour[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*hour[s]?%s+%d+%s*min[s]?$") ~= nil
            or WOW.string_find(lowerText, "^%d+%s*hr[s]?%s+%d+%s*min[s]?$") ~= nil
            or WOW.string_find(lowerText, "%d+%s*days?,%s*%d+") ~= nil
            or WOW.string_find(lowerText, "%d+%s*hours?,%s*%d+") ~= nil
            -- "Time Left: 4 Days 6 Hours" (label precedes the number; TextWithState widgets).
            or (WOW.string_find(lowerText, "time left") ~= nil
                and WOW.string_find(lowerText, "%d") ~= nil
                and (
                    WOW.string_find(lowerText, "day") ~= nil
                    or WOW.string_find(lowerText, "hour") ~= nil
                    or WOW.string_find(lowerText, "min") ~= nil
                    or WOW.string_find(lowerText, "sec") ~= nil
                    or WOW.string_find(lowerText, "hr") ~= nil
                ))
    end

    function eventFrame:GetAreaPOITimeTextFromStatusBarWidgetInfo(info)
        if not info then
            return nil
        end

        if info.hasTimer then
            if info.overrideBarText and info.overrideBarText ~= "" then
                return NormalizeInlineQuestText(info.overrideBarText)
            end

            if info.text and info.text ~= "" then
                return NormalizeInlineQuestText(info.text)
            end

            return nil
        end

        -- Locked capstone / special-assignment POIs often show remaining time on a
        -- status bar without hasTimer=true; AppendAreaPOIWidgetSetToTooltip still
        -- prints the line via GetAreaPOIWidgetTooltipLine — mirror that for the list.
        if info.overrideBarText and info.overrideBarText ~= "" then
            local candidate = NormalizeInlineQuestText(info.overrideBarText)
            if candidate and self:IsAreaPOITimeText(candidate) then
                return candidate
            end
        end

        if info.text and info.text ~= "" then
            local candidate = NormalizeInlineQuestText(info.text)
            if candidate and self:IsAreaPOITimeText(candidate) then
                return candidate
            end
        end

        return nil
    end

    function eventFrame:GetAreaPOITimeTextFromTextWithStateWidgetInfo(info)
        if not info or not info.text or info.text == "" then
            return nil
        end

        local text = NormalizeInlineQuestText(info.text)
        if self:IsAreaPOITimeText(text) then
            return text
        end

        return nil
    end

    -- Widget countdown strings are localized and not reliably machine-parseable in
    -- Lua without Blizzard-provided numeric seconds; list time is omitted (---).
    function eventFrame:GetCurrentAreaPOITimeText(widgetSetID)
        return nil
    end
end

local function GetLockedSpecialAssignmentUnlockText(questID)
    local numObjectives = WOW.C_QuestLog and WOW.C_QuestLog.GetNumQuestObjectives
        and WOW.C_QuestLog.GetNumQuestObjectives(questID) or 0
    if numObjectives and numObjectives > 0 then
        for objectiveIndex = 1, numObjectives do
            local objectiveText
            if WOW.GetQuestObjectiveInfo then
                objectiveText = WOW.GetQuestObjectiveInfo(questID, objectiveIndex, false)
            end
            objectiveText = NormalizeInlineQuestText(objectiveText)
            if objectiveText then
                return objectiveText
            end
        end
    end

    if WOW.C_QuestLog and WOW.C_QuestLog.GetLogIndexForQuestID and WOW.GetQuestLogQuestText then
        local logIndex = WOW.C_QuestLog.GetLogIndexForQuestID(questID)
        if logIndex and logIndex > 0 then
            local questDescription, questObjectives = WOW.GetQuestLogQuestText(logIndex)
            questObjectives = NormalizeInlineQuestText(questObjectives)
            if questObjectives then
                return questObjectives
            end

            questDescription = NormalizeInlineQuestText(questDescription)
            if questDescription then
                return questDescription
            end
        end
    end

    return "Unlock requirements not available yet."
end

do
    local function WithSelectedQuestLogEntry(questID, callback)
        if not questID or questID <= 0 or type(callback) ~= "function"
            or not WOW.C_QuestLog or not WOW.C_QuestLog.GetLogIndexForQuestID
        then
            return nil
        end

        local logIndex = WOW.C_QuestLog.GetLogIndexForQuestID(questID)
        local getSelection = _G and _G["GetQuestLogSelection"]
        local selectEntry = _G and _G["SelectQuestLogEntry"]
        if not logIndex or logIndex <= 0
            or type(getSelection) ~= "function"
            or type(selectEntry) ~= "function"
        then
            return nil
        end

        local previousSelection = getSelection()
        if previousSelection ~= nil and type(previousSelection) ~= "number" then
            return nil
        end
        previousSelection = previousSelection or 0

        local didSelect = pcall(selectEntry, logIndex)
        if not didSelect then
            return nil
        end

        local ok, result = pcall(callback, logIndex)
        pcall(selectEntry, previousSelection)

        if not ok then
            return nil
        end
        return result
    end

    local function ParseLegacyQuestRewardFactionEntry(...)
        local values = { ... }
        local label
        local icon
        local factionID
        local numericCandidates = {}

        for _, value in ipairs(values) do
            if type(value) == "string" and value ~= "" then
                if not label
                    and not WOW.string_find(value, "\\", 1, true)
                    and not WOW.string_find(value, "/", 1, true)
                then
                    label = value
                elseif not icon
                    and (WOW.string_find(value, "\\", 1, true)
                        or WOW.string_find(value, "/", 1, true))
                then
                    icon = value
                end
            elseif type(value) == "number" and value > 0 then
                numericCandidates[#numericCandidates + 1] = value
                if not icon and value >= 10000 then
                    icon = value
                end
            end
        end

        local normalizedLabel = NormalizeFactionName(label)
        if WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID then
            for _, value in ipairs(numericCandidates) do
                if value < 10000 then
                    local data = WOW.C_Reputation.GetFactionDataByID(value)
                    if data and data.name and data.name ~= "" then
                        local normalizedDataName = NormalizeFactionName(data.name)
                        if not normalizedLabel or normalizedDataName == normalizedLabel then
                            factionID = value
                            label = data.name
                            if data["texture"] then
                                icon = data["texture"]
                            end
                            break
                        end
                    end
                end
            end
        end

        local smallestAmountCandidate
        local amountCandidateCount = 0
        for _, value in ipairs(numericCandidates) do
            if value ~= factionID and value < 10000 then
                amountCandidateCount = amountCandidateCount + 1
                if not smallestAmountCandidate or value < smallestAmountCandidate then
                    smallestAmountCandidate = value
                end
            end
        end

        local amount = nil
        if amountCandidateCount > 0 then
            amount = smallestAmountCandidate
        end

        if not label or label == "" then
            return nil
        end

        return {
            label = label,
            icon = icon,
            amount = amount,
            factionID = factionID,
        }
    end

    ns._WorldQuestsGetLegacySelectedQuestRewardFactions = function(questID)
        return WithSelectedQuestLogEntry(questID, function()
            local entries = {}
            local entryByKey = {}

            local function MergeEntry(parsed)
                if not parsed or not parsed.label or parsed.label == "" then
                    return
                end

                local key = nil
                if parsed.factionID and parsed.factionID > 0 then
                    key = "id:" .. parsed.factionID
                else
                    local normalizedLabel = NormalizeFactionName(parsed.label)
                    if normalizedLabel then
                        key = "name:" .. normalizedLabel
                    end
                end
                if not key then
                    return
                end

                local existing = entryByKey[key]
                if existing then
                    if parsed.amount and parsed.amount ~= 0 then
                        existing.amount = (existing.amount or 0) + parsed.amount
                    end
                    if not existing.icon and parsed.icon then
                        existing.icon = parsed.icon
                    end
                    if not existing.factionID and parsed.factionID then
                        existing.factionID = parsed.factionID
                    end
                    return
                end

                local entry = {
                    label = parsed.label,
                    icon = parsed.icon,
                    amount = parsed.amount,
                    factionID = parsed.factionID,
                }
                entries[#entries + 1] = entry
                entryByKey[key] = entry
            end

            local function ConsumeProvider(provider)
                if type(provider) ~= "function" then
                    return
                end

                for rewardIndex = 1, 8 do
                    local ok, value1, value2, value3, value4, value5, value6, value7 =
                        pcall(provider, rewardIndex)
                    if not ok then
                        break
                    end
                    if value1 == nil and value2 == nil and value3 == nil and value4 == nil
                        and value5 == nil and value6 == nil and value7 == nil
                    then
                        break
                    end

                    MergeEntry(ParseLegacyQuestRewardFactionEntry(
                        value1, value2, value3, value4, value5, value6, value7))
                end
            end

            ConsumeProvider(_G and _G["GetQuestLogRewardFactionInfo"])
            ConsumeProvider(_G and _G["GetQuestLogChoiceRewardFactionInfo"])

            if #entries == 1 and (not entries[1].amount or entries[1].amount == 0) then
                local getQuestRewardReputation = _G and _G["GetQuestRewardReputation"]
                if type(getQuestRewardReputation) == "function" then
                    local ok, amount = pcall(getQuestRewardReputation)
                    if ok and type(amount) == "number" and amount > 0 then
                        entries[1].amount = amount
                    end
                end
            end

            return entries
        end)
    end
end

local function GetQuestFactionLabel(questID)
    -- No caching â€” queries are cheap C-side lookups once data is loaded.
    --
    -- Match Blizzard's world-map tooltip behaviour first: contracts can
    -- intentionally rewrite the displayed world-quest faction for the
    -- relevant cluster, and that association comes from WOW.C_TaskQuest.
    if WOW.HaveQuestData and not WOW.HaveQuestData(questID) then
        return nil
    end

    if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestInfoByQuestID then
        local _, factionID = WOW.C_TaskQuest.GetQuestInfoByQuestID(questID)
        if factionID and factionID > 0
            and WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID
        then
            local data = WOW.C_Reputation.GetFactionDataByID(factionID)
            if data and data.name and data.name ~= "" then
                return data.name
            end
        end
    end

    if WOW.HaveQuestRewardData and not WOW.HaveQuestRewardData(questID) then
        return nil
    end

    -- Fallback 1: major-faction reward entries when no task faction exists.
    if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestLogMajorFactionReputationRewards
    then
        local reps = WOW.C_QuestLog.GetQuestLogMajorFactionReputationRewards(questID)
        if reps and #reps > 0 then
            for i = #reps, 1, -1 do
                local entry = reps[i]
                if entry and entry.factionID and entry.factionID > 0
                    and WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID
                then
                    local data = WOW.C_Reputation.GetFactionDataByID(entry.factionID)
                    if data and data.name and data.name ~= "" then
                        return data.name
                    end
                end
            end
        end
    end

    -- Fallback 2: reputation currencies tied to this quest.
    if C_CurrencyInfo and C_CurrencyInfo.GetFactionGrantedByCurrency
        and WOW.C_QuestLog
    then
        local function GetFactionNameFromCurrencyReward(currencyInfo)
            if not currencyInfo or not currencyInfo.currencyID then
                return nil
            end

            local fid = C_CurrencyInfo.GetFactionGrantedByCurrency(currencyInfo.currencyID)
            if fid and fid > 0
                and WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID
            then
                local data = WOW.C_Reputation.GetFactionDataByID(fid)
                if data and data.name and data.name ~= "" then
                    return data.name
                end
            end

            return nil
        end

        local currRewards = WOW.C_QuestLog.GetQuestRewardCurrencies
            and WOW.C_QuestLog.GetQuestRewardCurrencies(questID)
        if currRewards then
            for _, cur in ipairs(currRewards) do
                local factionName = GetFactionNameFromCurrencyReward(cur)
                if factionName then
                    return factionName
                end
            end
        end

        if WOW.C_QuestLog.GetQuestRewardCurrencyInfo then
            local numChoices = GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true) or 0
            for choiceIndex = 1, numChoices do
                local factionName = GetFactionNameFromCurrencyReward(
                    WOW.C_QuestLog.GetQuestRewardCurrencyInfo(questID, choiceIndex, true))
                if factionName then
                    return factionName
                end
            end
        end
    end

    local legacyFactionRewards = ns._WorldQuestsGetLegacySelectedQuestRewardFactions(questID)
    if legacyFactionRewards then
        for _, rewardData in ipairs(legacyFactionRewards) do
            if rewardData.label and rewardData.label ~= "" then
                return rewardData.label
            end
        end
    end

    return nil
end

-- Recursively builds a set of map IDs that are descendants of mapID
-- (including mapID itself) using the World Map parent/child hierarchy only.
local function CollectDescendantMapIDs(mapID, set, depth)
    if set[mapID] then return end
    if (depth or 0) > 32 then return end
    set[mapID] = true
    if not WOW.C_Map then return end

    local children = WOW.C_Map.GetMapChildrenInfo and WOW.C_Map.GetMapChildrenInfo(mapID)
    if not children then return end

    for i = 1, #children do
        local child   = children[i]
        local childID = child and child.mapID
        if childID then
            CollectDescendantMapIDs(childID, set, (depth or 0) + 1)
        end
    end
end

-- Returns the cached descendant map ID set for mapID, computing it once per
-- unique mapID.  The WoW map hierarchy is static for the session, so the cache
-- entry is valid indefinitely.
local function GetDescendantMapSet(mapID)
    if not mapDescendantsCache[mapID] then
        local set = {}

        -- Prefer Blizzard's built-in all-descendants traversal so deeper
        -- subzone chains are not missed by a shallow recursive walk.
        if WOW.C_Map and WOW.C_Map.GetMapChildrenInfo then
            set[mapID] = true
            local descendants = WOW.C_Map.GetMapChildrenInfo(mapID, nil, true)
            if descendants then
                for i = 1, #descendants do
                    local childInfo = descendants[i]
                    local childID = childInfo and childInfo.mapID
                    if childID then
                        set[childID] = true
                    end
                end
            end
        end

        if not next(set) then
            CollectDescendantMapIDs(mapID, set, 0)
        end

        mapDescendantsCache[mapID] = set
    end
    return mapDescendantsCache[mapID]
end

function eventFrame:GetDescendantMapList(mapID)
    self._mapDescendantListCache = self._mapDescendantListCache or {}

    if not self._mapDescendantListCache[mapID] then
        local set = GetDescendantMapSet(mapID)
        local mapIDs = {}

        for descendantMapID in pairs(set) do
            mapIDs[#mapIDs + 1] = descendantMapID
        end

        WOW.table_sort(mapIDs)
        self._mapDescendantListCache[mapID] = mapIDs
    end

    return self._mapDescendantListCache[mapID]
end

function eventFrame:IsRelevantLockedAreaPOIInfo(poiInfo)
    return poiInfo and poiInfo.atlasName
        and WOW.string_find(poiInfo.atlasName, "Capstone", 1, true)
end

function eventFrame:ShouldAdmitLockedAreaPOIForQuery(mapsToQuery, poiInfo, allowOutsideHierarchy)
    if allowOutsideHierarchy == true then
        return true
    end

    local linkedUiMapID = poiInfo and poiInfo.linkedUiMapID or nil
    local isOwnedByQueryTree = poiInfo and poiInfo.isPrimaryMapForPOI == true or false

    if not isOwnedByQueryTree and linkedUiMapID and linkedUiMapID > 0 then
        isOwnedByQueryTree = mapsToQuery and mapsToQuery[linkedUiMapID] == true
    end

    return isOwnedByQueryTree
end

function eventFrame:IsOverviewOrClusterMapQuery(mapID)
    if type(mapID) ~= "number" or mapID <= 0 then
        return false
    end

    if hierarchyNodeSet[mapID] == true then
        return true
    end

    if WOW.C_Map and WOW.C_Map.GetMapInfo then
        local mapInfo = WOW.C_Map.GetMapInfo(mapID)
        return mapInfo and mapInfo.mapType == K.MAP_TYPE_WORLD or false
    end

    return false
end

local function IsOverviewStyleLockedQuestRow(q, queryMapID, mapsToQuery, allowOutsideHierarchy)
    if not q then
        return false
    end

    if allowOutsideHierarchy ~= true then
        return false
    end

    if q.isMapIndicatorQuest == true then
        return true
    end

    local questMapID = q.mapID
    if questMapID and questMapID ~= 0
        and queryMapID and questMapID ~= queryMapID
        and not mapsToQuery[questMapID]
    then
        return true
    end

    return false
end

local function IsProvisionalLockedSpecialAssignmentCandidate(
    q,
    queryMapID,
    mapsToQuery,
    allowOutsideHierarchy)
    local questID = q and q.questID
    if not questID then
        return false
    end

    if WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(questID) then
        return false
    end

    if IsSpecialAssignmentFromMetadata(q.questTagType, q.tagID) then
        return true
    end

    return IsOverviewStyleLockedQuestRow(q, queryMapID, mapsToQuery, allowOutsideHierarchy)
end

local function ResolveQuestMapIDForList(questID, fallbackMapID)
    if questID then
        if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestZoneID then
            local questZoneMapID = WOW.C_TaskQuest.GetQuestZoneID(questID)
            if questZoneMapID and questZoneMapID ~= 0 then
                return questZoneMapID
            end
        end

        if WOW.GetQuestUiMapIDCompat then
            local ok, questUiMapID = pcall(WOW.GetQuestUiMapIDCompat, questID, true)
            if ok and questUiMapID and questUiMapID ~= 0 then
                return questUiMapID
            end
        end
    end

    return fallbackMapID
end

local function IsInactiveSpecialAssignmentQuestID(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    if WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(questID) then
        return false
    end

    return IsSpecialAssignmentQuest(questID)
end

local function UpgradeExistingBountyLockedQuestEntry(
    rawEntries,
    mapsToQuery,
    questID,
    queryMapID,
    allowOutsideHierarchy)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    local questMapID = ResolveQuestMapIDForList(
        questID,
        allowOutsideHierarchy == true and queryMapID or nil)
    if not mapsToQuery[questMapID] and allowOutsideHierarchy == true then
        questMapID = queryMapID
    end

    if not mapsToQuery[questMapID] then
        return false
    end

    local qInfo = WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(questMapID)
    local tagInfo = WOW.C_QuestLog and WOW.C_QuestLog.GetQuestTagInfo
        and WOW.C_QuestLog.GetQuestTagInfo(questID)

    for _, entry in ipairs(rawEntries) do
        if entry.questID == questID then
            if entry.isLocked == true then
                entry.mapID = questMapID
                entry.mapName = qInfo and qInfo.name or entry.mapName or ""
                entry.rawQuestTagType = tagInfo and tagInfo.worldQuestType or entry.rawQuestTagType
                entry.rawTagID = tagInfo and tagInfo.tagID or entry.rawTagID
                entry.bountyLockedCandidate = true
                return true
            end
            return false
        end
    end

    return false
end

local function AppendLockedQuestByID(
    rawEntries,
    seen,
    mapsToQuery,
    questID,
    queryMapID,
    bountyLockedCandidate,
    allowOutsideHierarchy)
    if seen[questID] then
        if bountyLockedCandidate == true then
            UpgradeExistingBountyLockedQuestEntry(
                rawEntries,
                mapsToQuery,
                questID,
                queryMapID,
                allowOutsideHierarchy)
        end
        return
    end

    if bountyLockedCandidate == true then
        if type(questID) ~= "number" or questID <= 0 then
            return
        end

        if WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(questID) then
            return
        end
    elseif not IsInactiveSpecialAssignmentQuestID(questID) then
        return
    end

    local questMapID = ResolveQuestMapIDForList(
        questID,
        allowOutsideHierarchy == true and queryMapID or nil)
    if not mapsToQuery[questMapID] and allowOutsideHierarchy == true then
        questMapID = queryMapID
    end
    if not mapsToQuery[questMapID] then
        return
    end

    seen[questID] = true
    local qInfo = WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(questMapID)
    local tagInfo = WOW.C_QuestLog and WOW.C_QuestLog.GetQuestTagInfo
        and WOW.C_QuestLog.GetQuestTagInfo(questID)
    rawEntries[#rawEntries + 1] = {
        questID = questID,
        mapID = questMapID,
        mapName = qInfo and qInfo.name or "",
        rawQuestTagType = tagInfo and tagInfo.worldQuestType,
        rawTagID = tagInfo and tagInfo.tagID,
        isMapIndicatorQuest = false,
        isLocked = true,
        bountyLockedCandidate = bountyLockedCandidate == true,
    }
end

local function GetExistingQuestEntry(questID)
    if not questID then
        return nil
    end

    return eventFrame._currentQuestEntriesByID[questID] or nil
end

-- Inactive special-assignment bounty for this map (nil when unlocked or unknown).
local function GetBountyLockQuestIDForMap(mapID)
    if not mapID or not WOW.C_QuestLog or not WOW.C_QuestLog.GetBountySetInfoForMapID then
        return nil
    end

    local _, lockQuestID = WOW.C_QuestLog.GetBountySetInfoForMapID(mapID)
    if type(lockQuestID) ~= "number" or lockQuestID <= 0 then
        return nil
    end
    if WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(lockQuestID) then
        return nil
    end

    return lockQuestID
end

local function GetLockedSpecialAssignmentBountyQuestID(queryMapID)
    return GetBountyLockQuestIDForMap(queryMapID)
end

-- GetBountySetInfoForMapID often keys off a *different* map than the row's
-- display map (query map, link map, parent zone, or the map the player has open).
local function ResolveBountyLockQuestIDFromQuestEntry(questEntry)
    if not questEntry then
        return nil
    end

    local tried = {}

    local function tryMap(mapID)
        if type(mapID) ~= "number" or mapID <= 0 or tried[mapID] then
            return nil
        end
        tried[mapID] = true
        return GetBountyLockQuestIDForMap(mapID)
    end

    local id = tryMap(questEntry.mapID)
    if id then return id end
    id = tryMap(questEntry.poiQueryMapID)
    if id then return id end
    id = tryMap(questEntry.linkedUiMapID)
    if id then return id end

    if WOW.C_Map and WOW.C_Map.GetBestMapForUnit then
        local playerMapID = WOW.C_Map.GetBestMapForUnit("player")
        id = tryMap(playerMapID)
        if id then return id end
    end

    if WOW.WorldMapFrame and type(WOW.WorldMapFrame.mapID) == "number" then
        id = tryMap(WOW.WorldMapFrame.mapID)
        if id then return id end
    end
    local questMapFrame = rawget(_G, "QuestMapFrame")
    if questMapFrame and type(questMapFrame.mapID) == "number" then
        id = tryMap(questMapFrame.mapID)
        if id then return id end
    end

    local function walkParents(seed)
        local step = seed
        for _ = 1, 16 do
            if type(step) ~= "number" or step <= 0 then
                break
            end
            local info = WOW.C_Map and WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(step)
            if not info then
                break
            end
            local parentMapID = info.parentMapID
            if type(parentMapID) == "number" and parentMapID > 0 then
                local found = tryMap(parentMapID)
                if found then
                    return found
                end
                step = parentMapID
            else
                break
            end
        end
        return nil
    end

    id = walkParents(questEntry.mapID)
    if id then return id end
    id = walkParents(questEntry.poiQueryMapID)
    if id then return id end

    -- Any map in the same bounty set can expose the lock quest id (Harandar-style hubs).
    local function tryMapsInBountySet(seedMapID)
        if not seedMapID or not WOW.C_QuestLog or not WOW.C_QuestLog.GetBountySetInfoForMapID then
            return nil
        end
        local _, _, bountySetID = WOW.C_QuestLog.GetBountySetInfoForMapID(seedMapID)
        if type(bountySetID) ~= "number" or bountySetID <= 0
            or not WOW.C_Map or not WOW.C_Map.GetBountySetMaps
        then
            return nil
        end
        local bountyMaps = WOW.C_Map.GetBountySetMaps(bountySetID)
        if not bountyMaps then
            return nil
        end
        for _, bm in ipairs(bountyMaps) do
            local found = tryMap(bm)
            if found then
                return found
            end
        end
        return nil
    end

    for _, seed in ipairs({
        questEntry.mapID,
        questEntry.poiQueryMapID,
        questEntry.linkedUiMapID,
    }) do
        id = tryMapsInBountySet(seed)
        if id then return id end
    end

    return nil
end

local function AppendRawQuestEntry(
    rawEntries,
    seen,
    mapsToQuery,
    q,
    queryMapID,
    rootMapID,
    isLocked,
    provisionalLockedCandidate,
    allowOutsideHierarchy)
    local questID = q and q.questID
    if not questID or seen[questID] then
        return
    end

    local rawQuestMapID = (q.mapID and q.mapID ~= 0) and q.mapID or nil
    local useQueryMapAssociation = isLocked == true
        and rawQuestMapID
        and not mapsToQuery[rawQuestMapID]
        and IsOverviewStyleLockedQuestRow(q, queryMapID, mapsToQuery, allowOutsideHierarchy)
    local questMapID = useQueryMapAssociation and queryMapID or rawQuestMapID

    if not mapsToQuery[questMapID] then
        local resolvedQuestMapID = ResolveQuestMapIDForList(questID, rawQuestMapID or queryMapID)
        if mapsToQuery[resolvedQuestMapID] then
            questMapID = resolvedQuestMapID
        end
    end

    if not mapsToQuery[questMapID]
        and isLocked == true
        and allowOutsideHierarchy == true
    then
        questMapID = queryMapID
    end

    local queryMapIsSourceOnly = queryMapID and not mapsToQuery[queryMapID]

    if not mapsToQuery[questMapID]
        and allowOutsideHierarchy ~= true
        and queryMapIsSourceOnly
        and rootMapID
    then
        -- Ancestor/source scans are discovery-only. Resolve the quest to a
        -- map within the active root tree when possible before dropping it.
        local discoveredQuestMapID = eventFrame:_SelectDiscoveredQuestMapID(
            rootMapID,
            questID,
            queryMapID,
            rawQuestMapID)
        if mapsToQuery[discoveredQuestMapID] then
            questMapID = discoveredQuestMapID
        end
    end

    local allowRootFallbackForLockedSpecialAssignment = isLocked == true
        and (provisionalLockedCandidate == true
            or IsSpecialAssignmentFromMetadata(q.questTagType, q.tagID))

    if not mapsToQuery[questMapID]
        and allowRootFallbackForLockedSpecialAssignment
        and allowOutsideHierarchy ~= true
        and queryMapIsSourceOnly
        and rootMapID
        and mapsToQuery[rootMapID]
    then
        -- Ancestor discovery passes can report ownership on maps outside the
        -- active zone tree for locked special assignments. Keep those rows
        -- attached to the active root map.
        questMapID = rootMapID
    end

    if not mapsToQuery[questMapID] then
        return
    end

    seen[questID] = true
    local qInfo = WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(questMapID)
    local existingEntry = GetExistingQuestEntry(questID)
    rawEntries[#rawEntries + 1] = {
        questID = questID,
        mapID = questMapID,
        mapName = qInfo and qInfo.name or "",
        rawQuestTagType = q.questTagType,
        rawTagID = q.tagID,
        rawTimeLeftSeconds = q.timeLeftSeconds
            or q.secondsLeft
            or ((q.timeLeftMinutes or q.minutesLeft) and ((q.timeLeftMinutes or q.minutesLeft) * 60))
            or nil,
        rawTimeLeftSecondsConsumed = existingEntry and existingEntry.rawTimeLeftSecondsConsumed == true or false,
        expiresAt = existingEntry and existingEntry.expiresAt or nil,
        isMapIndicatorQuest = q.isMapIndicatorQuest == true,
        isLocked = isLocked == true,
        provisionalLockedCandidate = provisionalLockedCandidate == true,
    }
end

-- Forward declaration â€” defined after this function but called inside it for
-- reward-type filtering/sorting.
local GetQuestRewards

-- Compares two enriched quest entries using the active quest sort mode.
-- a, b: table â€” enriched entries from GatherQuestsForCurrentMap.
-- Returns: boolean â€” true when a should sort before b.
local function CompareQuestEntries(a, b)
    if sortMode == "alpha" then
        local at = a.title or ""
        local bt = b.title or ""
        if at ~= bt then return at < bt end
    elseif sortMode == "reward" then
        local pa = REWARD_TYPE_PRIORITY[a.primReward] or 99
        local pb = REWARD_TYPE_PRIORITY[b.primReward] or 99
        if pa ~= pb then return pa < pb end

        local ta = a.timeLeft or math.huge
        local tb = b.timeLeft or math.huge
        if ta ~= tb then return ta < tb end
    elseif sortMode == "faction" then
        local fa = a.faction or ""
        local fb = b.faction or ""
        if fa ~= fb then return fa < fb end

        local ta = a.timeLeft or math.huge
        local tb = b.timeLeft or math.huge
        if ta ~= tb then return ta < tb end
    else
        local ta = a.timeLeft or math.huge
        local tb = b.timeLeft or math.huge
        if ta ~= tb then return ta < tb end
    end

    if a.mapName ~= b.mapName then
        return a.mapName < b.mapName
    end

    local at = a.title or ""
    local bt = b.title or ""
    if at ~= bt then
        return at < bt
    end

    return (a.questID or 0) < (b.questID or 0)
end

-- Groups quest entries by zone and sorts the zone buckets independently.
-- quests: table[] â€” enriched quest entries.
-- Returns: table[] â€” array of { name, quests, earliestTimeLeft } zone groups.
local function BuildSortedZoneGroups(quests)
    local zoneGroups = {}
    local zoneIndex = {}

    for _, entry in ipairs(quests) do
        local zoneName = entry.mapName or ""
        local zoneGroup = zoneIndex[zoneName]
        if not zoneGroup then
            zoneGroup = {
                name = zoneName,
                quests = {},
                earliestTimeLeft = math.huge,
            }
            zoneIndex[zoneName] = zoneGroup
            zoneGroups[#zoneGroups + 1] = zoneGroup
        end

        zoneGroup.quests[#zoneGroup.quests + 1] = entry

        local remaining = entry.timeLeft or math.huge
        if remaining < zoneGroup.earliestTimeLeft then
            zoneGroup.earliestTimeLeft = remaining
        end
    end

    for _, zoneGroup in ipairs(zoneGroups) do
        if #zoneGroup.quests > 1 then
            WOW.table_sort(zoneGroup.quests, CompareQuestEntries)
        end
    end

    if zoneSortMode == "alpha" then
        WOW.table_sort(zoneGroups, function(a, b)
            if a.name ~= b.name then
                return a.name < b.name
            end
            return a.earliestTimeLeft < b.earliestTimeLeft
        end)
    else
        WOW.table_sort(zoneGroups, function(a, b)
            if a.earliestTimeLeft ~= b.earliestTimeLeft then
                return a.earliestTimeLeft < b.earliestTimeLeft
            end
            return a.name < b.name
        end)
    end

    return zoneGroups
end

local AppendRawQuestEntriesForMap
local BuildQuestEntriesFromRawEntries
local GetRelevantWorldQuestQuerySignature
local IsQuestEntryStillVisible

do
function eventFrame:_IsMapWithinParentChain(candidateMapID, rootMapID)
    if candidateMapID == rootMapID then
        return true
    end

    if not WOW.C_Map or not WOW.C_Map.GetMapInfo then
        return false
    end

    local currentMapID = candidateMapID
    for _ = 1, 16 do
        local mapInfo = currentMapID and WOW.C_Map.GetMapInfo(currentMapID) or nil
        currentMapID = mapInfo and mapInfo.parentMapID or nil
        if not currentMapID or currentMapID == 0 then
            break
        end
        if currentMapID == rootMapID then
            return true
        end
    end

    return false
end

function eventFrame:_IsMapWithinDiscoveryRoot(candidateMapID, rootMapID)
    if type(candidateMapID) ~= "number" or candidateMapID <= 0
        or type(rootMapID) ~= "number" or rootMapID <= 0
    then
        return false
    end

    return self:_IsMapWithinParentChain(candidateMapID, rootMapID)
end

---@param rootMapID number
---@param questID number?
---@param fallbackMapID number
---@param explicitMapID number?
---@return number
function eventFrame:_SelectDiscoveredQuestMapID(rootMapID, questID, fallbackMapID, explicitMapID)
    local outsideRootMapID = nil

    local function Consider(candidateMapID)
        if type(candidateMapID) ~= "number" or candidateMapID <= 0 then
            return nil
        end

        if candidateMapID == rootMapID
            or self:_IsMapWithinDiscoveryRoot(candidateMapID, rootMapID)
        then
            return candidateMapID
        end

        if not outsideRootMapID then
            outsideRootMapID = candidateMapID
        end

        return nil
    end

    local resolvedMapID = Consider(explicitMapID)
    if resolvedMapID then
        return resolvedMapID
    end

    if questID and WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestZoneID then
        resolvedMapID = Consider(WOW.C_TaskQuest.GetQuestZoneID(questID))
        if resolvedMapID then
            return resolvedMapID
        end
    end

    if questID and WOW.GetQuestUiMapIDCompat then
        local ok, questUiMapID = pcall(WOW.GetQuestUiMapIDCompat, questID, true)
        if ok then
            resolvedMapID = Consider(questUiMapID)
            if resolvedMapID then
                return resolvedMapID
            end
        end
    end

    return outsideRootMapID or fallbackMapID or rootMapID
end

function eventFrame:_CloneMapIDSet(sourceSet)
    local clonedSet = {}

    if sourceSet then
        for memberMapID in pairs(sourceSet) do
            clonedSet[memberMapID] = true
        end
    end

    return clonedSet
end

function eventFrame:_EnsureRelevantQueryMapsToQueryMutable(queryState)
    if queryState._mapsToQueryDetached ~= true then
        queryState.mapsToQuery = self:_CloneMapIDSet(queryState.mapsToQuery)
        queryState._mapsToQueryDetached = true
    end

    return queryState.mapsToQuery
end

function eventFrame:_AddRelevantQueryMap(queryState, candidateMapID, allowOutsideHierarchy)
    if type(candidateMapID) ~= "number" or candidateMapID <= 0 then
        return
    end

    if not queryState.mapsToQuery[candidateMapID] then
        if allowOutsideHierarchy ~= true or queryState.allowOutsideHierarchy ~= true then
            return
        end

        self:_EnsureRelevantQueryMapsToQueryMutable(queryState)[candidateMapID] = true
    end

    if queryState.queryMapSet[candidateMapID] then
        return
    end

    queryState.queryMapSet[candidateMapID] = true
    queryState.queryMapIDs[#queryState.queryMapIDs + 1] = candidateMapID
end

function eventFrame:_AddRelevantQuerySourceMap(queryState, candidateMapID)
    if type(candidateMapID) ~= "number" or candidateMapID <= 0 then
        return
    end

    if queryState.queryMapSet[candidateMapID] then
        return
    end

    queryState.queryMapSet[candidateMapID] = true
    queryState.queryMapIDs[#queryState.queryMapIDs + 1] = candidateMapID
end

function eventFrame:_AddAncestorDiscoveryQueryMaps(queryState)
    if not queryState or not queryState.mapID or not WOW.C_Map or not WOW.C_Map.GetMapInfo then
        return
    end

    local ancestorMapID = queryState.mapID
    for _ = 1, 12 do
        local mapInfo = WOW.C_Map.GetMapInfo(ancestorMapID)
        ancestorMapID = mapInfo and mapInfo.parentMapID or nil
        if not ancestorMapID or ancestorMapID == 0 then
            break
        end

        -- Ancestors are discovery sources only; visibility still depends on
        -- mapsToQuery so child maps do not show unrelated surrounding zones.
        self:_AddRelevantQuerySourceMap(queryState, ancestorMapID)
    end
end

local function IsRelevantDiscoveryQuestInfo(q)
    local questID = q and q.questID
    if not questID then
        return false
    end

    local isMapIndicator = q.isMapIndicatorQuest == true
        and not (WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive and WOW.C_TaskQuest.IsActive(questID))
    if isMapIndicator then
        return true
    end

    if IsSpecialAssignmentFromMetadata(q.questTagType, q.tagID) then
        return true
    end

    return not WOW.QuestUtils_IsQuestWorldQuest
        or WOW.QuestUtils_IsQuestWorldQuest(questID)
end

local function DiscoverQuestOwnerMaps(queryState, questInfos, queryMapID)
    if not questInfos then
        return
    end

    for _, q in ipairs(questInfos) do
        if IsRelevantDiscoveryQuestInfo(q) then
            local ownerMapID = eventFrame:_SelectDiscoveredQuestMapID(
                queryState.mapID,
                q.questID,
                queryMapID,
                q.mapID)
            eventFrame:_AddRelevantQueryMap(
                queryState,
                ownerMapID,
                ownerMapID ~= queryState.mapID
                    and not eventFrame:_IsMapWithinDiscoveryRoot(ownerMapID, queryState.mapID))
        end
    end
end

local function FinalizeRelevantQueryMapIDs(queryState)
    if queryState.expandDescendantQueryMapIDs == true then
        for targetMapID in pairs(queryState.mapsToQuery) do
            eventFrame:_AddRelevantQueryMap(queryState, targetMapID, false)
        end
    end

    local signatureMapIDs = eventFrame._relevantWorldQuestQuerySignatureMapIDs or {}
    local signatureMapIDCount = 0

    eventFrame._relevantWorldQuestQuerySignatureMapIDs = signatureMapIDs

    for queryMapID in pairs(queryState.queryMapSet) do
        signatureMapIDCount = signatureMapIDCount + 1
        signatureMapIDs[signatureMapIDCount] = queryMapID
    end

    for index = signatureMapIDCount + 1, #signatureMapIDs do
        signatureMapIDs[index] = nil
    end

    if signatureMapIDCount > 0 then
        WOW.table_sort(signatureMapIDs)
        queryState.querySignature = table.concat(signatureMapIDs, ":", 1, signatureMapIDCount)
    else
        queryState.querySignature = nil
    end

    return queryState
end

---@param mapID number
---@return table?
function eventFrame:BuildRelevantWorldQuestMapQueryState(mapID)
    if type(mapID) ~= "number" or mapID <= 0 then
        return nil
    end

    local cached = self._sessionQueryStateCache[mapID]
    if cached then
        return cached
    end

    -- Cave/micro maps should inherit their parent zone query scope.
    local queryRootMapID = select(1, self:GetWorldQuestDisplayMapInfo(mapID, nil)) or mapID
    if type(queryRootMapID) ~= "number" or queryRootMapID <= 0 then
        queryRootMapID = mapID
    end

    local scanRoots = hierarchyScanRoots[queryRootMapID]
    local expandDescendantQueryMapIDs = self:IsOverviewOrClusterMapQuery(queryRootMapID)

    local mapsToQuery
    if scanRoots then
        mapsToQuery = { [queryRootMapID] = true }
        for scanIndex = 1, #scanRoots do
            local descendants = GetDescendantMapSet(scanRoots[scanIndex])
            for descendantMapID in pairs(descendants) do
                mapsToQuery[descendantMapID] = true
            end
        end
    else
        mapsToQuery = GetDescendantMapSet(queryRootMapID)
    end

    local queryState = {
        mapID = mapID,
        queryRootMapID = queryRootMapID,
        mapsToQuery = mapsToQuery,
        _mapsToQueryDetached = scanRoots ~= nil,
        queryMapSet = { [queryRootMapID] = true },
        queryMapIDs = { queryRootMapID },
        allowOutsideHierarchy = expandDescendantQueryMapIDs,
        expandDescendantQueryMapIDs = expandDescendantQueryMapIDs,
    }

    if expandDescendantQueryMapIDs ~= true then
        self:_AddAncestorDiscoveryQueryMaps(queryState)
    end

    if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestsOnMap then
        DiscoverQuestOwnerMaps(
            queryState,
            WOW.C_TaskQuest.GetQuestsOnMap(queryRootMapID),
            queryRootMapID)
    end

    local mapQuests = eventFrame._WithQuestPOIMapContext(queryRootMapID, function()
        if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestsOnMap then
            return WOW.C_QuestLog.GetQuestsOnMap(queryRootMapID)
        end
        return nil
    end)
    if mapQuests then
        DiscoverQuestOwnerMaps(queryState, mapQuests, queryRootMapID)
    end

    local bountySetID = nil
    if WOW.C_QuestLog and WOW.C_QuestLog.GetBountySetInfoForMapID then
        local _, lockQuestID, discoveredBountySetID =
            WOW.C_QuestLog.GetBountySetInfoForMapID(queryRootMapID)
        bountySetID = discoveredBountySetID

        if type(lockQuestID) == "number" and lockQuestID > 0 then
            local lockMapID = self:_SelectDiscoveredQuestMapID(
                queryRootMapID,
                lockQuestID,
                queryRootMapID,
                nil)
            eventFrame:_AddRelevantQueryMap(
                queryState,
                lockMapID,
                lockMapID ~= queryRootMapID
                    and not self:_IsMapWithinDiscoveryRoot(lockMapID, queryRootMapID))
        end
    end

    if bountySetID and bountySetID > 0 and WOW.C_Map and WOW.C_Map.GetBountySetMaps then
        local bountySetMaps = WOW.C_Map.GetBountySetMaps(bountySetID)
        if bountySetMaps then
            for _, bountyMapID in ipairs(bountySetMaps) do
                eventFrame:_AddRelevantQueryMap(queryState, bountyMapID, false)
            end
        end
    end

    if WOW.C_QuestLog and WOW.C_QuestLog.GetActiveThreatMaps then
        local threatMaps = WOW.C_QuestLog.GetActiveThreatMaps()
        if threatMaps then
            for _, threatMapID in ipairs(threatMaps) do
                local includeThreatMap = true
                if bountySetID and bountySetID > 0 and WOW.C_QuestLog.GetBountySetInfoForMapID then
                    local _, _, threatBountySetID = WOW.C_QuestLog.GetBountySetInfoForMapID(threatMapID)
                    includeThreatMap = threatBountySetID == bountySetID
                end

                if includeThreatMap then
                    eventFrame:_AddRelevantQueryMap(queryState, threatMapID, false)
                end
            end
        end
    end

    if C_AreaPoiInfo and C_AreaPoiInfo.GetAreaPOIForMap and C_AreaPoiInfo.GetAreaPOIInfo then
        local poiIDs = C_AreaPoiInfo.GetAreaPOIForMap(queryRootMapID)
        if poiIDs then
            for _, poiID in ipairs(poiIDs) do
                local poiInfo = C_AreaPoiInfo.GetAreaPOIInfo(queryRootMapID, poiID)
                if self:IsRelevantLockedAreaPOIInfo(poiInfo) then
                    local ownerMapID = queryRootMapID
                    local allowOutsideHierarchy = false

                    if poiInfo and poiInfo.isPrimaryMapForPOI == true then
                        ownerMapID = queryRootMapID
                    elseif poiInfo and poiInfo.linkedUiMapID and poiInfo.linkedUiMapID > 0 then
                        ownerMapID = poiInfo.linkedUiMapID
                        allowOutsideHierarchy =
                            not self:_IsMapWithinDiscoveryRoot(ownerMapID, queryRootMapID)
                    end

                    self:_AddRelevantQueryMap(queryState, ownerMapID, allowOutsideHierarchy)
                end
            end
        end
    end

    local result = FinalizeRelevantQueryMapIDs(queryState)
    if ns.IsDebugEnabled() then
        local mapsToQueryCount = 0
        for _ in pairs(result.mapsToQuery or {}) do
            mapsToQueryCount = mapsToQueryCount + 1
        end

        ns.DebugPrint(WOW.string_format(
            "WorldQuests: QueryState detail mapID=%s scanRoots=%s mapsToQuery=%s queryMaps=%s allowOutside=%s",
            tostring(mapID),
            tostring(scanRoots and #scanRoots or 0),
            tostring(mapsToQueryCount),
            tostring(result.queryMapIDs and #result.queryMapIDs or 0),
            tostring(result.allowOutsideHierarchy == true)))
    end
    self._sessionQueryStateCache[mapID] = result
    return result
end

GetRelevantWorldQuestQuerySignature = function(queryState, mapID)
    if queryState and queryState.querySignature and queryState.querySignature ~= "" then
        return queryState.querySignature
    end

    if type(mapID) == "number" and mapID > 0 then
        return tostring(mapID)
    end

    return nil
end

function eventFrame:HasRelevantWorldQuestQuerySignatureChange(mapID, queryState)
    if type(mapID) ~= "number" or mapID <= 0 then
        return false, nil, nil
    end

    if self._activeRelevantWorldQuestMapID ~= mapID then
        return false, nil, nil
    end

    local previousSignature = self._activeRelevantWorldQuestQuerySignature
    if not previousSignature then
        return false, nil, nil
    end

    local discoveredSignature = GetRelevantWorldQuestQuerySignature(
        queryState or self:BuildRelevantWorldQuestMapQueryState(mapID),
        mapID)

    if not discoveredSignature then
        return false, previousSignature, discoveredSignature
    end

    return discoveredSignature ~= previousSignature, previousSignature,
        discoveredSignature
end

function eventFrame:CancelWorldQuestDescendantGather()
    self._descendantGatherGeneration = (self._descendantGatherGeneration or 0) + 1
    self._descendantGatherSession = nil
end

function eventFrame:IsWorldQuestDescendantGatherPending(mapID)
    local session = self._descendantGatherSession
    return session ~= nil
        and session.mapID == mapID
        and session.isComplete ~= true
end

local function GetLockedAreaPOIOwnerScore(poiInfo, queryMapID)
    if not poiInfo then
        return 0
    end

    if poiInfo.isPrimaryMapForPOI == true then
        return 3
    end

    local linkedUiMapID = poiInfo.linkedUiMapID
    if linkedUiMapID and linkedUiMapID > 0 then
        if linkedUiMapID == queryMapID then
            return 2
        end
        return 0
    end

    return 1
end

local function ShouldPreferLockedAreaPOIEntry(currentEntry, candidateEntry)
    local currentScore = currentEntry and currentEntry.areaPOIOwnerScore or 0
    local candidateScore = candidateEntry and candidateEntry.areaPOIOwnerScore or 0
    if candidateScore ~= currentScore then
        return candidateScore > currentScore
    end

    local currentMapID = currentEntry and currentEntry.mapID or 0
    local candidateMapID = candidateEntry and candidateEntry.mapID or 0
    if currentMapID ~= candidateMapID then
        local currentIsParent = currentMapID ~= 0
            and candidateMapID ~= 0
            and GetDescendantMapSet(currentMapID)[candidateMapID]
        local candidateIsParent = currentMapID ~= 0
            and candidateMapID ~= 0
            and GetDescendantMapSet(candidateMapID)[currentMapID]
        if currentIsParent ~= candidateIsParent then
            return currentIsParent and true or false
        end
    end

    return candidateMapID > currentMapID
end

local function AppendLockedAreaPOIRawEntry(
    rawEntries,
    seen,
    mapsToQuery,
    queryMapID,
    rootMapID,
    poiID,
    poiInfo,
    allowOutsideHierarchy)
    -- Admit if the normal ownership checks pass, OR if the queryMapID is
    -- itself part of the query set (we are scanning a map we explicitly
    -- want; any locked area POI found there belongs here).
    if not eventFrame:ShouldAdmitLockedAreaPOIForQuery(
        mapsToQuery,
        poiInfo,
        allowOutsideHierarchy)
        and not mapsToQuery[queryMapID]
    then
        return
    end

    local syntheticID = -poiID
    local linkedUiMapID = poiInfo and poiInfo.linkedUiMapID or nil
    local ownerMapID = queryMapID
    if linkedUiMapID and linkedUiMapID > 0 and mapsToQuery[linkedUiMapID] then
        ownerMapID = linkedUiMapID
    elseif not mapsToQuery[ownerMapID]
        and allowOutsideHierarchy ~= true
        and rootMapID
        and mapsToQuery[rootMapID]
    then
        ownerMapID = rootMapID
    end

    if not mapsToQuery[ownerMapID] then
        return
    end

    local existingEntry = GetExistingQuestEntry(syntheticID)
    local qInfo = WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(ownerMapID)
    local candidateEntry = {
        questID = syntheticID,
        mapID = ownerMapID,
        mapName = qInfo and qInfo.name or "",
        rawQuestTagType = WQT_CAPSTONE,
        rawTagID = TAG_ID_SPECIAL_ASSIGNMENT,
        isMapIndicatorQuest = false,
        isLocked = true,
        isAreaPOI = true,
        poiID = poiID,
        poiQueryMapID = queryMapID,
        poiName = poiInfo and poiInfo.name or nil,
        poiDescription = poiInfo and poiInfo.description or nil,
        tooltipWidgetSet = poiInfo and poiInfo.tooltipWidgetSet or nil,
        areaPOIInfo = poiInfo,
        linkedUiMapID = linkedUiMapID,
        isPrimaryAreaPOI = poiInfo and poiInfo.isPrimaryMapForPOI == true or false,
        areaPOIOwnerScore = GetLockedAreaPOIOwnerScore(poiInfo, queryMapID),
        expiresAt = existingEntry and existingEntry.expiresAt or nil,
        _syntheticID = syntheticID,
    }

    local existingIndex = seen[syntheticID]
    if existingIndex then
        local currentEntry = rawEntries[existingIndex]
        if currentEntry and ShouldPreferLockedAreaPOIEntry(currentEntry, candidateEntry) then
            rawEntries[existingIndex] = candidateEntry
        end
        return
    end

    rawEntries[#rawEntries + 1] = candidateEntry
    seen[syntheticID] = #rawEntries
end

AppendRawQuestEntriesForMap = function(
    rawEntries,
    seen,
    mapsToQuery,
    queryMapID,
    rootMapID,
    allowOutsideHierarchy)
    local activeQuestProvider = WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestsOnMap
    if activeQuestProvider then
        local quests = activeQuestProvider(queryMapID)
        if quests then
            for _, q in ipairs(quests) do
                local qid = q.questID
                if qid and not seen[qid] then
                    local isMapIndicator = q.isMapIndicatorQuest == true
                        and not (WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive
                                 and WOW.C_TaskQuest.IsActive(qid))
                    local rawTagID = q["tagID"]
                    local isWorldQuest = not WOW.QuestUtils_IsQuestWorldQuest
                        or WOW.QuestUtils_IsQuestWorldQuest(qid)
                    local isSpecialAssignment = IsSpecialAssignmentFromMetadata(
                        q.questTagType,
                        rawTagID)
                    if isWorldQuest or isSpecialAssignment or isMapIndicator then
                        AppendRawQuestEntry(
                            rawEntries,
                            seen,
                            mapsToQuery,
                            q,
                            queryMapID,
                            rootMapID,
                            isMapIndicator,
                            false,
                            allowOutsideHierarchy)
                    end
                end
            end
        end
    end

    local mapQuests = eventFrame._WithQuestPOIMapContext(queryMapID, function()
        if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestsOnMap then
            return WOW.C_QuestLog.GetQuestsOnMap(queryMapID)
        end
        return nil
    end)

    if mapQuests then
        for _, q in ipairs(mapQuests) do
            if q.questID and not seen[q.questID]
                and IsProvisionalLockedSpecialAssignmentCandidate(
                    q,
                    queryMapID,
                    mapsToQuery,
                    allowOutsideHierarchy)
            then
                AppendRawQuestEntry(
                    rawEntries,
                    seen,
                    mapsToQuery,
                    q,
                    queryMapID,
                    rootMapID,
                    true,
                    not IsSpecialAssignmentFromMetadata(q.questTagType, q.tagID),
                    allowOutsideHierarchy)
            end
        end
    end

    local lockedQuestID = GetLockedSpecialAssignmentBountyQuestID(queryMapID)
    if lockedQuestID then
        AppendLockedQuestByID(
            rawEntries,
            seen,
            mapsToQuery,
            lockedQuestID,
            queryMapID,
                true,
                allowOutsideHierarchy)
    end

    if C_AreaPoiInfo and C_AreaPoiInfo.GetAreaPOIForMap and C_AreaPoiInfo.GetAreaPOIInfo then
        local poiIDs = C_AreaPoiInfo.GetAreaPOIForMap(queryMapID)
        if poiIDs then
            for _, poiID in ipairs(poiIDs) do
                local poiInfo = C_AreaPoiInfo.GetAreaPOIInfo(queryMapID, poiID)
                if eventFrame:IsRelevantLockedAreaPOIInfo(poiInfo) then
                    AppendLockedAreaPOIRawEntry(
                        rawEntries,
                        seen,
                        mapsToQuery,
                        queryMapID,
                        rootMapID,
                        poiID,
                        poiInfo,
                        allowOutsideHierarchy)
                end
            end
        end
    end
end

IsQuestEntryStillVisible = function(entry)
    local questID = entry and entry.questID or nil
    return entry
        and (entry.isAreaPOI == true
            or entry.isLocked == true
            or not (questID and questID > 0 and WOW.C_TaskQuest and WOW.C_TaskQuest.IsActive)
            or WOW.C_TaskQuest.IsActive(questID))
end

local function IsAreaPOIWidgetShown(widgetInfo)
    local shownState = widgetInfo and widgetInfo.shownState
    return shownState ~= false and shownState ~= 0
end

function eventFrame:GetWorldQuestDisplayMapInfo(mapID, mapsToQuery)
    if type(mapID) ~= "number" or mapID <= 0 then
        return mapID, ""
    end

    local displayMapID = mapID
    local displayMapInfo = WOW.C_Map and WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(displayMapID) or nil
    local mapTypeDungeon = Enum and Enum.UIMapType and Enum.UIMapType.Dungeon or 4
    local mapTypeMicro = Enum and Enum.UIMapType and Enum.UIMapType.Micro or 5

    -- Cave-style maps should display under their parent zone instead of as
    -- standalone zone groups.
    for _ = 1, 8 do
        if not displayMapInfo then
            break
        end

        local mapType = displayMapInfo.mapType
        if mapType ~= mapTypeDungeon and mapType ~= mapTypeMicro then
            break
        end

        local parentMapID = displayMapInfo.parentMapID
        if not parentMapID or parentMapID <= 0 then
            break
        end

        if mapsToQuery and not mapsToQuery[parentMapID] then
            break
        end

        displayMapID = parentMapID
        displayMapInfo = WOW.C_Map and WOW.C_Map.GetMapInfo and WOW.C_Map.GetMapInfo(displayMapID) or nil
    end

    return displayMapID, displayMapInfo and displayMapInfo.name or ""
end

BuildQuestEntriesFromRawEntries = function(rawEntries, mapsToQuery)
    local activePendingQuestIDs = {}
    local gatherTime = WOW.GetTime()

    for _, entry in ipairs(rawEntries) do
        local questID = entry.questID
        entry.isLocked = ResolveQuestLockedState(questID, entry)

        if IsQuestEntryStillVisible(entry) then
            activePendingQuestIDs[questID] = true

            if entry.isAreaPOI then
                pendingQuestIDs[questID] = nil
                -- Locked Area POI rows show rewards from C_QuestLog's bounty lock quest via
                -- GetQuestRewards(lockQuestID). That path is gated on HaveQuestRewardData; until
                -- it is true we fall back to widget-scraped rewards (often wrong right after /reload).
                -- Track the *lock quest id* like other reward-pending quests so preload completion
                -- runs UpdateQuestRow / refresh; include it in activePendingQuestIDs so prune keeps it.
                -- Pending until HaveQuestRewardData *and* C_Item cache for bounty item rewards (see
                -- ITEM_DATA_LOAD_RESULT vs QUEST_DATA_LOAD_RESULT timing after /reload).
                if entry.isLocked then
                    local bountyLockQuestID = ResolveBountyLockQuestIDFromQuestEntry(entry)
                    if bountyLockQuestID and WOW.HaveQuestRewardData then
                        local bountyRewardNotReady = not WOW.HaveQuestRewardData(bountyLockQuestID)
                            or not rewardPreloadState.AreQuestRewardItemClientCachesReady(
                                bountyLockQuestID)
                        if bountyRewardNotReady then
                            activePendingQuestIDs[bountyLockQuestID] = true
                            local bountyPending = pendingQuestIDs[bountyLockQuestID]
                            if bountyPending then
                                bountyPending.needsRewardData = true
                            else
                                pendingQuestIDs[bountyLockQuestID] = {
                                    needsQuestData = false,
                                    needsRewardData = true,
                                    questDataRefreshDone = true,
                                }
                            end
                            if not WOW.HaveQuestRewardData(bountyLockQuestID) then
                                rewardPreloadState.QueueQuestRewardPreload(bountyLockQuestID, false)
                            end
                        end
                    end
                end
            else
                local hasQuestData = eventFrame:IsQuestCoreDataReady(questID)
                if hasQuestData then
                    requestedQuestData[questID] = nil
                    questDataRetrySuppressedUntil[questID] = nil
                end

                local retrySuppressedUntil = questDataRetrySuppressedUntil[questID]
                if retrySuppressedUntil and retrySuppressedUntil <= gatherTime then
                    questDataRetrySuppressedUntil[questID] = nil
                    retrySuppressedUntil = nil
                end

                local needsQuestData = not hasQuestData
                local needsRewardData = (not entry.isLocked or not entry.isAreaPOI)
                    and WOW.HaveQuestRewardData and not WOW.HaveQuestRewardData(questID)
                if retrySuppressedUntil and needsRewardData then
                    needsRewardData = false
                end

                if needsQuestData then
                    local pendingState = pendingQuestIDs[questID]
                    if pendingState then
                        pendingState.needsQuestData = true
                        pendingState.questDataRefreshDone = false
                    else
                        pendingQuestIDs[questID] = {
                            needsQuestData = true,
                            needsRewardData = false,
                            questDataRefreshDone = false,
                        }
                    end

                    eventFrame:QueueQuestCoreDataLoad(questID)
                end

                if needsRewardData then
                    local pendingState = pendingQuestIDs[questID]
                    if pendingState then
                        pendingState.needsRewardData = true
                    else
                        pendingQuestIDs[questID] = {
                            needsQuestData = false,
                            needsRewardData = true,
                            questDataRefreshDone = true,
                        }
                    end
                elseif not WOW.HaveQuestRewardData or WOW.HaveQuestRewardData(questID) then
                    rewardPreloadState.requestedQuestIDs[questID] = nil
                    rewardPreloadState.queuedQuestIDs[questID] = nil
                    rewardPreloadState.recordedRequestTime[questID] = nil
                end

                if not needsQuestData and not needsRewardData then
                    pendingQuestIDs[questID] = nil
                end
            end
        else
            pendingQuestIDs[questID] = nil
            requestedQuestData[questID] = nil
            questDataRetrySuppressedUntil[questID] = nil
            rewardPreloadState.requestedQuestIDs[questID] = nil
            rewardPreloadState.queuedQuestIDs[questID] = nil
            rewardPreloadState.recordedRequestTime[questID] = nil
        end
    end

    PruneQuestRequestBookkeeping(activePendingQuestIDs)
    UpdateQuestLogUpdateRegistration()

    local result = {}
    local hiddenRewardPreloadQuestIDs
    local searchLower = filterSearch ~= "" and filterSearch:lower() or nil
    local serverTime = GetCurrentServerTime()

    for _, entry in ipairs(rawEntries) do
        if IsQuestEntryStillVisible(entry) then
            local questID = entry.questID
            local dataReady = entry.isAreaPOI or eventFrame:IsQuestCoreDataReady(questID)
            local rewardDataReady
            if entry.isAreaPOI and entry.isLocked then
                local bountyLockQuestID = ResolveBountyLockQuestIDFromQuestEntry(entry)
                rewardDataReady = not bountyLockQuestID
                    or rewardPreloadState.IsQuestRewardDisplayReady(bountyLockQuestID)
            elseif entry.isAreaPOI then
                rewardDataReady = true
            elseif entry.isLocked then
                rewardDataReady = true
            else
                rewardDataReady = rewardPreloadState.IsQuestRewardDisplayReady(questID)
            end
            local timeLeft, expiresAt
            if entry.isAreaPOI then
                if entry.poiID and C_AreaPoiInfo and C_AreaPoiInfo.IsAreaPOITimed
                    and C_AreaPoiInfo.GetAreaPOISecondsLeft
                    and C_AreaPoiInfo.IsAreaPOITimed(entry.poiID)
                then
                    timeLeft = C_AreaPoiInfo.GetAreaPOISecondsLeft(entry.poiID)
                    if timeLeft and timeLeft > 0 then
                        expiresAt = serverTime + timeLeft
                    elseif entry.expiresAt and entry.expiresAt > serverTime then
                        expiresAt = entry.expiresAt
                    end
                elseif entry.expiresAt and entry.expiresAt > serverTime then
                    expiresAt = entry.expiresAt
                end
            else
                timeLeft, expiresAt = GetQuestExpirySnapshot(questID, serverTime, entry)
            end

            local displayMapID, displayMapName = eventFrame:GetWorldQuestDisplayMapInfo(
                entry.mapID,
                mapsToQuery)
            if not displayMapName or displayMapName == "" then
                displayMapName = entry.mapName
            end

            local title = entry.isAreaPOI and entry.poiName or GetQuestTitle(questID)
            local faction = not entry.isAreaPOI and GetQuestFactionLabel(questID) or nil
            local questType = entry.isAreaPOI and "special_assignment" or GetQuestType(
                questID,
                entry.rawQuestTagType,
                entry.rawTagID)
            if not entry.isAreaPOI then
                expiresAt = GetPersistedLockedSpecialAssignmentExpiresAt(
                    questID,
                    entry.isLocked,
                    entry.rawQuestTagType,
                    entry.rawTagID,
                    questType,
                    timeLeft,
                    expiresAt)
            end
            timeLeft = GetRemainingTimeLeft(timeLeft, expiresAt, serverTime)
            local expID = not entry.isAreaPOI and GetQuestExpansionID(questID) or nil
            local expLabel = expID and EXPANSION_LABELS[expID] or nil

            local excluded = false
            local excludedBySearch = false
            if entry.isLocked then
                if entry.isAreaPOI then
                    excluded = false
                elseif entry.isMapIndicatorQuest then
                    excluded = false
                elseif entry.bountyLockedCandidate == true
                    or entry.provisionalLockedCandidate == true
                then
                    local isSpecialAssignment, isConfirmed = IsSpecialAssignmentQuest(
                        questID,
                        entry.rawQuestTagType,
                        entry.rawTagID,
                        questType,
                        true)
                    excluded = isConfirmed and not isSpecialAssignment
                else
                    excluded = not IsSpecialAssignmentQuest(
                        questID,
                        entry.rawQuestTagType,
                        entry.rawTagID,
                        questType)
                end
            end

            local unlockText = nil
            local rewardText = nil
            local areaPOIRewards = {}
            local areaPOITimeText = nil
            local primReward = nil
            if not excluded then
                if entry.isLocked then
                    if entry.isAreaPOI and entry.tooltipWidgetSet and C_UIWidgetManager then
                        local widgets = C_UIWidgetManager.GetAllWidgetsBySetID
                            and C_UIWidgetManager.GetAllWidgetsBySetID(entry.tooltipWidgetSet)
                        if widgets and #widgets > 0 then
                            local ordered = {}
                            for _, widgetInfo in ipairs(widgets) do
                                ordered[#ordered + 1] = widgetInfo
                            end
                            WOW.table_sort(ordered, function(leftWidget, rightWidget)
                                local leftOrder = leftWidget.orderIndex or 0
                                local rightOrder = rightWidget.orderIndex or 0
                                if leftOrder ~= rightOrder then
                                    return leftOrder < rightOrder
                                end
                                return (leftWidget.widgetID or 0) < (rightWidget.widgetID or 0)
                            end)

                            local sawRewardHeader = false
                            local selectedRewardWidget = false
                            for _, widgetInfo in ipairs(ordered) do
                                if IsAreaPOIWidgetShown(widgetInfo) then
                                    local text = nil
                                    local rewardWidgetExpressed = false
                                    local widgetRewards = nil
                                    local widgetRewardParts = nil
                                    if widgetInfo.widgetType == 2
                                        and C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo(widgetInfo.widgetID)
                                        if info then
                                            if not areaPOITimeText then
                                                areaPOITimeText = eventFrame:GetAreaPOITimeTextFromStatusBarWidgetInfo(info)
                                            end
                                            if not areaPOITimeText and not info.hasTimer then
                                                if info.overrideBarText and info.overrideBarText ~= "" then
                                                    text = NormalizeInlineQuestText(info.overrideBarText)
                                                end
                                                if not text and info.text and info.text ~= "" then
                                                    text = NormalizeInlineQuestText(info.text)
                                                end
                                            end
                                        end
                                    elseif widgetInfo.widgetType == 8
                                        and C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo(widgetInfo.widgetID)
                                        if info and info.text and info.text ~= "" then
                                            text = NormalizeInlineQuestText(info.text)
                                            if not areaPOITimeText and eventFrame:IsAreaPOITimeText(text) then
                                                areaPOITimeText = text
                                                text = nil
                                            end
                                        end
                                    elseif widgetInfo.widgetType == 7
                                        and C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo(widgetInfo.widgetID)
                                        if info then
                                            if info.text and info.text ~= "" then
                                                text = NormalizeInlineQuestText(info.text)
                                            end
                                            if info.currencies then
                                                for _, currency in ipairs(info.currencies) do
                                                    local currencyID = currency["currencyID"] or nil
                                                    local currencyText = NormalizeInlineQuestText(
                                                        currency.text or currency.leadingText or info.text)
                                                    if currency.iconFileID or currencyText or currencyID then
                                                        widgetRewards = widgetRewards or {}
                                                        widgetRewards[#widgetRewards + 1] = {
                                                            rewardType = "currency",
                                                            icon = currency.iconFileID,
                                                            label = currencyText,
                                                            amount = nil,
                                                            currencyID = currencyID,
                                                        }
                                                        rewardWidgetExpressed = true
                                                    end
                                                end
                                            end
                                        end
                                    elseif widgetInfo.widgetType == 9
                                        and C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo(widgetInfo.widgetID)
                                        if info and info.currencies then
                                            local infoText = info["text"]
                                            for _, currency in ipairs(info.currencies) do
                                                local currencyID = currency["currencyID"] or nil
                                                local currencyText = NormalizeInlineQuestText(
                                                    currency.text or currency.leadingText or infoText)
                                                if currency.iconFileID or currencyText or currencyID then
                                                    widgetRewards = widgetRewards or {}
                                                    widgetRewards[#widgetRewards + 1] = {
                                                        rewardType = "currency",
                                                        icon = currency.iconFileID,
                                                        label = currencyText,
                                                        amount = nil,
                                                        currencyID = currencyID,
                                                    }
                                                    rewardWidgetExpressed = true
                                                end
                                            end
                                        end
                                    elseif widgetInfo.widgetType == 13
                                        and C_UIWidgetManager.GetSpellDisplayVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetSpellDisplayVisualizationInfo(widgetInfo.widgetID)
                                        if info and info.spellInfo and info.spellInfo.text and info.spellInfo.text ~= "" then
                                            text = NormalizeInlineQuestText(info.spellInfo.text)
                                        end
                                    elseif widgetInfo.widgetType == 27
                                        and C_UIWidgetManager.GetItemDisplayVisualizationInfo
                                    then
                                        local info = C_UIWidgetManager.GetItemDisplayVisualizationInfo(widgetInfo.widgetID)
                                        if info and info.itemInfo then
                                            local itemID = info.itemInfo.itemID or nil
                                            if info.itemInfo.overrideItemName and info.itemInfo.overrideItemName ~= "" then
                                                text = NormalizeInlineQuestText(info.itemInfo.overrideItemName)
                                            end
                                            if not text and itemID and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                                                text = NormalizeInlineQuestText(WOW.C_Item.GetItemNameByID(itemID))
                                            end
                                            if not text and info.itemInfo.infoText and info.itemInfo.infoText ~= "" then
                                                text = NormalizeInlineQuestText(info.itemInfo.infoText)
                                            end
                                            local icon = WOW.C_Item and WOW.C_Item.GetItemIconByID and itemID
                                                and WOW.C_Item.GetItemIconByID(itemID) or nil
                                            if itemID or icon or text then
                                                local iq = 0
                                                if itemID and C_Item and C_Item.GetItemQualityByID then
                                                    local qq = C_Item.GetItemQualityByID(itemID)
                                                    iq = type(qq) == "number" and qq or 0
                                                end
                                                widgetRewards = widgetRewards or {}
                                                widgetRewards[#widgetRewards + 1] = {
                                                    rewardType = "item",
                                                    itemID = itemID,
                                                    icon = icon,
                                                    label = text,
                                                    amount = info.itemInfo.stackCount or nil,
                                                    quality = iq,
                                                }
                                                rewardWidgetExpressed = true
                                            end
                                        end
                                    end

                                    if text and text ~= "" then
                                        local lowerText = text:lower()
                                        local headerText = WOW.string_gsub(lowerText, "[:ï¼š]+$", "")
                                        local rewardsLabel = REWARDS and REWARDS:lower() or "rewards"
                                        local rewardLabel = REWARD and REWARD:lower() or "reward"
                                        if headerText == rewardsLabel or headerText == rewardLabel then
                                            sawRewardHeader = true
                                        elseif widgetInfo.widgetType == 2 or widgetInfo.widgetType == 8 then
                                            if not unlockText then
                                                unlockText = text
                                            end
                                        elseif (widgetInfo.widgetType == 7
                                                or widgetInfo.widgetType == 9
                                                or widgetInfo.widgetType == 27)
                                            and not rewardWidgetExpressed
                                        then
                                            widgetRewardParts = widgetRewardParts or {}
                                            widgetRewardParts[#widgetRewardParts + 1] = text
                                        elseif widgetInfo.widgetType == 13 and sawRewardHeader then
                                            widgetRewardParts = widgetRewardParts or {}
                                            widgetRewardParts[#widgetRewardParts + 1] = text
                                        end
                                    end

                                    if rewardWidgetExpressed and widgetRewards then
                                        for _, rewardData in ipairs(widgetRewards) do
                                            areaPOIRewards[#areaPOIRewards + 1] = rewardData
                                        end
                                    end

                                    local hasRewardContent = rewardWidgetExpressed
                                        or (widgetRewardParts and #widgetRewardParts > 0)
                                    if hasRewardContent and not selectedRewardWidget then
                                        selectedRewardWidget = true
                                        if widgetRewardParts and #widgetRewardParts > 0 then
                                            rewardText = table.concat(widgetRewardParts, "  ")
                                        end
                                    end
                                end
                            end
                        end
                    end

                    if #areaPOIRewards > 0 then
                        areaPOIRewards = CollapseLockedAreaPOIParallelItemRewards(areaPOIRewards)
                    end

                    if not unlockText and entry.isAreaPOI and entry.poiDescription and entry.poiDescription ~= "" then
                        unlockText = NormalizeInlineQuestText(entry.poiDescription)
                    end
                    if not unlockText then
                        if entry.isAreaPOI then
                            unlockText = "Unlock requirements not available yet."
                        else
                            unlockText = GetLockedSpecialAssignmentUnlockText(questID)
                        end
                    end
                end

                local rawRewards = rewardDataReady
                    and (not entry.isLocked or not entry.isAreaPOI)
                    and GetQuestRewards(questID) or {}
                primReward = (#rawRewards > 0) and GetPrimaryRewardType(rawRewards) or nil
            end

            if next(filterTypes) and questType and filterTypes[questType] then
                excluded = true
            end
            if not excluded and next(filterRewards) and primReward and filterRewards[primReward] then
                excluded = true
            end
            if not excluded and searchLower then
                local matched =
                    (title and title:lower():find(searchLower, 1, true))
                    or (faction and faction:lower():find(searchLower, 1, true))
                    or (unlockText and unlockText:lower():find(searchLower, 1, true))
                    or (displayMapName and displayMapName:lower():find(searchLower, 1, true))
                    or (questType and QUEST_TYPE_LABEL[questType]
                        and QUEST_TYPE_LABEL[questType]:lower():find(searchLower, 1, true))
                    or (primReward and REWARD_TYPE_LABEL[primReward]
                        and REWARD_TYPE_LABEL[primReward]:lower():find(searchLower, 1, true))
                if not matched then
                    excluded = true
                    excludedBySearch = true
                end
            end

            local pendingState = pendingQuestIDs[questID]
            local needsHiddenRewardPreload = pendingState
                and pendingState.needsRewardData
                and excludedBySearch
                and dataReady
                and not entry.isLocked
                and entry.isAreaPOI ~= true
            if pendingState then
                pendingState.searchRefreshOnRewardReady = needsHiddenRewardPreload and true or nil
            end
            if needsHiddenRewardPreload then
                hiddenRewardPreloadQuestIDs = hiddenRewardPreloadQuestIDs or {}
                hiddenRewardPreloadQuestIDs[#hiddenRewardPreloadQuestIDs + 1] = questID
            end

            if not excluded then
                local timeText
                if entry.isLocked and entry.isAreaPOI then
                    timeText = FormatTimeLeft(nil)
                    timeLeft = nil
                    expiresAt = nil
                else
                    if entry.isAreaPOI and entry.poiID then
                        local liveSec = TryGetAreaPOISecondsLeft(entry.poiID)
                        if liveSec and liveSec > 0 then
                            timeLeft = liveSec
                            expiresAt = serverTime + liveSec
                        end
                    end
                    if timeLeft and timeLeft > 0 then
                        timeText = FormatTimeLeft(timeLeft)
                    elseif expiresAt and expiresAt > serverTime then
                        timeText = FormatTimeLeft(expiresAt - serverTime)
                    elseif entry.isAreaPOI and areaPOITimeText then
                        timeText = areaPOITimeText
                    else
                        timeText = FormatTimeLeft(timeLeft)
                    end
                end

                result[#result + 1] = {
                    questID = questID,
                    mapID = displayMapID,
                    mapName = displayMapName,
                    title = title,
                    faction = faction,
                    timeLeft = timeLeft,
                    timeText = timeText,
                    expiresAt = expiresAt,
                    questType = questType,
                    expID = expID,
                    expLabel = expLabel,
                    rawTimeLeftSeconds = entry.rawTimeLeftSeconds,
                    rawTimeLeftSecondsConsumed = entry.rawTimeLeftSecondsConsumed == true,
                    rawQuestTagType = entry.rawQuestTagType,
                    rawTagID = entry.rawTagID,
                    isMapIndicatorQuest = entry.isMapIndicatorQuest,
                    bountyLockedCandidate = entry.bountyLockedCandidate == true,
                    isAreaPOI = entry.isAreaPOI == true,
                    areaPOITimeText = areaPOITimeText,
                    areaPOIRewards = areaPOIRewards,
                    poiID = entry.poiID,
                    poiQueryMapID = entry.poiQueryMapID,
                    tooltipWidgetSet = entry.tooltipWidgetSet,
                    poiDescription = entry.poiDescription,
                    areaPOIInfo = entry.areaPOIInfo,
                    linkedUiMapID = entry.linkedUiMapID,
                    isPrimaryAreaPOI = entry.isPrimaryAreaPOI == true,
                    areaPOIOwnerScore = entry.areaPOIOwnerScore,
                    isLocked = entry.isLocked,
                    unlockText = unlockText,
                    rewardText = rewardText,
                    primReward = primReward,
                    rewardDataReady = rewardDataReady,
                    dataReady = dataReady,
                }
            end
        end
    end

    do
        local function GetRelatedSpecialAssignmentMapState(leftMapID, rightMapID)
            if not leftMapID or not rightMapID then
                return false, false, false
            end

            local leftIsParent = leftMapID ~= rightMapID
                and GetDescendantMapSet(leftMapID)[rightMapID]
            local rightIsParent = leftMapID ~= rightMapID
                and GetDescendantMapSet(rightMapID)[leftMapID]
            local sharesMapGroup = false

            if leftMapID ~= rightMapID
                and not leftIsParent
                and not rightIsParent
                and WOW.C_Map
                and WOW.C_Map.GetMapGroupID
                and WOW.C_Map.GetMapGroupMembersInfo
            then
                local groupID = WOW.C_Map.GetMapGroupID(leftMapID)
                if groupID then
                    local members = WOW.C_Map.GetMapGroupMembersInfo(groupID)
                    if members then
                        for index = 1, #members do
                            local memberID = members[index] and members[index].mapID
                            if memberID == rightMapID then
                                sharesMapGroup = true
                                break
                            end
                        end
                    end
                end
            end

            return leftMapID == rightMapID or leftIsParent or rightIsParent or sharesMapGroup,
                leftIsParent,
                rightIsParent,
                sharesMapGroup
        end

        local function GetLockedSpecialAssignmentSiblingDropIndex(
            leftEntry,
            rightEntry,
            leftIndex,
            rightIndex)
            local leftQuestBacked = leftEntry.questID and leftEntry.questID > 0
            local rightQuestBacked = rightEntry.questID and rightEntry.questID > 0
            if leftQuestBacked ~= rightQuestBacked then
                return leftQuestBacked and rightIndex or leftIndex
            end

            local leftAreaPOI = leftEntry.isAreaPOI == true
            local rightAreaPOI = rightEntry.isAreaPOI == true
            if leftAreaPOI ~= rightAreaPOI then
                return leftAreaPOI and leftIndex or rightIndex
            end

            local leftOwnerScore = leftEntry.areaPOIOwnerScore or 0
            local rightOwnerScore = rightEntry.areaPOIOwnerScore or 0
            if leftOwnerScore ~= rightOwnerScore then
                return leftOwnerScore < rightOwnerScore and leftIndex or rightIndex
            end

            local leftStableID = leftAreaPOI and (leftEntry.poiID or 0) or (leftEntry.questID or 0)
            local rightStableID = rightAreaPOI and (rightEntry.poiID or 0) or (rightEntry.questID or 0)
            if leftStableID ~= rightStableID then
                return leftStableID < rightStableID and rightIndex or leftIndex
            end

            local leftMapID = leftEntry.mapID or 0
            local rightMapID = rightEntry.mapID or 0
            if leftMapID ~= rightMapID then
                return leftMapID < rightMapID and rightIndex or leftIndex
            end

            return rightIndex
        end

        local function GetSpecialAssignmentSourcePriority(entry)
            if not entry then
                return 0
            end

            if entry.isLocked ~= true then
                if entry.questID and entry.questID > 0 then
                    return 4
                end
                return 0
            end

            if entry.isAreaPOI then
                local isPrimaryAreaPOI = entry.isPrimaryAreaPOI == true
                if not isPrimaryAreaPOI then
                    local linkedUiMapID = entry.linkedUiMapID
                    if linkedUiMapID and linkedUiMapID > 0 and linkedUiMapID == entry.mapID then
                        isPrimaryAreaPOI = true
                    end
                end
                if isPrimaryAreaPOI then
                    return 3
                end
                -- Prefer locked area POI rows over provisional locked quest
                -- placeholders; POI rows are the canonical source for these.
                return 3
            end

            if entry.questID and entry.questID > 0 then
                return 2
            end

            return 0
        end

        local function GetPendingTitleSpecialAssignmentAreaPOIDropIndex(
            leftEntry,
            rightEntry,
            leftIndex,
            rightIndex)
            local leftTitlePending = not leftEntry.title or leftEntry.title == ""
            local rightTitlePending = not rightEntry.title or rightEntry.title == ""
            if leftTitlePending == rightTitlePending then
                return nil
            end

            local pendingEntry = leftTitlePending and leftEntry or rightEntry
            local otherEntry = leftTitlePending and rightEntry or leftEntry
            if pendingEntry.questID and pendingEntry.questID > 0
                and pendingEntry.isAreaPOI ~= true
                and pendingEntry.dataReady == false
                and otherEntry.isAreaPOI and otherEntry.isLocked
            then
                -- Drop the pending quest placeholder and keep the locked area
                -- POI row so special assignments remain visible.
                return leftTitlePending and leftIndex or rightIndex
            end

            return nil
        end

        local dropIndexes = nil
        for i = 1, #result do
            local leftEntry = result[i]
            if leftEntry and IsSpecialAssignmentFromMetadata(
                    leftEntry.rawQuestTagType,
                    leftEntry.rawTagID,
                    leftEntry.questType)
            then
                for j = i + 1, #result do
                    local rightEntry = result[j]
                    if rightEntry and IsSpecialAssignmentFromMetadata(
                            rightEntry.rawQuestTagType,
                            rightEntry.rawTagID,
                            rightEntry.questType)
                    then
                        local mapsRelated, leftIsParent, rightIsParent, sharesMapGroup =
                            GetRelatedSpecialAssignmentMapState(
                                leftEntry.mapID,
                                rightEntry.mapID)
                        if mapsRelated then
                            local dropIndex = nil
                            local leftTitleKnown = leftEntry.title and leftEntry.title ~= ""
                            local rightTitleKnown = rightEntry.title and rightEntry.title ~= ""

                            if leftTitleKnown and rightTitleKnown then
                                if WOW.string_lower(rightEntry.title) == WOW.string_lower(leftEntry.title) then
                                    local leftPriority = GetSpecialAssignmentSourcePriority(leftEntry)
                                    local rightPriority = GetSpecialAssignmentSourcePriority(rightEntry)
                                    if leftPriority ~= rightPriority then
                                        dropIndex = leftPriority < rightPriority and i or j
                                    elseif leftEntry.isAreaPOI and rightEntry.isAreaPOI then
                                        local leftOwnerScore = leftEntry.areaPOIOwnerScore or 0
                                        local rightOwnerScore = rightEntry.areaPOIOwnerScore or 0
                                        if leftOwnerScore ~= rightOwnerScore then
                                            dropIndex = leftOwnerScore < rightOwnerScore and i or j
                                        elseif leftIsParent then
                                            dropIndex = i
                                        elseif rightIsParent then
                                            dropIndex = j
                                        end
                                    elseif leftIsParent then
                                        dropIndex = i
                                    elseif rightIsParent then
                                        dropIndex = j
                                    end

                                    if not dropIndex
                                        and sharesMapGroup
                                        and leftEntry.isLocked
                                        and rightEntry.isLocked
                                    then
                                        dropIndex = GetLockedSpecialAssignmentSiblingDropIndex(
                                            leftEntry,
                                            rightEntry,
                                            i,
                                            j)
                                    end
                                end
                            else
                                dropIndex = GetPendingTitleSpecialAssignmentAreaPOIDropIndex(
                                    leftEntry,
                                    rightEntry,
                                    i,
                                    j)
                            end

                            if dropIndex then
                                dropIndexes = dropIndexes or {}
                                dropIndexes[dropIndex] = true
                            end
                        end
                    end
                end
            end
        end

        if dropIndexes then
            local dedupedResult = {}
            for i = 1, #result do
                if not dropIndexes[i] then
                    dedupedResult[#dedupedResult + 1] = result[i]
                end
            end
            result = dedupedResult
        end
    end

    if #result > 1 then
        WOW.table_sort(result, CompareQuestEntries)
    end

    return result, hiddenRewardPreloadQuestIDs
end

end

do

local function ShouldStageDescendantGather(queryState)
    local queryMapIDs = queryState and queryState.queryMapIDs or nil
    return queryMapIDs and #queryMapIDs > 8 or false
end

local function BuildRawQuestMembershipSignature(rawEntries)
    local signatureParts = eventFrame._rawQuestMembershipSignatureParts or {}
    local partCount = 0

    eventFrame._rawQuestMembershipSignatureParts = signatureParts

    if rawEntries then
        for _, entry in ipairs(rawEntries) do
            if not entry.isAreaPOI then
                partCount = partCount + 1
                signatureParts[partCount] = WOW.string_format(
                    "%s:%s:%s:%s:%s:%s:%s",
                    tostring(entry.questID or 0),
                    tostring(entry.mapID or 0),
                    entry.isLocked == true and "1" or "0",
                    entry.isMapIndicatorQuest == true and "1" or "0",
                    entry.bountyLockedCandidate == true and "1" or "0",
                    tostring(entry.rawQuestTagType or 0),
                    tostring(entry.rawTagID or 0))
            end
        end
    end

    for index = partCount + 1, #signatureParts do
        signatureParts[index] = nil
    end

    if partCount == 0 then
        return ""
    end

    WOW.table_sort(signatureParts)
    return table.concat(signatureParts, "\031", 1, partCount)
end

local function BuildLiveDescendantGatherQuestMembershipSignature(mapID, queryState)
    local mapsToQuery = queryState and queryState.mapsToQuery or { [mapID] = true }
    local queryMapIDs = queryState and queryState.queryMapIDs or { mapID }
    local allowOutsideHierarchy = queryState and queryState.allowOutsideHierarchy
        or eventFrame:IsOverviewOrClusterMapQuery(mapID)
    local rawEntries = eventFrame._liveDescendantGatherMembershipRawEntries or {}
    local seen = eventFrame._liveDescendantGatherMembershipSeen or {}

    eventFrame._liveDescendantGatherMembershipRawEntries = rawEntries
    eventFrame._liveDescendantGatherMembershipSeen = seen

    wipe(rawEntries)
    wipe(seen)

    for index = 1, #queryMapIDs do
        AppendRawQuestEntriesForMap(
            rawEntries,
            seen,
            mapsToQuery,
            queryMapIDs[index],
            mapID,
            allowOutsideHierarchy)
    end

    return BuildRawQuestMembershipSignature(rawEntries)
end

local function FinalizeStagedDescendantGatherSession(session)
    session.rawQuestMembershipSignature = BuildRawQuestMembershipSignature(session.rawEntries)
    session.isComplete = true
end

local function CreateStagedDescendantGatherSession(mapID, animateRows, queryState)
    local mapsToQuery = queryState and queryState.mapsToQuery or { [mapID] = true }
    local queryMapIDs = queryState and queryState.queryMapIDs or { mapID }
    local querySignature = GetRelevantWorldQuestQuerySignature(queryState, mapID)
    local allowOutsideHierarchy = queryState and queryState.allowOutsideHierarchy
        or eventFrame:IsOverviewOrClusterMapQuery(mapID)

    eventFrame._descendantGatherGeneration = (eventFrame._descendantGatherGeneration or 0) + 1
    local myGeneration = eventFrame._descendantGatherGeneration
    local session = {
        mapID = mapID,
        mapsToQuery = mapsToQuery,
        queryMapIDs = queryMapIDs,
        querySignature = querySignature,
        allowOutsideHierarchy = allowOutsideHierarchy,
        rawEntries = {},
        seen = {},
        nextIndex = 1,
        isComplete = false,
        finalAnimateRows = animateRows ~= false,
    }
    eventFrame._descendantGatherSession = session

    local function finalize()
        FinalizeStagedDescendantGatherSession(session)
        if ns.IsWorldQuestsRefreshContextActive()
            and WOW.WorldMapFrame and WOW.WorldMapFrame.mapID == session.mapID
        then
            ScheduleRefresh(session.finalAnimateRows, "Staged descendant gather complete", true)
        end
    end

    local function processNext()
        if eventFrame._descendantGatherGeneration ~= myGeneration or session.isComplete then
            return
        end
        if not ns.IsWorldQuestsRefreshContextActive()
            or not WOW.WorldMapFrame
            or WOW.WorldMapFrame.mapID ~= session.mapID
        then
            return
        end

        local debugprofilestop = debugprofilestop
        local budgetStart = debugprofilestop()
        local BATCH_TIME_BUDGET_MS = 4

        while session.nextIndex <= #session.queryMapIDs do
            local queryMapID = session.queryMapIDs[session.nextIndex]
            if not queryMapID then
                break
            end

            AppendRawQuestEntriesForMap(
                session.rawEntries,
                session.seen,
                session.mapsToQuery,
                queryMapID,
                session.mapID,
                session.allowOutsideHierarchy)
            session.nextIndex = session.nextIndex + 1

            if debugprofilestop() - budgetStart > BATCH_TIME_BUDGET_MS * 1000 then
                break
            end
        end

        if session.nextIndex > #session.queryMapIDs then
            finalize()
            return
        end

        WOW.C_Timer.After(0.01, processNext)
    end

    if session.queryMapIDs[1] then
        local debugprofilestop = debugprofilestop
        local budgetStart = debugprofilestop()
        local INITIAL_TIME_BUDGET_MS = 4

        while session.nextIndex <= #session.queryMapIDs do
            local queryMapID = session.queryMapIDs[session.nextIndex]
            if not queryMapID then
                break
            end

            AppendRawQuestEntriesForMap(
                session.rawEntries,
                session.seen,
                session.mapsToQuery,
                queryMapID,
                session.mapID,
                session.allowOutsideHierarchy)
            session.nextIndex = session.nextIndex + 1

            if debugprofilestop() - budgetStart > INITIAL_TIME_BUDGET_MS * 1000 then
                break
            end
        end
    else
        FinalizeStagedDescendantGatherSession(session)
    end

    if not session.isComplete and session.nextIndex > #session.queryMapIDs then
        FinalizeStagedDescendantGatherSession(session)
    end

    if not session.isComplete then
        WOW.C_Timer.After(0.01, processNext)
    end

    return session
end

local function GetStagedDescendantGatherSession(mapID, animateRows, queryState)
    local querySignature = GetRelevantWorldQuestQuerySignature(queryState, mapID)
    local session = eventFrame._descendantGatherSession
    if session and session.mapID == mapID and session.querySignature == querySignature then
        if animateRows == true then
            session.finalAnimateRows = true
        end
        return session
    end

    return CreateStagedDescendantGatherSession(mapID, animateRows, queryState)
end

ns.HasCompletedStagedDescendantGatherQuestMembershipChange = function(mapID, queryState)
    local session = eventFrame._descendantGatherSession
    if not session or session.mapID ~= mapID or session.isComplete ~= true then
        return false
    end

    local querySignature = GetRelevantWorldQuestQuerySignature(queryState, mapID)
    if session.querySignature ~= querySignature then
        return false
    end

    local previousMembershipSignature = session.rawQuestMembershipSignature
        or BuildRawQuestMembershipSignature(session.rawEntries)
    local liveMembershipSignature = BuildLiveDescendantGatherQuestMembershipSignature(mapID, queryState)

    session.rawQuestMembershipSignature = previousMembershipSignature

    return liveMembershipSignature ~= previousMembershipSignature
end

-- Session fast path reuses enriched rows and only refreshes time + normal quest rewards.
-- Locked Area POI rows embed widget-scraped rewards; that scrape can be stale until Blizzard
-- finishes hydrating widget payloads. Skipping the fast path forces a full
-- BuildQuestEntriesFromRawEntries pass so reward fields match current APIs/widgets.
function eventFrame:SessionEnrichedCacheContainsLockedAreaPOI()
    local cache = self._sessionEnrichedEntries
    if not cache then
        return false
    end
    for index = 1, #cache do
        local entry = cache[index]
        if entry and entry.isLocked and entry.isAreaPOI then
            return true
        end
    end
    return false
end

function eventFrame:GatherQuestsForCurrentMap(animateRows)
    if not WOW.WorldMapFrame then return {}, nil, nil end
    local mapID = WOW.WorldMapFrame.mapID
    if not mapID then return {}, nil, nil end
    if not WOW.C_TaskQuest or not WOW.C_TaskQuest.GetQuestsOnMap then
        return {}, nil, nil
    end

    -- Skip scanning if the current map is excluded by user preference
    if eventFrame._excludedMaps[mapID] then
        return {}, nil, nil
    end

    if self._sessionGatherMapID ~= mapID then
        -- Session gather caches are map-scoped; reusing entries across map
        -- changes can produce false empty lists when entry ownership differs
        -- between maps (e.g. locked special-assignment states).
        self:CancelWorldQuestDescendantGather()
        self._sessionGatherMapID = mapID
        wipe(self._sessionScannedQueryMapIDs)
        wipe(self._sessionRawEntries)
        wipe(self._sessionRawEntriesSeen)
        wipe(self._sessionEnrichedEntries)
        wipe(self._sessionEnrichedEntriesSeen)
    end

    local queryStateStart = debugprofilestop()
    local queryState = self:BuildRelevantWorldQuestMapQueryState(mapID)
    local queryStateElapsed = debugprofilestop() - queryStateStart
    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: BuildQueryState mapID=%s elapsed=%.2fms cached=%s",
            tostring(mapID), queryStateElapsed / 1000,
            tostring(self._sessionQueryStateCache[mapID] == queryState and queryStateElapsed < 100)))
    end
    local querySignature = GetRelevantWorldQuestQuerySignature(queryState, mapID)
    local mapsToQuery = queryState and queryState.mapsToQuery or { [mapID] = true }
    local queryMapIDs = queryState and queryState.queryMapIDs or { mapID }
    local allowOutsideHierarchy = queryState and queryState.allowOutsideHierarchy
        or self:IsOverviewOrClusterMapQuery(mapID)

    -- â”€â”€ Session cache: check which query maps still need scanning â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local newQueryMapIDs
    local allMapsCached = true
    for queryIndex = 1, #queryMapIDs do
        local queryMapID = queryMapIDs[queryIndex]
        if not self._sessionScannedQueryMapIDs[queryMapID] then
            allMapsCached = false
            if not newQueryMapIDs then
                newQueryMapIDs = {}
            end
            newQueryMapIDs[#newQueryMapIDs + 1] = queryMapID
        end
    end

    if allMapsCached and next(self._sessionEnrichedEntries)
        and not self:SessionEnrichedCacheContainsLockedAreaPOI()
    then
        -- â”€â”€ Fast path: all maps already scanned â€” filter cached entries â”€â”€â”€â”€â”€â”€
        self:CancelWorldQuestDescendantGather()

        local cached = self._sessionEnrichedEntries
        local filtered = {}
        local hiddenRewardPreloadQuestIDs
        local searchLower = filterSearch ~= "" and filterSearch:lower() or nil

        for cacheIndex = 1, #cached do
            local entry = cached[cacheIndex]
            if mapsToQuery[entry.mapID] and IsQuestEntryStillVisible(entry) then
                -- Refresh time-sensitive fields
                local serverTime = GetCurrentServerTime()
                if entry.isAreaPOI then
                    if entry.poiID and C_AreaPoiInfo
                        and C_AreaPoiInfo.IsAreaPOITimed
                        and C_AreaPoiInfo.GetAreaPOISecondsLeft
                        and C_AreaPoiInfo.IsAreaPOITimed(entry.poiID)
                    then
                        local remaining = C_AreaPoiInfo.GetAreaPOISecondsLeft(entry.poiID)
                        if remaining and remaining > 0 then
                            entry.timeLeft = remaining
                            entry.expiresAt = serverTime + remaining
                        end
                    end
                else
                    local timeLeft, expiresAt = GetQuestExpirySnapshot(
                        entry.questID, serverTime, entry)
                    entry.timeLeft = timeLeft
                    entry.expiresAt = expiresAt
                end
                entry.timeLeft = GetRemainingTimeLeft(
                    entry.timeLeft, entry.expiresAt, serverTime)
                if entry.isLocked and entry.isAreaPOI then
                    entry.timeText = FormatTimeLeft(nil)
                    entry.timeLeft = nil
                    entry.expiresAt = nil
                elseif entry.isAreaPOI and entry.poiID then
                    local liveSec = TryGetAreaPOISecondsLeft(entry.poiID)
                    if liveSec and liveSec > 0 then
                        entry.timeLeft = liveSec
                        entry.expiresAt = serverTime + liveSec
                    end
                    if entry.timeLeft and entry.timeLeft > 0 then
                        entry.timeText = FormatTimeLeft(entry.timeLeft)
                    elseif entry.expiresAt and entry.expiresAt > serverTime then
                        entry.timeText = FormatTimeLeft(entry.expiresAt - serverTime)
                    elseif entry.areaPOITimeText then
                        entry.timeText = entry.areaPOITimeText
                    else
                        entry.timeText = FormatTimeLeft(entry.timeLeft)
                    end
                else
                    if entry.timeLeft and entry.timeLeft > 0 then
                        entry.timeText = FormatTimeLeft(entry.timeLeft)
                    elseif entry.expiresAt and entry.expiresAt > serverTime then
                        entry.timeText = FormatTimeLeft(entry.expiresAt - serverTime)
                    elseif entry.areaPOITimeText then
                        entry.timeText = entry.areaPOITimeText
                    else
                        entry.timeText = FormatTimeLeft(entry.timeLeft)
                    end
                end

                -- Re-check reward data readiness (may have loaded async)
                if entry.isAreaPOI and entry.isLocked then
                    local bountyLockQuestID = ResolveBountyLockQuestIDFromQuestEntry(entry)
                    entry.rewardDataReady = not bountyLockQuestID
                        or rewardPreloadState.IsQuestRewardDisplayReady(bountyLockQuestID)
                elseif not entry.isAreaPOI then
                    entry.rewardDataReady = rewardPreloadState.IsQuestRewardDisplayReady(
                        entry.questID)
                    if entry.rewardDataReady and not entry.primReward then
                        local rawRewards = GetQuestRewards(entry.questID)
                        if rawRewards and #rawRewards > 0 then
                            entry.primReward = GetPrimaryRewardType(rawRewards)
                        end
                    end
                end

                -- Apply current filters
                local excluded = false
                if next(filterTypes) and entry.questType
                    and filterTypes[entry.questType]
                then
                    excluded = true
                end
                if not excluded and next(filterRewards) and entry.primReward
                    and filterRewards[entry.primReward]
                then
                    excluded = true
                end
                if not excluded and searchLower then
                    local matched =
                        (entry.title and entry.title:lower():find(searchLower, 1, true))
                        or (entry.faction and entry.faction:lower():find(searchLower, 1, true))
                        or (entry.unlockText and entry.unlockText:lower():find(searchLower, 1, true))
                        or (entry.mapName and entry.mapName:lower():find(searchLower, 1, true))
                        or (entry.questType and QUEST_TYPE_LABEL[entry.questType]
                            and QUEST_TYPE_LABEL[entry.questType]:lower():find(searchLower, 1, true))
                        or (entry.primReward and REWARD_TYPE_LABEL[entry.primReward]
                            and REWARD_TYPE_LABEL[entry.primReward]:lower():find(searchLower, 1, true))
                    if not matched then
                        excluded = true
                    end
                end

                if not excluded then
                    filtered[#filtered + 1] = entry
                end
            end
        end

        if #filtered > 1 then
            WOW.table_sort(filtered, CompareQuestEntries)
        end

        if ns.IsDebugEnabled() then
            ns.DebugPrint(WOW.string_format(
                "WorldQuests: Gather fast-path mapID=%s cached=%s filtered=%s queryMaps=%s",
                tostring(mapID),
                tostring(#cached),
                tostring(#filtered),
                tostring(#queryMapIDs)))
        end

        return filtered, hiddenRewardPreloadQuestIDs, querySignature
    end

    -- â”€â”€ Incremental path: scan only new maps, merge with cached raw data â”€â”€â”€â”€
    if newQueryMapIDs and #newQueryMapIDs > 0 then
        local newMapCount = #newQueryMapIDs

        if newMapCount > 8 then
            -- Stage the gather for large map sets â€” but only for new maps
            local stagedQueryMapIDs = newQueryMapIDs
            local stagedQueryState = {
                mapID = mapID,
                mapsToQuery = mapsToQuery,
                queryMapSet = {},
                queryMapIDs = stagedQueryMapIDs,
                allowOutsideHierarchy = allowOutsideHierarchy,
                expandDescendantQueryMapIDs = false,
                _mapsToQueryDetached = true,
            }
            for _, stagedMapID in ipairs(stagedQueryMapIDs) do
                stagedQueryState.queryMapSet[stagedMapID] = true
            end

            local session = GetStagedDescendantGatherSession(
                mapID, animateRows, stagedQueryState)
            if not session or not session.isComplete then
                return {}, nil, querySignature
            end

            -- Merge new raw entries into session cache
            for _, rawEntry in ipairs(session.rawEntries) do
                local entryKey = rawEntry.isAreaPOI
                    and rawEntry._syntheticID or rawEntry.questID
                if entryKey and not self._sessionRawEntriesSeen[entryKey] then
                    self._sessionRawEntries[#self._sessionRawEntries + 1] = rawEntry
                    self._sessionRawEntriesSeen[entryKey] = #self._sessionRawEntries
                end
            end
            for _, scannedMapID in ipairs(stagedQueryMapIDs) do
                self._sessionScannedQueryMapIDs[scannedMapID] = true
            end
        else
            -- Synchronous gather for small new map sets
            self:CancelWorldQuestDescendantGather()

            for newIndex = 1, newMapCount do
                AppendRawQuestEntriesForMap(
                    self._sessionRawEntries,
                    self._sessionRawEntriesSeen,
                    mapsToQuery,
                    newQueryMapIDs[newIndex],
                    mapID,
                    allowOutsideHierarchy)
                self._sessionScannedQueryMapIDs[newQueryMapIDs[newIndex]] = true
            end
        end
    else
        self:CancelWorldQuestDescendantGather()
    end

    -- â”€â”€ Build enriched entries from session raw cache â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    -- Filter raw entries to only those relevant to current mapsToQuery
    local relevantRawEntries = {}
    local relevantSeen = {}
    for rawIndex = 1, #self._sessionRawEntries do
        local rawEntry = self._sessionRawEntries[rawIndex]
        if rawEntry and mapsToQuery[rawEntry.mapID] then
            local entryKey = rawEntry.isAreaPOI
                and rawEntry._syntheticID or rawEntry.questID
            if entryKey and not relevantSeen[entryKey] then
                relevantRawEntries[#relevantRawEntries + 1] = rawEntry
                relevantSeen[entryKey] = true
            end
        end
    end

    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: Gather raw mapID=%s sessionRaw=%s relevantRaw=%s queryMaps=%s newQueryMaps=%s",
            tostring(mapID),
            tostring(#self._sessionRawEntries),
            tostring(#relevantRawEntries),
            tostring(#queryMapIDs),
            tostring(newQueryMapIDs and #newQueryMapIDs or 0)))
    end

    local quests, hiddenRewardPreloadQuestIDs = BuildQuestEntriesFromRawEntries(
        relevantRawEntries,
        mapsToQuery)

    -- Merge enriched entries into cumulative cache for fast-path reuse
    for enrichIndex = 1, #quests do
        local entry = quests[enrichIndex]
        local entryKey = entry.isAreaPOI and entry._syntheticID or entry.questID
        if entryKey then
            local existingIndex = self._sessionEnrichedEntriesSeen[entryKey]
            if existingIndex then
                self._sessionEnrichedEntries[existingIndex] = entry
            else
                local newIndex = #self._sessionEnrichedEntries + 1
                self._sessionEnrichedEntries[newIndex] = entry
                self._sessionEnrichedEntriesSeen[entryKey] = newIndex
            end
        end
    end

    return quests, hiddenRewardPreloadQuestIDs, querySignature
end
end

local function ClearPOIButtonAreaPOIState(poiBtn)
    if not poiBtn then
        return
    end

    poiBtn.areaPOIInfo = nil
    if poiBtn.SetAreaPOIID then
        poiBtn:SetAreaPOIID(nil)
    else
        poiBtn.areaPOIID = nil
    end
    poiBtn.areaPoiID = nil
end

-- =============================================
-- Reward data helpers
-- =============================================

local function QueueQuestRewardItemLoadRefresh(questID, itemID, rewardIndex, questRewardType)
    local itemLoadState = rewardPreloadState.itemLoadState
    if type(questID) ~= "number" or questID <= 0
        or type(itemID) ~= "number" or itemID <= 0
        or not Item or not Item.CreateFromItemID
    then
        return
    end

    local key = WOW.string_format(
        "%d:%s:%d:%d",
        questID,
        questRewardType or "reward",
        rewardIndex or 0,
        itemID)

    if itemLoadState.pending[key] or itemLoadState.completed[key] then
        return
    end

    local item = Item:CreateFromItemID(itemID)
    if not item or not item.ContinueOnItemLoad then
        return
    end

    itemLoadState.pending[key] = true
    item:ContinueOnItemLoad(function()
        WOW.C_Timer.After(0, function()
            itemLoadState.pending[key] = nil

            if not ns.IsWorldQuestsRefreshContextActive()
                or not eventFrame._activeWorldQuestRawIDs[questID]
            then
                return
            end

            itemLoadState.completed[key] = true

            local rowUpdated, needsFullRefresh = eventFrame:UpdateQuestRow(questID)
            if needsFullRefresh or not rowUpdated then
                ScheduleRefresh(false, "Quest reward item load")
            end
            eventFrame:RefreshActiveQuestTooltipIfReady(questID)
        end)
    end)
end

function ns.FormatCompactRewardAmount(amount)
    local numericAmount = tonumber(amount)
    if not numericAmount then
        return nil
    end

    local absAmount = math.abs(numericAmount)
    if absAmount <= 0 then
        return nil
    end

    if absAmount >= 1000 then
        local abbreviateLargeNumbers = _G and _G["AbbreviateLargeNumbers"]
        if type(abbreviateLargeNumbers) == "function" then
            local abbreviated = abbreviateLargeNumbers(absAmount)
            if abbreviated and abbreviated ~= "" then
                return abbreviated
            end
        end

        local suffix = "k"
        local divisor = 1000
        if absAmount >= 1000000 then
            suffix = "m"
            divisor = 1000000
        end

        local roundedAmount = WOW.math_floor(((absAmount / divisor) * 10) + 0.5) / 10
        local compact = WOW.string_format("%.1f%s", roundedAmount, suffix)
        compact = WOW.string_gsub(compact, "%.0([km])$", "%1")
        return compact
    end

    return tostring(absAmount)
end

-- Weekly special-assignment cache tiers: GetSpecialAssignmentDisplayTierRank is defined
-- above (GetAssignmentCacheTierSortKey) with localized tier-name matching.

-- Scaled reward level from GetQuestLogRewardInfo / GetQuestLogChoiceInfo (7th return).
-- Blizzard uses this for the *player's* effective tier (e.g. Veteran vs Adventurer caches);
-- we previously only kept ilvl for armor/weapons, so caches never compared by tier.
-- Collapsing parallel cache rows uses WeeklyParallelRewardItemIsBetterForDisplay (above).

local function ItemIsParallelWeeklyTierCandidate(r)
    if r.rewardType ~= "item" then
        return false
    end
    local lab = r.label
    if (not lab or lab == "") and r.itemID and WOW.C_Item and WOW.C_Item.GetItemNameByID then
        lab = WOW.C_Item.GetItemNameByID(r.itemID)
    end
    if not lab or lab == "" then
        return false
    end
    local rk = GetSpecialAssignmentDisplayTierRank(lab)
    if rk >= 100 and rk ~= 150 then
        return true
    end
    local low = WOW.string_lower(lab)
    return WOW.string_find(low, "cach") or WOW.string_find(low, "cache")
end

-- Chat diagnostics for weekly-cache / parallel reward selection.
-- In-game: /run _G.NOMTOOLS_WQ_REWARD_DIAG = true   (verbose logs on each GetQuestRewards)
-- Or:       /nomtools wqrewarddiag <questID>         (one-shot dump for that quest)
local function WQRewardDiagLog(fmt, ...)
    if not _G.NOMTOOLS_WQ_REWARD_DIAG then
        return
    end
    local ok, msg = pcall(WOW.string_format, fmt, ...)
    if ok and msg then
        print("|cff88ccff[NomTools WQReward]|r", msg)
    else
        print("|cff88ccff[NomTools WQReward]|r", fmt, ...)
    end
end

-- Literal copy of Blizzard_FrameXMLUtil/Mainline/QuestUtils.lua (QuestUtils_GetQuestLogRewardInfo,
-- QuestUtils_GetBestQualityItemRewardIndex) for when globals are unavailable at load order.
local function NomTools_QuestUtils_GetQuestLogRewardInfo(itemIndex, questID, rewardType)
    if rewardType == "choice" then
        return GetQuestLogChoiceInfo(itemIndex, questID)
    end
    return GetQuestLogRewardInfo(itemIndex, questID)
end

local function NomTools_QuestUtils_GetBestQualityItemRewardIndex(questID)
    local index, rewardType
    local bestQuality = -1
    local numQuestRewards = GetNumQuestLogRewards and GetNumQuestLogRewards(questID) or 0
    for i = 1, numQuestRewards do
        local itemName, itemTexture, quantity, quality, isUsable, itemID, itemLevel =
            WOW.GetQuestLogRewardInfo(i, questID)
        if quality and quality > bestQuality then
            index = i
            bestQuality = quality
            rewardType = "reward"
        end
    end
    local numQuestChoices = GetNumQuestLogChoices and GetNumQuestLogChoices(questID) or 0
    for i = 1, numQuestChoices do
        local itemName, itemTexture, quantity, quality, isUsable, itemID, itemLevel =
            GetQuestLogChoiceInfo(i, questID)
        if quality and quality > bestQuality then
            index = i
            bestQuality = quality
            rewardType = "choice"
        end
    end
    return index, rewardType
end

-- How many choice *indices* on this quest are item rewards (not currency-as-choice).
-- Must match the choice loop in GetQuestRewards (currency row vs item row).
local function NomTools_GetExpectedQuestChoiceItemSlotCount(questID)
    if not questID then
        return 0
    end
    local numChoices = GetNumQuestLogChoices and GetNumQuestLogChoices(questID) or 0
    local numChoicesTrue = GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true) or 0
    local nIterate = math.max(numChoices, numChoicesTrue)
    local itemSlots = 0
    for choiceIndex = 1, nIterate do
        local choiceCurrencyInfo = WOW.C_QuestLog and WOW.C_QuestLog.GetQuestRewardCurrencyInfo
            and WOW.C_QuestLog.GetQuestRewardCurrencyInfo(questID, choiceIndex, true)
        if not choiceCurrencyInfo or not choiceCurrencyInfo.currencyID then
            itemSlots = itemSlots + 1
        end
    end
    return itemSlots
end

-- If two+ slots look like parallel weekly-cache tiers, pick the ladder winner with
-- WeeklyParallelRewardItemIsBetterForDisplay **across all qualities** (Veteran can report
-- lower item quality than Adventurer in GetQuestLogRewardInfo — max-quality-only logic loses).
-- Otherwise use QuestUtils_GetBestQualityItemRewardIndex like Blizzard's embedded tooltip.
local function NomTools_ResolvePrimaryItemRewardSlotForParallelDisplay(questID)
    if not questID then
        return nil, nil
    end
    local getBest = rawget(_G, "QuestUtils_GetBestQualityItemRewardIndex")
        or NomTools_QuestUtils_GetBestQualityItemRewardIndex

    local allItems = {}
    do
        local n = GetNumQuestLogRewards and GetNumQuestLogRewards(questID) or 0
        local rewardRowsResolved = 0
        for i = 1, n do
            local name, tex, qty, q, usable, iid, ilevel = WOW.GetQuestLogRewardInfo(i, questID)
            if iid and iid > 0 then
                if (not name or name == "") and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                    local resolved = WOW.C_Item.GetItemNameByID(iid)
                    if resolved and resolved ~= "" then
                        name = resolved
                    end
                end
                if (not tex or tex == "") and WOW.C_Item and WOW.C_Item.GetItemIconByID then
                    local icon = WOW.C_Item.GetItemIconByID(iid)
                    if icon and icon ~= "" then
                        tex = icon
                    end
                end
            end
            if name and tex then
                rewardRowsResolved = rewardRowsResolved + 1
                allItems[#allItems + 1] = {
                    rewardType = "item",
                    label = name,
                    itemID = iid,
                    quality = q,
                    rewardItemLevel = (type(ilevel) == "number" and ilevel > 0) and ilevel or nil,
                    _slot = i,
                    _qtype = "reward",
                }
            end
        end
        if n > rewardRowsResolved then
            WQRewardDiagLog(
                "resolve(%s) -> nil (incomplete GetQuestLogRewardInfo rows %d < %d)",
                tostring(questID),
                rewardRowsResolved,
                n)
            return nil, nil
        end
        local numChoices = GetNumQuestLogChoices and GetNumQuestLogChoices(questID) or 0
        local numChoicesTrue = GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true) or 0
        local nChoiceIterate = math.max(numChoices, numChoicesTrue)
        local choiceItemRowsResolved = 0
        for choiceIndex = 1, nChoiceIterate do
            local choiceCurrencyInfo = WOW.C_QuestLog and WOW.C_QuestLog.GetQuestRewardCurrencyInfo
                and WOW.C_QuestLog.GetQuestRewardCurrencyInfo(questID, choiceIndex, true)
            if choiceCurrencyInfo and choiceCurrencyInfo.currencyID then
                -- Currency-as-choice; not part of the parallel item ladder.
            else
                local name, tex, qty, q, usable, iid, ilevel = GetQuestLogChoiceInfo(choiceIndex, questID)
                if iid and iid > 0 then
                    if (not name or name == "") and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                        local resolved = WOW.C_Item.GetItemNameByID(iid)
                        if resolved and resolved ~= "" then
                            name = resolved
                        end
                    end
                    if (not tex or tex == "") and WOW.C_Item and WOW.C_Item.GetItemIconByID then
                        local icon = WOW.C_Item.GetItemIconByID(iid)
                        if icon and icon ~= "" then
                            tex = icon
                        end
                    end
                end
                if name and tex then
                    choiceItemRowsResolved = choiceItemRowsResolved + 1
                    allItems[#allItems + 1] = {
                        rewardType = "item",
                        label = name,
                        itemID = iid,
                        quality = q,
                        rewardItemLevel = (type(ilevel) == "number" and ilevel > 0) and ilevel or nil,
                        _slot = choiceIndex,
                        _qtype = "choice",
                    }
                end
            end
        end
        local expectedChoiceItemSlots = NomTools_GetExpectedQuestChoiceItemSlotCount(questID)
        if expectedChoiceItemSlots > choiceItemRowsResolved then
            WQRewardDiagLog(
                "resolve(%s) -> nil (incomplete GetQuestLogChoiceInfo item rows %d < %d)",
                tostring(questID),
                choiceItemRowsResolved,
                expectedChoiceItemSlots)
            return nil, nil
        end
    end

    WQRewardDiagLog(
        "resolve(%s) API numRewards=%s numChoices=%s numChoices(true)=%s allItems=%d",
        tostring(questID),
        tostring(GetNumQuestLogRewards and GetNumQuestLogRewards(questID)),
        tostring(GetNumQuestLogChoices and GetNumQuestLogChoices(questID)),
        tostring(GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true)),
        #allItems)
    for ji = 1, #allItems do
        local it = allItems[ji]
        WQRewardDiagLog(
            "  [%d] %s | slot=%s %s | q=%s ilvl=%s | tierKey=%s cacheCand=%s itemID=%s",
            ji,
            tostring(it.label),
            tostring(it._slot),
            tostring(it._qtype),
            tostring(it.quality),
            tostring(it.rewardItemLevel),
            tostring(GetAssignmentCacheTierSortKey(it.label or "")),
            tostring(ItemIsParallelWeeklyTierCandidate(it)),
            tostring(it.itemID))
    end

    if #allItems == 0 then
        WQRewardDiagLog("resolve(%s) -> nil (no item rows)", tostring(questID))
        return nil, nil
    end
    if #allItems == 1 then
        WQRewardDiagLog(
            "resolve(%s) -> slot=%s type=%s (single item)",
            tostring(questID),
            tostring(allItems[1]._slot),
            tostring(allItems[1]._qtype))
        return allItems[1]._slot, allItems[1]._qtype
    end

    local cacheLike = {}
    for j = 1, #allItems do
        if ItemIsParallelWeeklyTierCandidate(allItems[j]) then
            cacheLike[#cacheLike + 1] = j
        end
    end
    WQRewardDiagLog("resolve(%s) cacheLike count=%d", tostring(questID), #cacheLike)

    local bqIdx, bqTy = NomTools_QuestUtils_GetBestQualityItemRewardIndex(questID)
    WQRewardDiagLog(
        "resolve(%s) Blizzard NomTools_GetBestQuality copy -> slot=%s type=%s",
        tostring(questID),
        tostring(bqIdx),
        tostring(bqTy))
    local gq = rawget(_G, "QuestUtils_GetBestQualityItemRewardIndex")
    if gq then
        local gqIdx, gqTy = gq(questID)
        WQRewardDiagLog(
            "resolve(%s) global QuestUtils_GetBestQualityItemRewardIndex -> slot=%s type=%s",
            tostring(questID),
            tostring(gqIdx),
            tostring(gqTy))
    end

    if #cacheLike >= 2 then
        local best = cacheLike[1]
        for k = 2, #cacheLike do
            local jj = cacheLike[k]
            if WeeklyParallelRewardItemIsBetterForDisplay(allItems[jj], allItems[best]) then
                best = jj
            end
        end
        WQRewardDiagLog(
            "resolve(%s) -> slot=%s type=%s (multi cache-tier pick idx=%d)",
            tostring(questID),
            tostring(allItems[best]._slot),
            tostring(allItems[best]._qtype),
            best)
        return allItems[best]._slot, allItems[best]._qtype
    elseif #cacheLike == 1 then
        local j = cacheLike[1]
        WQRewardDiagLog(
            "resolve(%s) -> slot=%s type=%s (sole cacheCand idx=%d)",
            tostring(questID),
            tostring(allItems[j]._slot),
            tostring(allItems[j]._qtype),
            j)
        return allItems[j]._slot, allItems[j]._qtype
    end

    local fbIdx, fbTy = getBest(questID)
    WQRewardDiagLog(
        "resolve(%s) -> slot=%s type=%s (fallback getBest)",
        tostring(questID),
        tostring(fbIdx),
        tostring(fbTy))
    return fbIdx, fbTy
end

-- Blizzard QuestUtils.lua (FrameXML) literally uses QuestUtils_GetBestQualityItemRewardIndex
-- for the embedded quest item on tooltips, then EmbeddedItemTooltip_SetItemByQuestReward.
-- That function is *max quality only*; when several rows share max quality it keeps the
-- first encountered — wrong for Adventurer+Veteran caches with the same quality.
-- Below: copy Blizzard's helpers verbatim, then break quality ties among *all* max-quality
-- item slots with WeeklyParallelRewardItemIsBetterForDisplay (tier name, then scaled ilevel).

local nomToolsWQEmbedItemTooltip
local function EnsureNomToolsWQEmbedItemTooltip()
    if nomToolsWQEmbedItemTooltip then
        return nomToolsWQEmbedItemTooltip
    end
    if not WOW.CreateFrame then
        return nil
    end
    local ok, fr = pcall(WOW.CreateFrame, "GameTooltip", "NomToolsWQ_EmbeddedQuestItemProbe",
        UIParent, "InternalEmbeddedItemTooltipTemplate")
    if not ok or not fr then
        return nil
    end
    nomToolsWQEmbedItemTooltip = fr
    return nomToolsWQEmbedItemTooltip
end

local function ItemIDFromItemHyperlink(link)
    if type(link) ~= "string" or link == "" then
        return nil
    end
    return tonumber(string.match(link, "item:(%d+)"))
end

-- Keep the quest-log row that matches NomTools_ResolvePrimaryItemRewardSlotForParallelDisplay
-- (Blizzard max-quality bucket + tier tie-break); drop other parallel-cache ladder rows.
local function PruneParallelWeeklyCacheRewardsUsingEmbeddedQuestItem(questID, rewards)
    if not questID or type(rewards) ~= "table" then
        return rewards
    end

    local winSlot, winType = NomTools_ResolvePrimaryItemRewardSlotForParallelDisplay(questID)
    WQRewardDiagLog(
        "prune(%s) resolve -> slot=%s type=%s rows=%d",
        tostring(questID),
        tostring(winSlot),
        tostring(winType),
        #rewards)
    if not winSlot or not winType then
        return rewards
    end
    local winQRT = winType == "choice" and "choice" or "reward"

    local best
    for i = 1, #rewards do
        local r = rewards[i]
        if r.rewardType == "item"
            and r.rewardIndex == winSlot
            and (r.questRewardType or "reward") == winQRT
        then
            best = i
            break
        end
    end
    if not best then
        WQRewardDiagLog(
            "prune(%s) NO row matched rewardIndex=%s questRewardType=%s (list out of sync with API)",
            tostring(questID),
            tostring(winSlot),
            tostring(winQRT))
        return rewards
    end
    WQRewardDiagLog(
        "prune(%s) matched row[%d] label=%s cacheCand=%s",
        tostring(questID),
        best,
        tostring(rewards[best].label),
        tostring(ItemIsParallelWeeklyTierCandidate(rewards[best])))

    -- Optional: refresh winning row icon/item from embedded tooltip (player-scaled id).
    local setter = EmbeddedItemTooltip_SetItemByQuestReward
    local embedClear = EmbeddedItemTooltip_Clear
    local embedHide = EmbeddedItemTooltip_Hide
    local embed = (setter and embedClear) and EnsureNomToolsWQEmbedItemTooltip() or nil
    if embed and embed.Tooltip and embed.Tooltip.GetItem then
        embedClear(embed)
        if embedHide then
            embedHide(embed)
        end
        local r = rewards[best]
        local qrt = r.questRewardType == "choice" and "choice" or "reward"
        if pcall(setter, embed, r.rewardIndex, questID, qrt, false) then
            local tip = embed.Tooltip
            local name, link = tip:GetItem()
            local embedItemID = embed.itemID
            if embedItemID and embedItemID > 0 then
                rewards[best].itemID = embedItemID
            elseif link then
                local id = ItemIDFromItemHyperlink(link)
                if id and id > 0 then
                    rewards[best].itemID = id
                end
            end
            if name and name ~= "" then
                rewards[best].label = name
            end
            if link and WOW.C_Item and WOW.C_Item.GetItemInfo then
                local _, _, q = WOW.C_Item.GetItemInfo(link)
                if type(q) == "number" then
                    rewards[best].quality = q
                end
            elseif rewards[best].itemID and WOW.C_Item and WOW.C_Item.GetItemQualityByID then
                rewards[best].quality = WOW.C_Item.GetItemQualityByID(rewards[best].itemID)
            end
            local tex = rewards[best].itemID and WOW.C_Item and WOW.C_Item.GetItemIconByID
                and WOW.C_Item.GetItemIconByID(rewards[best].itemID) or nil
            if tex and tex ~= "" then
                rewards[best].icon = tex
            end
        end
        embedClear(embed)
        if embedHide then
            embedHide(embed)
        end
    end

    if not ItemIsParallelWeeklyTierCandidate(rewards[best]) then
        -- Primary slot is gear/other at max quality; leave every item row in the list.
        WQRewardDiagLog(
            "prune(%s) winner not a cache-tier candidate; skip omit",
            tostring(questID))
        return rewards
    end

    local omit = {}
    local omitCount = 0
    for i = 1, #rewards do
        if i ~= best and rewards[i].rewardType == "item" and ItemIsParallelWeeklyTierCandidate(rewards[i]) then
            omit[i] = true
            omitCount = omitCount + 1
        end
    end
    WQRewardDiagLog("prune(%s) omitting %d parallel-cache rows", tostring(questID), omitCount)
    local out = {}
    for i = 1, #rewards do
        if not omit[i] then
            out[#out + 1] = rewards[i]
        end
    end
    return out
end

-- Row icons fill in **reward table order** (first non-gold entry = leftmost icon).
-- Parallel tiers are often listed Adventurer then Veteran; after deduping we still
-- want the surviving tier in the first slot when other item rows precede it.
local function PromoteBestParallelWeeklyCacheToFirstRewardRow(rewards)
    if not rewards or #rewards == 0 then
        return rewards
    end
    local firstNonGold
    for i = 1, #rewards do
        if rewards[i].rewardType ~= "gold" then
            firstNonGold = i
            break
        end
    end
    if not firstNonGold then
        return rewards
    end

    local cand = {}
    for i = 1, #rewards do
        if rewards[i].rewardType == "item" and ItemIsParallelWeeklyTierCandidate(rewards[i]) then
            cand[#cand + 1] = i
        end
    end
    if #cand == 0 then
        return rewards
    end

    local bestPos = cand[1]
    for k = 2, #cand do
        local i = cand[k]
        if WeeklyParallelRewardItemIsBetterForDisplay(rewards[i], rewards[bestPos]) then
            bestPos = i
        end
    end
    if bestPos == firstNonGold then
        WQRewardDiagLog(
            "promote: already first non-gold (idx=%s label=%s)",
            tostring(bestPos),
            tostring(rewards[bestPos].label))
        return rewards
    end

    WQRewardDiagLog(
        "promote: moving row idx=%s label=%s to display slot idx=%s",
        tostring(bestPos),
        tostring(rewards[bestPos].label),
        tostring(firstNonGold))
    local entry = table.remove(rewards, bestPos)
    if bestPos < firstNonGold then
        firstNonGold = firstNonGold - 1
    end
    table.insert(rewards, firstNonGold, entry)
    WQRewardDiagLog(
        "promote: first reward row is now %s (type=%s)",
        tostring(rewards[firstNonGold] and rewards[firstNonGold].label),
        tostring(rewards[firstNonGold] and rewards[firstNonGold].rewardType))
    return rewards
end

local function GetItemClassSubclassKey(itemID)
    if not itemID or not WOW.C_Item or not WOW.C_Item.GetItemInfoInstant then
        return -1
    end
    local _, _, _, _, _, classID, subclassID = WOW.C_Item.GetItemInfoInstant(itemID)
    return (classID or 0) * 1000 + (subclassID or 0)
end

local function IndicesShouldCollapseAsTierLadder(rewards, indices)
    if #indices < 2 then
        return false
    end
    local minIl, maxIl
    local anyNameTier = false
    local anyCacheWord = false
    for k = 1, #indices do
        local r = rewards[indices[k]]
        local il = r.rewardItemLevel
        if type(il) == "number" and il > 0 then
            minIl = minIl and math.min(minIl, il) or il
            maxIl = maxIl and math.max(maxIl, il) or il
        end
        if ItemIsParallelWeeklyTierCandidate(r) then
            anyNameTier = true
        end
        do
            local lab = r.label
            if (not lab or lab == "") and r.itemID and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                lab = WOW.C_Item.GetItemNameByID(r.itemID)
            end
            if lab then
                local lower = WOW.string_lower(lab)
                if WOW.string_find(lower, "cach") or WOW.string_find(lower, "cache") then
                    anyCacheWord = true
                end
            end
        end
    end
    if anyNameTier or anyCacheWord then
        return true
    end
    if minIl and maxIl and maxIl > minIl then
        return true
    end
    return false
end

-- 1) Name/cache/tier-keyword groups (English + a few patterns).
-- 2) Same item class+subclass + same quality (parallel weekly caches share this); use
--    rewardItemLevel + name tier when Blizzard lists every ladder row at once.
local function CollapseParallelWeeklyCacheItemRewards(rewards)
    local function collapseIndexGroup(cacheIndices)
        if #cacheIndices < 2 then
            return
        end
        local bestIndex = cacheIndices[1]
        local best = rewards[bestIndex]
        for k = 2, #cacheIndices do
            local idx = cacheIndices[k]
            local cand = rewards[idx]
            if WeeklyParallelRewardItemIsBetterForDisplay(cand, best) then
                best = cand
                bestIndex = idx
            end
        end
        local omit = {}
        for _, idx in ipairs(cacheIndices) do
            if idx ~= bestIndex then
                omit[idx] = true
            end
        end
        return omit
    end

    local omit = {}
    do
        local cacheIndices = {}
        for i = 1, #rewards do
            if ItemIsParallelWeeklyTierCandidate(rewards[i]) then
                cacheIndices[#cacheIndices + 1] = i
            end
        end
        if #cacheIndices >= 2 then
            local o = collapseIndexGroup(cacheIndices)
            if o then
                for k, v in pairs(o) do
                    omit[k] = v
                end
            end
        end
    end

    do
        local buckets = {}
        for i = 1, #rewards do
            if not omit[i] then
                local r = rewards[i]
                if r.rewardType == "item" and r.itemID then
                    local key = (r.quality or 0) * 10000000 + GetItemClassSubclassKey(r.itemID)
                    buckets[key] = buckets[key] or {}
                    buckets[key][#buckets[key] + 1] = i
                end
            end
        end
        for _, indices in pairs(buckets) do
            if IndicesShouldCollapseAsTierLadder(rewards, indices) then
                local o = collapseIndexGroup(indices)
                if o then
                    for k, v in pairs(o) do
                        omit[k] = v
                    end
                end
            end
        end
    end

    -- 3) Adventurer vs Veteran often differ in item quality, so they land in
    -- separate buckets above. Same reward icon + cache/tier labels ⇒ one ladder.
    do
        local byIcon = {}
        for i = 1, #rewards do
            if not omit[i] then
                local r = rewards[i]
                if r.rewardType == "item" and r.icon and r.icon ~= "" then
                    local list = byIcon[r.icon]
                    if not list then
                        list = {}
                        byIcon[r.icon] = list
                    end
                    list[#list + 1] = i
                end
            end
        end
        for _, indices in pairs(byIcon) do
            if #indices >= 2 then
                local allLadderLike = true
                for k = 1, #indices do
                    local rr = rewards[indices[k]]
                    local lab = rr.label
                    if (not lab or lab == "") and rr.itemID and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                        lab = WOW.C_Item.GetItemNameByID(rr.itemID)
                    end
                    if not lab or lab == "" then
                        allLadderLike = false
                        break
                    end
                    local low = WOW.string_lower(lab)
                    local rk = GetSpecialAssignmentDisplayTierRank(lab)
                    if not (WOW.string_find(low, "cach")
                        or WOW.string_find(low, "cache")
                        or (rk >= 100 and rk ~= 150)) then
                        allLadderLike = false
                        break
                    end
                end
                if allLadderLike then
                    local o = collapseIndexGroup(indices)
                    if o then
                        for kk, vv in pairs(o) do
                            omit[kk] = vv
                        end
                    end
                end
            end
        end
    end

    if not next(omit) then
        return rewards
    end
    local out = {}
    for i = 1, #rewards do
        if not omit[i] then
            out[#out + 1] = rewards[i]
        end
    end
    return out
end

-- Returns the reward list for a quest, queried fresh each call.
-- Each entry: { rewardType, icon, label, amount, itemID, currencyID, factionID, currencyLabel, ilvl }
--   rewardType: "item" | "currency" | "gold" | "rep"
-- Returns an empty table when reward data has not been preloaded yet.
GetQuestRewards = function(questID)
    -- Reward APIs return partial or default values before the server has
    -- finished loading data.  Gate all queries behind WOW.HaveQuestRewardData.
    if WOW.HaveQuestRewardData and not WOW.HaveQuestRewardData(questID) then
        return {}
    end

    local rewards = {}
    local repRewards = {}
    local genericCurrencyRewards = {}
    local currencyRewardSources = {}

    local function GetCurrencyRewardSourceAmount(currencyInfo)
        if not currencyInfo then
            return nil
        end

        return currencyInfo.totalRewardAmount
            or currencyInfo.quantity
            or currencyInfo.rewardAmount
            or currencyInfo.amount
            or currencyInfo.totalAmount
    end

    local function GetDisplayedCurrencyRewardInfo(currencyInfo)
        if not currencyInfo or not currencyInfo.currencyID then
            return nil, nil, nil, nil, nil
        end

        local displayName = currencyInfo.name
        local displayIcon = currencyInfo.texture
        local sourceAmount = GetCurrencyRewardSourceAmount(currencyInfo)
        local displayAmount = sourceAmount
        local displayQuality = currencyInfo.quality

        if CurrencyContainerUtil
            and CurrencyContainerUtil.GetCurrencyContainerInfo
        then
            displayName, displayIcon, displayAmount, displayQuality =
                CurrencyContainerUtil.GetCurrencyContainerInfo(
                    currencyInfo.currencyID,
                    displayAmount,
                    displayName,
                    displayIcon,
                    displayQuality)
        end

        return displayName, displayIcon, displayAmount, displayQuality, sourceAmount
    end

    local function GetFactionRewardDisplayInfo(factionID, fallbackName, fallbackIcon)
        local label = fallbackName
        local icon = fallbackIcon

        if factionID and factionID > 0
            and WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID
        then
            local data = WOW.C_Reputation.GetFactionDataByID(factionID)
            if data then
                if data.name and data.name ~= "" then
                    label = data.name
                end
                if data["texture"] then
                    icon = data["texture"]
                end
            end
        end

        return label, icon
    end

    local itemRewardRowsMaterialized = 0
    local choiceItemRowsMaterialized = 0

    local function AddItemReward(name, texture, stackCount, quality, itemID, itemLevel, rewardIndex, questRewardType)
        local qrt = questRewardType or "reward"
        if itemID and itemID > 0 then
            if (not name or name == "") and WOW.C_Item and WOW.C_Item.GetItemNameByID then
                local resolved = WOW.C_Item.GetItemNameByID(itemID)
                if resolved and resolved ~= "" then
                    name = resolved
                end
            end
            if (not texture or texture == "") and WOW.C_Item and WOW.C_Item.GetItemIconByID then
                local icon = WOW.C_Item.GetItemIconByID(itemID)
                if icon and icon ~= "" then
                    texture = icon
                end
            end
        end
        if not name or not texture then
            QueueQuestRewardItemLoadRefresh(
                questID,
                itemID,
                rewardIndex,
                questRewardType)
            return
        end

        local amount = nil
        local isGearReward = ns._WorldQuestsIsActualGearRewardItem(itemID)
        if type(stackCount) == "number" and stackCount > 0 then
            amount = stackCount
        end

        local rewardItemLevel = (type(itemLevel) == "number" and itemLevel > 0) and itemLevel or nil

        rewards[#rewards + 1] = {
            rewardType = "item",
            icon = texture,
            label = name,
            amount = amount,
            itemID = itemID,
            ilvl = isGearReward and itemLevel and itemLevel > 0 and itemLevel or nil,
            rewardItemLevel = rewardItemLevel,
            quality = quality,
            rewardIndex = rewardIndex,
            questRewardType = qrt,
        }

        if qrt == "reward" then
            itemRewardRowsMaterialized = itemRewardRowsMaterialized + 1
        elseif qrt == "choice" then
            choiceItemRowsMaterialized = choiceItemRowsMaterialized + 1
        end

        QueueQuestRewardItemLoadRefresh(
            questID,
            itemID,
            rewardIndex,
            questRewardType)
    end

    local function AddCurrencyRewardSource(currencyInfo, rewardIndex, questRewardType)
        if not currencyInfo or not currencyInfo.currencyID then
            return false
        end

        currencyRewardSources[#currencyRewardSources + 1] = {
            info = currencyInfo,
            rewardIndex = rewardIndex,
            questRewardType = questRewardType or "reward",
        }
        return true
    end

    local numItems = GetNumQuestLogRewards and GetNumQuestLogRewards(questID) or 0
    for i = 1, numItems do
        local name, texture, numStacks, quality, _, itemID, itemLevel =
            WOW.GetQuestLogRewardInfo(i, questID)
        AddItemReward(name, texture, numStacks, quality, itemID, itemLevel, i, "reward")
    end

    -- Parallel weekly-cache rows (Adventurer + Veteran) can arrive one slot at a time after
    -- /reload: HaveQuestRewardData is true while GetQuestLogRewardInfo still omits name/texture
    -- for a slot. Building a partial list makes tier resolution pick the wrong cache; defer.
    if numItems > itemRewardRowsMaterialized then
        WQRewardDiagLog(
            "GetQuestRewards(%s) deferred incomplete item rows %d/%d",
            tostring(questID),
            itemRewardRowsMaterialized,
            numItems)
        return {}
    end

    local currencyRewards = WOW.C_QuestLog and WOW.C_QuestLog.GetQuestRewardCurrencies
        and WOW.C_QuestLog.GetQuestRewardCurrencies(questID)
    if currencyRewards then
        for rewardIndex, currencyInfo in ipairs(currencyRewards) do
            AddCurrencyRewardSource(currencyInfo, rewardIndex, "reward")
        end
    end

    -- Include GetNumQuestLogChoices(questID, true) when larger: tooltip / data can expose extra
    -- choice rows (parallel weekly-cache tiers) not counted by the plain choice count.
    local numChoices = GetNumQuestLogChoices and GetNumQuestLogChoices(questID) or 0
    local numChoicesTrue = GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true) or 0
    local numChoiceIterate = math.max(numChoices, numChoicesTrue)
    for choiceIndex = 1, numChoiceIterate do
        local choiceCurrencyInfo = WOW.C_QuestLog
            and WOW.C_QuestLog.GetQuestRewardCurrencyInfo
            and WOW.C_QuestLog.GetQuestRewardCurrencyInfo(questID, choiceIndex, true)
            or nil
        if not AddCurrencyRewardSource(choiceCurrencyInfo, choiceIndex, "choice") then
            local name, texture, numStacks, quality, ignoredUsable, itemID, itemLevel
            if GetQuestLogChoiceInfo then
                name, texture, numStacks, quality, ignoredUsable, itemID, itemLevel =
                    GetQuestLogChoiceInfo(choiceIndex, questID)
            end
            AddItemReward(name, texture, numStacks, quality, itemID, itemLevel, choiceIndex, "choice")
        end
    end

    local expectedChoiceItemSlots = NomTools_GetExpectedQuestChoiceItemSlotCount(questID)
    if expectedChoiceItemSlots > choiceItemRowsMaterialized then
        WQRewardDiagLog(
            "GetQuestRewards(%s) deferred incomplete choice item rows %d/%d",
            tostring(questID),
            choiceItemRowsMaterialized,
            expectedChoiceItemSlots)
        return {}
    end

    local directRepOrder = {}
    local directRepAmounts = {}
    local directRepLabels = {}
    local directRepIcons = {}

    if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestLogMajorFactionReputationRewards then
        local majorFactionRewards = WOW.C_QuestLog.GetQuestLogMajorFactionReputationRewards(questID)
        if majorFactionRewards then
            for _, repInfo in ipairs(majorFactionRewards) do
                local factionID = repInfo and repInfo.factionID or nil
                local rewardAmount = repInfo
                    and (repInfo.rewardAmount or repInfo["totalRewardAmount"] or repInfo["amount"] or repInfo["quantity"])
                    or nil
                if factionID and factionID > 0 and rewardAmount and rewardAmount ~= 0 then
                    if not directRepAmounts[factionID] then
                        directRepOrder[#directRepOrder + 1] = factionID
                    end
                    directRepAmounts[factionID] = (directRepAmounts[factionID] or 0) + rewardAmount

                    local label, icon = GetFactionRewardDisplayInfo(
                        factionID,
                        repInfo["name"],
                        repInfo["texture"])
                    if label and label ~= "" then
                        directRepLabels[factionID] = label
                    end
                    if icon then
                        directRepIcons[factionID] = icon
                    end
                end
            end
        end
    end

    local mappedRepOrder = {}
    local mappedRepAmounts = {}
    local mappedRepLabels = {}
    local mappedRepIcons = {}
    local mappedRepCurrencyLabels = {}
    local knownRepLabels = {}

    local function RememberRepLabel(label)
        local normalizedLabel = NormalizeFactionName(label)
        if normalizedLabel then
            knownRepLabels[normalizedLabel] = true
        end
    end

    for _, source in ipairs(currencyRewardSources) do
        local currencyInfo = source.info
        local displayName, displayIcon, displayAmount, _, sourceAmount =
            GetDisplayedCurrencyRewardInfo(currencyInfo)
        local factionID = C_CurrencyInfo
            and C_CurrencyInfo.GetFactionGrantedByCurrency
            and currencyInfo.currencyID
            and C_CurrencyInfo.GetFactionGrantedByCurrency(currencyInfo.currencyID)
            or nil

        if factionID and factionID > 0 then
            if not directRepAmounts[factionID] and sourceAmount and sourceAmount ~= 0 then
                if not mappedRepAmounts[factionID] then
                    mappedRepOrder[#mappedRepOrder + 1] = factionID
                end
                mappedRepAmounts[factionID] = (mappedRepAmounts[factionID] or 0) + sourceAmount

                local label, icon = GetFactionRewardDisplayInfo(factionID, nil, displayIcon)
                if label and label ~= "" then
                    mappedRepLabels[factionID] = label
                end
                if icon then
                    mappedRepIcons[factionID] = icon
                end

                if displayName and displayName ~= "" then
                    local currentLabel = mappedRepCurrencyLabels[factionID]
                    if currentLabel == nil then
                        mappedRepCurrencyLabels[factionID] = displayName
                    elseif currentLabel ~= false and currentLabel ~= displayName then
                        mappedRepCurrencyLabels[factionID] = false
                    end
                end
            end
        elseif displayName and displayIcon then
            genericCurrencyRewards[#genericCurrencyRewards + 1] = {
                rewardType = "currency",
                icon = displayIcon,
                label = displayName,
                amount = displayAmount,
                currencyID = currencyInfo.currencyID,
                rewardIndex = source.rewardIndex,
                questRewardType = source.questRewardType,
            }
        end
    end

    for _, factionID in ipairs(directRepOrder) do
        local label = directRepLabels[factionID]
        local amount = directRepAmounts[factionID]
        if label and label ~= "" and amount and amount ~= 0 then
            repRewards[#repRewards + 1] = {
                rewardType = "rep",
                icon = directRepIcons[factionID] or "Interface\\Icons\\Achievement_Reputation_01",
                label = label,
                amount = amount,
                factionID = factionID,
            }
            RememberRepLabel(label)
        end
    end

    for _, factionID in ipairs(mappedRepOrder) do
        local label = mappedRepLabels[factionID]
        local amount = mappedRepAmounts[factionID]
        if label and label ~= "" and amount and amount ~= 0 then
            local currencyLabel = mappedRepCurrencyLabels[factionID]
            if currencyLabel == false then
                currencyLabel = nil
            end

            repRewards[#repRewards + 1] = {
                rewardType = "rep",
                icon = mappedRepIcons[factionID] or "Interface\\Icons\\Achievement_Reputation_01",
                label = label,
                amount = amount,
                factionID = factionID,
                currencyLabel = currencyLabel,
            }
            RememberRepLabel(label)
        end
    end

    local legacyRepRewards = ns._WorldQuestsGetLegacySelectedQuestRewardFactions(questID)
    if legacyRepRewards then
        for _, legacyRewardData in ipairs(legacyRepRewards) do
            local label = legacyRewardData.label
            local amount = legacyRewardData.amount
            local factionID = legacyRewardData.factionID
            local normalizedLabel = NormalizeFactionName(label)
            local skipLegacy = false

            if factionID and (directRepAmounts[factionID] or mappedRepAmounts[factionID]) then
                skipLegacy = true
            elseif normalizedLabel and knownRepLabels[normalizedLabel] then
                skipLegacy = true
            end

            if not skipLegacy and label and label ~= "" and amount and amount ~= 0 then
                local resolvedLabel, resolvedIcon = GetFactionRewardDisplayInfo(
                    factionID,
                    label,
                    legacyRewardData.icon)
                repRewards[#repRewards + 1] = {
                    rewardType = "rep",
                    icon = resolvedIcon or "Interface\\Icons\\Achievement_Reputation_01",
                    label = resolvedLabel,
                    amount = amount,
                    factionID = factionID,
                }
                RememberRepLabel(resolvedLabel)
            end
        end
    end

    for _, rewardData in ipairs(repRewards) do
        rewards[#rewards + 1] = rewardData
    end

    for _, rewardData in ipairs(genericCurrencyRewards) do
        rewards[#rewards + 1] = rewardData
    end

    -- â”€â”€ Gold reward â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local money = WOW.GetQuestLogRewardMoney and WOW.GetQuestLogRewardMoney(questID) or 0
    if money and money > 0 then
        local gold   = WOW.math_floor(money / 10000)
        local silver = WOW.math_floor((money % 10000) / 100)
        local copper = money % 100
        local parts = {}
        if gold   > 0 then parts[#parts + 1] = gold   .. "|TInterface\\MoneyFrame\\UI-GoldIcon:12:12:0:0|t" end
        if silver > 0 then parts[#parts + 1] = silver .. "|TInterface\\MoneyFrame\\UI-SilverIcon:12:12:0:0|t" end
        if copper > 0 then parts[#parts + 1] = copper .. "|TInterface\\MoneyFrame\\UI-CopperIcon:12:12:0:0|t" end
        rewards[#rewards + 1] = {
            rewardType = "gold",
            label      = table.concat(parts, " "),
        }
    end

    rewards = PruneParallelWeeklyCacheRewardsUsingEmbeddedQuestItem(questID, rewards)
    rewards = CollapseParallelWeeklyCacheItemRewards(rewards)
    rewards = PromoteBestParallelWeeklyCacheToFirstRewardRow(rewards)

    WQRewardDiagLog("GetQuestRewards(%s) HaveQuestRewardData=%s finalCount=%d", tostring(questID),
        tostring(WOW.HaveQuestRewardData and WOW.HaveQuestRewardData(questID)), #rewards)
    for ri = 1, math.min(#rewards, 14) do
        local rr = rewards[ri]
        if rr.rewardType == "item" then
            WQRewardDiagLog(
                "  out[%d] item idx=%s %s label=%s itemID=%s q=%s ilvl=%s cacheCand=%s",
                ri,
                tostring(rr.rewardIndex),
                tostring(rr.questRewardType or "reward"),
                tostring(rr.label),
                tostring(rr.itemID),
                tostring(rr.quality),
                tostring(rr.rewardItemLevel),
                tostring(ItemIsParallelWeeklyTierCandidate(rr)))
        else
            WQRewardDiagLog("  out[%d] %s label=%s", ri, tostring(rr.rewardType), tostring(rr.label))
        end
    end

    return rewards
end

-- Blizzard pairs ITEM_DATA_LOAD_RESULT with C_Item.RequestLoadItemData (Item mixin), not
-- exclusively with RequestLoadItemByID; GET_ITEM_INFO_RECEIVED covers the legacy GetItemInfo
-- pipeline. Request both so whichever subsystem hydrates first resolves pending rows.
local function RequestQuestRewardItemClientHydration(itemID)
    if type(itemID) ~= "number" or itemID <= 0 then
        return
    end
    local itemApi = WOW.C_Item
    local ItemGlobal = rawget(_G, "Item")
    if ItemGlobal and ItemGlobal.CreateFromItemID and itemApi and itemApi.RequestLoadItemData then
        local itemObj = ItemGlobal:CreateFromItemID(itemID)
        if itemObj then
            pcall(itemApi.RequestLoadItemData, itemObj)
        end
    end
    if itemApi and itemApi.RequestLoadItemByID then
        pcall(itemApi.RequestLoadItemByID, itemID)
    end
end

-- After HaveQuestRewardData(questID) is true, item rows can still lack C_Item cache until
-- ITEM_DATA_LOAD_RESULT; keep reward-pending state until every expected item row is cached.
function rewardPreloadState.AreQuestRewardItemClientCachesReady(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    if not WOW.HaveQuestRewardData or not WOW.HaveQuestRewardData(questID) then
        return false
    end

    local numExpectedRewardItems = GetNumQuestLogRewards
        and GetNumQuestLogRewards(questID) or 0
    local numExpectedChoiceItems = NomTools_GetExpectedQuestChoiceItemSlotCount
        and NomTools_GetExpectedQuestChoiceItemSlotCount(questID) or 0
    local numExpectedItemRows = numExpectedRewardItems + numExpectedChoiceItems

    local rewards = GetQuestRewards(questID)
    local itemRowCount = 0
    if rewards then
        for rewardIndex = 1, #rewards do
            local rewardRow = rewards[rewardIndex]
            if rewardRow and rewardRow.rewardType == "item" then
                itemRowCount = itemRowCount + 1
            end
        end
    end

    if numExpectedItemRows > 0 and itemRowCount < numExpectedItemRows then
        return false
    end

    if not rewards or #rewards == 0 then
        return true
    end

    local itemApi = WOW.C_Item
    if not itemApi or not itemApi.IsItemDataCachedByID then
        return true
    end

    for rewardIndex = 1, #rewards do
        local rewardRow = rewards[rewardIndex]
        if rewardRow and rewardRow.rewardType == "item"
            and type(rewardRow.itemID) == "number"
            and rewardRow.itemID > 0
        then
            if not itemApi.IsItemDataCachedByID(rewardRow.itemID) then
                RequestQuestRewardItemClientHydration(rewardRow.itemID)
                return false
            end
        end
    end

    return true
end

-- One-shot diagnostic for a world quest ID (copy/paste output to the NomTools dev).
-- Usage: /nomtools wqrewarddiag <questID>   or   /run NomTools.DumpWorldQuestRewardDiagnostics(12345)
function ns.DumpWorldQuestRewardDiagnostics(questID)
    if type(questID) ~= "number" or questID == 0 then
        print("|cffff4422[NomTools WQReward]|r Dump: need a numeric questID (e.g. /nomtools wqrewarddiag 12345)")
        return
    end
    local wasDiag = _G.NOMTOOLS_WQ_REWARD_DIAG
    _G.NOMTOOLS_WQ_REWARD_DIAG = false
    print("|cff88ccff======== NomTools WQ reward dump ========|r questID=" .. tostring(questID))
    local title = select(1, WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestInfoByQuestID
        and WOW.C_TaskQuest.GetQuestInfoByQuestID(questID) or nil)
    print("title=" .. tostring(title))
    local have = WOW.HaveQuestRewardData and WOW.HaveQuestRewardData(questID)
    print("HaveQuestRewardData=" .. tostring(have))

    local nr = GetNumQuestLogRewards and GetNumQuestLogRewards(questID) or 0
    local nc = GetNumQuestLogChoices and GetNumQuestLogChoices(questID) or 0
    local nct = GetNumQuestLogChoices and GetNumQuestLogChoices(questID, true) or 0
    print("GetNumQuestLogRewards=" .. tostring(nr)
        .. " GetNumQuestLogChoices=" .. tostring(nc)
        .. " GetNumQuestLogChoices(,true)=" .. tostring(nct))

    local moneyCopper = WOW.GetQuestLogRewardMoney and WOW.GetQuestLogRewardMoney(questID) or 0
    print("GetQuestLogRewardMoney(copper)=" .. tostring(moneyCopper))

    if WOW.C_QuestLog and WOW.C_QuestLog.ShouldShowQuestRewards then
        local ok, show = pcall(WOW.C_QuestLog.ShouldShowQuestRewards, questID)
        print("C_QuestLog.ShouldShowQuestRewards="
            .. (ok and tostring(show) or "(error)"))
    end

    local cis = rawget(_G, "C_QuestInfoSystem")
    if cis then
        if cis.HasQuestRewardCurrencies then
            local ok, hc = pcall(cis.HasQuestRewardCurrencies, questID)
            print("C_QuestInfoSystem.HasQuestRewardCurrencies="
                .. (ok and tostring(hc) or "(error)"))
        end
        if cis.GetQuestRewardSpells then
            local ok, spells = pcall(cis.GetQuestRewardSpells, questID)
            if ok and type(spells) == "table" then
                local parts = {}
                for si = 1, math.min(#spells, 16) do
                    parts[#parts + 1] = tostring(spells[si])
                end
                print("C_QuestInfoSystem.GetQuestRewardSpells count=" .. tostring(#spells)
                    .. " ids=" .. table.concat(parts, ","))
            else
                print("C_QuestInfoSystem.GetQuestRewardSpells="
                    .. (ok and "nil" or "(error)"))
            end
        end
    end

    -- Blizzard tooltips still index reward slot 1 in some styles; verify whether
    -- data exists when GetNum* reports zero (Midnight / task-quest edge cases).
    if nr == 0 and WOW.GetQuestLogRewardInfo then
        local name, tex, qty, q, usable, iid, ilevel =
            WOW.GetQuestLogRewardInfo(1, questID)
        print("probe GetQuestLogRewardInfo(1) when numRewards=0: name="
            .. tostring(name) .. " itemID=" .. tostring(iid) .. " ilevel=" .. tostring(ilevel))
    end
    if nc == 0 and GetQuestLogChoiceInfo then
        local name, tex, qty, q, usable, iid, ilevel =
            GetQuestLogChoiceInfo(1, questID)
        print("probe GetQuestLogChoiceInfo(1) when numChoices=0: name="
            .. tostring(name) .. " itemID=" .. tostring(iid))
    end

    if nr == 0 and nc == 0 and moneyCopper > 0 then
        print(
            "Note: No item/choice/currency/spell rows from APIs; ShouldShowQuestRewards can still be true for money (and rep/XP lines not listed here).")
    end

    for i = 1, nr do
        local name, tex, qty, q, usable, iid, ilevel = WOW.GetQuestLogRewardInfo(i, questID)
        print(WOW.string_format(
            "  reward[%d] name=%s q=%s ilevel=%s itemID=%s tex=%s",
            i, tostring(name), tostring(q), tostring(ilevel), tostring(iid), tostring(tex)))
    end
    for i = 1, math.max(nc, nct) do
        local name, tex, qty, q, usable, iid, ilevel = GetQuestLogChoiceInfo(i, questID)
        print(WOW.string_format(
            "  choice[%d] name=%s q=%s ilevel=%s itemID=%s",
            i, tostring(name), tostring(q), tostring(ilevel), tostring(iid)))
    end

    local bqIdx, bqTy = NomTools_QuestUtils_GetBestQualityItemRewardIndex(questID)
    print("NomTools copy QuestUtils_GetBestQualityItemRewardIndex -> "
        .. tostring(bqIdx) .. " type=" .. tostring(bqTy))
    local gq = rawget(_G, "QuestUtils_GetBestQualityItemRewardIndex")
    if gq then
        local gi, gt = gq(questID)
        print("global QuestUtils_GetBestQualityItemRewardIndex -> "
            .. tostring(gi) .. " type=" .. tostring(gt))
    else
        print("global QuestUtils_GetBestQualityItemRewardIndex = nil")
    end

    local rs, rt = NomTools_ResolvePrimaryItemRewardSlotForParallelDisplay(questID)
    print("NomTools_ResolvePrimaryItemRewardSlotForParallelDisplay -> "
        .. tostring(rs) .. " type=" .. tostring(rt))

    if not have then
        print("|cffff9922[NomTools WQReward]|r Reward data not loaded; GetQuestRewards may be empty.")
        _G.NOMTOOLS_WQ_REWARD_DIAG = wasDiag
        return
    end

    local list = GetQuestRewards(questID)
    print("GetQuestRewards final count=" .. tostring(list and #list or 0))
    if list then
        for i = 1, math.min(#list, 16) do
            local r = list[i]
            if r.rewardType == "item" then
                print(WOW.string_format(
                    "  ->[%d] item slot=%s %s label=%s itemID=%s q=%s ril=%s",
                    i,
                    tostring(r.rewardIndex),
                    tostring(r.questRewardType or "reward"),
                    tostring(r.label),
                    tostring(r.itemID),
                    tostring(r.quality),
                    tostring(r.rewardItemLevel)))
            else
                print(WOW.string_format("  ->[%d] %s %s", i, tostring(r.rewardType), tostring(r.label)))
            end
        end
    end
    print("|cff88ccff======== end dump ========|r")
    _G.NOMTOOLS_WQ_REWARD_DIAG = wasDiag
end

function ns.ShowReputationRewardTooltip(anchorFrame, label, amount, currencyLabel)
    WOW.GameTooltip:SetOwner(anchorFrame, "ANCHOR_RIGHT")
    WOW.GameTooltip:AddLine(label or REPUTATION or "Reputation", 0.4, 0.8, 1, true)

    local numericAmount = tonumber(amount)
    if numericAmount and numericAmount ~= 0 then
        local absAmount = math.abs(numericAmount)
        local formattedAmount = BreakUpLargeNumbers and BreakUpLargeNumbers(absAmount)
            or tostring(absAmount)
        WOW.GameTooltip:AddLine("+" .. formattedAmount .. " " .. (REPUTATION or "Reputation"), 1, 1, 1)
    end

    if currencyLabel and currencyLabel ~= "" and currencyLabel ~= label then
        WOW.GameTooltip:AddLine(currencyLabel, 0.72, 0.72, 0.76, true)
    end

    WOW.GameTooltip:Show()
end

do
    local function TooltipHasContent(tooltip)
        if not tooltip then
            return false
        end

        if tooltip.GetItem then
            local itemName = tooltip:GetItem()
            if itemName then
                return true
            end
        end

        return tooltip.NumLines and tooltip:NumLines() > 0 or false
    end

    ns._WorldQuestsTryPopulateQuestRewardTooltip = function(setter, ...)
        if type(setter) ~= "function" then
            return false
        end

        if WOW.GameTooltip.ClearLines then
            WOW.GameTooltip:ClearLines()
        end

        local ok = pcall(setter, WOW.GameTooltip, ...)
        if not ok then
            if WOW.GameTooltip.ClearLines then
                WOW.GameTooltip:ClearLines()
            end
            return false
        end

        if TooltipHasContent(WOW.GameTooltip) then
            return true
        end

        if WOW.GameTooltip.ClearLines then
            WOW.GameTooltip:ClearLines()
        end
        return false
    end
end

-- =============================================
-- Quest tooltip helper
-- =============================================

-- Maps Enum.WidgetEnabledState to r, g, b tooltip line colors, mirroring
-- Blizzard's GetTextColorForEnabledState in Blizzard_UIWidgetTemplateBase.lua.
-- This is what produces red unlock text, gray timer text, etc. on POI tooltips.
local function GetWidgetColorForEnabledState(enabledState)
    local e = Enum and Enum.WidgetEnabledState
    -- Numeric fallbacks mirror the enum order in case Enum is nil/partial.
    local isDisabled  = e and (enabledState == e.Disabled)  or (enabledState == 1)
    local isRed       = e and (enabledState == e.Red)       or (enabledState == 2)
    local isWhite     = e and (enabledState == e.White)     or (enabledState == 3)
    local isGreen     = e and (enabledState == e.Green)     or (enabledState == 4)
    local isArtifact  = e and (enabledState == e.Artifact)  or (enabledState == 5)
    local isBlack     = e and (enabledState == e.Black)     or (enabledState == 6)
    local isBrightBlue = e and (enabledState == e.BrightBlue) or (enabledState == 7)

    if isDisabled then
        local c = rawget(_G, "DISABLED_FONT_COLOR")
        return c and c:GetRGB() or 0.50, 0.50, 0.50
    elseif isRed then
        local c = rawget(_G, "RED_FONT_COLOR")
        return c and c:GetRGB() or 1.00, 0.10, 0.10
    elseif isWhite then
        local c = rawget(_G, "HIGHLIGHT_FONT_COLOR")
        return c and c:GetRGB() or 1.00, 1.00, 1.00
    elseif isGreen then
        local c = rawget(_G, "GREEN_FONT_COLOR")
        return c and c:GetRGB() or 0.10, 1.00, 0.10
    elseif isArtifact then
        local c = rawget(_G, "ARTIFACT_GOLD_COLOR")
        return c and c:GetRGB() or 0.90, 0.80, 0.50
    elseif isBlack then
        return 0.00, 0.00, 0.00
    elseif isBrightBlue then
        local c = rawget(_G, "BRIGHTBLUE_FONT_COLOR")
        return c and c:GetRGB() or 0.28, 0.80, 0.95
    end
    -- Enum.WidgetEnabledState.Normal (0) / unset → NORMAL_FONT_COLOR (yellow-gold)
    local c = rawget(_G, "NORMAL_FONT_COLOR")
    return c and c:GetRGB() or 1.00, 0.82, 0.00
end

-- Extracts the visible line of text and its proper display color from a
-- locked Area POI widget, using ONLY the read-only C_UIWidgetManager
-- visualization-info getters (no frame creation, no taint).
-- Returns text, r, g, b  — or nil when there is nothing to show.
local function GetAreaPOIWidgetTooltipLine(widgetInfo)
    if not widgetInfo or not widgetInfo.widgetID or not C_UIWidgetManager then
        return nil
    end

    local widgetID   = widgetInfo.widgetID
    local widgetType = widgetInfo.widgetType

    if widgetType == 2 and C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo then
        local info = C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo(widgetID)
        if info then
            -- overrideBarText is the numeric progress label; use normal color.
            if info.overrideBarText and info.overrideBarText ~= "" then
                local r, g, b = GetWidgetColorForEnabledState(nil)
                return NormalizeInlineQuestText(info.overrideBarText), r, g, b
            end
            -- info.text is the widget label; color from textEnabledState.
            if info.text and info.text ~= "" then
                local r, g, b = GetWidgetColorForEnabledState(info.textEnabledState)
                return NormalizeInlineQuestText(info.text), r, g, b
            end
        end

    elseif widgetType == 8
        and C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo
    then
        local info = C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo(widgetID)
        if info and info.text and info.text ~= "" then
            -- enabledState drives the color: Red = locked requirement, Disabled = timer, etc.
            local r, g, b = GetWidgetColorForEnabledState(info.enabledState)
            return NormalizeInlineQuestText(info.text), r, g, b
        end

    elseif widgetType == 7
        and C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo
    then
        local info = C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo(widgetID)
        if info then
            local parts = {}
            if info.text and info.text ~= "" then
                parts[#parts + 1] = info.text
            end
            if info.currencies then
                for _, currency in ipairs(info.currencies) do
                    local ct = currency.text or currency.leadingText
                    if ct and ct ~= "" then
                        parts[#parts + 1] = ct
                    end
                end
            end
            if #parts > 0 then
                local r, g, b = GetWidgetColorForEnabledState(info.enabledState)
                return NormalizeInlineQuestText(table.concat(parts, " ")), r, g, b
            end
        end

    elseif widgetType == 9
        and C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo
    then
        local info = C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo(widgetID)
        if info then
            local parts = {}
            local infoText = info["text"]
            if infoText and infoText ~= "" then
                parts[#parts + 1] = infoText
            end
            if info.currencies then
                for _, currency in ipairs(info.currencies) do
                    local ct = currency.text or currency.leadingText
                    if ct and ct ~= "" then
                        parts[#parts + 1] = ct
                    end
                end
            end
            if #parts > 0 then
                local r, g, b = GetWidgetColorForEnabledState(info["enabledState"])
                return NormalizeInlineQuestText(table.concat(parts, " ")), r, g, b
            end
        end

    elseif widgetType == 13
        and C_UIWidgetManager.GetSpellDisplayVisualizationInfo
    then
        local info = C_UIWidgetManager.GetSpellDisplayVisualizationInfo(widgetID)
        if info and info.spellInfo
            and info.spellInfo.text and info.spellInfo.text ~= ""
        then
            local r, g, b = GetWidgetColorForEnabledState(nil)
            return NormalizeInlineQuestText(info.spellInfo.text), r, g, b
        end

    elseif widgetType == 27
        and C_UIWidgetManager.GetItemDisplayVisualizationInfo
    then
        local info = C_UIWidgetManager.GetItemDisplayVisualizationInfo(widgetID)
        if info and info.itemInfo then
            local text
            if info.itemInfo.overrideItemName and info.itemInfo.overrideItemName ~= "" then
                text = info.itemInfo.overrideItemName
            elseif info.itemInfo.itemID
                and WOW.C_Item and WOW.C_Item.GetItemNameByID
            then
                local name = WOW.C_Item.GetItemNameByID(info.itemInfo.itemID)
                if name and name ~= "" then
                    text = name
                end
            end
            if not text and info.itemInfo.infoText and info.itemInfo.infoText ~= "" then
                text = info.itemInfo.infoText
            end
            if text then
                -- Item names use highlight (white) color like Blizzard item widgets.
                local r, g, b = GetWidgetColorForEnabledState(
                    (Enum and Enum.WidgetEnabledState and Enum.WidgetEnabledState.White) or 3)
                return NormalizeInlineQuestText(text), r, g, b
            end
        end
    end

    return nil
end

-- Returns true if a widget from GetAllWidgetsBySetID should be shown.
-- Mirrors Blizzard's IsAreaPOIWidgetShown global, with a compat fallback.
local function IsAreaPOIWidgetShownSafe(widgetInfo)
    local globalFn = _G and rawget(_G, "IsAreaPOIWidgetShown")
    if type(globalFn) == "function" then
        return globalFn(widgetInfo)
    end
    local shownState = widgetInfo and widgetInfo.shownState
    return shownState ~= false and shownState ~= 0
end

-- If we cannot resolve C_QuestLog's bounty lock quest, item widgets may list every
-- reward tier (e.g. Adventurer + Veteran). Prefer higher tier when quality ties.
local function AppendSingleBestItemDisplayFromWidgetSet(tooltip, widgetSetID)
    if not tooltip
        or not widgetSetID or widgetSetID <= 0
        or not C_UIWidgetManager
        or not C_UIWidgetManager.GetAllWidgetsBySetID
        or not C_UIWidgetManager.GetItemDisplayVisualizationInfo
    then
        return false
    end

    local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID)
    if not widgets or #widgets == 0 then
        return false
    end

    local candidates = {}
    for _, widgetInfo in ipairs(widgets) do
        if IsAreaPOIWidgetShownSafe(widgetInfo) and widgetInfo.widgetType == 27 then
            local info = C_UIWidgetManager.GetItemDisplayVisualizationInfo(widgetInfo.widgetID)
            if info and info.itemInfo and info.itemInfo.itemID then
                local itemID = info.itemInfo.itemID
                local quality = 0
                if C_Item and C_Item.GetItemQualityByID then
                    local q = C_Item.GetItemQualityByID(itemID)
                    quality = type(q) == "number" and q or 0
                end
                local text
                if info.itemInfo.overrideItemName and info.itemInfo.overrideItemName ~= "" then
                    text = info.itemInfo.overrideItemName
                elseif WOW.C_Item and WOW.C_Item.GetItemNameByID then
                    text = WOW.C_Item.GetItemNameByID(itemID)
                end
                text = text and NormalizeInlineQuestText(text) or nil
                if text then
                    candidates[#candidates + 1] = {
                        itemID = itemID,
                        quality = quality,
                        text = text,
                    }
                end
            end
        end
    end

    if #candidates == 0 then
        return false
    end

    WOW.table_sort(candidates, function(a, b)
        return WeeklyParallelRewardItemIsBetterForDisplay(a, b)
    end)

    local best = candidates[1]
    local r, g, b = 1, 1, 1
    if best.quality
        and ITEM_QUALITY_COLORS
        and ITEM_QUALITY_COLORS[best.quality]
    then
        local qc = ITEM_QUALITY_COLORS[best.quality]
        r, g, b = qc.r, qc.g, qc.b
    end
    tooltip:AddLine(" ")
    tooltip:AddLine(best.text, r, g, b)
    return true
end

local function FindFirstTimeLikeTooltipLineInWidgetSet(widgetSetID)
    if not widgetSetID or widgetSetID <= 0 or not C_UIWidgetManager then
        return nil
    end

    local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID)
    if not widgets or #widgets == 0 then
        return nil
    end

    local ordered = {}
    for _, w in ipairs(widgets) do
        ordered[#ordered + 1] = w
    end
    WOW.table_sort(ordered, function(a, b)
        local ao = a.orderIndex or 0
        local bo = b.orderIndex or 0
        if ao ~= bo then return ao < bo end
        return (a.widgetID or 0) < (b.widgetID or 0)
    end)

    for _, widgetInfo in ipairs(ordered) do
        if IsAreaPOIWidgetShownSafe(widgetInfo) then
            local lineText = GetAreaPOIWidgetTooltipLine(widgetInfo)
            if lineText and lineText ~= ""
                and eventFrame:IsAreaPOITimeText(lineText)
            then
                return lineText
            end
        end
    end

    return nil
end

-- Blizzard timer widgets often emit "Time Left: 4 Days 6 Hours" in one color;
-- split label (NORMAL yellow) and duration (white).
local function AddTooltipStyledTimeLeftFromWidgetLine(lineText)
    local raw = NormalizeInlineQuestText(lineText)
    if not raw or raw == "" then
        return
    end

    local colonPos = WOW.string_find(raw, ":")
    if not colonPos then
        return
    end

    local head = WOW.string_sub(raw, 1, colonPos)
    local tail = WOW.string_gsub(WOW.string_sub(raw, colonPos + 1), "^%s+", "")
    local low = WOW.string_lower(head)
    if not WOW.string_find(low, "time") or not WOW.string_find(low, "left") then
        return
    end

    local nf = rawget(_G, "NORMAL_FONT_COLOR")
    local label = head
    if nf and nf.WrapTextInColorCode then
        label = nf:WrapTextInColorCode(head)
    end

    WOW.GameTooltip:AddLine(label .. "|cffffffff " .. tail .. "|r", 1, 1, 1, true)
end

-- Taint-safe replacement for GameTooltip_AddWidgetSet on our insecure hover
-- path. Reads widget visualization data (no frame creation), then adds
-- properly-colored tooltip lines that match Blizzard's native POI tooltip.
local function AppendAreaPOIWidgetSetToTooltip(tooltip, widgetSetID, filterOpts)
    if not tooltip
        or not widgetSetID or widgetSetID <= 0
        or not C_UIWidgetManager
        or not C_UIWidgetManager.GetAllWidgetsBySetID
    then
        return false
    end

    local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID)
    if not widgets or #widgets == 0 then
        return false
    end

    local ordered = {}
    for _, widgetInfo in ipairs(widgets) do
        ordered[#ordered + 1] = widgetInfo
    end
    WOW.table_sort(ordered, function(a, b)
        local ao = a.orderIndex or 0
        local bo = b.orderIndex or 0
        if ao ~= bo then return ao < bo end
        return (a.widgetID or 0) < (b.widgetID or 0)
    end)

    local addedAny = false
    for _, widgetInfo in ipairs(ordered) do
        if IsAreaPOIWidgetShownSafe(widgetInfo) then
            local lineText, r, g, b = GetAreaPOIWidgetTooltipLine(widgetInfo)
            if lineText and lineText ~= "" then
                local skip = false
                if filterOpts then
                    local skipTypes = filterOpts.skipWidgetTypes
                    if skipTypes and skipTypes[widgetInfo.widgetType] then
                        skip = true
                    elseif filterOpts.skipTimeLikeLines
                        and eventFrame:IsAreaPOITimeText(lineText)
                    then
                        skip = true
                    end
                end
                if not skip then
                    tooltip:AddLine(lineText, r or 1, g or 1, b or 1, true)
                    addedAny = true
                end
            end
        end
    end

    return addedAny
end

local function GetTooltipNormalYellowRGB()
    local c = rawget(_G, "NORMAL_FONT_COLOR")
    if c and c.GetRGB then
        return c:GetRGB()
    end
    return 1.00, 0.82, 0.00
end

-- Faction line: Blizzard "normal" yellow when reputation still matters; grey when capped.
local function AddTooltipFactionLinesForQuest(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return
    end

    local yr, yg, yb = GetTooltipNormalYellowRGB()
    local addedFromTask = false

    local factionID
    if WOW.C_TaskQuest and WOW.C_TaskQuest.GetQuestInfoByQuestID then
        _, factionID = WOW.C_TaskQuest.GetQuestInfoByQuestID(questID)
    end

    if factionID and factionID > 0
        and WOW.C_Reputation and WOW.C_Reputation.GetFactionDataByID
    then
        local factionData = WOW.C_Reputation.GetFactionDataByID(factionID)
        if factionData and factionData.name then
            local isCapped = false
            if WOW.C_QuestLog and WOW.C_QuestLog.DoesQuestAwardReputationWithFaction then
                isCapped = not WOW.C_QuestLog.DoesQuestAwardReputationWithFaction(
                    questID, factionID)
            end
            if not isCapped
                and WOW.C_Reputation.IsFactionParagonForCurrentPlayer
            then
                isCapped = WOW.C_Reputation.IsFactionParagonForCurrentPlayer(factionID)
            end
            if isCapped then
                WOW.GameTooltip:AddLine(factionData.name, 0.50, 0.50, 0.50)
            else
                WOW.GameTooltip:AddLine(factionData.name, yr, yg, yb)
            end
            addedFromTask = true
        end
    end

    if addedFromTask then
        return
    end

    local label = GetQuestFactionLabel(questID)
    if label and label ~= "" then
        WOW.GameTooltip:AddLine(label, yr, yg, yb)
    end
end

-- "Time Left:" in normal yellow; duration in white (list tooltip convention).
local function AddTooltipTimeLeftLineForQuest(questID, questEntry)
    if type(questID) ~= "number" or questID <= 0 then
        return
    end

    local secs = GetQuestExpirySnapshot(
        questID,
        GetCurrentServerTime(),
        questEntry)
    if not secs or secs <= 0 then
        return
    end

    local nf = rawget(_G, "NORMAL_FONT_COLOR")
    local label = "Time Left: "
    local prefix = label
    if nf and nf.WrapTextInColorCode then
        prefix = nf:WrapTextInColorCode(label)
    end
    WOW.GameTooltip:AddLine(prefix .. FormatTimeLeft(secs), 1, 1, 1, true)
end

local function AddTooltipTimeLeftSecondsLine(secs)
    if not secs or secs <= 0 then
        return
    end

    local nf = rawget(_G, "NORMAL_FONT_COLOR")
    local label = "Time Left: "
    local prefix = label
    if nf and nf.WrapTextInColorCode then
        prefix = nf:WrapTextInColorCode(label)
    end
    WOW.GameTooltip:AddLine(prefix .. FormatTimeLeft(secs), 1, 1, 1, true)
end

-- Taint-safe quest reward lines (same data as list WQ tooltips).
local function AppendTooltipQuestRewards(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return
    end

    if WOW.HaveQuestRewardData and not WOW.HaveQuestRewardData(questID) then
        rewardPreloadState.QueueQuestRewardPreload(questID, true)
        WOW.GameTooltip:AddLine(" ")
        WOW.GameTooltip:AddLine(RETRIEVING_DATA or "Loading rewards...", 1, 0.82, 0)
        return
    end

    local rewards = GetQuestRewards(questID)
    if not rewards or #rewards == 0 then
        return
    end

    WOW.GameTooltip:AddLine(" ")
    for _, reward in ipairs(rewards) do
        if reward.rewardType == "item" then
            local r, g, b = 1, 1, 1
            if reward.quality
                and ITEM_QUALITY_COLORS
                and ITEM_QUALITY_COLORS[reward.quality]
            then
                local qc = ITEM_QUALITY_COLORS[reward.quality]
                r, g, b = qc.r, qc.g, qc.b
            end
            local text = reward.label or "Unknown"
            if reward.amount and reward.amount > 1 then
                text = text .. " x" .. reward.amount
            end
            if reward.ilvl then
                text = text .. " (" .. reward.ilvl .. ")"
            end
            WOW.GameTooltip:AddLine(text, r, g, b)
        elseif reward.rewardType == "currency" then
            local icon = reward.icon
            local text
            if icon then
                text = "|T" .. icon .. ":12:12:0:0|t "
                    .. (reward.amount or "")
                    .. " " .. (reward.label or "")
            else
                text = (reward.amount and (reward.amount .. " ") or "")
                    .. (reward.label or "")
            end
            WOW.GameTooltip:AddLine(text, 1, 1, 1)
        elseif reward.rewardType == "rep" then
            local icon = reward.icon
            local text
            if icon then
                text = "|T" .. icon .. ":12:12:0:0|t +"
                    .. (reward.amount or "")
                    .. " " .. (reward.label or "")
            else
                text = "+" .. (reward.amount or "")
                    .. " " .. (reward.label or "")
            end
            WOW.GameTooltip:AddLine(text, 0.70, 0.78, 0.88)
        elseif reward.rewardType == "gold" then
            WOW.GameTooltip:AddLine(reward.label or "", 1, 0.82, 0)
        end
    end
end

-- Shows a world-quest tooltip anchored to anchorFrame.
-- Mirrors Blizzard's TaskPOI_OnEnter -> GameTooltip_AddQuest flow:
--   core-ready gate -> quality-colored title -> faction -> time -> objectives
--   -> GameTooltip_AddQuestRewardsToTooltip for full reward display.
local function ShowQuestTooltip(anchorFrame, questID, showTrackHint)
    local row = anchorFrame
    if row and row.GetParent and not row.questEntry then
        local parent = row:GetParent()
        if parent and parent.questEntry then
            row = parent
        end
    end

    local tooltipOwner = row or anchorFrame
    if not tooltipOwner then
        return
    end

    -- Phase 1 layout sets questID/mapID before Phase 2 assigns questEntry; fall back
    -- to the cached entry table so bounty resolution works on first hover.
    local questEntry = row.questEntry
        or (questID and GetExistingQuestEntry(questID))
        or nil
    local isLockedAreaPOIRow = questEntry
        and questEntry.isAreaPOI == true
        and questEntry.isLocked == true

    -- Synthetic id for list bookkeeping; prefer the entry when the row is a locked POI.
    local tooltipQuestID = questID
    if isLockedAreaPOIRow and questEntry then
        tooltipQuestID = questEntry._syntheticID or questEntry.questID or questID
    end

    activeTooltipAnchor = tooltipOwner
    activeTooltipQuestID = tooltipQuestID
    activeTooltipShowTrackHint = showTrackHint == true
    WOW.GameTooltip:SetOwner(tooltipOwner, "ANCHOR_RIGHT")
    if WOW.GameTooltip.ClearLines then
        WOW.GameTooltip:ClearLines()
    end

    if isLockedAreaPOIRow or (questID and questID < 0) then
        -- Locked Area POI path (entry flags +/or synthetic negative quest id).
        local poiInfo = questEntry and questEntry.areaPOIInfo or nil

        local title = (poiInfo and poiInfo.name)
            or (row and row.poiTitle)
            or "Special Assignment"
        GameTooltip_SetTitle(WOW.GameTooltip, title, HIGHLIGHT_FONT_COLOR)

        local description = poiInfo and poiInfo.description
        if description and description ~= "" then
            GameTooltip_AddNormalLine(WOW.GameTooltip, description, true)
        end

        local lockQuestID = ResolveBountyLockQuestIDFromQuestEntry(questEntry)

        if lockQuestID then
            AddTooltipFactionLinesForQuest(lockQuestID)
        end

        -- Always suppress raw item-display widgets when we replace rewards below.
        local widgetFilter = { skipWidgetTypes = { [27] = true } }
        if lockQuestID then
            local secs = GetQuestExpirySnapshot(
                lockQuestID,
                GetCurrentServerTime(),
                questEntry)
            if secs and secs > 0 then
                AddTooltipTimeLeftLineForQuest(lockQuestID, questEntry)
                widgetFilter.skipTimeLikeLines = true
            end
        else
            local poiSec = questEntry and questEntry.poiID
                and TryGetAreaPOISecondsLeft(questEntry.poiID)
            if poiSec and poiSec > 0 then
                AddTooltipTimeLeftSecondsLine(poiSec)
                widgetFilter.skipTimeLikeLines = true
            end
        end

        local widgetSetID = (poiInfo and poiInfo.tooltipWidgetSet)
            or (questEntry and questEntry.tooltipWidgetSet)
            or nil

        -- No C_TaskQuest/POI seconds: restyle the widget timer line (label yellow, value white).
        if not widgetFilter.skipTimeLikeLines and widgetSetID and widgetSetID > 0 then
            local timeLine = FindFirstTimeLikeTooltipLineInWidgetSet(widgetSetID)
            if timeLine then
                AddTooltipStyledTimeLeftFromWidgetLine(timeLine)
                widgetFilter.skipTimeLikeLines = true
            end
        end

        AppendAreaPOIWidgetSetToTooltip(WOW.GameTooltip, widgetSetID, widgetFilter)

        if lockQuestID then
            AppendTooltipQuestRewards(lockQuestID)
        elseif widgetSetID and widgetSetID > 0 then
            AppendSingleBestItemDisplayFromWidgetSet(WOW.GameTooltip, widgetSetID)
        end

        WOW.GameTooltip:Show()
        return
    end

    -- Gate: show "Retrieving data" until the quest has real core metadata.
    if not eventFrame:IsQuestCoreDataReady(questID) then
        eventFrame:QueueQuestCoreDataLoad(questID)
        WOW.GameTooltip:AddLine(RETRIEVING_DATA or "Retrieving data\226\128\166",
            1, 0, 0)
        WOW.GameTooltip:Show()
        return
    end

    -- Title from the authoritative task quest API.
    local title = select(1, WOW.C_TaskQuest.GetQuestInfoByQuestID(questID))
    title = title or GetQuestTitle(questID)
    if not title or title == "" then
        eventFrame:QueueQuestCoreDataLoad(questID)
        WOW.GameTooltip:AddLine(RETRIEVING_DATA or "Retrieving data\226\128\166",
            1, 0, 0)
        WOW.GameTooltip:Show()
        return
    end

    -- Quality-colored title for world quests (mirrors GameTooltip_AddQuest).
    local titleR, titleG, titleB = 1, 1, 1
    if WOW.C_QuestLog and WOW.C_QuestLog.GetQuestTagInfo then
        local tagInfo = WOW.C_QuestLog.GetQuestTagInfo(questID)
        if tagInfo and tagInfo.quality then
            local color = WORLD_QUEST_QUALITY_COLORS
                and WORLD_QUEST_QUALITY_COLORS[tagInfo.quality]
            if color then
                titleR, titleG, titleB = color.r, color.g, color.b
            end
        end
    end
    WOW.GameTooltip:AddLine(title, titleR, titleG, titleB, true)

    AddTooltipFactionLinesForQuest(questID)

    AddTooltipTimeLeftLineForQuest(questID, row and row.questEntry or nil)

    -- Objectives
    local numObj = WOW.C_QuestLog and WOW.C_QuestLog.GetNumQuestObjectives
        and WOW.C_QuestLog.GetNumQuestObjectives(questID) or 0
    if numObj and numObj > 0 then
        for i = 1, numObj do
            local objText, _, finished
            if WOW.GetQuestObjectiveInfo then
                objText, _, finished = WOW.GetQuestObjectiveInfo(questID, i, false)
            end
            if objText and #objText > 0 then
                local r, g, b = finished and 0.5 or 1,
                                finished and 0.5 or 1,
                                finished and 0.5 or 1
                WOW.GameTooltip:AddLine(
                    (QUEST_DASH or "- ") .. objText, r, g, b, true)
            end
        end
    end

    -- Reward details built from C_QuestLog APIs only (no MoneyFrame /
    -- EmbeddedItemTooltip).  Calling GameTooltip_AddQuestRewardsToTooltip from
    -- addon code taints Blizzard's MoneyFrame and EmbeddedItemTooltip frames,
    -- causing "secret number" arithmetic errors on every hover.  GetQuestRewards
    -- returns the same data through safe, non-secure paths so no taint occurs.
    AppendTooltipQuestRewards(questID)

    -- Optional tracking hint
    if showTrackHint then
        local cur = WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedQuestID
            and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil
        WOW.GameTooltip:AddLine(" ")
        WOW.GameTooltip:AddLine(
            cur == questID and "Click to stop tracking" or "Click to track",
            0.60, 0.60, 0.60)
    end
    WOW.GameTooltip:Show()
end

function eventFrame:RefreshActiveQuestTooltipIfReady(questID)
    if not activeTooltipAnchor or not activeTooltipAnchor:IsShown() then
        return
    end

    local row = activeTooltipAnchor
    if row.GetParent and not row.questEntry then
        local parent = row:GetParent()
        if parent and parent.questEntry then
            row = parent
        end
    end

    -- Locked Area POI rows use a synthetic negative questID; bounty metadata
    -- (rewards, faction, timer) comes from the positive lock quest ID.
    local questEntry = row.questEntry
        or (activeTooltipQuestID and GetExistingQuestEntry(activeTooltipQuestID))
        or nil
    local isLockedAreaPOIHover = questEntry
        and questEntry.isAreaPOI == true
        and questEntry.isLocked == true
    if isLockedAreaPOIHover or (activeTooltipQuestID and activeTooltipQuestID < 0) then
        local lockQuestID = ResolveBountyLockQuestIDFromQuestEntry(questEntry)
        if not lockQuestID or lockQuestID ~= questID then
            return
        end
        ShowQuestTooltip(activeTooltipAnchor, activeTooltipQuestID, activeTooltipShowTrackHint)
        return
    end

    if activeTooltipQuestID ~= questID then
        return
    end

    if not self:IsQuestCoreDataReady(questID) then
        return
    end

    ShowQuestTooltip(activeTooltipAnchor, questID, activeTooltipShowTrackHint)
end

local AcquireQuestRow
local ReleaseQuestRow
local AcquireZoneHeader
local ReleaseZoneHeader
local UpdateQuestRowTimeDisplay
local GetQuestRowHeight
local ApplyQuestRowLayout
local ResetQuestRowRewards
local ApplyQuestRewardListToRow
local ApplyQuestRowTitleColor
local IsLockedSpecialAssignmentEntry
local HasVisibleLockedSpecialAssignmentUnlock

do

-- =============================================
-- Frame pooling helpers
-- =============================================

-- Returns an inactive quest row frame or creates a new one.
-- parent: Frame
AcquireQuestRow = function(parent)
    local row = table.remove(questRowPool)
    if row then
        row:SetParent(parent)
        row:ClearAllPoints()
        if row.fadeIn then
            row.fadeIn:Stop()
        end
        row:SetAlpha(1)
        ApplyWorldQuestTypographyToRow(row)
        -- Do NOT show here; PopulateQuestRow calls row:Show() once all content
        -- has been written.  Showing an empty recycled frame causes a blank-row
        -- flash between Phase 1 (frame placement) and Phase 2 (data fill-in).
        return row
    end

    -- Create a new row frame.  Layout: 3 visual lines in K.ROW_HEIGHT pixels.
    -- Line 1 (top):   [Title text ...]              [â— time text]
    -- Line 2 (mid):   [Faction / type text]
    -- Line 3 (bot):   [Reward icons x4 with amount overlays]
    row = WOW.CreateFrame("Button", nil, parent)
    row:SetHeight(K.ROW_HEIGHT)
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    -- Hover background
    local hover = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    hover:SetAllPoints()
    hover:SetColorTexture(COL.rowHover[1], COL.rowHover[2], COL.rowHover[3], COL.rowHover[4])
    hover:Hide()
    row.hoverBg = hover

    -- POI tracking button.  Use the native Blizzard template
    -- ObjectiveTrackerPOIButtonTemplate (defined in Blizzard_ObjectiveTracker,
    -- inherits POIButtonTemplate).  Creating a frame with this template
    -- pre-builds all XML children (NormalTexture etc.) that POIButtonMixin's
    -- UpdateButtonStyle() requires, which is why plain WOW.CreateFrame without a
    -- template produces an invisible button.
    local poiBtn = WOW.CreateFrame("Button", nil, row, "ObjectiveTrackerPOIButtonTemplate")
    poiBtn:SetSize(K.POI_BUTTON_SIZE, K.POI_BUTTON_SIZE)
    poiBtn:SetPoint("LEFT", row, "LEFT", 8, 0)
    poiBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
    -- The C-level Button engine swaps NormalTextureâ†’PushedTexture before any Lua
    -- fires, causing the quest-type ring to briefly vanish on click.
    -- Fix: do the tracking toggle in OnMouseDown (which always fires), then
    -- immediately call SetButtonState("NORMAL") so the engine never renders the
    -- Toggle super-tracking on mouse-down so the action fires immediately and
    -- SetButtonState("NORMAL") suppresses the unwanted PUSHED texture swap.
    -- In WoW 12.0 SetButtonState no longer blocks the inherited OnClick from
    -- POIButtonMixin, so OnClick is explicitly cleared to prevent the Blizzard
    -- template from double-toggling tracking and playing unwanted sounds.
    poiBtn:SetScript("OnMouseDown", function(self)
        self:SetButtonState("NORMAL", false)
        if self.areaPoiID and self.areaPoiID > 0
            and WOW.C_SuperTrack and WOW.C_SuperTrack.SetSuperTrackedMapPin
        then
            local mapPinType = Enum and Enum.SuperTrackingMapPinType
                and Enum.SuperTrackingMapPinType.AreaPOI or 0
            local trackedType, trackedID
            if WOW.C_SuperTrack.GetSuperTrackedMapPin then
                trackedType, trackedID = WOW.C_SuperTrack.GetSuperTrackedMapPin()
            end
            if trackedType == mapPinType and trackedID == self.areaPoiID then
                if WOW.C_SuperTrack.ClearAllSuperTracked then
                    WOW.C_SuperTrack.ClearAllSuperTracked()
                end
            else
                WOW.C_SuperTrack.SetSuperTrackedMapPin(mapPinType, self.areaPoiID)
            end
            return
        end
        if not WOW.C_SuperTrack or not WOW.C_SuperTrack.SetSuperTrackedQuestID then return end
        local cur = WOW.C_SuperTrack.GetSuperTrackedQuestID
            and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil
        if not self.questID or self.questID <= 0 then return end
        if cur == self.questID then
            WOW.C_SuperTrack.SetSuperTrackedQuestID(0)
        else
            WOW.C_SuperTrack.SetSuperTrackedQuestID(self.questID)
        end
    end)
    poiBtn:SetScript("OnMouseUp", nil)
    poiBtn:SetScript("OnClick",    nil)
    poiBtn:SetScript("OnEnter", function(self)
        -- Fall back to the row's questID when poiBtn.questID has been cleared on release
        -- but Phase 2 hasn't re-populated it yet (pool LIFO shuffle can leave stale IDs).
        local rowParent = self:GetParent()
        local qid = self.questID or (rowParent and rowParent.questID) or nil
        local rowHovered = rowParent and rowParent.IsMouseOver and rowParent:IsMouseOver() or false
        local poiHovered = self.IsMouseOver and self:IsMouseOver() or false
        local contractBtn = rowParent and rowParent.contractBtn or nil
        local contractHovered = contractBtn
            and contractBtn.IsShown
            and contractBtn:IsShown()
            and contractBtn.IsMouseOver
            and contractBtn:IsMouseOver()
            or false
        local rewardHovered = false
        local rewardIcons = rowParent and rowParent.rewardIcons or nil
        if rewardIcons then
            for iconIndex = 1, #rewardIcons do
                local rewardButton = rewardIcons[iconIndex]
                if rewardButton
                    and rewardButton.IsShown
                    and rewardButton:IsShown()
                    and rewardButton.IsMouseOver
                    and rewardButton:IsMouseOver()
                then
                    rewardHovered = true
                    break
                end
            end
        end
        eventFrame._DebugHoverTrace(
            "POI.OnEnter",
            "questID=%s rowHovered=%s poiHovered=%s contractHovered=%s rewardHovered=%s call=StartOrResume",
            tostring(qid),
            tostring(rowHovered),
            tostring(poiHovered),
            tostring(contractHovered),
            tostring(rewardHovered))
        if qid then
            ShowQuestTooltip(rowParent or self, qid, true)
            activeHoverState.StartOrResume(rowParent, qid, true, "poi-onenter", "tooltip")
        end
    end)
    poiBtn:SetScript("OnLeave", function(self)
        local rowParent = self:GetParent()
        local qid = self.questID or (rowParent and rowParent.questID) or nil
        local rowHovered = rowParent and rowParent.IsMouseOver and rowParent:IsMouseOver() or false
        local poiHovered = self.IsMouseOver and self:IsMouseOver() or false
        local contractBtn = rowParent and rowParent.contractBtn or nil
        local contractHovered = contractBtn
            and contractBtn.IsShown
            and contractBtn:IsShown()
            and contractBtn.IsMouseOver
            and contractBtn:IsMouseOver()
            or false
        local rewardHovered = false
        local rewardIcons = rowParent and rowParent.rewardIcons or nil
        if rewardIcons then
            for iconIndex = 1, #rewardIcons do
                local rewardButton = rewardIcons[iconIndex]
                if rewardButton
                    and rewardButton.IsShown
                    and rewardButton:IsShown()
                    and rewardButton.IsMouseOver
                    and rewardButton:IsMouseOver()
                then
                    rewardHovered = true
                    break
                end
            end
        end
        eventFrame._DebugHoverTrace(
            "POI.OnLeave",
            "questID=%s rowHovered=%s poiHovered=%s contractHovered=%s rewardHovered=%s call=DeferStop",
            tostring(qid),
            tostring(rowHovered),
            tostring(poiHovered),
            tostring(contractHovered),
            tostring(rewardHovered))
        ClearActiveQuestTooltip(self)
        activeHoverState.DeferStop(qid, "poi-onleave")
    end)
    row.poiBtn = poiBtn
    local areaPoiIcon = poiBtn:CreateTexture(nil, "ARTWORK", nil, 1)
    areaPoiIcon:SetAllPoints()
    areaPoiIcon:Hide()
    if not eventFrame._poiButtonAreaPOIIcons then
        eventFrame._poiButtonAreaPOIIcons = setmetatable({}, { __mode = "k" })
    end
    eventFrame._poiButtonAreaPOIIcons[poiBtn] = areaPoiIcon

    -- Title (top line, left) â€” left margin accounts for POI button + gap; right for time
    local titleText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    titleText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -9)
    titleText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -70, -9)
    titleText:SetJustifyH("LEFT")
    titleText:SetWordWrap(false)
    titleText:SetTextColor(1, 0.82, 0)
    row.titleText = titleText

    -- Time remaining (top line, right-aligned)
    local timeText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    timeText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -21, -12)
    timeText:SetJustifyH("RIGHT")
    timeText:SetTextColor(COL.rowTime[1], COL.rowTime[2], COL.rowTime[3])
    row.timeText = timeText

    -- Expiry dot â€” circular, placed to the right of the time text
    local dot = row:CreateTexture(nil, "ARTWORK")
    dot:SetSize(9, 9)
    dot:SetPoint("LEFT", timeText, "RIGHT", 4, 0)
    dot:SetTexture("Interface\\Buttons\\WHITE8x8")
    -- Circular mask so the square texture appears as a solid disc
    local dotMask = row:CreateMaskTexture(nil, "ARTWORK")
    dotMask:SetAllPoints(dot)
    dotMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask",
                       "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    dot:AddMaskTexture(dotMask)
    row.timeDot = dot

    -- Faction / quest-type label (second line)
    local factionText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    factionText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -23)
    factionText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, -23)
    factionText:SetJustifyH("LEFT")
    factionText:SetWordWrap(false)
    factionText:SetTextColor(COL.rowFaction[1], COL.rowFaction[2], COL.rowFaction[3])
    row.factionText = factionText

    local contractBtn = WOW.CreateFrame("Button", nil, row)
    contractBtn:SetSize(K.CONTRACT_ICON_SIZE, K.CONTRACT_ICON_SIZE)
    contractBtn:Hide()

    local contractIcon = contractBtn:CreateTexture(nil, "ARTWORK")
    contractIcon:SetAllPoints()
    contractIcon:SetTexture("Interface\\AddOns\\NomTools\\media\\Contract_Icon_No_BG.png")
    contractBtn.icon = contractIcon

    contractBtn:SetScript("OnEnter", function(self)
        local rowParent = self:GetParent()
        local qid = self.questID or (rowParent and rowParent.questID) or nil
        local setAuraTooltip = WOW.GameTooltip["SetUnitAuraByAuraInstanceID"]
            or WOW.GameTooltip["SetUnitBuffByAuraInstanceID"]
        WOW.GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if self.auraInstanceID and setAuraTooltip then
            setAuraTooltip(WOW.GameTooltip, "player", self.auraInstanceID)
        elseif self.spellID and WOW.GameTooltip.SetSpellByID then
            WOW.GameTooltip:SetSpellByID(self.spellID)
        end
        WOW.GameTooltip:Show()
        if qid then
            activeHoverState.StartOrResume(rowParent, qid, false, "contract-onenter", "tooltip")
        end
    end)
    contractBtn:SetScript("OnLeave", function(self)
        local rowParent = self:GetParent()
        local qid = self.questID or (rowParent and rowParent.questID) or nil
        WOW.GameTooltip:Hide()
        activeHoverState.DeferStop(qid, "contract-onleave")
    end)
    row.contractBtn = contractBtn

    -- Reward icons (bottom line, left-aligned)
    local rewardIcons = {}
    for iconIdx = 1, K.MAX_REWARD_ICONS do
        local btn = WOW.CreateFrame("Button", nil, row)
        btn:SetSize(K.REWARD_ICON_SIZE, K.REWARD_ICON_SIZE)
        if iconIdx == 1 then
            btn:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -37)
        else
            btn:SetPoint("LEFT", rewardIcons[iconIdx - 1], "RIGHT", 3, 0)
        end

        local iconTex = btn:CreateTexture(nil, "ARTWORK")
        iconTex:SetAllPoints()
        iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        btn.iconTex = iconTex

        local countText = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
        countText:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 3, -1)
        countText:SetJustifyH("RIGHT")
        btn.countText = countText

        local borderTop = btn:CreateTexture(nil, "OVERLAY")
        borderTop:SetTexture("Interface\\Buttons\\WHITE8x8")
        borderTop:SetPoint("TOPLEFT",  btn, "TOPLEFT",  0, 0)
        borderTop:SetPoint("TOPRIGHT", btn, "TOPRIGHT", 0, 0)
        borderTop:SetHeight(1)
        borderTop:Hide()
        btn.borderTop = borderTop

        local borderBottom = btn:CreateTexture(nil, "OVERLAY")
        borderBottom:SetTexture("Interface\\Buttons\\WHITE8x8")
        borderBottom:SetPoint("BOTTOMLEFT",  btn, "BOTTOMLEFT",  0, 0)
        borderBottom:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0)
        borderBottom:SetHeight(1)
        borderBottom:Hide()
        btn.borderBottom = borderBottom

        local borderLeft = btn:CreateTexture(nil, "OVERLAY")
        borderLeft:SetTexture("Interface\\Buttons\\WHITE8x8")
        borderLeft:SetPoint("TOPLEFT",    btn, "TOPLEFT",    0, 0)
        borderLeft:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 0)
        borderLeft:SetWidth(1)
        borderLeft:Hide()
        btn.borderLeft = borderLeft

        local borderRight = btn:CreateTexture(nil, "OVERLAY")
        borderRight:SetTexture("Interface\\Buttons\\WHITE8x8")
        borderRight:SetPoint("TOPRIGHT",    btn, "TOPRIGHT",    0, 0)
        borderRight:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0)
        borderRight:SetWidth(1)
        borderRight:Hide()
        btn.borderRight = borderRight

        btn:SetScript("OnEnter", function(self)
            local parentRow = self:GetParent()
            local qid = self.questID or (parentRow and parentRow.questID) or nil
            if not self.rewardType then return end
            activeTooltipAnchor = nil
            activeTooltipQuestID = nil
            activeTooltipShowTrackHint = false
            WOW.GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if self.questID and not rewardPreloadState.IsQuestRewardDisplayReady(self.questID) then
                rewardPreloadState.QueueQuestRewardPreload(self.questID, true)
                WOW.GameTooltip:AddLine(RETRIEVING_DATA or "Loading rewards...", 1, 0.82, 0)
                WOW.GameTooltip:Show()
                if qid then
                    activeHoverState.StartOrResume(parentRow, qid, false, "reward-onenter", "reward-tooltip")
                end
                return
            end
            if self.rewardType == "item" then
                local usedQuestTooltip = false
                local canUseQuestRewardContext = self.questID
                    and self.rewardIndex
                    and self.questRewardType
                local setQuestLogItem = WOW.GameTooltip.SetQuestLogItem
                if setQuestLogItem and canUseQuestRewardContext then
                    usedQuestTooltip = ns._WorldQuestsTryPopulateQuestRewardTooltip(
                        setQuestLogItem,
                        self.questRewardType,
                        self.rewardIndex,
                        self.questID,
                        true)
                end
                if not usedQuestTooltip and canUseQuestRewardContext then
                    WOW.GameTooltip:SetText(self.label or (RETRIEVING_DATA or "Retrieving data..."), 1, 1, 1)
                    if self.ilvl and self.ilvl > 0 then
                        WOW.GameTooltip:AddLine("Item level " .. self.ilvl, 1, 1, 1)
                    end
                    WOW.GameTooltip:AddLine(RETRIEVING_DATA or "Reward details are still loading...", 1, 0.82, 0, true)
                elseif not usedQuestTooltip and self.itemID then
                    WOW.GameTooltip:SetHyperlink("item:" .. self.itemID)
                elseif not usedQuestTooltip then
                    WOW.GameTooltip:SetText(self.label or "", 1, 1, 1)
                    if self.ilvl and self.ilvl > 0 then
                        WOW.GameTooltip:AddLine("Item level " .. self.ilvl, 1, 1, 1)
                    end
                end
                if GameTooltip_ShowCompareItem
                    and (usedQuestTooltip or (not canUseQuestRewardContext and self.itemID))
                then
                    GameTooltip_ShowCompareItem(WOW.GameTooltip)
                end
            elseif self.rewardType == "rep" then
                ns.ShowReputationRewardTooltip(self, self.label, self.amount, self.currencyLabel)
                if qid then
                    activeHoverState.StartOrResume(parentRow, qid, false, "reward-onenter", "reward-tooltip")
                end
                return
            elseif self.rewardType == "currency" then
                local usedQuestTooltip = false
                local setQuestLogCurrency = WOW.GameTooltip.SetQuestLogCurrency or WOW.GameTooltip.SetQuestCurrency
                if setQuestLogCurrency and self.rewardIndex and self.questRewardType then
                    usedQuestTooltip = ns._WorldQuestsTryPopulateQuestRewardTooltip(
                        setQuestLogCurrency,
                        self.questRewardType,
                        self.rewardIndex,
                        self.questID)
                    if not usedQuestTooltip then
                        usedQuestTooltip = ns._WorldQuestsTryPopulateQuestRewardTooltip(
                            setQuestLogCurrency,
                            self.questRewardType,
                            self.rewardIndex)
                    end
                end
                if not usedQuestTooltip and self.currencyID and WOW.GameTooltip.SetCurrencyByID then
                    WOW.GameTooltip:SetCurrencyByID(self.currencyID)
                    local numericAmount = tonumber(self.amount)
                    if numericAmount and numericAmount ~= 0 then
                        local formattedAmount = BreakUpLargeNumbers
                            and BreakUpLargeNumbers(math.abs(numericAmount))
                            or tostring(math.abs(numericAmount))
                        WOW.GameTooltip:AddLine((REWARD or "Reward") .. ": " .. formattedAmount, 1, 1, 1)
                    end
                elseif not usedQuestTooltip then
                    WOW.GameTooltip:SetText(self.label or "", 1, 1, 1)
                    local numericAmount = tonumber(self.amount)
                    if numericAmount and numericAmount ~= 0 then
                        local formattedAmount = BreakUpLargeNumbers
                            and BreakUpLargeNumbers(math.abs(numericAmount))
                            or tostring(math.abs(numericAmount))
                        WOW.GameTooltip:AddLine((REWARD or "Reward") .. ": " .. formattedAmount, 1, 1, 1)
                    end
                end
            else
                WOW.GameTooltip:SetText(self.label or "", 1, 1, 1)
            end
            WOW.GameTooltip:Show()
            if qid then
                activeHoverState.StartOrResume(parentRow, qid, false, "reward-onenter", "reward-tooltip")
            end
        end)
        btn:SetScript("OnLeave", function(self)
            local parentRow = self:GetParent()
            local qid = self.questID or (parentRow and parentRow.questID) or nil
            WOW.GameTooltip:Hide()
            if GameTooltip_HideShoppingTooltips and WOW.GameTooltip then
                GameTooltip_HideShoppingTooltips(WOW.GameTooltip)
            end
            activeHoverState.DeferStop(qid, "reward-onleave")
        end)
        btn:Hide()

        rewardIcons[iconIdx] = btn
    end
    row.rewardIcons = rewardIcons

    -- Money text (bottom line): shown as a left-aligned text label in place of
    -- a gold icon so the full denomination (g / s / c) has room to display.
    -- Anchored dynamically in PopulateQuestRow based on icon count.
    local moneyText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    moneyText:SetJustifyH("LEFT")
    moneyText:SetTextColor(1, 1, 1)  -- per-denomination colours are baked into the label string
    moneyText:Hide()
    row.moneyText = moneyText

    -- Bottom separator line
    local sep = row:CreateTexture(nil, "BACKGROUND")
    sep:SetHeight(1)
    sep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    sep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
    sep:SetColorTexture(
        COL.separator[1], COL.separator[2],
        COL.separator[3], COL.separator[4])
    row.separator = sep

    -- Row hover: show/hide the hover background and the quest tooltip.
    row:SetScript("OnEnter", function(self)
        local rowHovered = self.IsMouseOver and self:IsMouseOver() or false
        local poiHovered = self.poiBtn and self.poiBtn.IsMouseOver and self.poiBtn:IsMouseOver() or false
        local contractBtn = self.contractBtn
        local contractHovered = contractBtn
            and contractBtn.IsShown
            and contractBtn:IsShown()
            and contractBtn.IsMouseOver
            and contractBtn:IsMouseOver()
            or false
        local rewardHovered = false
        local rewardIcons = self.rewardIcons
        if rewardIcons then
            for iconIndex = 1, #rewardIcons do
                local rewardButton = rewardIcons[iconIndex]
                if rewardButton
                    and rewardButton.IsShown
                    and rewardButton:IsShown()
                    and rewardButton.IsMouseOver
                    and rewardButton:IsMouseOver()
                then
                    rewardHovered = true
                    break
                end
            end
        end
        eventFrame._DebugHoverTrace(
            "Row.OnEnter",
            "questID=%s rowHovered=%s poiHovered=%s contractHovered=%s rewardHovered=%s call=StartOrResume",
            tostring(self.questID),
            tostring(rowHovered),
            tostring(poiHovered),
            tostring(contractHovered),
            tostring(rewardHovered))
        if self.questID then
            ShowQuestTooltip(self, self.questID, false)
        end
        activeHoverState.StartOrResume(self, self.questID, true, "row-onenter", "tooltip")
    end)
    row:SetScript("OnLeave", function(self)
        local rowHovered = self.IsMouseOver and self:IsMouseOver() or false
        local poiHovered = self.poiBtn and self.poiBtn.IsMouseOver and self.poiBtn:IsMouseOver() or false
        local contractBtn = self.contractBtn
        local contractHovered = contractBtn
            and contractBtn.IsShown
            and contractBtn:IsShown()
            and contractBtn.IsMouseOver
            and contractBtn:IsMouseOver()
            or false
        local rewardHovered = false
        local rewardIcons = self.rewardIcons
        if rewardIcons then
            for iconIndex = 1, #rewardIcons do
                local rewardButton = rewardIcons[iconIndex]
                if rewardButton
                    and rewardButton.IsShown
                    and rewardButton:IsShown()
                    and rewardButton.IsMouseOver
                    and rewardButton:IsMouseOver()
                then
                    rewardHovered = true
                    break
                end
            end
        end
        eventFrame._DebugHoverTrace(
            "Row.OnLeave",
            "questID=%s rowHovered=%s poiHovered=%s contractHovered=%s rewardHovered=%s call=DeferStop",
            tostring(self.questID),
            tostring(rowHovered),
            tostring(poiHovered),
            tostring(contractHovered),
            tostring(rewardHovered))
        ClearActiveQuestTooltip(self)
        activeHoverState.DeferStop(self.questID, "row-onleave")
    end)

    row:SetScript("OnClick", function(self, button)
        -- Toggle super-tracking (mirrors poiBtn OnMouseDown logic).
        local poiBtn = self.poiBtn
        if poiBtn then
            if poiBtn.areaPoiID and poiBtn.areaPoiID > 0
                and WOW.C_SuperTrack and WOW.C_SuperTrack.SetSuperTrackedMapPin
            then
                local mapPinType = Enum and Enum.SuperTrackingMapPinType
                    and Enum.SuperTrackingMapPinType.AreaPOI or 0
                local trackedType, trackedID
                if WOW.C_SuperTrack.GetSuperTrackedMapPin then
                    trackedType, trackedID = WOW.C_SuperTrack.GetSuperTrackedMapPin()
                end
                if trackedType == mapPinType and trackedID == poiBtn.areaPoiID then
                    if WOW.C_SuperTrack.ClearAllSuperTracked then
                        WOW.C_SuperTrack.ClearAllSuperTracked()
                    end
                else
                    WOW.C_SuperTrack.SetSuperTrackedMapPin(mapPinType, poiBtn.areaPoiID)
                end
            elseif poiBtn.questID and poiBtn.questID > 0
                and WOW.C_SuperTrack and WOW.C_SuperTrack.SetSuperTrackedQuestID
            then
                local cur = WOW.C_SuperTrack.GetSuperTrackedQuestID
                    and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil
                if cur == poiBtn.questID then
                    WOW.C_SuperTrack.SetSuperTrackedQuestID(0)
                else
                    WOW.C_SuperTrack.SetSuperTrackedQuestID(poiBtn.questID)
                end
            end
        end
        -- Navigate the world map (both left and right click).
        if not self.mapID or not WOW.WorldMapFrame then return end
        if not WOW.WorldMapFrame:IsVisible() then
            ShowUIPanel(WOW.WorldMapFrame)
        end
        if WOW.WorldMapFrame:GetMapID() ~= self.mapID then
            WOW.WorldMapFrame:SetMapID(self.mapID)
        end
    end)

    local fadeIn = row:CreateAnimationGroup()
    local fadeInAlpha = fadeIn:CreateAnimation("Alpha")
    fadeInAlpha:SetFromAlpha(0)
    fadeInAlpha:SetToAlpha(1)
    fadeInAlpha:SetDuration(0.2)
    if fadeInAlpha.SetSmoothing then
        fadeInAlpha:SetSmoothing("OUT")
    end
    fadeIn:SetScript("OnFinished", function(self)
        self:GetParent():SetAlpha(1)
    end)
    row.fadeIn = fadeIn
    ApplyWorldQuestTypographyToRow(row)

    -- Start fully hidden; PopulateQuestRow shows the row once all content is set.
    row:Hide()
    return row
end

-- Returns a row to the pool after hiding and detaching it.
-- row: Frame
ReleaseQuestRow = function(row, skipTooltipCheck)
    if row.fadeIn then
        row.fadeIn:Stop()
    end
    if activeHoverState.row == row then
        eventFrame._DebugHoverTrace(
            "ReleaseQuestRow",
            "releasing-active-row rowQuestID=%s activeQuestID=%s row=%s",
            tostring(rawget(row, "questID")),
            tostring(activeHoverState.questID),
            eventFrame._GetHoverRowIdentity(row))
    end
    if activeHoverState.row == row then
        activeHoverState.row = nil
        if activeHoverState.poiBtn == row.poiBtn then
            activeHoverState.poiBtn = nil
        end
    end
    row._hlLastPin = nil
    row._hlMisses = nil
    row._hlMissStart = nil
    if not skipTooltipCheck then
        local ownsTooltip = activeTooltipAnchor == row
            or activeTooltipAnchor == row.poiBtn
            or activeTooltipAnchor == row.contractBtn

        if not ownsTooltip and activeTooltipAnchor and activeTooltipAnchor.GetParent then
            ownsTooltip = activeTooltipAnchor:GetParent() == row
        end

        if not ownsTooltip and WOW.GameTooltip and WOW.GameTooltip.IsOwned then
            ownsTooltip = WOW.GameTooltip:IsOwned(row)
                or (row.poiBtn and WOW.GameTooltip:IsOwned(row.poiBtn))
                or (row.contractBtn and WOW.GameTooltip:IsOwned(row.contractBtn))

            if not ownsTooltip and row.rewardIcons then
                for _, rewardButton in ipairs(row.rewardIcons) do
                    if WOW.GameTooltip:IsOwned(rewardButton) then
                        ownsTooltip = true
                        break
                    end
                end
            end
        end

        if ownsTooltip then
            ClearActiveQuestTooltip()
        end
    end
    row:Hide()
    row:SetAlpha(1)
    if row.hoverBg then
        row.hoverBg:Hide()
    end
    row:SetParent(nil)
    row:ClearAllPoints()
    row.questID          = nil
    row.mapID            = nil
    row.mapName          = nil
    row.questEntry       = nil
        row.isAreaPOI        = nil
        row.poiTitle         = nil
        row.poiUnlockText    = nil
        if row.poiBtn.Reset then
            row.poiBtn:Reset()
        end
        ClearPOIButtonAreaPOIState(row.poiBtn)
        if row.poiBtn.Display and row.poiBtn.Display.SetIconShown then
            row.poiBtn.Display:SetIconShown(true)
        elseif row.poiBtn.Display and row.poiBtn.Display.Icon then
            row.poiBtn.Display.Icon:SetAlpha(1)
        end
        row.poiBtn.questID   = nil  -- poiBtn.questID is only set in Phase 2; clear here so that if the
                                    -- pooled frame is re-assigned to a different quest and OnEnter fires
                                    -- before Phase 2 runs, it falls back to row.questID (set in Phase 1)
        row.poiBtn.areaPoiID = nil
        row.poiBtn:Hide()
    local areaPOIIcon = eventFrame._poiButtonAreaPOIIcons and eventFrame._poiButtonAreaPOIIcons[row.poiBtn] or nil
    if areaPOIIcon then
        areaPOIIcon:Hide()
    end
    row.contractBtn.auraInstanceID = nil
    row.contractBtn.spellID = nil
    row.contractBtn:Hide()
    questRowPool[#questRowPool + 1] = row
end

-- Returns an inactive zone header frame or creates a new one.
-- parent: Frame
AcquireZoneHeader = function(parent)
    local hdr = table.remove(zoneHeaderPool)
    if hdr then
        hdr:SetParent(parent)
        hdr:ClearAllPoints()
        hdr:Show()
        ApplyWorldQuestTypographyToHeader(hdr)
        return hdr
    end

    -- QuestLogHeaderTemplate inherits both ListHeaderVisualTemplate (visual)
    -- and QuestLogHeaderCodeTemplate / ListHeaderCodeTemplate (behaviour).
    -- We need ListHeaderCodeTemplate too, so ListHeaderMixin:OnLoad fires and
    -- sets the initial DISABLED_FONT_COLOR (grey) text, and OnEnter/OnLeave
    -- toggle it to HIGHLIGHT_FONT_COLOR (white) on hover.
    hdr = WOW.CreateFrame("Button", nil, parent, "ListHeaderVisualTemplate, ListHeaderCodeTemplate")
    hdr:SetHeight(K.ZONE_HEADER_HEIGHT)

    -- Add a quest count label docked just left of the CollapseButton.
    local countLabel = hdr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    countLabel:SetPoint("RIGHT", hdr.CollapseButton, "LEFT", -4, 0)
    countLabel:SetJustifyH("RIGHT")
    countLabel:SetAlpha(0.7)
    hdr.countLabel = countLabel

    -- The template anchors ButtonText.RIGHT to CollapseButton.LEFT - 4.
    -- Re-anchor it to end before countLabel so the two don't overlap.
    hdr.ButtonText:ClearAllPoints()
    hdr.ButtonText:SetPoint("LEFT", hdr, "LEFT", 8, 1)
    hdr.ButtonText:SetPoint("RIGHT", countLabel, "LEFT", -4, 1)

    -- Keep .label pointing at ButtonText for LayoutScrollContent compatibility.
    hdr.label = hdr.ButtonText

    -- SetCollapsed: delegates to CollapseButtonMixin.UpdateCollapsedState, which
    -- switches questlog-icon-expand (collapsed) / questlog-icon-shrink (expanded).
    hdr.SetCollapsed = function(self, collapsed)
        self.isCollapsed = collapsed
        if self.CollapseButton then
            self.CollapseButton:UpdateCollapsedState(collapsed)
        end
    end

    -- Use SetClickHandler (ListHeaderMixin's click dispatch) instead of
    -- SetScript so the mixin's OnLoad/OnEnter/OnLeave chain is not broken.
    hdr:SetClickHandler(function(self)
        if not self.zoneName then return end
        local newCollapsed = not (collapseState[self.zoneName] or false)
        collapseState[self.zoneName] = newCollapsed
        self:SetCollapsed(newCollapsed)
        ScheduleRefresh()
    end)

    ApplyWorldQuestTypographyToHeader(hdr)

    return hdr
end

-- Returns a zone header to the pool.
-- hdr: Frame
ReleaseZoneHeader = function(hdr)
    hdr:Hide()
    hdr:SetParent(nil)
    hdr:ClearAllPoints()
    zoneHeaderPool[#zoneHeaderPool + 1] = hdr
end

-- =============================================
-- Populating a quest row with data
-- =============================================

UpdateQuestRowTimeDisplay = function(row, entry)
    local now = GetCurrentServerTime()

    if entry.isLocked and entry.isAreaPOI then
        entry.timeText = FormatTimeLeft(nil)
        entry.timeLeft = nil
        entry.expiresAt = nil
    else
        if entry.isAreaPOI and entry.poiID then
            local liveSec = TryGetAreaPOISecondsLeft(entry.poiID)
            if liveSec and liveSec > 0 then
                entry.timeLeft = liveSec
                entry.expiresAt = now + liveSec
            end
        end

        if entry.timeLeft and entry.timeLeft > 0 then
            entry.timeText = FormatTimeLeft(entry.timeLeft)
        elseif entry.expiresAt and entry.expiresAt > now then
            local remaining = entry.expiresAt - now
            entry.timeText = remaining > 0 and FormatTimeLeft(remaining) or FormatTimeLeft(nil)
        elseif entry.isAreaPOI and entry.areaPOITimeText and entry.areaPOITimeText ~= "" then
            entry.timeText = entry.areaPOITimeText
        else
            entry.timeText = FormatTimeLeft(nil)
        end
    end

    local dotR, dotG, dotB = GetDotColor(entry.timeLeft)
    row.timeDot:SetColorTexture(dotR, dotG, dotB)
    row.timeText:SetText(entry.timeText or "")
    row.timeText:Show()
    row.timeDot:Show()
end

GetQuestRowHeight = function(entry)
    if entry and entry.isAreaPOI == true then
        return K.ROW_HEIGHT
    end

    if entry and entry.isLocked then
        return K.LOCKED_ROW_HEIGHT
    end

    return K.ROW_HEIGHT
end

ApplyQuestRowLayout = function(row, entry)
    local isLocked = entry and entry.isLocked == true

    row:SetHeight(GetQuestRowHeight(entry))

    row.titleText:ClearAllPoints()
    row.titleText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -9)
    row.titleText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -70, -9)
    row.titleText:SetWordWrap(false)

    row.timeText:ClearAllPoints()
    row.timeText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -21, -12)
    row.timeDot:ClearAllPoints()
    row.timeDot:SetPoint("LEFT", row.timeText, "RIGHT", 4, 0)

    row.factionText:ClearAllPoints()
    row.factionText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -23)
    if entry and entry.isAreaPOI == true then
        row.factionText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, -23)
        row.factionText:SetWordWrap(false)
        row.factionText:SetJustifyV("MIDDLE")
        if row.factionText.SetMaxLines then
            row.factionText:SetMaxLines(1)
        end
    elseif isLocked then
        row.factionText:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -20, 9)
        row.factionText:SetWordWrap(true)
        row.factionText:SetJustifyV("TOP")
        if row.factionText.SetMaxLines then
            row.factionText:SetMaxLines(2)
        end
    else
        row.factionText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, -23)
        row.factionText:SetWordWrap(false)
        row.factionText:SetJustifyV("MIDDLE")
        if row.factionText.SetMaxLines then
            row.factionText:SetMaxLines(1)
        end
    end
end

ResetQuestRowRewards = function(row)
    for iconIdx = 1, K.MAX_REWARD_ICONS do
        local btn = row.rewardIcons[iconIdx]
        btn:Hide()
        btn.rewardType = nil
        btn.questRewardType = nil
        btn.amount     = nil
        btn.itemID     = nil
        btn.currencyID = nil
        btn.factionID  = nil
        btn.currencyLabel = nil
        btn.label      = nil
        btn.ilvl       = nil
        btn.quality    = nil
        btn.questID    = nil
        btn.rewardIndex = nil
        if btn.SetID then
            btn:SetID(0)
        end
        btn.countText:SetText("")
        if btn.borderTop then
            btn.borderTop:Hide()
            btn.borderBottom:Hide()
            btn.borderLeft:Hide()
            btn.borderRight:Hide()
        end
    end

    row.moneyText:SetText("")
    row.moneyText:Hide()
end

-- Row tooltips use SetQuestLogItem(questRewardType, index, questID); that reads live quest-log
-- slot data. Cached GetQuestRewards() can still carry a stale texture on first paint while the
-- slot binding is already correct — re-query GetQuestLog* at layout time so icons match hover.
local function GetLiveQuestItemRewardIconTexture(rewardOwnerQuestID, rewardData)
    if type(rewardOwnerQuestID) ~= "number" or rewardOwnerQuestID <= 0
        or type(rewardData) ~= "table"
        or rewardData.rewardType ~= "item"
        or type(rewardData.rewardIndex) ~= "number"
        or rewardData.rewardIndex <= 0
    then
        return nil
    end

    local questRewardType = rewardData.questRewardType or "reward"
    if questRewardType == "choice" then
        local getChoice = rawget(_G, "GetQuestLogChoiceInfo")
        if getChoice then
            local _, texture = getChoice(rewardData.rewardIndex, rewardOwnerQuestID)
            if texture and texture ~= "" then
                return texture
            end
        end
    elseif WOW.GetQuestLogRewardInfo then
        local _, texture = WOW.GetQuestLogRewardInfo(rewardData.rewardIndex, rewardOwnerQuestID)
        if texture and texture ~= "" then
            return texture
        end
    end

    return nil
end

-- Populates reward icon buttons + gold line from a GetQuestRewards-style list.
-- rewardOwnerQuestID is used for quest-scoped reward tooltips (SetQuestLogItem, etc.).
ApplyQuestRewardListToRow = function(row, rewards, rewardOwnerQuestID, rewardTopOffset)
    rewardTopOffset = rewardTopOffset or -37
    local goldReward = nil
    local nonGoldIdx = 0
    for _, rewardData in ipairs(rewards) do
        if rewardData.rewardType == "gold" then
            goldReward = rewardData
        else
            nonGoldIdx = nonGoldIdx + 1
            if nonGoldIdx <= K.MAX_REWARD_ICONS then
                local btn = row.rewardIcons[nonGoldIdx]
                btn.rewardType = rewardData.rewardType
                btn.amount = rewardData.amount
                btn.itemID = rewardData.itemID
                btn.currencyID = rewardData.currencyID
                btn.factionID = rewardData.factionID
                btn.currencyLabel = rewardData.currencyLabel
                btn.label = rewardData.label
                btn.ilvl = rewardData.ilvl
                btn.quality = rewardData.quality
                btn.questRewardType = rewardData.questRewardType
                btn.questID = rewardOwnerQuestID
                btn.rewardIndex = rewardData.rewardIndex
                if btn.SetID then
                    btn:SetID(rewardData.rewardIndex or 0)
                end

                local iconTexture = rewardData.icon
                if rewardData.rewardType == "item" then
                    local liveIcon = GetLiveQuestItemRewardIconTexture(rewardOwnerQuestID, rewardData)
                    if liveIcon then
                        iconTexture = liveIcon
                    elseif rewardData.itemID and WOW.C_Item and WOW.C_Item.GetItemIconByID then
                        local idIcon = WOW.C_Item.GetItemIconByID(rewardData.itemID)
                        if idIcon and idIcon ~= "" then
                            iconTexture = idIcon
                        end
                    end
                    btn.iconTex:SetTexture(iconTexture or "Interface\\Icons\\INV_Misc_QuestionMark")

                    if ns._WorldQuestsShouldShowRewardItemLevel(rewardData) then
                        btn.countText:SetText(rewardData.ilvl)
                        btn.countText:SetTextColor(1, 1, 1)
                    elseif rewardData.amount and rewardData.amount > 1 then
                        btn.countText:SetText(rewardData.amount)
                        btn.countText:SetTextColor(1, 1, 1)
                    else
                        btn.countText:SetText("")
                    end

                    local stickyQuestID = rewardOwnerQuestID
                    local stickyItemID = rewardData.itemID
                    local stickyRewardIndex = rewardData.rewardIndex
                    local stickyQuestRewardType = rewardData.questRewardType
                    local ItemGlobal = rawget(_G, "Item")
                    if ItemGlobal and ItemGlobal.CreateFromItemID
                        and type(stickyItemID) == "number" and stickyItemID > 0
                    then
                        local itemObj = ItemGlobal:CreateFromItemID(stickyItemID)
                        if itemObj and itemObj.ContinueOnItemLoad then
                            itemObj:ContinueOnItemLoad(function()
                                if not btn.iconTex then
                                    return
                                end
                                if btn.questID ~= stickyQuestID or btn.itemID ~= stickyItemID then
                                    return
                                end
                                local refreshData = {
                                    rewardType = "item",
                                    rewardIndex = stickyRewardIndex,
                                    questRewardType = stickyQuestRewardType,
                                    itemID = stickyItemID,
                                }
                                local tex = GetLiveQuestItemRewardIconTexture(stickyQuestID, refreshData)
                                if not tex and itemObj.GetItemIcon then
                                    tex = itemObj:GetItemIcon()
                                end
                                if not tex and WOW.C_Item and WOW.C_Item.GetItemIconByID then
                                    tex = WOW.C_Item.GetItemIconByID(stickyItemID)
                                end
                                if tex and tex ~= "" then
                                    btn.iconTex:SetTexture(tex)
                                end
                            end)
                        end
                    end
                else
                    btn.iconTex:SetTexture(iconTexture or "Interface\\Icons\\INV_Misc_QuestionMark")
                end

                if rewardData.rewardType == "currency" then
                    local displayAmount = ns.FormatCompactRewardAmount(rewardData.amount)
                    if displayAmount then
                        btn.countText:SetText(displayAmount)
                        btn.countText:SetTextColor(1, 1, 1)
                    else
                        btn.countText:SetText("")
                    end
                elseif rewardData.rewardType == "rep" then
                    local displayAmount = ns.FormatCompactRewardAmount(rewardData.amount)
                    if displayAmount then
                        btn.countText:SetText(displayAmount)
                        btn.countText:SetTextColor(0.4, 0.8, 1)
                    else
                        btn.countText:SetText("")
                    end
                elseif rewardData.rewardType ~= "item" then
                    btn.countText:SetText("")
                end
                btn:Show()
                if btn.quality and ITEM_QUALITY_COLORS
                    and ITEM_QUALITY_COLORS[btn.quality]
                then
                    local qc = ITEM_QUALITY_COLORS[btn.quality]
                    btn.borderTop:SetColorTexture(qc.r, qc.g, qc.b, 1)
                    btn.borderBottom:SetColorTexture(qc.r, qc.g, qc.b, 1)
                    btn.borderLeft:SetColorTexture(qc.r, qc.g, qc.b, 1)
                    btn.borderRight:SetColorTexture(qc.r, qc.g, qc.b, 1)
                    btn.borderTop:Show()
                    btn.borderBottom:Show()
                    btn.borderLeft:Show()
                    btn.borderRight:Show()
                end
            end
        end
    end

    if goldReward then
        row.moneyText:ClearAllPoints()
        if nonGoldIdx == 0 then
            row.moneyText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, rewardTopOffset)
        else
            local lastBtn = row.rewardIcons[math.min(nonGoldIdx, K.MAX_REWARD_ICONS)]
            row.moneyText:SetPoint("LEFT", lastBtn, "RIGHT", 6, 0)
        end
        row.moneyText:SetText(goldReward.label or "")
        row.moneyText:SetTextColor(1, 1, 1)
        row.moneyText:Show()
    else
        row.moneyText:SetText("")
        row.moneyText:Hide()
    end
end

ApplyQuestRowTitleColor = function(titleText, questID, isLocked, isPendingTitle)
    local superTrackedID = WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedQuestID
        and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil

    if superTrackedID and superTrackedID == questID then
        titleText:SetTextColor(
            COL.superTrackOn[1], COL.superTrackOn[2], COL.superTrackOn[3])
    elseif isLocked then
        titleText:SetTextColor(
            COL.rowTitleLocked[1], COL.rowTitleLocked[2], COL.rowTitleLocked[3])
    elseif isPendingTitle then
        titleText:SetTextColor(0.55, 0.55, 0.55)
    else
        titleText:SetTextColor(1, 0.82, 0)
    end
end

IsLockedSpecialAssignmentEntry = function(entry)
    if not entry or entry.isLocked ~= true then
        return false
    end

    return IsSpecialAssignmentFromMetadata(
        entry.rawQuestTagType,
        entry.rawTagID,
        entry.questType)
end

HasVisibleLockedSpecialAssignmentUnlock = function()
    if not questMapPanel or not questMapPanel:IsShown() then
        return false
    end

    for _, item in ipairs(activeContent) do
        if item.type == "row" then
            local entry = item._entry or (item.frame and item.frame.questEntry)
            local questID = (item.frame and item.frame.questID) or (entry and entry.questID)
            if questID and questID > 0 and IsLockedSpecialAssignmentEntry(entry) then
                return true
            end
        end
    end

    return false
end

end

do
    do
        local function CompareLockedAreaPOIWidgets(leftWidget, rightWidget)
            local leftOrder = leftWidget.orderIndex or 0
            local rightOrder = rightWidget.orderIndex or 0
            if leftOrder ~= rightOrder then
                return leftOrder < rightOrder
            end

            local leftType = leftWidget.widgetType or 0
            local rightType = rightWidget.widgetType or 0
            if leftType ~= rightType then
                return leftType < rightType
            end

            return (leftWidget.widgetID or 0) < (rightWidget.widgetID or 0)
        end

        local function AccumulateLockedAreaPOIWidgetHash(hash, value)
            return ((hash * 131) + (value or 0) + 1) % 2147483647
        end

        local function AccumulateLockedAreaPOIWidgetTextHash(hash, text, normalizeTimerText)
            local normalizedText = NormalizeInlineQuestText(text)
            if normalizeTimerText and normalizedText then
                normalizedText = "__timer__"
            end

                normalizedText = normalizedText or ""

            hash = AccumulateLockedAreaPOIWidgetHash(hash, #normalizedText)
            for index = 1, #normalizedText do
                hash = AccumulateLockedAreaPOIWidgetHash(hash, normalizedText:byte(index))
            end

            return hash
        end

        eventFrame._AccumulateLockedAreaPOICurrencyPayloadHash = function(hash, currencies)
            local currencyCount = currencies and #currencies or 0

            hash = AccumulateLockedAreaPOIWidgetHash(hash, currencyCount)

            if currencies then
                for currencyIndex = 1, currencyCount do
                    local currencyInfo = currencies[currencyIndex]

                    hash = AccumulateLockedAreaPOIWidgetHash(
                        hash,
                        currencyInfo and currencyInfo.currencyID or 0)
                    hash = AccumulateLockedAreaPOIWidgetHash(
                        hash,
                        currencyInfo and currencyInfo.iconFileID or 0)
                    hash = AccumulateLockedAreaPOIWidgetHash(
                        hash,
                        currencyInfo and (currencyInfo.quantity or currencyInfo.amount) or 0)
                    hash = AccumulateLockedAreaPOIWidgetTextHash(
                        hash,
                        currencyInfo and currencyInfo.leadingText or nil)
                    hash = AccumulateLockedAreaPOIWidgetTextHash(
                        hash,
                        currencyInfo and currencyInfo.text or nil)
                end
            end

            return hash
        end

        local function AccumulateLockedAreaPOIWidgetPayloadHash(hash, widgetInfo)
            local shownState = widgetInfo and widgetInfo.shownState
            if shownState == false or shownState == 0 then
                return AccumulateLockedAreaPOIWidgetHash(hash, 0)
            end

            hash = AccumulateLockedAreaPOIWidgetHash(hash, 1)

            local widgetType = widgetInfo.widgetType
            local widgetID = widgetInfo.widgetID

            if widgetType == 2 and C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo then
                local info = C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo(widgetID)
                local hasTimer = info and info.hasTimer == true
                    local overrideBarText = info and info.overrideBarText or nil
                    local infoText = info and info.text or nil
                    local normalizedOverrideBarText = NormalizeInlineQuestText(overrideBarText)
                    local normalizedInfoText = NormalizeInlineQuestText(infoText)
                    local overrideBarTextIsTimer = hasTimer and normalizedOverrideBarText
                        and eventFrame:IsAreaPOITimeText(normalizedOverrideBarText) or false
                    local infoTextIsTimer = hasTimer and normalizedInfoText
                        and eventFrame:IsAreaPOITimeText(normalizedInfoText) or false

                hash = AccumulateLockedAreaPOIWidgetHash(hash, info and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, hasTimer and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetTextHash(
                    hash,
                        overrideBarText,
                        overrideBarTextIsTimer)
                hash = AccumulateLockedAreaPOIWidgetTextHash(
                    hash,
                        infoText,
                        infoTextIsTimer)
            elseif widgetType == 8 and C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo then
                local info = C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo(widgetID)
                local normalizedText = info and NormalizeInlineQuestText(info.text) or nil
                local isTimerText = normalizedText and eventFrame:IsAreaPOITimeText(normalizedText) or false

                hash = AccumulateLockedAreaPOIWidgetHash(hash, info and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetTextHash(hash, normalizedText, isTimerText)
            elseif widgetType == 7 and C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo then
                local info = C_UIWidgetManager.GetIconTextAndCurrenciesWidgetVisualizationInfo(widgetID)

                hash = AccumulateLockedAreaPOIWidgetHash(hash, info and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetTextHash(hash, info and info.text or nil)
                hash = eventFrame._AccumulateLockedAreaPOICurrencyPayloadHash(
                    hash,
                    info and info.currencies or nil)
            elseif widgetType == 9 and C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo then
                local info = C_UIWidgetManager.GetHorizontalCurrenciesWidgetVisualizationInfo(widgetID)

                hash = AccumulateLockedAreaPOIWidgetHash(hash, info and 1 or 0)
                hash = eventFrame._AccumulateLockedAreaPOICurrencyPayloadHash(
                    hash,
                    info and info.currencies or nil)
            elseif widgetType == 13 and C_UIWidgetManager.GetSpellDisplayVisualizationInfo then
                local info = C_UIWidgetManager.GetSpellDisplayVisualizationInfo(widgetID)
                local spellInfo = info and info.spellInfo or nil

                hash = AccumulateLockedAreaPOIWidgetHash(hash, spellInfo and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, spellInfo and spellInfo.spellID or 0)
                hash = AccumulateLockedAreaPOIWidgetTextHash(hash, spellInfo and spellInfo.text or nil)
            elseif widgetType == 27 and C_UIWidgetManager.GetItemDisplayVisualizationInfo then
                local info = C_UIWidgetManager.GetItemDisplayVisualizationInfo(widgetID)
                local itemInfo = info and info.itemInfo or nil

                hash = AccumulateLockedAreaPOIWidgetHash(hash, itemInfo and 1 or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, itemInfo and itemInfo.itemID or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, itemInfo and itemInfo.stackCount or 0)
                hash = AccumulateLockedAreaPOIWidgetTextHash(hash, itemInfo and itemInfo.overrideItemName or nil)
                hash = AccumulateLockedAreaPOIWidgetTextHash(hash, itemInfo and itemInfo.infoText or nil)
            end

            return hash
        end

        eventFrame._GetLiveLockedAreaPOIWidgetFingerprint = function(widgetSetID)
            if not widgetSetID or widgetSetID <= 0 or not C_UIWidgetManager
                or not C_UIWidgetManager.GetAllWidgetsBySetID
            then
                return 0, 0, 0
            end

            local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID)
            if not widgets or #widgets == 0 then
                return 0, 0, 0
            end

            local orderedWidgets = eventFrame._liveRelevantAreaPOIWidgetFingerprintWidgets or {}
            local widgetCount = #widgets

            eventFrame._liveRelevantAreaPOIWidgetFingerprintWidgets = orderedWidgets

            for index = 1, widgetCount do
                orderedWidgets[index] = widgets[index]
            end

            for index = widgetCount + 1, #orderedWidgets do
                orderedWidgets[index] = nil
            end

            WOW.table_sort(orderedWidgets, CompareLockedAreaPOIWidgets)

            local hash = widgetCount
            local payloadHash = widgetCount
            for index = 1, widgetCount do
                local widgetInfo = orderedWidgets[index]
                local shownState = widgetInfo.shownState
                if shownState == true then
                    shownState = 1
                elseif shownState == false then
                    shownState = 0
                end

                hash = AccumulateLockedAreaPOIWidgetHash(hash, widgetInfo.orderIndex or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, widgetInfo.widgetType or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, widgetInfo.widgetID or 0)
                hash = AccumulateLockedAreaPOIWidgetHash(hash, shownState or 0)
                payloadHash = AccumulateLockedAreaPOIWidgetPayloadHash(payloadHash, widgetInfo)
            end

            return widgetCount, hash, payloadHash
        end
    end

    local function BuildLiveRelevantAreaPOISnapshotPart(questID, mapID, poiID, poiInfo)
        local widgetSetID = poiInfo and poiInfo.tooltipWidgetSet or 0
        local widgetCount, widgetFingerprint, widgetPayloadFingerprint =
            eventFrame._GetLiveLockedAreaPOIWidgetFingerprint(widgetSetID)

        return WOW.string_format(
            "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s",
            tostring(questID or 0),
            tostring(mapID or 0),
            tostring(poiID or 0),
            poiInfo and "1" or "0",
            poiInfo and poiInfo.atlasName or "",
            tostring(widgetSetID or 0),
            tostring(widgetCount),
            tostring(widgetFingerprint),
            tostring(widgetPayloadFingerprint),
            poiInfo and poiInfo.name or "",
            poiInfo and poiInfo.description or "")
    end

    local function SetLiveRelevantAreaPOISnapshotParts(parts, partCount)
        local previousParts = eventFrame._lastLiveRelevantAreaPOISnapshotParts or {}
        local widgetSetIDs = eventFrame._liveRelevantAreaPOIWidgetSetIDs or {}
        local entries = eventFrame._liveRelevantAreaPOISnapshotEntries
        local widgetSetCount = 0

        eventFrame._lastLiveRelevantAreaPOISnapshotParts = previousParts
        eventFrame._lastLiveRelevantAreaPOISnapshotCount = partCount
        eventFrame._liveRelevantAreaPOIWidgetSetIDs = widgetSetIDs

        wipe(widgetSetIDs)

        for index = 1, partCount do
            previousParts[index] = parts[index]
        end

        for index = partCount + 1, #previousParts do
            previousParts[index] = nil
        end

        if entries then
            for index = 1, partCount do
                local entry = entries[index]
                local widgetSetID = entry and entry.areaPOIInfo
                    and entry.areaPOIInfo.tooltipWidgetSet or 0
                if widgetSetID and widgetSetID > 0 and not widgetSetIDs[widgetSetID] then
                    widgetSetIDs[widgetSetID] = true
                    widgetSetCount = widgetSetCount + 1
                end
            end
        end

        eventFrame._liveRelevantAreaPOIWidgetSetCount = widgetSetCount
    end

    local function HasLiveRelevantAreaPOISnapshotDifference(parts, partCount)
        local previousCount = eventFrame._lastLiveRelevantAreaPOISnapshotCount or 0
        local previousParts = eventFrame._lastLiveRelevantAreaPOISnapshotParts

        if partCount ~= previousCount then
            return true
        end

        if not previousParts then
            return false
        end

        for index = 1, partCount do
            if parts[index] ~= previousParts[index] then
                return true
            end
        end

        return false
    end

    local function BuildVisibleLockedAreaPOISnapshotPart(questID, mapID, poiID, poiInfo,
        fallbackName, fallbackDescription, fallbackWidgetSetID)
        local poiName = poiInfo and poiInfo.name or fallbackName or ""
        local poiDescription = poiInfo and poiInfo.description or fallbackDescription or ""
        local atlasName = poiInfo and poiInfo.atlasName or ""
        local widgetSetID = poiInfo and poiInfo.tooltipWidgetSet
            or fallbackWidgetSetID or 0
        local widgetCount, widgetFingerprint, widgetPayloadFingerprint =
            eventFrame._GetLiveLockedAreaPOIWidgetFingerprint(widgetSetID)

        return WOW.string_format(
            "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s",
            tostring(questID or 0),
            tostring(mapID or 0),
            tostring(poiID or 0),
            poiInfo and "1" or "0",
            atlasName,
            tostring(widgetSetID or 0),
            tostring(widgetCount),
            tostring(widgetFingerprint),
            tostring(widgetPayloadFingerprint),
            poiName,
            NormalizeInlineQuestText(poiDescription) or "")
    end

    eventFrame._BuildVisibleLockedAreaPOISnapshot = function()
        if not currentQuestEntries or #currentQuestEntries == 0 then
            return nil
        end

        local parts = eventFrame._visibleLockedAreaPOISnapshotParts or {}
        local partCount = 0
        eventFrame._visibleLockedAreaPOISnapshotParts = parts

        for _, entry in ipairs(currentQuestEntries) do
            if entry.isAreaPOI and entry.isLocked then
                partCount = partCount + 1

                local poiInfo = nil
                if entry.mapID and entry.poiID and C_AreaPoiInfo
                    and C_AreaPoiInfo.GetAreaPOIInfo
                then
                    poiInfo = C_AreaPoiInfo.GetAreaPOIInfo(entry.mapID, entry.poiID)
                end

                parts[partCount] = BuildVisibleLockedAreaPOISnapshotPart(
                    entry.questID,
                    entry.mapID,
                    entry.poiID,
                    poiInfo,
                    entry.poiName or entry.title,
                    entry.poiDescription,
                    entry.tooltipWidgetSet)
            end
        end

        for index = partCount + 1, #parts do
            parts[index] = nil
        end

        if partCount == 0 then
            return nil
        end

        return table.concat(parts, "\031", 1, partCount)
    end

    eventFrame._BuildLiveRelevantAreaPOISnapshot = function(mapID)
        if not mapID
            or not C_AreaPoiInfo
            or not C_AreaPoiInfo.GetAreaPOIForMap
            or not C_AreaPoiInfo.GetAreaPOIInfo
        then
            return nil, 0
        end

        local queryState = eventFrame:BuildRelevantWorldQuestMapQueryState(mapID)
        local queryMapIDs = queryState and queryState.queryMapIDs or nil
    local mapsToQuery = queryState and queryState.mapsToQuery or nil
    local allowOutsideHierarchy = queryState and queryState.allowOutsideHierarchy or nil
        local parts = eventFrame._liveRelevantAreaPOISnapshotParts or {}
        local entries = eventFrame._liveRelevantAreaPOISnapshotEntries or {}
        local seen = eventFrame._liveRelevantAreaPOISnapshotSeen or {}
        local partCount = 0

        eventFrame._liveRelevantAreaPOISnapshotParts = parts
        eventFrame._liveRelevantAreaPOISnapshotEntries = entries
        eventFrame._liveRelevantAreaPOISnapshotSeen = seen

        for syntheticID in pairs(seen) do
            seen[syntheticID] = nil
        end

        if not queryMapIDs then
            return parts, 0
        end

        for index = 1, #queryMapIDs do
            local queryMapID = queryMapIDs[index]
            local poiIDs = C_AreaPoiInfo.GetAreaPOIForMap(queryMapID)
            if poiIDs then
                for _, poiID in ipairs(poiIDs) do
                    local poiInfo = C_AreaPoiInfo.GetAreaPOIInfo(queryMapID, poiID)
                    if eventFrame:IsRelevantLockedAreaPOIInfo(poiInfo)
                        and (eventFrame:ShouldAdmitLockedAreaPOIForQuery(
                                mapsToQuery,
                                poiInfo,
                                allowOutsideHierarchy)
                            or (mapsToQuery and mapsToQuery[queryMapID]))
                    then
                        local linkedUiMapID = poiInfo and poiInfo.linkedUiMapID or nil
                        local ownerMapID = queryMapID
                        if linkedUiMapID and linkedUiMapID > 0
                            and mapsToQuery and mapsToQuery[linkedUiMapID]
                        then
                            ownerMapID = linkedUiMapID
                        elseif mapsToQuery and not mapsToQuery[ownerMapID]
                            and allowOutsideHierarchy ~= true
                            and mapsToQuery[mapID]
                        then
                            ownerMapID = mapID
                        end

                        if mapsToQuery and not mapsToQuery[ownerMapID] then
                            ownerMapID = queryMapID
                        end

                        local isPrimaryAreaPOI = poiInfo and poiInfo.isPrimaryMapForPOI == true
                            or false
                        local areaPOIOwnerScore = 0
                        if isPrimaryAreaPOI then
                            areaPOIOwnerScore = 3
                        elseif linkedUiMapID and linkedUiMapID > 0 then
                            if linkedUiMapID == ownerMapID then
                                areaPOIOwnerScore = 2
                            end
                        else
                            areaPOIOwnerScore = 1
                        end

                        local candidateEntry = {
                            questID = -poiID,
                            mapID = ownerMapID,
                            poiID = poiID,
                            areaPOIInfo = poiInfo,
                            linkedUiMapID = linkedUiMapID,
                            isPrimaryAreaPOI = isPrimaryAreaPOI,
                            areaPOIOwnerScore = areaPOIOwnerScore,
                        }
                        local syntheticID = candidateEntry.questID
                        local existingIndex = seen[syntheticID]
                        if existingIndex then
                            local currentEntry = entries[existingIndex]
                            local shouldReplace = not currentEntry
                            if not shouldReplace then
                                local currentScore = currentEntry.areaPOIOwnerScore or 0
                                local candidateScore = candidateEntry.areaPOIOwnerScore or 0
                                if candidateScore ~= currentScore then
                                    shouldReplace = candidateScore > currentScore
                                else
                                    local currentMapID = currentEntry.mapID or 0
                                    local candidateMapID = candidateEntry.mapID or 0
                                    if currentMapID ~= candidateMapID then
                                        local currentIsParent = currentMapID ~= 0
                                            and candidateMapID ~= 0
                                            and GetDescendantMapSet(currentMapID)[candidateMapID]
                                        local candidateIsParent = currentMapID ~= 0
                                            and candidateMapID ~= 0
                                            and GetDescendantMapSet(candidateMapID)[currentMapID]
                                        if currentIsParent ~= candidateIsParent then
                                            shouldReplace = currentIsParent and true or false
                                        else
                                            shouldReplace = candidateMapID > currentMapID
                                        end
                                    end
                                end
                            end

                            if shouldReplace then
                                entries[existingIndex] = candidateEntry
                                parts[existingIndex] = BuildLiveRelevantAreaPOISnapshotPart(
                                    candidateEntry.questID,
                                    candidateEntry.mapID,
                                    candidateEntry.poiID,
                                    candidateEntry.areaPOIInfo)
                            end
                        else
                            partCount = partCount + 1
                            seen[syntheticID] = partCount
                            entries[partCount] = candidateEntry
                            parts[partCount] = BuildLiveRelevantAreaPOISnapshotPart(
                                candidateEntry.questID,
                                candidateEntry.mapID,
                                candidateEntry.poiID,
                                candidateEntry.areaPOIInfo)
                        end
                    end
                end
            end
        end

        for index = partCount + 1, #parts do
            parts[index] = nil
        end

        for index = partCount + 1, #entries do
            entries[index] = nil
        end

        if partCount == 0 then
            return parts, 0
        end

        WOW.table_sort(parts)
        return parts, partCount
    end

    function eventFrame:SyncLiveRelevantAreaPOISnapshot(mapID)
        local parts, partCount = eventFrame._BuildLiveRelevantAreaPOISnapshot(mapID)
        SetLiveRelevantAreaPOISnapshotParts(parts or {}, partCount or 0)
    end

    function eventFrame:HasLiveRelevantAreaPOIStateChange(mapID)
        local parts, partCount = eventFrame._BuildLiveRelevantAreaPOISnapshot(mapID)
        parts = parts or {}
        partCount = partCount or 0

        if HasLiveRelevantAreaPOISnapshotDifference(parts, partCount) then
            SetLiveRelevantAreaPOISnapshotParts(parts, partCount)
            return true
        end

        return false
    end

    function eventFrame:SyncVisibleLockedAreaPOISnapshot()
        self._visibleLockedAreaPOISnapshot = eventFrame._BuildVisibleLockedAreaPOISnapshot()
    end

    function eventFrame:HasVisibleLockedAreaPOIStateChange()
        local snapshot = eventFrame._BuildVisibleLockedAreaPOISnapshot()
        if snapshot ~= self._visibleLockedAreaPOISnapshot then
            self._visibleLockedAreaPOISnapshot = snapshot
            return true
        end

        return false
    end
end

function ns.HasVisibleQuestRemoval()
    if not ns.IsWorldQuestsRefreshContextActive()
        or not currentQuestEntries
        or #currentQuestEntries == 0
        or not WOW.C_TaskQuest
        or not WOW.C_TaskQuest.IsActive
    then
        return false
    end

    for _, entry in ipairs(currentQuestEntries) do
        local questID = entry.questID
        if questID and questID > 0
            and not entry.isLocked
            and not entry.isAreaPOI
            and not WOW.C_TaskQuest.IsActive(questID)
        then
            return true
        end
    end

    return false
end

local RefreshPanel

do

-- Applies quest data to a row frame.
-- row: Frame from AcquireQuestRow()
-- entry: enriched quest entry from GatherQuestsForCurrentMap()
eventFrame._PopulateQuestRow = function(row, entry, animate)
    local questID = entry.questID
    local rewardTopOffset = -37
    row.questID = questID
    row.questEntry = entry
    row.isAreaPOI = entry.isAreaPOI == true
    row.poiTitle = entry.isAreaPOI and (entry.title or entry.poiName) or nil
    row.poiUnlockText = entry.isAreaPOI and entry.unlockText or nil
    ApplyQuestRowLayout(row, entry)

    UpdateQuestRowTimeDisplay(row, entry)

    -- Title; super-tracked overrides locked styling everywhere.
    -- If title is nil (data not loaded yet), show a grey "Loading..." placeholder.
    local superTrackedID = WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedQuestID
        and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil
    local displayTitle = entry.title
    if displayTitle and displayTitle ~= "" then
        ApplyQuestRowTitleColor(row.titleText, questID, entry.isLocked, false)
        row.titleText:SetText(displayTitle)
    elseif entry.isLocked then
        ApplyQuestRowTitleColor(row.titleText, questID, true, false)
        row.titleText:SetText(QUEST_TYPE_LABEL.special_assignment)
    else
        ApplyQuestRowTitleColor(row.titleText, questID, false, true)
        row.titleText:SetText("Loading\226\128\166")
    end

    -- POI button: sync quest ID and tracking-state visual.
    -- Always use WorldQuest style; POIButtonUtil.Style.SuperTracked does not
    -- exist in 12.0 and would be nil, which causes POIButton_UpdateNormalStyle
    -- to skip the WorldQuest branch and never re-set questTagInfo â€” leaving
    -- elite/boss POI dragon borders permanently hidden after a tracking change.
    local isSuperTracked = superTrackedID and (superTrackedID == questID)
    if entry.isAreaPOI then
        row.poiBtn.questID = nil
        row.poiBtn.areaPoiID = entry.poiID
        if POIButtonUtil and row.poiBtn.SetStyle then
            row.poiBtn:SetStyle(POIButtonUtil.Style.AreaPOI)
        end
        if row.poiBtn.SetAreaPOIInfo and entry.areaPOIInfo then
            row.poiBtn:SetAreaPOIInfo(entry.areaPOIInfo)
        end
        local areaPOIIcon = eventFrame._poiButtonAreaPOIIcons and eventFrame._poiButtonAreaPOIIcons[row.poiBtn] or nil
        if areaPOIIcon then
            local atlas = entry.areaPOIInfo and entry.areaPOIInfo.atlasName
            if atlas and areaPOIIcon.SetAtlas then
                areaPOIIcon:SetAtlas(atlas, true)
                areaPOIIcon:Show()
            else
                areaPOIIcon:Hide()
            end
        end
        do
            local trackedType, trackedID
            if WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedMapPin then
                trackedType, trackedID = WOW.C_SuperTrack.GetSuperTrackedMapPin()
            end
            local mapPinType = Enum and Enum.SuperTrackingMapPinType
                and Enum.SuperTrackingMapPinType.AreaPOI or 0
            local isTrackedPOI = trackedType == mapPinType and trackedID == entry.poiID
            if row.poiBtn.SetSelected then
                row.poiBtn:SetSelected(isTrackedPOI and true or false)
            end
            if row.poiBtn.UpdateSelected then
                row.poiBtn:UpdateSelected()
            end
        end
        if row.poiBtn.UpdateButtonStyle then
            row.poiBtn:UpdateButtonStyle()
        end
        if row.poiBtn.Display and row.poiBtn.Display.SetIconShown then
            row.poiBtn.Display:SetIconShown(false)
        elseif row.poiBtn.Display and row.poiBtn.Display.Icon then
            row.poiBtn.Display.Icon:SetAlpha(0)
        end
        if row.poiBtn.Display and row.poiBtn.Display.SubTypeIcon then
            row.poiBtn.Display.SubTypeIcon:Hide()
        end
        row.poiBtn:Show()
    else
        ClearPOIButtonAreaPOIState(row.poiBtn)
        if row.poiBtn.SetQuestID then
            row.poiBtn:SetQuestID(questID)
        end
        row.poiBtn.questID = questID
        row.poiBtn.areaPoiID = nil
        if POIButtonUtil and row.poiBtn.SetStyle then
            row.poiBtn:SetStyle(POIButtonUtil.Style.WorldQuest)
            row.poiBtn:SetSelected(isSuperTracked and true or false)
            if row.poiBtn.UpdateButtonStyle then row.poiBtn:UpdateButtonStyle() end
        end
        if row.poiBtn.Display and row.poiBtn.Display.SetIconShown then
            row.poiBtn.Display:SetIconShown(true)
        elseif row.poiBtn.Display and row.poiBtn.Display.Icon then
            row.poiBtn.Display.Icon:SetAlpha(1)
        end
        local areaPOIIcon = eventFrame._poiButtonAreaPOIIcons and eventFrame._poiButtonAreaPOIIcons[row.poiBtn] or nil
        if areaPOIIcon then
            areaPOIIcon:Hide()
        end
        row.poiBtn:Show()
    end

    row.contractBtn.auraInstanceID = nil
    row.contractBtn.spellID = nil
    row.contractBtn:Hide()
    ResetQuestRowRewards(row)

    if entry.isLocked then
        local lockedText = entry.unlockText
        if not lockedText and not entry.isAreaPOI then
            lockedText = GetLockedSpecialAssignmentUnlockText(questID)
        end
        row.factionText:SetText("Locked: " .. (lockedText or "Unlock requirements not available yet."))
        row.factionText:SetTextColor(
            COL.rowLockedRequirement[1],
            COL.rowLockedRequirement[2],
            COL.rowLockedRequirement[3])
        row.factionText:Show()
        -- Match ShowQuestTooltip: bounty lock quest rewards (not raw POI widgets), so
        -- weekly cache tier matches the tooltip when widgets list multiple tiers.
        local usedBountyLockRowRewards = false
        if entry.isAreaPOI then
            local bountyLockQuestID = ResolveBountyLockQuestIDFromQuestEntry(entry)
            if bountyLockQuestID then
                local function showBountyRewardLoadingLine()
                    row.moneyText:ClearAllPoints()
                    row.moneyText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, rewardTopOffset)
                    row.moneyText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, rewardTopOffset)
                    if row.moneyText.SetWordWrap then
                        row.moneyText:SetWordWrap(false)
                    end
                    if row.moneyText.SetMaxLines then
                        row.moneyText:SetMaxLines(1)
                    end
                    row.moneyText:SetText(RETRIEVING_DATA or "Loading rewards...")
                    row.moneyText:SetTextColor(COL.rowTime[1], COL.rowTime[2], COL.rowTime[3])
                    row.moneyText:Show()
                    usedBountyLockRowRewards = true
                end

                -- Until quest reward APIs *and* item client data agree (QUEST_LOG_UPDATE /
                -- QUEST_DATA_LOAD_RESULT / ITEM_DATA_LOAD_RESULT / GET_ITEM_INFO_RECEIVED),
                -- do not paint widget-scraped rewards (they show the wrong weekly-cache tier early).
                if not WOW.HaveQuestRewardData or not WOW.HaveQuestRewardData(bountyLockQuestID) then
                    rewardPreloadState.QueueQuestRewardPreload(bountyLockQuestID, animate == true)
                    showBountyRewardLoadingLine()
                elseif not rewardPreloadState.IsQuestRewardDisplayReady(bountyLockQuestID) then
                    showBountyRewardLoadingLine()
                else
                    local bountyRewards = GetQuestRewards(bountyLockQuestID)
                    local numExpectedRewardItems = GetNumQuestLogRewards
                        and GetNumQuestLogRewards(bountyLockQuestID) or 0
                    local numExpectedChoiceItems = NomTools_GetExpectedQuestChoiceItemSlotCount(
                        bountyLockQuestID)
                    local numExpectedItemRows = numExpectedRewardItems + numExpectedChoiceItems
                    local bountyItemRewardRows = 0
                    if bountyRewards then
                        for _, rr in ipairs(bountyRewards) do
                            if rr.rewardType == "item" then
                                bountyItemRewardRows = bountyItemRewardRows + 1
                            end
                        end
                    end
                    local bountyItemRewardsIncomplete = WOW.HaveQuestRewardData(bountyLockQuestID)
                        and numExpectedItemRows > 0
                        and bountyItemRewardRows < numExpectedItemRows

                    if bountyRewards and #bountyRewards > 0 and not bountyItemRewardsIncomplete then
                        ApplyQuestRewardListToRow(row, bountyRewards, bountyLockQuestID, rewardTopOffset)
                        usedBountyLockRowRewards = true
                    elseif bountyItemRewardsIncomplete then
                        -- GetQuestRewards may return {} while parallel item rows materialize; do not
                        -- fall back to POI widgets (they often list Adventurer before Veteran).
                        showBountyRewardLoadingLine()
                        if WOW.C_Timer and WOW.C_Timer.After then
                            eventFrame._bountyIncompleteRefreshGeneration =
                                (eventFrame._bountyIncompleteRefreshGeneration or 0) + 1
                            local refreshGen = eventFrame._bountyIncompleteRefreshGeneration
                            WOW.C_Timer.After(0.2, function()
                                if eventFrame._bountyIncompleteRefreshGeneration ~= refreshGen then
                                    return
                                end
                                if ns.IsWorldQuestsRefreshContextActive() then
                                    ScheduleRefresh(false, "Bounty lock parallel rewards materializing")
                                end
                            end)
                        end
                    end
                end
            end
        end
        if not usedBountyLockRowRewards and entry.isAreaPOI and entry.areaPOIRewards and #entry.areaPOIRewards > 0 then
            local goldReward = nil
            local nonGoldIdx = 0
            for _, rewardData in ipairs(entry.areaPOIRewards) do
                if rewardData.rewardType == "gold" then
                    goldReward = rewardData
                else
                    nonGoldIdx = nonGoldIdx + 1
                    if nonGoldIdx <= K.MAX_REWARD_ICONS then
                        local btn = row.rewardIcons[nonGoldIdx]
                        btn.iconTex:SetTexture(rewardData.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
                        btn.rewardType = rewardData.rewardType
                        btn.amount = rewardData.amount
                        btn.itemID = rewardData.itemID
                        btn.currencyID = rewardData.currencyID
                        btn.factionID = rewardData.factionID
                        btn.currencyLabel = rewardData.currencyLabel
                        btn.label = rewardData.label
                        btn.ilvl = rewardData.ilvl
                        btn.quality = rewardData.quality
                        btn.questRewardType = rewardData.questRewardType
                        btn.questID = nil
                        btn.rewardIndex = rewardData.rewardIndex
                        if btn.SetID then
                            btn:SetID(rewardData.rewardIndex or 0)
                        end
                        if rewardData.rewardType == "item" then
                            if ns._WorldQuestsShouldShowRewardItemLevel(rewardData) then
                                btn.countText:SetText(rewardData.ilvl)
                                btn.countText:SetTextColor(1, 1, 1)
                            elseif rewardData.amount and rewardData.amount > 1 then
                                btn.countText:SetText(rewardData.amount)
                                btn.countText:SetTextColor(1, 1, 1)
                            else
                                btn.countText:SetText("")
                            end
                        elseif rewardData.rewardType == "currency" then
                            local displayAmount = ns.FormatCompactRewardAmount(rewardData.amount)
                            if displayAmount then
                                btn.countText:SetText(displayAmount)
                                btn.countText:SetTextColor(1, 1, 1)
                            else
                                btn.countText:SetText("")
                            end
                        elseif rewardData.rewardType == "rep" then
                            local displayAmount = ns.FormatCompactRewardAmount(rewardData.amount)
                            if displayAmount then
                                btn.countText:SetText(displayAmount)
                                btn.countText:SetTextColor(0.4, 0.8, 1)
                            else
                                btn.countText:SetText("")
                            end
                        else
                            btn.countText:SetText("")
                        end
                        btn:Show()
                        if btn.quality and ITEM_QUALITY_COLORS
                            and ITEM_QUALITY_COLORS[btn.quality]
                        then
                            local qc = ITEM_QUALITY_COLORS[btn.quality]
                            btn.borderTop:SetColorTexture(qc.r, qc.g, qc.b, 1)
                            btn.borderBottom:SetColorTexture(qc.r, qc.g, qc.b, 1)
                            btn.borderLeft:SetColorTexture(qc.r, qc.g, qc.b, 1)
                            btn.borderRight:SetColorTexture(qc.r, qc.g, qc.b, 1)
                            btn.borderTop:Show()
                            btn.borderBottom:Show()
                            btn.borderLeft:Show()
                            btn.borderRight:Show()
                        end
                    end
                end
            end
            if goldReward then
                row.moneyText:ClearAllPoints()
                if nonGoldIdx == 0 then
                    row.moneyText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -37)
                    row.moneyText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, -37)
                else
                    local lastBtn = row.rewardIcons[math.min(nonGoldIdx, K.MAX_REWARD_ICONS)]
                    row.moneyText:SetPoint("LEFT", lastBtn, "RIGHT", 6, 0)
                end
                row.moneyText:SetText(goldReward.label or "")
                row.moneyText:SetTextColor(1, 1, 1)
                row.moneyText:Show()
            else
                row.moneyText:SetText("")
                row.moneyText:Hide()
            end
        elseif entry.isAreaPOI and entry.rewardText and entry.rewardText ~= "" then
            row.moneyText:ClearAllPoints()
            row.moneyText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, -37)
            row.moneyText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, -37)
            if row.moneyText.SetWordWrap then
                row.moneyText:SetWordWrap(false)
            end
            if row.moneyText.SetMaxLines then
                row.moneyText:SetMaxLines(1)
            end
            row.moneyText:SetText(entry.rewardText)
            row.moneyText:SetTextColor(1, 1, 1)
            row.moneyText:Show()
        else
            row.moneyText:SetText("")
            row.moneyText:Hide()
        end
    else
        -- Faction / tag line â€” prefer a fresh query so the row mirrors Blizzard's
        -- current task-quest faction label, including active-contract overrides.
        local faction = GetQuestFactionLabel(questID) or entry.faction
        if faction and faction ~= "" then
            row.factionText:SetText(faction)
            local factionColor = COL.rowFaction
            local hasActiveContract = DoesFactionMatchContract(currentRelevantContract, faction)
            if hasActiveContract then
                factionColor = COL.rowFactionActiveContract
            end
            row.factionText:SetTextColor(
                factionColor[1], factionColor[2], factionColor[3])
            row.factionText:Show()

            if hasActiveContract then
                local textWidth = row.factionText.GetUnboundedStringWidth
                    and row.factionText:GetUnboundedStringWidth()
                    or row.factionText:GetStringWidth()
                    or 0
                local maxOffset = math.max(0, row:GetWidth() - 54)
                row.contractBtn:ClearAllPoints()
                row.contractBtn:SetPoint(
                    "LEFT", row.factionText, "LEFT", math.min(textWidth + 4, maxOffset), 0)
                row.contractBtn.auraInstanceID = currentRelevantContract.auraInstanceID
                row.contractBtn.spellID = currentRelevantContract.spellID
                row.contractBtn:Show()
            end
        else
            row.factionText:SetText("")
            row.factionText:SetTextColor(
                COL.rowFaction[1], COL.rowFaction[2], COL.rowFaction[3])
            row.factionText:Hide()
            rewardTopOffset = -23
        end
    end

    if row.rewardIcons and row.rewardIcons[1] then
        row.rewardIcons[1]:ClearAllPoints()
        row.rewardIcons[1]:SetPoint("TOPLEFT", row, "TOPLEFT", 36, rewardTopOffset)
    end

    -- Rewards with amount overlays.
    -- Gold is separated out and shown as a text label (moneyText) so the full
    -- denomination fits.  All other reward types fill the icon slots.
    -- Locked Area POI rows already populated reward icons / text above.
    if not entry.isLocked or not entry.isAreaPOI then
        if not rewardPreloadState.IsQuestRewardDisplayReady(entry) then
            rewardPreloadState.QueueQuestRewardPreload(questID, animate == true)
            row.moneyText:ClearAllPoints()
            row.moneyText:SetPoint("TOPLEFT", row, "TOPLEFT", 36, rewardTopOffset)
            row.moneyText:SetPoint("TOPRIGHT", row, "TOPRIGHT", -20, rewardTopOffset)
            if row.moneyText.SetWordWrap then
                row.moneyText:SetWordWrap(false)
            end
            if row.moneyText.SetMaxLines then
                row.moneyText:SetMaxLines(1)
            end
            row.moneyText:SetText(RETRIEVING_DATA or "Loading rewards...")
            row.moneyText:SetTextColor(COL.rowTime[1], COL.rowTime[2], COL.rowTime[3])
            row.moneyText:Show()
        else
            ApplyQuestRewardListToRow(row, GetQuestRewards(questID), questID, rewardTopOffset)
        end
    end

    -- Show the row now that all content is applied (suppresses blank-row flash).
    if animate == true then
        if row.fadeIn then
            row.fadeIn:Stop()
        end
        row:SetAlpha(0)
        row:Show()
        if row.fadeIn then
            row.fadeIn:Play()
        else
            row:SetAlpha(1)
        end
    elseif animate == false then
        row:SetAlpha(1)
        row:Show()
    end

    if activeHoverState.questID and row.questID == activeHoverState.questID then
        eventFrame._DebugHoverTrace(
            "RowShow",
            "phase=populate questID=%s animate=%s row=%s",
            tostring(row.questID),
            tostring(animate),
            eventFrame._GetHoverRowIdentity(row))
    end

    if activeHoverState.questID == questID then
        activeHoverState.ResumeForShownRow(row, "populate")
    end

    -- When animate is nil: data populated, visibility unchanged (deferred reveal).
end

-- =============================================
-- Panel layout
-- =============================================

-- Clears all active content rows and headers back to pools.
local function ClearActiveContent()
    local activeRowCount = 0
    local activeZoneCount = 0
    for _, item in ipairs(activeContent) do
        if item.type == "row" then
            activeRowCount = activeRowCount + 1
        elseif item.type == "zone" then
            activeZoneCount = activeZoneCount + 1
        end
    end
    if #activeContent > 0 then
        eventFrame._DebugHoverTrace(
            "ClearActiveContent",
            "items=%s rows=%s zones=%s hoverQuestID=%s",
            tostring(#activeContent),
            tostring(activeRowCount),
            tostring(activeZoneCount),
            tostring(activeHoverState.questID))
    end

    if #activeContent > 0 and activeTooltipAnchor then
        ClearActiveQuestTooltip()
    end
    for _, item in ipairs(activeContent) do
        if item.type == "row" and item.frame then
            ReleaseQuestRow(item.frame, true)
        elseif item.type == "zone" then
            ReleaseZoneHeader(item.frame)
        end
    end
    wipe(activeContent)
end

function eventFrame:InvalidateActiveWorldQuestsLayout()
    layoutGeneration = layoutGeneration + 1
    ClearActiveContent()
    currentQuestEntries = {}
    wipe(eventFrame._currentQuestEntriesByID)
    if noQuestsLabel then
        noQuestsLabel:Hide()
    end
    if scrollChild then
        scrollChild:SetHeight(60)
    end
    if scrollFrame and scrollFrame.UpdateScrollChildRect then
        scrollFrame:UpdateScrollChildRect()
    end
end

-- Rebuilds the scroll child contents from the provided quest list.
-- quests: array of enriched entries from GatherQuestsForCurrentMap()
local function LayoutScrollContent(quests, animateRows)
    -- Bump generation so any in-flight batch from a previous layout self-cancels.
    layoutGeneration = layoutGeneration + 1
    local myGen = layoutGeneration

    local clearStart = debugprofilestop()
    ClearActiveContent()
    local clearElapsed = debugprofilestop() - clearStart

    -- Derive content width from the scroll frame; fall back if not sized yet.
    local contentWidth = scrollFrame and scrollFrame:GetWidth() or 220
    if contentWidth < 60 then contentWidth = 220 end

    if not quests or #quests == 0 then
        if noQuestsLabel then
            local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil
            if eventFrame:IsWorldQuestDescendantGatherPending(mapID) then
                noQuestsLabel:SetText(RETRIEVING_DATA or "Loading world quests...")
            else
                noQuestsLabel:SetText("No active world quests in this zone.")
            end
        end
        noQuestsLabel:Show()
        scrollChild:SetHeight(60)
        return
    end
    noQuestsLabel:SetText("No active world quests in this zone.")
    noQuestsLabel:Hide()

    local zoneGroupsStart = debugprofilestop()
    local zoneGroups = BuildSortedZoneGroups(quests)
    local zoneGroupsElapsed = debugprofilestop() - zoneGroupsStart
    local isMultiZone = #zoneGroups > 1
    local pendingRowItems = {}

    -- â”€â”€ Phase 1: acquire frames and set positions (synchronous, fast) â”€â”€â”€â”€â”€â”€â”€â”€
    -- No heavy WoW API calls here â€” the scroll skeleton (zone headers + row
    -- placeholders) appears instantly in the same frame as the layout command.
    -- Per-quest data (reward icons, text, POI styling) fills in via Phase 2.
    -- Compute viewport bounds BEFORE Phase 1 for lazy frame creation.
    local scrollOffset
    if animateRows then
        scrollOffset = 0
    else
        scrollOffset = scrollFrame and scrollFrame:GetVerticalScroll() or 0
    end
    local viewportHeight = scrollFrame and scrollFrame:GetHeight() or 420
    if viewportHeight < 1 then viewportHeight = 420 end
    local viewportBottom = scrollOffset + viewportHeight

    local skeletonStart = debugprofilestop()
    local currentY = 8
    for _, zoneGroup in ipairs(zoneGroups) do
        local zoneName = zoneGroup.name
        local zoneEntries = zoneGroup.quests
        local isZoneCollapsed = false

        if isMultiZone then
            isZoneCollapsed = collapseState[zoneName] or false

            local hdr = AcquireZoneHeader(scrollChild)
            hdr:SetWidth(contentWidth - 15)
            hdr:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 9, -currentY)
            hdr.label:SetText(zoneName)
            hdr.countLabel:SetText(#zoneEntries .. " quest"
                .. (#zoneEntries ~= 1 and "s" or ""))
            hdr.zoneName = zoneName
            hdr:SetCollapsed(isZoneCollapsed)
            currentY = currentY + K.ZONE_HEADER_HEIGHT
            activeContent[#activeContent + 1] = { type = "zone", frame = hdr }
        end

        if not isMultiZone or not isZoneCollapsed then
            for _, entry in ipairs(zoneEntries) do
                local rowHeight = GetQuestRowHeight(entry)
                local rowTop = currentY
                local rowBottom = currentY + rowHeight
                local isInViewport = rowTop < viewportBottom and rowBottom > scrollOffset

                local row = nil
                if isInViewport then
                    row = AcquireQuestRow(scrollChild)
                    row:SetWidth(contentWidth)
                    row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -currentY)
                    row:SetHeight(rowHeight)
                    row.questID = entry.questID
                    row.mapID   = entry.mapID
                    row.mapName = entry.mapName
                    row.questEntry = entry
                end

                local rowItem = {
                    type = "row", frame = row, _entry = entry, _animate = animateRows == true,
                    _top = currentY, _bottom = currentY + rowHeight,
                    _height = rowHeight, _contentWidth = contentWidth }
                activeContent[#activeContent + 1] = rowItem
                pendingRowItems[#pendingRowItems + 1] = rowItem
                currentY = currentY + rowHeight
            end
        end
    end
    local skeletonElapsed = debugprofilestop() - skeletonStart

    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: Phase1 clear=%.1fms zoneGroups=%.1fms skeleton=%.1fms rows=%d",
            clearElapsed / 1000, zoneGroupsElapsed / 1000, skeletonElapsed / 1000, #pendingRowItems))
    end

    scrollChild:SetHeight(math.max(currentY + 4, 60))
    if scrollFrame and scrollFrame.UpdateScrollChildRect then
        scrollFrame:UpdateScrollChildRect()
    end

    -- Reset scroll to the top when displaying new content (map change).
    if animateRows and scrollFrame then
        scrollFrame:SetVerticalScroll(0)
        if scrollFrame.ScrollBar and scrollFrame.ScrollBar.SetScrollPercentage then
            scrollFrame.ScrollBar:SetScrollPercentage(0)
        end
    end

    if ns.IsDebugEnabled() and scrollFrame then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: LayoutScrollContent pre-split scrollPos=%.0f frameH=%.0f contentH=%.0f rows=%d",
            scrollFrame:GetVerticalScroll(),
            scrollFrame:GetHeight(),
            scrollChild:GetHeight(),
            #pendingRowItems))
    end

    -- â”€â”€ Viewport split: prioritize visible rows â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local visibleRowItems = {}
    local deferredRowItems = {}

    for _, item in ipairs(pendingRowItems) do
        if item._top < viewportBottom and item._bottom > scrollOffset then
            visibleRowItems[#visibleRowItems + 1] = item
        else
            deferredRowItems[#deferredRowItems + 1] = item
        end
    end

    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: LayoutScrollContent total=%d visible=%d deferred=%d scrollOffset=%.0f viewportH=%.0f viewportBottom=%.0f animate=%s",
            #pendingRowItems, #visibleRowItems, #deferredRowItems,
            scrollOffset, viewportHeight, viewportBottom,
            tostring(animateRows)))
        if #visibleRowItems > 0 then
            local firstVis = visibleRowItems[1]
            local lastVis = visibleRowItems[#visibleRowItems]
            ns.DebugPrint(WOW.string_format(
                "WorldQuests:   visible range: top=%.0f..%.0f  questIDs: first=%s last=%s",
                firstVis._top, lastVis._bottom,
                tostring(firstVis.frame.questID), tostring(lastVis.frame.questID)))
        end
        if #deferredRowItems > 0 then
            local firstDef = deferredRowItems[1]
            local lastDef = deferredRowItems[#deferredRowItems]
            ns.DebugPrint(WOW.string_format(
                "WorldQuests:   deferred range: top=%.0f..%.0f  count=%d",
                firstDef._top, lastDef._bottom, #deferredRowItems))
        end
    end

    if eventFrame._hoverLastLayoutGenLogged ~= myGen then
        eventFrame._hoverLastLayoutGenLogged = myGen
        eventFrame._DebugHoverTrace(
            "LayoutSummary",
            "gen=%s reason=%s questCount=%s visible=%s deferred=%s animateRows=%s activeHoverQuestID=%s",
            tostring(myGen),
            tostring(eventFrame._lastLayoutReason or "unspecified"),
            tostring(quests and #quests or 0),
            tostring(#visibleRowItems),
            tostring(#deferredRowItems),
            tostring(animateRows == true),
            tostring(activeHoverState.questID))
    end

    -- â”€â”€ Phase 2: populate visible rows only â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    do
        local debugprofilestop = debugprofilestop
        local POPULATE_BUDGET_MS = 6

        local visIdx = 1
        local function populateVisibleBatch()
            if layoutGeneration ~= myGen then return end
            local budgetStart = debugprofilestop()
            while visIdx <= #visibleRowItems do
                local item = visibleRowItems[visIdx]
                if item._entry ~= nil then
                    eventFrame._PopulateQuestRow(item.frame, item._entry, nil)
                    item._entry = nil
                end
                visIdx = visIdx + 1
                if debugprofilestop() - budgetStart > POPULATE_BUDGET_MS * 1000 then
                    return false
                end
            end
            return true
        end

        local p2Start = debugprofilestop()
        local allVisibleDone = populateVisibleBatch()
        local p2Elapsed = debugprofilestop() - p2Start
        if ns.IsDebugEnabled() then
            ns.DebugPrint(WOW.string_format(
                "WorldQuests: Phase2 sync elapsed=%.1fms rows=%d allDone=%s",
                p2Elapsed / 1000, #visibleRowItems, tostring(allVisibleDone)))
        end
        if not allVisibleDone then
            local function continueVisible()
                if layoutGeneration ~= myGen then return end
                local done = populateVisibleBatch()
                if not done then
                    WOW.C_Timer.After(0.01, continueVisible)
                end
            end
            WOW.C_Timer.After(0.01, continueVisible)
        end
    end

    -- â”€â”€ On-scroll: populate deferred rows entering the viewport on demand â”€â”€â”€â”€
    scrollFrame._nomtoolsOnScroll = function()
        if layoutGeneration ~= myGen then
            scrollFrame._nomtoolsOnScroll = nil
            return
        end
        local scrollOff = scrollFrame:GetVerticalScroll()
        local viewH = scrollFrame:GetHeight()
        if viewH < 1 then return end
        local viewBot = scrollOff + viewH
        for i = 1, #pendingRowItems do
            local item = pendingRowItems[i]
            if item._entry ~= nil and item._top < viewBot and item._bottom > scrollOff then
                if not item.frame then
                    local row = AcquireQuestRow(scrollChild)
                    row:SetWidth(item._contentWidth)
                    row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -item._top)
                    row:SetHeight(item._height)
                    row.questID = item._entry.questID
                    row.mapID = item._entry.mapID
                    row.mapName = item._entry.mapName
                    item.frame = row
                end
                eventFrame._PopulateQuestRow(item.frame, item._entry, false)
                item._entry = nil
                if ns.IsDebugEnabled() then
                    ns.DebugPrint(WOW.string_format(
                        "WorldQuests: OnScroll populate questID=%s top=%.0f",
                        tostring(item.frame.questID), item._top))
                end
            end
        end
    end

    if not scrollFrame._nomtoolsScrollHooked then
        scrollFrame:HookScript("OnVerticalScroll", function()
            if scrollFrame._nomtoolsOnScroll then scrollFrame._nomtoolsOnScroll() end
        end)
        scrollFrame._nomtoolsScrollHooked = true
    end

    -- â”€â”€ Idle pre-populate: very slowly process off-screen rows â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local idleIdx = 1
    local IDLE_INTERVAL = 0.05
    local IDLE_BATCH = 8

    local function idlePopulate()
        if layoutGeneration ~= myGen then return end
        local processed = 0
        while idleIdx <= #deferredRowItems and processed < IDLE_BATCH do
            local item = deferredRowItems[idleIdx]
            if item._entry ~= nil then
                if not item.frame then
                    local row = AcquireQuestRow(scrollChild)
                    row:SetWidth(item._contentWidth)
                    row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -item._top)
                    row:SetHeight(item._height)
                    row.questID = item._entry.questID
                    row.mapID = item._entry.mapID
                    row.mapName = item._entry.mapName
                    item.frame = row
                end
                eventFrame._PopulateQuestRow(item.frame, item._entry, false)
                item._entry = nil
                if ns.IsDebugEnabled() then
                    ns.DebugPrint(WOW.string_format(
                        "WorldQuests: Idle populate questID=%s idx=%d",
                        tostring(item.frame.questID), idleIdx))
                end
                processed = processed + 1
            end
            idleIdx = idleIdx + 1
        end
        if idleIdx <= #deferredRowItems then
            WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
        end
    end

    -- â”€â”€ Phase 3: reveal visible rows only â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if animateRows then
        local ROW_REVEAL_INTERVAL = 0.025
        local populateRetryCount = 0

        local function PrimeRevealPreload(item)
            local row = item and item.frame or nil
            if row and row.questID then
                rewardPreloadState.QueueQuestRewardPreload(row.questID, true)
            end
        end

        local visRevealIdx = 1
        local function revealNextVisibleRow()
            if layoutGeneration ~= myGen or not ns.IsWorldQuestsRefreshContextActive() then
                return
            end

            if visRevealIdx > #visibleRowItems then
                -- All visible rows revealed; start idle pre-populate for off-screen
                if #deferredRowItems > 0 then
                    WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
                end
                return
            end

            local item = visibleRowItems[visRevealIdx]

            if item._entry ~= nil then
                populateRetryCount = populateRetryCount + 1
                if populateRetryCount > 100 then
                    if #deferredRowItems > 0 then
                        WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
                    end
                    return
                end
                WOW.C_Timer.After(0.01, revealNextVisibleRow)
                return
            end
            populateRetryCount = 0

            local row = item.frame
            if row.fadeIn then row.fadeIn:Stop() end
            row:SetAlpha(0)
            row:Show()
            if row.fadeIn then
                row.fadeIn:Play()
            else
                row:SetAlpha(1)
            end
            if activeHoverState.questID and row.questID == activeHoverState.questID then
                eventFrame._DebugHoverTrace(
                    "RowShow",
                    "phase=reveal-visible questID=%s row=%s",
                    tostring(row.questID),
                    eventFrame._GetHoverRowIdentity(row))
            end
            activeHoverState.ResumeForShownRow(row, "reveal-visible")

            visRevealIdx = visRevealIdx + 1

            if visRevealIdx <= #visibleRowItems then
                PrimeRevealPreload(visibleRowItems[visRevealIdx])
                WOW.C_Timer.After(ROW_REVEAL_INTERVAL, revealNextVisibleRow)
            else
                -- All visible revealed; start idle pre-populate
                if #deferredRowItems > 0 then
                    WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
                end
            end
        end

        if visibleRowItems[1] then
            PrimeRevealPreload(visibleRowItems[1])
            revealNextVisibleRow()
        elseif #deferredRowItems > 0 then
            WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
        end
    else
        -- Non-animated: show visible rows immediately, idle pre-populate deferred
        local readyWaitCount = 0
        local function showReadyRows()
            if layoutGeneration ~= myGen then return end
            local allShown = true
            for _, item in ipairs(visibleRowItems) do
                if item._entry == nil then
                    local row = item.frame
                    if row:GetAlpha() ~= 1 or not row:IsShown() then
                        row:SetAlpha(1)
                        row:Show()
                    end
                    if activeHoverState.questID and row.questID == activeHoverState.questID then
                        eventFrame._DebugHoverTrace(
                            "RowShow",
                            "phase=show-ready questID=%s row=%s",
                            tostring(row.questID),
                            eventFrame._GetHoverRowIdentity(row))
                    end
                    activeHoverState.ResumeForShownRow(row, "show-ready")
                else
                    allShown = false
                end
            end
            if not allShown then
                readyWaitCount = readyWaitCount + 1
                if readyWaitCount > 100 then return end
                WOW.C_Timer.After(0.01, showReadyRows)
            else
                -- All visible shown; start idle pre-populate
                if #deferredRowItems > 0 then
                    WOW.C_Timer.After(IDLE_INTERVAL, idlePopulate)
                end
            end
        end
        showReadyRows()
    end
end

-- =============================================
-- Panel refresh
-- =============================================

-- Refreshes the panel contents from the current map.
RefreshPanel = function(animateRows, reason)
    if not ns.IsWorldQuestsRefreshContextActive() then return end

    local s = GetSettings()
    if not IsModuleEnabled(s) then
        eventFrame:ResetWorldQuestsRefreshGateState()
        ClearPendingDisplayModeRequest()
        return
    end

    local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil
    local previousRelevantWorldQuestMapID = eventFrame._activeRelevantWorldQuestMapID
    local previousRelevantWorldQuestQuerySignature =
        eventFrame._activeRelevantWorldQuestQuerySignature
    local gatherStart = debugprofilestop()
    local quests, hiddenRewardPreloadQuestIDs, querySignature =
        eventFrame:GatherQuestsForCurrentMap(animateRows)
    local gatherElapsed = debugprofilestop() - gatherStart
    local isDescendantGatherPending = eventFrame:IsWorldQuestDescendantGatherPending(mapID)
    local activeRelevantWorldQuestQuerySignature = querySignature
        or (mapID and tostring(mapID) or nil)
    local relevantWorldQuestQuerySignatureChanged =
        previousRelevantWorldQuestMapID ~= mapID
        or previousRelevantWorldQuestQuerySignature ~= activeRelevantWorldQuestQuerySignature

    eventFrame._lastScheduledWorldQuestsMapID = mapID
    eventFrame._activeRelevantWorldQuestMapID = mapID
    eventFrame._activeRelevantWorldQuestQuerySignature =
        activeRelevantWorldQuestQuerySignature
    currentQuestEntries = quests
    wipe(eventFrame._currentQuestEntriesByID)
    for entryIndex = 1, #quests do
        local entry = quests[entryIndex]
        if entry.questID then
            eventFrame._currentQuestEntriesByID[entry.questID] = entry
        end
    end
    rewardPreloadState.CancelDrain()
    wipe(rewardPreloadState.queuedQuestIDs)
    wipe(rewardPreloadState.queue)
    rewardPreloadState.queueHead = 1
    rewardPreloadState.queueTail = 0
    if quests and #quests > 0 then
        for _, entry in ipairs(quests) do
            if entry and not entry.isLocked and entry.isAreaPOI ~= true
                and entry.rewardDataReady == false
            then
                local questID = entry.questID
                if not pendingQuestIDs[questID] then
                    pendingQuestIDs[questID] = {
                        needsQuestData = false,
                        needsRewardData = true,
                        questDataRefreshDone = true,
                    }
                elseif not pendingQuestIDs[questID].needsRewardData then
                    pendingQuestIDs[questID].needsRewardData = true
                end
                rewardPreloadState.QueueQuestRewardPreload(questID, false)
            end
        end
    end
    if hiddenRewardPreloadQuestIDs then
        for _, questID in ipairs(hiddenRewardPreloadQuestIDs) do
            rewardPreloadState.QueueQuestRewardPreload(questID, false)
        end
    end
    rewardPreloadState.StartPoll()
    -- Synchronously drain up to 25 items to close the race window
    rewardPreloadState.DrainSyncPass(25)
    -- Schedule a fallback guarantee to catch any remaining pending quests
    rewardPreloadState.ScheduleFallbackGuarantee()
    local syncPOIElapsed = 0
    if not isDescendantGatherPending then
        local shouldSyncLiveAreaPOIs = eventFrame._lastLiveRelevantAreaPOISnapshotCount == nil
            or relevantWorldQuestQuerySignatureChanged
            or reason == "OnMapChanged"
            or reason == "AREA_POIS_UPDATED"
            or reason == "Staged descendant gather complete"
        if shouldSyncLiveAreaPOIs then
            local syncPOIStart = debugprofilestop()
            eventFrame:SyncLiveRelevantAreaPOISnapshot(mapID)
            syncPOIElapsed = debugprofilestop() - syncPOIStart
        end
    end
    local syncVisibleStart = debugprofilestop()
    eventFrame:SyncVisibleLockedAreaPOISnapshot()
    local syncVisibleElapsed = debugprofilestop() - syncVisibleStart
    if ns.IsDebugEnabled() then
        eventFrame:DebugWorldQuestsRefreshTrace(
            "RefreshPanel execute",
            WOW.string_format(
                "reason=%s animate=%s mapID=%s questCount=%s",
                tostring(reason or "unspecified"),
                tostring(animateRows ~= false),
                tostring(mapID),
                tostring(#quests)))
    end
    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: RefreshPanel timing gather=%.1fms syncPOI=%.1fms syncVisible=%.1fms",
            gatherElapsed / 1000, syncPOIElapsed / 1000, syncVisibleElapsed / 1000))
    end
    UpdateQuestLogUpdateRegistration()
    EnsureQuestDataRetryRefresh()
    local typoStart = debugprofilestop()
    ApplyWorldQuestTypography()
    local typoElapsed = debugprofilestop() - typoStart
    if ns.IsDebugEnabled() then
        ns.DebugPrint(WOW.string_format(
            "WorldQuests: RefreshPanel typography=%.1fms",
            typoElapsed / 1000))
    end
    RefreshContractNotice()
    eventFrame._lastLayoutReason = reason or "unspecified"
    LayoutScrollContent(quests, not isDescendantGatherPending and animateRows ~= false)
end

end

eventFrame._GetDelayUntilNextServerMinute = function()
    local serverTime = GetCurrentServerTime()
    local delay = 60 - (serverTime % 60)
    if delay <= 0 then
        delay = 60
    end
    return delay
end

local function UpdateCurrentQuestTimeState()
    if not currentQuestEntries or #currentQuestEntries == 0 then
        RefreshContractNotice()
        EnsureQuestDataRetryRefresh()
        return
    end

    local now = GetCurrentServerTime()
    local needsAreaPOITextRefresh = false

    for _, entry in ipairs(currentQuestEntries) do
        if entry.isAreaPOI then
            local previousExpiresAt = entry.expiresAt
            local previousTimeText = entry.timeText
            local previousAreaPOITimeText = entry.areaPOITimeText
            if entry.poiID and C_AreaPoiInfo and C_AreaPoiInfo.IsAreaPOITimed
                and C_AreaPoiInfo.GetAreaPOISecondsLeft
                and C_AreaPoiInfo.IsAreaPOITimed(entry.poiID)
            then
                local remaining = C_AreaPoiInfo.GetAreaPOISecondsLeft(entry.poiID)
                if remaining and remaining > 0 then
                    entry.timeLeft = remaining
                    entry.expiresAt = now + remaining
                elseif previousExpiresAt and previousExpiresAt > now then
                    entry.timeLeft = previousExpiresAt - now
                    entry.expiresAt = previousExpiresAt
                else
                    entry.timeLeft = nil
                    entry.expiresAt = nil
                end
            elseif previousExpiresAt and previousExpiresAt > now then
                entry.timeLeft = previousExpiresAt - now
                entry.expiresAt = previousExpiresAt
            else
                entry.timeLeft = nil
                entry.expiresAt = nil
            end

            local hasRemainingAreaPOITime = (entry.timeLeft and entry.timeLeft > 0)
                or (entry.expiresAt and entry.expiresAt > now)
            if entry.tooltipWidgetSet then
                local refreshedAreaPOITimeText = eventFrame:GetCurrentAreaPOITimeText(entry.tooltipWidgetSet)
                if refreshedAreaPOITimeText ~= nil then
                    entry.areaPOITimeText = refreshedAreaPOITimeText
                end
            end

            local hadAreaPOITimerDisplay = (previousAreaPOITimeText and previousAreaPOITimeText ~= "")
                or (previousExpiresAt and previousExpiresAt > now)
            local hasAreaPOITimerDisplay = (entry.areaPOITimeText and entry.areaPOITimeText ~= "")
                or hasRemainingAreaPOITime

            if hadAreaPOITimerDisplay and not hasAreaPOITimerDisplay then
                needsAreaPOITextRefresh = true
            end

            if entry.isLocked and entry.isAreaPOI then
                entry.timeText = FormatTimeLeft(nil)
                entry.timeLeft = nil
                entry.expiresAt = nil
            elseif entry.timeLeft and entry.timeLeft > 0 then
                entry.timeText = FormatTimeLeft(entry.timeLeft)
            elseif entry.expiresAt and entry.expiresAt > now then
                entry.timeText = FormatTimeLeft(entry.expiresAt - now)
            elseif entry.areaPOITimeText then
                entry.timeText = entry.areaPOITimeText
            elseif previousTimeText and previousTimeText ~= "" then
                entry.timeText = previousTimeText
            else
                entry.timeText = FormatTimeLeft(nil)
            end
        else
            local previousExpiresAt = entry.expiresAt
            local remaining, expiresAt = GetQuestExpirySnapshot(entry.questID, now, entry)
            if remaining and remaining > 0 then
                entry.timeLeft = remaining
                entry.timeText = FormatTimeLeft(remaining)
                entry.expiresAt = expiresAt
            elseif previousExpiresAt and previousExpiresAt > now then
                entry.timeLeft = previousExpiresAt - now
                entry.timeText = FormatTimeLeft(entry.timeLeft)
                entry.expiresAt = previousExpiresAt
            else
                entry.timeLeft = nil
                entry.expiresAt = nil
                entry.timeText = FormatTimeLeft(nil)
            end
        end
    end

    RefreshContractNotice()
    EnsureQuestDataRetryRefresh()

    if needsAreaPOITextRefresh then
        ScheduleRefresh(false)
    end

    for _, item in ipairs(activeContent) do
        if item.type == "row" then
            local entry = item._entry or (item.frame and item.frame.questEntry)
            if entry then
                if item._entry then
                    item._entry.timeLeft = entry.timeLeft
                    item._entry.timeText = entry.timeText
                elseif item.frame then
                    UpdateQuestRowTimeDisplay(item.frame, entry)
                end
            end
        end
    end
end

local function GetNextPendingQuestDataRetryTime()
    if not next(pendingQuestIDs) then
        return nil
    end

    local now = WOW.GetTime()
    local nextRetryAt
    for questID, _ in pairs(pendingQuestIDs) do
        local retryAt = questDataRetrySuppressedUntil[questID]
        if retryAt and (not nextRetryAt or retryAt < nextRetryAt) then
            nextRetryAt = retryAt
        end

    end

    return nextRetryAt
end

StopQuestDataRetryRefresh = function()
    retryRefreshGeneration = retryRefreshGeneration + 1
    retryRefreshDueAt = nil
end

EnsureQuestDataRetryRefresh = function()
    local nextRetryAt = GetNextPendingQuestDataRetryTime()
    if not nextRetryAt then
        StopQuestDataRetryRefresh()
        return
    end

    if retryRefreshDueAt and nextRetryAt == retryRefreshDueAt then
        return
    end

    retryRefreshGeneration = retryRefreshGeneration + 1
    local myGeneration = retryRefreshGeneration
    retryRefreshDueAt = nextRetryAt

    local delay = nextRetryAt - WOW.GetTime()
    if delay < 0.3 then
        delay = 0.3
    end

    WOW.C_Timer.After(delay, function()
        if retryRefreshGeneration ~= myGeneration then
            return
        end

        retryRefreshDueAt = nil

        if not ns.IsWorldQuestsRefreshContextActive() then
            return
        end

        ScheduleRefresh(false)
    end)
end

EnsureMinuteAlignedTimeUpdates = function()
    if minuteUpdateActive then
        return
    end

    minuteUpdateActive = true
    minuteUpdateGeneration = minuteUpdateGeneration + 1
    local myGeneration = minuteUpdateGeneration

    local function tick()
        if minuteUpdateGeneration ~= myGeneration then
            return
        end
        if not ns.IsWorldQuestsRefreshContextActive() then
            minuteUpdateActive = false
            return
        end

        UpdateCurrentQuestTimeState()
        WOW.C_Timer.After(eventFrame._GetDelayUntilNextServerMinute(), tick)
    end

    WOW.C_Timer.After(eventFrame._GetDelayUntilNextServerMinute(), tick)
end

StopMinuteAlignedTimeUpdates = function()
    minuteUpdateGeneration = minuteUpdateGeneration + 1
    minuteUpdateActive = false
end

-- Schedules a refresh on the next frame to debounce rapid event bursts.
ScheduleRefresh = function(animateRows, reason, immediate)
    if animateRows == nil then
        animateRows = true
    end

    local refreshReason = reason or "unspecified"

    if not ns.IsWorldQuestsRefreshContextActive() then
        if ns.IsDebugEnabled() then
            eventFrame:DebugWorldQuestsRefreshTrace(
                "ScheduleRefresh skipped inactive",
                WOW.string_format("reason=%s animate=%s", refreshReason, tostring(animateRows)))
        end
        return
    end

    if refreshPending then
        if animateRows == true then
            refreshPendingAnimateRows = true
        elseif refreshPendingAnimateRows ~= true then
            refreshPendingAnimateRows = false
        end

        if reason then
            eventFrame._refreshPendingReason = reason
        end

        if ns.IsDebugEnabled() then
            eventFrame:DebugWorldQuestsRefreshTrace(
                "ScheduleRefresh coalesced",
                WOW.string_format(
                    "reason=%s animate=%s pendingAnimate=%s pendingReason=%s",
                    refreshReason,
                    tostring(animateRows),
                    tostring(refreshPendingAnimateRows),
                    tostring(eventFrame._refreshPendingReason)))
        end
        if activeHoverState.questID then
            eventFrame._DebugHoverTrace(
                "ScheduleRefresh",
                "phase=coalesced reason=%s animate=%s pendingAnimate=%s pendingReason=%s activeQuestID=%s",
                refreshReason,
                tostring(animateRows),
                tostring(refreshPendingAnimateRows),
                tostring(eventFrame._refreshPendingReason),
                tostring(activeHoverState.questID))
        end
        return
    end

    refreshPending = true
    refreshPendingAnimateRows = animateRows
    eventFrame._refreshPendingReason = refreshReason
    if ns.IsDebugEnabled() then
        eventFrame:DebugWorldQuestsRefreshTrace(
            "ScheduleRefresh scheduled",
            WOW.string_format("reason=%s animate=%s", refreshReason, tostring(animateRows)))
    end
    if activeHoverState.questID then
        eventFrame._DebugHoverTrace(
            "ScheduleRefresh",
            "phase=scheduled reason=%s animate=%s immediate=%s activeQuestID=%s",
            refreshReason,
            tostring(animateRows),
            tostring(immediate == true),
            tostring(activeHoverState.questID))
    end
    WOW.C_Timer.After(immediate == true and 0 or 0.25, function()
        local shouldAnimate = refreshPendingAnimateRows
        local pendingReason = eventFrame._refreshPendingReason
        refreshPending = false
        refreshPendingAnimateRows = true
        eventFrame._refreshPendingReason = "unspecified"
        if not ns.IsWorldQuestsRefreshContextActive() then
            if ns.IsDebugEnabled() then
                eventFrame:DebugWorldQuestsRefreshTrace(
                    "ScheduleRefresh skipped inactive",
                    WOW.string_format(
                        "reason=%s animate=%s phase=timer",
                        tostring(pendingReason),
                        tostring(shouldAnimate)))
            end
            return
        end
        RefreshPanel(shouldAnimate, pendingReason)
    end)
end

function eventFrame:HandleWorldMapChanged()
    local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "OnMapChanged entry",
            WOW.string_format(
                "mapID=%s lastMapID=%s",
                tostring(mapID),
                tostring(self._lastScheduledWorldQuestsMapID)))
    end

    if not ns.IsWorldQuestsRefreshContextActive() then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "OnMapChanged skipped inactive",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    if not mapID then
        self:DebugWorldQuestsRefreshTrace("OnMapChanged skipped no-map", "mapID=nil")
        return
    end

    if mapID == self._lastScheduledWorldQuestsMapID then
        local hasRelevantWorldQuestQuerySignatureChange,
            previousRelevantWorldQuestQuerySignature,
            discoveredRelevantWorldQuestQuerySignature =
                self:HasRelevantWorldQuestQuerySignatureChange(mapID)

        if hasRelevantWorldQuestQuerySignatureChange then
            layoutGeneration = layoutGeneration + 1
            self:CancelWorldQuestDescendantGather()
            if ns.IsDebugEnabled() then
                self:DebugWorldQuestsRefreshTrace(
                    "OnMapChanged relevant-map signature",
                    WOW.string_format(
                        "mapID=%s previousSignature=%s discoveredSignature=%s",
                        tostring(mapID),
                        tostring(previousRelevantWorldQuestQuerySignature),
                        tostring(discoveredRelevantWorldQuestQuerySignature)))
            end
            if activeHoverState.questID then
                eventFrame._DebugHoverTrace(
                    "HandleWorldMapChanged",
                    "trigger-refresh reason=OnMapChanged signature-change mapID=%s activeQuestID=%s",
                    tostring(mapID),
                    tostring(activeHoverState.questID))
            end
            ScheduleRefresh(true, "OnMapChanged")
            return
        end

        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "OnMapChanged skipped duplicate",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    self._lastScheduledWorldQuestsMapID = mapID
    layoutGeneration = layoutGeneration + 1
    self:CancelWorldQuestDescendantGather()
    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "OnMapChanged scheduled",
            WOW.string_format("mapID=%s", tostring(mapID)))
    end
    if activeHoverState.questID then
        eventFrame._DebugHoverTrace(
            "HandleWorldMapChanged",
            "trigger-refresh reason=OnMapChanged mapID=%s activeQuestID=%s",
            tostring(mapID),
            tostring(activeHoverState.questID))
    end
    ScheduleRefresh(true, "OnMapChanged")
end

function eventFrame:HandleAreaPOIsUpdated()
    local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "AREA_POIS_UPDATED entry",
            WOW.string_format("mapID=%s", tostring(mapID)))
    end

    if not ns.IsWorldQuestsRefreshContextActive() then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "AREA_POIS_UPDATED skipped inactive",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    if not mapID then
        self:DebugWorldQuestsRefreshTrace("AREA_POIS_UPDATED skipped no-map", "mapID=nil")
        return
    end

    if self:IsWorldQuestDescendantGatherPending(mapID) then
        self:CancelWorldQuestDescendantGather()
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "AREA_POIS_UPDATED staged restart",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        if activeHoverState.questID then
            eventFrame._DebugHoverTrace(
                "HandleAreaPOIsUpdated",
                "trigger-refresh reason=AREA_POIS_UPDATED staged-restart mapID=%s activeQuestID=%s",
                tostring(mapID),
                tostring(activeHoverState.questID))
        end
        ScheduleRefresh(true, "AREA_POIS_UPDATED")
        return
    end

    local hasRelevantWorldQuestQuerySignatureChange,
        previousRelevantWorldQuestQuerySignature,
        discoveredRelevantWorldQuestQuerySignature =
            self:HasRelevantWorldQuestQuerySignatureChange(mapID)

    if hasRelevantWorldQuestQuerySignatureChange then
        layoutGeneration = layoutGeneration + 1
        rewardPreloadState.CancelDrain()
        self:CancelWorldQuestDescendantGather()
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "AREA_POIS_UPDATED relevant-map signature",
                WOW.string_format(
                    "mapID=%s previousSignature=%s discoveredSignature=%s",
                    tostring(mapID),
                    tostring(previousRelevantWorldQuestQuerySignature),
                    tostring(discoveredRelevantWorldQuestQuerySignature)))
        end
        if activeHoverState.questID then
            eventFrame._DebugHoverTrace(
                "HandleAreaPOIsUpdated",
                "trigger-refresh reason=AREA_POIS_UPDATED signature-change mapID=%s activeQuestID=%s",
                tostring(mapID),
                tostring(activeHoverState.questID))
        end
        ScheduleRefresh(true, "AREA_POIS_UPDATED")
        return
    end

    if not self:HasLiveRelevantAreaPOIStateChange(mapID) then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "AREA_POIS_UPDATED skipped unchanged",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    layoutGeneration = layoutGeneration + 1
    rewardPreloadState.CancelDrain()
    self:CancelWorldQuestDescendantGather()
    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "AREA_POIS_UPDATED scheduled",
            WOW.string_format("mapID=%s", tostring(mapID)))
    end
    if activeHoverState.questID then
        eventFrame._DebugHoverTrace(
            "HandleAreaPOIsUpdated",
            "trigger-refresh reason=AREA_POIS_UPDATED live-state-change mapID=%s activeQuestID=%s",
            tostring(mapID),
            tostring(activeHoverState.questID))
    end
    ScheduleRefresh(true, "AREA_POIS_UPDATED")
end

function eventFrame:GetLockedAreaPOIWidgetUpdateScope(widgetInfo)
    local function ScanTrackedLockedAreaPOIWidgetSetsForWidgetID(widgetID, widgetSetIDs)
        if not widgetID or widgetID <= 0 or not widgetSetIDs
            or not C_UIWidgetManager
            or not C_UIWidgetManager.GetAllWidgetsBySetID
        then
            return false
        end

        for widgetSetID in pairs(widgetSetIDs) do
            local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID)
            if widgets then
                for index = 1, #widgets do
                    local currentWidgetInfo = widgets[index]
                    if currentWidgetInfo and currentWidgetInfo.widgetID == widgetID then
                        return true
                    end
                end
            end
        end

        return false
    end

    if type(widgetInfo) ~= "table" then
        return false, false
    end

    local widgetSetID = widgetInfo.widgetSetID or 0
    local widgetID = widgetInfo.widgetID or 0
    local hasVisibleMatch = false
    local hasLiveMatch = false

    if widgetSetID > 0 then
        hasVisibleMatch = self._visibleLockedAreaPOIWidgetSetIDs
            and self._visibleLockedAreaPOIWidgetSetIDs[widgetSetID] == true or false
        hasLiveMatch = self._liveRelevantAreaPOIWidgetSetIDs
            and self._liveRelevantAreaPOIWidgetSetIDs[widgetSetID] == true or false
    elseif widgetID > 0 then
        hasVisibleMatch = ScanTrackedLockedAreaPOIWidgetSetsForWidgetID(
            widgetID,
            self._visibleLockedAreaPOIWidgetSetIDs)
        hasLiveMatch = ScanTrackedLockedAreaPOIWidgetSetsForWidgetID(
            widgetID,
            self._liveRelevantAreaPOIWidgetSetIDs)
    end

    return hasVisibleMatch, hasLiveMatch
end

function eventFrame:HandleLockedAreaPOIWidgetUpdated(widgetInfo)
    local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil
    local widgetSetID = widgetInfo and widgetInfo.widgetSetID or nil
    local widgetID = widgetInfo and widgetInfo.widgetID or nil

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "UPDATE_UI_WIDGET entry",
            WOW.string_format(
                "mapID=%s widgetSetID=%s widgetID=%s",
                tostring(mapID),
                tostring(widgetSetID),
                tostring(widgetID)))
    end

    if not ns.IsWorldQuestsRefreshContextActive() then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET skipped inactive",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    if not mapID then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET skipped no-map",
                WOW.string_format(
                    "widgetSetID=%s widgetID=%s",
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    if self._activeRelevantWorldQuestMapID ~= mapID then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET skipped unsynced-map",
                WOW.string_format(
                    "mapID=%s activeMapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(self._activeRelevantWorldQuestMapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    if (self._visibleLockedAreaPOIWidgetSetCount or 0) == 0
        and (self._liveRelevantAreaPOIWidgetSetCount or 0) == 0
    then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET skipped untracked",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    local hasVisibleMatch, hasLiveMatch = self:GetLockedAreaPOIWidgetUpdateScope(widgetInfo)
    if not hasVisibleMatch and not hasLiveMatch then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET skipped irrelevant",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    if hasVisibleMatch and self:HasVisibleLockedAreaPOIStateChange() then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET scheduled visible",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        ScheduleRefresh(false, "UPDATE_UI_WIDGET visible locked area POI")
        return
    end

    if self:IsWorldQuestDescendantGatherPending(mapID) then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET deferred staged gather",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        return
    end

    if hasLiveMatch and self:HasLiveRelevantAreaPOIStateChange(mapID) then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_UI_WIDGET scheduled live",
                WOW.string_format(
                    "mapID=%s widgetSetID=%s widgetID=%s",
                    tostring(mapID),
                    tostring(widgetSetID),
                    tostring(widgetID)))
        end
        ScheduleRefresh(false, "UPDATE_UI_WIDGET live locked area POI")
        return
    end

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "UPDATE_UI_WIDGET skipped unchanged",
            WOW.string_format(
                "mapID=%s widgetSetID=%s widgetID=%s",
                tostring(mapID),
                tostring(widgetSetID),
                tostring(widgetID)))
    end
end

function eventFrame:HandleAllLockedAreaPOIWidgetsUpdated()
    local mapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "UPDATE_ALL_UI_WIDGETS entry",
            WOW.string_format("mapID=%s", tostring(mapID)))
    end

    if not ns.IsWorldQuestsRefreshContextActive() then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS skipped inactive",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    if not mapID then
        self:DebugWorldQuestsRefreshTrace(
            "UPDATE_ALL_UI_WIDGETS skipped no-map",
            "mapID=nil")
        return
    end

    if self._activeRelevantWorldQuestMapID ~= mapID then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS skipped unsynced-map",
                WOW.string_format(
                    "mapID=%s activeMapID=%s",
                    tostring(mapID),
                    tostring(self._activeRelevantWorldQuestMapID)))
        end
        return
    end

    if (self._visibleLockedAreaPOIWidgetSetCount or 0) == 0
        and (self._liveRelevantAreaPOIWidgetSetCount or 0) == 0
    then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS skipped untracked",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    if (self._visibleLockedAreaPOIWidgetSetCount or 0) > 0
        and self:HasVisibleLockedAreaPOIStateChange()
    then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS scheduled visible",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        ScheduleRefresh(false, "UPDATE_ALL_UI_WIDGETS visible locked area POI")
        return
    end

    if self:IsWorldQuestDescendantGatherPending(mapID) then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS deferred staged gather",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        return
    end

    if (self._liveRelevantAreaPOIWidgetSetCount or 0) > 0
        and self:HasLiveRelevantAreaPOIStateChange(mapID)
    then
        if ns.IsDebugEnabled() then
            self:DebugWorldQuestsRefreshTrace(
                "UPDATE_ALL_UI_WIDGETS scheduled live",
                WOW.string_format("mapID=%s", tostring(mapID)))
        end
        ScheduleRefresh(false, "UPDATE_ALL_UI_WIDGETS live locked area POI")
        return
    end

    if ns.IsDebugEnabled() then
        self:DebugWorldQuestsRefreshTrace(
            "UPDATE_ALL_UI_WIDGETS skipped unchanged",
            WOW.string_format("mapID=%s", tostring(mapID)))
    end
end

-- Updates the super-track highlight on all active quest rows without
-- rebuilding the full layout.
-- POI button style is only updated once Phase 2 has populated the row
-- (item._entry == nil), preventing stale internal questID state from causing
-- POIButton_UpdateNormalStyle to clear the elite dragon border underlay.
local function UpdateSuperTrackHighlights()
    local superTrackedID = WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedQuestID
        and WOW.C_SuperTrack.GetSuperTrackedQuestID() or nil
    for _, item in ipairs(activeContent) do
        if item.type == "row" and item.frame then
            local row = item.frame
            local entry = item._entry or row.questEntry
            -- Skip rows that are still showing "Loading..." â€” their title
            -- colour is managed by PopulateQuestRow once data arrives.
                local hasTitle = not (entry and entry.isAreaPOI)
                    and row.questID and GetQuestTitle(row.questID)
            local isLocked = entry and entry.isLocked == true
            local isTracked = superTrackedID and (superTrackedID == row.questID)
            if hasTitle or isLocked then
                ApplyQuestRowTitleColor(row.titleText, row.questID, isLocked, false)
            end
            -- POI button style update (guarded by Phase 2 completion).
            if item._entry == nil and row.poiBtn then
                if entry and entry.isAreaPOI then
                    if row.poiBtn.SetSelected then
                        local trackedType, trackedID
                        if WOW.C_SuperTrack and WOW.C_SuperTrack.GetSuperTrackedMapPin then
                            trackedType, trackedID = WOW.C_SuperTrack.GetSuperTrackedMapPin()
                        end
                        local mapPinType = Enum and Enum.SuperTrackingMapPinType
                            and Enum.SuperTrackingMapPinType.AreaPOI or 0
                        local isTrackedPOI = trackedType == mapPinType and trackedID == entry.poiID
                        row.poiBtn:SetSelected(isTrackedPOI and true or false)
                    end
                    if row.poiBtn.UpdateSelected then row.poiBtn:UpdateSelected() end
                    local areaPOIIcon = eventFrame._poiButtonAreaPOIIcons and eventFrame._poiButtonAreaPOIIcons[row.poiBtn] or nil
                    if areaPOIIcon then
                        local atlas = entry.areaPOIInfo and entry.areaPOIInfo.atlasName
                        if atlas and areaPOIIcon.SetAtlas then
                            areaPOIIcon:SetAtlas(atlas, true)
                            areaPOIIcon:Show()
                        else
                            areaPOIIcon:Hide()
                        end
                    end
                    if row.poiBtn.Display and row.poiBtn.Display.SetIconShown then
                        row.poiBtn.Display:SetIconShown(false)
                    elseif row.poiBtn.Display and row.poiBtn.Display.Icon then
                        row.poiBtn.Display.Icon:SetAlpha(0)
                    end
                    if row.poiBtn.Display and row.poiBtn.Display.SubTypeIcon then
                        row.poiBtn.Display.SubTypeIcon:Hide()
                    end
                elseif POIButtonUtil and row.poiBtn.SetStyle then
                    ClearPOIButtonAreaPOIState(row.poiBtn)
                    row.poiBtn:SetStyle(POIButtonUtil.Style.WorldQuest)
                    row.poiBtn:SetSelected(isTracked and true or false)
                    if row.poiBtn.UpdateButtonStyle then row.poiBtn:UpdateButtonStyle() end
                    if row.poiBtn.Display and row.poiBtn.Display.SetIconShown then
                        row.poiBtn.Display:SetIconShown(true)
                    elseif row.poiBtn.Display and row.poiBtn.Display.Icon then
                        row.poiBtn.Display.Icon:SetAlpha(1)
                    end
                end
            end
        end
    end
end

function eventFrame:UpdateQuestRow(questID)
    local function BuildQuestRowEntry(questID, mapID, mapName, baseEntry)
        local now = GetCurrentServerTime()
        local isAreaPOI = baseEntry and baseEntry.isAreaPOI == true or false
        local rawQuestTagType = baseEntry and baseEntry.rawQuestTagType or nil
        local rawTagID = baseEntry and baseEntry.rawTagID or nil
        local isLocked = ResolveQuestLockedState(questID, baseEntry)
        local rewardDataReady
        if isAreaPOI and isLocked then
            local bountyLockQuestID = ResolveBountyLockQuestIDFromQuestEntry(baseEntry)
            rewardDataReady = not bountyLockQuestID
                or rewardPreloadState.IsQuestRewardDisplayReady(bountyLockQuestID)
        elseif isAreaPOI or isLocked then
            rewardDataReady = true
        else
            rewardDataReady = rewardPreloadState.IsQuestRewardDisplayReady(questID)
        end
        local rawTimeLeftSeconds = baseEntry and baseEntry.rawTimeLeftSeconds or nil
        local rawTimeLeftSecondsConsumed = baseEntry and baseEntry.rawTimeLeftSecondsConsumed == true or false
        local areaPOITimeText = baseEntry and baseEntry.areaPOITimeText or nil
        local previousExpiresAt = baseEntry and baseEntry.expiresAt or nil
        local previousTimeText = baseEntry and baseEntry.timeText or nil

        local timeLeft, expiresAt
        if isAreaPOI then
            if baseEntry and baseEntry.poiID and C_AreaPoiInfo and C_AreaPoiInfo.IsAreaPOITimed
                and C_AreaPoiInfo.GetAreaPOISecondsLeft
                and C_AreaPoiInfo.IsAreaPOITimed(baseEntry.poiID)
            then
                timeLeft = C_AreaPoiInfo.GetAreaPOISecondsLeft(baseEntry.poiID)
                if timeLeft and timeLeft > 0 then
                    expiresAt = now + timeLeft
                elseif previousExpiresAt and previousExpiresAt > now then
                    timeLeft = previousExpiresAt - now
                    expiresAt = previousExpiresAt
                end
            elseif previousExpiresAt and previousExpiresAt > now then
                timeLeft = previousExpiresAt - now
                expiresAt = previousExpiresAt
            end
        else
            timeLeft, expiresAt = GetQuestExpirySnapshot(questID, now, baseEntry)
            rawTimeLeftSecondsConsumed = baseEntry and baseEntry.rawTimeLeftSecondsConsumed == true or false
        end

        if isAreaPOI and baseEntry and baseEntry.tooltipWidgetSet then
            local refreshedAreaPOITimeText = eventFrame:GetCurrentAreaPOITimeText(baseEntry.tooltipWidgetSet)
            if refreshedAreaPOITimeText ~= nil then
                areaPOITimeText = refreshedAreaPOITimeText
            end
        end

        local questType = isAreaPOI and "special_assignment" or GetQuestType(questID, rawQuestTagType, rawTagID)

        if not isAreaPOI then
            expiresAt = GetPersistedLockedSpecialAssignmentExpiresAt(
                questID,
                isLocked,
                rawQuestTagType,
                rawTagID,
                questType,
                timeLeft,
                expiresAt)
        end

        local rawRewards = rewardDataReady
            and (not isLocked or not isAreaPOI)
            and GetQuestRewards(questID) or {}
        local expID = not isAreaPOI and GetQuestExpansionID(questID) or nil

        local title = isAreaPOI and (baseEntry.title or baseEntry.poiName) or GetQuestTitle(questID)
        local unlockText = isLocked and (isAreaPOI and baseEntry.unlockText or GetLockedSpecialAssignmentUnlockText(questID)) or nil
        local faction = not isAreaPOI and GetQuestFactionLabel(questID) or nil
        local timeText
        if isLocked and isAreaPOI then
            timeText = FormatTimeLeft(nil)
            timeLeft = nil
            expiresAt = nil
        elseif timeLeft and timeLeft > 0 then
            timeText = FormatTimeLeft(timeLeft)
        elseif expiresAt and expiresAt > now then
            timeText = FormatTimeLeft(expiresAt - now)
        elseif isAreaPOI and areaPOITimeText then
            timeText = areaPOITimeText
        elseif previousTimeText and previousTimeText ~= "" then
            timeText = previousTimeText
        else
            timeText = FormatTimeLeft(nil)
        end

        return {
            questID  = questID,
            mapID    = mapID or 0,
            mapName  = mapName or "",
            title    = title,
            faction  = faction,
            timeLeft = timeLeft,
            timeText = timeText,
            expiresAt = expiresAt,
            questType = questType,
            expID = expID,
            expLabel = expID and EXPANSION_LABELS[expID] or nil,
            rawTimeLeftSeconds = rawTimeLeftSeconds,
            rawTimeLeftSecondsConsumed = rawTimeLeftSecondsConsumed,
            rawQuestTagType = rawQuestTagType,
            rawTagID = rawTagID,
            isMapIndicatorQuest = baseEntry and baseEntry.isMapIndicatorQuest == true or false,
            bountyLockedCandidate = baseEntry and baseEntry.bountyLockedCandidate == true or false,
            isLocked = isLocked,
            isAreaPOI = isAreaPOI,
            areaPOITimeText = areaPOITimeText,
            areaPOIRewards = baseEntry and baseEntry.areaPOIRewards or nil,
            rewardText = baseEntry and baseEntry.rewardText or nil,
            poiID = baseEntry and baseEntry.poiID or nil,
            tooltipWidgetSet = baseEntry and baseEntry.tooltipWidgetSet or nil,
            poiDescription = baseEntry and baseEntry.poiDescription or nil,
            areaPOIInfo = baseEntry and baseEntry.areaPOIInfo or nil,
            linkedUiMapID = baseEntry and baseEntry.linkedUiMapID or nil,
            isPrimaryAreaPOI = baseEntry and baseEntry.isPrimaryAreaPOI == true or false,
            areaPOIOwnerScore = baseEntry and baseEntry.areaPOIOwnerScore or nil,
            unlockText = unlockText,
            primReward = (#rawRewards > 0) and GetPrimaryRewardType(rawRewards) or nil,
            rewardDataReady = rewardDataReady,
            dataReady = isAreaPOI or eventFrame:IsQuestCoreDataReady(questID),
        }
    end

    local function CopyQuestRowEntry(target, source)
        target.questID = source.questID
        target.mapID = source.mapID
        target.mapName = source.mapName
        target.title = source.title
        target.faction = source.faction
        target.timeLeft = source.timeLeft
        target.timeText = source.timeText
        target.expiresAt = source.expiresAt
        target.questType = source.questType
        target.expID = source.expID
        target.expLabel = source.expLabel
        target.rawTimeLeftSeconds = source.rawTimeLeftSeconds
        target.rawTimeLeftSecondsConsumed = source.rawTimeLeftSecondsConsumed
        target.rawQuestTagType = source.rawQuestTagType
        target.rawTagID = source.rawTagID
        target.isMapIndicatorQuest = source.isMapIndicatorQuest
        target.bountyLockedCandidate = source.bountyLockedCandidate
        target.isLocked = source.isLocked
        target.isAreaPOI = source.isAreaPOI
        target.areaPOITimeText = source.areaPOITimeText
        target.areaPOIRewards = source.areaPOIRewards
        target.rewardText = source.rewardText
        target.poiID = source.poiID
        target.tooltipWidgetSet = source.tooltipWidgetSet
        target.poiDescription = source.poiDescription
        target.areaPOIInfo = source.areaPOIInfo
        target.linkedUiMapID = source.linkedUiMapID
        target.isPrimaryAreaPOI = source.isPrimaryAreaPOI
        target.areaPOIOwnerScore = source.areaPOIOwnerScore
        target.unlockText = source.unlockText
        target.primReward = source.primReward
        target.rewardDataReady = source.rewardDataReady
        target.dataReady = source.dataReady
    end

    local function DoesRewardDataAffectDisplayedResult(previousEntry, refreshedEntry)
        if not previousEntry or not refreshedEntry then
            return false
        end

        local function DoesQuestEntryMatchSearch(entry, searchLower, primReward, faction)
            if not searchLower or searchLower == "" then
                return true
            end

            local factionLabel = faction ~= nil and faction or (entry and entry.faction or nil)
            local rewardType = primReward ~= nil and primReward or (entry and entry.primReward or nil)
            local questType = entry and entry.questType or nil

            return (entry and entry.title and WOW.string_lower(entry.title):find(searchLower, 1, true))
                or (factionLabel and WOW.string_lower(factionLabel):find(searchLower, 1, true))
                or (entry and entry.unlockText and WOW.string_lower(entry.unlockText):find(searchLower, 1, true))
                or (entry and entry.mapName and WOW.string_lower(entry.mapName):find(searchLower, 1, true))
                or (questType and QUEST_TYPE_LABEL[questType]
                    and WOW.string_lower(QUEST_TYPE_LABEL[questType]):find(searchLower, 1, true))
                or (rewardType and REWARD_TYPE_LABEL[rewardType]
                    and WOW.string_lower(REWARD_TYPE_LABEL[rewardType]):find(searchLower, 1, true))
        end

        local previousPrimReward = previousEntry.primReward
        local refreshedPrimReward = refreshedEntry.primReward
        local previousFaction = previousEntry.faction
        local refreshedFaction = refreshedEntry.faction

        if sortMode == "reward" and previousPrimReward ~= refreshedPrimReward then
            return true
        end

        if sortMode == "faction" and previousFaction ~= refreshedFaction then
            return true
        end

        if next(filterRewards) then
            local previousExcluded = previousPrimReward and filterRewards[previousPrimReward] or false
            local refreshedExcluded = refreshedPrimReward and filterRewards[refreshedPrimReward] or false
            if previousExcluded ~= refreshedExcluded then
                return true
            end
        end

        if filterSearch ~= "" then
            local searchLower = WOW.string_lower(filterSearch)
            local previousMatched = DoesQuestEntryMatchSearch(
                previousEntry,
                searchLower,
                previousPrimReward,
                previousFaction) and true or false
            local refreshedMatched = DoesQuestEntryMatchSearch(
                refreshedEntry,
                searchLower,
                refreshedPrimReward,
                refreshedFaction) and true or false
            if previousMatched ~= refreshedMatched then
                return true
            end
        end

        return false
    end

    questTypeCache[questID] = nil

    local existingEntry = GetExistingQuestEntry(questID)
    local rowItem = nil
    for _, item in ipairs(activeContent) do
        if item.type == "row" and item.frame and item.frame.questID == questID then
            rowItem = item
            break
        end
    end

    -- Rows for locked Area POIs use a synthetic quest id; bounty reward preload and
    -- pendingQuestIDs use C_QuestLog's lock quest id. Map that id back to the visible row.
    if not rowItem and questID and questID > 0 then
        for _, item in ipairs(activeContent) do
            if item.type == "row" and item.frame then
                local qe = item.frame.questEntry
                if qe and qe.isAreaPOI and qe.isLocked then
                    local bountyID = ResolveBountyLockQuestIDFromQuestEntry(qe)
                    if bountyID == questID then
                        rowItem = item
                        local syntheticID = item.frame.questID
                        if syntheticID then
                            existingEntry = GetExistingQuestEntry(syntheticID) or existingEntry
                            questID = syntheticID
                            questTypeCache[questID] = nil
                        end
                        break
                    end
                end
            end
        end
    end

    local row = rowItem and rowItem.frame or nil
    local rowQuestEntry = row and row["questEntry"] or nil
    local currentEntry = rowItem and (rowItem._entry or rowQuestEntry) or existingEntry
    if not currentEntry and not existingEntry then
        return false, false
    end

    local previousEntry = existingEntry or currentEntry
    local previousRewardDataReady = previousEntry and previousEntry.rewardDataReady
    local refreshedEntry = BuildQuestRowEntry(
        questID,
        row and row.mapID or (currentEntry and currentEntry.mapID) or nil,
        row and row.mapName or (currentEntry and currentEntry.mapName) or nil,
        currentEntry or existingEntry)
    local needsFullRefresh = previousEntry
        and DoesRewardDataAffectDisplayedResult(previousEntry, refreshedEntry)
        or false

    if existingEntry then
        CopyQuestRowEntry(existingEntry, refreshedEntry)
    end
    if currentEntry and currentEntry ~= existingEntry then
        CopyQuestRowEntry(currentEntry, refreshedEntry)
    end

    if rowItem and rowItem._entry == nil and row then
        local rowEntry = rowQuestEntry or existingEntry or currentEntry or refreshedEntry
        if rowEntry ~= existingEntry and rowEntry ~= currentEntry then
            CopyQuestRowEntry(rowEntry, refreshedEntry)
        end
        eventFrame._PopulateQuestRow(
            row,
            rowEntry,
            previousRewardDataReady == false and rowEntry.rewardDataReady == true)
    end

    return rowItem ~= nil, needsFullRefresh
end

function rewardPreloadState.PollPendingRewardData()
    local anyStillPending = false
    local anyResolved = false
    local anyNeedsFullRefresh = false
    local now = WOW.GetTime()

    for questID, pendingState in pairs(pendingQuestIDs) do
        if pendingState and pendingState.needsRewardData then
            if rewardPreloadState.IsQuestRewardDisplayReady(questID) then
                local rowNeedsUpdate, _, searchRefreshOnRewardReady =
                    ResolvePendingQuestDataState(questID)
                if rowNeedsUpdate then
                    local _, rowNeedsFullRefresh = eventFrame:UpdateQuestRow(questID)
                    eventFrame:RefreshActiveQuestTooltipIfReady(questID)
                    if rowNeedsFullRefresh then
                        anyNeedsFullRefresh = true
                    end
                end
                if searchRefreshOnRewardReady then
                    anyNeedsFullRefresh = true
                end
                anyResolved = true
            else
                anyStillPending = true
                local requestedAt = rewardPreloadState.requestedQuestIDs[questID]
                local isExpired = not requestedAt
                    or (type(requestedAt) == "number"
                        and requestedAt + rewardPreloadState.requestRetryCooldown <= now)
                
                -- Also check if request has timed out (>10 seconds since request)
                if not isExpired then
                    local recordedTime = rewardPreloadState.recordedRequestTime[questID]
                    if recordedTime and recordedTime + 10 <= now then
                        isExpired = true
                    end
                end
                
                if isExpired then
                    local retryAt = questDataRetrySuppressedUntil[questID]
                    if not (retryAt and retryAt > now) then
                        if WOW.C_TaskQuest and WOW.C_TaskQuest.RequestPreloadRewardData then
                            rewardPreloadState.requestedQuestIDs[questID] = now
                            rewardPreloadState.recordedRequestTime[questID] = now
                            WOW.C_TaskQuest.RequestPreloadRewardData(questID)
                        end
                    end
                end
            end
        end
    end

    if anyResolved then
        UpdateQuestLogUpdateRegistration()
    end
    if anyNeedsFullRefresh then
        ScheduleRefresh(false)
    end

    return anyStillPending
end

-- =============================================
-- Tab + panel creation (QuestMapFrame native tab system)
-- =============================================

-- Creates the content panel parented to QuestMapFrame and registers it as a
-- native side-panel tab (alongside Quests, Events, Map Legend, etc.).
-- Safe to call multiple times â€” returns immediately if already created.
eventFrame._CreateQuestMapTab = function()
    if questMapPanel then return end
    if not QuestMapFrame then return end

    -- â”€â”€ Content panel â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local panel = WOW.CreateFrame("Frame", "NomToolsWorldQuestPanel", QuestMapFrame)
    panel:Hide()
    panel.displayMode = K.DISPLAY_MODE
    panel:HookScript("OnShow", function()
        EnsureMinuteAlignedTimeUpdates()
        ScheduleRefresh()
    end)
    panel:HookScript("OnHide", function()
        eventFrame:ResetWorldQuestsRefreshGateState()
        StopMinuteAlignedTimeUpdates()
        StopQuestDataRetryRefresh()
        UpdateQuestLogUpdateRegistration()
    end)

    -- Anchor to ContentsAnchor once it has dimensions; re-anchor on resize.
    local function AnchorPanel()
        panel:ClearAllPoints()
        local ca = QuestMapFrame.ContentsAnchor
        if ca and ca:GetWidth() > 0 and ca:GetHeight() > 0 then
            panel:SetPoint("TOPLEFT",     ca, "TOPLEFT",     0,    -4)
            panel:SetPoint("BOTTOMRIGHT", ca, "BOTTOMRIGHT", -22,   0)
        else
            panel:SetAllPoints(QuestMapFrame)
        end
    end
    AnchorPanel()
    WOW.C_Timer.After(0,   AnchorPanel)
    WOW.C_Timer.After(0.1, AnchorPanel)

    -- Native QuestLog background atlas
    local bg = panel:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    if bg.SetAtlas then bg:SetAtlas("QuestLog-main-background", true) end

    -- â”€â”€ Search bar + cogwheel â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local SEARCH_H = 28   -- height of the search bar row

    worldQuestSearchBox = WOW.CreateFrame("EditBox", "NomToolsWQSearchBox", panel,
                                      "SearchBoxTemplate")
    local searchBox = worldQuestSearchBox
    searchBox:SetPoint("TOPLEFT",  panel, "TOPLEFT",   6, -4)
    searchBox:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -30, -4)
    searchBox:SetHeight(SEARCH_H)
    searchBox:SetAutoFocus(false)
    if searchBox.Left then searchBox.Left:SetAlpha(0) end
    if searchBox.Middle then searchBox.Middle:SetAlpha(0) end
    if searchBox.Right then searchBox.Right:SetAlpha(0) end
    -- Search starts empty every session (filterSearch is not persisted).
    searchBox:HookScript("OnTextChanged", function(self)
        local newText = self:GetText() or ""
        if newText == filterSearch then
            return
        end
        filterSearch = newText
        ScheduleRefresh()
    end)
    searchBox:HookScript("OnEscapePressed", function(self)
        self:SetText("")
        -- filterSearch updated by OnTextChanged hook above; just clear focus
        self:ClearFocus()
    end)

    -- Filter button (opens the filter dropdown)
    local cogBtn = WOW.CreateFrame("Button", nil, panel)
    cogBtn:SetSize(22, 22)
    cogBtn:SetPoint("LEFT", searchBox, "RIGHT", 2, 0)

    local function CreateTransparentButtonTexture()
        local tex = cogBtn:CreateTexture(nil, "BACKGROUND")
        tex:SetAllPoints()
        tex:SetColorTexture(1, 1, 1, 0)
        return tex
    end

    cogBtn:SetNormalTexture(CreateTransparentButtonTexture())
    cogBtn:SetPushedTexture(CreateTransparentButtonTexture())
    cogBtn:SetHighlightTexture(CreateTransparentButtonTexture())

    local cogBtnIcon = cogBtn:CreateTexture(nil, "ARTWORK")
    cogBtnIcon:SetAllPoints()
    cogBtnIcon:SetAtlas("Map-Filter-Button", true)
    cogBtn.icon = cogBtnIcon

    cogBtn:SetScript("OnMouseDown", function(self)
        if self.icon then
            self.icon:SetAtlas("Map-Filter-Button-down", true)
        end
    end)
    cogBtn:SetScript("OnMouseUp", function(self)
        if self.icon then
            self.icon:SetAtlas("Map-Filter-Button", true)
        end
    end)
    cogBtn:SetScript("OnEnter", function(self)
        WOW.GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        WOW.GameTooltip:SetText("World Quest Filter", 1, 1, 1)
        WOW.GameTooltip:Show()
    end)
    cogBtn:HookScript("OnLeave", function()
        WOW.GameTooltip:Hide()
    end)

    -- â”€â”€ Filter/sort dropdown (shown/hidden by cogBtn) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local DROP_W = 200
    local dropFrame = WOW.CreateFrame("Frame", "NomToolsWQFilterDrop", panel,
                                  "BackdropTemplate")
    dropFrame:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    dropFrame:SetWidth(DROP_W)
    dropFrame:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -2, -(SEARCH_H + 8))
    dropFrame:SetFrameLevel((panel:GetFrameLevel() or 2) + 20)
    dropFrame:Hide()

    -- Close dropdown when clicking anywhere outside it
    dropFrame:EnableMouse(true)
    local dropCloseListener = WOW.CreateFrame("Frame", nil, UIParent)
    dropCloseListener:EnableMouse(true)
    dropCloseListener:SetAllPoints(UIParent)
    dropCloseListener:SetFrameLevel(dropFrame:GetFrameLevel() - 1)
    dropCloseListener:Hide()
    dropCloseListener:SetScript("OnMouseDown", function()
        dropFrame:Hide()
        dropCloseListener:Hide()
    end)

    -- Helper: create a section header label inside the dropdown.
    local function AddDropHeader(yPos, text)
        local lbl = dropFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        lbl:SetPoint("TOPLEFT", dropFrame, "TOPLEFT", 8, yPos)
        lbl:SetText(text)
        lbl:SetTextColor(0.8, 0.72, 0.42)
        return lbl
    end

    -- Helper: build one row in the dropdown.
    -- Anchors TOP to anchor.BOTTOM (vertical position) and LEFT/RIGHT to
    -- dropFrame (full-width span â€” avoids text truncation from FontString anchors).
    local function AddDropButton(anchor, yOff, labelText, onClick)
        local btn = WOW.CreateFrame("Button", nil, dropFrame)
        btn:SetHeight(18)
        btn:SetPoint("TOP",   anchor,    "BOTTOM",  0, yOff)
        btn:SetPoint("LEFT",  dropFrame, "LEFT",    4, 0)
        btn:SetPoint("RIGHT", dropFrame, "RIGHT",  -4, 0)

        local check = btn:CreateTexture(nil, "ARTWORK")
        check:SetSize(14, 14)
        check:SetPoint("LEFT", btn, "LEFT", 10, 0)
        check:SetAtlas("checkmark-minimal")
        check:Hide()
        btn.checkmark = check

        local lbl = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        lbl:SetPoint("LEFT", check, "RIGHT", 4, 0)
        lbl:SetPoint("RIGHT", btn, "RIGHT", -8, 0)
        lbl:SetJustifyH("LEFT")
        lbl:SetText(labelText)
        btn.label = lbl

        btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestLogTitleHighlight", "ADD")
        btn:SetScript("OnClick", onClick)
        return btn
    end

    -- Build dropdown content.
    -- Section order: Zone Sort â†’ Quest Sort â†’ Quest Type â†’ Reward Type.
    local curY = -8

    -- â”€â”€ Zone sort section â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local zoneSortHeader = AddDropHeader(curY, "Zone Sort")
    curY = curY - 18

    local zoneSortButtons = {}
    local ZONE_SORT_DEFS = {
        { key = "time",  label = "Time Left" },
        { key = "alpha", label = "Alphabetical" },
    }

    local function UpdateZoneSortChecks()
        for _, zb in ipairs(zoneSortButtons) do
            zb.checkmark:SetShown(zb.sortKey == zoneSortMode)
        end
    end

    local lastZoneSortAnchor = zoneSortHeader
    for _, def in ipairs(ZONE_SORT_DEFS) do
        local zb = AddDropButton(lastZoneSortAnchor, -2, def.label, function()
            zoneSortMode = def.key
            SaveFilterState()
            UpdateZoneSortChecks()
            ScheduleRefresh()
            eventFrame:UpdateWQFilterResetState()
        end)
        zb.sortKey = def.key
        zoneSortButtons[#zoneSortButtons + 1] = zb
        lastZoneSortAnchor = zb
        curY = curY - 20
    end

    local divZoneSort = dropFrame:CreateTexture(nil, "ARTWORK")
    divZoneSort:SetHeight(1)
    divZoneSort:SetPoint("TOPLEFT",  dropFrame, "TOPLEFT",  8, curY - 4)
    divZoneSort:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY - 4)
    divZoneSort:SetColorTexture(0.3, 0.3, 0.3, 0.8)
    curY = curY - 12

    -- â”€â”€ Quest sort section â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local sortHeader = AddDropHeader(curY, "Quest Sort")
    curY = curY - 18

    local sortButtons = {}
    local SORT_DEFS = {
        { key = "time",    label = "Time Left" },
        { key = "alpha",   label = "Alphabetical" },
        { key = "reward",  label = "Reward Type" },
        { key = "faction", label = "Faction" },
    }

    local function UpdateQuestSortChecks()
        for _, sb in ipairs(sortButtons) do
            sb.checkmark:SetShown(sb.sortKey == sortMode)
        end
    end

    local lastSortAnchor = sortHeader
    for _, def in ipairs(SORT_DEFS) do
        local sb = AddDropButton(lastSortAnchor, -2, def.label, function()
            sortMode = def.key
            SaveFilterState()
            UpdateQuestSortChecks()
            ScheduleRefresh()
            eventFrame:UpdateWQFilterResetState()
        end)
        sb.sortKey = def.key
        sortButtons[#sortButtons + 1] = sb
        lastSortAnchor = sb
        curY = curY - 20
    end

    -- Divider 1
    local div1 = dropFrame:CreateTexture(nil, "ARTWORK")
    div1:SetHeight(1)
    div1:SetPoint("TOPLEFT",  dropFrame, "TOPLEFT",  8, curY - 4)
    div1:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY - 4)
    div1:SetColorTexture(0.3, 0.3, 0.3, 0.8)
    curY = curY - 12

    -- â”€â”€ Quest type filter section (multi-select) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local typeHeader = AddDropHeader(curY, "Quest Type")
    curY = curY - 18

    local typeButtons = {}
    local function UpdateTypeChecks()
        for _, tb in ipairs(typeButtons) do
            -- Checkmark = shown (not excluded); exclusion-set semantics.
            tb.checkmark:SetShown(not filterTypes[tb.typeKey])
        end
    end

    local lastTypeAnchor = typeHeader
    for _, typeKey in ipairs(QUEST_TYPE_ORDER) do
        local lbl = QUEST_TYPE_LABEL[typeKey]
        local tb = AddDropButton(lastTypeAnchor, -2, lbl, function()
            if filterTypes[typeKey] then
                filterTypes[typeKey] = nil   -- remove from exclusion â†’ show
            else
                filterTypes[typeKey] = true  -- add to exclusion â†’ hide
            end
            SaveFilterState()
            UpdateTypeChecks()
            ScheduleRefresh()
            eventFrame:UpdateWQFilterResetState()
        end)
        tb.typeKey = typeKey
        typeButtons[#typeButtons + 1] = tb
        lastTypeAnchor = tb
        curY = curY - 20
    end

    -- Divider 2
    local div2 = dropFrame:CreateTexture(nil, "ARTWORK")
    div2:SetHeight(1)
    div2:SetPoint("TOPLEFT",  dropFrame, "TOPLEFT",  8, curY - 4)
    div2:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY - 4)
    div2:SetColorTexture(0.3, 0.3, 0.3, 0.8)
    curY = curY - 12

    -- â”€â”€ Reward type filter section (multi-select) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local rewardHeader = AddDropHeader(curY, "Reward Type")
    curY = curY - 18

    local rewardButtons = {}
    local function UpdateRewardChecks()
        for _, rb in ipairs(rewardButtons) do
            -- Checkmark = shown (not excluded); exclusion-set semantics.
            rb.checkmark:SetShown(not filterRewards[rb.rewardKey])
        end
    end

    local lastRewardAnchor = rewardHeader
    for _, rewardKey in ipairs(REWARD_TYPE_ORDER) do
        local lbl = REWARD_TYPE_LABEL[rewardKey]
        local rb = AddDropButton(lastRewardAnchor, -2, lbl, function()
            if filterRewards[rewardKey] then
                filterRewards[rewardKey] = nil   -- remove from exclusion â†’ show
            else
                filterRewards[rewardKey] = true  -- add to exclusion â†’ hide
            end
            SaveFilterState()
            UpdateRewardChecks()
            ScheduleRefresh()
            eventFrame:UpdateWQFilterResetState()
        end)
        rb.rewardKey = rewardKey
        rewardButtons[#rewardButtons + 1] = rb
        lastRewardAnchor = rb
        curY = curY - 20
    end

    -- â”€â”€ Reset Filters button at bottom â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local divReset = dropFrame:CreateTexture(nil, "ARTWORK")
    divReset:SetHeight(1)
    divReset:SetPoint("TOPLEFT",  dropFrame, "TOPLEFT",  8, curY - 4)
    divReset:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY - 4)
    divReset:SetColorTexture(0.3, 0.3, 0.3, 0.8)
    curY = curY - 12

    local resetBtn = WOW.CreateFrame("Button", nil, dropFrame)
    resetBtn:SetHeight(20)
    resetBtn:SetPoint("TOPLEFT", dropFrame, "TOPLEFT", 8, curY)
    resetBtn:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY)
    local resetLabel = resetBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    resetLabel:SetPoint("CENTER")
    resetLabel:SetText(RESET)
    resetLabel:SetTextColor(1, 0.82, 0)
    resetBtn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestLogTitleHighlight", "ADD")
    resetBtn:SetScript("OnClick", function()
        wipe(filterTypes)
        wipe(filterRewards)
        sortMode     = "time"
        zoneSortMode = "time"
        SaveFilterState()
        UpdateZoneSortChecks()
        UpdateQuestSortChecks()
        UpdateTypeChecks()
        UpdateRewardChecks()
        eventFrame:UpdateWQFilterResetState()
        ScheduleRefresh()
        PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
    end)
    resetBtn:SetScript("OnEnter", function()
        if not eventFrame:IsWQFilterDefault() then
            resetLabel:SetTextColor(1, 1, 1)
        else
            resetLabel:SetTextColor(0.5, 0.5, 0.5)
        end
    end)
    resetBtn:SetScript("OnLeave", function()
        resetLabel:SetTextColor(1, 0.82, 0)
    end)
    curY = curY - 22

    dropFrame:SetHeight(math.abs(curY) + 12)

    -- Cogwheel click: toggle dropdown and refresh all check states.
    cogBtn:SetScript("OnClick", function()
        if dropFrame:IsShown() then
            dropFrame:Hide()
            dropCloseListener:Hide()
        else
            UpdateZoneSortChecks()
            UpdateQuestSortChecks()
            UpdateTypeChecks()
            UpdateRewardChecks()
            dropFrame:Show()
            dropCloseListener:Show()
        end
        eventFrame:UpdateWQFilterResetState()
    end)

    -- â”€â”€ Scroll frame (ScrollFrameTemplate provides a WowTrimScrollBar) â”€â”€â”€â”€â”€
    local sf = WOW.CreateFrame("ScrollFrame", nil, panel, "ScrollFrameTemplate")

    local FOOTER_H = 34
    local footer = WOW.CreateFrame("Frame", nil, panel)
    footer:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 0, 0)
    footer:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0, 0)
    footer:SetHeight(FOOTER_H)
    footer:Show()

    local footerText = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    footerText:SetPoint("TOPLEFT", footer, "TOPLEFT", 10, -6)
    footerText:SetPoint("BOTTOMRIGHT", footer, "BOTTOMRIGHT", -10, 6)
    footerText:SetJustifyH("LEFT")
    footerText:SetJustifyV("MIDDLE")
    footerText:SetWordWrap(true)
    footerText:SetTextColor(
        COL.contractNotice[1], COL.contractNotice[2], COL.contractNotice[3])

    UpdateContractNoticeLayout = function()
        sf:ClearAllPoints()
        sf:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, -(SEARCH_H + 8))
        sf:SetPoint("BOTTOMRIGHT", footer, "TOPRIGHT", 0, 0)
    end

    UpdateContractNoticeLayout()

    -- Reposition the scroll bar to float just outside the right edge so it
    -- does not eat into the content width (see EnhanceQoL DungeonPortals).
    if sf.ScrollBar then
        sf.ScrollBar:ClearAllPoints()
        sf.ScrollBar:SetPoint("TOPLEFT",    sf, "TOPRIGHT",    8,  2)
        sf.ScrollBar:SetPoint("BOTTOMLEFT", sf, "BOTTOMRIGHT", 8, -4)
    end

    -- Scroll child
    local sc = WOW.CreateFrame("Frame", nil, sf)
    sc:SetSize(1, 1)
    sf:SetScrollChild(sc)

    -- "No active world quests" placeholder
    local noQuestsText = sc:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    noQuestsText:SetPoint("TOPLEFT", sc, "TOPLEFT", 14, -20)
    noQuestsText:SetText("No active world quests in this zone.")
    noQuestsText:SetTextColor(COL.noQuests[1], COL.noQuests[2], COL.noQuests[3])
    noQuestsText:Hide()

    -- Native QuestLog border frame
    local bf = WOW.CreateFrame("Frame", nil, panel, "QuestLogBorderFrameTemplate")
    bf:SetPoint("TOPLEFT",     sf, "TOPLEFT",     -3,  7)
    bf:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT",  3, -6)
    bf:SetFrameLevel((panel:GetFrameLevel() or 2) + 3)
    bf:EnableMouse(false)

    -- Mouse-wheel scrolling forwarded to the scroll frame controller
    panel:EnableMouseWheel(true)
    panel:SetScript("OnMouseWheel", function(_, delta)
        local ctrl = sf.GetScrollController and sf:GetScrollController()
        if ctrl and ctrl.OnMouseWheel then
            ctrl:OnMouseWheel(delta)
        else
            sf:SetVerticalScroll(
                math.max(0, sf:GetVerticalScroll() - delta * K.ROW_HEIGHT))
        end
    end)

    -- Re-anchor when QuestMapFrame or its ContentsAnchor resize
    if not QuestMapFrame._nomtoolsWQSizeHook then
        QuestMapFrame:HookScript("OnSizeChanged", AnchorPanel)
        QuestMapFrame._nomtoolsWQSizeHook = true
    end
    if QuestMapFrame.ContentsAnchor
        and not QuestMapFrame.ContentsAnchor._nomtoolsWQSizeHook then
        QuestMapFrame.ContentsAnchor:HookScript("OnSizeChanged", AnchorPanel)
        QuestMapFrame.ContentsAnchor._nomtoolsWQSizeHook = true
    end
    questMapPanel = panel
    scrollFrame   = sf
    scrollChild   = sc
    noQuestsLabel = noQuestsText
    contractNoticeFrame = footer
    contractNoticeLabel = footerText

    -- â”€â”€ Tab button (uses Blizzard QuestLogTabButtonTemplate) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    local tab = WOW.CreateFrame("Button", "NomToolsWQTab", QuestMapFrame,
                            "QuestLogTabButtonTemplate")
    -- Use valid atlases so the mixin's state machine doesn't log errors,
    -- but we'll keep the template Icon hidden and overlay our own texture.
    tab.activeAtlas   = "questlog-tab-icon-maplegend"
    tab.inactiveAtlas = "questlog-tab-icon-maplegend-inactive"
    tab.tooltipText   = "World Quests"
    tab.displayMode   = K.DISPLAY_MODE
    if tab.SetChecked then tab:SetChecked(false) end

    -- Hide the mixin-managed Icon and keep it hidden even if the mixin
    -- tries to Show() it or swap its atlas (same pattern as EnhanceQoL).
    if tab.Icon then
        tab.Icon:SetAlpha(0)
        if not tab.Icon._nomtoolsHook then
            hooksecurefunc(tab.Icon, "Show",     function(icon) icon:SetAlpha(0) end)
            hooksecurefunc(tab.Icon, "SetAtlas", function(icon) icon:SetAlpha(0) end)
            tab.Icon._nomtoolsHook = true
        end
    end

    -- Persistent custom icon texture.
    local WQ_ICON_PATH = "Interface\\AddOns\\NomTools\\media\\WorldQuest_POI.png"
    local customIcon = tab:CreateTexture(nil, "ARTWORK")
    customIcon:SetPoint("CENTER", -2, 0)
    customIcon:SetSize(20, 20)
    customIcon:SetTexture(WQ_ICON_PATH)
    tab.CustomIcon = customIcon

    -- Match Blizzard tab behavior: selected state uses highlight/checked visuals;
    -- icon desaturation reflects disabled state only.
    if not tab._nomtoolsStateHooks then
        hooksecurefunc(tab, "Disable", function(self)
            if self.CustomIcon then self.CustomIcon:SetDesaturated(true) end
        end)
        hooksecurefunc(tab, "Enable", function(self)
            if self.CustomIcon then self.CustomIcon:SetDesaturated(false) end
        end)
        tab._nomtoolsStateHooks = true
    end

    tab:SetScript("OnEnter", function(self)
        WOW.GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        WOW.GameTooltip:SetText(self.tooltipText or "World Quests", 1, 0.82, 0)
        WOW.GameTooltip:Show()
    end)
    tab:SetScript("OnLeave", function() WOW.GameTooltip:Hide() end)
    tab:SetScript("OnMouseUp", function(self, btn, upInside)
        if btn ~= "LeftButton" or not upInside then return end
        RequestWorldQuestsDisplayMode("manual")
    end)

    questMapTab = tab

    -- Initial anchor + two deferred retries so that EQOL's tab (created in
    -- the same OnShow chain) is found even if it fires after our hook.
    RebuildTabAnchor()
    WOW.C_Timer.After(0,   RebuildTabAnchor)
    WOW.C_Timer.After(0.3, RebuildTabAnchor)

    -- â”€â”€ Register with QuestMapFrame management tables â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    if QuestMapFrame.ContentFrames then
        local found = false
        for _, f in ipairs(QuestMapFrame.ContentFrames) do
            if f == panel then found = true; break end
        end
        if not found then table.insert(QuestMapFrame.ContentFrames, panel) end
    end

    if QuestMapFrame.TabButtons then
        local found = false
        for _, b in ipairs(QuestMapFrame.TabButtons) do
            if b == tab then found = true; break end
        end
        if not found then table.insert(QuestMapFrame.TabButtons, tab) end
    end

    if QuestMapFrame.ValidateTabs then QuestMapFrame:ValidateTabs() end

    -- â”€â”€ React to display-mode changes â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    -- Primary: EventRegistry callback (fires for all SetDisplayMode calls)
    if EventRegistry and not displayHookRegistered then
        displayHookRegistered = true
        EventRegistry:RegisterCallback("QuestLog.SetDisplayMode",
            function(_, mode)
                if mode == K.DISPLAY_MODE then
                    eventFrame.isWorldQuestsDisplayActive = true
                    local settings = GetSettings()
                    if not IsModuleEnabled(settings) then
                        ClearPendingDisplayModeRequest()
                        RestoreBuiltinDisplayModeIfNeeded()
                        if not ShouldKeepCustomWorldQuestsVisible() then
                            if tab.SetChecked then tab:SetChecked(false) end
                            panel:Hide()
                        end
                        return
                    end
                    if tab.SetChecked then tab:SetChecked(true) end
                    panel:Show()
                    ScheduleRefresh()
                else
                    eventFrame.isWorldQuestsDisplayActive = false
                    ClearPendingBuiltinDisplayModeFallback()
                    if tab.SetChecked then tab:SetChecked(false) end
                    panel:Hide()
                end
            end, questMapPanel)
    end

    -- Secondary: hooksecurefunc as belt-and-suspenders
    if QuestMapFrame.SetDisplayMode
        and not QuestMapFrame._nomtoolsWQDisplayHook then
        hooksecurefunc(QuestMapFrame, "SetDisplayMode", function(_, mode)
            if mode == K.DISPLAY_MODE then
                eventFrame.isWorldQuestsDisplayActive = true
                local settings = GetSettings()
                if not IsModuleEnabled(settings) then
                    ClearPendingDisplayModeRequest()
                    RestoreBuiltinDisplayModeIfNeeded()
                    if not ShouldKeepCustomWorldQuestsVisible() then
                        panel:Hide()
                    end
                    return
                end
                panel:Show()
                ScheduleRefresh()
            else
                eventFrame.isWorldQuestsDisplayActive = false
                ClearPendingBuiltinDisplayModeFallback()
                panel:Hide()
            end
        end)
        QuestMapFrame._nomtoolsWQDisplayHook = true
    end
end

-- =============================================
-- Public API
-- =============================================

-- Called by the options panel after resetting settings so that the runtime
-- filter locals re-sync from the DB and the quest list refreshes.
function ns.SyncAndRefreshWorldQuests(clearSearch)
    SyncFilterState(clearSearch == true)
    eventFrame:UpdateWQFilterResetState()
    ApplyWorldQuestTypography()
    if questMapPanel and questMapPanel:IsShown() then
        ScheduleRefresh()
    end
end

-- Public refresh callback (called by NomTools core)
function ns.RefreshWorldQuestsUI()
    local s = GetSettings()
    if not IsModuleEnabled(s) then
        eventFrame:ResetWorldQuestsRefreshGateState()
        ClearPendingDisplayModeRequest()
        RestoreBuiltinDisplayModeIfNeeded()
        if not ShouldKeepCustomWorldQuestsVisible() then
            if questMapPanel then questMapPanel:Hide() end
            if questMapTab   then questMapTab:Hide()   end
        end
        return
    end

    -- Ensure tab exists; show it
    if not questMapPanel then
        eventFrame._CreateQuestMapTab()
    end
    if questMapTab then questMapTab:Show() end

    -- Content refresh only while the panel is visible
    if questMapPanel and questMapPanel:IsShown() then
        ScheduleRefresh()
    end
end

-- Resolve pending reward rows after item client data arrives (ITEM_DATA_LOAD_RESULT is
-- documented for C_Item.RequestLoadItemData; GET_ITEM_INFO_RECEIVED covers GetItemInfo).
local function ProcessPendingQuestRewardsAfterItemHydrationEvent(reason)
    if not ns.IsWorldQuestsRefreshContextActive() then
        UpdateQuestLogUpdateRegistration()
        return
    end
    if not next(pendingQuestIDs) then
        UpdateQuestLogUpdateRegistration()
        return
    end

    local needsFullRefresh = false
    for qid in pairs(pendingQuestIDs) do
        local pendingState = pendingQuestIDs[qid]
        if pendingState and pendingState.needsRewardData
            and rewardPreloadState.IsQuestRewardDisplayReady(qid)
        then
            local rowNeedsUpdate, questNeedsFullRefresh, searchRefreshOnRewardReady =
                ResolvePendingQuestDataState(qid)
            if rowNeedsUpdate then
                local rowUpdated, rowNeedsFullRefreshFromRow = eventFrame:UpdateQuestRow(qid)
                eventFrame:RefreshActiveQuestTooltipIfReady(qid)
                if rowNeedsFullRefreshFromRow
                    or questNeedsFullRefresh
                    or (searchRefreshOnRewardReady and not rowUpdated)
                then
                    needsFullRefresh = true
                end
            elseif searchRefreshOnRewardReady then
                needsFullRefresh = true
            end
            if questNeedsFullRefresh then
                needsFullRefresh = true
            end
        end
    end
    if needsFullRefresh then
        ScheduleRefresh(false, reason)
    end
    UpdateQuestLogUpdateRegistration()
end

-- =============================================
-- Module initialization (called by NomTools core after addon load)
-- =============================================
function ns.InitializeWorldQuestsModule()
    eventFrame.isWorldQuestsDisplayActive = false

    local s = GetSettings()
    if not IsModuleEnabled(s) then
        return
    end

    -- Load persisted filter/sort state before first refresh.
    SyncFilterState(true)
    eventFrame:UpdateWQFilterResetState()

    -- Build the tab and panel (deferred by one tick if QuestMapFrame children
    -- are not fully initialised yet â€” ContentsAnchor may have zero size).
    if QuestMapFrame then
        eventFrame._CreateQuestMapTab()
    else
        -- QuestMapFrame is always present in retail but guard just in case.
        eventFrame:RegisterEvent("PLAYER_LOGIN")
    end

    -- Re-inject whenever the World Map first opens (handles deferred loads and
    -- ensures ValidateTabs is called after Blizzard finishes building its tab list).
    -- A short deferred anchor re-check corrects positions when EQOL or other
    -- addons create their own tabs in a later OnShow hook in the same frame.
    WOW.WorldMapFrame:HookScript("OnShow", function()
        local settings = GetSettings()
        if not IsModuleEnabled(settings) then
            ClearPendingDisplayModeRequest()
            return
        end
        eventFrame._CreateQuestMapTab()   -- no-op if already created
        if QuestMapFrame and QuestMapFrame.ValidateTabs then
            QuestMapFrame:ValidateTabs()
        end
        -- Re-anchor after all addons' OnShow hooks have had a chance to run
        -- and register their own tabs into QuestMapFrame.TabButtons.
        if questMapTab then
            WOW.C_Timer.After(0, RebuildTabAnchor)
        end
        if settings.openOnWorldQuestsTab == true and QuestMapFrame and QuestMapFrame.SetDisplayMode then
            WOW.C_Timer.After(0, function()
                RequestWorldQuestsDisplayMode("auto")
            end)
        end
        if ns.IsWorldQuestsRefreshContextActive() then
            ScheduleRefresh()
        end
    end)

    WOW.WorldMapFrame:HookScript("OnHide", function()
        eventFrame.isWorldQuestsDisplayActive = false
        eventFrame:ResetWorldQuestsRefreshGateState()
        ClearPendingDisplayModeRequest()
    end)

    -- Hook map navigation so the panel refreshes on zone/continent changes.
    hooksecurefunc(WOW.WorldMapFrame, "OnMapChanged", function()
        eventFrame:HandleWorldMapChanged()
    end)

    -- Register events.
    -- QUEST_LOG_UPDATE and UI widget update events are registered/unregistered
    -- dynamically while the visible World Quests panel has relevant rows or
    -- pending data.
    eventFrame:RegisterEvent("SUPER_TRACKING_CHANGED")
    eventFrame:RegisterEvent("QUEST_DATA_LOAD_RESULT")
    eventFrame:RegisterEvent("AREA_POIS_UPDATED")
    eventFrame:RegisterUnitEvent("UNIT_AURA", "player")

    eventFrame:SetScript("OnEvent", function(_, event, ...)
        if event == "PLAYER_LOGIN" then
            eventFrame:UnregisterEvent("PLAYER_LOGIN")
            eventFrame._CreateQuestMapTab()
        elseif event == "PLAYER_REGEN_ENABLED" then
            if eventFrame._pendingTabAnchorRebuild then
                RebuildTabAnchor()
            end
            if pendingBuiltinDisplayModeFallback then
                RestoreBuiltinDisplayModeIfNeeded()
            end
            if pendingDisplayModeSource then
                RequestWorldQuestsDisplayMode(pendingDisplayModeSource)
            else
                UpdateDisplayModeRetryRegistration()
            end
            local settings = GetSettings()
            if not IsModuleEnabled(settings) and not ShouldKeepCustomWorldQuestsVisible() then
                if questMapPanel then questMapPanel:Hide() end
                if questMapTab   then questMapTab:Hide()   end
            end
        elseif event == "SUPER_TRACKING_CHANGED" then
            if ns.IsWorldQuestsRefreshContextActive() then
                UpdateSuperTrackHighlights()
            end
        elseif event == "QUEST_DATA_LOAD_RESULT" then
            local questID, success = ...
            if questID then
                if success then
                    -- Clear stale cache so re-query picks up real data.
                    questTypeCache[questID] = nil
                    if eventFrame:IsQuestCoreDataReady(questID) then
                        requestedQuestData[questID] = nil
                        questDataRetrySuppressedUntil[questID] = nil
                    else
                        requestedQuestData[questID] = nil
                        questDataRetrySuppressedUntil[questID] = WOW.GetTime() + 1
                    end

                    local rowNeedsUpdate, needsFullRefresh, searchRefreshOnRewardReady = ResolvePendingQuestDataState(questID)
                    if rowNeedsUpdate and ns.IsWorldQuestsRefreshContextActive() then
                        local rowUpdated, rowNeedsFullRefresh = eventFrame:UpdateQuestRow(questID)
                        if rowNeedsFullRefresh
                            or (searchRefreshOnRewardReady and not rowUpdated)
                        then
                            needsFullRefresh = true
                        end
                    elseif searchRefreshOnRewardReady and ns.IsWorldQuestsRefreshContextActive() then
                        needsFullRefresh = true
                    end
                    if needsFullRefresh then
                        ScheduleRefresh(false)
                    end
                    if ns.IsWorldQuestsRefreshContextActive() then
                        eventFrame:RefreshActiveQuestTooltipIfReady(questID)
                    end
                else
                    SuppressPendingQuestDataAfterFailure(questID)
                end
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
            end
        elseif event == "ITEM_DATA_LOAD_RESULT" then
            local _, success = ...
            if success == false then
                return
            end
            ProcessPendingQuestRewardsAfterItemHydrationEvent("ITEM_DATA_LOAD_RESULT")
        elseif event == "GET_ITEM_INFO_RECEIVED" then
            local _, success = ...
            if success == false then
                return
            end
            ProcessPendingQuestRewardsAfterItemHydrationEvent("GET_ITEM_INFO_RECEIVED")
        elseif event == "QUEST_LOG_UPDATE" then
            if not ns.IsWorldQuestsRefreshContextActive() then
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
                return
            end

            local activeMapID = WOW.WorldMapFrame and WOW.WorldMapFrame.mapID or nil
            local queryState = activeMapID
                and eventFrame:BuildRelevantWorldQuestMapQueryState(activeMapID)
                or nil
            local hasRelevantWorldQuestQuerySignatureChange,
                previousRelevantWorldQuestQuerySignature,
                discoveredRelevantWorldQuestQuerySignature =
                    eventFrame:HasRelevantWorldQuestQuerySignatureChange(activeMapID, queryState)

            if hasRelevantWorldQuestQuerySignatureChange then
                layoutGeneration = layoutGeneration + 1
                rewardPreloadState.CancelDrain()
                eventFrame:CancelWorldQuestDescendantGather()
                wipe(eventFrame._sessionScannedQueryMapIDs)
                wipe(eventFrame._sessionRawEntries)
                wipe(eventFrame._sessionRawEntriesSeen)
                wipe(eventFrame._sessionEnrichedEntries)
                wipe(eventFrame._sessionEnrichedEntriesSeen)
                wipe(eventFrame._sessionQueryStateCache)
                if ns.IsDebugEnabled() then
                    eventFrame:DebugWorldQuestsRefreshTrace(
                        "QUEST_LOG_UPDATE relevant-map signature",
                        WOW.string_format(
                            "mapID=%s previousSignature=%s discoveredSignature=%s",
                            tostring(activeMapID),
                            tostring(previousRelevantWorldQuestQuerySignature),
                            tostring(discoveredRelevantWorldQuestQuerySignature)))
                end
                if activeHoverState.questID then
                    eventFrame._DebugHoverTrace(
                        "QUEST_LOG_UPDATE",
                        "trigger-refresh reason=relevant-map-signature mapID=%s activeQuestID=%s",
                        tostring(activeMapID),
                        tostring(activeHoverState.questID))
                end
                ScheduleRefresh(false, "QUEST_LOG_UPDATE relevant-map signature")
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
                return
            end

            if ns.HasCompletedStagedDescendantGatherQuestMembershipChange(activeMapID, queryState) then
                layoutGeneration = layoutGeneration + 1
                rewardPreloadState.CancelDrain()
                eventFrame:CancelWorldQuestDescendantGather()
                wipe(eventFrame._sessionScannedQueryMapIDs)
                wipe(eventFrame._sessionRawEntries)
                wipe(eventFrame._sessionRawEntriesSeen)
                wipe(eventFrame._sessionEnrichedEntries)
                wipe(eventFrame._sessionEnrichedEntriesSeen)
                wipe(eventFrame._sessionQueryStateCache)
                if ns.IsDebugEnabled() then
                    eventFrame:DebugWorldQuestsRefreshTrace(
                        "QUEST_LOG_UPDATE staged quest membership",
                        WOW.string_format("mapID=%s", tostring(activeMapID)))
                end
                if activeHoverState.questID then
                    eventFrame._DebugHoverTrace(
                        "QUEST_LOG_UPDATE",
                        "trigger-refresh reason=staged-quest-membership mapID=%s activeQuestID=%s",
                        tostring(activeMapID),
                        tostring(activeHoverState.questID))
                end
                ScheduleRefresh(false, "QUEST_LOG_UPDATE staged quest membership")
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
                return
            end

            -- QUEST_LOG_UPDATE fires broadly.  Only act for the active World
            -- Quests panel when visible rows can change or pending data resolves.
            local needsUnlockRefresh = HasVisibleLockedSpecialAssignmentUnlock()
            local needsLockedAreaPOIRefresh = eventFrame:HasVisibleLockedAreaPOIStateChange()
            local needsVisibleQuestRefresh = ns.HasVisibleQuestRemoval()
            if next(pendingQuestIDs) then
                local needsFullRefresh = false
                for qid in pairs(pendingQuestIDs) do
                    local pendingState = pendingQuestIDs[qid]
                    if rewardPreloadState.pollActive
                        and pendingState
                        and pendingState.needsRewardData
                        and not pendingState.needsQuestData
                    then
                        -- Poll handles reward-data-only resolution
                    else
                        local rowNeedsUpdate, questNeedsFullRefresh, searchRefreshOnRewardReady = ResolvePendingQuestDataState(qid)
                        if rowNeedsUpdate then
                            local rowUpdated, rowNeedsFullRefresh = eventFrame:UpdateQuestRow(qid)
                            eventFrame:RefreshActiveQuestTooltipIfReady(qid)
                            if rowNeedsFullRefresh
                                or (searchRefreshOnRewardReady and not rowUpdated)
                            then
                                needsFullRefresh = true
                            end
                        elseif searchRefreshOnRewardReady then
                            needsFullRefresh = true
                        end
                        if questNeedsFullRefresh then
                            needsFullRefresh = true
                        end
                    end
                end
                if needsFullRefresh or needsUnlockRefresh
                    or needsLockedAreaPOIRefresh or needsVisibleQuestRefresh
                then
                    if activeHoverState.questID then
                        eventFrame._DebugHoverTrace(
                            "QUEST_LOG_UPDATE",
                            "trigger-refresh reason=visible-or-pending-change mapID=%s activeQuestID=%s",
                            tostring(activeMapID),
                            tostring(activeHoverState.questID))
                    end
                    ScheduleRefresh(false)
                end
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
            else
                if needsUnlockRefresh or needsLockedAreaPOIRefresh
                    or needsVisibleQuestRefresh
                then
                    if activeHoverState.questID then
                        eventFrame._DebugHoverTrace(
                            "QUEST_LOG_UPDATE",
                            "trigger-refresh reason=visible-change-no-pending mapID=%s activeQuestID=%s",
                            tostring(activeMapID),
                            tostring(activeHoverState.questID))
                    end
                    ScheduleRefresh(false)
                end
                EnsureQuestDataRetryRefresh()
                UpdateQuestLogUpdateRegistration()
            end
        elseif event == "UPDATE_UI_WIDGET" then
            local widgetInfo = ...
            eventFrame:HandleLockedAreaPOIWidgetUpdated(widgetInfo)
        elseif event == "UPDATE_ALL_UI_WIDGETS" then
            eventFrame:HandleAllLockedAreaPOIWidgetsUpdated()
        elseif event == "AREA_POIS_UPDATED" then
            if activeHoverState.questID then
                eventFrame._DebugHoverTrace(
                    "AREA_POIS_UPDATED",
                    "event-branch activeQuestID=%s dispatch=HandleAreaPOIsUpdated",
                    tostring(activeHoverState.questID))
            end
            eventFrame:HandleAreaPOIsUpdated()
        elseif event == "UNIT_AURA" then
            local previousSpellID = currentRelevantContract and currentRelevantContract.spellID or nil
            local previousAuraInstanceID = currentRelevantContract and currentRelevantContract.auraInstanceID or nil
            local previousFactionName = currentRelevantContract and currentRelevantContract.normalizedFactionName or nil
            RefreshContractNotice()
            local currentSpellID = currentRelevantContract and currentRelevantContract.spellID or nil
            local currentAuraInstanceID = currentRelevantContract and currentRelevantContract.auraInstanceID or nil
            local currentFactionName = currentRelevantContract and currentRelevantContract.normalizedFactionName or nil
            if previousSpellID ~= currentSpellID
                or previousAuraInstanceID ~= currentAuraInstanceID
            then
                if ns.IsWorldQuestsRefreshContextActive() then
                    local needsFullRefresh = false
                    for _, item in ipairs(activeContent) do
                        if item.type == "row" and item.frame and item.frame.questID then
                            local _, rowNeedsFullRefresh = eventFrame:UpdateQuestRow(item.frame.questID)
                            if rowNeedsFullRefresh then
                                needsFullRefresh = true
                            end
                        end
                    end
                    if previousFactionName ~= currentFactionName
                        and (sortMode == "faction" or filterSearch ~= "")
                    then
                        needsFullRefresh = true
                    end
                    if needsFullRefresh then
                        ScheduleRefresh(false, "UNIT_AURA contract change")
                    end
                end
            end
        end
    end)

    -- If the World Map is already open at load time, populate immediately.
    if WOW.WorldMapFrame and WOW.WorldMapFrame:IsShown() then
        eventFrame._CreateQuestMapTab()
        if ns.IsWorldQuestsRefreshContextActive() then
            ScheduleRefresh()
        end
    end
end

