local ADDON_NAME, NS = ...
local L = LibStub("AceLocale-3.0"):GetLocale("MDungeonTeleports")

local MDTWorldMapTab = {}
MDTWorldMapTab.tabSpacing = 4
local DISPLAY_MODE = "MDT_PEEPO" -- lib assigns its own displayMode
local LWMT = LibStub("LibWorldMapTabs", true)

-- Housing data (populated by PLAYER_HOUSE_LIST_UPDATED)
local mdtHouseData = {}
local mdtHouseDataDirty = false
local mdtFetchingHouseData = false

-- Try to fetch housing data from the API directly (handles both sync return and request trigger)
local function TryFetchHousingData()
    if #mdtHouseData > 0 then return true end
    if mdtFetchingHouseData then return false end
    if C_Housing and C_Housing.GetPlayerOwnedHouses then
        mdtFetchingHouseData = true
        local data = C_Housing.GetPlayerOwnedHouses()
        mdtFetchingHouseData = false
        if data and type(data) == "table" and #data > 0 then
            mdtHouseData = data
            mdtHouseDataDirty = true
            return true
        end
    end
    return false
end

-- MARK: Check if the tab is enabled
function MDTWorldMapTab:IsEnabled()
    return not MDungeonTeleports or MDungeonTeleports.worldMapTab ~= false
end

-- MARK: Initialize the tab button
function MDTWorldMapTab:Initialize()
    if self.initialized then return end

    if not self:IsEnabled() then
        return
    end
    
    if not WorldMapFrame or not WorldMapFrame.BorderFrame then
        C_Timer.After(0.5, function() self:Initialize() end)
        return
    end

    if not QuestMapFrame then
        C_Timer.After(0.5, function() self:Initialize() end)
        return
    end

    -- Create tab button parented to QuestMapFrame (lib will re-parent it)
    self.button = CreateFrame("Button", "MDTWorldMapTabButton", QuestMapFrame, "LargeSideTabButtonTemplate")
    self.button:SetFrameStrata("HIGH")
    self.button:SetFrameLevel(WorldMapFrame.BorderFrame:GetFrameLevel() + 5)

    -- Set up button properties
    self.button.tooltipText = L and L["TELEPORTS_TITLE"] or "M+ Dungeon Teleports"
    self.button.activeAtlas = "Interface\\AddOns\\MDungeonTeleports\\media\\ui\\map_icon_active.blp"
    self.button.inactiveAtlas = "Interface\\AddOns\\MDungeonTeleports\\media\\ui\\map_icon_inactive.blp"
    self:SetupButtonVisuals()

    if not self.button.mdtIconHooked then
        self.button:HookScript("OnShow", function()
            MDTWorldMapTab:SetupButtonVisuals()
        end)
        self.button.mdtIconHooked = true
    end
    self:ApplyElvUISkin()
    self:ApplyNDuiSkin()
    self.button:SetChecked(false)

    -- Register with LibWorldMapTabs:
    -- Lib hooks OnMouseUp via HookScript (not SetCustomOnMouseUpHandler) so no taint.
    -- Lib handles: tab placement, displayMode assignment, content frame show/hide,
    -- deselecting when official tabs are clicked, EventRegistry for SetDisplayMode.
    if LWMT then
        LWMT:AddCustomTab(self.button)
    end

    self.button:Show()

    -- Create the content panel
    self:CreatePanel()

    -- Link content panel to lib so it manages show/hide automatically
    if LWMT and self.panel then
        LWMT:LinkTabToContentFrame(self.button, self.panel)
    end

    self.initialized = true
end

-- MARK: Apply ElvUI skin to the tab button
function MDTWorldMapTab:ApplyElvUISkin()
    if not self.button then
        return
    end
    if not C_AddOns.IsAddOnLoaded("ElvUI") then
        return
    end
    if not ElvUI then
        return
    end

    local ok, E = pcall(unpack, ElvUI)
    if not ok or not E or not E.GetModule then
        return
    end

    local blizzardSkins = E.private and E.private.skins and E.private.skins.blizzard
    if blizzardSkins and blizzardSkins.worldmap == false then
        return
    end

    local S = E:GetModule("Skins", true)
    if not S then
        return
    end

    if S.HandleTab then
        S:HandleTab(self.button)
    elseif S.HandleButton then
        S:HandleButton(self.button)
    end

    self:SetupButtonVisuals()

    if not self.button.mdtCheckedHighlight then
        local checkedHighlight = self.button:CreateTexture(nil, "OVERLAY")
        checkedHighlight:ClearAllPoints()
        checkedHighlight:SetDrawLayer("BACKGROUND", 0)
        checkedHighlight:SetPoint("TOPLEFT", self.button, "TOPLEFT", 4, -2)
        checkedHighlight:SetPoint("BOTTOMRIGHT", self.button, "BOTTOMRIGHT", -4, 4)
        checkedHighlight:SetColorTexture(1, 0.82, 0, 0.25)
        checkedHighlight:Hide()
        self.button.mdtCheckedHighlight = checkedHighlight

        local function UpdateCheckedHighlight(checked)
            local isChecked = checked
            if isChecked == nil and self.button.GetChecked then
                isChecked = self.button:GetChecked()
            end
            if isChecked == nil and self.button.SelectedTexture and self.button.SelectedTexture.IsShown then
                isChecked = self.button.SelectedTexture:IsShown()
            end
            if isChecked then
                self.button.mdtCheckedHighlight:Show()
            else
                self.button.mdtCheckedHighlight:Hide()
            end
        end

        hooksecurefunc(self.button, "SetChecked", function(_, checked)
            UpdateCheckedHighlight(checked)
        end)
        self.button:HookScript("OnShow", function()
            UpdateCheckedHighlight(nil)
        end)
        UpdateCheckedHighlight(nil)
    end

    if not self.button.mdtHoverHighlight then
        local highlight = self.button:GetHighlightTexture()
        if not highlight then
            highlight = self.button:CreateTexture(nil, "HIGHLIGHT")
        end
        highlight:ClearAllPoints()
        highlight:SetPoint("TOPLEFT", self.button, "TOPLEFT", 4, -2)
        highlight:SetPoint("BOTTOMRIGHT", self.button, "BOTTOMRIGHT", -4, 4)
        highlight:SetColorTexture(1, 1, 1, 0.15)
        highlight:SetBlendMode("ADD")
        self.button.mdtHoverHighlight = highlight
    end

    self.tabSpacing = 1
    if self.button then
        self:AnchorButton()
    end

    self.button:SetSize(38, 46)
end

-- MARK: Apply NDui skin to the tab button
function MDTWorldMapTab:ApplyNDuiSkin()
    if not self.button then
        return
    end
    if not C_AddOns.IsAddOnLoaded("NDui") then
        return
    end

    local NDui = _G["NDui"]
    if not NDui then
        return
    end

    local ok, B, C, L, DB = pcall(unpack, NDui)
    if not ok or not B then
        return
    end

    -- Respect NDui's BlizzardSkins setting
    if C and C.db and C.db["Skins"] and C.db["Skins"]["BlizzardSkins"] == false then
        return
    end

    -- Use NDui's side-tab skinning pattern (matches Blizzard_CooldownViewer style)
    if B.StripTextures then
        B.StripTextures(self.button, 2)
    end

    if B.SetBD then
        self.button.bg = B.SetBD(self.button)
        if self.button.bg and self.button.bg.SetInside then
            self.button.bg:SetInside(nil, 2, 2)
        end
    end

    -- Highlight texture inside the bg
    if self.button.bg then
        local hl = self.button:CreateTexture(nil, "HIGHLIGHT")
        hl:SetColorTexture(1, 1, 1, .25)
        if hl.SetInside then
            hl:SetInside(self.button.bg)
        else
            hl:SetAllPoints(self.button.bg)
        end
        self.button.mdtHoverHighlight = hl
    end

    -- SelectedTexture for checked state (class-colored)
    if self.button.SelectedTexture then
        local cr, cg, cb = 1, 0.82, 0
        if DB and DB.r then
            cr, cg, cb = DB.r, DB.g, DB.b
        end
        self.button.SelectedTexture:SetDrawLayer("BACKGROUND")
        self.button.SelectedTexture:SetColorTexture(cr, cg, cb, .25)
        if self.button.bg and self.button.SelectedTexture.SetInside then
            self.button.SelectedTexture:SetInside(self.button.bg)
        end
    end

    -- Checked highlight fallback if no SelectedTexture
    if not self.button.SelectedTexture and not self.button.mdtCheckedHighlight then
        local cr, cg, cb = 1, 0.82, 0
        if DB and DB.r then
            cr, cg, cb = DB.r, DB.g, DB.b
        end
        local checkedHighlight = self.button:CreateTexture(nil, "OVERLAY")
        checkedHighlight:SetDrawLayer("BACKGROUND", 0)
        if self.button.bg then
            if checkedHighlight.SetInside then
                checkedHighlight:SetInside(self.button.bg)
            else
                checkedHighlight:SetAllPoints(self.button.bg)
            end
        else
            checkedHighlight:SetPoint("TOPLEFT", self.button, "TOPLEFT", 4, -2)
            checkedHighlight:SetPoint("BOTTOMRIGHT", self.button, "BOTTOMRIGHT", -4, 4)
        end
        checkedHighlight:SetColorTexture(cr, cg, cb, .25)
        checkedHighlight:Hide()
        self.button.mdtCheckedHighlight = checkedHighlight

        local function UpdateCheckedHighlight(checked)
            local isChecked = checked
            if isChecked == nil and self.button.GetChecked then
                isChecked = self.button:GetChecked()
            end
            if isChecked then
                self.button.mdtCheckedHighlight:Show()
            else
                self.button.mdtCheckedHighlight:Hide()
            end
        end

        hooksecurefunc(self.button, "SetChecked", function(_, checked)
            UpdateCheckedHighlight(checked)
        end)
        self.button:HookScript("OnShow", function()
            UpdateCheckedHighlight(nil)
        end)
        UpdateCheckedHighlight(nil)
    end

    self:SetupButtonVisuals()

    self.tabSpacing = 1
    if self.button then
        self:AnchorButton()
    end

    -- Match sibling tab size instead of forcing a fixed size
    local refTab = QuestMapFrame and (QuestMapFrame.MapLegendTab or QuestMapFrame.EventsTab or QuestMapFrame.QuestsTab)
    if refTab then
        local w, h = refTab:GetSize()
        if w > 0 and h > 0 then
            self.button:SetSize(w, h)
        end
    end

    self.button.mdtNDuiSkinned = true
end

-- MARK: Set up button visuals to match LargeSideTabButtonTemplate
function MDTWorldMapTab:SetupButtonVisuals()
    if self.button and self.button.Icon then
        if not self.button.mdtApplyIconLayout then
            self.button.mdtApplyIconLayout = function()
                if not MDTWorldMapTab.button or not MDTWorldMapTab.button.Icon then
                    return
                end
                local icon = MDTWorldMapTab.button.Icon
                local isSkinned = C_AddOns.IsAddOnLoaded("ElvUI") or C_AddOns.IsAddOnLoaded("NDui")
                local offsetX = isSkinned and 0 or -2
                icon:ClearAllPoints()
                icon:SetPoint("CENTER", MDTWorldMapTab.button, "CENTER", offsetX, 0)
                icon:SetSize(20, 20)
            end
        end

        self.button.Icon:SetTexture(self.button.inactiveAtlas)
        self.button.mdtApplyIconLayout()

        if not self.button.mdtIconPositionHooked then
            self.button:HookScript("OnMouseDown", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button:HookScript("OnMouseUp", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button:HookScript("OnClick", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button.mdtIconPositionHooked = true
        end
    end
end

-- MARK: Select this tab
-- LibWorldMapTabs handles this via its HookScript("OnMouseUp"); SelectTab is
-- a direct fallback used only from TogglePanel when the lib is unavailable.
function MDTWorldMapTab:SelectTab()
    if not self:IsEnabled() then return end
    if LWMT and self.button and self.button.displayMode then
        LWMT:SetDisplayMode(self.button.displayMode)
    end
end

-- MARK: parent
function MDTWorldMapTab:GetPanelParent()
    if QuestMapFrame then
        return QuestMapFrame
    end
    if WorldMapFrame and WorldMapFrame.QuestLog then
        return WorldMapFrame.QuestLog
    end
    return WorldMapFrame
end

-- MARK: content anchor
function MDTWorldMapTab:GetContentAnchor()
    if QuestMapFrame then
        if QuestMapFrame.ContentsAnchor then
            return QuestMapFrame.ContentsAnchor
        end
        if QuestMapFrame.MapLegendFrame then
            return QuestMapFrame.MapLegendFrame
        end
        if QuestMapFrame.EventsFrame then
            return QuestMapFrame.EventsFrame
        end
        if QuestMapFrame.QuestsFrame then
            return QuestMapFrame.QuestsFrame
        end
    end
    return self:GetPanelParent()
end

-- MARK: Find other side tabs to hook into
function MDTWorldMapTab:FindSideTabs()
    local tabs = {}
    local function AddIfTab(frame)
        if frame and frame.Background and frame.Icon and frame.SetChecked then
            if frame.Background.GetAtlas and frame.Background:GetAtlas() == "questlog-tab-side" then
                table.insert(tabs, frame)
            end
        end
    end

    if QuestMapFrame then
        AddIfTab(QuestMapFrame.QuestsTab)
        AddIfTab(QuestMapFrame.EventsTab)
        AddIfTab(QuestMapFrame.MapLegendTab)
    end

    local function Scan(frame)
        if not frame or not frame.GetChildren then
            return
        end
        for _, child in ipairs({ frame:GetChildren() }) do
            if child ~= self.button then
                AddIfTab(child)
            end
            Scan(child)
        end
    end

    Scan(WorldMapFrame)
    return tabs
end

-- MARK: Anchor the tab button
-- LibWorldMapTabs handles tab placement via PlaceTabs(); AnchorButton is a no-op when lib is active.
function MDTWorldMapTab:AnchorButton()
    if LWMT then return end -- lib places the tab
    if InCombatLockdown() then return end
    local anchorTab = (QuestMapFrame and QuestMapFrame.MapLegendTab) or (QuestMapFrame and QuestMapFrame.EventsTab) or (QuestMapFrame and QuestMapFrame.QuestsTab)
    self.button:ClearAllPoints()
    if anchorTab then
        self.button:SetPoint("TOP", anchorTab, "BOTTOM", 0, -(self.tabSpacing or 4))
    else
        self.button:SetPoint("TOPRIGHT", WorldMapFrame.BorderFrame, "TOPRIGHT", -8, -100)
    end
end

-- MARK: Hook other side tabs to hide panel on click
-- No-op: LibWorldMapTabs handles deselection of our tab via EventRegistry QuestLog.SetDisplayMode.
function MDTWorldMapTab:HookOtherTabs() end

-- MARK: Hook display mode changes
-- No-op: LibWorldMapTabs handles this via EventRegistry.
function MDTWorldMapTab:HookDisplayMode() end

-- MARK: Hook other content frames to hide panel on show
-- No-op: LibWorldMapTabs handles content frame visibility.
function MDTWorldMapTab:HookOtherContentFrames() end

-- MARK: Inject panel and button into QuestMapFrame content frames
function MDTWorldMapTab:InjectIntoQuestMapFrame()
    if not QuestMapFrame or not self.panel or not self.button then
        return
    end

    if QuestMapFrame.ContentFrames then
        local exists = false
        for _, frame in ipairs(QuestMapFrame.ContentFrames) do
            if frame == self.panel then
                exists = true
                break
            end
        end
        if not exists then
            table.insert(QuestMapFrame.ContentFrames, self.panel)
        end
    end

    if QuestMapFrame.TabButtons then
        local exists = false
        for _, tab in ipairs(QuestMapFrame.TabButtons) do
            if tab == self.button then
                exists = true
                break
            end
        end
        if not exists then
            table.insert(QuestMapFrame.TabButtons, self.button)
        end
    end

    if QuestMapFrame.ValidateTabs then
        QuestMapFrame:ValidateTabs()
    end
end

-- MARK: Create the main content panel
function MDTWorldMapTab:CreatePanel()
    -- Parent to ContentsAnchor so SetAllPoints anchors relative to it.
    -- LinkTabToContentFrame will re-parent to QuestMapFrame; anchors survive re-parenting.
    local contentAnchor = QuestMapFrame.ContentsAnchor or QuestMapFrame
    self.panel = CreateFrame("Frame", "MDTWorldMapPanel", contentAnchor)
    self.panel:SetPoint("TOPLEFT", contentAnchor, "TOPLEFT", 0, -29)
    self.panel:SetPoint("BOTTOMRIGHT", contentAnchor, "BOTTOMRIGHT", -22, 0)
    self.panel:SetFrameStrata(contentAnchor:GetFrameStrata())
    self.panel:SetFrameLevel(contentAnchor:GetFrameLevel() + 10)
    self.panel:EnableMouse(true)
    self.panel:Hide()

    -- Create scroll frame (MapLegend-style)
    local scrollFrame = CreateFrame("ScrollFrame", "MDTWorldMapPanelScrollFrame", self.panel, "ScrollFrameTemplate")
    scrollFrame:ClearAllPoints()
    scrollFrame:SetPoint("TOPLEFT")
    scrollFrame:SetPoint("BOTTOMRIGHT")
    self.panel.ScrollFrame = scrollFrame

    -- Background inset to reveal border artwork
    local bg = scrollFrame:CreateTexture(nil, "BACKGROUND")
    bg:SetAtlas("QuestLog-main-background", true)
    bg:ClearAllPoints()
    bg:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 3, -1)
    bg:SetPoint("BOTTOMRIGHT", scrollFrame, "BOTTOMRIGHT", -3, 0)
    scrollFrame.Background = bg

    -- Scrollbar alignment similar to MapLegend
    if scrollFrame.ScrollBar then
        scrollFrame.ScrollBar:ClearAllPoints()
        scrollFrame.ScrollBar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", 8, 2)
        scrollFrame.ScrollBar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", 8, -4)
    end

    -- Scroll child
    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(1, 1)
    scrollFrame:SetScrollChild(scrollChild)
    self.panel.ScrollFrame.ScrollChild = scrollChild

    -- Border Frame around the scroll area
    local borderFrame = CreateFrame("Frame", nil, self.panel, "QuestLogBorderFrameTemplate")
    borderFrame:ClearAllPoints()
    borderFrame:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", -3, 7)
    borderFrame:SetPoint("BOTTOMRIGHT", scrollFrame, "BOTTOMRIGHT", 3, -6)
    self.panel.BorderFrame = borderFrame

    -- Title
    local titleText = self.panel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    titleText:SetPoint("BOTTOM", borderFrame, "TOP", -1, 3)
    local fontPath, fontSize, fontFlags = GameFontNormal:GetFont()
    if fontPath and fontSize then
        titleText:SetFont(fontPath, fontSize + 3, fontFlags)
    end
    titleText:SetTextColor(1, 1, 1)
    titleText:SetText(L["TELEPORTS_TITLE"])
    self.panel.TitleText = titleText

    -- Settings button
    local settingsBtn = CreateFrame("Button", nil, scrollChild)
    settingsBtn:SetSize(16, 16)
    settingsBtn:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 7, -8)
    settingsBtn:SetFrameLevel(self.panel:GetFrameLevel() + 20)
    settingsBtn:SetNormalAtlas("QuestLog-icon-setting")
    settingsBtn:SetHighlightAtlas("QuestLog-icon-setting")
    settingsBtn:SetScript("OnClick", function()
        if InCombatLockdown() then return end
        if MDTSettingsFrame and MDTSettingsFrame:IsShown() then
            MDTSettingsFrame:Hide()
            PlaySound(SOUNDKIT.IG_CHARACTER_INFO_CLOSE)
        elseif NS.OpenSettingsToWorldMapTab then
            PlaySound(SOUNDKIT.IG_CHARACTER_INFO_OPEN)
            NS.OpenSettingsToWorldMapTab()
        end
    end)
    self.panel.SettingsBtn = settingsBtn

    self:BuildScrollContent()
end

local function GetSpellIcon(spellID)
    if not spellID then return "Interface\\Icons\\INV_Misc_QuestionMark" end
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    if spellInfo and spellInfo.iconID then
        return spellInfo.iconID
    end
    return "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function GetSpellName(spellID)
    if not spellID then return nil end
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    return spellInfo and spellInfo.name or nil
end

local function GetItemIcon(itemID)
    if not itemID then return "Interface\\Icons\\INV_Misc_QuestionMark" end
    local _, _, _, _, icon = C_Item.GetItemInfoInstant(itemID)
    return icon or "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function GetItemName(itemID)
    if not itemID then return nil end
    if C_Item and C_Item.GetItemNameByID then
        local name = C_Item.GetItemNameByID(itemID)
        if name and name ~= "" then
            return name
        end
    end
    local name = GetItemInfo(itemID)
    return name
end

local function IsSpellKnown(spellID)
    if not spellID then return false end
    return C_SpellBook.IsSpellInSpellBook(spellID)
end

local function IsSpellKnownOrUsable(spellID)
    if not spellID then return false end
    if C_Spell and C_Spell.IsSpellKnown then
        return C_Spell.IsSpellKnown(spellID)
    end
    return IsSpellKnown(spellID)
end

local function IsSpellKnownForDisplay(spellID)
    if not spellID then return false end
    if spellID == 1233637 and C_Spell and C_Spell.IsSpellUsable then
        local usable = C_Spell.IsSpellUsable(spellID)
        if usable then
            return true
        end
    end
    return IsSpellKnownOrUsable(spellID)
end

local function IsItemKnown(itemID)
    if not itemID then return false end
    if C_ToyBox.GetToyInfo(itemID) then
        return PlayerHasToy(itemID)
    end
    return (C_Item.GetItemCount(itemID) or 0) > 0
end

local function IsToyKnown(itemID)
    if not itemID then return false end
    return PlayerHasToy(itemID)
end

local function GetKnownHearthstoneIDs(list)
    local knownIDs = {}
    for _, data in ipairs(list or {}) do
        local itemID = tonumber(data.itemID)
        if itemID then
            local isToy = C_ToyBox and C_ToyBox.GetToyInfo and C_ToyBox.GetToyInfo(itemID)
            local isKnown = isToy and PlayerHasToy(itemID) or (C_Item.GetItemCount(itemID) or 0) > 0
            if isKnown then
                table.insert(knownIDs, itemID)
            end
        end
    end
    return knownIDs
end

local function PickRandomHearthstone(list)
    local knownIDs = GetKnownHearthstoneIDs(list)
    if #knownIDs == 0 then return nil end
    return knownIDs[math.random(#knownIDs)]
end

local function NormalizeHearthstones(list, selectedItemID, preferIcon)
    local entries = {}
    local fallback = nil
    for _, data in ipairs(list or {}) do
        local itemID = tonumber(data.itemID)
        if itemID then
            local isToy = C_ToyBox and C_ToyBox.GetToyInfo and C_ToyBox.GetToyInfo(itemID)
            local itemName = GetItemName(itemID) or data.name
            local itemIcon = GetItemIcon(itemID)
            local entry = {
                name = itemName,
                itemID = itemID,
                actionType = isToy and "toy" or "item",
                icon = itemIcon,
                preferIcon = false
            }
            if not fallback then
                fallback = entry
            end
            if selectedItemID and itemID == selectedItemID then
                return { entry }
            end
            table.insert(entries, entry)
        end
    end
    if selectedItemID then
        return fallback and { fallback } or {}
    end
    return entries
end

local function NormalizeToyList(list, preferIcon)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local itemID = tonumber(data.toyID) or tonumber(data.name)
        local name = data.name
        if tonumber(data.name) and data.toyID then
            name = data.toyID
        end
        if itemID then
            local itemName = GetItemName(itemID) or name
            local itemIcon = GetItemIcon(itemID)
            table.insert(entries, {
                name = itemName,
                itemID = itemID,
                actionType = "toy",
                icon = itemIcon,
                preferIcon = false
            })
        end
    end
    return entries
end

local function NormalizeSpellList(list, preferIcon)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            local spellName = GetSpellName(spellID) or data.name
            local spellIcon = GetSpellIcon(spellID)
            table.insert(entries, {
                name = spellName,
                spellID = spellID,
                actionType = "spell",
                icon = spellIcon,
                preferIcon = false
            })
        end
    end
    return entries
end

local function NormalizePortalList(list)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            local spellName = GetSpellName(spellID) or data.fullname or data.name
            local spellIcon = GetSpellIcon(spellID)
            table.insert(entries, {
                name = spellName,
                spellID = spellID,
                actionType = "spell",
                icon = spellIcon
            })
        end
    end
    return entries
end

local function NormalizePortalListTableData(list)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            table.insert(entries, {
                name = data.fullname or data.name,
                spellID = spellID,
                actionType = "spell",
                icon = data.icon,
                preferIcon = true,
                useTableData = true
            })
        end
    end
    return entries
end

-- MARK: Time Formatting
local function FormatCooldownTime(seconds)
    if seconds >= 3600 then
        local hours = math.floor(seconds / 3600)
        return string.format("%dh", hours)
    elseif seconds >= 60 then
        local minutes = math.floor(seconds / 60)
        return string.format("%dm", minutes)
    else
        return string.format("%ds", math.floor(seconds))
    end
end

-- MARK: Cooldown Text Function
local function UpdateCooldownText(button, entry)
    if InCombatLockdown() then return end

    local cooldownFrame = button.cooldownText
    local cooldownOverlay = button.cooldownOverlay
    local startTime
    local duration
    local isEnabled = true

    if entry and entry.actionType == "housing" then
        local canReturn = C_HousingNeighborhood and C_HousingNeighborhood.CanReturnAfterVisitingHouse and C_HousingNeighborhood.CanReturnAfterVisitingHouse()
        if not canReturn and C_Housing and C_Housing.GetVisitCooldownInfo then
            local cdInfo = C_Housing.GetVisitCooldownInfo()
            if cdInfo then
                startTime = cdInfo.startTime
                duration = cdInfo.duration
                isEnabled = cdInfo.isEnabled
            end
        end
    elseif entry and entry.actionType == "spell" and entry.spellID then
        if entry.spellID ~= 193753 then 
            local spellCooldownInfo = C_Spell.GetSpellCooldown(entry.spellID)
            if spellCooldownInfo then
                startTime = spellCooldownInfo.startTime
                duration = spellCooldownInfo.duration
                isEnabled = spellCooldownInfo.isEnabled
            end
        end
    elseif entry and (entry.actionType == "toy" or entry.actionType == "item") and entry.itemID then
        startTime, duration, isEnabled = C_Item.GetItemCooldown(entry.itemID)
    end

    if startTime and duration and not issecretvalue(startTime) and not issecretvalue(duration) then
        if startTime > 0 and duration > 0 and isEnabled then
            local remaining = max(0, (startTime + duration) - GetTime())

            if remaining > 5 then
                cooldownFrame:SetText(FormatCooldownTime(remaining))
                cooldownOverlay:Show()
                return
            end
        end
    end

    cooldownFrame:SetText("")
    cooldownOverlay:Hide()
end

-- MARK: All Content
function MDTWorldMapTab:BuildScrollContent()
    local scrollFrame = self.panel.ScrollFrame
    local scrollChild = scrollFrame.ScrollChild

    scrollChild.entryPool = scrollChild.entryPool or {}
    scrollChild.headerPool = scrollChild.headerPool or {}

    -- MARK: Header Text
    local function GetHeader(index)
        if not scrollChild.headerPool[index] then
            local header = CreateFrame("Frame", nil, scrollChild)
            header:SetSize(1, 18)
            local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            text:SetPoint("LEFT", header, "LEFT", 0, 0)
            text:SetFont(GameFontNormal:GetFont(), 12, "OUTLINE")
            text:SetTextColor(1, 1, 1)
            header.text = text
            scrollChild.headerPool[index] = header
        end
        return scrollChild.headerPool[index]
    end

    -- MARK: Button Setup
    local function GetEntry(index)
        if not scrollChild.entryPool[index] then
            local entry = CreateFrame("Frame", nil, scrollChild)
            entry:SetSize(1, 32)
            entry:EnableMouse(true)

            local button = CreateFrame("Button", nil, entry, "SecureActionButtonTemplate")
            button:SetAllPoints(entry)  -- full row is clickable
            button:RegisterForClicks("AnyUp", "AnyDown")
            entry.button = button

            local icon = button:CreateTexture(nil, "BACKGROUND")
            icon:SetPoint("LEFT", button, "LEFT", 0, 0)
            icon:SetSize(24, 24)
            icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            button.icon = icon

            local cooldownOverlay = button:CreateTexture(nil, "ARTWORK")
            cooldownOverlay:SetPoint("LEFT", button, "LEFT", 0, 0)
            cooldownOverlay:SetSize(24, 24)
            cooldownOverlay:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
            cooldownOverlay:SetVertexColor(0, 0, 0, 0.6)
            cooldownOverlay:Hide()
            button.cooldownOverlay = cooldownOverlay

            local borderThickness = 1
            local borderTop = button:CreateTexture(nil, "OVERLAY")
            borderTop:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderTop:SetPoint("TOPLEFT", icon, "TOPLEFT", borderThickness, 0)
            borderTop:SetPoint("TOPRIGHT", icon, "TOPRIGHT", -borderThickness, 0)
            borderTop:SetHeight(borderThickness)

            local borderBottom = button:CreateTexture(nil, "OVERLAY")
            borderBottom:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderBottom:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", borderThickness, 0)
            borderBottom:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -borderThickness, 0)
            borderBottom:SetHeight(borderThickness)

            local borderLeft = button:CreateTexture(nil, "OVERLAY")
            borderLeft:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderLeft:SetPoint("TOPLEFT", icon, "TOPLEFT", 0, 0)
            borderLeft:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", 0, 0)
            borderLeft:SetWidth(borderThickness)

            local borderRight = button:CreateTexture(nil, "OVERLAY")
            borderRight:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderRight:SetPoint("TOPRIGHT", icon, "TOPRIGHT", 0, 0)
            borderRight:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0)
            borderRight:SetWidth(borderThickness)

            button.borderTop = borderTop
            button.borderBottom = borderBottom
            button.borderLeft = borderLeft
            button.borderRight = borderRight
            
            local rowHighlight = button:CreateTexture(nil, "HIGHLIGHT")
            rowHighlight:SetAtlas("Options_List_Active")
            rowHighlight:SetBlendMode("ADD")
            rowHighlight:SetAllPoints(button)
            entry.rowHighlight = rowHighlight

            local cooldownText = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            cooldownText:SetPoint("CENTER", icon, "CENTER", 1, 0)
            cooldownText:SetTextColor(1, 1, 1)
            button.cooldownText = cooldownText

            local label = entry:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            label:SetPoint("LEFT", icon, "RIGHT", 6, 0)
            label:SetJustifyH("LEFT")
            label:SetJustifyV("TOP")
            label:SetWordWrap(true)
            label:SetFont(GameFontNormal:GetFont(), 11, "OUTLINE")
            label:SetTextColor(1, 1, 1)
            entry.label = label

            local function OnEvent(self, event)
                if event == "CHALLENGE_MODE_COMPLETED" then
                    local btn = self
                    C_Timer.After(2, function()
                        if btn and btn.currentEntry then
                            local e = btn.currentEntry
                            if e.actionType == "housing" then
                                UpdateCooldownText(btn, e)
                            elseif e.spellID or e.itemID then
                                UpdateCooldownText(btn, e)
                            end
                        end
                    end)
                    return
                end
                local currentEntry = self.currentEntry
                if currentEntry and currentEntry.actionType == "housing" then
                    if not InCombatLockdown() then
                        local canReturn = C_HousingNeighborhood and C_HousingNeighborhood.CanReturnAfterVisitingHouse and C_HousingNeighborhood.CanReturnAfterVisitingHouse()
                        if canReturn then
                            self.icon:SetAtlas("dashboard-panel-homestone-teleport-out-button")
                            self:GetParent().label:SetText("Return to Previous Location")
                            self:SetAttribute("type", "returnhome")
                        else
                            local housingIcon = C_Spell.GetSpellTexture(1263273) or C_Spell.GetSpellTexture(1233637) or "Interface\\Icons\\INV_Misc_QuestionMark"
                            self.icon:SetTexture(housingIcon)
                            self:GetParent().label:SetText(GetSpellName(1233637) or "Teleport to Plot")
                            local houseInfo = currentEntry.houseInfo
                            if houseInfo then
                                self:SetAttribute("type", "teleporthome")
                                self:SetAttribute("house-neighborhood-guid", houseInfo.neighborhoodGUID)
                                self:SetAttribute("house-guid", houseInfo.houseGUID)
                                self:SetAttribute("house-plot-id", houseInfo.plotID)
                            end
                        end
                        self.icon:SetDesaturated(false)
                    end
                    UpdateCooldownText(self, currentEntry)
                elseif currentEntry and (currentEntry.spellID or currentEntry.itemID) then
                    UpdateCooldownText(self, currentEntry)
                else
                    self.cooldownText:SetText("")
                    self.cooldownOverlay:Hide()
                end
            end

            button:RegisterEvent("SPELLS_CHANGED")
            button:RegisterEvent("PLAYER_ENTERING_WORLD")
            button:RegisterEvent("CHALLENGE_MODE_COMPLETED")
            button:RegisterEvent("BAG_UPDATE_DELAYED")
            button:RegisterEvent("TOYS_UPDATED")
            button:RegisterEvent("ZONE_CHANGED_NEW_AREA")
            button:RegisterEvent("ZONE_CHANGED")
            button:SetScript("OnEvent", OnEvent)

            scrollChild.entryPool[index] = entry
        end
        return scrollChild.entryPool[index]
    end

    -- MARK: Build Sections and Entries
    local sections = {}
    local function AddSection(title, entries)
        if entries and #entries > 0 then
            table.insert(sections, { title = title, entries = entries })
        end
    end

    -- MARK: Hearthstones, Dalaran Hearthstones, Garrison Hearthstones, Housing Teleport Section.
    local hearthstoneEntries = {}
    local selectedHearthstone = MDungeonTeleports and MDungeonTeleports.selectedHearthstoneId or nil
    if MDungeonTeleports and MDungeonTeleports.randomHearthstone then
        selectedHearthstone = PickRandomHearthstone(MDTHearthstones) or selectedHearthstone
    end
    for _, entry in ipairs(NormalizeHearthstones(MDTHearthstones, selectedHearthstone, true)) do
        table.insert(hearthstoneEntries, entry)
    end
    if not MDungeonTeleports or MDungeonTeleports.ShowDalaranHS ~= false then
        for _, entry in ipairs(NormalizeToyList(MDTDalaran, true)) do
            table.insert(hearthstoneEntries, entry)
        end
    end
    if not MDungeonTeleports or MDungeonTeleports.ShowGarrisonHS ~= false then
        for _, entry in ipairs(NormalizeToyList(MDTGarrison, true)) do
            table.insert(hearthstoneEntries, entry)
        end
    end
    -- Housing: Teleport to Plot
    if (not MDungeonTeleports or MDungeonTeleports.ShowPlotTeleport ~= false)
        and C_Housing and C_Housing.HasHousingExpansionAccess and C_Housing.HasHousingExpansionAccess() then
        -- Try to fetch housing data inline if we don't have it yet
        if #mdtHouseData == 0 then
            TryFetchHousingData()
        end
    end
    if (not MDungeonTeleports or MDungeonTeleports.ShowPlotTeleport ~= false)
        and C_Housing and C_Housing.HasHousingExpansionAccess and C_Housing.HasHousingExpansionAccess()
        and #mdtHouseData > 0 then
        local playerFaction = UnitFactionGroup("player")
        local houseInfo
        if #mdtHouseData == 1 then
            houseInfo = mdtHouseData[1]
        elseif playerFaction == "Alliance" then
            houseInfo = mdtHouseData[1]
        else
            houseInfo = mdtHouseData[2]
        end
        if houseInfo then
            local housingIcon = C_Spell.GetSpellTexture(1263273) or C_Spell.GetSpellTexture(1233637) or "Interface\\Icons\\INV_Misc_QuestionMark"
            local housingName = GetSpellName(1233637) or "Teleport to Plot"
            local housingEntry = {
                name = housingName,
                actionType = "housing",
                icon = housingIcon,
                preferIcon = true,
                houseInfo = houseInfo,
            }
            table.insert(hearthstoneEntries, housingEntry)
        end
    end
    
    AddSection(L["HEARTHSTONE_TITLE"], hearthstoneEntries)

    -- MARK: Arcantina Section
    if not MDungeonTeleports or MDungeonTeleports.ShowArcantina ~= false then
        local arcantinaEntry = {}
        for _, entry in ipairs(NormalizeToyList(MDTArcantina, true)) do
            table.insert(arcantinaEntry, entry)
        end
        
        AddSection(L["ARCANTINA_TITLE"] , arcantinaEntry)
    end

    -- MARK: Class Teleports Section
    local _, classTag = UnitClass("player")
    if not MDungeonTeleports or MDungeonTeleports.ClassTeleports ~= false then
        if classTag == "MAGE" then
            local faction = UnitFactionGroup("player")
            local mageTeleports = MDTMage
            local magePortals = nil
            if faction == "Alliance" then
                if MDTMageAllianceTeleports then
                    mageTeleports = MDTMageAllianceTeleports
                end
                magePortals = MDTMageAlliancePortals
            elseif faction == "Horde" then
                if MDTMageHordeTeleports then
                    mageTeleports = MDTMageHordeTeleports
                end
                magePortals = MDTMageHordePortals
            end

            local mageTeleportEntries = NormalizeSpellList(mageTeleports)
            local magePortalEntries = NormalizeSpellList(magePortals)

            AddSection(L["TOOLTIP_WORLD_MAP_TAB_MAGE"], mageTeleportEntries)
            AddSection(L["TOOLTIP_WORLD_MAP_TAB_MAGE_PORTALS"], magePortalEntries)
        end

        if classTag == "DRUID" then
            local druidPortals = {}
            local playerLevel = UnitLevel("player")
            for _, data in ipairs(MDTDruidPortals) do
                local spellID = tonumber(data.spellID)
                -- At level 32+, Dreamwalk replaces Moonglade
                if spellID == 18960 and playerLevel >= 32 then
                    -- skip Moonglade
                elseif spellID == 193753 and playerLevel < 32 then
                    -- skip Dreamwalk
                else
                    table.insert(druidPortals, data)
                end
            end
            AddSection(L["DRUID_PORTALS_TITLE"], NormalizeSpellList(druidPortals))
        end

        if classTag == "DEATHKNIGHT" then
            AddSection(L["DK_PORTALS_TITLE"], NormalizeSpellList(MDTDeathKnightPortals))
        end

        if classTag == "MONK" then
            AddSection(L["MONK_PORTALS_TITLE"], NormalizeSpellList(MDTMonkPortals))
        end

        if classTag == "SHAMAN" then
            AddSection(L["SHAMAN_PORTALS_TITLE"], NormalizeSpellList(MDTShamanPortals))
        end
    end

    -- MARK: Race Teleports Section
    if not MDungeonTeleports or MDungeonTeleports.RaceTeleports ~= false then
        local _, raceTag = UnitRace("player")
        if raceTag == "Vulpera" then
            AddSection(L["VULPERA_PORTALS_TITLE"], NormalizeSpellList(MDTVulperaPortals))
        end

        if raceTag == "DarkIronDwarf" then
            AddSection(L["DARK_IRON_PORTALS_TITLE"], NormalizeSpellList(MDTDarkIronPortals))
        end

        if raceTag == "Harronir" then
            AddSection(L["HARANIR_PORTALS_TITLE"], NormalizeSpellList(MDTHaranirPortals))
        end
    end

    -- MARK: Midnight Season 1 Teleports Section
    if not MDungeonTeleports or MDungeonTeleports.MapSeason1 ~= false then
        local seasonEntries = {}
        for _, entry in ipairs(NormalizePortalListTableData(MDT_Season1Row1)) do
            table.insert(seasonEntries, entry)
        end
        for _, entry in ipairs(NormalizePortalListTableData(MDT_Season1Row2)) do
            table.insert(seasonEntries, entry)
        end
        AddSection(L["TOOLTIP_WORLD_MAP_TAB_SEASON"], seasonEntries)
    end

    -- MARK: Expansion Portals Section
    if not MDungeonTeleports or MDungeonTeleports.MapMidnight ~= false then
        AddSection(L["EXP_MID"], NormalizePortalListTableData(MDT_PL_MID_AVAILABLE))
    end
    local expansionTables = {
        { name = L["EXP_TWW"], data = MDT_PL_TWW },
        { name = L["EXP_TWW_RAIDS"], data = MDT_PLR_TWW },
        { name = L["EXP_DF"], data = MDT_PL_DF },
        { name = L["EXP_DF_RAIDS"], data = MDT_PLR_DF },
        { name = L["EXP_SL"], data = MDT_PL_SL },
        { name = L["EXP_SL_RAIDS"], data = MDT_PLR_SL },
        { name = L["EXP_BFA"], data = MDT_PL_BFA },
        { name = L["EXP_LEG"], data = MDT_PL_Legion },
        { name = L["EXP_WOD"], data = MDT_PL_WOD },
        { name = L["EXP_MOP"], data = MDT_PL_MOP },
        { name = L["EXP_CATA"], data = MDT_PL_CATA },
        { name = L["EXP_WRATH"], data = MDT_PL_WRATH }
    }

    for _, expansion in ipairs(expansionTables) do
        AddSection(expansion.name or "", NormalizePortalListTableData(expansion.data))
    end

    -- MARK: Create UI Elements for Sections and Entries all the data goop.
    local function UpdateEntry(entryFrame, entry)
        local button = entryFrame.button
        local label = entryFrame.label

        local displayName = entry.name
        local icon

        if entry.actionType == "housing" then
            local canReturn = C_HousingNeighborhood and C_HousingNeighborhood.CanReturnAfterVisitingHouse and C_HousingNeighborhood.CanReturnAfterVisitingHouse()
            if canReturn then
                displayName = "Return to Previous Location"
                button.icon:SetAtlas("dashboard-panel-homestone-teleport-out-button")
            else
                displayName = GetSpellName(1233637) or "Teleport to Plot"
                icon = C_Spell.GetSpellTexture(1263273) or C_Spell.GetSpellTexture(1233637) or "Interface\\Icons\\INV_Misc_QuestionMark"
                button.icon:SetTexture(icon)
            end
        else
            if not entry.useTableData then
                if entry.actionType == "spell" and entry.spellID then
                    local spellName = GetSpellName(entry.spellID)
                    if spellName and spellName ~= "" then
                        displayName = spellName
                    end
                elseif (entry.actionType == "toy" or entry.actionType == "item") and entry.itemID then
                    local itemName = GetItemName(entry.itemID)
                    if itemName and itemName ~= "" then
                        displayName = itemName
                    end
                end
            end

            if entry.actionType == "spell" then
                if entry.useTableData then
                    icon = entry.icon
                else
                    icon = GetSpellIcon(entry.spellID)
                end
            elseif entry.preferIcon and entry.icon and entry.icon ~= "" then
                icon = entry.icon
            else
                icon = entry.icon and entry.icon ~= "" and entry.icon or GetItemIcon(entry.itemID)
            end
            button.icon:SetTexture(icon)
        end
        label:SetText(displayName or "")

        local known = true
        if entry.actionType == "housing" then
            known = entry.houseInfo ~= nil
        elseif entry.actionType == "spell" then
            known = IsSpellKnownForDisplay(entry.spellID)
        elseif entry.actionType == "toy" then
            known = IsToyKnown(entry.itemID)
        else
            known = IsItemKnown(entry.itemID)
        end

        button.icon:SetDesaturated(not known)
        if not known then
            label:SetTextColor(0.5, 0.5, 0.5)
        else
            label:SetTextColor(255/255, 221/255, 0/255) -- Text Colour
        end

        if button.cooldownText then
            button.cooldownText:SetFont(MDungeonTeleports.customFont, 14, "OUTLINE")
        end
        button.currentEntry = entry
        if entry and (entry.spellID or entry.itemID or entry.actionType == "housing") then
            UpdateCooldownText(button, entry)
        else
            if button.cooldownText then
                button.cooldownText:SetText("")
            end
            if button.cooldownOverlay then
                button.cooldownOverlay:Hide()
            end
        end

        if not InCombatLockdown() then
            if entry.actionType == "housing" then
                if C_HousingNeighborhood and C_HousingNeighborhood.CanReturnAfterVisitingHouse and C_HousingNeighborhood.CanReturnAfterVisitingHouse() then
                    button:SetAttribute("type", "returnhome")
                else
                    local houseInfo = entry.houseInfo
                    if houseInfo then
                        button:SetAttribute("type", "teleporthome")
                        button:SetAttribute("house-neighborhood-guid", houseInfo.neighborhoodGUID)
                        button:SetAttribute("house-guid", houseInfo.houseGUID)
                        button:SetAttribute("house-plot-id", houseInfo.plotID)
                    end
                end
            elseif entry.actionType == "spell" then
                button:SetAttribute("type", "spell")
                button:SetAttribute("spell", entry.spellID)
            elseif entry.actionType == "toy" then
                button:SetAttribute("type", "toy")
                button:SetAttribute("toy", entry.itemID)
            else
                button:SetAttribute("type", "item")
                button:SetAttribute("item", "item:" .. tostring(entry.itemID))
            end
        end

        button:SetScript("OnEnter", function(self)
            if not MDungeonTeleports.wmtTooltips then return end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if entry.actionType == "housing" then
                local canReturn = C_HousingNeighborhood and C_HousingNeighborhood.CanReturnAfterVisitingHouse and C_HousingNeighborhood.CanReturnAfterVisitingHouse()
                if canReturn then
                    GameTooltip:AddLine("Return to Previous Location", 1, 1, 1)
                else
                    GameTooltip:SetSpellByID(1233637)
                end
            elseif entry.actionType == "spell" and entry.spellID then
                GameTooltip:SetSpellByID(entry.spellID)
            elseif entry.actionType == "toy" and entry.itemID then
                GameTooltip:SetToyByItemID(entry.itemID)
            elseif entry.itemID then
                GameTooltip:SetItemByID(entry.itemID)
            end
            GameTooltip:Show()
        end)

        button:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
    end

    -- MARK: Layout Logic
    local function Layout()
        if not scrollFrame or not scrollChild then return end
        local contentWidth = max(1, scrollFrame:GetWidth() - 20)
        local columnGap = 26
        local columnWidth = floor((contentWidth - columnGap) / 2)
        local leftX = 12
        local rightX = leftX + columnWidth + columnGap

        local y = -6
        local headerIndex = 0
        local entryIndex = 0

        for _, section in ipairs(sections) do
            headerIndex = headerIndex + 1
            local header = GetHeader(headerIndex)
            header:Show()
            header:ClearAllPoints()
            header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", leftX, y)
            header:SetSize(contentWidth, 18)
            header.text:SetText(section.title)
            y = y - 20

            local entries = section.entries or {}
            local rowHeight = 32
            for i, entry in ipairs(entries) do
                entryIndex = entryIndex + 1
                local frame = GetEntry(entryIndex)
                frame:Show()
                frame:ClearAllPoints()

                local col = ((i - 1) % 2) + 1
                local row = floor((i - 1) / 2)
                local x = (col == 1) and leftX or rightX
                local rowY = y - (row * rowHeight)

                frame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, rowY)
                frame:SetSize(columnWidth, rowHeight)
                frame.label:SetWidth(columnWidth - 30)
                UpdateEntry(frame, entry)
            end

            local rows = ceil(#entries / 2)
            y = y - (rows * rowHeight) - 12
        end

        for i = headerIndex + 1, #scrollChild.headerPool do
            scrollChild.headerPool[i]:Hide()
        end
        for i = entryIndex + 1, #scrollChild.entryPool do
            scrollChild.entryPool[i]:Hide()
        end

        scrollChild:SetHeight(max(1, -y + 10))
        scrollChild:SetWidth(contentWidth)
    end

    self.LayoutScrollContent = Layout
    Layout()

    if not scrollFrame.mdtSizeHooked then
        scrollFrame:SetScript("OnSizeChanged", function()
            if MDTWorldMapTab.LayoutScrollContent then
                MDTWorldMapTab:LayoutScrollContent()
            end
        end)
        scrollFrame.mdtSizeHooked = true
    end

    if not self.panel.UpdateFrame then
        local updateFrame = CreateFrame("Frame", nil, self.panel)
        updateFrame:RegisterEvent("SPELLS_CHANGED")
        updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        updateFrame:RegisterEvent("BAG_UPDATE_DELAYED")
        updateFrame:RegisterEvent("TOYS_UPDATED")
        updateFrame:SetScript("OnEvent", function()
            if MDTWorldMapTab.LayoutScrollContent and not InCombatLockdown() then
                MDTWorldMapTab:LayoutScrollContent()
            end
        end)
        self.panel.UpdateFrame = updateFrame
    end
end

-- MARK: Panel Visibility
function MDTWorldMapTab:TogglePanel()
    if not self.panel then return end

    if not self:IsEnabled() then
        return
    end

    self:SelectTab()
end

-- ShowPanel/HidePanel: direct fallbacks when lib is unavailable.
-- LibWorldMapTabs normally manages content frame visibility via lib:SetDisplayMode.
function MDTWorldMapTab:ShowPanel()
    if not self.panel then return end
    if not self:IsEnabled() then return end
    self.panel:Show()
    if self.button then
        self.button:SetChecked(true)
        if self.button.Icon then self.button.Icon:SetTexture(self.button.activeAtlas) end
    end
end

function MDTWorldMapTab:HidePanel()
    if not self.panel then return end
    self.panel:Hide()
    if self.button then
        self.button:SetChecked(false)
        if self.button.Icon then self.button.Icon:SetTexture(self.button.inactiveAtlas) end
    end
end

function MDTWorldMapTab:UpdateVisibility(enabled)
    if enabled then
        if not self.initialized then
            self:Initialize()
            return
        end
        if self.button then
            self.button:Show()
            -- If lib is active it will handle re-placing the tab; otherwise do it manually.
            if not LWMT then self:AnchorButton() end
        end
        return
    end

    -- Hiding the button via lib: lib's OnHide hook will call SetDisplayMode(quests) if we were active.
    if self.button then self.button:Hide() end
end

-- MARK: Event handling
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName == "MDungeonTeleports" then
            -- Addon loaded
        elseif addonName == "ElvUI" then
            if MDTWorldMapTab.button then
                MDTWorldMapTab:ApplyElvUISkin()
            end
        elseif addonName == "NDui" then
            if MDTWorldMapTab.button then
                MDTWorldMapTab:ApplyNDuiSkin()
            end
        end
    elseif event == "PLAYER_LOGIN" then
        -- Request housing data (also try to capture return value if cached)
        if C_Housing and C_Housing.GetPlayerOwnedHouses then
            eventFrame:RegisterEvent("PLAYER_HOUSE_LIST_UPDATED")
            TryFetchHousingData()
        end
        -- Initialize after login
        C_Timer.After(1, function()
            if not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab:Initialize()
            end
        end)
    elseif event == "PLAYER_ENTERING_WORLD" then
        -- Also try on entering world
        C_Timer.After(2, function()
            if not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab:Initialize()
            end
        end)
        -- Retry housing data fetch if still missing
        C_Timer.After(5, function()
            if #mdtHouseData == 0 and TryFetchHousingData() then
                if MDTWorldMapTab.panel and not InCombatLockdown() then
                    MDTWorldMapTab:BuildScrollContent()
                    mdtHouseDataDirty = false
                end
            end
        end)
    elseif event == "PLAYER_HOUSE_LIST_UPDATED" then
        -- Try event payload first
        local housingInfo = ...
        if housingInfo and type(housingInfo) == "table" and #housingInfo > 0 then
            mdtHouseData = housingInfo
            mdtHouseDataDirty = true
        else
            -- Event payload was nil/empty; try calling the API directly (data may now be available)
            TryFetchHousingData()
        end
        if #mdtHouseData > 0 then
            eventFrame:UnregisterEvent("PLAYER_HOUSE_LIST_UPDATED")
            -- Rebuild content if panel already exists
            if MDTWorldMapTab.panel and not InCombatLockdown() then
                MDTWorldMapTab:BuildScrollContent()
                mdtHouseDataDirty = false
            end
        end
    elseif event == "PLAYER_REGEN_ENABLED" then
        if MDTWorldMapTab.pendingContentRefresh then
            MDTWorldMapTab.pendingContentRefresh = nil
            if MDTWorldMapTab.panel then
                MDTWorldMapTab:BuildScrollContent()
            end
        end
    end
end)

-- MARK: Handle WorldMap show/hide with delayed hook
C_Timer.After(3, function()
    if WorldMapFrame then
        hooksecurefunc(WorldMapFrame, "Show", function()
            if MDTWorldMapTab.initialized and MDTWorldMapTab.button and MDTWorldMapTab:IsEnabled() then
                -- Rebuild content if random hearthstone is enabled or housing data arrived
                if not InCombatLockdown() and MDTWorldMapTab.panel then
                    if (MDungeonTeleports and MDungeonTeleports.randomHearthstone) or mdtHouseDataDirty then
                        MDTWorldMapTab:BuildScrollContent()
                        mdtHouseDataDirty = false
                    end
                end
                -- LibWorldMapTabs handles tab placement and content visibility via WorldMapOnShow EventRegistry
            elseif not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab:Initialize()
            end
        end)
        -- LibWorldMapTabs does not need a Hide hook; content visibility is managed by the lib
    end
end)

NS.WorldMapTab = MDTWorldMapTab