local addonName, ns = ...
local L = ns.L or {}

local ASSET_ROOT = "Interface\\AddOns\\" .. tostring(addonName or "Vaultloom") .. "\\Assets\\"

local PREY_BAR_COLORS = {
    fill = { 0.84, 0.18, 0.19, 0.98 },
    spark = { 0.98, 0.42, 0.32, 0.92 },
    text = { 0.98, 0.84, 0.78, 1 },
    note = { 0.82, 0.74, 0.72, 0.96 },
    accent = { 0.74, 0.12, 0.15, 0.78 },
    border = { 0.66, 0.18, 0.20, 0.82 },
    marker = { 0.95, 0.68, 0.54, 0.36 },
}
local PREY_BAR_SCALE_MIN = 0.70
local PREY_BAR_SCALE_MAX = 1.35
local PREY_BAR_SCALE_STEP = 0.05
local PREY_BAR_POSITION_KEY = "prey_bar_position"
local PREY_BAR_SCALE_KEY = "prey_bar_scale"
local PREY_BAR_DEFAULT_POSITION = {
    point = "TOP",
    relativePoint = "TOP",
    x = 0,
    y = -160,
}

local PREY_PROGRESS_FINAL = 3
local PREY_WIDGET_ID = 7663
local PREY_WIDGET_SETUP_FRESH_SECONDS = 1.5
local PREY_WIDGET_CACHE_FRESH_SECONDS = 1.6
local PREY_PROGRESS_REGRESSION_CONFIRM_SECONDS = 0.35
local PREY_PROGRESS_REGRESSION_GRACE_SECONDS = 1.4
local PREY_PROGRESS_INCREASE_BLOCK_SECONDS = 0.6
local PREY_WIDGET_SHOWN = (Enum and Enum.WidgetShownState and Enum.WidgetShownState.Shown) or 1
local PREY_BAR_STAGE_PERCENT_BY_STATE = {
    [0] = 0,
    [1] = 33,
    [2] = 66,
    [3] = 100,
}

local PREY_WIDGET_FRAMES
local PREY_WIDGET_STORED_ALPHA
local PREY_WIDGET_STORED_MOUSE
local PREY_WIDGET_STORED_SHOWN
local PREY_BAR_STATE
local preyHuntIconFrame
local preyWidgetInfoCache
local preyHuntMixinHooked

local PREY_MAP_ID_EQUIVALENTS = {
    [2395] = 2395,
    [2405] = 2405,
    [2413] = 2413,
    [2437] = 2437,
    [2444] = 2405,
    [2536] = 2437,
    [2576] = 2413,
}

local PREY_HUNT_ZONE_MAP_IDS = {
    [2395] = true,
    [2405] = true,
    [2413] = true,
    [2437] = true,
}

local PREY_BROAD_MAP_IDS = {
    [2537] = true,
}

local function canonicalizeMapID(mapID)
    mapID = tonumber(mapID)
    if not mapID or mapID < 1 then
        return nil
    end
    return PREY_MAP_ID_EQUIVALENTS[mapID] or mapID
end

local function choosePreyQuestMapID(candidates)
    for _, candidateMapID in ipairs(candidates or {}) do
        local mapID = canonicalizeMapID(candidateMapID)
        if mapID and PREY_HUNT_ZONE_MAP_IDS[mapID] then
            return mapID
        end
    end
    return nil
end

local function isMapOrChildOf(mapID, parentMapID)
    mapID = tonumber(mapID)
    parentMapID = canonicalizeMapID(parentMapID)
    if not mapID or mapID < 1 or not parentMapID then
        return false
    end

    local seen = {}
    while mapID and mapID > 0 and not seen[mapID] do
        seen[mapID] = true
        if canonicalizeMapID(mapID) == parentMapID then
            return true
        end

        if not C_Map or type(C_Map.GetMapInfo) ~= "function" then
            return false
        end

        local okInfo, mapInfo = pcall(C_Map.GetMapInfo, mapID)
        mapID = okInfo and type(mapInfo) == "table" and tonumber(mapInfo.parentMapID) or nil
    end

    return false
end

local function resolveQuestTitle(questID)
    questID = tonumber(questID)
    if not questID or questID < 1 then
        return nil
    end

    if C_QuestLog and type(C_QuestLog.GetTitleForQuestID) == "function" then
        local okTitle, title = pcall(C_QuestLog.GetTitleForQuestID, questID)
        if okTitle and type(title) == "string" and title ~= "" then
            return title
        end
    end

    if C_QuestLog and type(C_QuestLog.GetLogIndexForQuestID) == "function" and type(C_QuestLog.GetInfo) == "function" then
        local logIndex = C_QuestLog.GetLogIndexForQuestID(questID)
        if logIndex then
            local okInfo, info = pcall(C_QuestLog.GetInfo, logIndex)
            if okInfo and type(info) == "table" and type(info.title) == "string" and info.title ~= "" then
                return info.title
            end
        end
    end

    return nil
end

local function extractPreyTargetName(questTitle)
    if type(questTitle) ~= "string" or questTitle == "" then
        return nil
    end

    local targetName = questTitle:match("^%s*[^:]+:%s*(.-)%s*%((.-)%)%s*$")
    if targetName and targetName ~= "" then
        return targetName
    end

    targetName = questTitle:match("^%s*[^:]+:%s*(.-)%s*$")
    if targetName and targetName ~= "" then
        return targetName
    end

    return questTitle
end

local function resolveQuestMapID(questID)
    questID = tonumber(questID)
    if not questID or questID < 1 then
        return nil
    end

    local candidateMapIDs = {}
    if C_TaskQuest and type(C_TaskQuest.GetQuestZoneID) == "function" then
        local okZoneID, zoneMapID = pcall(C_TaskQuest.GetQuestZoneID, questID)
        if okZoneID and tonumber(zoneMapID) then
            candidateMapIDs[#candidateMapIDs + 1] = zoneMapID
        end
    end

    if type(GetQuestUiMapID) == "function" then
        local okQuestMapID, questMapID = pcall(GetQuestUiMapID, questID)
        if okQuestMapID and tonumber(questMapID) then
            candidateMapIDs[#candidateMapIDs + 1] = questMapID
        end
    end

    if C_QuestLog and type(C_QuestLog.GetNextWaypoint) == "function" then
        local okWaypoint, waypoint = pcall(C_QuestLog.GetNextWaypoint, questID)
        if okWaypoint and type(waypoint) == "table" then
            local waypointMapID = tonumber(waypoint.uiMapID or waypoint.mapID)
            if waypointMapID then
                candidateMapIDs[#candidateMapIDs + 1] = waypointMapID
            end
        end
    end

    return choosePreyQuestMapID(candidateMapIDs)
end

local function resolveMapName(mapID)
    mapID = canonicalizeMapID(mapID)
    if not mapID or not C_Map or type(C_Map.GetMapInfo) ~= "function" then
        return nil
    end

    local okInfo, mapInfo = pcall(C_Map.GetMapInfo, mapID)
    if okInfo and type(mapInfo) == "table" and type(mapInfo.name) == "string" and mapInfo.name ~= "" then
        return mapInfo.name
    end

    return nil
end

local function isPlayerInQuestZone(questMapID)
    questMapID = canonicalizeMapID(questMapID)
    if not questMapID or not C_Map or type(C_Map.GetBestMapForUnit) ~= "function" then
        return false
    end
    if PREY_BROAD_MAP_IDS[questMapID] then
        return false
    end

    local okPlayerMap, playerMapID = pcall(C_Map.GetBestMapForUnit, "player")
    playerMapID = okPlayerMap and tonumber(playerMapID) or nil
    if not playerMapID then
        return false
    end

    if canonicalizeMapID(playerMapID) == questMapID then
        return true
    end

    return isMapOrChildOf(playerMapID, questMapID)
end

local function coerceProtectedNumber(value)
    local okString, asString = pcall(tostring, value)
    if not okString or type(asString) ~= "string" then
        return nil
    end

    local numericToken = asString:match("^%s*([%+%-]?%d+%.?%d*)%s*$")
        or asString:match("^%s*([%+%-]?%d*%.%d+)%s*$")
    if not numericToken then
        return nil
    end

    local okNumber, asNumber = pcall(tonumber, numericToken)
    if okNumber and type(asNumber) == "number" then
        return asNumber
    end

    return nil
end

local function getRuntimeSeconds()
    return (GetTime and GetTime()) or 0
end

local function readPreyValueFromObject(obj, keyName)
    if type(obj) ~= "table" then
        return nil
    end

    local okDirect, directValue = pcall(function()
        return obj[keyName]
    end)
    if okDirect and directValue ~= nil then
        return directValue
    end

    local getterName = "Get" .. string.upper(string.sub(keyName, 1, 1)) .. string.sub(keyName, 2)
    local okGetter, getter = pcall(function()
        return obj[getterName]
    end)
    if okGetter and type(getter) == "function" then
        local okCall, value = pcall(getter, obj)
        if okCall and value ~= nil then
            return value
        end
    end

    return nil
end

local function extractPercentFromTooltip(text)
    if type(text) ~= "string" or text == "" then
        return nil
    end

    local percentText = text:match("(%d+)%s*%%")
    local percentValue = tonumber(percentText)
    if percentValue then
        return ns:ClampNumber(percentValue, 0, 100)
    end

    return nil
end

local function extractProgressPercentFromInfoScan(info)
    if type(info) ~= "table" then
        return nil
    end

    for key, value in pairs(info) do
        value = coerceProtectedNumber(value)
        if value ~= nil then
            local keyText = string.lower(tostring(key))
            if string.find(keyText, "percent", 1, true) then
                if value >= 0 and value <= 1 then
                    return ns:ClampNumber(value * 100, 0, 100)
                end
                return ns:ClampNumber(value, 0, 100)
            end
        end
    end

    local currentValues = {}
    local maxValues = {}
    for key, value in pairs(info) do
        value = coerceProtectedNumber(value)
        if value ~= nil and value >= 0 then
            local keyText = string.lower(tostring(key))
            if string.find(keyText, "current", 1, true)
                or string.find(keyText, "value", 1, true)
                or string.find(keyText, "progress", 1, true)
                or string.find(keyText, "fulfilled", 1, true)
                or string.find(keyText, "completed", 1, true)
            then
                currentValues[#currentValues + 1] = value
            end

            if string.find(keyText, "max", 1, true)
                or string.find(keyText, "total", 1, true)
                or string.find(keyText, "required", 1, true)
            then
                maxValues[#maxValues + 1] = value
            end
        end
    end

    for _, currentValue in ipairs(currentValues) do
        for _, maxValue in ipairs(maxValues) do
            if maxValue > 0 and currentValue <= maxValue then
                return ns:ClampNumber((currentValue / maxValue) * 100, 0, 100)
            end
        end
    end

    return nil
end

local function extractWidgetQuestID(info)
    if type(info) ~= "table" then
        return nil
    end

    local possibleFields = {
        "questID",
        "questId",
        "associatedQuestID",
        "associatedQuestId",
    }

    for _, fieldName in ipairs(possibleFields) do
        local questID = coerceProtectedNumber(readPreyValueFromObject(info, fieldName))
        if questID and questID > 0 then
            return questID
        end
    end

    return nil
end

local function extractWidgetProgressPercent(info, tooltipText)
    if type(info) == "table" then
        local directFields = {
            "progressPercentage",
            "progressPercent",
            "fillPercentage",
            "percentage",
            "percent",
            "progress",
            "progressValue",
        }

        for _, fieldName in ipairs(directFields) do
            local rawValue = coerceProtectedNumber(readPreyValueFromObject(info, fieldName))
            if rawValue ~= nil then
                if rawValue >= 0 and rawValue <= 1 then
                    return ns:ClampNumber(rawValue * 100, 0, 100)
                end
                return ns:ClampNumber(rawValue, 0, 100)
            end
        end

        local valueFields = { "barValue", "value", "currentValue" }
        local maxFields = { "barMax", "maxValue", "totalValue", "total", "max" }
        for _, valueField in ipairs(valueFields) do
            local currentValue = coerceProtectedNumber(readPreyValueFromObject(info, valueField))
            if currentValue ~= nil then
                for _, maxField in ipairs(maxFields) do
                    local maxValue = coerceProtectedNumber(readPreyValueFromObject(info, maxField))
                    if maxValue ~= nil and maxValue > 0 then
                        return ns:ClampNumber((currentValue / maxValue) * 100, 0, 100)
                    end
                end
            end
        end
    end

    local scannedPercent = extractProgressPercentFromInfoScan(info)
    if scannedPercent ~= nil then
        return scannedPercent
    end

    return extractPercentFromTooltip(tooltipText)
end

local function isPreyHuntProgressFrame(frameRef)
    return frameRef
        and type(frameRef.ResetAnimState) == "function"
        and type(frameRef.AnimIn) == "function"
        or false
end

local function findVisiblePreyWidgetFrame()
    local container = _G.UIWidgetPowerBarContainerFrame
    if not container or not container.GetChildren then
        return nil
    end

    local okChildren, children = pcall(function()
        return { container:GetChildren() }
    end)
    if not okChildren or type(children) ~= "table" then
        return nil
    end

    for _, child in ipairs(children) do
        if isPreyHuntProgressFrame(child) and child.IsShown and child:IsShown() then
            return child
        end
    end

    return nil
end

local function resolvePreyFieldsFromFrame(frameRef)
    if type(frameRef) ~= "table" then
        return nil
    end

    local sources = {
        frameRef,
        readPreyValueFromObject(frameRef, "widgetInfo"),
        readPreyValueFromObject(frameRef, "widgetData"),
        readPreyValueFromObject(frameRef, "dataSource"),
        readPreyValueFromObject(frameRef, "info"),
    }

    for _, source in ipairs(sources) do
        if type(source) == "table" then
            local shownState = coerceProtectedNumber(readPreyValueFromObject(source, "shownState"))
            local progressState = coerceProtectedNumber(readPreyValueFromObject(source, "progressState"))
            local tooltipText = readPreyValueFromObject(source, "tooltip")
            if type(tooltipText) ~= "string" then
                tooltipText = nil
            end

            if shownState ~= nil or progressState ~= nil or tooltipText ~= nil then
                return {
                    shownState = shownState,
                    progressState = progressState,
                    tooltipText = tooltipText,
                    percent = extractWidgetProgressPercent(source, tooltipText),
                    questID = extractWidgetQuestID(source),
                }
            end
        end
    end

    return nil
end

local function tryBuildPreyWidgetCacheFromFrame(frameRef)
    if not frameRef then
        return nil
    end

    local frameShown = frameRef.IsShown and frameRef:IsShown() or false
    local now = (GetTime and GetTime()) or 0
    local setupFresh = (now - (PREY_BAR_STATE.lastWidgetSetupAt or 0)) <= PREY_WIDGET_SETUP_FRESH_SECONDS
    if not frameShown and not setupFresh then
        return nil
    end

    local info = resolvePreyFieldsFromFrame(frameRef)
    if not info then
        return nil
    end

    if info.questID == nil then
        info.questID = PREY_BAR_STATE.lastWidgetBoundQuestID
    end

    return info
end

local function resetPreyBarQuestState(questID, forceReset)
    if forceReset == true or PREY_BAR_STATE.activeQuestID ~= questID then
        PREY_BAR_STATE.activeQuestID = questID
        PREY_BAR_STATE.progressState = nil
        PREY_BAR_STATE.progressPercent = nil
        PREY_BAR_STATE.pendingRegressionState = nil
        PREY_BAR_STATE.pendingRegressionPercent = nil
        PREY_BAR_STATE.pendingRegressionSince = nil
        PREY_BAR_STATE.allowRegressionUntil = nil
        PREY_BAR_STATE.blockIncreaseUntil = nil
        PREY_BAR_STATE.lastWidgetSeenAt = 0

        if forceReset == true then
            PREY_BAR_STATE.lastWidgetBoundQuestID = nil
            PREY_BAR_STATE.lastWidgetSetupAt = 0
            preyWidgetInfoCache = nil
        end
    end
end

local function clearPreyWidgetInfoCache()
    PREY_BAR_STATE.lastWidgetSeenAt = 0
    PREY_BAR_STATE.lastWidgetSetupAt = 0
    PREY_BAR_STATE.lastWidgetBoundQuestID = nil
    preyWidgetInfoCache = nil
end

local function clearPreyProgressCache()
    PREY_BAR_STATE.progressState = nil
    PREY_BAR_STATE.progressPercent = nil
    PREY_BAR_STATE.pendingRegressionState = nil
    PREY_BAR_STATE.pendingRegressionPercent = nil
    PREY_BAR_STATE.pendingRegressionSince = nil
    PREY_BAR_STATE.allowRegressionUntil = getRuntimeSeconds() + PREY_PROGRESS_REGRESSION_GRACE_SECONDS
    PREY_BAR_STATE.blockIncreaseUntil = nil
    PREY_BAR_STATE.lastWidgetSeenAt = 0
    preyWidgetInfoCache = nil
end

local function getPreyWidgetProgressFromApi()
    if not C_UIWidgetManager or type(C_UIWidgetManager.GetPreyHuntProgressWidgetVisualizationInfo) ~= "function" then
        return nil
    end

    local okInfo, info = pcall(C_UIWidgetManager.GetPreyHuntProgressWidgetVisualizationInfo, PREY_WIDGET_ID)
    if not okInfo or type(info) ~= "table" then
        return {
            hidden = true,
            source = "api",
        }
    end

    local shownState = coerceProtectedNumber(readPreyValueFromObject(info, "shownState"))
    if shownState ~= PREY_WIDGET_SHOWN then
        return {
            shownState = shownState,
            hidden = true,
            source = "api",
        }
    end

    local tooltipText = readPreyValueFromObject(info, "tooltip")
    if type(tooltipText) ~= "string" then
        tooltipText = nil
    end
    local okPercent, percent = pcall(extractWidgetProgressPercent, info, tooltipText)
    if not okPercent then
        percent = nil
    end
    local okQuestID, questID = pcall(extractWidgetQuestID, info)
    if not okQuestID then
        questID = nil
    end

    return {
        shownState = shownState,
        progressState = coerceProtectedNumber(readPreyValueFromObject(info, "progressState")),
        percent = percent,
        tooltipText = tooltipText,
        questID = questID,
        source = "api",
        observedAt = getRuntimeSeconds(),
    }
end

local function isFreshSetupWidgetCache(info)
    if type(info) ~= "table" or info.fromSetup ~= true then
        return false
    end

    local observedAt = tonumber(info.observedAt)
    return observedAt ~= nil and (getRuntimeSeconds() - observedAt) <= PREY_WIDGET_CACHE_FRESH_SECONDS
end

local function clearPendingRegression()
    PREY_BAR_STATE.pendingRegressionState = nil
    PREY_BAR_STATE.pendingRegressionPercent = nil
    PREY_BAR_STATE.pendingRegressionSince = nil
end

local function applyStablePreyProgress(newProgressState, newProgressPercent)
    if newProgressState == nil then
        return false
    end

    local currentProgressState = tonumber(PREY_BAR_STATE.progressState)
    local now = getRuntimeSeconds()
    local allowRegressionUntil = tonumber(PREY_BAR_STATE.allowRegressionUntil)
    local allowRegression = allowRegressionUntil ~= nil and now <= allowRegressionUntil
    local blockIncreaseUntil = tonumber(PREY_BAR_STATE.blockIncreaseUntil)

    if currentProgressState ~= nil
        and newProgressState > currentProgressState
        and blockIncreaseUntil ~= nil
        and now <= blockIncreaseUntil
    then
        return false
    end

    if currentProgressState == nil or newProgressState >= currentProgressState then
        PREY_BAR_STATE.progressState = newProgressState
        PREY_BAR_STATE.blockIncreaseUntil = nil
        clearPendingRegression()
        return true
    end

    if allowRegression then
        PREY_BAR_STATE.progressState = newProgressState
        PREY_BAR_STATE.blockIncreaseUntil = now + PREY_PROGRESS_INCREASE_BLOCK_SECONDS
        clearPendingRegression()
        return true
    end

    if PREY_BAR_STATE.pendingRegressionState == newProgressState
        and tonumber(PREY_BAR_STATE.pendingRegressionSince)
        and (now - PREY_BAR_STATE.pendingRegressionSince) >= PREY_PROGRESS_REGRESSION_CONFIRM_SECONDS
    then
        PREY_BAR_STATE.progressState = newProgressState
        PREY_BAR_STATE.blockIncreaseUntil = now + PREY_PROGRESS_INCREASE_BLOCK_SECONDS
        clearPendingRegression()
        return true
    end

    PREY_BAR_STATE.pendingRegressionState = newProgressState
    PREY_BAR_STATE.pendingRegressionPercent = newProgressPercent
    PREY_BAR_STATE.pendingRegressionSince = now
    return false
end

local function getLivePreyWidgetProgress(questID)
    local apiProgress = getPreyWidgetProgressFromApi()
    if type(apiProgress) == "table" then
        if apiProgress.hidden == true then
            preyWidgetInfoCache = nil
            PREY_BAR_STATE.lastWidgetSeenAt = 0
            return {
                hidden = true,
                source = "api",
            }
        end

        if apiProgress.questID == nil then
            apiProgress.questID = questID
        end

        preyWidgetInfoCache = apiProgress
        PREY_BAR_STATE.lastWidgetSeenAt = getRuntimeSeconds()

        if apiProgress.progressState ~= nil or apiProgress.percent ~= nil then
            return {
                percent = apiProgress.percent ~= nil and ns:ClampNumber(apiProgress.percent, 0, 100) or nil,
                progressState = apiProgress.progressState,
                tooltipText = type(apiProgress.tooltipText) == "string" and apiProgress.tooltipText or nil,
                source = "api",
            }
        end
    end

    local refreshed = nil

    if isFreshSetupWidgetCache(preyWidgetInfoCache) then
        refreshed = preyWidgetInfoCache
    elseif preyHuntIconFrame then
        refreshed = tryBuildPreyWidgetCacheFromFrame(preyHuntIconFrame)
    end

    if not refreshed then
        for frameRef in pairs(PREY_WIDGET_FRAMES) do
            refreshed = tryBuildPreyWidgetCacheFromFrame(frameRef)
            if refreshed then
                break
            end
        end
    end

    if not refreshed then
        local visibleFrame = findVisiblePreyWidgetFrame()
        refreshed = tryBuildPreyWidgetCacheFromFrame(visibleFrame)
    end

    if refreshed then
        if type(preyWidgetInfoCache) == "table" then
            if refreshed.questID == nil and preyWidgetInfoCache.questID ~= nil then
                refreshed.questID = preyWidgetInfoCache.questID
            end
        end

        preyWidgetInfoCache = refreshed
    elseif type(preyWidgetInfoCache) ~= "table" or preyWidgetInfoCache.progressState == nil then
        return nil
    end

    local info = preyWidgetInfoCache
    if info.shownState ~= nil and info.shownState ~= PREY_WIDGET_SHOWN then
        return nil
    end

    local widgetQuestID = tonumber(info.questID)
    if widgetQuestID ~= nil and widgetQuestID > 0 and widgetQuestID ~= tonumber(questID) then
        return nil
    end

    local progressState = coerceProtectedNumber(info.progressState)
    local percent = coerceProtectedNumber(info.percent)
    if progressState == nil and percent == nil then
        return nil
    end

    PREY_BAR_STATE.lastWidgetSeenAt = (GetTime and GetTime()) or 0

    return {
        percent = percent ~= nil and ns:ClampNumber(percent, 0, 100) or nil,
        progressState = progressState,
        tooltipText = type(info.tooltipText) == "string" and info.tooltipText or nil,
        source = info.source or "frame",
    }
end

local function extractObjectiveStage(objectives)
    if type(objectives) ~= "table" or #objectives == 0 then
        return nil
    end

    local function isObjectiveDone(objective)
        if type(objective) ~= "table" then
            return false
        end
        if objective.finished == true then
            return true
        end

        local fulfilled = tonumber(objective.numFulfilled or objective.fulfilled)
        local required = tonumber(objective.numRequired or objective.required)
        if fulfilled and required and required > 0 then
            return fulfilled >= required
        end

        local text = type(objective.text) == "string" and objective.text or nil
        if text then
            local currentText, totalText = text:match("(%d+)%s*/%s*(%d+)")
            local currentValue = tonumber(currentText)
            local totalValue = tonumber(totalText)
            if currentValue and totalValue and totalValue > 0 then
                return currentValue >= totalValue
            end
        end

        return false
    end

    local firstDone = isObjectiveDone(objectives[1])
    local secondDone = isObjectiveDone(objectives[2])
    local secondPresent = type(objectives[2]) == "table"
        and ((type(objectives[2].text) == "string" and objectives[2].text ~= "")
            or tonumber(objectives[2].numRequired or objectives[2].required))

    if secondDone or (firstDone and secondPresent) then
        return PREY_PROGRESS_FINAL
    elseif firstDone then
        return 1
    else
        return 0
    end
end

local function buildQuestProgressState(questID)
    resetPreyBarQuestState(questID, false)

    local widgetProgress = getLivePreyWidgetProgress(questID)
    if widgetProgress and widgetProgress.hidden == true then
        return {
            hidden = true,
            percent = 0,
            current = 0,
            total = PREY_PROGRESS_FINAL,
            progressState = 0,
        }
    end

    local newProgressState = widgetProgress and tonumber(widgetProgress.progressState) or nil
    local newProgressPercent = widgetProgress and tonumber(widgetProgress.percent) or nil

    local objectives = nil
    if C_QuestLog and type(C_QuestLog.GetQuestObjectives) == "function" then
        local okObjectives, questObjectives = pcall(C_QuestLog.GetQuestObjectives, questID)
        objectives = okObjectives and type(questObjectives) == "table" and questObjectives or nil
    end

    local totalFulfilled = 0
    local totalRequired = 0
    local anyNumericObjective = false
    local activeObjectiveText = nil

    for _, objective in ipairs(objectives or {}) do
        if type(objective) == "table" then
            local fulfilled = tonumber(objective.numFulfilled or objective.fulfilled)
            local required = tonumber(objective.numRequired or objective.required)
            if fulfilled and required == nil and objective.finished ~= nil then
                required = 1
                fulfilled = objective.finished and 1 or math.max(0, fulfilled)
            end
            if fulfilled and required and required > 0 then
                totalFulfilled = totalFulfilled + math.max(0, fulfilled)
                totalRequired = totalRequired + math.max(0, required)
                anyNumericObjective = true
                if activeObjectiveText == nil and objective.finished ~= true and type(objective.text) == "string" and objective.text ~= "" then
                    activeObjectiveText = objective.text
                end
            else
                local text = type(objective.text) == "string" and objective.text or nil
                if text and text ~= "" then
                    if activeObjectiveText == nil and objective.finished ~= true then
                        activeObjectiveText = text
                    end
                end
            end
        end
    end

    if newProgressState ~= nil then
        newProgressState = math.max(0, math.min(PREY_PROGRESS_FINAL, math.floor(newProgressState + 0.5)))
    end

    if newProgressState == nil then
        local inferredProgressState = extractObjectiveStage(objectives)
        if inferredProgressState ~= nil then
            newProgressState = inferredProgressState
        end
    end

    local progressStateApplied
    if widgetProgress and widgetProgress.source == "api" and newProgressState ~= nil then
        PREY_BAR_STATE.progressState = newProgressState
        PREY_BAR_STATE.blockIncreaseUntil = nil
        PREY_BAR_STATE.allowRegressionUntil = nil
        clearPendingRegression()
        progressStateApplied = true
    else
        progressStateApplied = applyStablePreyProgress(newProgressState, newProgressPercent)
    end

    if progressStateApplied then
        if newProgressPercent ~= nil then
            PREY_BAR_STATE.progressPercent = ns:ClampNumber(newProgressPercent, 0, 100)
        elseif newProgressState == PREY_PROGRESS_FINAL then
            PREY_BAR_STATE.progressPercent = 100
        else
            PREY_BAR_STATE.progressPercent = nil
        end
    end

    local progressState = tonumber(PREY_BAR_STATE.progressState) or 0
    local percent = PREY_BAR_STAGE_PERCENT_BY_STATE[progressState] or 0
    if progressState >= PREY_PROGRESS_FINAL then
        percent = 100
    else
        local livePercent = tonumber(PREY_BAR_STATE.progressPercent)
        local stageCeiling = PREY_BAR_STAGE_PERCENT_BY_STATE[progressState] or 0
        if livePercent ~= nil and livePercent > 0 and stageCeiling > 0 then
            percent = math.min(livePercent, stageCeiling)
        end
    end

    return {
        percent = percent,
        current = progressState,
        total = PREY_PROGRESS_FINAL,
        note = activeObjectiveText,
        stage = progressState + 1,
        progressState = progressState,
    }
end

local PreyBarRuntime = {
    enabled = false,
    activeEventsEnabled = false,
    refreshPending = false,
    currentContextInZone = false,
}
PREY_WIDGET_FRAMES = setmetatable({}, { __mode = "k" })
PREY_WIDGET_STORED_ALPHA = setmetatable({}, { __mode = "k" })
PREY_WIDGET_STORED_MOUSE = setmetatable({}, { __mode = "k" })
PREY_WIDGET_STORED_SHOWN = setmetatable({}, { __mode = "k" })
local PREY_WIDGET_REGION_STORED_ALPHA = setmetatable({}, { __mode = "k" })
local PREY_WIDGET_REGION_STORED_SHOWN = setmetatable({}, { __mode = "k" })
local PREY_WIDGET_ONSHOW_HOOKED = setmetatable({}, { __mode = "k" })
PREY_BAR_STATE = {
    activeQuestID = nil,
    progressState = nil,
    progressPercent = nil,
    pendingRegressionState = nil,
    pendingRegressionPercent = nil,
    pendingRegressionSince = nil,
    allowRegressionUntil = nil,
    blockIncreaseUntil = nil,
    lastWidgetSeenAt = 0,
    lastWidgetSetupAt = 0,
    lastWidgetBoundQuestID = nil,
}
preyHuntIconFrame = nil
preyWidgetInfoCache = nil
preyHuntMixinHooked = false

local PREY_BAR_BASE_EVENTS = {
    "PLAYER_ENTERING_WORLD",
    "ZONE_CHANGED",
    "ZONE_CHANGED_INDOORS",
    "ZONE_CHANGED_NEW_AREA",
    "QUEST_ACCEPTED",
    "QUEST_COMPLETE",
    "QUEST_TURNED_IN",
    "QUEST_REMOVED",
}

local PREY_BAR_ACTIVE_EVENTS = {
    "QUEST_LOG_UPDATE",
    "UPDATE_UI_WIDGET",
    "UPDATE_ALL_UI_WIDGETS",
    "PLAYER_DEAD",
    "PLAYER_ALIVE",
    "PLAYER_UNGHOST",
}

local function captureLivePreyHuntFrames()
    local container = _G.UIWidgetPowerBarContainerFrame
    if not container or not container.GetChildren then
        return
    end

    local okChildren, children = pcall(function()
        return { container:GetChildren() }
    end)
    if not okChildren or type(children) ~= "table" then
        return
    end

    for _, child in ipairs(children) do
        if isPreyHuntProgressFrame(child) then
            PREY_WIDGET_FRAMES[child] = true
            preyHuntIconFrame = child
        end
    end
end

local function setPreyWidgetRegionShown(region, shouldShow)
    if not region then
        return
    end

    if region.SetShown then
        pcall(region.SetShown, region, shouldShow)
        return
    end

    if shouldShow and region.Show then
        pcall(region.Show, region)
        return
    end

    if (not shouldShow) and region.Hide then
        pcall(region.Hide, region)
    end
end

local function applySinglePreyWidgetFrameSuppression(frameRef, suppress)
    if not frameRef or type(frameRef.SetAlpha) ~= "function" then
        return
    end

    if suppress then
        if PREY_WIDGET_STORED_SHOWN[frameRef] == nil and frameRef.IsShown then
            PREY_WIDGET_STORED_SHOWN[frameRef] = frameRef:IsShown() and true or false
        end
        if PREY_WIDGET_STORED_ALPHA[frameRef] == nil and type(frameRef.GetAlpha) == "function" then
            local okAlpha, alpha = pcall(frameRef.GetAlpha, frameRef)
            if okAlpha and type(alpha) == "number" then
                PREY_WIDGET_STORED_ALPHA[frameRef] = alpha
            end
        end
        if PREY_WIDGET_STORED_MOUSE[frameRef] == nil and type(frameRef.IsMouseEnabled) == "function" then
            local okMouse, mouseEnabled = pcall(frameRef.IsMouseEnabled, frameRef)
            if okMouse then
                PREY_WIDGET_STORED_MOUSE[frameRef] = mouseEnabled and true or false
            end
        end
        pcall(frameRef.SetAlpha, frameRef, 0)
        if type(frameRef.EnableMouse) == "function" then
            pcall(frameRef.EnableMouse, frameRef, false)
        end
        if frameRef.Hide and not (type(_G.InCombatLockdown) == "function" and _G.InCombatLockdown()) then
            pcall(frameRef.Hide, frameRef)
        end
        return
    end

    local restoreAlpha = PREY_WIDGET_STORED_ALPHA[frameRef]
    PREY_WIDGET_STORED_ALPHA[frameRef] = nil
    pcall(frameRef.SetAlpha, frameRef, type(restoreAlpha) == "number" and restoreAlpha or 1)

    local restoreMouse = PREY_WIDGET_STORED_MOUSE[frameRef]
    PREY_WIDGET_STORED_MOUSE[frameRef] = nil
    if type(frameRef.EnableMouse) == "function" then
        pcall(frameRef.EnableMouse, frameRef, restoreMouse == true)
    end

    local restoreShown = PREY_WIDGET_STORED_SHOWN[frameRef]
    PREY_WIDGET_STORED_SHOWN[frameRef] = nil
    if restoreShown == true and frameRef.Show then
        pcall(frameRef.Show, frameRef)
    end
end

local function stopPreyWidgetAnimations(frameRef, visited)
    if not frameRef then
        return
    end

    visited = visited or {}
    if visited[frameRef] then
        return
    end
    visited[frameRef] = true

    local effectController = frameRef.effectController
    if effectController and type(effectController.CancelEffect) == "function" then
        pcall(effectController.CancelEffect, effectController)
    end

    if frameRef.GetAnimationGroups then
        local okGroups, groups = pcall(function()
            return { frameRef:GetAnimationGroups() }
        end)
        if okGroups and type(groups) == "table" then
            for _, group in ipairs(groups) do
                if group and group.Stop then
                    pcall(group.Stop, group)
                end
            end
        end
    end

    for _, fieldName in ipairs({ "AnimIn", "AnimOut", "GlowAnim", "PulseAnim", "Loop", "LoopingGlow", "Shine" }) do
        local anim = frameRef[fieldName]
        if anim and type(anim) ~= "function" and anim.Stop then
            pcall(anim.Stop, anim)
        end
    end
end

local function applyPreyWidgetVisualSuppression(frameRef, suppress, visited)
    if not frameRef then
        return
    end

    visited = visited or {}
    if visited[frameRef] then
        return
    end
    visited[frameRef] = true

    if suppress then
        stopPreyWidgetAnimations(frameRef)
    end

    applySinglePreyWidgetFrameSuppression(frameRef, suppress)

    if frameRef.GetRegions then
        local okRegions, regions = pcall(function()
            return { frameRef:GetRegions() }
        end)
        if okRegions and type(regions) == "table" then
            for _, region in ipairs(regions) do
                local objectType = region and region.GetObjectType and region:GetObjectType() or nil
                if objectType == "Texture" or objectType == "FontString" then
                    if suppress then
                        if PREY_WIDGET_REGION_STORED_SHOWN[region] == nil and region.IsShown then
                            PREY_WIDGET_REGION_STORED_SHOWN[region] = region:IsShown() and true or false
                        end
                        if PREY_WIDGET_REGION_STORED_ALPHA[region] == nil and region.GetAlpha then
                            local okAlpha, alpha = pcall(region.GetAlpha, region)
                            if okAlpha and type(alpha) == "number" then
                                PREY_WIDGET_REGION_STORED_ALPHA[region] = alpha
                            end
                        end
                        if region.SetAlpha then
                            pcall(region.SetAlpha, region, 0)
                        end
                        setPreyWidgetRegionShown(region, false)
                    else
                        local restoreAlpha = PREY_WIDGET_REGION_STORED_ALPHA[region]
                        PREY_WIDGET_REGION_STORED_ALPHA[region] = nil
                        if region.SetAlpha then
                            pcall(region.SetAlpha, region, type(restoreAlpha) == "number" and restoreAlpha or 1)
                        end
                        local restoreShown = PREY_WIDGET_REGION_STORED_SHOWN[region]
                        PREY_WIDGET_REGION_STORED_SHOWN[region] = nil
                        if restoreShown ~= nil then
                            setPreyWidgetRegionShown(region, restoreShown == true)
                        end
                    end
                end
            end
        end
    end

    if frameRef.GetChildren then
        local okChildren, children = pcall(function()
            return { frameRef:GetChildren() }
        end)
        if okChildren and type(children) == "table" then
            for _, child in ipairs(children) do
                applyPreyWidgetVisualSuppression(child, suppress, visited)
            end
        end
    end
end

local function setPreyWidgetFrameSuppressed(frameRef, suppress)
    applyPreyWidgetVisualSuppression(frameRef, suppress, {})
end

local function shouldSuppressDefaultPreyIcon()
    if PreyBarRuntime.enabled ~= true then
        return false
    end

    if (ns.GetFeatureOptionValue and ns:GetFeatureOptionValue("prey_bar", "widget_visibility") or "hidden") == "shown" then
        return false
    end

    return true
end

local function ensurePreyWidgetOnShowSuppression(frameRef)
    if not frameRef or PREY_WIDGET_ONSHOW_HOOKED[frameRef] then
        return
    end

    if type(frameRef.HookScript) ~= "function" then
        return
    end

    PREY_WIDGET_ONSHOW_HOOKED[frameRef] = true
    pcall(frameRef.HookScript, frameRef, "OnShow", function(selfFrame)
        if shouldSuppressDefaultPreyIcon() ~= true then
            return
        end

        local function suppressAgain()
            if shouldSuppressDefaultPreyIcon() == true then
                setPreyWidgetFrameSuppressed(selfFrame, true)
            end
        end

        if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            C_Timer.After(0, suppressAgain)
            C_Timer.After(0.08, suppressAgain)
        else
            suppressAgain()
        end
    end)
end

local function applyDefaultPreyIconVisibility()
    captureLivePreyHuntFrames()

    if preyHuntIconFrame then
        PREY_WIDGET_FRAMES[preyHuntIconFrame] = true
    end

    local suppress = shouldSuppressDefaultPreyIcon()
    for frameRef in pairs(PREY_WIDGET_FRAMES) do
        ensurePreyWidgetOnShowSuppression(frameRef)
        setPreyWidgetFrameSuppressed(frameRef, suppress)
    end
end

local function ensurePreyHuntMixinSuppressionHook()
    if preyHuntMixinHooked then
        return
    end

    local mixin = _G.UIWidgetTemplatePreyHuntProgressMixin
    if not mixin or type(hooksecurefunc) ~= "function" then
        return
    end

    preyHuntMixinHooked = true
    pcall(hooksecurefunc, mixin, "Setup", function(self, widgetInfo)
        preyHuntIconFrame = self
        PREY_WIDGET_FRAMES[self] = true

        PREY_BAR_STATE.lastWidgetSetupAt = (GetTime and GetTime()) or 0

        local shownState = nil
        local progressState = nil
        local tooltipText = nil
        local widgetQuestID = nil
        local percent = nil

        if type(widgetInfo) == "table" then
            shownState = coerceProtectedNumber(widgetInfo.shownState)
            progressState = coerceProtectedNumber(widgetInfo.progressState)
            widgetQuestID = extractWidgetQuestID(widgetInfo)
            tooltipText = type(widgetInfo.tooltip) == "string" and widgetInfo.tooltip or nil
            percent = extractWidgetProgressPercent(widgetInfo, tooltipText)
        end

        if widgetQuestID and widgetQuestID > 0 then
            PREY_BAR_STATE.lastWidgetBoundQuestID = widgetQuestID
        elseif PREY_BAR_STATE.activeQuestID and PREY_BAR_STATE.activeQuestID > 0 then
            PREY_BAR_STATE.lastWidgetBoundQuestID = PREY_BAR_STATE.activeQuestID
        end

        if shownState ~= nil or progressState ~= nil or tooltipText ~= nil or percent ~= nil then
            preyWidgetInfoCache = {
                shownState = shownState,
                progressState = progressState,
                tooltipText = tooltipText,
                percent = percent,
                questID = PREY_BAR_STATE.lastWidgetBoundQuestID,
                observedAt = getRuntimeSeconds(),
                fromSetup = true,
            }
        end

        if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
            C_Timer.After(0, function()
                applyDefaultPreyIconVisibility()
                if PreyBarRuntime.enabled then
                    PreyBarRuntime:QueueRefresh(0.05)
                end
            end)
        else
            applyDefaultPreyIconVisibility()
            if PreyBarRuntime.enabled then
                PreyBarRuntime:QueueRefresh(0.05)
            end
        end
    end)
end

local function ensurePreyBarFrame()
    if PreyBarRuntime.frame then
        return PreyBarRuntime.frame
    end

    local frame = CreateFrame("Frame", "VaultloomPreyFeatureBar", UIParent, BACKDROP_TEMPLATE)
    frame:SetSize(312, 48)
    frame:SetFrameStrata("MEDIUM")
    frame:SetFrameLevel(8)
    frame:SetClampedToScreen(true)
    frame:EnableMouse(true)
    frame:EnableMouseWheel(true)
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:Hide()
    ns:ApplyFeaturePosition(frame, PREY_BAR_POSITION_KEY, PREY_BAR_DEFAULT_POSITION)
    ns:ApplyFeatureScale(frame, PREY_BAR_SCALE_KEY, PREY_BAR_SCALE_MIN, PREY_BAR_SCALE_MAX)

    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(0.06, 0.02, 0.03, 0.88)
        frame:SetBackdropBorderColor(PREY_BAR_COLORS.border[1], PREY_BAR_COLORS.border[2], PREY_BAR_COLORS.border[3], PREY_BAR_COLORS.border[4])
    end

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()
    frame.bg:SetTexture(ASSET_ROOT .. "bar_bg_reskin.tga")
    frame.bg:SetVertexColor(0.28, 0.04, 0.06, 0.96)

    frame.overlay = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
    frame.overlay:SetAllPoints()
    frame.overlay:SetTexture(ASSET_ROOT .. "panel_overlay_reskin.tga")
    frame.overlay:SetVertexColor(0.26, 0.06, 0.08, 0.14)

    frame.topAccent = frame:CreateTexture(nil, "ARTWORK")
    frame.topAccent:SetPoint("TOPLEFT", 10, -5)
    frame.topAccent:SetPoint("TOPRIGHT", -10, -5)
    frame.topAccent:SetHeight(1)
    frame.topAccent:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.topAccent:SetColorTexture(PREY_BAR_COLORS.accent[1], PREY_BAR_COLORS.accent[2], PREY_BAR_COLORS.accent[3], PREY_BAR_COLORS.accent[4])

    frame.caption = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.caption:SetPoint("TOPLEFT", 12, -8)
    frame.caption:SetPoint("TOPRIGHT", -72, -8)
    frame.caption:SetJustifyH("LEFT")
    frame.caption:SetTextColor(PREY_BAR_COLORS.text[1], PREY_BAR_COLORS.text[2], PREY_BAR_COLORS.text[3], PREY_BAR_COLORS.text[4])
    frame.caption:SetText(L.FEATURE_PREY_BAR_RUNTIME_LABEL or "Prey Hunt")

    frame.percent = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.percent:SetPoint("TOPRIGHT", -12, -8)
    frame.percent:SetJustifyH("RIGHT")
    frame.percent:SetTextColor(PREY_BAR_COLORS.text[1], PREY_BAR_COLORS.text[2], PREY_BAR_COLORS.text[3], PREY_BAR_COLORS.text[4])
    frame.percent:SetText("0%")

    frame.bar = CreateFrame("StatusBar", nil, frame)
    frame.bar:SetPoint("TOPLEFT", frame.caption, "BOTTOMLEFT", 0, -6)
    frame.bar:SetPoint("TOPRIGHT", frame.percent, "BOTTOMRIGHT", 0, -6)
    frame.bar:SetHeight(17)
    frame.bar:SetMinMaxValues(0, 1)
    frame.bar:SetValue(0)
    frame.bar:SetStatusBarTexture(ASSET_ROOT .. "bar_fill_reskin.tga")
    frame.bar:SetStatusBarColor(PREY_BAR_COLORS.fill[1], PREY_BAR_COLORS.fill[2], PREY_BAR_COLORS.fill[3], PREY_BAR_COLORS.fill[4])

    frame.barBackdrop = frame.bar:CreateTexture(nil, "BACKGROUND", nil, -1)
    frame.barBackdrop:SetPoint("TOPLEFT", -1, 1)
    frame.barBackdrop:SetPoint("BOTTOMRIGHT", 1, -1)
    frame.barBackdrop:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.barBackdrop:SetColorTexture(0.02, 0.01, 0.02, 0.86)

    frame.barBg = frame.bar:CreateTexture(nil, "BACKGROUND")
    frame.barBg:SetAllPoints()
    frame.barBg:SetTexture(ASSET_ROOT .. "bar_bg_reskin.tga")
    frame.barBg:SetVertexColor(0.16, 0.03, 0.04, 0.96)

    frame.barFill = frame.bar:GetStatusBarTexture()
    frame.barFill:SetHorizTile(false)
    frame.barFill:SetVertTile(false)

    frame.barOverlay = frame.bar:CreateTexture(nil, "ARTWORK")
    frame.barOverlay:SetPoint("TOPLEFT", frame.barFill, "TOPLEFT", 0, 0)
    frame.barOverlay:SetPoint("BOTTOMRIGHT", frame.barFill, "BOTTOMRIGHT", 0, 0)
    frame.barOverlay:SetTexture(ASSET_ROOT .. "bar_overlay_reskin.tga")
    frame.barOverlay:SetBlendMode("ADD")
    frame.barOverlay:SetVertexColor(1, 1, 1, 0.22)

    frame.barSpark = frame.bar:CreateTexture(nil, "OVERLAY")
    frame.barSpark:SetSize(15, 21)
    frame.barSpark:SetTexture(ASSET_ROOT .. "bar_spark_reskin.tga")
    frame.barSpark:SetBlendMode("ADD")
    frame.barSpark:SetVertexColor(PREY_BAR_COLORS.spark[1], PREY_BAR_COLORS.spark[2], PREY_BAR_COLORS.spark[3], PREY_BAR_COLORS.spark[4])
    frame.barSpark:SetAlpha(0.54)

    frame.stepMarkers = {}
    for _, markerData in ipairs({
        { key = "stage33", ratio = 0.33 },
        { key = "stage66", ratio = 0.66 },
    }) do
        local marker = {}
        marker.ratio = markerData.ratio

        marker.line = frame.bar:CreateTexture(nil, "OVERLAY")
        marker.line:SetWidth(2)
        marker.line:SetHeight(13)
        marker.line:SetTexture("Interface\\Buttons\\WHITE8X8")
        marker.line:SetColorTexture(PREY_BAR_COLORS.marker[1], PREY_BAR_COLORS.marker[2], PREY_BAR_COLORS.marker[3], PREY_BAR_COLORS.marker[4])

        frame.stepMarkers[#frame.stepMarkers + 1] = marker
    end

    frame.note = frame:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    frame.note:SetPoint("TOPLEFT", frame.bar, "BOTTOMLEFT", 1, -6)
    frame.note:SetPoint("TOPRIGHT", frame.bar, "BOTTOMRIGHT", -1, -6)
    frame.note:SetJustifyH("LEFT")
    frame.note:SetTextColor(PREY_BAR_COLORS.note[1], PREY_BAR_COLORS.note[2], PREY_BAR_COLORS.note[3], PREY_BAR_COLORS.note[4])
    frame.note:SetText(L.FEATURE_PREY_BAR_RUNTIME_SUMMARY_FALLBACK or "Active prey tracked live")

    frame:SetScript("OnDragStart", function(selfFrame)
        selfFrame:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(selfFrame)
        selfFrame:StopMovingOrSizing()
        ns:StoreFeaturePosition(selfFrame, PREY_BAR_POSITION_KEY)
    end)
    frame:SetScript("OnEnter", function(selfFrame)
        GameTooltip:SetOwner(selfFrame, "ANCHOR_TOP")
        GameTooltip:AddLine(L.FEATURE_PREY_BAR_TITLE or "Prey Progress Bar", 1, 0.82, 0.22, true)
        GameTooltip:AddLine(L.FEATURE_PREY_BAR_RUNTIME_TOOLTIP_DRAG or "Left drag: move", 0.84, 0.84, 0.82, true)
        GameTooltip:AddLine(L.FEATURE_PREY_BAR_RUNTIME_TOOLTIP_WHEEL or "Shift + mouse wheel: size", 0.84, 0.84, 0.82, true)
        GameTooltip:Show()
    end)
    frame:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    frame:SetScript("OnMouseWheel", function(selfFrame, delta)
        if type(IsShiftKeyDown) == "function" and not IsShiftKeyDown() then
            return
        end
        local nextScale = ns:GetFeatureScale(PREY_BAR_SCALE_KEY, PREY_BAR_SCALE_MIN, PREY_BAR_SCALE_MAX)
        if delta > 0 then
            nextScale = nextScale + PREY_BAR_SCALE_STEP
        elseif delta < 0 then
            nextScale = nextScale - PREY_BAR_SCALE_STEP
        end

        nextScale = ns:ClampNumber(nextScale, PREY_BAR_SCALE_MIN, PREY_BAR_SCALE_MAX)
        selfFrame:SetScale(nextScale)
        ns:StoreFeatureScale(PREY_BAR_SCALE_KEY, nextScale, PREY_BAR_SCALE_MIN, PREY_BAR_SCALE_MAX)
    end)

    PreyBarRuntime.frame = frame
    return frame
end

function PreyBarRuntime:ApplyFrameStyle()
    local frame = ensurePreyBarFrame()
    local frameStyle = ns.GetFeatureOptionValue and ns:GetFeatureOptionValue("prey_bar", "frame_style") or "shown"
    local showFrame = frameStyle ~= "hidden"

    if frame.SetBackdropColor then
        frame:SetBackdropColor(0.06, 0.02, 0.03, showFrame and 0.88 or 0)
    end
    if frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(
            PREY_BAR_COLORS.border[1],
            PREY_BAR_COLORS.border[2],
            PREY_BAR_COLORS.border[3],
            showFrame and PREY_BAR_COLORS.border[4] or 0
        )
    end
    frame.bg:SetShown(showFrame)
    frame.overlay:SetShown(showFrame)
    frame.topAccent:SetShown(showFrame)

    frame:SetSize(showFrame and 312 or 286, showFrame and 48 or 32)
    frame.caption:ClearAllPoints()
    frame.percent:ClearAllPoints()
    frame.bar:ClearAllPoints()

    if showFrame then
        frame.caption:SetPoint("TOPLEFT", 12, -8)
        frame.caption:SetPoint("TOPRIGHT", -72, -8)
        frame.percent:SetPoint("TOPRIGHT", -12, -8)
        frame.bar:SetPoint("TOPLEFT", frame.caption, "BOTTOMLEFT", 0, -6)
        frame.bar:SetPoint("TOPRIGHT", frame.percent, "BOTTOMRIGHT", 0, -6)
        frame.bar:SetHeight(17)
    else
        frame.caption:SetPoint("TOPLEFT", 0, -1)
        frame.caption:SetPoint("TOPRIGHT", -58, -1)
        frame.percent:SetPoint("TOPRIGHT", 0, -1)
        frame.bar:SetPoint("TOPLEFT", frame.caption, "BOTTOMLEFT", 0, -4)
        frame.bar:SetPoint("TOPRIGHT", frame.percent, "BOTTOMRIGHT", 0, -4)
        frame.bar:SetHeight(16)
    end
end

function PreyBarRuntime:SetActiveEventsEnabled(enabled)
    local eventFrame = self.eventFrame
    if not eventFrame or self.activeEventsEnabled == (enabled == true) then
        return
    end

    self.activeEventsEnabled = enabled == true
    for _, eventName in ipairs(PREY_BAR_ACTIVE_EVENTS) do
        if self.activeEventsEnabled then
            eventFrame:RegisterEvent(eventName)
        else
            eventFrame:UnregisterEvent(eventName)
        end
    end
end

function PreyBarRuntime:BuildContext()
    if not C_QuestLog or type(C_QuestLog.GetActivePreyQuest) ~= "function" then
        resetPreyBarQuestState(nil, true)
        return nil
    end

    local okQuest, activeQuestID = pcall(C_QuestLog.GetActivePreyQuest)
    activeQuestID = okQuest and tonumber(activeQuestID) or nil
    if not activeQuestID or activeQuestID < 1 then
        resetPreyBarQuestState(nil, true)
        return nil
    end

    local questMapID = resolveQuestMapID(activeQuestID)
    if not questMapID or not isPlayerInQuestZone(questMapID) then
        clearPreyWidgetInfoCache()
        return {
            activeQuestID = activeQuestID,
            active = true,
            inZone = false,
        }
    end

    local progressState = buildQuestProgressState(activeQuestID)
    if progressState and progressState.hidden == true then
        return {
            activeQuestID = activeQuestID,
            active = true,
            inZone = false,
        }
    end

    local noteText = nil
    if progressState.current and progressState.total then
        noteText = string.format(L.FEATURE_PREY_BAR_RUNTIME_SUMMARY or "%d/%d progress", progressState.current, progressState.total)
    elseif progressState.stage then
        noteText = string.format(L.FEATURE_PREY_BAR_RUNTIME_STAGE or "Stage %d/3", progressState.stage)
    elseif type(progressState.note) == "string" and progressState.note ~= "" then
        noteText = progressState.note
    else
        noteText = L.FEATURE_PREY_BAR_RUNTIME_SUMMARY_FALLBACK or "Active prey tracked live"
    end

    return {
        activeQuestID = activeQuestID,
        active = true,
        inZone = true,
        questMapID = questMapID,
        mapName = resolveMapName(questMapID),
        questTitle = resolveQuestTitle(activeQuestID),
        targetName = extractPreyTargetName(resolveQuestTitle(activeQuestID)),
        percent = ns:ClampNumber(progressState.percent or 0, 0, 100),
        noteText = noteText,
    }
end

function PreyBarRuntime:UpdateDisplay()
    local frame = ensurePreyBarFrame()
    self:ApplyFrameStyle()
    local context = self:BuildContext()
    ensurePreyHuntMixinSuppressionHook()
    self.currentContextInZone = context and context.inZone == true

    self:SetActiveEventsEnabled(context and context.active == true and context.inZone == true)

    if not self.enabled or not context or context.inZone ~= true then
        frame:Hide()
        applyDefaultPreyIconVisibility()
        return
    end

    local percent = ns:ClampNumber(context.percent or 0, 0, 100)
    local ratio = percent / 100
    local caption = context.targetName or context.questTitle or context.mapName or (L.FEATURE_PREY_BAR_RUNTIME_LABEL or "Prey Hunt")

    frame.caption:SetText(string.upper(caption))
    frame.percent:SetText(string.format("%d%%", math.floor(percent + 0.5)))
    frame.note:SetText("")
    frame.note:Hide()
    frame.bar:SetStatusBarColor(PREY_BAR_COLORS.fill[1], PREY_BAR_COLORS.fill[2], PREY_BAR_COLORS.fill[3], PREY_BAR_COLORS.fill[4])
    frame.bar:SetValue(ratio)

    local barWidth = frame.bar:GetWidth() or 1
    local sparkOffset = math.max(2, math.min(barWidth - 2, math.floor((barWidth * ratio) + 0.5)))
    frame.barSpark:ClearAllPoints()
    frame.barSpark:SetPoint("CENTER", frame.bar, "LEFT", sparkOffset, 0)
    frame.barSpark:SetShown(ratio > 0.01 and ratio < 1)
    frame.barOverlay:SetAlpha(ratio > 0 and 0.22 or 0)
    for _, marker in ipairs(frame.stepMarkers or {}) do
        local markerRatio = marker.ratio or 0
        local markerOffset = math.max(1, math.min(barWidth, math.floor((barWidth * markerRatio) + 0.5)))
        marker.line:ClearAllPoints()
        marker.line:SetPoint("CENTER", frame.bar, "LEFT", markerOffset, 0)
        marker.line:SetShown(true)
    end

    frame:Show()
    applyDefaultPreyIconVisibility()
end

function PreyBarRuntime:QueueRefresh(delaySeconds)
    if not self.enabled or self.refreshPending then
        return
    end

    self.refreshPending = true
    C_Timer.After(tonumber(delaySeconds) or 0.1, function()
        self.refreshPending = false
        if not PreyBarRuntime.enabled then
            return
        end
        PreyBarRuntime:UpdateDisplay()
    end)
end

function PreyBarRuntime:SetEnabled(enabled)
    enabled = enabled == true
    if not enabled then
        self.enabled = false
        self.refreshPending = false
        self.currentContextInZone = false
        resetPreyBarQuestState(nil, true)
        if self.eventFrame then
            self.eventFrame:UnregisterAllEvents()
        end
        self.activeEventsEnabled = false
        if self.frame then
            self.frame:Hide()
        end
        applyDefaultPreyIconVisibility()
        return
    end

    if self.enabled == true and self.eventFrame then
        self:UpdateDisplay()
        return
    end

    self.enabled = true
    self.refreshPending = false
    ensurePreyHuntMixinSuppressionHook()

    if not self.eventFrame then
        self.eventFrame = CreateFrame("Frame")
        self.eventFrame:SetScript("OnEvent", function(_, event, arg1)
            if event == "PLAYER_ENTERING_WORLD"
                or event == "ZONE_CHANGED"
                or event == "ZONE_CHANGED_INDOORS"
                or event == "ZONE_CHANGED_NEW_AREA"
            then
                clearPreyWidgetInfoCache()
            end
            if event == "PLAYER_DEAD" or event == "PLAYER_ALIVE" or event == "PLAYER_UNGHOST" then
                clearPreyProgressCache()
                PreyBarRuntime:QueueRefresh(0.05)
                if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
                    C_Timer.After(0.35, function()
                        if PreyBarRuntime.enabled then
                            PreyBarRuntime:UpdateDisplay()
                        end
                    end)
                end
            elseif event == "UPDATE_UI_WIDGET" then
                local widgetID = type(arg1) == "table" and tonumber(arg1.widgetID) or tonumber(arg1)
                if widgetID == PREY_WIDGET_ID or widgetID == nil then
                    PreyBarRuntime:QueueRefresh(0.05)
                end
            elseif event == "UPDATE_ALL_UI_WIDGETS" then
                PreyBarRuntime:QueueRefresh(0.15)
            else
                PreyBarRuntime:QueueRefresh(0.05)
            end
        end)
    end

    self.eventFrame:UnregisterAllEvents()
    self.activeEventsEnabled = false

    for _, eventName in ipairs(PREY_BAR_BASE_EVENTS) do
        self.eventFrame:RegisterEvent(eventName)
    end

    self:UpdateDisplay()
end

ns:RegisterFeatureDefinition("prey_bar", {
    id = "prey_bar",
    order = 10,
    category = "progress",
    title = function() return L.FEATURE_PREY_BAR_TITLE or "Prey Progress Bar" end,
    description = function() return L.FEATURE_PREY_BAR_DESCRIPTION or "Shows a prey progress bar in the hunt zone." end,
    note = function() return L.FEATURE_PREY_BAR_NOTE or "Only visible during an active prey hunt." end,
    previewLabel = function() return L.FEATURE_PREY_BAR_PREVIEW_LABEL or "Live Preview" end,
    previewNote = function() return L.FEATURE_PREY_BAR_PREVIEW_NOTE or "Slim world bar with percentage, zone context, and quest progress." end,
    optionToggle = {
        key = "widget_visibility",
        label = function() return L.FEATURE_PREY_BAR_WIDGET_LABEL or "Blizzard UI" end,
        values = { "hidden", "shown" },
        defaultValue = "hidden",
        valueLabels = {
            hidden = function() return L.FEATURE_PREY_BAR_WIDGET_HIDDEN or "Hide UI" end,
            shown = function() return L.FEATURE_PREY_BAR_WIDGET_SHOWN or "Show UI" end,
        },
    },
    secondaryOptionToggle = {
        key = "frame_style",
        label = function() return L.FEATURE_PREY_BAR_FRAME_LABEL or "Frame" end,
        values = { "shown", "hidden" },
        defaultValue = "shown",
        valueLabels = {
            shown = function() return L.FEATURE_PREY_BAR_FRAME_SHOWN or "Frame" end,
            hidden = function() return L.FEATURE_PREY_BAR_FRAME_HIDDEN or "Clean" end,
        },
    },
    requiresReload = false,
    apply = function(isEnabled)
        PreyBarRuntime:SetEnabled(isEnabled == true)
    end,
    onOptionChanged = function(_, optionKey)
        if optionKey == "frame_style" then
            PreyBarRuntime:ApplyFrameStyle()
        end
        applyDefaultPreyIconVisibility()
        if PreyBarRuntime.enabled then
            PreyBarRuntime:UpdateDisplay()
        end
    end,
})
