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 = 276371, label = "Lightveil Recall Beacon" },
        { 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 = 221966, label = "Wormhole Generator: Khaz Algar" },
        { itemID = 248485, label = "Wormhole Generator: Quel'Thalas" },
        { 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, clearFavoritesButton, clearRecentButton
local contentFrame
local isEmbedded = false
local buttons = {}
local favoriteButtons = {}
local selectorRows = {}
local headers = {}
local emptyLabels = {}
local searchText = ""
local initialized = false
local visibleCooldowns = false
local cooldownRefreshElapsed = 0
local BUTTON_SIZE = 44
local CELL_WIDTH = 168
local CELL_HEIGHT = 72
local PANEL_MIN_WIDTH = 540
local PANEL_MIN_HEIGHT = 380
local LIST_ROW_HEIGHT = 58
local POOL_SIZE = 180
local RECENT_LIMIT = 8
local FALLBACK_TELEPORT_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
local PREFERRED_HEARTHSTONE_RANDOM = "random"
local VIRTUAL_HEARTHSTONE_KEY = "virtual:preferred-hearthstone"
local DESTINATION_HEARTHSTONE_ITEM_IDS = {
    [110560] = true, -- Garrison Hearthstone
    [140192] = true, -- Dalaran Hearthstone
}
local UTILITY_RECALL_ITEM_IDS = {
    [276371] = true, -- Lightveil Recall Beacon
}
local HEARTHSTONE_ITEM_IDS = {
    [6948] = true,
    [64488] = true,
    [162973] = true,
    [163045] = true,
    [165669] = true,
    [165670] = true,
    [165802] = true,
    [166746] = true,
    [166747] = true,
    [168907] = true,
    [172179] = true,
    [180290] = true,
    [182773] = true,
    [184353] = true,
    [188952] = true,
    [190196] = true,
    [193588] = true,
    [200630] = true,
    [208704] = true,
    [209035] = true,
    [212337] = true,
    [212766] = true,
    [228940] = true,
    [232517] = true,
    [245970] = true,
    [246565] = true,
    [257736] = true,
    [265100] = true,
    [142542] = true,
}

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 true
end

local function IsMenuView()
    return false
end

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

local function IsHearthstoneEntry(entry)
    if not entry then return false end
    if entry.virtualHearthstone then return true end
    if entry.category == "hearth" then return true end
    return entry.itemID and HEARTHSTONE_ITEM_IDS[entry.itemID] == true
end

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

local function ShowFavorites()
    return SinsToolkitDB and SinsToolkitDB.teleportHubShowFavorites == true
end

local function ShowRecent()
    return not SinsToolkitDB or SinsToolkitDB.teleportHubShowRecent ~= false
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 GetPreferredHearthstoneLabel

local function EntryDestination(entry)
    if not entry then return "" end
    if entry.virtualHearthstone then
        return "Using: " .. GetPreferredHearthstoneLabel()
    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)
    if not ShowRecent() then return end
    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 false
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 BuildUseMacro(...)
    local lines = {}
    for index = 1, select("#", ...) do
        local line = select(index, ...)
        if line and line ~= "" then
            lines[#lines + 1] = line
        end
    end
    return table.concat(lines, "\n")
end

local function GetItemUseMacro(entry)
    local itemID = entry and entry.itemID
    if not itemID then return nil end

    if not entry.alwaysKnown and PlayerHasToy and PlayerHasToy(itemID) then
        local toyName = (C_ToyBox and C_ToyBox.GetToyInfo and select(2, C_ToyBox.GetToyInfo(itemID)))
            or (C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID))
            or entry.label
        if toyName and toyName ~= "" then
            return BuildUseMacro("/cast " .. toyName, "/use item:" .. tostring(itemID))
        end
    end

    return BuildUseMacro("/use item:" .. tostring(itemID))
end

local function GetItemCooldownRemaining(itemID)
    local start, duration, enable
    if C_Item and C_Item.GetItemCooldown then
        local info = C_Item.GetItemCooldown(itemID)
        if type(info) == "table" then
            start, duration, enable = info.startTime, info.duration, info.enable
        else
            start, duration, enable = info
        end
    elseif GetItemCooldown then
        start, duration, enable = GetItemCooldown(itemID)
    end

    if enable == 0 then return math.huge end
    start = start or 0
    duration = duration or 0
    if start <= 0 or duration <= 0 then return 0 end
    local remaining = duration - ((GetTime and GetTime()) or 0) + start
    if remaining < 0 then return 0 end
    return remaining
end

local function IsEntryOnCooldown(entry)
    return entry and entry.itemID and GetItemCooldownRemaining(entry.itemID) > 0 or false
end

local function GetEntryCooldownRemaining(entry)
    if not entry then return 0 end
    if entry.virtualHearthstone then
        return GetEntryCooldownRemaining(entry.resolvedEntry or entry.displayEntry)
    end
    if entry.itemID then
        return GetItemCooldownRemaining(entry.itemID)
    end
    if entry.spellID then
        local start, duration
        if C_Spell and C_Spell.GetSpellCooldown then
            local info = C_Spell.GetSpellCooldown(entry.spellID)
            if type(info) == "table" then
                start, duration = info.startTime, info.duration
            else
                start, duration = info
            end
        elseif GetSpellCooldown then
            start, duration = GetSpellCooldown(entry.spellID)
        end
        start = start or 0
        duration = duration or 0
        if start <= 0 or duration <= 0 then return 0 end
        local remaining = duration - ((GetTime and GetTime()) or 0) + start
        if remaining < 0 then return 0 end
        return remaining
    end
    return 0
end

local function FormatCooldownRemaining(seconds)
    if seconds == math.huge then return "Cooldown" end
    seconds = math.max(0, math.floor((seconds or 0) + 0.5))
    if seconds >= 3600 then
        return ("%dh %dm"):format(math.floor(seconds / 3600), math.floor((seconds % 3600) / 60))
    elseif seconds >= 60 then
        return ("%dm %ds"):format(math.floor(seconds / 60), seconds % 60)
    end
    return ("%ds"):format(seconds)
end

local function EntryName(entry)
    if entry.virtualHearthstone then
        return "Hearthstone"
    end
    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 (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.virtualHearthstone then
        return EntryIcon(entry.resolvedEntry or entry.displayEntry or TELEPORTS.hearth[1])
    end
    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 GetSafeTeleportIcon(entry)
    local icon = EntryIcon(entry)
    if type(icon) == "number" then return icon end
    if type(icon) == "string" and icon ~= "" then return icon end
    return FALLBACK_TELEPORT_ICON
end

local function SetButtonIconSafely(button, entry)
    local icon = GetSafeTeleportIcon(entry)
    if button.icon then
        button.icon:SetTexture(icon)
    end
    if button.GetNormalTexture then
        local normalTexture = button:GetNormalTexture()
        if normalTexture then
            normalTexture:Hide()
        end
    end
end

local function IsKnown(entry)
    if entry and entry.virtualHearthstone then
        local resolved = entry.resolvedEntry
        return (resolved and IsKnown(resolved)) or (entry.ownedCount and entry.ownedCount > 0) or false
    end
    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 GetOwnedHearthstones()
    local entries = {}
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            entry.category = category
            if IsHearthstoneEntry(entry) and not entry.virtualHearthstone and IsKnown(entry) then
                entries[#entries + 1] = entry
            end
        end
    end
    table.sort(entries, function(a, b) return (EntryName(a) or "") < (EntryName(b) or "") end)
    return entries
end

local function ResolvePreferredHearthstone()
    local owned = GetOwnedHearthstones()
    if #owned == 0 then return nil end

    local preferred = SinsToolkitDB and SinsToolkitDB.teleportHubPreferredHearthstone or PREFERRED_HEARTHSTONE_RANDOM
    if preferred and preferred ~= "" and preferred ~= PREFERRED_HEARTHSTONE_RANDOM then
        for _, entry in ipairs(owned) do
            if EntryKey(entry) == preferred then
                return entry
            end
        end
    end

    local available = {}
    for _, entry in ipairs(owned) do
        if not IsEntryOnCooldown(entry) then
            available[#available + 1] = entry
        end
    end
    if #available > 0 then
        return available[math.random(#available)]
    end
    return nil
end

local function ResolveHearthstoneDisplayEntry(owned, resolved)
    if resolved then return resolved end
    local bestEntry
    local bestRemaining = math.huge
    for _, entry in ipairs(owned or {}) do
        local remaining = GetEntryCooldownRemaining(entry)
        if remaining > 0 and remaining < bestRemaining then
            bestEntry = entry
            bestRemaining = remaining
        end
    end
    return bestEntry
end

GetPreferredHearthstoneLabel = function()
    local preferred = SinsToolkitDB and SinsToolkitDB.teleportHubPreferredHearthstone or PREFERRED_HEARTHSTONE_RANDOM
    if preferred == PREFERRED_HEARTHSTONE_RANDOM or preferred == "" then
        return "Random Available"
    end
    for _, entry in ipairs(GetOwnedHearthstones()) do
        if EntryKey(entry) == preferred then return entry.label or EntryName(entry) end
    end
    return "Random Available"
end

local function GetPreferredHearthstoneEntry()
    local resolved = ResolvePreferredHearthstone()
    local owned = GetOwnedHearthstones()
    local displayEntry = ResolveHearthstoneDisplayEntry(owned, resolved)
    return {
        virtualHearthstone = true,
        category = "hearth",
        label = "Hearthstone",
        resolvedEntry = resolved,
        displayEntry = displayEntry,
        ownedCount = #owned,
    }
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)
    button.resolvedEntry = nil
    if entry and entry.virtualHearthstone then
        entry.resolvedEntry = ResolvePreferredHearthstone()
        button.resolvedEntry = entry.resolvedEntry
        entry = entry.resolvedEntry
    end
    if not IsKnown(entry) then return end
    if entry.itemID then
        button:SetAttribute("type", "macro")
        button:SetAttribute("macrotext", GetItemUseMacro(entry))
    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,BackdropTemplate")
    button:RegisterForClicks("AnyUp", "AnyDown")
    button:SetSize(CELL_WIDTH, CELL_HEIGHT - 10)
    button:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    button.icon = button:CreateTexture(nil, "ARTWORK")
    button.icon:SetSize(32, 32)
    button.label = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    button.label:SetJustifyH("LEFT")
    button.label:SetMaxLines(2)
    button.status = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    button.status:SetJustifyH("LEFT")
    button.status:SetTextColor(0.74, 0.66, 0.82)
    button.status:SetText("Use")
    button:SetScript("OnEnter", function(self)
        if not self.entry or not GameTooltip then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if self.entry.virtualHearthstone then
            GameTooltip:AddLine("Hearthstone", 0.85, 0.66, 0.30)
            GameTooltip:AddLine("Using: " .. GetPreferredHearthstoneLabel(), 1, 1, 1)
        elseif 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("PreClick", function(self)
        if self.entry and not (InCombatLockdown and InCombatLockdown()) then
            AssignButton(self, self.entry)
        end
    end)
    button:SetScript("PostClick", function(self, mouseButton)
        local recentEntry = self.resolvedEntry or self.entry
        if mouseButton == "LeftButton" and recentEntry and IsKnown(recentEntry) and self:GetAttribute("type") then
            RememberRecent(recentEntry)
            if C_Timer then
                C_Timer.After(0.35, function()
                    if contentFrame and contentFrame:IsVisible() and not (InCombatLockdown and InCombatLockdown()) then
                        Hub:Render()
                    end
                end)
            end
        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()
    button.status:ClearAllPoints()

    button:SetSize(width, CELL_HEIGHT - 10)
    button:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, y)
    button.icon:SetSize(32, 32)
    button.icon:SetPoint("LEFT", button, "LEFT", 8, 0)
    button.label:SetPoint("LEFT", button.icon, "RIGHT", 8, 0)
    button.label:SetWidth(math.max(80, width - 76))
    button.label:SetJustifyH("LEFT")
    button.label:SetMaxLines(2)
    button.status:SetPoint("TOPLEFT", button.label, "BOTTOMLEFT", 0, -1)
    button.status:SetWidth(math.max(80, width - 76))

    local known = IsKnown(entry)
    local cooldownRemaining = GetEntryCooldownRemaining(entry)
    local onCooldown = known and cooldownRemaining > 0
    if onCooldown then visibleCooldowns = true end
    SetButtonIconSafely(button, entry)
    button.icon:SetDesaturated((not known) or onCooldown)
    button.icon:SetAlpha((not known) and 0.42 or (onCooldown and 0.58 or 1))
    button.icon:SetVertexColor(1, 1, 1, 1)
    button.label:SetText(EntryName(entry))
    button.label:SetTextColor(known and 1 or 0.55, known and (onCooldown and 0.72 or 0.82) or 0.55, known and (onCooldown and 0.20 or 0.35) or 0.55)
    if entry.virtualHearthstone then
        button.status:SetText((entry.resolvedEntry or entry.displayEntry) and (onCooldown and ("Ready in " .. FormatCooldownRemaining(cooldownRemaining)) or EntryDestination(entry)) or "Unavailable")
    else
        button.status:SetText(known and (onCooldown and ("Ready in " .. FormatCooldownRemaining(cooldownRemaining)) or "Use") or "Unavailable")
    end
    if onCooldown then
        button:SetBackdropColor(0.12, 0.08, 0.02, 0.82)
    else
        button:SetBackdropColor(known and 0.05 or 0.025, known and 0.04 or 0.025, known and 0.07 or 0.03, 0.78)
    end
    local theme = Theme()
    button:SetBackdropBorderColor(onCooldown and 0.85 or theme.accentR, onCooldown and 0.55 or theme.accentG, onCooldown and 0.20 or theme.accentB, known and 0.34 or 0.12)
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 = {}
    if ShowFavorites() then
        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 }
    end

    if ShowRecent() then
        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
    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:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, theme.borderA)
        if SinsToolkit.RegisterOpacityFrame then
            SinsToolkit.RegisterOpacityFrame(panel, theme.bgR, theme.bgG, theme.bgB, theme.accentR, theme.accentG, theme.accentB, theme.borderA)
        else
            panel:SetBackdropColor(theme.bgR, theme.bgG, theme.bgB, theme.bgA)
        end
    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:SetBackdropBorderColor(theme.accentR, theme.accentG, theme.accentB, 0.95)
    if SinsToolkit.RegisterOpacityFrame then
        SinsToolkit.RegisterOpacityFrame(selectorMenu, theme.bgR, theme.bgG, theme.bgB, theme.accentR, theme.accentG, theme.accentB, 0.95)
    else
        selectorMenu:SetBackdropColor(theme.bgR, theme.bgG, theme.bgB, 0.97)
    end
    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(GetSafeTeleportIcon(entry))
            row.icon:SetDesaturated(not IsKnown(entry))
            row.icon:SetAlpha(IsKnown(entry) and 1 or 0.42)
            row.icon:SetVertexColor(1, 1, 1, 1)
            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 GetSafeTeleportIcon(entry) or FALLBACK_TELEPORT_ICON)
    selectedIcon:SetDesaturated(entry and not IsKnown(entry))
    selectedIcon:SetAlpha((entry and IsKnown(entry)) and 1 or 0.42)
    selectedIcon:SetVertexColor(1, 1, 1, 1)
    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", nil)
    header:SetScript("OnEnter", nil)
    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 560, window.height or 520)
    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

local function LayoutHeaderButtons()
    if not searchBox or not rescanButton then return end

    rescanButton:ClearAllPoints()
    rescanButton:SetPoint("TOPRIGHT", -10, -8)

    local leftAnchor = rescanButton
    if clearRecentButton then
        clearRecentButton:ClearAllPoints()
        clearRecentButton:SetShown(ShowRecent())
        clearRecentButton:SetEnabled(ShowRecent())
        if ShowRecent() then
            clearRecentButton:SetPoint("RIGHT", leftAnchor, "LEFT", -6, 0)
            leftAnchor = clearRecentButton
        end
    end

    if clearFavoritesButton then
        clearFavoritesButton:ClearAllPoints()
        clearFavoritesButton:SetShown(ShowFavorites())
        clearFavoritesButton:SetEnabled(ShowFavorites())
        if ShowFavorites() then
            clearFavoritesButton:SetPoint("RIGHT", leftAnchor, "LEFT", -6, 0)
            leftAnchor = clearFavoritesButton
        end
    end

    searchBox:ClearAllPoints()
    searchBox:SetPoint("TOPLEFT", 12, -8)
    searchBox:SetPoint("RIGHT", leftAnchor, "LEFT", -10, 0)
end

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

    contentFrame = CreateFrame("Frame", nil, parent)
    contentFrame:SetPoint("TOPLEFT", 6, -38)
    contentFrame:SetPoint("BOTTOMRIGHT", -6, 16)

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

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

    clearRecentButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    clearRecentButton:SetSize(104, 24)
    clearRecentButton:SetPoint("RIGHT", rescanButton, "LEFT", -6, 0)
    clearRecentButton:SetText("Clear Recent")
    clearRecentButton:SetScript("OnClick", function()
        if not SinsToolkitDB then return end
        SinsToolkitDB.teleportHubRecent = {}
        Print("Teleport Hub recent teleports cleared.")
        Hub:Render()
    end)

    clearFavoritesButton = CreateFrame("Button", nil, contentFrame, "UIPanelButtonTemplate")
    clearFavoritesButton:SetSize(112, 24)
    clearFavoritesButton:SetPoint("RIGHT", clearRecentButton, "LEFT", -6, 0)
    clearFavoritesButton:SetText("Clear Favorites")
    clearFavoritesButton:SetScript("OnClick", function()
        if not SinsToolkitDB then return end
        SinsToolkitDB.teleportHubFavorites = {}
        Print("Teleport Hub favorites cleared.")
        Hub:Render()
    end)

    LayoutHeaderButtons()

    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)
    contentFrame:SetScript("OnUpdate", function(_, elapsed)
        if not visibleCooldowns then return end
        cooldownRefreshElapsed = cooldownRefreshElapsed + (elapsed or 0)
        if cooldownRefreshElapsed >= 1 then
            cooldownRefreshElapsed = 0
            if contentFrame:IsVisible() and not (InCombatLockdown and InCombatLockdown()) then
                Hub:Render()
            end
        end
    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 then return end
    if InCombatLockdown and InCombatLockdown() then
        Print("Teleport Hub refresh is paused during combat.")
        return
    end
    ApplyPanelTheme()
    HideSelectorMenu()
    visibleCooldowns = false
    cooldownRefreshElapsed = 0
    if viewButton then viewButton:Hide() end
    if collapseAllButton then collapseAllButton:Hide() end
    if expandAllButton then expandAllButton:Hide() end
    if selectorButton then selectorButton:Hide() end
    if selectedUseButton then selectedUseButton:Hide() end
    if selectedIcon then selectedIcon:Hide() end
    if selectedName then selectedName:Hide() end
    if selectedMeta then selectedMeta:Hide() end
    if selectedStatus then selectedStatus:Hide() end
    if selectedFavoriteButton then selectedFavoriteButton:Hide() end

    for _, button in ipairs(buttons) do
        button:Hide()
        button:SetParent(nil)
        button.entry = nil
        button.resolvedEntry = 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 index = 1
    local favoriteIndex = 1
    local headerIndex = 1
    local emptyLabelIndex = 1
    local width = math.max(1, scrollFrame:GetWidth() - 18)
    scrollChild:SetWidth(width)
    local entriesByKey = GetAllEntriesByKey()
    if searchBox then searchBox:Show() end
    if rescanButton then rescanButton:Show() end
    LayoutHeaderButtons()
    if scrollFrame then
        scrollFrame:ClearAllPoints()
        scrollFrame:SetPoint("TOPLEFT", 10, -42)
        scrollFrame:SetPoint("BOTTOMRIGHT", -24, 0)
    end

    local y = -8
    local used = {}

    local function EntryGroupName(entry)
        local name = (entry and (entry.label or EntryName(entry)) or ""):lower()
        if entry and entry.itemID and DESTINATION_HEARTHSTONE_ITEM_IDS[entry.itemID] then
            return "Destination Hearthstones"
        end
        if entry and entry.itemID and UTILITY_RECALL_ITEM_IDS[entry.itemID] then
            return "Utility Recalls"
        end
        if name:find("wormhole", 1, true)
            or name:find("wyrmhole", 1, true)
            or name:find("mole machine", 1, true)
            or name:find("centrifuge", 1, true) then
            return "Wormholes"
        end
        if entry.category == "current" then return "Current Season Dungeons" end
        if entry.category == "legacy" then return "Legacy Dungeons" end
        if entry.category == "raids" then return "Raids" end
        if entry.category == "class" then return "Class Teleports" end
        return "Other"
    end

    local groupOrder = {
        "Utility Recalls",
        "Destination Hearthstones",
        "Wormholes",
        "Current Season Dungeons",
        "Legacy Dungeons",
        "Raids",
        "Class Teleports",
        "Other",
    }

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

        local header = GetHeader(headerIndex)
        headerIndex = headerIndex + 1
        header:ClearAllPoints()
        header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 4, y)
        header:SetWidth(width - 8)
        header.category = nil
        header.virtual = true
        header.text:SetText(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(emptyText or "Nothing to show.")
            label:Show()
            y = y - 24
            return
        end

        local columns = math.max(1, math.min(4, math.floor(width / CELL_WIDTH)))
        local gap = 8
        local cardWidth = math.floor((width - ((columns - 1) * gap)) / columns)
        local column = 0
        for _, entry in ipairs(entries) do
            local button = GetButton(index)
            index = index + 1
            button.entry = entry
            AssignButton(button, entry)
            LayoutButton(button, entry, 4 + (column * (cardWidth + gap)), y, cardWidth)
            button:Show()

            if ShowFavorites() and not entry.virtualHearthstone then
                local favButton = GetFavoriteButton(favoriteIndex)
                favoriteIndex = favoriteIndex + 1
                favButton.entry = entry
                local isFavorite = IsFavorite(entry)
                favButton.text:SetText(isFavorite and "Fav" 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()
                favButton:SetSize(isFavorite and 36 or 22, 18)
                favButton:SetPoint("TOPRIGHT", button, "TOPRIGHT", -4, -4)
                favButton:Show()
            end

            column = column + 1
            if column >= columns then
                column = 0
                y = y - CELL_HEIGHT
            end
        end
        if column ~= 0 then y = y - CELL_HEIGHT end
        y = y - 10
    end

    local function SortByName(entries)
        table.sort(entries, function(a, b) return (EntryName(a) or "") < (EntryName(b) or "") end)
    end

    local function AddUsed(entry)
        local key = EntryKey(entry)
        if key then used[key] = true end
    end

    if searchText ~= "" then
        local results = {}
        for _, category in ipairs(CATEGORY_ORDER) do
            for _, entry in ipairs(TELEPORTS[category] or {}) do
                entry.category = category
                if CanShowEntry(entry, classToken) then results[#results + 1] = entry end
            end
        end
        SortByName(results)
        AddSection("Search Results", results, true, "No teleports match your search.")
        scrollChild:SetHeight(math.max(1, -y + 24))
        return
    end

    if ShowFavorites() then
        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
                    AddUsed(entry)
                end
            end
            SortByName(favorites)
        end
        AddSection("Favorites", favorites, true, "No favorites yet. Use + on a teleport to pin it here.")
    end

    if ShowRecent() then
        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) and not used[key] then
                    recent[#recent + 1] = entry
                    AddUsed(entry)
                end
            end
        end
        AddSection("Recently Used", recent, true, "No recent teleports yet.")
    end

    local preferredHearthstone = GetPreferredHearthstoneEntry()
    if (SinsToolkitDB and SinsToolkitDB.teleportHubShowLocked) or IsKnown(preferredHearthstone) then
        AddSection("Hearthstone", { preferredHearthstone }, false)
    end

    local grouped = {}
    for _, groupName in ipairs(groupOrder) do
        grouped[groupName] = {}
    end
    for _, category in ipairs(CATEGORY_ORDER) do
        for _, entry in ipairs(TELEPORTS[category] or {}) do
            entry.category = category
            local key = EntryKey(entry)
            if not IsHearthstoneEntry(entry) and CanShowEntry(entry, classToken) and (not key or not used[key]) then
                local groupName = EntryGroupName(entry)
                grouped[groupName] = grouped[groupName] or {}
                grouped[groupName][#grouped[groupName] + 1] = entry
                AddUsed(entry)
            end
        end
    end
    for _, groupName in ipairs(groupOrder) do
        local entries = grouped[groupName] or {}
        SortByName(entries)
        AddSection(groupName, entries, 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 parent and parent.CreateFontString then
        local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        label:SetPoint("CENTER")
        label:SetText("Teleport Hub now opens as a compact floating launcher.")
    end
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

function Hub:GetPreferredHearthstone()
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
    local value = SinsToolkitDB and SinsToolkitDB.teleportHubPreferredHearthstone or PREFERRED_HEARTHSTONE_RANDOM
    if value == "" then value = PREFERRED_HEARTHSTONE_RANDOM end
    if value ~= PREFERRED_HEARTHSTONE_RANDOM then
        local valid = false
        for _, entry in ipairs(GetOwnedHearthstones()) do
            if EntryKey(entry) == value then
                valid = true
                break
            end
        end
        if not valid then value = PREFERRED_HEARTHSTONE_RANDOM end
    end
    return value
end

function Hub:SetPreferredHearthstone(value)
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
    value = value or PREFERRED_HEARTHSTONE_RANDOM
    if value ~= PREFERRED_HEARTHSTONE_RANDOM then
        local valid = false
        for _, entry in ipairs(GetOwnedHearthstones()) do
            if EntryKey(entry) == value then
                valid = true
                break
            end
        end
        if not valid then value = PREFERRED_HEARTHSTONE_RANDOM end
    end
    SinsToolkitDB.teleportHubPreferredHearthstone = value
    if contentFrame and contentFrame:IsVisible() and not (InCombatLockdown and InCombatLockdown()) then
        Hub:Render()
    end
end

function Hub:GetPreferredHearthstoneOptions()
    local options = {
        { value = PREFERRED_HEARTHSTONE_RANDOM, text = "Random Available Hearthstone" },
    }
    for _, entry in ipairs(GetOwnedHearthstones()) do
        options[#options + 1] = {
            value = EntryKey(entry),
            text = entry.label or EntryName(entry),
        }
    end
    return options
end

function Hub:ResetWindow()
    if InCombatLockdown and InCombatLockdown() then
        Print("Teleport Hub window cannot be reset in combat.")
        return
    end
    if SinsToolkitDB then
        SinsToolkitDB.teleportHubWindow = {
            point = "CENTER",
            relativePoint = "CENTER",
            x = 0,
            y = 0,
            width = 560,
            height = 520,
        }
    end
    if panel then
        panel:ClearAllPoints()
        panel:SetSize(560, 520)
        panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        Hub:SaveWindow()
        if contentFrame and contentFrame:IsVisible() then Hub:Render() end
    end
    Print("Teleport Hub window reset.")
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 unavailable teleports")
    print("  |cffffff00/stktp reset|r - reset the hub window position")
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 == "reset" or msg == "resetwindow" then
        Hub:ResetWindow()
    elseif msg == "view" then
        Print("Teleport Hub now uses a simplified launcher view.")
    elseif msg == "grid" or msg == "list" or msg == "menu" then
        Print("Teleport Hub now uses a simplified launcher view.")
    elseif msg == "expand" or msg == "expandall" then
        Print("Teleport Hub now uses a simplified launcher view.")
    elseif msg == "collapse" or msg == "collapseall" then
        Print("Teleport Hub now uses a simplified launcher view.")
    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)
