local addonName, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end
local Unpack = unpack or (table and table.unpack)
local IsSecretValue = issecretvalue
local AceSerializer = LibStub("AceSerializer-3.0")
local DungeonTeleport = core.DungeonTeleport or {}
core.DungeonTeleport = DungeonTeleport

local COMM_PREFIX = addonTable.ADDON_SHORT_NAME or "MK"

local TELEPORT_DIALOG_TITLE_KEY = "跟随传送"
local TELEPORT_DIALOG_WIDTH = 480
local TELEPORT_DIALOG_HEIGHT = 210
local TELEPORT_DIALOG_BUTTON_WIDTH = 72
local TELEPORT_DIALOG_BUTTON_HEIGHT = 26
local TELEPORT_DIALOG_BUTTON_GAP = 36
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_BUTTON_TEXT_SIZE = 15
local TELEPORT_DIALOG_BUTTON_PRESSED_TEXT_OFFSET_X = 1
local TELEPORT_DIALOG_BUTTON_PRESSED_TEXT_OFFSET_Y = -1
local TELEPORT_DIALOG_MAIN_WINDOW_ALPHA = 0.64
local TELEPORT_DIALOG_CHAT_MESSAGE_MAX_BYTES = core.DUNGEON_TELEPORT_CHAT_MESSAGE_MAX_BYTES or 255
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 LOCAL_DUNGEON_TELEPORT_ACTIVE_CAST_SECONDS = 20
local PEER_DUNGEON_TELEPORT_FALLBACK_DELAY_SECONDS = 1.25
local PEER_DUNGEON_TELEPORT_COMM_SUPPRESS_SECONDS = 4
local DUNGEON_TELEPORT_PREVIEW_MAP_ID = 560

local QueueSecureButtonAttributes = core.SetSecureButtonAttributes or function() return false end
local GetDungeonTeleportStatus = DungeonTeleport.GetStatus or function() return nil end
local GetDungeonTeleportSpellName = DungeonTeleport.GetSpellName or function() return nil end
local GetDungeonTeleportSpellLink = DungeonTeleport.GetSpellLink or function() return nil end
local GetDungeonTeleportMacroText = DungeonTeleport.GetMacroText or function() return nil end
local GetDungeonTeleportMapIDBySpellID = DungeonTeleport.GetMapIDBySpellID or function() return nil end
local NormalizeDungeonTeleportSpellID = DungeonTeleport.NormalizeSpellID or function(spellID)
    local numericSpellID = tonumber(spellID)
    return type(numericSpellID) == "number" and numericSpellID or nil
end

local function IsDungeonTeleportSecretValue(value)
    if type(IsSecretValue) ~= "function" then
        return false
    end

    local ok, isSecret = pcall(IsSecretValue, value)
    return not ok or isSecret == true
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

    if core.CharacterStore then
        local store = core.CharacterStore
        local entries = {}
        if store.GetRemoteCharacterEntries then
            for _, entry in ipairs(store:GetRemoteCharacterEntries(self)) do
                entries[#entries + 1] = entry
            end
        end
        if store.GetLocalCharacterEntries then
            for _, entry in ipairs(store:GetLocalCharacterEntries(self, true)) do
                entries[#entries + 1] = entry
            end
        end
        for _, entry in ipairs(entries) do
            local character = entry and entry.data
            if type(character) == "table"
                and character.class
                and (
                    DungeonTeleportNamesMatch(character.name, senderName)
                    or DungeonTeleportNamesMatch(entry.key, senderName)
                    or DungeonTeleportNamesMatch(entry.owner, senderName)
                )
            then
                return character.class
            end
        end
    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 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 ""
    if core.TruncateUtf8ToByteLimit then
        message = core.TruncateUtf8ToByteLimit(message, TELEPORT_DIALOG_CHAT_MESSAGE_MAX_BYTES)
    end
    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("{player} 正在使用传送[{spell}]，即将前往[{dungeon}]！")
    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("%s 正在传送前往 [%s] %s"),
        FormatDungeonTeleportSenderText(self, senderName, senderClass, participantCount),
        WrapDungeonTeleportTextColor(dungeonText, TELEPORT_DIALOG_DUNGEON_COLOR),
        WrapDungeonTeleportTextColor(TRANSLATE("是否跟随传送"), 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 IsDungeonTeleportSecretValue(castGUID) or (type(castGUID) ~= "string" and type(castGUID) ~= "number") then
        return nil
    end
    local ok, value = pcall(tostring, castGUID)
    if not ok then
        return nil
    end
    if value == "" then
        return nil
    end
    return value
end

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

local function PruneDungeonTeleportTimedRecords(records, now)
    if type(records) ~= "table" then
        return
    end

    now = now or (GetTime and GetTime() or 0)
    for key, record in pairs(records) do
        if not record or (tonumber(record.expiresAt) or 0) <= now then
            records[key] = nil
        end
    end
end

local function MarkLocalDungeonTeleportCastActive(self, mapID, castGUID, spellID, source)
    if not (self and type(mapID) == "number") then
        return
    end

    local now = GetTime and GetTime() or 0
    local castKey = GetDungeonTeleportOutgoingCastKey(castGUID, spellID)
    local record = {
        mapID = mapID,
        castGUID = NormalizeDungeonTeleportCastGUID(castGUID),
        source = source,
        expiresAt = now + LOCAL_DUNGEON_TELEPORT_ACTIVE_CAST_SECONDS,
    }
    self.activeLocalDungeonTeleportCasts = self.activeLocalDungeonTeleportCasts or {}
    self.activeLocalDungeonTeleportCasts[castKey] = record
    self.activeLocalDungeonTeleportCasts["map:" .. tostring(mapID)] = record

    if self.DebugLog then
        self:DebugLog(
            "MarkLocalDungeonTeleportCastActive mapID=%s source=%s castGUID=%s",
            tostring(mapID),
            tostring(source),
            tostring(record.castGUID)
        )
    end
end

local function ClearLocalDungeonTeleportCastActive(self, mapID, castGUID, spellID)
    if type(self and self.activeLocalDungeonTeleportCasts) ~= "table" then
        return
    end

    local records = self.activeLocalDungeonTeleportCasts
    local castKey = GetDungeonTeleportOutgoingCastKey(castGUID, spellID)
    local mapKey = type(mapID) == "number" and ("map:" .. tostring(mapID))
    local record = records[castKey]
    records[castKey] = nil
    if mapKey and (not record or records[mapKey] == record) then
        records[mapKey] = nil
    end
end

local function IsLocalDungeonTeleportCastActive(self, mapID)
    if type(self and self.activeLocalDungeonTeleportCasts) ~= "table" or type(mapID) ~= "number" then
        return false
    end

    local now = GetTime and GetTime() or 0
    local records = self.activeLocalDungeonTeleportCasts
    PruneDungeonTeleportTimedRecords(records, now)

    local record = records["map:" .. tostring(mapID)]
    return record ~= nil and (tonumber(record.expiresAt) or 0) > now
end

function MK:IsLocalDungeonTeleportCastActive(mapID)
    return IsLocalDungeonTeleportCastActive(self, mapID)
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.mkTeleportParticipants = dialog.mkTeleportParticipants or {}
    dialog.mkTeleportParticipantOrder = dialog.mkTeleportParticipantOrder or {}

    local key = GetDungeonTeleportParticipantKey(displayName)
    local participant = dialog.mkTeleportParticipants[key]
    if not participant then
        participant = {
            name = displayName,
            class = senderClass,
        }
        dialog.mkTeleportParticipants[key] = participant
        table.insert(dialog.mkTeleportParticipantOrder, 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.mkTeleportParticipantOrder[1]
    local firstParticipant = firstKey and dialog.mkTeleportParticipants[firstKey] or participant
    local count = #dialog.mkTeleportParticipantOrder
    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.mkTeleportParticipants) == "table" and type(dialog.mkTeleportParticipantOrder) == "table") then
        return false
    end

    local normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    local targetKey
    if normalizedCastGUID then
        for key, participant in pairs(dialog.mkTeleportParticipants) 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.mkTeleportParticipants[targetKey] then
        return false
    end

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

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

    local firstKey = dialog.mkTeleportParticipantOrder[1]
    local firstParticipant = firstKey and dialog.mkTeleportParticipants[firstKey]
    local participantCount = #dialog.mkTeleportParticipantOrder
    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.mkTeleportParticipants) == "table") then
        return
    end

    local nextExpiresAt
    for _, participant in pairs(dialog.mkTeleportParticipants) 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.mkTeleportTimeoutTicket = (dialog.mkTeleportTimeoutTicket or 0) + 1
    local ticket = dialog.mkTeleportTimeoutTicket
    local delay = math.max(0.1, nextExpiresAt - (GetTime and GetTime() or 0))
    C_Timer.After(delay, function()
        if not (dialog and dialog.mkTeleportTimeoutTicket == ticket and dialog.IsShown and dialog:IsShown() and dialog.forcePreview ~= true) then
            return
        end

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

        if not RefreshDungeonTeleportDialogParticipants(self, dialog) then
            dialog.mkHideReason = "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


function MK:IsDungeonTeleportFollowEnabled()
    local profile = self.db and self.db.profile
    if not profile then
        return false
    end
    if profile.dungeonTeleportPrompt ~= nil then
        return profile.dungeonTeleportPrompt == true
    end
    return false
end

---@param enabled boolean
function MK:SetDungeonTeleportFollowEnabled(enabled)
    if not self.db or not self.db.profile then
        return
    end

    self.db.profile.dungeonTeleportPrompt = enabled and true or false
    if self.RefreshDungeonTeleportPeerSpellcastWatcher then
        self:RefreshDungeonTeleportPeerSpellcastWatcher("SetDungeonTeleportFollowEnabled")
    end

    if self.IsMainWindowShown and self:IsMainWindowShown() then
        self:RefreshMainWindow("SetDungeonTeleportFollowEnabled")
    end
end

function MK:BroadcastDungeonTeleportDialog(mapID, options)
    options = type(options) == "table" and options or {}
    local announcementEnabled = options.announcement ~= false and self:IsDungeonTeleportAnnouncementEnabled()
    local followPromptEnabled = options.prompt ~= false and self:IsDungeonTeleportFollowEnabled()
    local teleportState = type(options.state) == "string" and options.state or "start"
    local castGUID = NormalizeDungeonTeleportCastGUID(options.castGUID)
    local tacticalMessage = self:GetDungeonTeleportTacticalAnnouncement(mapID)
    local tacticalAnnouncementEnabled = options.tactical ~= false
        and type(tacticalMessage) == "string"
        and tacticalMessage:match("%S") ~= nil
    if not announcementEnabled and not followPromptEnabled and not tacticalAnnouncementEnabled then
        self:DebugLog("BroadcastDungeonTeleportDialog skipped mapID=%s reason=settings_disabled", tostring(mapID))
        return false
    end

    local channel = self:GetGroupChannel()
    if not channel or type(mapID) ~= "number" then
        self:DebugLog(
            "BroadcastDungeonTeleportDialog skipped mapID=%s channel=%s reason=%s",
            tostring(mapID),
            tostring(channel),
            type(mapID) ~= "number" and "invalid_map" or "no_channel"
        )
        return false
    end

    local payload = {
        messageType = "teleport_dialog",
        sender = GetDungeonTeleportLocalPlayerName(),
        senderClass = select(2, UnitClass("player")),
        mapID = mapID,
        dungeonName = self:GetLocalizedDungeonName(mapID),
        requestedAt = time(),
        teleportState = teleportState,
        castGUID = castGUID,
        timeoutSeconds = options.timeoutSeconds,
    }

    if announcementEnabled then
        SendDungeonTeleportAnnouncement(self, mapID, payload.dungeonName, self:GetDungeonTeleportDialogMessage())
    end
    local tacticalAnnouncementHandled = false
    if tacticalAnnouncementEnabled then
        tacticalAnnouncementHandled = self:BroadcastDungeonTeleportTacticalAnnouncement(mapID, "active_teleport") == true
    end
    payload.tacticalAnnouncementHandled = tacticalAnnouncementHandled

    if not followPromptEnabled then
        self:DebugLog(
            "BroadcastDungeonTeleportDialog chat_only mapID=%s dungeon=%s channel=%s announcement=%s tactical=%s follow_prompt=%s",
            tostring(mapID),
            tostring(payload.dungeonName),
            tostring(channel),
            tostring(announcementEnabled),
            tostring(tacticalAnnouncementEnabled),
            tostring(followPromptEnabled)
        )
        return false
    end

    self:DebugLog(
        "BroadcastDungeonTeleportDialog sending mapID=%s dungeon=%s channel=%s announcement=%s tactical=%s follow_prompt=%s state=%s castGUID=%s",
        tostring(mapID),
        tostring(payload.dungeonName),
        tostring(channel),
        tostring(announcementEnabled),
        tostring(tacticalAnnouncementEnabled),
        tostring(followPromptEnabled),
        tostring(teleportState),
        tostring(castGUID)
    )

    local serialized = AceSerializer:Serialize(payload)
    self:SendCommMessage(COMM_PREFIX, serialized, channel, nil, "ALERT")
    return true
end

function MK:BroadcastDungeonTeleportDialogCancel(mapID, options)
    options = type(options) == "table" and options or {}
    local castGUID = NormalizeDungeonTeleportCastGUID(options.castGUID)
    local channel = self:GetGroupChannel()
    if not channel or type(mapID) ~= "number" then
        self:DebugLog(
            "BroadcastDungeonTeleportDialogCancel skipped mapID=%s channel=%s reason=%s castGUID=%s",
            tostring(mapID),
            tostring(channel),
            type(mapID) ~= "number" and "invalid_map" or "no_channel",
            tostring(castGUID)
        )
        return false
    end

    local payload = {
        messageType = "teleport_dialog",
        teleportState = "cancel",
        sender = GetDungeonTeleportLocalPlayerName(),
        senderClass = select(2, UnitClass("player")),
        mapID = mapID,
        dungeonName = self:GetLocalizedDungeonName(mapID),
        requestedAt = time(),
        castGUID = castGUID,
        cancelReason = options.reason,
    }

    self:DebugLog(
        "BroadcastDungeonTeleportDialogCancel sending mapID=%s dungeon=%s channel=%s reason=%s castGUID=%s",
        tostring(mapID),
        tostring(payload.dungeonName),
        tostring(channel),
        tostring(options.reason),
        tostring(castGUID)
    )

    local serialized = AceSerializer:Serialize(payload)
    self:SendCommMessage(COMM_PREFIX, serialized, channel, nil, "ALERT")
    return true
end

function MK:HandleDungeonTeleportCommPayload(payload, sender, distribution)
    local castGUID = NormalizeDungeonTeleportCastGUID(payload and payload.castGUID)
    self:DebugLog(
        "OnCommReceived teleport_dialog sender=%s distribution=%s payloadSender=%s mapID=%s dungeon=%s state=%s castGUID=%s",
        tostring(sender),
        tostring(distribution),
        tostring(payload and payload.sender),
        tostring(payload and payload.mapID),
        tostring(payload and payload.dungeonName),
        tostring(payload and payload.teleportState),
        tostring(castGUID)
    )

    local mapID = tonumber(payload and payload.mapID)
    if not mapID then
        self:DebugLog("OnCommReceived teleport_dialog ignored reason=invalid_map payloadMapID=%s", tostring(payload and payload.mapID))
        return false
    end

    local teleportState = type(payload.teleportState) == "string" and payload.teleportState or "start"
    if teleportState == "cancel" then
        if self.HandleIncomingDungeonTeleportDialogCancel then
            self:HandleIncomingDungeonTeleportDialogCancel(
                payload.sender or sender,
                mapID,
                castGUID,
                payload.cancelReason
            )
        else
            self:DebugLog("OnCommReceived teleport_dialog cancel ignored reason=no_cancel_handler")
        end
        return true
    end

    if self.NoteIncomingDungeonTeleportDialogComm then
        self:NoteIncomingDungeonTeleportDialogComm(payload.sender or sender, mapID, castGUID)
    end

    if not self:IsDungeonTeleportFollowEnabled() then
        self:DebugLog("OnCommReceived teleport_dialog ignored reason=follow_disabled")
        return false
    end

    if self.IsDungeonTeleportPromptSuppressed and self:IsDungeonTeleportPromptSuppressed(mapID) then
        self:DebugLog("OnCommReceived teleport_dialog ignored reason=prompt_suppressed mapID=%s", tostring(mapID))
        return false
    end

    if self.IsLocalDungeonTeleportCastActive and self:IsLocalDungeonTeleportCastActive(mapID) then
        self:DebugLog("OnCommReceived teleport_dialog ignored reason=local_cast_active mapID=%s", tostring(mapID))
        return false
    end

    if self.HandleIncomingDungeonTeleportDialog then
        self:DebugLog(
            "OnCommReceived teleport_dialog forwarding sender=%s mapID=%s dungeon=%s",
            tostring(payload.sender or sender),
            tostring(mapID),
            tostring(payload.dungeonName)
        )
        self:HandleIncomingDungeonTeleportDialog(
            payload.sender or sender,
            mapID,
            payload.dungeonName,
            payload.dialogMessage,
            payload.senderClass,
            payload.tacticalAnnouncementHandled == true,
            castGUID,
            payload.timeoutSeconds
        )
        return true
    end

    self:DebugLog("OnCommReceived teleport_dialog ignored reason=no_handler")
    return false
end

local function ApplyDungeonTeleportMenuFrameStyle(dialog)
    if not dialog or dialog.mkTeleportMenuFrameStyled 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.mkTeleportBackgroundPieces then
        dialog.mkTeleportBackgroundPieces = {}
        for index = 1, 3 do
            dialog.mkTeleportBackgroundPieces[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.mkTeleportBackgroundPieces) 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.mkTeleportBackgroundPieces[1]
    topBackground:SetPoint("TOPLEFT", dialog, "TOPLEFT", backgroundLeftInset + backgroundCornerRadius, -backgroundInset)
    topBackground:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -(backgroundInset + backgroundCornerRadius), -backgroundInset)
    topBackground:SetHeight(backgroundCornerRadius)

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

    local bottomBackground = dialog.mkTeleportBackgroundPieces[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.mkTeleportTitleBasePoint then
                titleText.mkTeleportTitleBasePoint = { titleText:GetPoint(1) }
            end
            local basePoint = titleText.mkTeleportTitleBasePoint
            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.mkTeleportMenuFrameStyled = 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 CenterDungeonTeleportDialogButtonText(button)
    local text = button and (button.Label or button.Text or (button.GetFontString and button:GetFontString()))
    if not text then
        return
    end

    text:SetJustifyH("CENTER")
    text:SetJustifyV("MIDDLE")
end

local function SetDungeonTeleportDialogButtonLabelOffset(button, pressed)
    local label = button and button.Label
    if not label then
        return
    end

    label:ClearAllPoints()
    label:SetPoint(
        "CENTER",
        button,
        "CENTER",
        pressed and TELEPORT_DIALOG_BUTTON_PRESSED_TEXT_OFFSET_X or 0,
        pressed and TELEPORT_DIALOG_BUTTON_PRESSED_TEXT_OFFSET_Y or 0
    )
end

local function SetDungeonTeleportDialogButtonVisual(button, state)
    if not button then
        return
    end

    local disabled = button.mkTeleportDialogButtonDisabled == true
    local pressed = state == "pressed" and not disabled
    local hovered = state == "hover" and not disabled
    button.mkMeetingStoneButtonPressed = pressed and true or nil
    if button.HoverTexture then
        button.HoverTexture:SetShown(hovered)
    end
    if button.PressedTexture then
        button.PressedTexture:SetShown(pressed)
    end
    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
        core.MeetingStoneButtonArt.Update(button)
    end

    if button.Label then
        SetDungeonTeleportDialogButtonLabelOffset(button, pressed)
        if disabled then
            button.Label:SetTextColor(0.55, 0.45, 0.34, 0.8)
        elseif pressed then
            button.Label:SetTextColor(1, 0.92, 0.48, 1)
        elseif hovered then
            button.Label:SetTextColor(1, 1, 0.72, 1)
        elseif button.mkDanger then
            button.Label:SetTextColor(1, 0.82, 0, 1)
        else
            button.Label:SetTextColor(1, 0.82, 0, 1)
        end
    end
end

local function CreateDungeonTeleportDialogButton(parent, name, text, secure, danger)
    local button
    if secure then
        button = CreateFrame("Button", name, parent, "SecureActionButtonTemplate,UIPanelButtonTemplate")
    else
        local createSystemButton = core.MeetingStoneForm and core.MeetingStoneForm.CreateSystemButton
        button = createSystemButton and createSystemButton(parent, name, TELEPORT_DIALOG_BUTTON_WIDTH, TELEPORT_DIALOG_BUTTON_HEIGHT, text, 0)
            or CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
    end

    button:SetSize(TELEPORT_DIALOG_BUTTON_WIDTH, TELEPORT_DIALOG_BUTTON_HEIGHT)
    if button.SetText then
        button:SetText(text or "")
    end
    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Apply then
        core.MeetingStoneButtonArt.Apply(button)
    end

    local label = button.Text or (button.GetFontString and button:GetFontString())
    if label then
        label:ClearAllPoints()
        label:SetPoint("CENTER", button, "CENTER", 0, 0)
        label:SetJustifyH("CENTER")
        label:SetJustifyV("MIDDLE")
        if core.ApplyFont then
            core.ApplyFont(label, TELEPORT_DIALOG_BUTTON_TEXT_SIZE, "")
        end
    end
    button.Label = label
    button.mkDanger = danger == true
    CenterDungeonTeleportDialogButtonText(button)
    SetDungeonTeleportDialogButtonVisual(button, "normal")

    button:SetScript("OnEnter", function(frame)
        SetDungeonTeleportDialogButtonVisual(frame, "hover")
    end)
    button:SetScript("OnLeave", function(frame)
        SetDungeonTeleportDialogButtonVisual(frame, "normal")
    end)
    button:SetScript("OnMouseDown", function(frame, mouseButton)
        if mouseButton == "LeftButton" then
            SetDungeonTeleportDialogButtonVisual(frame, "pressed")
        end
    end)
    button:SetScript("OnMouseUp", function(frame)
        if frame.IsMouseOver and frame:IsMouseOver() then
            SetDungeonTeleportDialogButtonVisual(frame, "hover")
        else
            SetDungeonTeleportDialogButtonVisual(frame, "normal")
        end
    end)

    return button
end

local function GetDungeonTeleportDialogMainWindow(dialog)
    local owner = dialog and dialog.mkTeleportOwner
    local mainWindow = owner and owner.mainWindow
    if mainWindow and mainWindow.frame then
        mainWindow = mainWindow.frame
    end
    if mainWindow and mainWindow.SetAlpha then
        return mainWindow
    end
    return nil
end

local function SetDungeonTeleportDialogMainWindowAlpha(dialog, transparent)
    local mainWindow = GetDungeonTeleportDialogMainWindow(dialog)
    if not mainWindow then
        return
    end

    if transparent then
        if mainWindow.IsShown and not mainWindow:IsShown() then
            return
        end
        if dialog.mkMainWindowPreviousAlpha == nil then
            dialog.mkMainWindowPreviousAlpha = mainWindow.GetAlpha and mainWindow:GetAlpha() or 1
        end
        mainWindow:SetAlpha(TELEPORT_DIALOG_MAIN_WINDOW_ALPHA)
        return
    end

    if dialog.mkMainWindowPreviousAlpha == nil then
        return
    end
    local previousAlpha = tonumber(dialog.mkMainWindowPreviousAlpha) or 1
    dialog.mkMainWindowPreviousAlpha = nil
    mainWindow:SetAlpha(previousAlpha)
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 MK:SuppressDungeonTeleportPrompt(mapID, reason)
    SuppressDungeonTeleportPrompt(self, mapID, reason)
end

function MK: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", "MKDungeonTeleportDialog", 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("OnShow", function(frame)
            SetDungeonTeleportDialogMainWindowAlpha(frame, true)
        end)
        dialog:SetScript("OnHide", function(frame)
            SetDungeonTeleportDialogMainWindowAlpha(frame, false)
            local hideReason = frame.mkHideReason or "dismissed"
            if frame.mapID and not frame.forcePreview and hideReason ~= "cancelled" and hideReason ~= "expired" then
                SuppressDungeonTeleportPrompt(self, frame.mapID, hideReason)
            end
            frame.mkHideReason = nil
            frame.mkTeleportTimeoutTicket = (frame.mkTeleportTimeoutTicket or 0) + 1
            frame.mkTeleportParticipants = {}
            frame.mkTeleportParticipantOrder = {}
        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 = CreateDungeonTeleportDialogButton(
            dialog,
            "MKDungeonTeleportDialogAcceptButton",
            TRANSLATE("是"),
            true,
            true
        )
        acceptButton:SetPoint(
            "BOTTOM",
            dialog,
            "BOTTOM",
            -((TELEPORT_DIALOG_BUTTON_WIDTH + TELEPORT_DIALOG_BUTTON_GAP) / 2),
            TELEPORT_DIALOG_BUTTON_BOTTOM_OFFSET
        )
        acceptButton:RegisterForClicks("AnyUp", "AnyDown")
        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")
        SetDungeonTeleportDialogButtonVisual(acceptButton, "normal")
        acceptButton:HookScript("OnClick", function()
            dialog.mkHideReason = "accepted"
            dialog:Hide()
        end)
        dialog.acceptButton = acceptButton

        local declineButton = CreateDungeonTeleportDialogButton(
            dialog,
            "MKDungeonTeleportDialogDeclineButton",
            TRANSLATE("否")
        )
        declineButton:SetPoint(
            "BOTTOM",
            dialog,
            "BOTTOM",
            (TELEPORT_DIALOG_BUTTON_WIDTH + TELEPORT_DIALOG_BUTTON_GAP) / 2,
            TELEPORT_DIALOG_BUTTON_BOTTOM_OFFSET
        )
        declineButton:SetScript("OnClick", function()
            dialog.mkHideReason = "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, "MKDungeonTeleportDialog")
        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.mkTeleportParticipants = {}
        dialog.mkTeleportParticipantOrder = {}
    end

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

    dialog.mapID = mapID
    dialog.mkTeleportOwner = self
    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.mkHideReason = nil
    local normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    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(normalizedCastGUID)
    )
    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 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 normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    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(normalizedCastGUID)
                )
            end
            return false
        end

        local pendingCast = {
            mapID = mapID,
            startedAt = now,
            castGUID = normalizedCastGUID,
            promptSent = false,
        }
        self.pendingOutgoingDungeonTeleportCasts[castKey] = pendingCast
        MarkLocalDungeonTeleportCastActive(self, mapID, castGUID, spellID, "outgoing")
        pendingCast.promptSent = self:BroadcastDungeonTeleportDialog(mapID, {
            announcement = true,
            tactical = true,
            prompt = true,
            state = "start",
            castGUID = normalizedCastGUID,
            timeoutSeconds = INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS,
        }) == true
        return true
    end

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

    if isCastSucceeded then
        self.pendingOutgoingDungeonTeleportCasts[castKey] = nil
        ClearLocalDungeonTeleportCastActive(self, mapID, castGUID, spellID)
    end
    return false
end

local function UpdateDungeonTeleportCastVisual(self, mapID, event)
    if not (
        self
        and type(mapID) == "number"
        and type(self.SetDungeonTeleportCastVisualState) == "function"
    ) then
        return
    end

    if event == "UNIT_SPELLCAST_START" then
        self:SetDungeonTeleportCastVisualState(mapID, true, event)
        return
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED"
        or event == "UNIT_SPELLCAST_INTERRUPTED"
        or event == "UNIT_SPELLCAST_FAILED"
        or event == "UNIT_SPELLCAST_FAILED_QUIET"
    then
        self:SetDungeonTeleportCastVisualState(mapID, false, event)
    end
end

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

    button.bkDungeonTeleportDialogHooked = true
end

function MK:HandleIncomingDungeonTeleportDialog(senderName, mapID, dungeonName, dialogMessage, senderClass, incomingTacticalAnnouncementHandled, castGUID, timeoutSeconds)
    if not self:IsDungeonTeleportFollowEnabled() 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 MK:HandleIncomingDungeonTeleportDialogCancel(senderName, mapID, castGUID, reason)
    local normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    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(normalizedCastGUID)
        )
        return false
    end

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

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

    QueueDungeonTeleportDialogTimeout(self, dialog)
    return true
end

function MK: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 PEER_DUNGEON_TELEPORT_SPELLCAST_EVENTS = {
    "UNIT_SPELLCAST_START",
    "UNIT_SPELLCAST_INTERRUPTED",
    "UNIT_SPELLCAST_FAILED",
    "UNIT_SPELLCAST_FAILED_QUIET",
}

local function GetDungeonTeleportFallbackKeys(senderName, mapID, castGUID)
    local keys = {}
    local normalizedCastGUID = NormalizeDungeonTeleportCastGUID(castGUID)
    if normalizedCastGUID then
        keys[#keys + 1] = "cast:" .. normalizedCastGUID
    end

    if type(mapID) == "number" then
        keys[#keys + 1] = "sender:" .. GetDungeonTeleportParticipantKey(senderName) .. ":" .. tostring(mapID)
    end

    return keys
end

local function GetPrimaryDungeonTeleportFallbackKey(senderName, mapID, castGUID)
    local keys = GetDungeonTeleportFallbackKeys(senderName, mapID, castGUID)
    return keys[1], keys
end

local function NoteDungeonTeleportIncomingComm(self, senderName, mapID, castGUID)
    if not (self and type(mapID) == "number") then
        return
    end

    local keys = GetDungeonTeleportFallbackKeys(senderName, mapID, castGUID)
    if #keys == 0 then
        return
    end

    local now = GetTime and GetTime() or 0
    self.recentIncomingDungeonTeleportDialogComm = self.recentIncomingDungeonTeleportDialogComm or {}
    for _, key in ipairs(keys) do
        self.recentIncomingDungeonTeleportDialogComm[key] = now
        local pending = self.pendingPeerDungeonTeleportFallbacks and self.pendingPeerDungeonTeleportFallbacks[key]
        if pending then
            pending.cancelled = true
            pending.cancelReason = "comm_received"
            self.pendingPeerDungeonTeleportFallbacks[key] = nil
        end
    end
end

function MK:NoteIncomingDungeonTeleportDialogComm(senderName, mapID, castGUID)
    NoteDungeonTeleportIncomingComm(self, senderName, mapID, castGUID)
end

local function HasRecentDungeonTeleportIncomingComm(self, senderName, mapID, castGUID)
    if not (self and type(self.recentIncomingDungeonTeleportDialogComm) == "table") then
        return false
    end

    local keys = GetDungeonTeleportFallbackKeys(senderName, mapID, castGUID)
    if #keys == 0 then
        return false
    end

    local now = GetTime and GetTime() or 0
    for _, key in ipairs(keys) do
        local previousAt = self.recentIncomingDungeonTeleportDialogComm[key]
        if previousAt and (now - previousAt) < PEER_DUNGEON_TELEPORT_COMM_SUPPRESS_SECONDS then
            return true
        end
        if previousAt then
            self.recentIncomingDungeonTeleportDialogComm[key] = nil
        end
    end
    return false
end

local function GetDungeonTeleportPeerUnits()
    local units = {}
    if IsInRaid and IsInRaid() then
        local memberCount = GetNumGroupMembers and GetNumGroupMembers() or 0
        for index = 1, math.min(memberCount, 40) do
            local unit = "raid" .. index
            if UnitExists and UnitExists(unit) and not (UnitIsUnit and UnitIsUnit(unit, "player")) then
                units[#units + 1] = unit
            end
        end
        return units
    end

    if IsInGroup and IsInGroup() then
        for index = 1, 4 do
            local unit = "party" .. index
            if UnitExists and UnitExists(unit) then
                units[#units + 1] = unit
            end
        end
    end
    return units
end

local ClearDungeonTeleportPeerFallbackRecord

local function FindDungeonTeleportPeerFallbackRecord(records, keys)
    if type(records) ~= "table" or type(keys) ~= "table" then
        return nil
    end

    local now = GetTime and GetTime() or 0
    for _, key in ipairs(keys) do
        local record = records[key]
        if record and (not record.expiresAt or (tonumber(record.expiresAt) or 0) > now) then
            return record
        end
        if record then
            ClearDungeonTeleportPeerFallbackRecord(records, record)
        end
    end
    return nil
end

function ClearDungeonTeleportPeerFallbackRecord(records, record)
    if type(records) ~= "table" or type(record) ~= "table" or type(record.keys) ~= "table" then
        return
    end

    for _, key in ipairs(record.keys) do
        if records[key] == record then
            records[key] = nil
        end
    end
end

local function StoreDungeonTeleportPeerFallbackRecord(records, record)
    if type(records) ~= "table" or type(record) ~= "table" or type(record.keys) ~= "table" then
        return
    end

    for _, key in ipairs(record.keys) do
        records[key] = record
    end
end

local function HandlePeerDungeonTeleportFallbackStart(self, unitTarget, castGUID, spellID, mapID)
    if not (
        self
        and self.IsDungeonTeleportFollowEnabled
        and self:IsDungeonTeleportFollowEnabled()
        and C_Timer
        and C_Timer.After
    ) then
        return false
    end

    local senderName = GetDungeonTeleportUnitFullName(unitTarget)
    if type(senderName) ~= "string" or senderName == "" then
        return false
    end

    if HasRecentDungeonTeleportIncomingComm(self, senderName, mapID, castGUID) then
        return false
    end

    local key, keys = GetPrimaryDungeonTeleportFallbackKey(senderName, mapID, castGUID)
    if not key then
        return false
    end

    self.pendingPeerDungeonTeleportFallbacks = self.pendingPeerDungeonTeleportFallbacks or {}
    if FindDungeonTeleportPeerFallbackRecord(self.pendingPeerDungeonTeleportFallbacks, keys) then
        return false
    end

    local record = {
        key = key,
        keys = keys,
        unit = unitTarget,
        senderName = senderName,
        senderClass = UnitClass and select(2, UnitClass(unitTarget)) or nil,
        mapID = mapID,
        castGUID = NormalizeDungeonTeleportCastGUID(castGUID),
        expiresAt = (GetTime and GetTime() or 0)
            + PEER_DUNGEON_TELEPORT_FALLBACK_DELAY_SECONDS
            + INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS,
    }
    StoreDungeonTeleportPeerFallbackRecord(self.pendingPeerDungeonTeleportFallbacks, record)

    C_Timer.After(PEER_DUNGEON_TELEPORT_FALLBACK_DELAY_SECONDS, function()
        if record.cancelled or not self.pendingPeerDungeonTeleportFallbacks then
            return
        end
        if self.pendingPeerDungeonTeleportFallbacks[record.key] ~= record then
            return
        end

        ClearDungeonTeleportPeerFallbackRecord(self.pendingPeerDungeonTeleportFallbacks, record)
        if HasRecentDungeonTeleportIncomingComm(self, record.senderName, record.mapID, record.castGUID) then
            return
        end

        local dungeonName = self.GetLocalizedDungeonName and self:GetLocalizedDungeonName(record.mapID) or tostring(record.mapID)
        local shown = self.HandleIncomingDungeonTeleportDialog and self:HandleIncomingDungeonTeleportDialog(
            record.senderName,
            record.mapID,
            dungeonName,
            nil,
            record.senderClass,
            false,
            record.castGUID,
            INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS
        )
        if shown then
            self.activePeerDungeonTeleportFallbacks = self.activePeerDungeonTeleportFallbacks or {}
            record.expiresAt = (GetTime and GetTime() or 0) + INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS
            StoreDungeonTeleportPeerFallbackRecord(self.activePeerDungeonTeleportFallbacks, record)
            C_Timer.After(INCOMING_DUNGEON_TELEPORT_PROMPT_ACTIVE_SECONDS, function()
                ClearDungeonTeleportPeerFallbackRecord(self.activePeerDungeonTeleportFallbacks, record)
            end)
        end
    end)

    return true
end

local function HandlePeerDungeonTeleportFallbackCancel(self, unitTarget, castGUID, spellID, mapID, reason)
    if not self then
        return false
    end

    local senderName = GetDungeonTeleportUnitFullName(unitTarget)
    if type(senderName) ~= "string" or senderName == "" then
        return false
    end

    local _, keys = GetPrimaryDungeonTeleportFallbackKey(senderName, mapID, castGUID)
    local pending = FindDungeonTeleportPeerFallbackRecord(self.pendingPeerDungeonTeleportFallbacks, keys)
    if pending then
        pending.cancelled = true
        pending.cancelReason = reason
        ClearDungeonTeleportPeerFallbackRecord(self.pendingPeerDungeonTeleportFallbacks, pending)
        return true
    end

    local active = FindDungeonTeleportPeerFallbackRecord(self.activePeerDungeonTeleportFallbacks, keys)
    if not active then
        return false
    end

    ClearDungeonTeleportPeerFallbackRecord(self.activePeerDungeonTeleportFallbacks, active)
    if self.HandleIncomingDungeonTeleportDialogCancel then
        return self:HandleIncomingDungeonTeleportDialogCancel(senderName, mapID or active.mapID, castGUID, reason)
    end
    return false
end

local dungeonTeleportPeerSpellcastWatcher = CreateFrame("Frame")

function MK:RefreshDungeonTeleportPeerSpellcastWatcher(reason)
    for _, eventName in ipairs(PEER_DUNGEON_TELEPORT_SPELLCAST_EVENTS) do
        dungeonTeleportPeerSpellcastWatcher:UnregisterEvent(eventName)
    end

    if not (
        self.IsDungeonTeleportFollowEnabled
        and self:IsDungeonTeleportFollowEnabled()
    ) then
        return false
    end

    local units = GetDungeonTeleportPeerUnits()
    if #units <= 0 then
        return false
    end

    local registerUnitEvent = dungeonTeleportPeerSpellcastWatcher.RegisterUnitEvent
    if type(registerUnitEvent) ~= "function" or type(Unpack) ~= "function" then
        return false
    end

    local registeredAny = false
    for _, eventName in ipairs(PEER_DUNGEON_TELEPORT_SPELLCAST_EVENTS) do
        local ok, registered = pcall(
            registerUnitEvent,
            dungeonTeleportPeerSpellcastWatcher,
            eventName,
            Unpack(units)
        )
        registeredAny = registeredAny or (ok and registered ~= false)
    end

    if self.DebugLog then
        self:DebugLog(
            "RefreshDungeonTeleportPeerSpellcastWatcher enabled=%s units=%s reason=%s registered=%s",
            tostring(true),
            tostring(#units),
            tostring(reason),
            tostring(registeredAny)
        )
    end
    return registeredAny
end

local function GetDungeonTeleportCurrentCastSpellID(unitTarget)
    if not (unitTarget and UnitCastingInfo) then
        return nil
    end

    local ok, spellID = pcall(function()
        local _, _, _, _, _, _, _, _, castingSpellID = UnitCastingInfo(unitTarget)
        return castingSpellID
    end)
    if ok then
        return NormalizeDungeonTeleportSpellID(spellID)
    end
    return nil
end

local function GetDungeonTeleportEventSpellID(unitTarget, spellID)
    return NormalizeDungeonTeleportSpellID(spellID) or GetDungeonTeleportCurrentCastSpellID(unitTarget)
end

local function HandlePeerDungeonTeleportSpellcastEvent(self, event, unitTarget, castGUID, spellID)
    if not (
        self
        and unitTarget
        and (not UnitIsUnit or not UnitIsUnit(unitTarget, "player"))
        and self.IsDungeonTeleportFollowEnabled
        and self:IsDungeonTeleportFollowEnabled()
    ) then
        return
    end

    local readableSpellID = GetDungeonTeleportEventSpellID(unitTarget, spellID)
    local mapID = GetDungeonTeleportMapIDBySpellID(readableSpellID)

    if event == "UNIT_SPELLCAST_START" then
        if mapID then
            HandlePeerDungeonTeleportFallbackStart(self, unitTarget, castGUID, readableSpellID, mapID)
        end
        return
    end

    if event == "UNIT_SPELLCAST_INTERRUPTED"
        or event == "UNIT_SPELLCAST_FAILED"
        or event == "UNIT_SPELLCAST_FAILED_QUIET"
    then
        if mapID or NormalizeDungeonTeleportCastGUID(castGUID) then
            HandlePeerDungeonTeleportFallbackCancel(self, unitTarget, castGUID, readableSpellID, mapID, event)
        end
    end
end

dungeonTeleportPeerSpellcastWatcher:SetScript("OnEvent", function(_, event, ...)
    if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ENTERING_WORLD" then
        MK:RefreshDungeonTeleportPeerSpellcastWatcher(event)
        return
    end

    HandlePeerDungeonTeleportSpellcastEvent(MK, event, ...)
end)
dungeonTeleportPeerSpellcastWatcher:RegisterEvent("GROUP_ROSTER_UPDATE")
dungeonTeleportPeerSpellcastWatcher:RegisterEvent("PLAYER_ENTERING_WORLD")

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

    local readableSpellID = GetDungeonTeleportEventSpellID(unitTarget, spellID)
    local mapID = GetDungeonTeleportMapIDBySpellID(readableSpellID)
    if mapID then
        UpdateDungeonTeleportCastVisual(MK, mapID, event)
        TryBroadcastDungeonTeleportDialog(MK, mapID, event, castGUID, readableSpellID)
    end
end)
do
    local registerUnitEvent = dungeonTeleportSpellcastWatcher.RegisterUnitEvent
    if type(registerUnitEvent) == "function" then
        pcall(registerUnitEvent, dungeonTeleportSpellcastWatcher, "UNIT_SPELLCAST_START", "player")
        pcall(registerUnitEvent, dungeonTeleportSpellcastWatcher, "UNIT_SPELLCAST_SUCCEEDED", "player")
        pcall(registerUnitEvent, dungeonTeleportSpellcastWatcher, "UNIT_SPELLCAST_INTERRUPTED", "player")
        pcall(registerUnitEvent, dungeonTeleportSpellcastWatcher, "UNIT_SPELLCAST_FAILED", "player")
        pcall(registerUnitEvent, dungeonTeleportSpellcastWatcher, "UNIT_SPELLCAST_FAILED_QUIET", "player")
    end
end


DungeonTeleport.AttachDialogTrigger = AttachDungeonTeleportDialogTrigger
DungeonTeleport.SendAnnouncement = SendDungeonTeleportAnnouncement
DungeonTeleport.FormatPreviewMessage = FormatDungeonTeleportPreviewMessage

core.DungeonTeleportFollow = {
    IsEnabled = function(owner)
        return owner and owner.IsDungeonTeleportFollowEnabled and owner:IsDungeonTeleportFollowEnabled()
    end,
}
