local addonName, addonTable = ...
---@type MKA
local MKA = addonTable.MKA
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end

core.DungeonTab = core.DungeonTab or {}
local DungeonTab = core.DungeonTab

local DungeonTeleport = core.DungeonTeleport or {}
local AddDungeonTooltipScoreLine = DungeonTeleport.AddTooltipScoreLine or function() end
local AddDungeonTeleportTooltipLine = DungeonTeleport.AddTeleportTooltipLine or function() end
local GetDungeonTeleportSpellID = DungeonTeleport.GetSpellID or function() return nil end
local GetDungeonTeleportSpellName = DungeonTeleport.GetSpellName or function() return nil end
local GetDungeonTeleportStatus = DungeonTeleport.GetStatus or function() return nil end
local GetDungeonTeleportMacroText = DungeonTeleport.GetMacroText or function() return nil end
local UpdateDungeonTeleportBadge = DungeonTeleport.UpdateBadge or function() end
local GetDungeonTeleportDebugFields = DungeonTeleport.GetDebugFields or function() return nil, nil, nil, nil, nil end
local AttachDungeonTeleportDialogTrigger = DungeonTeleport.AttachDialogTrigger or function() end
local SetSecureButtonAttributes = core.SetSecureButtonAttributes or function(button, ...)
    if not (button and button.SetAttribute) then
        return false
    end

    local count = select("#", ...)
    for index = 1, count, 2 do
        local key = select(index, ...)
        if key ~= nil then
            local value = select(index + 1, ...)
            button:SetAttribute(key, value)
        end
    end
    return true
end

local DUNGEON_TILE_KEY_STYLE = {
    colorIron = "ffffd100",
    colorMythic = "ff1eff00",
    trackIron = "iron",
    trackMythic = "mythic",
    cache = {},
}

local function TrySetAtlas(texture, atlas, useAtlasSize)
    if not texture or not atlas or not texture.SetAtlas then
        return false
    end

    local ok = pcall(texture.SetAtlas, texture, atlas, useAtlasSize)
    return ok == true
end

local FALLBACK_SEASON_DUNGEON_MAP_IDS = {
    560, -- Maisarahohlen
    559, -- Nexuspunkt Xenas
    558, -- Terrasse der Magister
    557, -- Windlauferturm
    402, -- Akademie von Algeth'ar
    556, -- Grube von Saron
    161, -- Die Himmelsnadel
    239, -- Sitz des Triumvirats
}

local function SnapshotBool(value)
    return value and "1" or "0"
end

local function SnapshotValue(value)
    if value == nil then
        return ""
    end

    return tostring(value)
end

function MKA:GetDungeonViewCharacters()
    ---@type BKCharacterEntry[]
    local entries = {}

    if self.GetCharacters then
        for _, characterEntry in ipairs(self:GetCharacters()) do
            if not self:IsCharacterHidden(characterEntry.data) then
                entries[#entries + 1] = characterEntry
            end
        end
    end

    if self.IsInGroupForComm and self:IsInGroupForComm() and self.GetGroupCharacters then
        for _, characterEntry in ipairs(self:GetGroupCharacters()) do
            entries[#entries + 1] = characterEntry
        end
    end

    return entries
end

function MKA:GetCurrentSeasonDungeonMapIDs()
    ---@type integer[]
    local mapIDs = {}

    if not C_ChallengeMode or not C_ChallengeMode.GetMapTable then
        return mapIDs
    end

    local currentSeasonMaps = C_ChallengeMode.GetMapTable()
    if type(currentSeasonMaps) ~= "table" then
        return mapIDs
    end

    for _, mapID in ipairs(currentSeasonMaps) do
        if type(mapID) == "number" then
            mapIDs[#mapIDs + 1] = mapID
        end
    end

    if #mapIDs == 0 then
        for _, mapID in ipairs(FALLBACK_SEASON_DUNGEON_MAP_IDS) do
            mapIDs[#mapIDs + 1] = mapID
        end
    end

    return mapIDs
end

function MKA:GetPlayerDungeonBestLevel(mapID)
    if not mapID or not C_MythicPlus or not C_MythicPlus.GetSeasonBestForMap then
        return nil
    end

    local ok, inTimeInfo, overtimeInfo = pcall(C_MythicPlus.GetSeasonBestForMap, mapID)
    if not ok then
        if self.DebugLog then
            self:DebugLog("GetSeasonBestForMap failed mapID=%s error=%s", tostring(mapID), tostring(inTimeInfo))
        end

        return nil
    end

    local bestLevel = 0
    local bestTimed = nil
    if type(inTimeInfo) == "table" then
        local inTimeLevel = tonumber(inTimeInfo.level) or 0
        if inTimeLevel > bestLevel then
            bestLevel = inTimeLevel
            bestTimed = true
        end
    end
    if type(overtimeInfo) == "table" then
        local overtimeLevel = tonumber(overtimeInfo.level) or 0
        if overtimeLevel > bestLevel then
            bestLevel = overtimeLevel
            bestTimed = false
        end
    end

    if bestLevel <= 0 then
        return nil
    end

    return bestLevel, bestTimed
end

function MKA:GetCurrentSeasonDungeonGroups()
    ---@type table<integer, BKDungeonGroup>
    local groupsByMapID = {}
    ---@type BKDungeonGroup[]
    local dungeonGroups = {}

    for _, mapID in ipairs(self:GetCurrentSeasonDungeonMapIDs()) do
        local playerBestLevel, playerBestTimed = self:GetPlayerDungeonBestLevel(mapID)
        local dungeonGroup = {
            dungeonName = self:GetAbbreviatedDungeonName(mapID),
            mapID = mapID,
            playerBestLevel = playerBestLevel,
            playerBestTimed = playerBestTimed,
            entries = {},
        }
        groupsByMapID[mapID] = dungeonGroup
        dungeonGroups[#dungeonGroups + 1] = dungeonGroup
    end

    for _, characterEntry in ipairs(self:GetDungeonViewCharacters()) do
        local character = characterEntry.data or {}
        local dungeonGroup = character.mapID and groupsByMapID[character.mapID]

        if dungeonGroup then
            dungeonGroup.entries[#dungeonGroup.entries + 1] = {
                key = characterEntry.key,
                owner = characterEntry.owner,
                data = character,
            }
        end
    end

    for _, dungeonGroup in ipairs(dungeonGroups) do
        table.sort(dungeonGroup.entries, function(left, right)
            local leftLevel = left.data.keyLevel or 0
            local rightLevel = right.data.keyLevel or 0

            if leftLevel == rightLevel then
                return (left.data.name or left.key) < (right.data.name or right.key)
            end

            return leftLevel > rightLevel
        end)
    end

    return dungeonGroups
end

function MKA:BuildDungeonsTabSnapshot()
    local groupParts = {}

    for _, dungeonGroup in ipairs(self:GetCurrentSeasonDungeonGroups()) do
        local entryParts = {}

        for _, dungeonEntry in ipairs(dungeonGroup.entries or {}) do
            local character = dungeonEntry.data or {}
            entryParts[#entryParts + 1] = table.concat({
                SnapshotValue(dungeonEntry.key),
                SnapshotValue(dungeonEntry.owner),
                SnapshotValue(character.name),
                SnapshotValue(character.keyLevel),
                SnapshotValue(character.mapID),
                SnapshotValue(character.keystoneLink),
                SnapshotValue(character.keyUpgradeTrack),
            }, "~")
        end

        groupParts[#groupParts + 1] = table.concat({
            SnapshotValue(dungeonGroup.mapID),
            SnapshotValue(dungeonGroup.dungeonName),
            SnapshotValue(dungeonGroup.playerBestLevel),
            SnapshotBool(dungeonGroup.playerBestTimed),
            table.concat(entryParts, "%%"),
        }, "~~")
    end

    return table.concat(groupParts, "||")
end

local function ColorizeDungeonTileKeyText(text, color)
    return string.format("|c%s%s|r", color or DUNGEON_TILE_KEY_STYLE.colorIron, text or "")
end

local MatchKeystoneUpgradeTrackText = core.MatchKeystoneUpgradeTrackText or function() return nil end
local ResolveKeystoneUpgradeTrackFromLink = core.ResolveKeystoneUpgradeTrackFromLink or function() return nil end

local function ResolveKeystoneUpgradeTrackFromTooltip(keystoneLink)
    if type(keystoneLink) ~= "string" or keystoneLink == "" then
        return nil
    end

    local directTrack = MatchKeystoneUpgradeTrackText(keystoneLink)
    if directTrack then
        return directTrack
    end

    local linkTrack = ResolveKeystoneUpgradeTrackFromLink(keystoneLink)
    if linkTrack then
        DUNGEON_TILE_KEY_STYLE.cache[keystoneLink] = linkTrack
        return linkTrack
    end

    local cachedTrack = DUNGEON_TILE_KEY_STYLE.cache[keystoneLink]
    if cachedTrack ~= nil then
        return cachedTrack ~= false and cachedTrack or nil
    end

    if C_TooltipInfo and C_TooltipInfo.GetHyperlink then
        local ok, tooltipData = pcall(C_TooltipInfo.GetHyperlink, keystoneLink)
        if ok and type(tooltipData) == "table" and type(tooltipData.lines) == "table" then
            for _, line in ipairs(tooltipData.lines) do
                if type(line) == "table" then
                    local track = MatchKeystoneUpgradeTrackText(line.leftText)
                        or MatchKeystoneUpgradeTrackText(line.rightText)
                        or MatchKeystoneUpgradeTrackText(line.text)
                    if not track and type(line.args) == "table" then
                        for _, arg in ipairs(line.args) do
                            if type(arg) == "table" then
                                track = MatchKeystoneUpgradeTrackText(arg.stringVal)
                                if track then
                                    break
                                end
                            end
                        end
                    end
                    if track then
                        DUNGEON_TILE_KEY_STYLE.cache[keystoneLink] = track
                        return track
                    end
                end
            end
        end
    end

    if CreateFrame and UIParent then
        local tooltipName = "MKAKeystoneTrackTooltip"
        local tooltip = _G[tooltipName] or CreateFrame("GameTooltip", tooltipName, UIParent, "GameTooltipTemplate")
        tooltip:SetOwner(UIParent, "ANCHOR_NONE")
        tooltip:ClearLines()
        tooltip:SetHyperlink(keystoneLink)

        local lineCount = tooltip.NumLines and tooltip:NumLines() or 0
        for lineIndex = 1, lineCount do
            local leftLine = _G[tooltipName .. "TextLeft" .. lineIndex]
            local rightLine = _G[tooltipName .. "TextRight" .. lineIndex]
            local track = MatchKeystoneUpgradeTrackText(leftLine and leftLine:GetText())
                or MatchKeystoneUpgradeTrackText(rightLine and rightLine:GetText())
            if track then
                DUNGEON_TILE_KEY_STYLE.cache[keystoneLink] = track
                tooltip:Hide()
                return track
            end
        end

        tooltip:Hide()
    end

    return nil
end

local function GetDungeonTileKeyUpgradeTrack(character)
    if not character then
        return nil
    end

    local linkTrack = ResolveKeystoneUpgradeTrackFromLink(character.keystoneLink, character.keyLevel)
    if linkTrack then
        return linkTrack
    end

    local tooltipTrack = ResolveKeystoneUpgradeTrackFromTooltip(character.keystoneLink)
    if tooltipTrack then
        return tooltipTrack
    end

    if character.keyUpgradeTrack == DUNGEON_TILE_KEY_STYLE.trackIron
        or character.keyUpgradeTrack == DUNGEON_TILE_KEY_STYLE.trackMythic then
        return character.keyUpgradeTrack
    end

    return nil
end

local function GetDungeonTileKeyColor(character)
    local upgradeTrack = GetDungeonTileKeyUpgradeTrack(character)
    if upgradeTrack == DUNGEON_TILE_KEY_STYLE.trackIron then
        return DUNGEON_TILE_KEY_STYLE.colorIron
    end
    if upgradeTrack == DUNGEON_TILE_KEY_STYLE.trackMythic then
        return DUNGEON_TILE_KEY_STYLE.colorMythic
    end

    return DUNGEON_TILE_KEY_STYLE.colorIron
end

local function GetDungeonTileKeyText(character, showUpgradeTrackLabel)
    local keyLevel = character and character.keyLevel
    if not keyLevel or keyLevel <= 0 then
        return nil
    end

    local upgradeTrack = GetDungeonTileKeyUpgradeTrack(character)
    local text = tostring(keyLevel)
    if showUpgradeTrackLabel and upgradeTrack == DUNGEON_TILE_KEY_STYLE.trackIron then
        text = string.format("%s %s", text, TRANSLATE("AFFIX.FORTIFIED"))
    end

    return ColorizeDungeonTileKeyText(text, GetDungeonTileKeyColor(character))
end

local DUNGEON_TILE_WIDTH = 215
local DUNGEON_TILE_HEIGHT = 173
local DUNGEON_TILE_COLUMNS = 4
local DUNGEON_TILE_GAP_X = 11
local DUNGEON_TILE_GAP_Y = 14
local DUNGEON_TILE_BORDER_LEFT = -4
local DUNGEON_TILE_BORDER_TOP = 5
local DUNGEON_TILE_BORDER_RIGHT = 5
local DUNGEON_TILE_BORDER_BOTTOM = -5
local DUNGEON_TILE_IMAGE_LEFT = 2
local DUNGEON_TILE_IMAGE_TOP = 2
local DUNGEON_TILE_IMAGE_WIDTH = 211
local DUNGEON_TILE_IMAGE_HEIGHT = 169
local DUNGEON_TILE_UI = {
    topMaskTexture = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\InfoTips.png",
    topMaskHeight = math.floor((DUNGEON_TILE_IMAGE_WIDTH * 88 / 659) + 0.5),
    keystoneIconTexture = "Interface\\Icons\\INV_Relics_Hourglass_02",
    keystoneIconSize = 14,
    keystoneIconInset = 2,
    keystoneIconBorderTexture = "Interface\\Buttons\\UI-Quickslot2",
    keystoneIconOuterBorderPadding = 9,
    keystoneIconGap = 4,
    keySummaryHeight = 18,
    keySummaryMaxWidth = DUNGEON_TILE_IMAGE_WIDTH - 24,
    keySummaryOffsetY = 3,
    bestBadgeTexture = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Chakram.png",
    bestBadgeWidth = 64,
    bestBadgeHeight = 64,
    bestBadgeOffsetX = 0,
    bestBadgeOffsetY = 0,
    bestTextOffsetX = 0,
    bestTextOffsetY = 0,
    bestTextWidth = 64,
    bestTextHeight = 64,
    bestFontSize = 18,
    nameBackgroundOffsetX = 1,
    nameBackgroundOffsetY = 2,
    nameBackgroundExtraWidth = 2,
    nameBackgroundFallbackWidth = DUNGEON_TILE_WIDTH - 18,
    nameBackgroundFallbackHeight = 40,
    teleportBadgeSize = 22,
    teleportBadgeRight = -12,
    teleportBadgeOffsetY = 0,
    teleportRefreshSeconds = 1,
    pressOffsetX = 2,
    pressOffsetY = -2,
}
local DUNGEON_TILE_NAME_TEXT_WIDTH = 200
local DUNGEON_TILE_NAME_TEXT_HEIGHT = 32

local function ApplyDungeonTileImageTexCoords(image, baseTexCoords)
    if not image then
        return
    end

    local left = 0
    local right = 1
    local top = 0
    local bottom = 1

    if baseTexCoords and #baseTexCoords == 4 then
        left = baseTexCoords[1]
        right = baseTexCoords[2]
        top = baseTexCoords[3]
        bottom = baseTexCoords[4]
    end

    image:SetTexCoord(left, right, top, bottom)
end

local function UpdateDungeonTileKeySummary(tile)
    if not tile or not tile.keySummary or not tile.keyIcon or not tile.entries then
        return
    end

    local text = tile.entries:GetText()
    if type(text) ~= "string" or text == "" then
        tile.keySummary:Hide()
        tile.keyIcon:Hide()
        if tile.keyIconOuterBorder then
            tile.keyIconOuterBorder:Hide()
        end
        tile.entries:Hide()
        return
    end

    local scale = tile.layoutScale or 1
    local iconSize = math.max(
        1,
        math.floor((DUNGEON_TILE_UI.keystoneIconSize - (DUNGEON_TILE_UI.keystoneIconInset or 0)) * scale + 0.5)
    )
    local iconOuterBorderSize = math.max(
        iconSize,
        math.floor((DUNGEON_TILE_UI.keystoneIconSize + DUNGEON_TILE_UI.keystoneIconOuterBorderPadding) * scale + 0.5)
    )
    local iconGap = math.max(0, math.floor(DUNGEON_TILE_UI.keystoneIconGap * scale + 0.5))
    local summaryHeight = math.max(iconSize, math.floor(DUNGEON_TILE_UI.keySummaryHeight * scale + 0.5))
    local maxSummaryWidth = math.max(1, math.floor(DUNGEON_TILE_UI.keySummaryMaxWidth * scale + 0.5))
    local maxTextWidth = math.max(1, maxSummaryWidth - ((iconOuterBorderSize + iconGap) * 2))

    tile.entries:SetWidth(maxTextWidth)

    local measuredTextWidth = tile.entries:GetStringWidth() or 0
    if measuredTextWidth <= 0 then
        measuredTextWidth = maxTextWidth
    end

    local textWidth = math.ceil(measuredTextWidth)
    textWidth = math.max(1, math.min(maxTextWidth, textWidth))

    tile.keySummary:ClearAllPoints()
    tile.keySummary:SetPoint("CENTER", tile.topMask or tile, "CENTER", 0, DUNGEON_TILE_UI.keySummaryOffsetY * scale)
    tile.keySummary:SetSize(maxSummaryWidth, summaryHeight)

    tile.keyIcon:ClearAllPoints()
    tile.keyIcon:SetPoint("RIGHT", tile.entries, "LEFT", -iconGap, 0)
    tile.keyIcon:SetSize(iconSize, iconSize)

    if tile.keyIconOuterBorder then
        tile.keyIconOuterBorder:ClearAllPoints()
        tile.keyIconOuterBorder:SetPoint("CENTER", tile.keyIcon, "CENTER", 0, 0)
        tile.keyIconOuterBorder:SetSize(iconOuterBorderSize, iconOuterBorderSize)
        tile.keyIconOuterBorder:Show()
    end

    tile.entries:ClearAllPoints()
    tile.entries:SetPoint("CENTER", tile.keySummary, "CENTER", 0, 0)
    tile.entries:SetSize(textWidth, summaryHeight)
    tile.entries:SetJustifyH("CENTER")
    tile.entries:SetJustifyV("MIDDLE")

    tile.keySummary:Show()
    tile.keyIcon:Show()
    tile.entries:Show()
end

local function UpdateDungeonTileBestLevel(tile, level, timed)
    if not tile or not tile.bestBadge or not tile.bestText then
        return
    end

    local bestLevel = tonumber(level) or 0
    if bestLevel <= 0 then
        tile.bestBadge:Hide()
        tile.bestText:Hide()
        return
    end

    local scale = tile.layoutScale or 1
    local badgeWidth = math.max(1, math.floor(DUNGEON_TILE_UI.bestBadgeWidth * scale + 0.5))
    local badgeHeight = math.max(1, math.floor(DUNGEON_TILE_UI.bestBadgeHeight * scale + 0.5))
    local badgeOffsetX = (DUNGEON_TILE_UI.bestBadgeOffsetX or 0) * scale
    local badgeOffsetY = (DUNGEON_TILE_UI.bestBadgeOffsetY or 0) * scale
    local textOffsetX = (DUNGEON_TILE_UI.bestTextOffsetX or 0) * scale
    local textOffsetY = (DUNGEON_TILE_UI.bestTextOffsetY or 0) * scale

    tile.bestBadge:ClearAllPoints()
    tile.bestBadge:SetPoint("CENTER", tile, "CENTER", badgeOffsetX, badgeOffsetY)
    tile.bestBadge:SetSize(badgeWidth, badgeHeight)

    tile.bestText:ClearAllPoints()
    tile.bestText:SetPoint("CENTER", tile.bestBadge, "CENTER", textOffsetX, textOffsetY)
    tile.bestText:SetSize(
        math.max(1, math.floor(DUNGEON_TILE_UI.bestTextWidth * scale + 0.5)),
        math.max(1, math.floor(DUNGEON_TILE_UI.bestTextHeight * scale + 0.5))
    )
    tile.bestText:SetJustifyH("CENTER")
    tile.bestText:SetJustifyV("MIDDLE")
    tile.bestText:SetText(tostring(bestLevel))
    if timed == false then
        tile.bestText:SetTextColor(1, 0.22, 0.18)
    else
        tile.bestText:SetTextColor(1, 1, 1)
    end

    if core.ApplyFont then
        core.ApplyFont(tile.bestText, math.max(1, math.floor(DUNGEON_TILE_UI.bestFontSize * scale + 0.5)), "OUTLINE")
        tile.bestText:SetJustifyH("CENTER")
        tile.bestText:SetJustifyV("MIDDLE")
    end

    tile.bestBadge:Show()
    tile.bestText:Show()
end

local function UpdateDungeonTileSelectedState(tile, selected)
    if not tile then
        return
    end

    local wasSelected = tile.isSelected
    tile.isSelected = selected and true or false
    if tile.selectedBorder then
        if tile.isSelected then
            tile.selectedBorder:Show()
            if not wasSelected and tile.selectedBorderFadeIn then
                tile.selectedBorderFadeIn:Stop()
                tile.selectedBorder:SetAlpha(0)
                tile.selectedBorderFadeIn:Play()
            else
                tile.selectedBorder:SetAlpha(1)
            end
        else
            if tile.selectedBorderFadeIn then
                tile.selectedBorderFadeIn:Stop()
            end
            tile.selectedBorder:SetAlpha(0)
            tile.selectedBorder:Hide()
        end
    end
    if tile.highlightTexture then
        tile.highlightTexture:SetAlpha(tile.isSelected and 0 or 1)
        if tile.SetHighlightTexture then
            tile:SetHighlightTexture(tile.highlightTexture)
        end
    end
end

local function PositionDungeonTileTeleportBadge(tile, pressed)
    if not tile or not tile.teleportBadge then
        return
    end

    local scale = tile.layoutScale or 1
    local offsetX = DUNGEON_TILE_UI.teleportBadgeRight
    local offsetY = DUNGEON_TILE_UI.teleportBadgeOffsetY
    if pressed then
        offsetX = offsetX + (DUNGEON_TILE_UI.pressOffsetX or 0)
        offsetY = offsetY + (DUNGEON_TILE_UI.pressOffsetY or 0)
    end

    tile.teleportBadge:ClearAllPoints()
    tile.teleportBadge:SetPoint(
        "RIGHT",
        tile.nameBackground or tile,
        "RIGHT",
        offsetX * scale,
        offsetY * scale
    )
end

local function SetDungeonTileTeleportBadgePressed(tile, pressed)
    if not tile or not tile.teleportBadge then
        return
    end

    tile.teleportBadgePressed = pressed and true or false
    PositionDungeonTileTeleportBadge(tile, tile.teleportBadgePressed)
    if tile.teleportBadge.SetVertexColor and tile.teleportBadge:IsShown() then
        if tile.teleportBadgePressed then
            tile.teleportBadge:SetVertexColor(0.78, 0.78, 0.78, 0.95)
        else
            UpdateDungeonTeleportBadge(tile, tile.mapID or (tile.dungeonGroup and tile.dungeonGroup.mapID) or nil)
        end
    end
end

function MKA:GetDungeonTileEntriesText(dungeonGroup)
    local parts = {}
    local levelEntries = {}

    if not dungeonGroup or not dungeonGroup.entries or #dungeonGroup.entries == 0 then
        return TRANSLATE("KEYSTONE.UNAVAILABLE")
    end

    for _, dungeonEntry in ipairs(dungeonGroup.entries) do
        local character = dungeonEntry.data
        levelEntries[#levelEntries + 1] = {
            keyLevel = (character and character.keyLevel) or 0,
            keystoneLink = character and character.keystoneLink or nil,
            keyUpgradeTrack = character and character.keyUpgradeTrack or nil,
        }
    end

    table.sort(levelEntries, function(left, right)
        return left.keyLevel < right.keyLevel
    end)

    local visibleEntries = math.min(#levelEntries, 6)

    for index = 1, visibleEntries do
        local entry = levelEntries[index]
        if entry.keyLevel and entry.keyLevel > 0 then
            parts[#parts + 1] = GetDungeonTileKeyText(entry)
        else
            parts[#parts + 1] = ColorizeDungeonTileKeyText(TRANSLATE("COLUMN.KEYSTONE"), DUNGEON_TILE_KEY_STYLE.colorIron)
        end
    end

    if #levelEntries > visibleEntries then
        parts[#parts + 1] = string.format("|cffcccccc%d|r", #levelEntries - visibleEntries)
    end

    return table.concat(parts, " / ")
end

function MKA:CreateDungeonTile(parentFrame)
    local tile = CreateFrame("Button", nil, parentFrame, "SecureActionButtonTemplate")
    tile:SetSize(DUNGEON_TILE_WIDTH, DUNGEON_TILE_HEIGHT)
    tile:RegisterForClicks("AnyUp", "AnyDown")
    SetSecureButtonAttributes(tile, "type", "macro", "type1", "macro")
    tile:Hide()

    local image = tile:CreateTexture(nil, "BACKGROUND")
    image:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_IMAGE_LEFT, -DUNGEON_TILE_IMAGE_TOP)
    image:SetSize(DUNGEON_TILE_IMAGE_WIDTH, DUNGEON_TILE_IMAGE_HEIGHT)
    image:SetTexture("Interface\\Buttons\\WHITE8X8")
    image:SetVertexColor(0.08, 0.08, 0.08, 0.95)

    local topMask = tile:CreateTexture(nil, "ARTWORK", nil, 1)
    topMask:SetTexture(DUNGEON_TILE_UI.topMaskTexture)
    topMask:SetPoint("TOPLEFT", image, "TOPLEFT", 0, 0)
    topMask:SetPoint("TOPRIGHT", image, "TOPRIGHT", 0, 0)
    topMask:SetHeight(DUNGEON_TILE_UI.topMaskHeight)
    topMask:SetAlpha(1)

    local nameBackground = tile:CreateTexture(nil, "ARTWORK", nil, 1)
    if not TrySetAtlas(nameBackground, "campcollection-bg-text", true) then
        nameBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
        nameBackground:SetVertexColor(0, 0, 0, 0.72)
    end
    nameBackground.mkaBaseWidth = (nameBackground.GetWidth and nameBackground:GetWidth()) or DUNGEON_TILE_UI.nameBackgroundFallbackWidth
    nameBackground.mkaBaseHeight = (nameBackground.GetHeight and nameBackground:GetHeight()) or DUNGEON_TILE_UI.nameBackgroundFallbackHeight
    if nameBackground.mkaBaseWidth <= 0 then
        nameBackground.mkaBaseWidth = DUNGEON_TILE_UI.nameBackgroundFallbackWidth
    end
    if nameBackground.mkaBaseHeight <= 0 then
        nameBackground.mkaBaseHeight = DUNGEON_TILE_UI.nameBackgroundFallbackHeight
    end
    nameBackground:SetSize(nameBackground.mkaBaseWidth + DUNGEON_TILE_UI.nameBackgroundExtraWidth, nameBackground.mkaBaseHeight)
    nameBackground:SetPoint("BOTTOM", tile, "BOTTOM", DUNGEON_TILE_UI.nameBackgroundOffsetX, DUNGEON_TILE_UI.nameBackgroundOffsetY)

    local border = tile:CreateTexture(nil, "ARTWORK", nil, 2)
    if not TrySetAtlas(border, "campcollection-frame", false) then
        border:SetTexture(nil)
    end
    border:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
    border:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)

    local highlight = tile:CreateTexture(nil, "HIGHLIGHT")
    if not TrySetAtlas(highlight, "campcollection-frame-hover", false) then
        highlight:SetTexture(nil)
    end
    highlight:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
    highlight:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)
    highlight:SetBlendMode("ADD")
    tile:SetHighlightTexture(highlight)

    local selectedBorder = tile:CreateTexture(nil, "ARTWORK", nil, 3)
    if not TrySetAtlas(selectedBorder, "campcollection-frame-hover", false) then
        selectedBorder:SetTexture(nil)
    end
    selectedBorder:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
    selectedBorder:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)
    selectedBorder:SetBlendMode("ADD")
    selectedBorder:SetVertexColor(1, 0.72, 0.05, 1)
    selectedBorder:SetAlpha(0)
    selectedBorder:Hide()

    local selectedBorderFadeIn = selectedBorder:CreateAnimationGroup()
    local selectedBorderFade = selectedBorderFadeIn:CreateAnimation("Alpha")
    selectedBorderFade:SetFromAlpha(0)
    selectedBorderFade:SetToAlpha(1)
    selectedBorderFade:SetDuration(0.18)
    selectedBorderFade:SetSmoothing("OUT")
    selectedBorderFadeIn:SetScript("OnFinished", function()
        selectedBorder:SetAlpha(1)
    end)

    local title = tile:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("CENTER", nameBackground, "CENTER", 0, 0)
    title:SetSize(DUNGEON_TILE_NAME_TEXT_WIDTH, DUNGEON_TILE_NAME_TEXT_HEIGHT)
    title:SetJustifyH("CENTER")
    title:SetJustifyV("MIDDLE")
    title:SetTextColor(1, 1, 1)
    title:SetWordWrap(true)
    if title.SetSpacing then
        title:SetSpacing(2)
    end

    local bestBadge = tile:CreateTexture(nil, "OVERLAY", nil, 0)
    bestBadge:SetTexture(DUNGEON_TILE_UI.bestBadgeTexture)
    bestBadge:SetPoint("CENTER", tile, "CENTER", DUNGEON_TILE_UI.bestBadgeOffsetX, DUNGEON_TILE_UI.bestBadgeOffsetY)
    bestBadge:SetSize(DUNGEON_TILE_UI.bestBadgeWidth, DUNGEON_TILE_UI.bestBadgeHeight)
    bestBadge:Hide()

    local bestText = tile:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    bestText:SetPoint("CENTER", bestBadge, "CENTER", DUNGEON_TILE_UI.bestTextOffsetX, DUNGEON_TILE_UI.bestTextOffsetY)
    bestText:SetSize(DUNGEON_TILE_UI.bestTextWidth, DUNGEON_TILE_UI.bestTextHeight)
    bestText:SetJustifyH("CENTER")
    bestText:SetJustifyV("MIDDLE")
    bestText:SetTextColor(1, 1, 1)
    if core.ApplyFont then
        core.ApplyFont(bestText, DUNGEON_TILE_UI.bestFontSize, "OUTLINE")
        bestText:SetJustifyH("CENTER")
        bestText:SetJustifyV("MIDDLE")
    end
    bestText:Hide()

    local teleportBadge = tile:CreateTexture(nil, "OVERLAY", nil, 2)
    teleportBadge:SetPoint("RIGHT", nameBackground, "RIGHT", DUNGEON_TILE_UI.teleportBadgeRight, DUNGEON_TILE_UI.teleportBadgeOffsetY)
    teleportBadge:SetSize(DUNGEON_TILE_UI.teleportBadgeSize, DUNGEON_TILE_UI.teleportBadgeSize)
    if teleportBadge.SetAtlas then
        teleportBadge:SetAtlas("MagePortalAlliance", false)
    else
        teleportBadge:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
    end
    teleportBadge:Hide()

    local keySummary = CreateFrame("Frame", nil, tile)
    keySummary:SetPoint("CENTER", topMask, "CENTER", 0, DUNGEON_TILE_UI.keySummaryOffsetY)
    keySummary:SetSize(DUNGEON_TILE_UI.keySummaryMaxWidth, DUNGEON_TILE_UI.keySummaryHeight)
    keySummary:Hide()

    local keyIconOuterBorder = keySummary:CreateTexture(nil, "OVERLAY", nil, 2)
    keyIconOuterBorder:SetTexture(DUNGEON_TILE_UI.keystoneIconBorderTexture)
    keyIconOuterBorder:SetVertexColor(1, 0.82, 0, 1)
    keyIconOuterBorder:SetSize(
        DUNGEON_TILE_UI.keystoneIconSize + DUNGEON_TILE_UI.keystoneIconOuterBorderPadding,
        DUNGEON_TILE_UI.keystoneIconSize + DUNGEON_TILE_UI.keystoneIconOuterBorderPadding
    )
    keyIconOuterBorder:Hide()

    local keyIcon = keySummary:CreateTexture(nil, "OVERLAY", nil, 1)
    keyIcon:SetTexture(DUNGEON_TILE_UI.keystoneIconTexture)
    local initialKeyIconSize = math.max(1, DUNGEON_TILE_UI.keystoneIconSize - (DUNGEON_TILE_UI.keystoneIconInset or 0))
    keyIcon:SetSize(
        initialKeyIconSize,
        initialKeyIconSize
    )
    keyIcon:Hide()

    local entries = keySummary:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    entries:SetPoint("CENTER", keySummary, "CENTER", 0, 0)
    entries:SetSize(DUNGEON_TILE_UI.keySummaryMaxWidth - ((DUNGEON_TILE_UI.keystoneIconSize + DUNGEON_TILE_UI.keystoneIconGap) * 2), DUNGEON_TILE_UI.keySummaryHeight)
    entries:SetJustifyH("CENTER")
    entries:SetJustifyV("MIDDLE")
    entries:SetTextColor(1, 1, 1)
    keyIcon:SetPoint("RIGHT", entries, "LEFT", -DUNGEON_TILE_UI.keystoneIconGap, 0)
    keyIconOuterBorder:SetPoint("CENTER", keyIcon, "CENTER", 0, 0)
    if entries.SetWordWrap then
        entries:SetWordWrap(false)
    end
    if core.ApplyFont then
        core.ApplyFont(entries, 11, "OUTLINE")
    end
    entries:Hide()

    tile:SetScript("OnEnter", function(frame)
        local dungeonGroup = frame.dungeonGroup
        if not dungeonGroup then
            return
        end

        GameTooltip:SetOwner(frame, "ANCHOR_CURSOR")
        GameTooltip:AddLine(dungeonGroup.dungeonName, 1, 0.82, 0)
        AddDungeonTooltipScoreLine(GameTooltip, dungeonGroup.mapID)
        AddDungeonTeleportTooltipLine(GameTooltip, dungeonGroup.mapID)

        if #dungeonGroup.entries == 0 then
            GameTooltip:AddLine(TRANSLATE("GROUP.EMPTY.NO_KEYSTONES_SHORT"), 0.85, 0.85, 0.85)
        else
            for _, dungeonEntry in ipairs(dungeonGroup.entries) do
                local character = dungeonEntry.data
                local ownerName = self:GetCharacterDisplayNameColored(character)
                local keyText = GetDungeonTileKeyText(character, true)

                if keyText then
                    GameTooltip:AddLine(string.format("%s  %s", ownerName, keyText), 1, 1, 1)
                else
                    GameTooltip:AddLine(ownerName, 1, 1, 1)
                end
            end
        end

        GameTooltip:Show()
    end)
    tile:HookScript("OnMouseDown", function(frame, mouseButton)
        if mouseButton == "LeftButton" then
            SetDungeonTileTeleportBadgePressed(frame, true)
        end

        local dungeonGroup = frame.dungeonGroup
        local mapID = dungeonGroup and dungeonGroup.mapID or nil
        local spellID, spellName, status, secureType, secureSpell = GetDungeonTeleportDebugFields(frame, mapID)

        self:DebugLog(
            "DungeonTile OnMouseDown button=%s mapID=%s spellID=%s spellName=%s status=%s secureType=%s secureSpell=%s protected=%s combat=%s",
            tostring(mouseButton),
            tostring(mapID),
            tostring(spellID),
            tostring(spellName),
            tostring(status),
            tostring(secureType),
            tostring(secureSpell),
            tostring(frame.IsProtected and frame:IsProtected()),
            tostring(InCombatLockdown and InCombatLockdown())
        )
    end)
    tile:HookScript("OnMouseUp", function(frame, mouseButton)
        if mouseButton == "LeftButton" then
            SetDungeonTileTeleportBadgePressed(frame, false)
        end

        local dungeonGroup = frame.dungeonGroup
        local mapID = dungeonGroup and dungeonGroup.mapID or nil
        local spellID, spellName, status, secureType, secureSpell = GetDungeonTeleportDebugFields(frame, mapID)
        self:DebugLog(
            "DungeonTile OnMouseUp button=%s mapID=%s spellID=%s spellName=%s status=%s secureType=%s secureSpell=%s",
            tostring(mouseButton),
            tostring(mapID),
            tostring(spellID),
            tostring(spellName),
            tostring(status),
            tostring(secureType),
            tostring(secureSpell)
        )
    end)
    tile:HookScript("OnClick", function(frame, mouseButton)
        local dungeonGroup = frame.dungeonGroup
        local mapID = dungeonGroup and dungeonGroup.mapID or nil
        if mouseButton == "LeftButton" and mapID then
            self.selectedDungeonMapID = mapID
            self:RefreshDungeonTileSelection()
        end

        local spellID, spellName, status, secureType, secureSpell = GetDungeonTeleportDebugFields(frame, mapID)
        self:DebugLog(
            "DungeonTile OnClick button=%s mapID=%s spellID=%s spellName=%s status=%s secureType=%s secureSpell=%s",
            tostring(mouseButton),
            tostring(mapID),
            tostring(spellID),
            tostring(spellName),
            tostring(status),
            tostring(secureType),
            tostring(secureSpell)
        )
    end)
    tile:SetScript("OnLeave", function(frame)
        SetDungeonTileTeleportBadgePressed(frame, false)
        GameTooltip:Hide()
    end)
    tile:SetScript("OnUpdate", function(frame, elapsed)
        frame.teleportBadgeElapsed = (frame.teleportBadgeElapsed or 0) + (elapsed or 0)
        if frame.teleportBadgeElapsed < DUNGEON_TILE_UI.teleportRefreshSeconds then
            return
        end

        frame.teleportBadgeElapsed = 0
        UpdateDungeonTeleportBadge(frame, frame.mapID or (frame.dungeonGroup and frame.dungeonGroup.mapID) or nil)
    end)

    tile.image = image
    tile.topMask = topMask
    tile.nameBackground = nameBackground
    tile.border = border
    tile.highlightTexture = highlight
    tile.selectedBorder = selectedBorder
    tile.selectedBorderFadeIn = selectedBorderFadeIn
    tile.title = title
    tile.bestBadge = bestBadge
    tile.bestText = bestText
    tile.keySummary = keySummary
    tile.keyIconOuterBorder = keyIconOuterBorder
    tile.keyIcon = keyIcon
    tile.entries = entries
    tile.teleportBadge = teleportBadge
    AttachDungeonTeleportDialogTrigger(self, tile, function(frame)
        return frame.mapID or (frame.dungeonGroup and frame.dungeonGroup.mapID) or nil
    end)

    return tile
end

local function ApplyDungeonTileLayout(tile, width, height, scale)
    if not tile then
        return
    end

    tile:SetSize(width, height)
    tile.layoutScale = scale

    if tile.image then
        tile.image:SetSize(
            math.max(1, math.floor(DUNGEON_TILE_IMAGE_WIDTH * scale + 0.5)),
            math.max(1, math.floor(DUNGEON_TILE_IMAGE_HEIGHT * scale + 0.5))
        )
    end
    if tile.topMask then
        tile.topMask:ClearAllPoints()
        tile.topMask:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_IMAGE_LEFT * scale, -DUNGEON_TILE_IMAGE_TOP * scale)
        tile.topMask:SetPoint(
            "TOPRIGHT",
            tile,
            "TOPLEFT",
            (DUNGEON_TILE_IMAGE_LEFT + DUNGEON_TILE_IMAGE_WIDTH) * scale,
            -DUNGEON_TILE_IMAGE_TOP * scale
        )
        tile.topMask:SetHeight(math.max(1, math.floor(DUNGEON_TILE_UI.topMaskHeight * scale + 0.5)))
    end
    if tile.nameBackground then
        tile.nameBackground:ClearAllPoints()
        tile.nameBackground:SetPoint(
            "BOTTOM",
            tile,
            "BOTTOM",
            DUNGEON_TILE_UI.nameBackgroundOffsetX * scale,
            DUNGEON_TILE_UI.nameBackgroundOffsetY * scale
        )
        tile.nameBackground:SetSize(
            math.max(1, math.floor(((tile.nameBackground.mkaBaseWidth or DUNGEON_TILE_UI.nameBackgroundFallbackWidth) + DUNGEON_TILE_UI.nameBackgroundExtraWidth) * scale + 0.5)),
            math.max(1, math.floor((tile.nameBackground.mkaBaseHeight or DUNGEON_TILE_UI.nameBackgroundFallbackHeight) * scale + 0.5))
        )
    end
    if tile.border then
        tile.border:ClearAllPoints()
        tile.border:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
        tile.border:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)
    end
    if tile.highlightTexture then
        tile.highlightTexture:ClearAllPoints()
        tile.highlightTexture:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
        tile.highlightTexture:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)
    end
    if tile.selectedBorder then
        tile.selectedBorder:ClearAllPoints()
        tile.selectedBorder:SetPoint("TOPLEFT", tile, "TOPLEFT", DUNGEON_TILE_BORDER_LEFT, DUNGEON_TILE_BORDER_TOP)
        tile.selectedBorder:SetPoint("BOTTOMRIGHT", tile, "BOTTOMRIGHT", DUNGEON_TILE_BORDER_RIGHT, DUNGEON_TILE_BORDER_BOTTOM)
    end
    if tile.title then
        tile.title:ClearAllPoints()
        tile.title:SetPoint("CENTER", tile.nameBackground or tile, "CENTER", 0, 0)
        tile.title:SetSize(
            math.max(1, math.floor(DUNGEON_TILE_NAME_TEXT_WIDTH * scale + 0.5)),
            math.max(1, math.floor(DUNGEON_TILE_NAME_TEXT_HEIGHT * scale + 0.5))
        )
        tile.title:SetJustifyH("CENTER")
        tile.title:SetJustifyV("MIDDLE")
        tile.title:SetTextColor(1, 1, 1)
    end

    if tile.teleportBadge then
        PositionDungeonTileTeleportBadge(tile, tile.teleportBadgePressed)
        tile.teleportBadge:SetSize(
            math.max(1, math.floor(DUNGEON_TILE_UI.teleportBadgeSize * scale + 0.5)),
            math.max(1, math.floor(DUNGEON_TILE_UI.teleportBadgeSize * scale + 0.5))
        )
    end

    UpdateDungeonTileBestLevel(tile, tile.playerBestLevel, tile.playerBestTimed)

    if tile.entries then
        if tile.entries.SetWordWrap then
            tile.entries:SetWordWrap(false)
        end
        if core.ApplyFont then
            core.ApplyFont(tile.entries, 11, "OUTLINE")
        end
    end

    UpdateDungeonTileKeySummary(tile)
end

function MKA:EnsureDungeonTiles(requiredCount)
    local dungeonTiles = self.widgets.dungeonTiles
    local parentFrame = self.widgets.dungeonContentFrame or self.widgets.dungeonContainer
    if not dungeonTiles or not parentFrame then
        return
    end

    while #dungeonTiles < requiredCount do
        dungeonTiles[#dungeonTiles + 1] = self:CreateDungeonTile(parentFrame)
    end

    for _, tile in ipairs(dungeonTiles) do
        if tile:GetParent() ~= parentFrame then
            tile:SetParent(parentFrame)
        end
    end
end

function MKA:RefreshDungeonTileSelection()
    local dungeonTiles = self.widgets and self.widgets.dungeonTiles or nil
    if not dungeonTiles then
        return
    end

    local selectedMapID = tonumber(self.selectedDungeonMapID)
    for _, tile in ipairs(dungeonTiles) do
        UpdateDungeonTileSelectedState(tile, selectedMapID and tonumber(tile.mapID) == selectedMapID)
    end
end

function MKA:LayoutDungeonTiles(tileCount)
    local dungeonContentFrame = self.widgets and self.widgets.dungeonContentFrame
    local dungeonContainer = self.widgets.dungeonContainer
    local dungeonTiles = self.widgets.dungeonTiles or {}
    if not dungeonContentFrame or not dungeonContainer then
        return
    end

    local availableWidth = math.max(dungeonContentFrame:GetWidth() or 0, 1)
    local availableHeight = math.max(dungeonContentFrame:GetHeight() or 0, 1)
    local columns = math.min(math.max(tileCount, 1), DUNGEON_TILE_COLUMNS)
    local rows = math.max(1, math.ceil(tileCount / DUNGEON_TILE_COLUMNS))
    local tileWidth = DUNGEON_TILE_WIDTH
    local tileHeight = DUNGEON_TILE_HEIGHT
    local gapX = DUNGEON_TILE_GAP_X
    local gapY = DUNGEON_TILE_GAP_Y

    local freeWidth = availableWidth - (columns * tileWidth)
    if freeWidth > 0 then
        gapX = math.max(0, math.floor((freeWidth / (columns + 1)) + 0.5))
    end

    local freeHeight = availableHeight - (rows * tileHeight)
    if freeHeight > 0 then
        gapY = math.max(0, math.floor((freeHeight / (rows + 1)) + 0.5))
    end

    local gridWidth = (columns * tileWidth) + (math.max(0, columns - 1) * gapX)
    local totalHeight = rows * tileHeight + math.max(0, rows - 1) * gapY
    local startX = math.floor(((availableWidth - gridWidth) * 0.5) + 0.5)
    local startY = -math.floor(((availableHeight - totalHeight) * 0.5) + 0.5)
    startY = math.min(0, startY + (core.DUNGEON_TILE_GRID_OFFSET_Y or 0))

    dungeonContainer:ClearAllPoints()
    dungeonContainer:SetSize(gridWidth, totalHeight)
    dungeonContainer:SetPoint("TOPLEFT", dungeonContentFrame, "TOPLEFT", startX, startY)

    for index = 1, tileCount do
        local tile = dungeonTiles[index]
        local column = (index - 1) % DUNGEON_TILE_COLUMNS
        local row = math.floor((index - 1) / DUNGEON_TILE_COLUMNS)
        local xOffset = column * (tileWidth + gapX)
        local yOffset = -(row * (tileHeight + gapY))

        tile:ClearAllPoints()
        tile:SetPoint("TOPLEFT", dungeonContentFrame, "TOPLEFT", startX + xOffset, startY + yOffset)
        ApplyDungeonTileLayout(tile, tileWidth, tileHeight, 1)
    end
end

function MKA:RefreshDungeonTile(tile, dungeonGroup)
    local imageTexture, imageTexCoords, _, imageZoom = self:GetDungeonJournalTexture(dungeonGroup.mapID)
    local teleportSpellID = GetDungeonTeleportSpellID(dungeonGroup.mapID)
    local teleportSpellName = GetDungeonTeleportSpellName(dungeonGroup.mapID)
    local teleportMacroText = GetDungeonTeleportMacroText(dungeonGroup.mapID)
    tile.dungeonGroup = dungeonGroup
    tile.mapID = dungeonGroup.mapID
    tile.playerBestLevel = dungeonGroup.playerBestLevel
    tile.playerBestTimed = dungeonGroup.playerBestTimed
    tile.title:SetText(dungeonGroup.dungeonName)
    tile.entries:SetText(self:GetDungeonTileEntriesText(dungeonGroup))
    UpdateDungeonTileKeySummary(tile)
    UpdateDungeonTileBestLevel(tile, tile.playerBestLevel, tile.playerBestTimed)
    SetSecureButtonAttributes(
        tile,
        "type", teleportMacroText and "macro" or nil,
        "type1", teleportMacroText and "macro" or nil,
        "macrotext", teleportMacroText,
        "macrotext1", teleportMacroText,
        "spell", nil,
        "spell1", nil
    )
    UpdateDungeonTeleportBadge(tile, dungeonGroup.mapID)
    UpdateDungeonTileSelectedState(tile, self.selectedDungeonMapID and tonumber(self.selectedDungeonMapID) == tonumber(dungeonGroup.mapID))
    self:DebugLog(
        "RefreshDungeonTile mapID=%s dungeon=%s spellID=%s spellName=%s secureType=%s secureMacro=%s status=%s",
        tostring(dungeonGroup.mapID),
        tostring(dungeonGroup.dungeonName),
        tostring(teleportSpellID),
        tostring(teleportSpellName),
        tostring(tile:GetAttribute("type1")),
        tostring(tile:GetAttribute("macrotext1")),
        tostring(GetDungeonTeleportStatus(dungeonGroup.mapID))
    )

    if imageTexture then
        tile.image:SetTexture(imageTexture)
        ApplyDungeonTileImageTexCoords(tile.image, imageTexCoords, imageZoom)
        tile.image:SetVertexColor(1, 1, 1, 1)
    else
        tile.image:SetTexture("Interface\\Buttons\\WHITE8X8")
        ApplyDungeonTileImageTexCoords(tile.image, nil, 1)
        tile.image:SetVertexColor(0.08, 0.08, 0.08, 0.95)
    end

    tile:Show()
end

function MKA:SetDungeonTileVisible(tile, visible)
    if not tile then
        return
    end

    if visible then
        tile:Show()
        return
    end

    tile.dungeonGroup = nil
    tile.mapID = nil
    tile.playerBestLevel = nil
    tile.playerBestTimed = nil
    tile.teleportBadgePressed = false
    SetSecureButtonAttributes(tile, "type", nil, "type1", nil, "macrotext", nil, "macrotext1", nil, "spell", nil, "spell1", nil)
    tile.title:SetText("")
    tile.entries:SetText("")
    UpdateDungeonTileKeySummary(tile)
    UpdateDungeonTileBestLevel(tile, nil, nil)
    tile.image:SetTexture("Interface\\Buttons\\WHITE8X8")
    ApplyDungeonTileImageTexCoords(tile.image, nil, 1)
    tile.image:SetVertexColor(0.08, 0.08, 0.08, 0.95)
    UpdateDungeonTileSelectedState(tile, false)
    UpdateDungeonTeleportBadge(tile, nil)
    tile:Hide()
end

function DungeonTab.Build(owner, tabGroup)
    local container = CreateFrame("Frame", nil, UIParent)
    tabGroup:AddChild(container)

    local dungeonContentFrame = CreateFrame("Frame", nil, container)
    dungeonContentFrame:ClearAllPoints()
    dungeonContentFrame:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    dungeonContentFrame:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)
    dungeonContentFrame:SetFrameLevel(container:GetFrameLevel() + 1)

    local dungeonContainer = CreateFrame("Frame", nil, dungeonContentFrame)
    dungeonContainer:SetPoint("CENTER", dungeonContentFrame, "CENTER", 0, 0)
    dungeonContainer:SetSize(1, 1)
    dungeonContainer:Show()

    local dungeonEmptyContainer = CreateFrame("Frame", nil, dungeonContentFrame)
    dungeonEmptyContainer:SetPoint("TOPLEFT", dungeonContainer, "BOTTOMLEFT", 0, -12)
    dungeonEmptyContainer:SetPoint("TOPRIGHT", dungeonContentFrame, "TOPRIGHT", -8, -12)
    dungeonEmptyContainer:SetHeight(28)

    local dungeonEmpty = dungeonEmptyContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    dungeonEmpty:SetPoint("TOPLEFT", dungeonEmptyContainer, "TOPLEFT", 0, 0)
    dungeonEmpty:SetPoint("TOPRIGHT", dungeonEmptyContainer, "TOPRIGHT", 0, 0)
    dungeonEmpty:SetJustifyH("LEFT")
    dungeonEmpty:SetJustifyV("TOP")
    dungeonEmpty:SetText(TRANSLATE("GROUP.EMPTY.NO_KEYSTONES"))
    if core.ApplyFont then
        core.ApplyFont(dungeonEmpty, 12, "")
    end

    owner.widgets = {
        tabGroup = owner.widgets and owner.widgets.tabGroup or tabGroup,
        root = container,
        dungeonContentFrame = dungeonContentFrame,
        dungeonContainer = dungeonContainer,
        dungeonEmptyContainer = dungeonEmptyContainer,
        dungeonEmpty = dungeonEmpty,
        dungeonTiles = {},
    }
    owner.tabViews = owner.tabViews or {}
    owner.tabViews.dungeons = owner.widgets

    dungeonContentFrame:SetScript("OnSizeChanged", function()
        if owner.activeTab == "dungeons" and owner.widgets and owner.widgets.dungeonContentFrame == dungeonContentFrame then
            local dungeonGroups = owner:GetCurrentSeasonDungeonGroups()
            owner:EnsureDungeonTiles(#dungeonGroups)
            owner:LayoutDungeonTiles(#dungeonGroups)
        end
    end)

    if core.ApplyThemeToTabGroup then
        core.ApplyThemeToTabGroup(tabGroup)
    end
    if core.ApplyFontToFrame then
        core.ApplyFontToFrame(container)
    end

    C_Timer.After(0, function()
        if core.FinalizeFlatTabLayout then
            core.FinalizeFlatTabLayout(tabGroup)
        end
        if owner.activeTab == "dungeons" and owner.widgets and owner.widgets.dungeonContentFrame == dungeonContentFrame then
            owner:RefreshMainWindow("DungeonContentFrame", "deferred_layout")
        end
    end)

    owner:RefreshMainWindow("BuildSelectedTab", "dungeons")
end
