local _, ns = ...
local L = ns.L or {}
local addonName = ns.ADDON_NAME or "Vaultloom"

local DATA = ns.Data or {}
local TREASURE_NODES = DATA.MIDNIGHT_TREASURE_NODES or {}
local TREASURE_MAP_LABELS = DATA.MIDNIGHT_TREASURE_MAP_LABELS or {}
local TREASURE_QUEST_LOOKUP = DATA.MIDNIGHT_TREASURE_QUEST_LOOKUP or {}
local TREASURE_PROFESSION_QUESTS = DATA.MIDNIGHT_TREASURE_PROFESSION_QUESTS or {}

local FEATURE_ID = "midnight_treasure_map_pins"
local WORLD_MAP_PIN_TEMPLATE = "VaultloomMidnightTreasureWorldMapPinTemplate"
local PIN_SIZE = 16
local ICON_SIZE = 11
local TREASURE_ATLAS = "VignetteLoot"
local TREASURE_FALLBACK_TEXTURE = "Interface\\Icons\\INV_Misc_TreasureChest01"
local ASSET_ROOT = "Interface\\AddOns\\" .. tostring(addonName) .. "\\Assets\\"
local PIN_RING_TEXTURE = ASSET_ROOT .. "gathering_pin_ring_world.tga"
local MASK_TEXTURE = "Interface\\CHARACTERFRAME\\TempPortraitAlphaMask"

local GOLD = { 0.96, 0.76, 0.28, 1 }

local PROFESSION_SKILL_IDS = {
    Alchemy = 171,
    Blacksmithing = 164,
    Enchanting = 333,
    Engineering = 202,
    Herbalism = 182,
    Inscription = 773,
    Jewelcrafting = 755,
    Leatherworking = 165,
    Mining = 186,
    Skinning = 393,
    Tailoring = 197,
}

local PROFESSION_SPELL_IDS = {
    Alchemy = 2259,
    Blacksmithing = 2018,
    Enchanting = 7411,
    Engineering = 4036,
    Herbalism = 2366,
    Inscription = 45357,
    Jewelcrafting = 25229,
    Leatherworking = 2108,
    Mining = 2575,
    Skinning = 8613,
    Tailoring = 3908,
}

local professionIconCache = {}
local professionNameCache = {}

local Quest = ns.Quest or {}

local Runtime = {
    enabled = false,
    providerAdded = false,
    worldMapHooked = false,
    mapDirty = false,
    refreshToken = 0,
}

local basePinMixin = type(MapCanvasPinMixin) == "table" and MapCanvasPinMixin or {}
local WorldMapPinMixin = type(CreateFromMixins) == "function" and CreateFromMixins(basePinMixin) or {}
_G.VaultloomMidnightTreasureWorldMapPinMixin = WorldMapPinMixin

local WorldMapDataProvider = type(CreateFromMixins) == "function"
    and type(MapCanvasDataProviderMixin) == "table"
    and CreateFromMixins(MapCanvasDataProviderMixin)
    or nil

local function isQuestCompleted(questID)
    questID = tonumber(questID)
    if not questID then
        return false
    end

    if type(Quest.IsQuestCompleted) == "function" then
        local ok, completed = pcall(Quest.IsQuestCompleted, questID)
        if ok and completed == true then
            return true
        end
    end

    if C_QuestLog and type(C_QuestLog.IsQuestFlaggedCompleted) == "function" then
        local ok, completed = pcall(C_QuestLog.IsQuestFlaggedCompleted, questID)
        if ok and completed == true then
            return true
        end
    end

    if C_QuestLog and type(C_QuestLog.IsQuestFlaggedCompletedOnAccount) == "function" then
        local ok, completed = pcall(C_QuestLog.IsQuestFlaggedCompletedOnAccount, questID)
        if ok and completed == true then
            return true
        end
    end

    return false
end

local function getQuestTitle(questID)
    questID = tonumber(questID)
    if not questID or not C_QuestLog then
        return nil
    end

    if type(C_QuestLog.RequestLoadQuestByID) == "function" then
        pcall(C_QuestLog.RequestLoadQuestByID, questID)
    end
    if type(C_QuestLog.GetTitleForQuestID) ~= "function" then
        return nil
    end

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

    return nil
end

local function getTreasureName(treasure)
    if type(treasure) ~= "table" then
        return ""
    end
    return getQuestTitle(treasure.questID) or treasure.name or L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_FALLBACK_NAME or "Treasure"
end

local function getMapName(mapID)
    mapID = tonumber(mapID)
    if mapID and C_Map and type(C_Map.GetMapInfo) == "function" then
        local ok, info = pcall(C_Map.GetMapInfo, mapID)
        if ok and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then
            return info.name
        end
    end
    return TREASURE_MAP_LABELS[mapID] or ""
end

local function lowerText(value)
    value = tostring(value or "")
    if value == "" then
        return ""
    end
    return string.lower(value)
end

local function getSpellName(spellID)
    spellID = tonumber(spellID)
    if not spellID then
        return nil
    end

    if C_Spell and type(C_Spell.GetSpellInfo) == "function" then
        local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
        if ok and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then
            return info.name
        elseif ok and type(info) == "string" and info ~= "" then
            return info
        end
    end

    if type(GetSpellInfo) == "function" then
        local ok, name = pcall(GetSpellInfo, spellID)
        if ok and type(name) == "string" and name ~= "" then
            return name
        end
    end

    return nil
end

local function getProfessionLocalizedName(professionName)
    if type(professionName) ~= "string" or professionName == "" then
        return nil
    end
    if professionNameCache[professionName] ~= nil then
        return professionNameCache[professionName] or nil
    end

    local localizedName = getSpellName(PROFESSION_SPELL_IDS[professionName])
    professionNameCache[professionName] = localizedName or false
    return localizedName
end

local function professionTextMatches(value, professionName)
    local text = lowerText(value)
    if text == "" or type(professionName) ~= "string" or professionName == "" then
        return false
    end

    local englishName = lowerText(professionName)
    if englishName ~= "" and (text == englishName or string.find(text, englishName, 1, true)) then
        return true
    end

    local localizedName = lowerText(getProfessionLocalizedName(professionName))
    return localizedName ~= ""
        and (
            text == localizedName
            or string.find(text, localizedName, 1, true) ~= nil
            or string.find(localizedName, text, 1, true) ~= nil
        )
end

local function getPlayerProfessionMatches()
    local matches = {}
    if type(GetProfessions) ~= "function" or type(GetProfessionInfo) ~= "function" then
        return matches, false
    end

    for _, professionIndex in ipairs({ GetProfessions() }) do
        if professionIndex then
            local ok, name, _, _, _, _, skillLine, _, _, _, skillLineName = pcall(GetProfessionInfo, professionIndex)
            if ok then
                if type(name) == "string" and name ~= "" then
                    matches[lowerText(name)] = name
                end
                if type(skillLineName) == "string" and skillLineName ~= "" then
                    matches[lowerText(skillLineName)] = skillLineName
                end
                skillLine = tonumber(skillLine)
                if skillLine then
                    matches[skillLine] = name or true
                end
                for professionName, skillID in pairs(PROFESSION_SKILL_IDS) do
                    if (skillLine and skillLine == skillID)
                        or professionTextMatches(name, professionName)
                        or professionTextMatches(skillLineName, professionName)
                    then
                        matches[professionName] = name or skillLineName or true
                    end
                end
            end
        end
    end

    return matches, true
end

local function getProfessionDisplayName(professionName, matches)
    local skillID = PROFESSION_SKILL_IDS[professionName]
    if skillID and type(matches[skillID]) == "string" then
        return matches[skillID]
    end
    if type(matches[professionName]) == "string" then
        return matches[professionName]
    end
    local localizedName = matches[lowerText(professionName)]
    if type(localizedName) == "string" then
        return localizedName
    end
    localizedName = matches[lowerText(getProfessionLocalizedName(professionName))]
    if type(localizedName) == "string" then
        return localizedName
    end
    return professionName
end

local function playerHasProfession(professionName, matches, apiReady)
    if type(professionName) ~= "string" or professionName == "" then
        return true
    end
    if apiReady ~= true then
        return true
    end

    local skillID = PROFESSION_SKILL_IDS[professionName]
    if skillID and matches[skillID] then
        return true
    end
    if matches[professionName] then
        return true
    end
    local localizedName = getProfessionLocalizedName(professionName)
    if localizedName and matches[lowerText(localizedName)] then
        return true
    end
    return matches[lowerText(professionName)] ~= nil
end

local function getSpellIcon(spellID)
    spellID = tonumber(spellID)
    if not spellID then
        return nil
    end

    if C_Spell and type(C_Spell.GetSpellInfo) == "function" then
        local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
        if ok and type(info) == "table" and info.iconID then
            return info.iconID
        end
    end

    if type(GetSpellInfo) == "function" then
        local ok, _, _, icon = pcall(GetSpellInfo, spellID)
        if ok and icon then
            return icon
        end
    end

    return nil
end

local function getProfessionIcon(professionName)
    if type(professionName) ~= "string" or professionName == "" then
        return nil
    end

    local skillID = PROFESSION_SKILL_IDS[professionName]
    if type(GetProfessions) == "function" and type(GetProfessionInfo) == "function" then
        for _, professionIndex in ipairs({ GetProfessions() }) do
            if professionIndex then
                local ok, name, icon, _, _, _, skillLine, _, _, _, skillLineName = pcall(GetProfessionInfo, professionIndex)
                if ok and icon and (
                    tonumber(skillLine) == skillID
                    or professionTextMatches(name, professionName)
                    or professionTextMatches(skillLineName, professionName)
                ) then
                    return icon
                end
            end
        end
    end

    if professionIconCache[professionName] ~= nil then
        return professionIconCache[professionName] or nil
    end

    local icon = getSpellIcon(PROFESSION_SPELL_IDS[professionName])
    professionIconCache[professionName] = icon or false
    return icon
end

local function shouldShowTreasure(treasure, professionMatches, professionApiReady)
    if isQuestCompleted(treasure and treasure.questID) == true then
        return false
    end

    if treasure and treasure.kind == "profession" then
        return playerHasProfession(TREASURE_PROFESSION_QUESTS[tonumber(treasure.questID)], professionMatches or {}, professionApiReady)
    end

    return true
end

local function shouldCountTreasureForPlayer(treasure, professionMatches, professionApiReady)
    if treasure and treasure.kind == "profession" then
        return playerHasProfession(TREASURE_PROFESSION_QUESTS[tonumber(treasure.questID)], professionMatches or {}, professionApiReady)
    end
    return true
end

local function addProgressPart(parts, label, progress)
    if type(progress) ~= "table" or tonumber(progress.total) == nil or progress.total <= 0 then
        return
    end

    parts[#parts + 1] = string.format("%s: %d/%d", label, tonumber(progress.completed) or 0, tonumber(progress.total) or 0)
end

local function getTreasureProgress()
    local progress = {
        treasure = { completed = 0, total = 0 },
        profession = { completed = 0, total = 0 },
        delve = { completed = 0, total = 0 },
        ritual = { completed = 0, total = 0 },
    }

    local professionMatches, professionApiReady = getPlayerProfessionMatches()
    for _, treasure in ipairs(TREASURE_NODES) do
        if type(treasure) == "table" and shouldCountTreasureForPlayer(treasure, professionMatches, professionApiReady) then
            local kind = treasure.kind or "treasure"
            local bucket = progress[kind] or progress.treasure
            bucket.total = bucket.total + 1
            if isQuestCompleted(treasure.questID) then
                bucket.completed = bucket.completed + 1
            end
        end
    end

    return progress
end

local function buildProgressSummary()
    local progress = getTreasureProgress()
    local parts = {}
    addProgressPart(parts, L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_TREASURES or "Treasures", progress.treasure)
    addProgressPart(parts, L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_PROFESSION_KNOWLEDGE or "Profession knowledge", progress.profession)
    addProgressPart(parts, L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_DELVES or "Delves", progress.delve)
    addProgressPart(parts, L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_RITUALS or "Rituals", progress.ritual)

    if #parts == 0 then
        return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_EMPTY or ""
    end

    return table.concat(parts, L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PROGRESS_SEPARATOR or " | ")
end

local function getKindLabel(kind, questID)
    if kind == "profession" then
        local matches = getPlayerProfessionMatches()
        local professionName = TREASURE_PROFESSION_QUESTS[tonumber(questID)]
        local label = L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_KIND_PROFESSION or "Profession treasure"
        if type(professionName) == "string" and professionName ~= "" then
            return label .. ": " .. getProfessionDisplayName(professionName, matches)
        end
        return label
    elseif kind == "delve" then
        return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_KIND_DELVE or "Delve treasure"
    elseif kind == "ritual" then
        return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_KIND_RITUAL or "Ritual treasure"
    end
    return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_KIND_TREASURE or "Treasure"
end

local function buildPinData(treasure)
    if type(treasure) ~= "table" then
        return nil
    end

    local mapID = tonumber(treasure.mapID)
    local x = tonumber(treasure.x)
    local y = tonumber(treasure.y)
    if not mapID or not x or not y then
        return nil
    end

    return {
        key = treasure.key,
        name = treasure.name,
        questID = treasure.questID,
        kind = treasure.kind or "treasure",
        professionName = TREASURE_PROFESSION_QUESTS[tonumber(treasure.questID)],
        mapID = mapID,
        x = x,
        y = y,
        zoneName = getMapName(mapID),
    }
end

local function collectVisibleTreasuresForMap(mapID)
    mapID = tonumber(mapID)
    if not mapID then
        return {}
    end

    local pins = {}
    local professionMatches, professionApiReady = getPlayerProfessionMatches()
    for _, treasure in ipairs(TREASURE_NODES) do
        if tonumber(treasure and treasure.mapID) == mapID and shouldShowTreasure(treasure, professionMatches, professionApiReady) then
            local pin = buildPinData(treasure)
            if pin then
                pins[#pins + 1] = pin
            end
        end
    end

    return pins
end

local function applyIconMask(owner, texture)
    if not owner or not texture or owner.vlTreasureIconMask then
        return
    end
    if type(owner.CreateMaskTexture) ~= "function" or type(texture.AddMaskTexture) ~= "function" then
        return
    end

    local mask = owner:CreateMaskTexture(nil, "ARTWORK")
    mask:SetTexture(MASK_TEXTURE, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    mask:SetAllPoints(texture)
    texture:AddMaskTexture(mask)
    owner.vlTreasureIconMask = mask
end

local function applyTreasureTextures(pin)
    if not pin then
        return
    end

    if pin.Icon then
        pin.Icon:ClearAllPoints()
        pin.Icon:SetPoint("CENTER", 0, 0)
        pin.Icon:SetSize(ICON_SIZE, ICON_SIZE)
        pin.Icon:SetVertexColor(1, 1, 1, 1)
        pin.Icon:SetTexCoord(0, 1, 0, 1)

        local professionIcon = pin.treasure and pin.treasure.kind == "profession" and getProfessionIcon(pin.treasure.professionName) or nil
        local usedAtlas = false
        if professionIcon then
            pin.Icon:SetTexture(professionIcon)
            pin.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            usedAtlas = true
        elseif pin.Icon.SetAtlas then
            local ok = pcall(pin.Icon.SetAtlas, pin.Icon, TREASURE_ATLAS, false)
            usedAtlas = ok == true
        end
        if not usedAtlas then
            pin.Icon:SetTexture(TREASURE_FALLBACK_TEXTURE)
            pin.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        end

        applyIconMask(pin, pin.Icon)
        pin.Icon:Show()
    end

    if pin.Ring then
        pin.Ring:SetTexture(PIN_RING_TEXTURE)
        pin.Ring:SetVertexColor(GOLD[1], GOLD[2], GOLD[3], 0.98)
        pin.Ring:ClearAllPoints()
        pin.Ring:SetAllPoints(pin)
        pin.Ring:Show()
    end
end

local function setTreasureWaypoint(treasure)
    if type(treasure) ~= "table" then
        return false
    end

    local mapID = tonumber(treasure.mapID)
    local x = tonumber(treasure.x)
    local y = tonumber(treasure.y)
    if not mapID or not x or not y then
        return false
    end

    if not C_Map
        or type(C_Map.SetUserWaypoint) ~= "function"
        or not UiMapPoint
        or type(UiMapPoint.CreateFromCoordinates) ~= "function"
    then
        if ns.Print then
            ns:Print(L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_WAYPOINT_UNAVAILABLE or "Blizzard waypoint API is not available.")
        end
        return false
    end

    local okPoint, point = pcall(UiMapPoint.CreateFromCoordinates, mapID, x / 100, y / 100)
    if not okPoint or not point then
        return false
    end

    local okWaypoint = pcall(C_Map.SetUserWaypoint, point)
    if okWaypoint and C_SuperTrack and type(C_SuperTrack.SetSuperTrackedUserWaypoint) == "function" then
        pcall(C_SuperTrack.SetSuperTrackedUserWaypoint, true)
    end
    if okWaypoint and ns.Print then
        ns:Print(string.format(L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_WAYPOINT_SET_FORMAT or "Waypoint set: %s", getTreasureName(treasure)))
    end

    return okWaypoint == true
end

function WorldMapPinMixin:SetPassThroughButtons()
end

function WorldMapPinMixin:CheckMouseButtonPassthrough()
    return false
end

function WorldMapPinMixin:OnLoad()
    self:SetSize(PIN_SIZE, PIN_SIZE)
    if self.RegisterForClicks then
        self:RegisterForClicks("LeftButtonUp")
    end
    if self.UseFrameLevelType then
        self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI")
    end
    if self.SetScalingLimits then
        self:SetScalingLimits(1, 1.0, 1.25)
    end
end

function WorldMapPinMixin:OnAcquired(treasure)
    self.treasure = treasure
    self:SetSize(PIN_SIZE, PIN_SIZE)
    if self.SetAlpha then
        self:SetAlpha(0.96)
    end
    applyTreasureTextures(self)

    local x = (tonumber(treasure and treasure.x) or 0) / 100
    local y = (tonumber(treasure and treasure.y) or 0) / 100
    if self.SetPosition then
        self:SetPosition(x, y)
    end
end

function WorldMapPinMixin:OnReleased()
    self.treasure = nil
end

function WorldMapPinMixin:OnMouseEnter()
    local treasure = self.treasure
    if type(treasure) ~= "table" then
        return
    end

    GameTooltip:SetOwner(self, "ANCHOR_CURSOR_RIGHT")
    GameTooltip:AddLine(getTreasureName(treasure), 1, 0.82, 0.18, true)
    GameTooltip:AddLine(getKindLabel(treasure.kind, treasure.questID), 0.74, 0.80, 0.90, true)
    if treasure.zoneName and treasure.zoneName ~= "" then
        GameTooltip:AddLine(treasure.zoneName, 0.78, 0.78, 0.72, true)
    end
    GameTooltip:AddLine(
        string.format(L.PVE_RARES_COORDS_FORMAT or "Coordinates: %.1f, %.1f", tonumber(treasure.x) or 0, tonumber(treasure.y) or 0),
        0.88,
        0.88,
        0.86,
        true
    )
    GameTooltip:AddLine(L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_TOOLTIP_HINT or "Left-click to set a Blizzard waypoint.", 0.64, 0.74, 1, true)
    GameTooltip:AddLine(L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_HIDE_DONE_HINT or "Pins disappear after the treasure is collected.", 0.68, 0.82, 0.68, true)
    GameTooltip:Show()
end

function WorldMapPinMixin:OnMouseLeave()
    GameTooltip:Hide()
end

function WorldMapPinMixin:OnMouseDown()
end

function WorldMapPinMixin:OnMouseUp()
end

function WorldMapPinMixin:OnClick(button)
    if button == "LeftButton" then
        setTreasureWaypoint(self.treasure)
    end
end

if WorldMapDataProvider then
    function WorldMapDataProvider:RemoveAllData()
        local map = self.GetMap and self:GetMap() or nil
        if map and type(map.RemoveAllPinsByTemplate) == "function" then
            map:RemoveAllPinsByTemplate(WORLD_MAP_PIN_TEMPLATE)
        end
    end

    function WorldMapDataProvider:RefreshAllData()
        self:RemoveAllData()
        if Runtime.enabled ~= true then
            return
        end

        local map = self.GetMap and self:GetMap() or nil
        local mapID = map and map.GetMapID and map:GetMapID() or nil
        if not mapID or not map or type(map.AcquirePin) ~= "function" then
            return
        end

        for _, treasure in ipairs(collectVisibleTreasuresForMap(mapID)) do
            map:AcquirePin(WORLD_MAP_PIN_TEMPLATE, treasure)
        end
    end
end

local function safeRegisterEvent(frame, eventName)
    if frame and type(eventName) == "string" and eventName ~= "" then
        pcall(frame.RegisterEvent, frame, eventName)
    end
end

local function isWorldMapShown()
    local worldMap = _G.WorldMapFrame
    return worldMap and (type(worldMap.IsShown) ~= "function" or worldMap:IsShown())
end

local function refreshFeaturesPanel()
    if ns.UI
        and ns.UI.featuresPanelOpen == true
        and type(ns.UI.RefreshFeaturesPanel) == "function"
    then
        ns.UI:RefreshFeaturesPanel()
    end
end

function Runtime:RefreshNow()
    self.mapDirty = true
    if not isWorldMapShown() then
        return
    end
    if not self:EnsureWorldMapProvider() then
        return
    end
    if WorldMapDataProvider and WorldMapDataProvider.RefreshAllData then
        WorldMapDataProvider:RefreshAllData()
        self.mapDirty = false
    end
end

function Runtime:QueueRefresh(delay)
    if self.enabled ~= true then
        return
    end

    self.refreshToken = (tonumber(self.refreshToken) or 0) + 1
    local token = self.refreshToken
    local function refreshIfCurrent()
        if Runtime.enabled == true and token == Runtime.refreshToken then
            Runtime:RefreshNow()
        end
    end

    delay = tonumber(delay) or 0
    if delay > 0 and C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(delay, refreshIfCurrent)
    else
        refreshIfCurrent()
    end
end

function Runtime:RequestRefresh(delay)
    if self.enabled ~= true then
        return
    end

    if not isWorldMapShown() then
        self.mapDirty = true
        return
    end

    self:QueueRefresh(delay)
end

function Runtime:EnsureEventFrame()
    if self.eventFrame then
        return
    end

    self.eventFrame = CreateFrame("Frame")
    self.eventFrame:SetScript("OnEvent", function(_, event, ...)
        if Runtime.enabled ~= true then
            return
        end

        if event == "ADDON_LOADED" then
            local loadedName = ...
            if loadedName ~= addonName and loadedName ~= "Blizzard_WorldMap" then
                return
            end
            Runtime:HookWorldMap(_G.WorldMapFrame)
            Runtime:RequestRefresh(0.10)
            refreshFeaturesPanel()
            return
        end

        if event == "QUEST_TURNED_IN" then
            local questID = tonumber((...))
            if questID and TREASURE_QUEST_LOOKUP[questID] then
                Runtime:RequestRefresh(0.05)
                refreshFeaturesPanel()
            end
            return
        end

        local delay = event == "QUEST_LOG_UPDATE" and 0.30 or 0.10
        Runtime:RequestRefresh(delay)
        refreshFeaturesPanel()
    end)
end

function Runtime:RegisterEvents()
    self:EnsureEventFrame()
    if not self.eventFrame then
        return
    end

    self.eventFrame:UnregisterAllEvents()
    safeRegisterEvent(self.eventFrame, "ADDON_LOADED")
    safeRegisterEvent(self.eventFrame, "PLAYER_ENTERING_WORLD")
    safeRegisterEvent(self.eventFrame, "QUEST_LOG_UPDATE")
    safeRegisterEvent(self.eventFrame, "QUEST_TURNED_IN")
    safeRegisterEvent(self.eventFrame, "ZONE_CHANGED_NEW_AREA")
    safeRegisterEvent(self.eventFrame, "AREA_POIS_UPDATED")
    safeRegisterEvent(self.eventFrame, "SKILL_LINES_CHANGED")
    safeRegisterEvent(self.eventFrame, "TRADE_SKILL_LIST_UPDATE")
end

function Runtime:EnsureWorldMapProvider()
    if not WorldMapDataProvider or not _G.WorldMapFrame then
        return false
    end

    local worldMap = _G.WorldMapFrame
    if self.providerAdded ~= true then
        if not (type(worldMap.dataProviders) == "table" and worldMap.dataProviders[WorldMapDataProvider]) then
            worldMap:AddDataProvider(WorldMapDataProvider)
        end
        self.providerAdded = true
    end
    self:HookWorldMap(worldMap)

    return true
end

function Runtime:HookWorldMap(worldMap)
    if self.worldMapHooked == true or not worldMap then
        return
    end

    self.worldMapHooked = true
    if worldMap.HookScript then
        worldMap:HookScript("OnShow", function()
            if Runtime.enabled == true then
                Runtime:EnsureWorldMapProvider()
                Runtime:QueueRefresh(0.05)
            end
        end)
    end
    if type(hooksecurefunc) == "function" then
        pcall(hooksecurefunc, worldMap, "OnMapChanged", function()
            if Runtime.enabled == true then
                Runtime:QueueRefresh(0.05)
            end
        end)
    end
end

function Runtime:RemoveWorldMapProvider()
    if WorldMapDataProvider and WorldMapDataProvider.RemoveAllData then
        WorldMapDataProvider:RemoveAllData()
    end

    local worldMap = _G.WorldMapFrame
    if worldMap
        and type(worldMap.dataProviders) == "table"
        and worldMap.dataProviders[WorldMapDataProvider]
        and type(worldMap.RemoveDataProvider) == "function"
    then
        worldMap:RemoveDataProvider(WorldMapDataProvider)
    end
    self.providerAdded = false
end

function Runtime:SetEnabled(enabled)
    enabled = enabled == true
    self.enabled = enabled
    self.refreshToken = (tonumber(self.refreshToken) or 0) + 1

    if not enabled then
        if self.eventFrame then
            self.eventFrame:UnregisterAllEvents()
        end
        self:RemoveWorldMapProvider()
        return
    end

    self:RegisterEvents()
    self:HookWorldMap(_G.WorldMapFrame)
    self:RequestRefresh(0.05)
end

function ns:RefreshMidnightTreasureMapPins()
    Runtime:RequestRefresh(0.05)
end

function ns:GetMidnightTreasureProgressSummary()
    return buildProgressSummary()
end

ns:RegisterFeatureDefinition(FEATURE_ID, {
    id = FEATURE_ID,
    order = 36,
    category = "world_map",
    title = function() return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_TITLE or "Midnight Treasures" end,
    description = function() return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_DESCRIPTION or "Shows uncollected one-time Midnight treasures on the Blizzard world map." end,
    summary = function() return buildProgressSummary() end,
    note = function() return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_NOTE or "Uses small round chest pins and hides them after the treasure credit is earned." end,
    previewLabel = function() return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PREVIEW_LABEL or "Treasure Map" end,
    previewNote = function() return L.FEATURE_MIDNIGHT_TREASURE_MAP_PINS_PREVIEW_NOTE or "One-time treasures, ritual locations, profession treasures, and Delve sturdy chests." end,
    requiresReload = false,
    apply = function(isEnabled)
        Runtime:SetEnabled(isEnabled == true)
    end,
})
