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

local TELEPORT_DIALOG_TITLE_KEY = "TELEPORT.PROMPT"
local TELEPORT_DIALOG_WIDTH = 480
local TELEPORT_DIALOG_HEIGHT = 210
local TELEPORT_DIALOG_BUTTON_WIDTH = 128
local TELEPORT_DIALOG_BUTTON_HEIGHT = 32
local TELEPORT_DIALOG_CENTER_OFFSET_X = 0
local TELEPORT_DIALOG_CENTER_OFFSET_Y = 0
local TELEPORT_DIALOG_BACKGROUND_ALPHA = 0.82
local TELEPORT_DIALOG_BACKGROUND_LEFT_INSET = 6
local TELEPORT_DIALOG_TITLE_OFFSET_Y = 1
local TELEPORT_DIALOG_MESSAGE_INSET_X = 32
local TELEPORT_DIALOG_MESSAGE_TOP_OFFSET = -58
local TELEPORT_DIALOG_MESSAGE_HEIGHT = 72
local TELEPORT_DIALOG_MESSAGE_LINE_SPACING = 4
local TELEPORT_DIALOG_BUTTON_BOTTOM_OFFSET = 26
local TELEPORT_DIALOG_SENDER_FALLBACK_COLOR = "ffffd100"
local TELEPORT_DIALOG_DUNGEON_COLOR = "ff58d7ff"
local TELEPORT_DIALOG_PROMPT_COLOR = "ffffd100"
local TELEPORT_DIALOG_PARTICIPANT_TEXT_COLOR = "ffffffff"
local TELEPORT_DIALOG_PARTICIPANT_COUNT_COLOR = "ffffd100"
local OUTGOING_DUNGEON_TELEPORT_BROADCAST_DEDUPE_SECONDS = 8
local INCOMING_DUNGEON_TELEPORT_PROMPT_SUPPRESS_SECONDS = 60
local INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS = 45
local SECURE_ATTRIBUTE_NIL_VALUE = {}

local secureAttributeEventFrame
local pendingSecureAttributeButtons = {}

local function IsInCombatLockdown()
    return InCombatLockdown and InCombatLockdown()
end

local function ApplySecureButtonAttributes(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 function FlushSecureButtonAttributes()
    if IsInCombatLockdown() then
        return false
    end

    for button, pending in pairs(pendingSecureAttributeButtons) do
        pendingSecureAttributeButtons[button] = nil
        if button and button.SetAttribute and type(pending) == "table" then
            for _, key in ipairs(pending.order or {}) do
                local value = pending.values and pending.values[key]
                if value == SECURE_ATTRIBUTE_NIL_VALUE then
                    value = nil
                end
                button:SetAttribute(key, value)
            end
        end
    end
    return true
end

local function EnsureSecureAttributeEventFrame()
    if secureAttributeEventFrame then
        return
    end

    secureAttributeEventFrame = CreateFrame("Frame")
    secureAttributeEventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    secureAttributeEventFrame:SetScript("OnEvent", FlushSecureButtonAttributes)
end

local function QueueSecureButtonAttributes(button, ...)
    if not (button and button.SetAttribute) then
        return false
    end

    if not IsInCombatLockdown() then
        return ApplySecureButtonAttributes(button, ...)
    end

    EnsureSecureAttributeEventFrame()

    local pending = pendingSecureAttributeButtons[button]
    if not pending then
        pending = {
            order = {},
            known = {},
            values = {},
        }
        pendingSecureAttributeButtons[button] = pending
    end

    local count = select("#", ...)
    for index = 1, count, 2 do
        local key = select(index, ...)
        if key ~= nil then
            if not pending.known[key] then
                pending.known[key] = true
                pending.order[#pending.order + 1] = key
            end

            local value = select(index + 1, ...)
            if value == nil then
                value = SECURE_ATTRIBUTE_NIL_VALUE
            end
            pending.values[key] = value
        end
    end
    return false
end

core.SetSecureButtonAttributes = QueueSecureButtonAttributes
core.FlushSecureButtonAttributes = FlushSecureButtonAttributes

local DUNGEON_TELEPORT_SPELLS = {
    [402] = { spellID = 393273,  spellName = "Algeth'ar Academy" },
    [558] = { spellID = 1254572, spellName = "Magisters' Terrace" },
    [560] = { spellID = 1254559, spellName = "Maisara Caverns" },
    [559] = { spellID = 1254563, spellName = "Nexus-Point Xenas" },
    [556] = { spellID = 1254555, spellName = "Pit of Saron" },
    [239] = { spellID = 1254551, spellName = "Seat of the Triumvirate" },
    [161] = { spellID = 159898,  spellName = "Skyreach" },
    [557] = { spellID = 1254400, spellName = "Windrunner Spire" },
}
local DUNGEON_TELEPORT_MAP_ID_BY_SPELL_ID = {}
for mapID, teleportData in pairs(DUNGEON_TELEPORT_SPELLS) do
    if teleportData.spellID then
        DUNGEON_TELEPORT_MAP_ID_BY_SPELL_ID[teleportData.spellID] = mapID
    end
end
local DUNGEON_TELEPORT_PREVIEW_MAP_ID = 560

local function GetDungeonTeleportMapIDBySpellID(spellID)
    local ok, numericSpellID = pcall(tonumber, spellID)
    if not ok or type(numericSpellID) ~= "number" then
        return nil
    end

    local lookupOk, mapID = pcall(function()
        return DUNGEON_TELEPORT_MAP_ID_BY_SPELL_ID[numericSpellID]
    end)
    if lookupOk then
        return mapID
    end
    return nil
end

local function GetDungeonTeleportSpellID(mapID)
    local teleportData = mapID and DUNGEON_TELEPORT_SPELLS[mapID] or nil
    return teleportData and teleportData.spellID or nil
end

local function GetDungeonTeleportSpellName(mapID)
    local spellID = GetDungeonTeleportSpellID(mapID)
    if spellID then
        if C_Spell and C_Spell.GetSpellInfo then
            local spellInfo = C_Spell.GetSpellInfo(spellID)
            if type(spellInfo) == "table" and type(spellInfo.name) == "string" and spellInfo.name ~= "" then
                return spellInfo.name
            end
        end

        if C_Spell and C_Spell.GetSpellName then
            local spellName = C_Spell.GetSpellName(spellID)
            if type(spellName) == "string" and spellName ~= "" then
                return spellName
            end
        end

        if GetSpellInfo then
            local spellName = GetSpellInfo(spellID)
            if type(spellName) == "string" and spellName ~= "" then
                return spellName
            end
        end
    end

    local teleportData = mapID and DUNGEON_TELEPORT_SPELLS[mapID] or nil
    return teleportData and teleportData.spellName or nil
end

local function GetDungeonTeleportSpellLink(mapID)
    local spellID = GetDungeonTeleportSpellID(mapID)
    if spellID then
        if C_Spell and C_Spell.GetSpellLink then
            local spellLink = C_Spell.GetSpellLink(spellID)
            if type(spellLink) == "string" and spellLink ~= "" then
                return spellLink
            end
        end

        if GetSpellLink then
            local spellLink = GetSpellLink(spellID)
            if type(spellLink) == "string" and spellLink ~= "" then
                return spellLink
            end
        end
    end

    return GetDungeonTeleportSpellName(mapID)
end

local function IsDungeonTeleportLearned(spellID)
    if not spellID then
        return false
    end

    if IsSpellKnownOrOverridesKnown then
        return IsSpellKnownOrOverridesKnown(spellID)
    end

    if IsSpellKnown then
        return IsSpellKnown(spellID)
    end

    return false
end

local function IsDungeonTeleportOnCooldown(spellID)
    if not spellID then
        return false
    end

    if C_Spell and C_Spell.GetSpellCooldown then
        local cooldownInfo = C_Spell.GetSpellCooldown(spellID)
        if type(cooldownInfo) == "table" then
            local startTime = cooldownInfo.startTime or 0
            local duration = cooldownInfo.duration or 0
            local isEnabled = cooldownInfo.isEnabled

            if isEnabled == false or startTime <= 0 or duration <= 0 then
                return false
            end

            return (startTime + duration) > GetTime()
        end
    end

    local startTime, duration, isEnabled = GetSpellCooldown(spellID)
    if isEnabled == 0 or not startTime or startTime <= 0 or not duration or duration <= 0 then
        return false
    end

    return (startTime + duration) > GetTime()
end

local function GetDungeonTeleportStatus(mapID)
    local spellID = GetDungeonTeleportSpellID(mapID)
    if not spellID then
        return nil, nil
    end

    if not IsDungeonTeleportLearned(spellID) then
        return "not_learned", spellID
    end

    if IsDungeonTeleportOnCooldown(spellID) then
        return "cooldown", spellID
    end

    return "ready", spellID
end

local function UpdateDungeonTeleportBadge(tile, mapID)
    local badge = tile and tile.teleportBadge
    if not badge then
        return
    end

    local teleportStatus = GetDungeonTeleportStatus(mapID)
    if teleportStatus == "ready" then
        if badge.SetDesaturated then
            badge:SetDesaturated(false)
        end
        badge:SetVertexColor(1, 1, 1, 0.95)
        badge:SetAlpha(1)
        badge:Show()
        return
    end

    if teleportStatus == "cooldown" then
        if badge.SetDesaturated then
            badge:SetDesaturated(true)
        end
        badge:SetVertexColor(0.45, 0.45, 0.45, 0.85)
        badge:SetAlpha(0.82)
        badge:Show()
        return
    end

    badge:Hide()
end

local function GetDungeonTooltipScoreAndBestTime(mapID)
    if not mapID or not C_MythicPlus then
        return nil, nil
    end

    local score
    local bestDurationSec

    if C_MythicPlus.GetSeasonBestAffixScoreInfoForMap then
        local ok, affixScores, overallScore = pcall(C_MythicPlus.GetSeasonBestAffixScoreInfoForMap, mapID)
        if ok then
            score = tonumber(overallScore)
            if type(affixScores) == "table" then
                for _, affixScore in ipairs(affixScores) do
                    local durationSec = tonumber(affixScore and affixScore.durationSec)
                    if durationSec and durationSec > 0 and (not bestDurationSec or durationSec < bestDurationSec) then
                        bestDurationSec = durationSec
                    end
                end
            end
        end
    end

    if (not score or not bestDurationSec) and C_MythicPlus.GetSeasonBestForMap then
        local ok, inTimeInfo, overtimeInfo = pcall(C_MythicPlus.GetSeasonBestForMap, mapID)
        if ok then
            for _, seasonBestInfo in ipairs({ inTimeInfo, overtimeInfo }) do
                if type(seasonBestInfo) == "table" then
                    local dungeonScore = tonumber(seasonBestInfo.dungeonScore)
                    if dungeonScore and (not score or dungeonScore > score) then
                        score = dungeonScore
                    end

                    local durationSec = tonumber(seasonBestInfo.durationSec)
                    if durationSec and durationSec > 0 and (not bestDurationSec or durationSec < bestDurationSec) then
                        bestDurationSec = durationSec
                    end
                end
            end
        end
    end

    return score, bestDurationSec
end

local DUNGEON_TOOLTIP_LABEL_COLOR = "ffffd100"
local DUNGEON_TOOLTIP_VALUE_COLOR = "ffffffff"

local function WrapDungeonTooltipColor(text, color)
    return string.format("|c%s%s|r", color or DUNGEON_TOOLTIP_VALUE_COLOR, text or "")
end

local function WrapDungeonTeleportTextColor(text, color)
    return string.format("|c%s%s|r", color or TELEPORT_DIALOG_SENDER_FALLBACK_COLOR, tostring(text or ""))
end

local function GetDungeonTeleportClassColorString(classToken)
    local classColors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    local color = classToken and classColors and classColors[classToken]
    if color and color.colorStr then
        return color.colorStr
    end
    if color and color.r and color.g and color.b then
        return string.format(
            "ff%02x%02x%02x",
            math.floor((color.r or 1) * 255 + 0.5),
            math.floor((color.g or 1) * 255 + 0.5),
            math.floor((color.b or 1) * 255 + 0.5)
        )
    end
    return nil
end

local function AddDungeonTeleportNameAliases(aliases, value)
    if type(value) ~= "string" or value == "" then
        return
    end

    local function add(alias)
        if type(alias) == "string" and alias ~= "" then
            aliases[alias:lower()] = true
        end
    end

    add(value)
    if Ambiguate then
        add(Ambiguate(value, "none"))
    end
    add(value:match("^[^-]+"))
end

local function DungeonTeleportNamesMatch(left, right)
    local aliases = {}
    AddDungeonTeleportNameAliases(aliases, left)
    if next(aliases) == nil then
        return false
    end

    local rightAliases = {}
    AddDungeonTeleportNameAliases(rightAliases, right)
    for alias in pairs(rightAliases) do
        if aliases[alias] then
            return true
        end
    end
    return false
end

local function GetDungeonTeleportUnitFullName(unit)
    local name, realm = UnitFullName and UnitFullName(unit)
    if type(name) ~= "string" or name == "" then
        name = UnitName and UnitName(unit) or nil
    end
    if type(name) ~= "string" or name == "" then
        return nil
    end
    if type(realm) ~= "string" or realm == "" then
        realm = GetRealmName and GetRealmName() or nil
    end
    if type(realm) == "string" and realm ~= "" then
        return name .. "-" .. realm
    end
    return name
end

local function GetDungeonTeleportGroupUnitClassToken(senderName)
    if type(senderName) ~= "string" or senderName == "" or not (IsInRaid() or IsInGroup()) then
        return nil
    end

    if DungeonTeleportNamesMatch(GetDungeonTeleportUnitFullName("player"), senderName) then
        return select(2, UnitClass("player"))
    end

    local groupPrefix = IsInRaid() and "raid" or "party"
    local memberCount = GetNumGroupMembers and GetNumGroupMembers() or 0
    if not IsInRaid() and IsInGroup() then
        memberCount = math.max(memberCount - 1, 0)
    end

    for index = 1, memberCount do
        local unit = groupPrefix .. index
        if UnitExists and UnitExists(unit) and DungeonTeleportNamesMatch(GetDungeonTeleportUnitFullName(unit), senderName) then
            return select(2, UnitClass(unit))
        end
    end

    return nil
end

local function GetDungeonTeleportStoredClassToken(self, senderName)
    if type(senderName) ~= "string" or senderName == "" then
        return nil
    end

    for ownerKey, ownerData in pairs((self and self.groupCharacters) or {}) do
        for _, character in pairs((ownerData and ownerData.characters) or {}) do
            if type(character) == "table"
                and character.class
                and (
                    DungeonTeleportNamesMatch(character.name, senderName)
                    or DungeonTeleportNamesMatch(ownerKey, senderName)
                )
            then
                return character.class
            end
        end
    end

    local stored = self and self.db and self.db.global and self.db.global.characters
    for _, character in pairs(stored or {}) do
        if type(character) == "table" and character.class and DungeonTeleportNamesMatch(character.name, senderName) then
            return character.class
        end
    end

    return nil
end

local function GetDungeonTeleportSenderClassToken(self, senderName, senderClass)
    if type(senderClass) == "string" and senderClass ~= "" then
        return senderClass
    end
    return GetDungeonTeleportGroupUnitClassToken(senderName) or GetDungeonTeleportStoredClassToken(self, senderName)
end

local function GetDungeonTooltipScoreColor(score)
    if not score then
        return DUNGEON_TOOLTIP_VALUE_COLOR
    end

    if MKA.GetRaiderIOScoreColorString then
        return MKA:GetRaiderIOScoreColorString(score)
    end

    return DUNGEON_TOOLTIP_VALUE_COLOR
end

local function AddDungeonTooltipScoreLine(tooltip, mapID)
    if not tooltip then
        return
    end

    local score, bestDurationSec = GetDungeonTooltipScoreAndBestTime(mapID)
    if (not score or score <= 0) and not bestDurationSec then
        return
    end

    local roundedScore = math.floor((score or 0) + 0.5)
    local scoreColor = GetDungeonTooltipScoreColor(roundedScore)
    local scoreLine = WrapDungeonTooltipColor(TRANSLATE("RATING.PREFIX"), DUNGEON_TOOLTIP_LABEL_COLOR)
        .. WrapDungeonTooltipColor(tostring(roundedScore), scoreColor)
        .. WrapDungeonTooltipColor(TRANSLATE("RATING.POINTS_SUFFIX"), DUNGEON_TOOLTIP_LABEL_COLOR)
    tooltip:AddLine(scoreLine, 1, 1, 1)
    tooltip:AddLine(" ")

    if bestDurationSec then
        local totalSeconds = math.floor(bestDurationSec + 0.5)
        local minutes = math.floor(totalSeconds / 60)
        local seconds = totalSeconds % 60
        local bestLine = WrapDungeonTooltipColor(TRANSLATE("DUNGEON.BEST_PREFIX"), DUNGEON_TOOLTIP_LABEL_COLOR)
            .. WrapDungeonTooltipColor(string.format("%d : %02d", minutes, seconds), DUNGEON_TOOLTIP_VALUE_COLOR)
        tooltip:AddLine(bestLine, 1, 1, 1)
        tooltip:AddLine(" ")
        return
    end

    local bestLine = WrapDungeonTooltipColor(TRANSLATE("DUNGEON.BEST_PREFIX"), DUNGEON_TOOLTIP_LABEL_COLOR)
        .. WrapDungeonTooltipColor("--", DUNGEON_TOOLTIP_VALUE_COLOR)
    tooltip:AddLine(bestLine, 1, 1, 1)
    tooltip:AddLine(" ")
end

AddDungeonTeleportTooltipLine = function(tooltip, mapID)
    if not tooltip then
        return
    end

    local status = GetDungeonTeleportStatus(mapID)
    if status == "ready" then
        tooltip:AddLine(TRANSLATE("TELEPORT.STATUS.READY"), 0.1, 1, 0.1)
    elseif status == "cooldown" then
        tooltip:AddLine(TRANSLATE("TELEPORT.STATUS.COOLDOWN"), 1, 0.15, 0.15)
    elseif status == "not_learned" then
        tooltip:AddLine(TRANSLATE("TELEPORT.STATUS.NOT_LEARNED"), 1, 0.15, 0.15)
    end
end

local function GetDungeonTeleportMacroText(mapID)
    local spellName = GetDungeonTeleportSpellName(mapID)
    if not spellName or spellName == "" then
        return nil
    end

    return "/cast " .. spellName
end

local function GetDungeonTeleportLocalPlayerName()
    local name, realm = UnitFullName and UnitFullName("player")
    if type(name) ~= "string" or name == "" then
        name = UnitName and UnitName("player") or UNKNOWN
    end
    if type(realm) == "string" and realm ~= "" then
        return name .. "-" .. realm
    end
    return name
end

local function NormalizeDungeonTeleportChatMessage(message)
    if type(message) ~= "string" then
        return ""
    end
    message = message:gsub("[%c\r\n]+", " ")
    message = message:match("^%s*(.-)%s*$") or ""
    return message
end

local function FormatDungeonTeleportDialogMessage(self, senderName, mapID, dungeonName, messageTemplate)
    local template = messageTemplate
    if type(template) ~= "string" or template == "" then
        template = self and self.GetDefaultDungeonTeleportDialogMessage and self:GetDefaultDungeonTeleportDialogMessage()
    end
    if type(template) ~= "string" or template == "" then
        template = TRANSLATE("TELEPORT.ANNOUNCEMENT_DEFAULT")
    end

    local playerText = tostring(senderName or UNKNOWN)
    local dungeonText = tostring(dungeonName or (self and self.GetLocalizedDungeonName and self:GetLocalizedDungeonName(mapID)) or UNKNOWN)
    local spellText = tostring(GetDungeonTeleportSpellLink(mapID) or GetDungeonTeleportSpellName(mapID) or UNKNOWN)
    local message = template:gsub("{player}", function()
        return playerText
    end)
    message = message:gsub("{dungeon}", function()
        return dungeonText
    end)
    message = message:gsub("%[%{spell%}%]", function()
        return spellText
    end)
    message = message:gsub("{spell}", function()
        return spellText
    end)
    return message
end

local function FormatDungeonTeleportSenderText(self, senderName, senderClass, participantCount)
    local playerText = tostring(senderName or UNKNOWN)
    local classToken = GetDungeonTeleportSenderClassToken(self, senderName, senderClass)
    local senderColor = GetDungeonTeleportClassColorString(classToken) or TELEPORT_DIALOG_SENDER_FALLBACK_COLOR
    local senderText = WrapDungeonTeleportTextColor(playerText, senderColor)
    participantCount = tonumber(participantCount) or 1
    if participantCount <= 1 then
        return senderText
    end

    return senderText
        .. WrapDungeonTeleportTextColor(" 等 ", TELEPORT_DIALOG_PARTICIPANT_TEXT_COLOR)
        .. WrapDungeonTeleportTextColor(tostring(participantCount), TELEPORT_DIALOG_PARTICIPANT_COUNT_COLOR)
        .. WrapDungeonTeleportTextColor(" 人", TELEPORT_DIALOG_PARTICIPANT_TEXT_COLOR)
end

local function FormatDungeonTeleportFollowMessage(self, senderName, mapID, dungeonName, senderClass, participantCount)
    local dungeonText = tostring(dungeonName or (self and self.GetLocalizedDungeonName and self:GetLocalizedDungeonName(mapID)) or UNKNOWN)

    return string.format(
        TRANSLATE("TELEPORT.FOLLOW_MESSAGE_FORMAT"),
        FormatDungeonTeleportSenderText(self, senderName, senderClass, participantCount),
        WrapDungeonTeleportTextColor(dungeonText, TELEPORT_DIALOG_DUNGEON_COLOR),
        WrapDungeonTeleportTextColor(TRANSLATE("TELEPORT.FOLLOW_QUESTION"), TELEPORT_DIALOG_PROMPT_COLOR)
    )
end

local function GetDungeonTeleportParticipantKey(senderName)
    local value = tostring(senderName or UNKNOWN)
    value = value:match("^%s*(.-)%s*$") or value
    if value == "" then
        value = UNKNOWN
    end
    return value:lower()
end

local function NormalizeDungeonTeleportCastGUID(castGUID)
    if type(castGUID) ~= "string" and type(castGUID) ~= "number" then
        return nil
    end
    local value = tostring(castGUID)
    if value == "" then
        return nil
    end
    return value
end

local function AddDungeonTeleportDialogParticipant(dialog, senderName, senderClass, castGUID, timeoutSeconds)
    if not dialog then
        return UNKNOWN, nil, 1
    end

    local displayName = tostring(senderName or UNKNOWN)
    if displayName == "" then
        displayName = UNKNOWN
    end

    dialog.mkaTeleportParticipants = dialog.mkaTeleportParticipants or {}
    dialog.mkaTeleportParticipantOrder = dialog.mkaTeleportParticipantOrder or {}

    local key = GetDungeonTeleportParticipantKey(displayName)
    local participant = dialog.mkaTeleportParticipants[key]
    if not participant then
        participant = {
            name = displayName,
            class = senderClass,
        }
        dialog.mkaTeleportParticipants[key] = participant
        table.insert(dialog.mkaTeleportParticipantOrder, key)
    else
        participant.name = participant.name or displayName
        participant.class = participant.class or senderClass
    end
    participant.castGUID = NormalizeDungeonTeleportCastGUID(castGUID) or participant.castGUID
    participant.expiresAt = (GetTime and GetTime() or 0)
        + (tonumber(timeoutSeconds) or INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS)

    local firstKey = dialog.mkaTeleportParticipantOrder[1]
    local firstParticipant = firstKey and dialog.mkaTeleportParticipants[firstKey] or participant
    local count = #dialog.mkaTeleportParticipantOrder
    return firstParticipant and firstParticipant.name or displayName,
        firstParticipant and firstParticipant.class or senderClass,
        count
end

local function RemoveDungeonTeleportDialogParticipant(dialog, senderName, castGUID)
    if not (dialog and type(dialog.mkaTeleportParticipants) == "table" and type(dialog.mkaTeleportParticipantOrder) == "table") then
        return false
    end

    local normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    local targetKey
    if normalizedCastGUID then
        for key, participant in pairs(dialog.mkaTeleportParticipants) do
            if participant and participant.castGUID == normalizedCastGUID then
                targetKey = key
                break
            end
        end
    end

    if not targetKey then
        targetKey = GetDungeonTeleportParticipantKey(senderName)
    end

    if not dialog.mkaTeleportParticipants[targetKey] then
        return false
    end

    dialog.mkaTeleportParticipants[targetKey] = nil
    for index = #dialog.mkaTeleportParticipantOrder, 1, -1 do
        if dialog.mkaTeleportParticipantOrder[index] == targetKey then
            table.remove(dialog.mkaTeleportParticipantOrder, index)
            break
        end
    end
    return true
end

local function RefreshDungeonTeleportDialogParticipants(self, dialog)
    if not (self and dialog and dialog.message and type(dialog.mkaTeleportParticipants) == "table" and type(dialog.mkaTeleportParticipantOrder) == "table") then
        return false
    end

    local firstKey = dialog.mkaTeleportParticipantOrder[1]
    local firstParticipant = firstKey and dialog.mkaTeleportParticipants[firstKey]
    local participantCount = #dialog.mkaTeleportParticipantOrder
    if not firstParticipant or participantCount <= 0 then
        return false
    end

    dialog.senderName = firstParticipant.name
    dialog.senderClass = firstParticipant.class
    dialog.message:SetText(FormatDungeonTeleportFollowMessage(
        self,
        firstParticipant.name,
        dialog.mapID,
        dialog.dungeonName,
        firstParticipant.class,
        participantCount
    ))
    return true
end

local function QueueDungeonTeleportDialogTimeout(self, dialog)
    if not (self and dialog and C_Timer and C_Timer.After and type(dialog.mkaTeleportParticipants) == "table") then
        return
    end

    local nextExpiresAt
    for _, participant in pairs(dialog.mkaTeleportParticipants) do
        local expiresAt = tonumber(participant and participant.expiresAt)
        if expiresAt and (not nextExpiresAt or expiresAt < nextExpiresAt) then
            nextExpiresAt = expiresAt
        end
    end
    if not nextExpiresAt then
        return
    end

    dialog.mkaTeleportTimeoutTicket = (dialog.mkaTeleportTimeoutTicket or 0) + 1
    local ticket = dialog.mkaTeleportTimeoutTicket
    local delay = math.max(0.1, nextExpiresAt - (GetTime and GetTime() or 0))
    C_Timer.After(delay, function()
        if not (dialog and dialog.mkaTeleportTimeoutTicket == ticket and dialog.IsShown and dialog:IsShown() and dialog.forcePreview ~= true) then
            return
        end

        local now = GetTime and GetTime() or 0
        for index = #dialog.mkaTeleportParticipantOrder, 1, -1 do
            local key = dialog.mkaTeleportParticipantOrder[index]
            local participant = key and dialog.mkaTeleportParticipants[key]
            if participant and tonumber(participant.expiresAt) and tonumber(participant.expiresAt) <= now then
                dialog.mkaTeleportParticipants[key] = nil
                table.remove(dialog.mkaTeleportParticipantOrder, index)
            end
        end

        if not RefreshDungeonTeleportDialogParticipants(self, dialog) then
            dialog.mkaHideReason = "expired"
            dialog:Hide()
            return
        end

        QueueDungeonTeleportDialogTimeout(self, dialog)
    end)
end

local function SendDungeonTeleportAnnouncement(self, mapID, dungeonName, messageTemplate)
    if not (self and self.GetGroupChannel) or type(mapID) ~= "number" then
        return false
    end

    local channel = self:GetGroupChannel()
    if not channel then
        return false
    end

    local message = NormalizeDungeonTeleportChatMessage(FormatDungeonTeleportDialogMessage(
        self,
        GetDungeonTeleportLocalPlayerName(),
        mapID,
        dungeonName,
        messageTemplate
    ))
    if message == "" then
        return false
    end

    local sendChatMessage = SendChatMessage or (C_ChatInfo and C_ChatInfo.SendChatMessage)
    if not sendChatMessage then
        return false
    end

    local ok = pcall(sendChatMessage, message, channel)
    return ok == true
end

local function FormatDungeonTeleportPreviewMessage(self, messageTemplate)
    local mapID = DUNGEON_TELEPORT_PREVIEW_MAP_ID
    local dungeonName = self and self.GetLocalizedDungeonName and self:GetLocalizedDungeonName(mapID)
    return NormalizeDungeonTeleportChatMessage(FormatDungeonTeleportDialogMessage(
        self,
        GetDungeonTeleportLocalPlayerName(),
        mapID,
        dungeonName,
        messageTemplate
    ))
end

local function ApplyDungeonTeleportMenuFrameStyle(dialog)
    if not dialog or dialog.mkaTeleportMenuFrameStyled then
        return
    end

    if dialog.Bg and dialog.Bg.Hide then
        dialog.Bg:Hide()
    end
    if dialog.Inset and dialog.Inset.Hide then
        dialog.Inset:Hide()
    end
    if not dialog.mkaTeleportBackgroundPieces then
        dialog.mkaTeleportBackgroundPieces = {}
        for index = 1, 3 do
            dialog.mkaTeleportBackgroundPieces[index] = dialog:CreateTexture(nil, "BACKGROUND", nil, -8)
        end
    end

    local backgroundCornerRadius = 1
    local backgroundInset = 2
    local backgroundLeftInset = TELEPORT_DIALOG_BACKGROUND_LEFT_INSET
    for _, backgroundPiece in ipairs(dialog.mkaTeleportBackgroundPieces) do
        backgroundPiece:ClearAllPoints()
        backgroundPiece:SetDrawLayer("BACKGROUND", -8)
        if backgroundPiece.SetColorTexture then
            backgroundPiece:SetColorTexture(0, 0, 0, TELEPORT_DIALOG_BACKGROUND_ALPHA)
            backgroundPiece:SetVertexColor(1, 1, 1, 1)
        else
            backgroundPiece:SetTexture("Interface\\Buttons\\WHITE8X8")
            backgroundPiece:SetVertexColor(0, 0, 0, TELEPORT_DIALOG_BACKGROUND_ALPHA)
        end
        backgroundPiece:Show()
    end

    local topBackground = dialog.mkaTeleportBackgroundPieces[1]
    topBackground:SetPoint("TOPLEFT", dialog, "TOPLEFT", backgroundLeftInset + backgroundCornerRadius, -backgroundInset)
    topBackground:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -(backgroundInset + backgroundCornerRadius), -backgroundInset)
    topBackground:SetHeight(backgroundCornerRadius)

    local middleBackground = dialog.mkaTeleportBackgroundPieces[2]
    middleBackground:SetPoint("TOPLEFT", dialog, "TOPLEFT", backgroundLeftInset, -(backgroundInset + backgroundCornerRadius))
    middleBackground:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -backgroundInset, backgroundInset + backgroundCornerRadius)

    local bottomBackground = dialog.mkaTeleportBackgroundPieces[3]
    bottomBackground:SetPoint("BOTTOMLEFT", dialog, "BOTTOMLEFT", backgroundLeftInset + backgroundCornerRadius, backgroundInset)
    bottomBackground:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -(backgroundInset + backgroundCornerRadius), backgroundInset)
    bottomBackground:SetHeight(backgroundCornerRadius)

    if ButtonFrameTemplate_HidePortrait then
        pcall(ButtonFrameTemplate_HidePortrait, dialog)
    end
    if ButtonFrameTemplate_HideButtonBar then
        pcall(ButtonFrameTemplate_HideButtonBar, dialog)
    end
    if NineSliceUtil and NineSliceUtil.ApplyLayoutByName then
        pcall(NineSliceUtil.ApplyLayoutByName, dialog, "ButtonFrameTemplateNoPortrait")
    end
    if dialog.SetTitle then
        pcall(dialog.SetTitle, dialog, TRANSLATE(TELEPORT_DIALOG_TITLE_KEY))
    end
    local titleText = dialog.TitleText or (dialog.TitleContainer and dialog.TitleContainer.TitleText)
    if titleText then
        titleText:SetText(TRANSLATE(TELEPORT_DIALOG_TITLE_KEY))
        titleText:SetTextColor(1, 0.82, 0, 1)
        if core.ApplyFont then
            core.ApplyFont(titleText, 16, "OUTLINE")
        end
        if titleText.GetPoint and titleText.SetPoint then
            if not titleText.mkaTeleportTitleBasePoint then
                titleText.mkaTeleportTitleBasePoint = { titleText:GetPoint(1) }
            end
            local basePoint = titleText.mkaTeleportTitleBasePoint
            if basePoint and basePoint[1] then
                titleText:ClearAllPoints()
                titleText:SetPoint(
                    basePoint[1],
                    basePoint[2],
                    basePoint[3],
                    basePoint[4] or 0,
                    (basePoint[5] or 0) + TELEPORT_DIALOG_TITLE_OFFSET_Y
                )
            end
        end
    end

    dialog.mkaTeleportMenuFrameStyled = true
end

local function CenterDungeonTeleportDialog(dialog)
    if not dialog then
        return
    end

    dialog:ClearAllPoints()
    dialog:SetPoint("CENTER", UIParent, "CENTER", TELEPORT_DIALOG_CENTER_OFFSET_X, TELEPORT_DIALOG_CENTER_OFFSET_Y)
end

local function SuppressDungeonTeleportPrompt(self, mapID, reason)
    if not (self and type(mapID) == "number") then
        return
    end

    self.suppressedIncomingDungeonTeleportPrompts = self.suppressedIncomingDungeonTeleportPrompts or {}
    self.suppressedIncomingDungeonTeleportPrompts[mapID] = GetTime and GetTime() or 0

    if self.DebugLog then
        self:DebugLog(
            "SuppressDungeonTeleportPrompt mapID=%s reason=%s duration=%s",
            tostring(mapID),
            tostring(reason),
            tostring(INCOMING_DUNGEON_TELEPORT_PROMPT_SUPPRESS_SECONDS)
        )
    end
end

local function IsDungeonTeleportPromptSuppressed(self, mapID)
    if not (self and type(mapID) == "number" and type(self.suppressedIncomingDungeonTeleportPrompts) == "table") then
        return false
    end

    local previousAt = self.suppressedIncomingDungeonTeleportPrompts[mapID]
    if not previousAt then
        return false
    end

    local now = GetTime and GetTime() or 0
    if (now - previousAt) < INCOMING_DUNGEON_TELEPORT_PROMPT_SUPPRESS_SECONDS then
        return true
    end

    self.suppressedIncomingDungeonTeleportPrompts[mapID] = nil
    return false
end

function MKA:SuppressDungeonTeleportPrompt(mapID, reason)
    SuppressDungeonTeleportPrompt(self, mapID, reason)
end

function MKA:IsDungeonTeleportPromptSuppressed(mapID)
    return IsDungeonTeleportPromptSuppressed(self, mapID)
end

local function ShowDungeonTeleportDialog(self, senderName, mapID, dungeonName, dialogMessage, forcePreview, senderClass, incomingTacticalAnnouncementHandled, castGUID, timeoutSeconds)
    if not self or type(mapID) ~= "number" then
        if self and self.DebugLog then
            self:DebugLog(
                "ShowDungeonTeleportDialog skipped sender=%s mapID=%s reason=%s",
                tostring(senderName),
                tostring(mapID),
                not self and "missing_self" or "invalid_map"
            )
        end
        return false
    end

    if InCombatLockdown and InCombatLockdown() then
        self:DebugLog(
            "ShowDungeonTeleportDialog skipped sender=%s mapID=%s dungeon=%s reason=in_combat",
            tostring(senderName),
            tostring(mapID),
            tostring(dungeonName)
        )
        return false
    end

    local teleportStatus, spellID = GetDungeonTeleportStatus(mapID)
    local teleportSpellName = GetDungeonTeleportSpellName(mapID)
    local teleportMacroText = GetDungeonTeleportMacroText(mapID)
    if not forcePreview and (teleportStatus ~= "ready" or not teleportSpellName or teleportSpellName == "" or not teleportMacroText) then
        self:DebugLog(
            "ShowDungeonTeleportDialog skipped sender=%s mapID=%s dungeon=%s status=%s spellID=%s spellName=%s hasMacro=%s",
            tostring(senderName),
            tostring(mapID),
            tostring(dungeonName),
            tostring(teleportStatus),
            tostring(spellID),
            tostring(teleportSpellName),
            tostring(teleportMacroText ~= nil)
        )
        return false
    end

    local dialog = self.dungeonTeleportDialogFrame
    if not dialog then
        dialog = CreateFrame("Frame", "MKADungeonTeleportDialog", UIParent, "ButtonFrameTemplate")
        dialog:SetSize(TELEPORT_DIALOG_WIDTH, TELEPORT_DIALOG_HEIGHT)
        CenterDungeonTeleportDialog(dialog)
        dialog:SetFrameStrata("DIALOG")
        dialog:SetClampedToScreen(true)
        dialog:SetMovable(true)
        dialog:EnableMouse(true)
        dialog:RegisterForDrag("LeftButton")
        dialog:SetScript("OnDragStart", function(frame)
            frame:StartMoving()
        end)
        dialog:SetScript("OnDragStop", function(frame)
            frame:StopMovingOrSizing()
        end)
        dialog:SetScript("OnHide", function(frame)
            local hideReason = frame.mkaHideReason or "dismissed"
            if frame.mapID and not frame.forcePreview and hideReason ~= "cancelled" and hideReason ~= "expired" then
                SuppressDungeonTeleportPrompt(self, frame.mapID, hideReason)
            end
            frame.mkaHideReason = nil
            frame.mkaTeleportTimeoutTicket = (frame.mkaTeleportTimeoutTicket or 0) + 1
            frame.mkaTeleportParticipants = {}
            frame.mkaTeleportParticipantOrder = {}
        end)

        ApplyDungeonTeleportMenuFrameStyle(dialog)

        local message = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        message:SetPoint("TOPLEFT", dialog, "TOPLEFT", TELEPORT_DIALOG_MESSAGE_INSET_X, TELEPORT_DIALOG_MESSAGE_TOP_OFFSET)
        message:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -TELEPORT_DIALOG_MESSAGE_INSET_X, TELEPORT_DIALOG_MESSAGE_TOP_OFFSET)
        message:SetHeight(TELEPORT_DIALOG_MESSAGE_HEIGHT)
        message:SetJustifyH("CENTER")
        message:SetJustifyV("MIDDLE")
        message:SetWordWrap(true)
        if message.SetSpacing then
            message:SetSpacing(TELEPORT_DIALOG_MESSAGE_LINE_SPACING)
        end
        if core.ApplyFont then
            core.ApplyFont(message, 18, "OUTLINE")
        end
        dialog.message = message

        local prompt = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        prompt:SetPoint("TOPLEFT", message, "BOTTOMLEFT", 0, -4)
        prompt:SetPoint("TOPRIGHT", message, "BOTTOMRIGHT", 0, -4)
        prompt:SetHeight(20)
        prompt:SetJustifyH("CENTER")
        prompt:SetJustifyV("MIDDLE")
        prompt:SetText("")
        prompt:Hide()
        if core.ApplyFont then
            core.ApplyFont(prompt, 16, "OUTLINE")
        end
        dialog.prompt = prompt

        local acceptButton = CreateFrame("Button", "MKADungeonTeleportDialogAcceptButton", dialog, "SecureActionButtonTemplate,GameMenuButtonTemplate")
        acceptButton:SetSize(TELEPORT_DIALOG_BUTTON_WIDTH, TELEPORT_DIALOG_BUTTON_HEIGHT)
        acceptButton:SetPoint("BOTTOMLEFT", dialog, "BOTTOM", -137, TELEPORT_DIALOG_BUTTON_BOTTOM_OFFSET)
        acceptButton:RegisterForClicks("AnyUp", "AnyDown")
        acceptButton:SetText(YES)
        QueueSecureButtonAttributes(acceptButton, "type", "macro", "type1", "macro")
        acceptButton:SetAttribute("_onstate-bkcombat", [[
            if newstate == "combat" then
                self:SetAttribute("type", nil)
                self:SetAttribute("type1", nil)
            else
                self:SetAttribute("type", "macro")
                self:SetAttribute("type1", "macro")
            end
        ]])
        RegisterStateDriver(acceptButton, "bkcombat", "[combat] combat; nocombat")
        if acceptButton.SetNormalFontObject then
            acceptButton:SetNormalFontObject("GameFontNormalLarge")
        end
        if acceptButton.SetHighlightFontObject then
            acceptButton:SetHighlightFontObject("GameFontHighlightLarge")
        end
        acceptButton:HookScript("OnClick", function()
            dialog.mkaHideReason = "accepted"
            dialog:Hide()
        end)
        dialog.acceptButton = acceptButton

        local declineButton = CreateFrame("Button", "MKADungeonTeleportDialogDeclineButton", dialog, "GameMenuButtonTemplate")
        declineButton:SetSize(TELEPORT_DIALOG_BUTTON_WIDTH, TELEPORT_DIALOG_BUTTON_HEIGHT)
        declineButton:SetPoint("BOTTOMRIGHT", dialog, "BOTTOM", 137, TELEPORT_DIALOG_BUTTON_BOTTOM_OFFSET)
        declineButton:SetText(NO)
        if declineButton.SetNormalFontObject then
            declineButton:SetNormalFontObject("GameFontNormalLarge")
        end
        if declineButton.SetHighlightFontObject then
            declineButton:SetHighlightFontObject("GameFontHighlightLarge")
        end
        declineButton:SetScript("OnClick", function()
            dialog.mkaHideReason = "declined"
            dialog:Hide()
        end)
        dialog.declineButton = declineButton

        local closeButton = dialog.CloseButton or CreateFrame("Button", nil, dialog, "UIPanelCloseButton")
        dialog.closeButton = closeButton

        if core.ApplyFontToFrame then
            core.ApplyFontToFrame(dialog)
        end
        if dialog.message and core.ApplyFont then
            core.ApplyFont(dialog.message, 18, "OUTLINE")
        end
        if dialog.prompt and core.ApplyFont then
            core.ApplyFont(dialog.prompt, 16, "OUTLINE")
        end
        ApplyDungeonTeleportMenuFrameStyle(dialog)

        table.insert(UISpecialFrames, "MKADungeonTeleportDialog")
        dialog:Hide()
        self.dungeonTeleportDialogFrame = dialog
    end

    local isPreview = forcePreview == true
    local sameVisibleRequest = dialog:IsShown()
        and dialog.mapID == mapID
        and dialog.forcePreview == isPreview
    if not sameVisibleRequest then
        dialog.mkaTeleportParticipants = {}
        dialog.mkaTeleportParticipantOrder = {}
    end

    local summarySenderName, summarySenderClass, participantCount = AddDungeonTeleportDialogParticipant(
        dialog,
        senderName,
        senderClass,
        castGUID,
        timeoutSeconds
    )

    dialog.mapID = mapID
    dialog.senderName = summarySenderName
    dialog.senderClass = summarySenderClass
    dialog.dungeonName = dungeonName or self:GetLocalizedDungeonName(mapID)
    dialog.forcePreview = isPreview
    if sameVisibleRequest then
        dialog.incomingTacticalAnnouncementHandled = dialog.incomingTacticalAnnouncementHandled == true
            or incomingTacticalAnnouncementHandled == true
    else
        dialog.incomingTacticalAnnouncementHandled = incomingTacticalAnnouncementHandled == true
    end
    dialog.mkaHideReason = nil
    self:DebugLog(
        "ShowDungeonTeleportDialog showing sender=%s count=%s mapID=%s dungeon=%s spellID=%s spellName=%s preview=%s castGUID=%s",
        tostring(senderName),
        tostring(participantCount),
        tostring(mapID),
        tostring(dialog.dungeonName),
        tostring(spellID),
        tostring(teleportSpellName),
        tostring(forcePreview == true),
        tostring(castGUID)
    )
    if dialog.message.SetSpacing then
        dialog.message:SetSpacing(TELEPORT_DIALOG_MESSAGE_LINE_SPACING)
    end
    RefreshDungeonTeleportDialogParticipants(self, dialog)
    if dialog.prompt then
        dialog.prompt:SetText("")
        dialog.prompt:Hide()
    end
    CenterDungeonTeleportDialog(dialog)
    if dialog.forcePreview then
        QueueSecureButtonAttributes(
            dialog.acceptButton,
            "type", nil,
            "type1", nil,
            "macrotext", nil,
            "macrotext1", nil
        )
    else
        QueueSecureButtonAttributes(
            dialog.acceptButton,
            "type", "macro",
            "type1", "macro",
            "macrotext", teleportMacroText,
            "macrotext1", teleportMacroText
        )
    end
    dialog:Show()
    if not dialog.forcePreview then
        QueueDungeonTeleportDialogTimeout(self, dialog)
    end
    return true
end

local function GetDungeonTeleportOutgoingCastKey(castGUID, spellID)
    return NormalizeDungeonTeleportCastGUID(castGUID) or ("spell:" .. tostring(spellID or "unknown"))
end

local function TryBroadcastDungeonTeleportDialog(self, mapID, reason, castGUID, spellID)
    if not (self and self.BroadcastDungeonTeleportDialog) or type(mapID) ~= "number" then
        return false
    end

    local isCastStart = reason == "UNIT_SPELLCAST_START"
    local isCastSucceeded = reason == "UNIT_SPELLCAST_SUCCEEDED"
    local isCastCancelled = reason == "UNIT_SPELLCAST_INTERRUPTED"
        or reason == "UNIT_SPELLCAST_FAILED"
        or reason == "UNIT_SPELLCAST_FAILED_QUIET"
    local castKey = GetDungeonTeleportOutgoingCastKey(castGUID, spellID)
    local now = GetTime and GetTime() or 0
    self.pendingOutgoingDungeonTeleportCasts = self.pendingOutgoingDungeonTeleportCasts or {}

    if isCastStart then
        local previousCast = self.pendingOutgoingDungeonTeleportCasts[castKey]
        if previousCast and (now - (previousCast.startedAt or 0)) < OUTGOING_DUNGEON_TELEPORT_BROADCAST_DEDUPE_SECONDS then
            if self.DebugLog then
                self:DebugLog(
                    "TryBroadcastDungeonTeleportDialog skipped mapID=%s reason=dedupe source=%s delta=%.2f castGUID=%s",
                    tostring(mapID),
                    tostring(reason),
                    now - (previousCast.startedAt or 0),
                    tostring(castGUID)
                )
            end
            return false
        end

        self.pendingOutgoingDungeonTeleportCasts[castKey] = {
            mapID = mapID,
            startedAt = now,
            castGUID = NormalizeDungeonTeleportCastGUID(castGUID),
            promptSent = self.IsDungeonTeleportPromptEnabled and self:IsDungeonTeleportPromptEnabled() == true,
        }
        self:BroadcastDungeonTeleportDialog(mapID, {
            announcement = true,
            tactical = true,
            prompt = true,
            state = "start",
            castGUID = castGUID,
            timeoutSeconds = INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS,
        })
        return true
    end

    local pendingCast = self.pendingOutgoingDungeonTeleportCasts[castKey]
    if isCastCancelled then
        if pendingCast and pendingCast.promptSent and self.BroadcastDungeonTeleportDialogCancel then
            self:BroadcastDungeonTeleportDialogCancel(mapID, {
                castGUID = castGUID,
                reason = reason,
            })
        end
        self.pendingOutgoingDungeonTeleportCasts[castKey] = nil
        return pendingCast ~= nil
    end

    if isCastSucceeded then
        self.pendingOutgoingDungeonTeleportCasts[castKey] = nil
    end
    return false
end

AttachDungeonTeleportDialogTrigger = function(self, button, getMapID)
    if not self or not button or button.bkDungeonTeleportDialogHooked then
        return
    end

    button.bkDungeonTeleportDialogHooked = true
end

function MKA:HandleIncomingDungeonTeleportDialog(senderName, mapID, dungeonName, dialogMessage, senderClass, incomingTacticalAnnouncementHandled, castGUID, timeoutSeconds)
    if not self:IsDungeonTeleportDialogEnabled() then
        self:DebugLog(
            "HandleIncomingDungeonTeleportDialog skipped sender=%s mapID=%s dungeon=%s reason=setting_disabled",
            tostring(senderName),
            tostring(mapID),
            tostring(dungeonName)
        )
        return false
    end

    if self:IsDungeonTeleportPromptSuppressed(mapID) then
        self:DebugLog(
            "HandleIncomingDungeonTeleportDialog skipped sender=%s mapID=%s dungeon=%s reason=prompt_suppressed",
            tostring(senderName),
            tostring(mapID),
            tostring(dungeonName)
        )
        return false
    end

    local teleportStatus = GetDungeonTeleportStatus(mapID)
    self:DebugLog(
        "HandleIncomingDungeonTeleportDialog sender=%s mapID=%s dungeon=%s localStatus=%s",
        tostring(senderName),
        tostring(mapID),
        tostring(dungeonName),
        tostring(teleportStatus)
    )
    if teleportStatus ~= "ready" then
        return false
    end

    return ShowDungeonTeleportDialog(self, senderName, mapID, dungeonName, dialogMessage, nil, senderClass, incomingTacticalAnnouncementHandled, castGUID, timeoutSeconds)
end

function MKA:HandleIncomingDungeonTeleportDialogCancel(senderName, mapID, castGUID, reason)
    local dialog = self.dungeonTeleportDialogFrame
    if not (dialog and dialog.IsShown and dialog:IsShown() and dialog.mapID == mapID and dialog.forcePreview ~= true) then
        self:DebugLog(
            "HandleIncomingDungeonTeleportDialogCancel skipped sender=%s mapID=%s reason=%s state=no_dialog castGUID=%s",
            tostring(senderName),
            tostring(mapID),
            tostring(reason),
            tostring(castGUID)
        )
        return false
    end

    local removed = RemoveDungeonTeleportDialogParticipant(dialog, senderName, castGUID)
    self:DebugLog(
        "HandleIncomingDungeonTeleportDialogCancel sender=%s mapID=%s reason=%s removed=%s castGUID=%s",
        tostring(senderName),
        tostring(mapID),
        tostring(reason),
        tostring(removed == true),
        tostring(castGUID)
    )
    if not removed then
        return false
    end

    if not RefreshDungeonTeleportDialogParticipants(self, dialog) then
        dialog.mkaHideReason = "cancelled"
        dialog:Hide()
        return true
    end

    QueueDungeonTeleportDialogTimeout(self, dialog)
    return true
end

function MKA:ShowDungeonTeleportDialogTest(mapID)
    mapID = tonumber(mapID) or DUNGEON_TELEPORT_PREVIEW_MAP_ID
    local dungeonName = self.GetLocalizedDungeonName and self:GetLocalizedDungeonName(mapID) or tostring(mapID)
    ShowDungeonTeleportDialog(self, GetDungeonTeleportLocalPlayerName(), mapID, dungeonName, nil, true, select(2, UnitClass("player")))
    return mapID, dungeonName
end

local dungeonTeleportSpellcastWatcher = CreateFrame("Frame")
dungeonTeleportSpellcastWatcher:SetScript("OnEvent", function(_, event, unitTarget, castGUID, spellID)
    if unitTarget ~= "player" then
        return
    end

    local mapID = GetDungeonTeleportMapIDBySpellID(spellID)
    if mapID then
        TryBroadcastDungeonTeleportDialog(MKA, mapID, event, castGUID, spellID)
    end
end)
dungeonTeleportSpellcastWatcher:RegisterUnitEvent("UNIT_SPELLCAST_START", "player")
dungeonTeleportSpellcastWatcher:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
dungeonTeleportSpellcastWatcher:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", "player")
dungeonTeleportSpellcastWatcher:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", "player")
dungeonTeleportSpellcastWatcher:RegisterUnitEvent("UNIT_SPELLCAST_FAILED_QUIET", "player")

local function GetDungeonTeleportDebugFields(tile, mapID)
    local spellID = GetDungeonTeleportSpellID(mapID)
    local status = GetDungeonTeleportStatus(mapID)
    local spellName = GetDungeonTeleportSpellName(mapID)
    local secureType = tile and tile.GetAttribute and tile:GetAttribute("type1") or nil
    local secureAction = tile and tile.GetAttribute and (tile:GetAttribute("spell1") or tile:GetAttribute("macrotext1")) or nil

    return spellID, spellName, status, secureType, secureAction
end

core.DungeonTeleport = {
    GetSpellID = GetDungeonTeleportSpellID,
    GetSpellName = GetDungeonTeleportSpellName,
    GetSpellLink = GetDungeonTeleportSpellLink,
    GetStatus = GetDungeonTeleportStatus,
    GetMacroText = GetDungeonTeleportMacroText,
    UpdateBadge = UpdateDungeonTeleportBadge,
    AddTooltipScoreLine = AddDungeonTooltipScoreLine,
    AddTeleportTooltipLine = AddDungeonTeleportTooltipLine,
    GetDebugFields = GetDungeonTeleportDebugFields,
    AttachDialogTrigger = AttachDungeonTeleportDialogTrigger,
    SendAnnouncement = SendDungeonTeleportAnnouncement,
    FormatPreviewMessage = FormatDungeonTeleportPreviewMessage,
}
