local addonName, SinsToolkit = ...

local Hub = {}
SinsToolkit.TeleportHub = Hub

local CATEGORY_ORDER = {
    "current",
    "legacy",
    "raids",
    "class",
    "hearth",
    "toys",
}

local CATEGORY_NAMES = {
    current = "M+ Current Season",
    legacy = "M+ Legacy",
    raids = "Raids",
    class = "Class Teleports",
    hearth = "Hearthstone",
    toys = "Toys",
}

local TELEPORTS = {
    current = {
        { spellID = 1254572, label = "Magisters' Terrace" },
        { spellID = 1254559, label = "Maisara Caverns" },
        { spellID = 1254563, label = "Nexus-Point Xenas" },
        { spellID = 1254400, label = "Windrunner Spire" },
        { spellID = 393273, label = "Algeth'ar Academy" },
        { spellID = 1254555, label = "Pit of Saron" },
        { spellID = 159898, label = "Skyreach" },
        { spellID = 1254551, label = "Seat of the Triumvirate" },
    },
    legacy = {
        { spellID = 410080, label = "The Vortex Pinnacle" },
        { spellID = 424142, label = "Throne of the Tides" },
        { spellID = 445424, label = "Grim Batol" },
        { spellID = 131204, label = "Temple of the Jade Serpent" },
        { spellID = 131228, label = "Siege of Niuzao Temple" },
        { spellID = 131232, label = "Scholomance" },
        { spellID = 131229, label = "Scarlet Monastery" },
        { spellID = 131231, label = "Scarlet Halls" },
        { spellID = 131225, label = "Gate of the Setting Sun" },
        { spellID = 131222, label = "Mogu'shan Palace" },
        { spellID = 131206, label = "Shado-Pan Monastery" },
        { spellID = 131205, label = "Stormstout Brewery" },
        { spellID = 159899, label = "Shadowmoon Burial Grounds" },
        { spellID = 159901, label = "Everbloom" },
        { spellID = 159895, label = "Bloodmaul Slag Mines" },
        { spellID = 159897, label = "Auchindoun" },
        { spellID = 159902, label = "Upper Blackrock Spire" },
        { spellID = 159900, label = "Grimrail Depot" },
        { spellID = 159896, label = "Iron Docks" },
        { spellID = 424163, label = "Darkheart Thicket" },
        { spellID = 424153, label = "Black Rook Hold" },
        { spellID = 393764, label = "Halls of Valor" },
        { spellID = 410078, label = "Neltharion's Lair" },
        { spellID = 393766, label = "Court of Stars" },
        { spellID = 373262, label = "Karazhan" },
        { spellID = 424187, label = "Atal'Dazar" },
        { spellID = 410071, label = "Freehold" },
        { spellID = 424167, label = "Waycrest Manor" },
        { spellID = 410074, label = "The Underrot" },
        { spellID = 373274, label = "Operation: Mechagon" },
        { spellID = 354462, label = "The Necrotic Wake" },
        { spellID = 354463, label = "Plaguefall" },
        { spellID = 354464, label = "Mists of Tirna Scithe" },
        { spellID = 354465, label = "Halls of Atonement" },
        { spellID = 354466, label = "Spires of Ascension" },
        { spellID = 354467, label = "Theater of Pain" },
        { spellID = 354468, label = "De Other Side" },
        { spellID = 354469, label = "Sanguine Depths" },
        { spellID = 367416, label = "Tazavesh, the Veiled Market" },
        { spellID = 393256, label = "Ruby Life Pools" },
        { spellID = 393262, label = "The Nokhud Offensive" },
        { spellID = 393279, label = "The Azure Vault" },
        { spellID = 393276, label = "Neltharus" },
        { spellID = 393267, label = "Brackenhide Hollow" },
        { spellID = 393283, label = "Halls of Infusion" },
        { spellID = 393222, label = "Uldaman: Legacy of Tyr" },
        { spellID = 424197, label = "Dawn of the Infinite" },
        { spellID = 445416, label = "City of Threads" },
        { spellID = 445417, label = "Ara-Kara, City of Echoes" },
        { spellID = 445269, label = "The Stonevault" },
        { spellID = 445414, label = "The Dawnbreaker" },
        { spellID = 445443, label = "The Rookery" },
        { spellID = 445441, label = "Darkflame Cleft" },
        { spellID = 445440, label = "Cinderbrew Brewery" },
        { spellID = 445444, label = "Priory of the Sacred Flame" },
        { spellID = 1216786, label = "Operation: Floodgate" },
        { spellID = 1237215, label = "Eco-Dome Al'dani" },
    },
    raids = {
        { spellID = 373190, label = "Castle Nathria" },
        { spellID = 373191, label = "Sanctum of Domination" },
        { spellID = 373192, label = "Sepulcher of the First Ones" },
        { spellID = 432254, label = "Vault of the Incarnates" },
        { spellID = 432257, label = "Aberrus, the Shadowed Crucible" },
        { spellID = 432258, label = "Amirdrassil, the Dream's Hope" },
        { spellID = 1226482, label = "Liberation of Undermine" },
        { spellID = 1239155, label = "Manaforge Omega" },
    },
    class = {
        { spellID = 3561, class = "MAGE" }, { spellID = 3562, class = "MAGE" },
        { spellID = 3563, class = "MAGE" }, { spellID = 3566, class = "MAGE" },
        { spellID = 3567, class = "MAGE" }, { spellID = 32271, class = "MAGE" },
        { spellID = 32272, class = "MAGE" }, { spellID = 49359, class = "MAGE" },
        { spellID = 49358, class = "MAGE" }, { spellID = 33690, class = "MAGE" },
        { spellID = 53140, class = "MAGE" }, { spellID = 88342, class = "MAGE" },
        { spellID = 88344, class = "MAGE" }, { spellID = 132621, class = "MAGE" },
        { spellID = 132627, class = "MAGE" }, { spellID = 176248, class = "MAGE" },
        { spellID = 176242, class = "MAGE" }, { spellID = 281403, class = "MAGE" },
        { spellID = 281404, class = "MAGE" }, { spellID = 344587, class = "MAGE" },
        { spellID = 1259190, class = "MAGE" },
        { spellID = 10059, class = "MAGE" }, { spellID = 11416, class = "MAGE" },
        { spellID = 11418, class = "MAGE" }, { spellID = 11420, class = "MAGE" },
        { spellID = 11417, class = "MAGE" }, { spellID = 32266, class = "MAGE" },
        { spellID = 32267, class = "MAGE" }, { spellID = 49360, class = "MAGE" },
        { spellID = 49361, class = "MAGE" }, { spellID = 33691, class = "MAGE" },
        { spellID = 53142, class = "MAGE" }, { spellID = 88345, class = "MAGE" },
        { spellID = 88346, class = "MAGE" }, { spellID = 132620, class = "MAGE" },
        { spellID = 132626, class = "MAGE" }, { spellID = 176246, class = "MAGE" },
        { spellID = 176244, class = "MAGE" }, { spellID = 224871, class = "MAGE" },
        { spellID = 281400, class = "MAGE" }, { spellID = 281402, class = "MAGE" },
        { spellID = 344597, class = "MAGE" }, { spellID = 1259194, class = "MAGE" },
        { spellID = 18960, class = "DRUID" }, { spellID = 193753, class = "DRUID" },
        { spellID = 126892, class = "MONK" }, { spellID = 50977, class = "DEATHKNIGHT" },
    },
    hearth = {
        { itemID = 6948, label = "Hearthstone", alwaysKnown = true },
    },
    toys = {
        { itemID = 110560, label = "Garrison Hearthstone" },
        { itemID = 64488, label = "The Innkeeper's Daughter" },
        { itemID = 93672, label = "Dark Portal" },
        { itemID = 54452, label = "Ethereal Portal" },
        { itemID = 162973, label = "Greatfather Winter's Hearthstone" },
        { itemID = 163045, label = "Headless Horseman's Hearthstone" },
        { itemID = 165669, label = "Lunar Elder's Hearthstone" },
        { itemID = 165670, label = "Peddlefeet's Lovely Hearthstone" },
        { itemID = 165802, label = "Noble Gardener's Hearthstone" },
        { itemID = 166746, label = "Fire Eater's Hearthstone" },
        { itemID = 166747, label = "Brewfest Reveler's Hearthstone" },
        { itemID = 168907, label = "Holographic Digitalization Hearthstone" },
        { itemID = 172179, label = "Eternal Traveler's Hearthstone" },
        { itemID = 180290, label = "Night Fae Hearthstone" },
        { itemID = 182773, label = "Necrolord Hearthstone" },
        { itemID = 183716, label = "Venthyr Sinstone" },
        { itemID = 184353, label = "Kyrian Hearthstone" },
        { itemID = 188952, label = "Dominated Hearthstone" },
        { itemID = 190196, label = "Enlightened Hearthstone" },
        { itemID = 190237, label = "Broker's Translocation Matrix" },
        { itemID = 193588, label = "Timewalker's Hearthstone" },
        { itemID = 200630, label = "Ohn'ir Windsage's Hearthstone" },
        { itemID = 206195, label = "Path of the Naaru" },
        { itemID = 208704, label = "Deepdweller's Earthen Hearthstone" },
        { itemID = 209035, label = "Hearthstone of the Flame" },
        { itemID = 210455, label = "Draenic Hologem" },
        { itemID = 212337, label = "Stone of the Hearth" },
        { itemID = 212766, label = "Notorious Thread's Hearthstone" },
        { itemID = 228940, label = "Postmaster's Hearthstone" },
        { itemID = 232517, label = "Explosive Hearthstone" },
        { itemID = 245970, label = "P.O.S.T. Master's Express Hearthstone" },
        { itemID = 246565, label = "Cosmic Hearthstone" },
        { itemID = 257736, label = "Lightcalled Hearthstone" },
        { itemID = 265100, label = "Corewarden's Hearthstone" },
        { itemID = 140192, label = "Dalaran Hearthstone" },
        { itemID = 142542, label = "Tome of Town Portal" },
        { itemID = 168807, label = "Wormhole Generator: Kul Tiras" },
        { itemID = 168808, label = "Wormhole Generator: Zandalar" },
        { itemID = 172924, label = "Wormhole Generator: Shadowlands" },
        { itemID = 198156, label = "Wyrmhole Generator: Dragon Isles" },
        { itemID = 151652, label = "Wormhole Generator: Argus" },
        { itemID = 48933, label = "Wormhole Generator: Northrend" },
        { itemID = 87215, label = "Wormhole Generator: Pandaria" },
        { itemID = 112059, label = "Wormhole Centrifuge" },
    },
}

local panel, scrollFrame, scrollChild, searchBox, viewButton, minimapButton, expandAllButton, collapseAllButton
local selectorButton, selectorMenu, selectorScroll, selectorChild, selectedUseButton, selectedIcon, selectedName, selectedMeta, selectedFavoriteButton, selectedStatus, rescanButton
local contentFrame
local isEmbedded = false
local buttons = {}
local favoriteButtons = {}
local selectorRows = {}
local headers = {}
local emptyLabels = {}
local searchText = ""
local initialized = false
local BUTTON_SIZE = 44
local CELL_WIDTH = 150
local CELL_HEIGHT = 124
local PANEL_MIN_WIDTH = 540
local PANEL_MIN_HEIGHT = 380
local LIST_ROW_HEIGHT = 58
local POOL_SIZE = 180
local RECENT_LIMIT = 8

local function Theme()
    return SinsToolkit.GetTheme and SinsToolkit.GetTheme() or {
        accentR = 0.72, accentG = 0.41, accentB = 0.88,
        accentHex = "b868dc", bgR = 0.02, bgG = 0.015, bgB = 0.025, bgA = 0.94,
        borderA = 0.9, blizzard = false,
    }
end

local function Print(message)
    print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. message)
end

local function IsEnabled()
    return SinsToolkit.IsFeatureActive and SinsToolkit.IsFeatureActive("teleportHubEnabled")
end

local function IsListView()
    return SinsToolkitDB and SinsToolkitDB.teleportHubView == "list"
end

local function IsMenuView()
    return not SinsToolkitDB or SinsToolkitDB.teleportHubView == "menu"
end

local function EntryKey(entry)
    if not entry then return nil end
    if entry.spellID then return "spell:" .. tostring(entry.spellID) end
    if entry.itemID then return "item:" .. tostring(entry.itemID) end
end

local function IsFavorite(entry)
    local key = EntryKey(entry)
    return key and SinsToolkitDB and SinsToolkitDB.teleportHubFavorites and SinsToolkitDB.teleportHubFavorites[key] == true
end

local function SetFavorite(entry, enabled)
    local key = EntryKey(entry)
    if not key or not SinsToolkitDB then return end
    SinsToolkitDB.teleportHubFavorites = SinsToolkitDB.teleportHubFavorites or {}
    SinsToolkitDB.teleportHubFavorites[key] = enabled and true or nil
end

local function EntryDestination(entry)
    if not entry then return "" end
    if entry.destination then return entry.destination end
    if entry.category == "hearth" then
        return "Hearth location: " .. ((GetBindLocation and GetBindLocation()) or "Current bind location")
    end
    return CATEGORY_NAMES[entry.category] or "Teleport"
end

local function RememberRecent(entry)
    local key = EntryKey(entry)
    if not key or not SinsToolkitDB then return end
    SinsToolkitDB.teleportHubRecent = SinsToolkitDB.teleportHubRecent or {}
    for i = #SinsToolkitDB.teleportHubRecent, 1, -1 do
        if SinsToolkitDB.teleportHubRecent[i] == key then
            table.remove(SinsToolkitDB.teleportHubRecent, i)
        end
    end
    table.insert(SinsToolkitDB.teleportHubRecent, 1, key)
    while #SinsToolkitDB.teleportHubRecent > RECENT_LIMIT do
        table.remove(SinsToolkitDB.teleportHubRecent)
    end
end

local function IsCollapsed(category)
    return SinsToolkitDB
        and SinsToolkitDB.teleportHubCollapsed
        and SinsToolkitDB.teleportHubCollapsed[category] == true
end

local function SetAllCollapsed(collapsed)
    if not SinsToolkitDB then return end
    SinsToolkitDB.teleportHubCollapsed = SinsToolkitDB.teleportHubCollapsed or {}
    for _, category in ipairs(CATEGORY_ORDER) do
        SinsToolkitDB.teleportHubCollapsed[category] = collapsed and true or nil
    end
end

local function GetAllEntriesByKey()
    local entries = {}
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            entry.category = category
            local key = EntryKey(entry)
            if key and not entries[key] then
                entries[key] = entry
            end
        end
    end
    return entries
end

local function GetSpellName(spellID)
    return C_Spell and C_Spell.GetSpellName and C_Spell.GetSpellName(spellID) or GetSpellInfo and GetSpellInfo(spellID)
end

local function GetSpellIcon(spellID)
    return C_Spell and C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(spellID) or select(3, GetSpellInfo(spellID))
end

local function GetSecureSpell(entry)
    if entry.secureSpell then return entry.secureSpell end
    entry.secureSpell = entry.spellID and GetSpellName(entry.spellID) or nil
    return entry.secureSpell
end

local function IsSpellKnown(spellID)
    if C_SpellBook and C_SpellBook.IsSpellKnown then
        return C_SpellBook.IsSpellKnown(spellID)
    end
    return IsSpellKnown and IsSpellKnown(spellID)
end

local function GetToyNameIcon(itemID)
    if C_ToyBox and C_ToyBox.GetToyInfo then
        local _, name, icon = C_ToyBox.GetToyInfo(itemID)
        if name then return name, icon end
    end
    local name = C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID)
    local icon = C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(itemID)
    return name, icon
end

local function IsToyKnown(itemID)
    return PlayerHasToy and PlayerHasToy(itemID)
end

local function EntryName(entry)
    if entry.category == "hearth" then
        return GetBindLocation and GetBindLocation() or entry.label or "Hearthstone"
    end
    if entry.itemID and not entry.alwaysKnown then
        local name = GetToyNameIcon(entry.itemID)
        return name or entry.label or ("Item " .. entry.itemID)
    end
    return (SinsToolkitDB and SinsToolkitDB.teleportHubShowDungeonNames and entry.label)
        or (entry.spellID and GetSpellName(entry.spellID))
        or entry.label
        or "Unknown"
end

local function EntryGridName(entry)
    local name = EntryName(entry) or "Unknown"
    name = name:gsub("^Wyrmhole Generator:%s*", "Wyrmhole: ")
    name = name:gsub("^Wormhole Generator:%s*", "Wormhole: ")
    name = name:gsub("^Mole Machine:%s*", "Mole: ")
    name = name:gsub("^Path of the%s*", "")
    return name
end

local function EntryIcon(entry)
    if entry.itemID then
        if entry.alwaysKnown and C_Item and C_Item.GetItemIconByID then
            return C_Item.GetItemIconByID(entry.itemID)
        end
        local _, icon = GetToyNameIcon(entry.itemID)
        return icon or (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(entry.itemID))
    end
    return entry.spellID and GetSpellIcon(entry.spellID)
end

local function IsKnown(entry)
    if entry.alwaysKnown then return true end
    if entry.itemID then return IsToyKnown(entry.itemID) end
    return entry.spellID and IsSpellKnown(entry.spellID)
end

local function EntryMatches(entry)
    if searchText == "" then return true end
    local haystack = table.concat({
        EntryName(entry) or "",
        EntryDestination(entry) or "",
        CATEGORY_NAMES[entry.category] or entry.category or "",
        tostring(entry.spellID or ""),
        tostring(entry.itemID or ""),
    }, " "):lower()
    return haystack:find(searchText, 1, true) ~= nil
end

local function CanShowEntry(entry, classToken)
    if entry.class and entry.class ~= classToken then return false end
    if not EntryMatches(entry) then return false end
    return (SinsToolkitDB and SinsToolkitDB.teleportHubShowLocked) or IsKnown(entry)
end

local function ClearButton(button)
    if InCombatLockdown and InCombatLockdown() then return end
    button:SetAttribute("type", nil)
    button:SetAttribute("spell", nil)
    button:SetAttribute("item", nil)
    button:SetAttribute("toy", nil)
    button:SetAttribute("macrotext", nil)
end

local function AssignButton(button, entry)
    if InCombatLockdown and InCombatLockdown() then return end
    ClearButton(button)
    if not IsKnown(entry) then return end
    if entry.itemID and entry.alwaysKnown then
        local name = C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(entry.itemID)
        button:SetAttribute("type", "item")
        button:SetAttribute("item", name or entry.itemID)
    elseif entry.itemID then
        button:SetAttribute("type", "toy")
        button:SetAttribute("toy", entry.itemID)
    elseif entry.spellID then
        button:SetAttribute("type", "spell")
        button:SetAttribute("spell", entry.spellID)
    end
end

local function GetButton(index)
    if buttons[index] then return buttons[index] end
    local button = CreateFrame("Button", addonName .. "TeleportHubButton" .. index, nil, "SecureActionButtonTemplate")
    button:RegisterForClicks("AnyUp", "AnyDown")
    button:SetSize(BUTTON_SIZE, BUTTON_SIZE)
    button.icon = button:CreateTexture(nil, "BACKGROUND")
    button.icon:SetAllPoints(button)
    button.label = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    button.label:SetPoint("TOP", button, "BOTTOM", 0, -2)
    button.label:SetWidth(CELL_WIDTH)
    button.label:SetJustifyH("CENTER")
    button.label:SetMaxLines(2)
    button:SetScript("OnEnter", function(self)
        if not self.entry or not GameTooltip then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if self.entry.itemID and not self.entry.alwaysKnown and GameTooltip.SetToyByItemID then
            GameTooltip:SetToyByItemID(self.entry.itemID)
        elseif self.entry.itemID then
            GameTooltip:SetItemByID(self.entry.itemID)
        elseif self.entry.spellID then
            GameTooltip:SetSpellByID(self.entry.spellID)
        end
        if not IsKnown(self.entry) then
            GameTooltip:AddLine("Not learned or not owned.", 1, 0.25, 0.25)
        end
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
    button:SetScript("PostClick", function(self, mouseButton)
        if mouseButton == "LeftButton" and self.entry and IsKnown(self.entry) then
            RememberRecent(self.entry)
        end
    end)
    buttons[index] = button
    return button
end

local function GetFavoriteButton(index)
    if favoriteButtons[index] then return favoriteButtons[index] end
    local button = CreateFrame("Button", nil, scrollChild, "BackdropTemplate")
    button:SetSize(44, 18)
    button:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    button.text = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    button.text:SetPoint("CENTER")
    button:SetScript("OnClick", function(self)
        if not self.entry then return end
        SetFavorite(self.entry, not IsFavorite(self.entry))
        Hub:Render()
    end)
    button:SetScript("OnEnter", function(self)
        if not GameTooltip then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(IsFavorite(self.entry) and "Remove Favorite" or "Add Favorite", 0.85, 0.66, 0.30)
        GameTooltip:AddLine("Pins this teleport near the top of the hub.", 1, 1, 1)
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
    favoriteButtons[index] = button
    return button
end

local function GetEmptyLabel(index)
    if emptyLabels[index] then return emptyLabels[index] end
    local label = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    label:SetJustifyH("LEFT")
    label:SetTextColor(0.74, 0.66, 0.82)
    emptyLabels[index] = label
    return label
end

local function InitButtonPool()
    for i = 1, POOL_SIZE do
        GetButton(i)
    end
end

local function LayoutButton(button, entry, x, y, width)
    button:SetParent(scrollChild)
    button:ClearAllPoints()
    button.icon:ClearAllPoints()
    button.label:ClearAllPoints()

    if IsListView() then
        button:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        button:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, y)
        button.icon:SetAllPoints(button)
        button.label:SetPoint("LEFT", button.icon, "RIGHT", 8, 0)
        button.label:SetWidth(width - BUTTON_SIZE - 72)
        button.label:SetJustifyH("LEFT")
        button.label:SetMaxLines(1)
    else
        button:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        button:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, y)
        button.icon:SetAllPoints(button)
        button.label:SetPoint("TOP", button, "BOTTOM", 0, -2)
        button.label:SetJustifyH("CENTER")
        button.label:SetMaxLines(3)
        button.label:SetWidth(math.max(112, width - 14))
    end

    button.icon:SetTexture(EntryIcon(entry) or "Interface\\Icons\\INV_Misc_QuestionMark")
    button:SetNormalTexture(EntryIcon(entry) or "Interface\\Icons\\INV_Misc_QuestionMark")
    button.icon:SetDesaturated(not IsKnown(entry))
    button.label:SetText((IsListView() or IsMenuView()) and EntryName(entry) or EntryGridName(entry))
    button.label:SetTextColor(IsKnown(entry) and 1 or 0.55, IsKnown(entry) and 0.82 or 0.55, IsKnown(entry) and 0.35 or 0.55)
end

local function GetSelectedEntry(entriesByKey)
    if not SinsToolkitDB then return nil end
    local key = SinsToolkitDB.teleportHubSelected
    if key and key ~= "" and entriesByKey and entriesByKey[key] then
        return entriesByKey[key]
    end
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            entry.category = category
            if IsKnown(entry) then
                SinsToolkitDB.teleportHubSelected = EntryKey(entry) or ""
                return entry
            end
        end
    end
end

local function SetSelectedEntry(entry)
    if not SinsToolkitDB or not entry then return end
    SinsToolkitDB.teleportHubSelected = EntryKey(entry) or ""
end

local function BuildVisibleEntries()
    local _, classToken = UnitClass("player")
    local out = {}
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            entry.category = category
            if CanShowEntry(entry, classToken) then
                out[#out + 1] = entry
            end
        end
    end
    table.sort(out, function(a, b)
        local ac, bc = a.category or "", b.category or ""
        if ac ~= bc then
            local ai, bi = 999, 999
            for i, key in ipairs(CATEGORY_ORDER) do
                if key == ac then ai = i end
                if key == bc then bi = i end
            end
            if ai ~= bi then return ai < bi end
        end
        return (EntryName(a) or "") < (EntryName(b) or "")
    end)
    return out
end

local function BuildSelectorGroups()
    local visible = BuildVisibleEntries()
    local visibleByKey = {}
    local used = {}
    for _, entry in ipairs(visible) do
        local key = EntryKey(entry)
        if key then visibleByKey[key] = entry end
    end

    local groups = {}
    local favoriteEntries = {}
    if SinsToolkitDB and SinsToolkitDB.teleportHubFavorites then
        for key in pairs(SinsToolkitDB.teleportHubFavorites) do
            local entry = visibleByKey[key]
            if entry then
                favoriteEntries[#favoriteEntries + 1] = entry
                used[key] = true
            end
        end
    end
    table.sort(favoriteEntries, function(a, b) return (EntryName(a) or "") < (EntryName(b) or "") end)
    groups[#groups + 1] = { title = "Favorites", entries = favoriteEntries, always = true }

    local recentEntries = {}
    if SinsToolkitDB and SinsToolkitDB.teleportHubRecent then
        for _, key in ipairs(SinsToolkitDB.teleportHubRecent) do
            local entry = visibleByKey[key]
            if entry and not used[key] then
                recentEntries[#recentEntries + 1] = entry
                used[key] = true
            end
        end
    end
    if #recentEntries > 0 then
        groups[#groups + 1] = { title = "Recently Used", entries = recentEntries }
    end

    for _, category in ipairs(CATEGORY_ORDER) do
        local categoryEntries = {}
        for _, entry in ipairs(visible) do
            local key = EntryKey(entry)
            if entry.category == category and (not key or not used[key]) then
                categoryEntries[#categoryEntries + 1] = entry
                if key then used[key] = true end
            end
        end
        if #categoryEntries > 0 then
            groups[#groups + 1] = { title = CATEGORY_NAMES[category] or category or "Teleports", entries = categoryEntries }
        end
    end

    return groups
end

local function RefreshSelectorButton(entry)
    if not selectorButton then return end
    if entry then
        selectorButton:SetText((EntryName(entry) or "Select teleport") .. " - " .. EntryDestination(entry))
    else
        selectorButton:SetText("Select a teleport")
    end
end

local function HideSelectorMenu()
    if selectorMenu then selectorMenu:Hide() end
end

local function ApplyPanelTheme()
    if not panel then return end
    local theme = Theme()
    if theme.blizzard then
        panel:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
            tile = true,
            tileSize = 32,
            edgeSize = 32,
            insets = { left = 11, right = 12, top = 12, bottom = 11 },
        })
        panel:SetBackdropColor(1, 1, 1, 1)
        panel:SetBackdropBorderColor(1, 1, 1, 1)
    else
        panel:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        panel:SetBackdropColor(theme.bgR, theme.bgG, theme.bgB, theme.bgA)
        panel:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, theme.borderA)
    end
end

local function GetSelectorRow(index)
    if selectorRows[index] then return selectorRows[index] end
    local row = CreateFrame("Button", nil, selectorChild, "BackdropTemplate")
    row:SetHeight(28)
    row:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(22, 22)
    row.icon:SetPoint("LEFT", 4, 0)
    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.text:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
    row.text:SetPoint("RIGHT", -8, 0)
    row.text:SetJustifyH("LEFT")
    row.text:SetMaxLines(1)
    row:SetScript("OnClick", function(self)
        if not self.entry then return end
        SetSelectedEntry(self.entry)
        HideSelectorMenu()
        Hub:Render()
    end)
    selectorRows[index] = row
    return row
end

local function EnsureSelectorMenu()
    if selectorMenu then return end
    selectorMenu = CreateFrame("Frame", addonName .. "TeleportHubSelectorMenu", contentFrame, "BackdropTemplate")
    selectorMenu:SetFrameStrata("FULLSCREEN_DIALOG")
    selectorMenu:SetFrameLevel((contentFrame:GetFrameLevel() or 1) + 30)
    selectorMenu:SetSize(420, 300)
    selectorMenu:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    selectorMenu:Hide()
    selectorScroll = CreateFrame("ScrollFrame", nil, selectorMenu, "UIPanelScrollFrameTemplate")
    selectorScroll:SetPoint("TOPLEFT", 6, -6)
    selectorScroll:SetPoint("BOTTOMRIGHT", -26, 6)
    selectorChild = CreateFrame("Frame", nil, selectorScroll)
    selectorChild:SetSize(380, 1)
    selectorScroll:SetScrollChild(selectorChild)
end

local function PopulateSelectorMenu()
    EnsureSelectorMenu()
    local theme = Theme()
    selectorMenu:SetBackdropColor(theme.bgR, theme.bgG, theme.bgB, 0.97)
    selectorMenu:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, 0.95)
    for _, row in ipairs(selectorRows) do
        row:Hide()
        row.entry = nil
    end

    local groups = BuildSelectorGroups()
    local y = -4
    local rowIndex = 1
    local width = math.max(280, (selectorScroll:GetWidth() or 380) - 8)
    for _, group in ipairs(groups) do
        local header = GetSelectorRow(rowIndex)
        rowIndex = rowIndex + 1
        header.entry = nil
        header:ClearAllPoints()
        header:SetPoint("TOPLEFT", selectorChild, "TOPLEFT", 4, y)
        header:SetWidth(width)
        header:SetBackdropColor(0, 0, 0, 0)
        header:SetBackdropBorderColor(0, 0, 0, 0)
        header.icon:SetTexture(nil)
        header.text:SetText("|cff" .. theme.accentHex .. group.title .. " (" .. #group.entries .. ")|r")
        header:Show()
        y = y - 24

        if #group.entries == 0 and group.always then
            local empty = GetSelectorRow(rowIndex)
            rowIndex = rowIndex + 1
            empty.entry = nil
            empty:ClearAllPoints()
            empty:SetPoint("TOPLEFT", selectorChild, "TOPLEFT", 4, y)
            empty:SetWidth(width)
            empty:SetBackdropColor(0.03, 0.03, 0.035, 0.45)
            empty:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, 0.12)
            empty.icon:SetTexture("Interface\\Common\\FavoritesIcon")
            empty.icon:SetDesaturated(true)
            empty.text:SetText("|cffaaaaaaAdd Favorite on any teleport to pin it here.|r")
            empty:Show()
            y = y - 30
        end

        for _, entry in ipairs(group.entries) do
            local row = GetSelectorRow(rowIndex)
            rowIndex = rowIndex + 1
            row.entry = entry
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", selectorChild, "TOPLEFT", 4, y)
            row:SetWidth(width)
            row:SetBackdropColor(0.03, 0.03, 0.035, 0.75)
            row:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, IsKnown(entry) and 0.32 or 0.12)
            row.icon:SetTexture(EntryIcon(entry) or "Interface\\Icons\\INV_Misc_QuestionMark")
            row.icon:SetDesaturated(not IsKnown(entry))
            row.text:SetText((EntryName(entry) or "Unknown") .. " |cffaaaaaa- " .. EntryDestination(entry) .. "|r")
            row:Show()
            y = y - 30
        end
    end
    selectorChild:SetHeight(math.max(1, -y + 8))
end

local function ToggleSelectorMenu()
    EnsureSelectorMenu()
    if selectorMenu:IsShown() then
        selectorMenu:Hide()
        return
    end
    PopulateSelectorMenu()
    selectorMenu:ClearAllPoints()
    selectorMenu:SetPoint("TOPLEFT", selectorButton, "BOTTOMLEFT", 0, -4)
    selectorMenu:SetPoint("TOPRIGHT", selectorButton, "BOTTOMRIGHT", 0, -4)
    selectorMenu:Show()
end

local function RefreshSelectedAction(entry)
    if not selectedUseButton then return end
    ClearButton(selectedUseButton)
    selectedUseButton.entry = entry
    if entry then AssignButton(selectedUseButton, entry) end
    selectedIcon:SetTexture(entry and (EntryIcon(entry) or "Interface\\Icons\\INV_Misc_QuestionMark") or "Interface\\Icons\\INV_Misc_QuestionMark")
    selectedIcon:SetDesaturated(entry and not IsKnown(entry))
    selectedName:SetText(entry and EntryName(entry) or "No teleport selected")
    selectedMeta:SetText(entry and EntryDestination(entry) or "Choose a teleport from the dropdown.")
    selectedStatus:SetText(entry and (IsKnown(entry) and "|cff00ff00Available|r" or "|cffff4040Not learned or not owned|r") or "")
    if selectedFavoriteButton then
        selectedFavoriteButton.entry = entry
        selectedFavoriteButton:SetText(entry and (IsFavorite(entry) and "Favorite" or "Add Favorite") or "Add Favorite")
        if entry then selectedFavoriteButton:Enable() else selectedFavoriteButton:Disable() end
    end
end

local function GetHeader(index)
    if headers[index] then return headers[index] end
    local header = CreateFrame("Button", nil, scrollChild)
    header:SetHeight(22)
    header.text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    header.text:SetPoint("LEFT", 2, 0)
    header.text:SetTextColor(0.85, 0.66, 0.30)
    header:SetScript("OnClick", function(self)
        if not self.category or self.virtual then return end
        SinsToolkitDB.teleportHubCollapsed = SinsToolkitDB.teleportHubCollapsed or {}
        SinsToolkitDB.teleportHubCollapsed[self.category] = not SinsToolkitDB.teleportHubCollapsed[self.category] or nil
        Hub:Render()
    end)
    header:SetScript("OnEnter", function(self)
        if self.virtual or not GameTooltip then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Collapse or expand this section", 0.85, 0.66, 0.30)
        GameTooltip:AddLine("Expansion state is remembered.", 1, 1, 1)
        GameTooltip:Show()
    end)
    header:SetScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)
    headers[index] = header
    return header
end

local function CreateLegacyWindow()
    if panel then return end
    panel = CreateFrame("Frame", addonName .. "TeleportHubPanel", UIParent, "BackdropTemplate")
    local window = SinsToolkitDB and SinsToolkitDB.teleportHubWindow or {}
    panel:SetSize(window.width or 760, window.height or 620)
    panel:SetPoint(window.point or "CENTER", UIParent, window.relativePoint or "CENTER", window.x or 0, window.y or 0)
    if panel.SetResizeBounds then
        panel:SetResizeBounds(PANEL_MIN_WIDTH, PANEL_MIN_HEIGHT, 980, 780)
    else
        panel:SetMinResize(PANEL_MIN_WIDTH, PANEL_MIN_HEIGHT)
        panel:SetMaxResize(980, 780)
    end
    panel:SetFrameStrata("FULLSCREEN_DIALOG")
    panel:SetToplevel(true)
    panel:SetClampedToScreen(true)
    panel:EnableMouse(true)
    panel:SetMovable(true)
    panel:SetResizable(true)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", panel.StartMoving)
    panel:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        Hub:SaveWindow()
    end)
    ApplyPanelTheme()
    panel:Hide()

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -14)
    title:SetText("|cffb868dcSin's Toolkit: QOL|r |cffe8b4ecTeleport Hub|r")

    local close = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -4, -4)

    local resize = CreateFrame("Button", nil, panel)
    resize:SetSize(16, 16)
    resize:SetPoint("BOTTOMRIGHT", -4, 4)
    resize:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    resize:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
    resize:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
    resize:SetScript("OnMouseDown", function()
        panel:StartSizing("BOTTOMRIGHT")
    end)
    resize:SetScript("OnMouseUp", function()
        panel:StopMovingOrSizing()
        Hub:SaveWindow()
        Hub:Render()
    end)
end

local function BringPanelToFront()
    if not panel then return end
    panel:SetFrameStrata("FULLSCREEN_DIALOG")
    panel:SetToplevel(true)
    if panel.Raise then
        panel:Raise()
    end
end

function Hub:CreateContent(parent)
    isEmbedded = parent ~= panel
    if contentFrame then
        contentFrame:SetParent(parent)
        contentFrame:ClearAllPoints()
        if parent == panel then
            contentFrame:SetPoint("TOPLEFT", 6, -38)
            contentFrame:SetPoint("BOTTOMRIGHT", -6, 16)
        else
            contentFrame:SetAllPoints()
        end
        return
    end

    contentFrame = CreateFrame("Frame", nil, parent)
    if parent == panel then
        contentFrame:SetPoint("TOPLEFT", 6, -38)
        contentFrame:SetPoint("BOTTOMRIGHT", -6, 16)
    else
        contentFrame:SetAllPoints()
    end

    searchBox = CreateFrame("EditBox", nil, contentFrame, "InputBoxTemplate")
    searchBox:SetSize(300, 24)
    searchBox:SetPoint("TOPRIGHT", -8, -8)
    searchBox:SetAutoFocus(false)
    searchBox:SetText(searchText or "")
    searchBox:SetScript("OnTextChanged", function(self)
        searchText = (self:GetText() or ""):lower()
        Hub:Render()
    end)

    viewButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    viewButton:SetSize(62, 24)
    viewButton:SetPoint("RIGHT", searchBox, "LEFT", -12, 0)
    viewButton:SetScript("OnClick", function()
        if not SinsToolkitDB then return end
        local current = SinsToolkitDB.teleportHubView or "menu"
        SinsToolkitDB.teleportHubView = current == "menu" and "grid" or (current == "grid" and "list" or "menu")
        Hub:Render()
    end)

    rescanButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    rescanButton:SetSize(76, 24)
    rescanButton:SetPoint("RIGHT", viewButton, "LEFT", -8, 0)
    rescanButton:SetText("Rescan")
    rescanButton:SetScript("OnClick", function()
        Hub:Scan(true)
        Hub:Render()
    end)

    expandAllButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    expandAllButton:SetSize(82, 24)
    expandAllButton:SetPoint("RIGHT", rescanButton, "LEFT", -8, 0)
    expandAllButton:SetText("Expand")
    expandAllButton:SetScript("OnClick", function()
        SetAllCollapsed(false)
        Hub:Render()
    end)

    collapseAllButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    collapseAllButton:SetSize(82, 24)
    collapseAllButton:SetPoint("RIGHT", expandAllButton, "LEFT", -8, 0)
    collapseAllButton:SetText("Collapse")
    collapseAllButton:SetScript("OnClick", function()
        SetAllCollapsed(true)
        Hub:Render()
    end)

    scrollFrame = CreateFrame("ScrollFrame", nil, contentFrame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 10, -42)
    scrollFrame:SetPoint("BOTTOMRIGHT", -24, 0)
    scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(math.max(1, scrollFrame:GetWidth()), 1)
    scrollFrame:SetScrollChild(scrollChild)

    contentFrame:SetScript("OnSizeChanged", function()
        if scrollChild and scrollFrame then
            scrollChild:SetWidth(math.max(1, scrollFrame:GetWidth()))
        end
        if contentFrame:IsVisible() then Hub:Render() end
    end)

    selectorButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    selectorButton:SetHeight(26)
    selectorButton:SetPoint("TOPLEFT", 12, -42)
    selectorButton:SetPoint("TOPRIGHT", -28, -42)
    selectorButton:SetScript("OnClick", ToggleSelectorMenu)

    selectedUseButton = CreateFrame("Button", addonName .. "TeleportHubSelectedUseButton", contentFrame, "SecureActionButtonTemplate,UIPanelButtonTemplate")
    selectedUseButton:SetSize(120, 28)
    selectedUseButton:SetText("Use")
    selectedUseButton:RegisterForClicks("AnyUp", "AnyDown")
    selectedUseButton:SetScript("PostClick", function(self, mouseButton)
        if mouseButton == "LeftButton" and self.entry and IsKnown(self.entry) then
            RememberRecent(self.entry)
            Hub:Render()
        end
    end)

    selectedIcon = contentFrame:CreateTexture(nil, "ARTWORK")
    selectedIcon:SetSize(44, 44)
    selectedName = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    selectedName:SetJustifyH("LEFT")
    selectedMeta = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    selectedMeta:SetJustifyH("LEFT")
    selectedStatus = contentFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    selectedStatus:SetJustifyH("LEFT")
    selectedFavoriteButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    selectedFavoriteButton:SetSize(116, 24)
    selectedFavoriteButton:SetScript("OnClick", function(self)
        if not self.entry then return end
        SetFavorite(self.entry, not IsFavorite(self.entry))
        Hub:Render()
    end)
end

function Hub:SaveWindow()
    if not panel or not SinsToolkitDB then return end
    SinsToolkitDB.teleportHubWindow = SinsToolkitDB.teleportHubWindow or {}
    local point, _, relativePoint, x, y = panel:GetPoint(1)
    SinsToolkitDB.teleportHubWindow.width = panel:GetWidth()
    SinsToolkitDB.teleportHubWindow.height = panel:GetHeight()
    SinsToolkitDB.teleportHubWindow.point = point or "CENTER"
    SinsToolkitDB.teleportHubWindow.relativePoint = relativePoint or point or "CENTER"
    SinsToolkitDB.teleportHubWindow.x = x or 0
    SinsToolkitDB.teleportHubWindow.y = y or 0
end

function Hub:Render()
    if not contentFrame or not scrollChild or InCombatLockdown() then return end
    ApplyPanelTheme()
    local view = (SinsToolkitDB and SinsToolkitDB.teleportHubView) or "menu"
    if viewButton then
        viewButton:SetText(view == "menu" and "Grid" or (view == "grid" and "List" or "Menu"))
        if isEmbedded then viewButton:Hide() else viewButton:Show() end
    end
    for _, button in ipairs(buttons) do
        button:Hide()
        button:SetParent(nil)
        button.entry = nil
        ClearButton(button)
    end
    for _, button in ipairs(favoriteButtons) do
        button:Hide()
        button.entry = nil
    end
    for _, label in ipairs(emptyLabels) do
        label:Hide()
    end
    for _, header in ipairs(headers) do
        header:Hide()
        header.category = nil
        header.virtual = nil
    end

    local _, classToken = UnitClass("player")
    local x, y = 10, -8
    local column = 0
    local index = 1
    local favoriteIndex = 1
    local headerIndex = 1
    local emptyLabelIndex = 1
    local width = math.max(1, scrollFrame:GetWidth() - 18)
    scrollChild:SetWidth(width)
    local columns = math.max(1, math.floor(width / CELL_WIDTH))
    local cellWidth = math.max(132, math.floor(width / columns))
    local listView = IsListView()
    local menuView = IsMenuView()
    local entriesByKey = GetAllEntriesByKey()
    local selectedEntry = GetSelectedEntry(entriesByKey)
    if searchBox then
        if isEmbedded then searchBox:Hide() else searchBox:Show() end
    end
    if collapseAllButton then
        if isEmbedded then collapseAllButton:Hide() else collapseAllButton:Show() end
    end
    if expandAllButton then
        if isEmbedded then expandAllButton:Hide() else expandAllButton:Show() end
    end
    if rescanButton then
        if isEmbedded then rescanButton:Hide() else rescanButton:Show() end
    end
    if scrollFrame then
        scrollFrame:ClearAllPoints()
        scrollFrame:SetPoint("TOPLEFT", 10, menuView and (isEmbedded and -92 or -78) or (isEmbedded and -8 or -42))
        scrollFrame:SetPoint("BOTTOMRIGHT", -24, 0)
    end
    RefreshSelectorButton(selectedEntry)
    RefreshSelectedAction(selectedEntry)
    if selectorMenu and selectorMenu:IsShown() then PopulateSelectorMenu() end

    if selectorButton then
        if menuView and not isEmbedded then selectorButton:Show() else selectorButton:Hide(); HideSelectorMenu() end
    end
    if selectedUseButton then
        if menuView then
            selectedIcon:ClearAllPoints()
            selectedIcon:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 14, -12)
            selectedName:ClearAllPoints()
            selectedName:SetPoint("TOPLEFT", selectedIcon, "TOPRIGHT", 12, -2)
            selectedName:SetWidth(width - 210)
            selectedMeta:ClearAllPoints()
            selectedMeta:SetPoint("TOPLEFT", selectedName, "BOTTOMLEFT", 0, -2)
            selectedMeta:SetWidth(width - 210)
            selectedStatus:ClearAllPoints()
            selectedStatus:SetPoint("TOPLEFT", selectedMeta, "BOTTOMLEFT", 0, -2)
            selectedUseButton:ClearAllPoints()
            selectedUseButton:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", width - 12, -14)
            selectedFavoriteButton:ClearAllPoints()
            selectedFavoriteButton:SetPoint("TOP", selectedUseButton, "BOTTOM", 0, -8)
            selectedIcon:Show()
            selectedName:Show()
            selectedMeta:Show()
            selectedStatus:Show()
            selectedUseButton:Show()
            selectedFavoriteButton:Show()
            y = isEmbedded and -92 or -84
        else
            selectedIcon:Hide()
            selectedName:Hide()
            selectedMeta:Hide()
            selectedStatus:Hide()
            selectedUseButton:Hide()
            selectedFavoriteButton:Hide()
        end
    end

    local function AddSection(category, name, entries, virtual, showWhenEmpty)
        if #entries == 0 and not showWhenEmpty then return end

        local collapsed = not virtual and IsCollapsed(category)
        local header = GetHeader(headerIndex)
        headerIndex = headerIndex + 1
        header:ClearAllPoints()
        header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 4, y)
        header:SetWidth(width - 8)
        header.category = category
        header.virtual = virtual
        header.text:SetText((virtual and "" or (collapsed and "+ " or "- ")) .. name .. " (" .. #entries .. ")")
        header:Show()
        y = y - 26
        if #entries == 0 and showWhenEmpty then
            local label = GetEmptyLabel(emptyLabelIndex)
            emptyLabelIndex = emptyLabelIndex + 1
            label:ClearAllPoints()
            label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 14, y)
            label:SetWidth(width - 28)
            label:SetText("Use Add Favorite on any teleport to pin it here.")
            label:Show()
            y = y - 24
            return
        end
        if collapsed or #entries == 0 then
            y = y - 6
            return
        end

        column = 0
        for _, entry in ipairs(entries) do
            local button = GetButton(index)
            index = index + 1
            button.entry = entry
            AssignButton(button, entry)
            local rowY = y
            if listView or menuView then
                LayoutButton(button, entry, x + 4, y, width - 42)
                y = y - LIST_ROW_HEIGHT
            else
                LayoutButton(button, entry, x + (column * cellWidth) + ((cellWidth - BUTTON_SIZE) / 2), y, cellWidth)
                column = column + 1
            end
            button:Show()

            local favButton = GetFavoriteButton(favoriteIndex)
            favoriteIndex = favoriteIndex + 1
            favButton.entry = entry
            local isFavorite = IsFavorite(entry)
            favButton.text:SetText((listView or menuView) and (isFavorite and "Fav" or "+ Fav") or (isFavorite and "★" or "+"))
            favButton.text:SetTextColor(IsFavorite(entry) and 1 or 0.55, IsFavorite(entry) and 0.82 or 0.55, IsFavorite(entry) and 0.35 or 0.55)
            favButton:SetBackdropColor(isFavorite and 0.20 or 0.05, isFavorite and 0.11 or 0.04, isFavorite and 0.26 or 0.07, 0.92)
            favButton:SetBackdropBorderColor(isFavorite and 0.85 or 0.46, isFavorite and 0.66 or 0.28, isFavorite and 0.30 or 0.58, 0.95)
            favButton:SetParent(scrollChild)
            favButton:SetFrameLevel(button:GetFrameLevel() + 8)
            favButton:ClearAllPoints()
            if listView or menuView then
                favButton:SetSize(44, 18)
                favButton:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", width - 8, rowY - 10)
            else
                favButton:SetSize(22, 18)
                favButton:SetPoint("TOPRIGHT", button, "TOPRIGHT", 9, 8)
            end
            favButton:Show()

            if not (listView or menuView) and column >= columns then
                column = 0
                y = y - CELL_HEIGHT
            end
        end
        if not (listView or menuView) and column ~= 0 then y = y - CELL_HEIGHT end
        y = y - 10
    end

    local favorites = {}
    if SinsToolkitDB and SinsToolkitDB.teleportHubFavorites then
        for key, enabled in pairs(SinsToolkitDB.teleportHubFavorites) do
            local entry = enabled and entriesByKey[key]
            if entry and CanShowEntry(entry, classToken) then favorites[#favorites + 1] = entry end
        end
        table.sort(favorites, function(a, b) return EntryName(a) < EntryName(b) end)
    end
    AddSection("favorites", "Favorites", favorites, true, true)

    local recent = {}
    if SinsToolkitDB and SinsToolkitDB.teleportHubRecent then
        for _, key in ipairs(SinsToolkitDB.teleportHubRecent) do
            local entry = entriesByKey[key]
            if entry and CanShowEntry(entry, classToken) then recent[#recent + 1] = entry end
        end
    end
    AddSection("recent", "Recently Used", recent, true)

    for _, category in ipairs(CATEGORY_ORDER) do
        local entries = TELEPORTS[category]
        local shown = {}
        for _, entry in ipairs(entries or {}) do
            entry.category = category
            if CanShowEntry(entry, classToken) then shown[#shown + 1] = entry end
        end

        AddSection(category, CATEGORY_NAMES[category] or category, shown, false)
    end

    scrollChild:SetHeight(math.max(1, -y + 24))
end

function Hub:ShowLegacyWindow()
    if not IsEnabled() then
        Print("Teleport Hub is disabled.")
        return
    end
    if InCombatLockdown and InCombatLockdown() then
        Print("Teleport Hub cannot open in combat.")
        return
    end
    if not initialized then self:Init() end
    CreateLegacyWindow()
    self:CreateContent(panel)
    panel:Show()
    BringPanelToFront()
    contentFrame:Show()
    self:Render()
end

function Hub:HideLegacyWindow()
    if panel and panel:IsShown() and not InCombatLockdown() then
        panel:Hide()
    end
end

function Hub:RenderEmbedded(parent)
    if not initialized then self:Init() end
    self:CreateContent(parent)
    contentFrame:Show()
    self:Render()
end

function Hub:Open()
    if not IsEnabled() then
        Print("Teleport Hub is disabled.")
        return
    end
    if InCombatLockdown and InCombatLockdown() then
        Print("Teleport Hub cannot open in combat.")
        return
    end
    if not initialized then self:Init() end
    self:ShowLegacyWindow()
end

function Hub:Toggle()
    if panel and panel:IsShown() then
        self:HideLegacyWindow()
    else
        self:ShowLegacyWindow()
    end
end

function Hub:Scan()
    local known, total = 0, 0
    local _, classToken = UnitClass("player")
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            if not entry.class or entry.class == classToken then
                total = total + 1
                if IsKnown(entry) then known = known + 1 end
            end
        end
    end
    Print(("Teleport Hub found %d usable entries out of %d tracked entries for this character."):format(known, total))
    if contentFrame and contentFrame:IsVisible() and not InCombatLockdown() then
        Hub:Render()
    end
end

local function UpdateMinimapButton()
    if not minimapButton then return end
    if SinsToolkitDB and SinsToolkitDB.teleportHubEnabled and SinsToolkitDB.teleportHubMinimap then
        minimapButton:Show()
    else
        minimapButton:Hide()
    end
end

local function CreateMinimapButton()
    minimapButton = CreateFrame("Button", addonName .. "TeleportHubMinimapButton", Minimap)
    minimapButton:SetSize(31, 31)
    minimapButton:SetFrameStrata("MEDIUM")
    minimapButton:SetPoint("TOPLEFT", Minimap, "TOPLEFT", 0, 0)
    minimapButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    minimapButton:RegisterForDrag("LeftButton")
    minimapButton.icon = minimapButton:CreateTexture(nil, "BACKGROUND")
    minimapButton.icon:SetTexture("Interface\\AddOns\\SinsToolkit\\Media\\Logo.tga")
    minimapButton.icon:SetSize(22, 22)
    minimapButton.icon:SetPoint("CENTER")
    minimapButton.border = minimapButton:CreateTexture(nil, "OVERLAY")
    minimapButton.border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    minimapButton.border:SetSize(54, 54)
    minimapButton.border:SetPoint("TOPLEFT")
    minimapButton:SetScript("OnClick", function(_, button)
        if button == "RightButton" then
            SinsToolkitDB.teleportHubMinimap = false
            UpdateMinimapButton()
            Print("Teleport Hub minimap button hidden. Use /stktp minimap to show it again.")
        else
            Hub:Toggle()
        end
    end)
    minimapButton:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:AddLine("Sin's Toolkit: QOL Teleport Hub", 0.74, 0.45, 0.95)
        GameTooltip:AddLine("Left-click: open teleport panel", 1, 1, 1)
        GameTooltip:AddLine("Right-click: hide minimap button", 1, 1, 1)
        GameTooltip:AddLine("/stktp minimap shows it again", 0.85, 0.66, 0.30)
        GameTooltip:Show()
    end)
    minimapButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
end

function Hub:Init()
    if initialized then return end
    initialized = true
    InitButtonPool()
    CreateMinimapButton()
    UpdateMinimapButton()
end

function SinsToolkit.ApplyTeleportHub()
    if not initialized then Hub:Init() end
    UpdateMinimapButton()
    if contentFrame and contentFrame:IsVisible() and not IsEnabled() and not InCombatLockdown() then
        if panel and panel:IsShown() then panel:Hide() end
        contentFrame:Hide()
    elseif contentFrame and contentFrame:IsVisible() and not InCombatLockdown() then
        Hub:Render()
    end
end

local function ShowHelp()
    Print("commands:")
    print("  |cffffff00/stktp|r - open or close the Teleport Hub")
    print("  |cffffff00/stktp on|r / |cffffff00off|r - enable or disable Teleport Hub")
    print("  |cffffff00/stktp minimap|r - show or hide the minimap button")
    print("  |cffffff00/stktp scan|r - print known teleport count")
    print("  |cffffff00/stktp locked|r - show or hide locked teleports")
    print("  |cffffff00/stktp view|r - switch between menu, grid, and list view")
    print("  |cffffff00/stktp menu|r / |cffffff00grid|r / |cffffff00list|r - choose a Teleport Hub view")
    print("  |cffffff00/stktp expand|r / |cffffff00collapse|r - expand or collapse teleport sections")
end

SLASH_SINSTOOLKITTELEPORTHUB1 = "/stktp"
SLASH_SINSTOOLKITTELEPORTHUB2 = "/stkteleport"
SlashCmdList.SINSTOOLKITTELEPORTHUB = function(msg)
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
    msg = (msg or ""):lower():match("^%s*(.-)%s*$")

    if msg == "" or msg == "toggle" then
        Hub:Toggle()
    elseif msg == "open" or msg == "show" then
        Hub:Open()
    elseif msg == "close" or msg == "hide" then
        Hub:HideLegacyWindow()
    elseif msg == "on" or msg == "enable" then
        SinsToolkitDB.teleportHubEnabled = true
        SinsToolkit.ApplyTeleportHub()
        Print("Teleport Hub enabled.")
    elseif msg == "off" or msg == "disable" then
        SinsToolkitDB.teleportHubEnabled = false
        SinsToolkit.ApplyTeleportHub()
        Print("Teleport Hub disabled.")
    elseif msg == "minimap" then
        SinsToolkitDB.teleportHubMinimap = not SinsToolkitDB.teleportHubMinimap
        UpdateMinimapButton()
        Print("Teleport Hub minimap button " .. (SinsToolkitDB.teleportHubMinimap and "|cff00ff00shown|r." or "|cffff4040hidden|r."))
    elseif msg == "scan" then
        Hub:Scan()
    elseif msg == "locked" then
        SinsToolkitDB.teleportHubShowLocked = not SinsToolkitDB.teleportHubShowLocked
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
        Print("Locked teleports " .. (SinsToolkitDB.teleportHubShowLocked and "|cff00ff00shown|r." or "|cffff4040hidden|r."))
    elseif msg == "view" then
        local current = SinsToolkitDB.teleportHubView or "menu"
        SinsToolkitDB.teleportHubView = current == "menu" and "grid" or (current == "grid" and "list" or "menu")
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
        Print("Teleport Hub view set to |cffffff00" .. SinsToolkitDB.teleportHubView .. "|r.")
    elseif msg == "grid" or msg == "list" or msg == "menu" then
        SinsToolkitDB.teleportHubView = msg
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
        Print("Teleport Hub view set to |cffffff00" .. SinsToolkitDB.teleportHubView .. "|r.")
    elseif msg == "expand" or msg == "expandall" then
        SetAllCollapsed(false)
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
        Print("Teleport Hub sections expanded.")
    elseif msg == "collapse" or msg == "collapseall" then
        SetAllCollapsed(true)
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
        Print("Teleport Hub sections collapsed.")
    else
        ShowHelp()
    end
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("SPELLS_CHANGED")
eventFrame:RegisterEvent("NEW_TOY_ADDED")
eventFrame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_LOGIN" then
        if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
        Hub:Init()
    elseif contentFrame and contentFrame:IsVisible() and not InCombatLockdown() then
        Hub:Render()
    end
end)
