-- 目标标记助手：快速设置鼠标指向焦点与团队标记，支持标记喊话、打断提醒和打断通报。
-- 作者：草东 - 白银之手 -WoWChina

local ADDON_NAME = "TargetMarkerAssistant"
local L = TargetMarkerAssistantL
TargetMarkerAssistant = TargetMarkerAssistant or {}
local TMA = TargetMarkerAssistant
local optionsFrame
local RefreshOptionsPanel
local ShowOptionsPage
local pendingSecureHotkeyRefresh = false
local pendingInterruptFocusMacroUpdate = false
local addonIconPath = "Interface\\AddOns\\TargetMarkerAssistant\\Art\\TMA\\TargetMarkerAssistant_logo.png"
local minimapIconPath = "Interface\\AddOns\\TargetMarkerAssistant\\Art\\TMA\\TargetMarkerAssistant_icon.png"
local soundPath = "Interface\\AddOns\\TargetMarkerAssistant\\Sounds\\"
local function GetAddonMetadataValue(field)
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata(ADDON_NAME, field)
    end
    if GetAddOnMetadata then
        return GetAddOnMetadata(ADDON_NAME, field)
    end
end
local addonVersion = GetAddonMetadataValue("Version") or "1.1.3"
local addonAuthor = GetAddonMetadataValue("Author") or "草东 - 白银之手"

if not TMA.InitializeDB or not TMA.DEFAULT_SETTINGS then
    error("TargetMarkerAssistant config module is not loaded.")
end
if not TMA.ResolveSucceededSpell or not TMA.ColorizeChannelText then
    error("TargetMarkerAssistant interrupt module is not loaded.")
end

TMA.InitializeDB()
local settingsPageDefaultKeys = TMA.SETTINGS_PAGE_DEFAULT_KEYS
local interruptAlertPageDefaultKeys = TMA.INTERRUPT_ALERT_PAGE_DEFAULT_KEYS
local interruptAnnouncePageDefaultKeys = TMA.INTERRUPT_ANNOUNCE_PAGE_DEFAULT_KEYS
local ColorizeChannelText = TMA.ColorizeChannelText

local lastRuntimeError

local function RecordRuntimeError(scope, err)
    lastRuntimeError = tostring(scope) .. ": " .. tostring(err)
    if DEFAULT_CHAT_FRAME then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff4040" .. L["ADDON_TITLE"] .. " " .. (L["ERROR_LABEL"] or "Error") .. ":|r " .. lastRuntimeError)
    end
end

local function SafeCall(scope, func, ...)
    local ok, err = pcall(func, ...)
    if not ok then
        RecordRuntimeError(scope, err)
    end
    return ok
end

local ResetKeysToDefaults = TMA.ResetKeysToDefaults

local function GetConfig()
    return TargetMarkerAssistantDB.modifier, TargetMarkerAssistantDB.mouseButton
end

local function CenterOptionsFrame()
    if not optionsFrame then return end
    if optionsFrame.StopMovingOrSizing then
        optionsFrame:StopMovingOrSizing()
    end
    optionsFrame:ClearAllPoints()
    optionsFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end

TMA.PlayOptionsPanelSound = function(action)
    if not PlaySound then return end
    local sound
    if _G.SOUNDKIT then
        if action == "close" then
            sound = _G.SOUNDKIT.IG_CHARACTER_INFO_CLOSE
        else
            sound = _G.SOUNDKIT.IG_CHARACTER_INFO_OPEN
        end
    end
    if sound then
        pcall(PlaySound, sound)
    end
end

TMA.PlayOptionsTabSound = function()
    if not PlaySound then return end
    local sound = _G.SOUNDKIT and _G.SOUNDKIT.IG_CHARACTER_INFO_TAB
    if sound then
        pcall(PlaySound, sound)
    end
end

local function ShowSettingsPanel(page)
    if not optionsFrame then return end
    if InCombatLockdown and InCombatLockdown() then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00" .. L["ADDON_TITLE"] .. "|r " .. (L["OPEN_PANEL_COMBAT_LOCKED"] or "Settings cannot be opened in combat."))
        end
        return
    end
    optionsFrame.TMA_OpenPage = page or "MARKER"
    CenterOptionsFrame()
    if RefreshOptionsPanel then RefreshOptionsPanel() end
    if optionsFrame:IsShown() then
        if ShowOptionsPage then ShowOptionsPage(optionsFrame.TMA_OpenPage) end
        optionsFrame.TMA_OpenPage = nil
    else
        optionsFrame:Show()
    end
    optionsFrame:Raise()
end

local function OpenSettingsPanel(page)
    if not optionsFrame then return end
    if optionsFrame:IsShown() and not page then
        optionsFrame:Hide()
        return
    end
    ShowSettingsPanel(page)
end

local function ToggleSettingsPanelFromMinimap(page)
    if not optionsFrame then return end
    page = page or "MARKER"
    if optionsFrame:IsShown() and optionsFrame.TMA_CurrentPage == page then
        optionsFrame:Hide()
        return
    end
    ShowSettingsPanel(page)
end

local nameToIndex = {
    [L["MARKER_0"]] = 0, [L["MARKER_1"]] = 1, [L["MARKER_2"]] = 2, [L["MARKER_3"]] = 3, [L["MARKER_4"]] = 4, 
    [L["MARKER_5"]] = 5, [L["MARKER_6"]] = 6, [L["MARKER_7"]] = 7, [L["MARKER_8"]] = 8,
}

local markerTextures = {
    [1] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_1:12:12|t",
    [2] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_2:12:12|t",
    [3] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_3:12:12|t",
    [4] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_4:12:12|t",
    [5] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_5:12:12|t",
    [6] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_6:12:12|t",
    [7] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_7:12:12|t",
    [8] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:12:12|t",
    [0] = "|TInterface\\Buttons\\UI-GroupLoot-Pass-Up:12:12|t",
}

local function GetMarkerChatIcon(marker)
    marker = tonumber(marker) or 0
    if marker >= 1 and marker <= 8 then
        return "{rt" .. tostring(marker) .. "}"
    end
    return ""
end

local optionsOrder = { L["MARKER_0"], L["MARKER_1"], L["MARKER_2"], L["MARKER_3"], L["MARKER_4"], L["MARKER_5"], L["MARKER_6"], L["MARKER_7"], L["MARKER_8"] }

local announceChannels = {
    { text = L["CHANNEL_SAY"], value = "SAY" },
    { text = L["CHANNEL_YELL"], value = "YELL" },
    { text = L["CHANNEL_PARTY"], value = "PARTY" },
    { text = L["CHANNEL_RAID"], value = "RAID" },
    { text = L["CHANNEL_INSTANCE_CHAT"], value = "INSTANCE_CHAT" },
    { text = L["CHANNEL_RAID_WARNING"], value = "RAID_WARNING" },
}

local announceSlashCommands = {
    SAY = "/s",
    YELL = "/y",
    PARTY = "/p",
    RAID = "/raid",
    INSTANCE_CHAT = "/i",
    RAID_WARNING = "/rw",
}

local castAlertSounds = {
    { text = L["SOUND_COLD_MATURE_MALE"], value = "cold_mature_male", file = soundPath .. "cold_mature_male.wav" },
    { text = L["SOUND_SYNTHETIC_ADULT_FEMALE"], value = "synthetic_adult_female", file = soundPath .. "synthetic_adult_female.wav" },
    { text = L["SOUND_ARTHAS"], value = "arthas", file = soundPath .. "arthas.mp3" },
    { text = L["SOUND_BRANN_BRONZEBEARD"], value = "brann_bronzebeard", file = soundPath .. "brann_bronzebeard.mp3" },
    { text = L["SOUND_ILLIDAN"], value = "illidan", file = soundPath .. "illidan.mp3" },
    { text = L["SOUND_SYLVANAS"], value = "sylvanas", file = soundPath .. "sylvanas.mp3" },
    { text = L["SOUND_XALATATH"], value = "xalatath", file = soundPath .. "xalatath.mp3" },
    { text = L["SOUND_ANDUIN"], value = "anduin", file = soundPath .. "anduin.mp3" },
    { text = L["SOUND_MILLHOUSE"], value = "millhouse", file = soundPath .. "millhouse.mp3" },
    { text = L["SOUND_MURLOC"], value = "murloc", file = soundPath .. "murloc.ogg" },
}

TMA.danmakuAlertSounds = {
    { text = L["SOUND_PING"], value = "ping", file = soundPath .. "Ping.aiff" },
    { text = L["SOUND_GLASS"], value = "glass", file = soundPath .. "Glass.aiff" },
    { text = L["SOUND_FUNK"], value = "funk", file = soundPath .. "Funk.aiff" },
    { text = L["SOUND_SUBMARINE"], value = "submarine", file = soundPath .. "Submarine.aiff" },
}

local soundChannels = {
    { text = L["SOUND_CHANNEL_MASTER"], value = "Master" },
    { text = L["SOUND_CHANNEL_SFX"], value = "SFX" },
    { text = L["SOUND_CHANNEL_MUSIC"], value = "Music" },
    { text = L["SOUND_CHANNEL_AMBIENCE"], value = "Ambience" },
    { text = L["SOUND_CHANNEL_DIALOG"], value = "Dialog" },
}

local function GetAnnounceChannelText(value)
    for _, channel in ipairs(announceChannels) do
        if channel.value == value then
            return ColorizeChannelText(channel.text, channel.value)
        end
    end
    return ColorizeChannelText(L["CHANNEL_SAY"], "SAY")
end

local function GetAnnounceChannelItems()
    local items = {}
    for _, channel in ipairs(announceChannels) do
        items[#items + 1] = {
            text = ColorizeChannelText(channel.text, channel.value),
            value = channel.value,
        }
    end
    return items
end

local function GetCastAlertSound(value)
    for _, sound in ipairs(castAlertSounds) do
        if sound.value == value then
            return sound
        end
    end
    return castAlertSounds[1]
end

function TMA.GetDanmakuAlertSound(value)
    for _, sound in ipairs(TMA.danmakuAlertSounds) do
        if sound.value == value then
            return sound
        end
    end
    return TMA.danmakuAlertSounds[1]
end

local function GetSoundChannel(value)
    for _, channel in ipairs(soundChannels) do
        if channel.value == value then
            return channel
        end
    end
    return soundChannels[1]
end

local function NormalizeAnnounceMessage(message)
    if not message or message == "" then
        message = L["ANNOUNCE_DEFAULT_MESSAGE"]
    end

    message = tostring(message):gsub("%%s", "")
    message = message:gsub("%%%%", "%%")
    message = message:gsub("^%s+", ""):gsub("%s+$", "")
    if message == "" then
        message = L["ANNOUNCE_DEFAULT_MESSAGE"]
    end
    return message
end

local function GetConfiguredAnnounceMessage()
    return NormalizeAnnounceMessage(TargetMarkerAssistantDB.announceMessage)
end

local function FormatAnnounceSubject(marker)
    local icon = GetMarkerChatIcon(marker)
    if icon == "" then return "" end
    return " " .. icon
end

local function FormatMacroAnnounceMessage(unit, marker)
    local message = GetConfiguredAnnounceMessage()
    local formatted = message .. FormatAnnounceSubject(marker)
    return formatted:gsub("[\r\n]+", " ")
end

local function FormatAnnounceMessageByName(unitName, marker)
    local message = GetConfiguredAnnounceMessage()
    local formatted = message .. FormatAnnounceSubject(marker)
    return formatted:gsub("[\r\n]+", " ")
end

local function FormatAnnouncePreviewMessage(messageOverride, marker)
    local message = NormalizeAnnounceMessage(messageOverride)
    marker = tonumber(marker) or 0
    local icon = markerTextures[marker] or ""
    if marker <= 0 then
        icon = ""
    end

    local preview = (L["INTERRUPT_CHAT_PREVIEW_PREFIX"] or "") .. message
    if icon ~= "" then
        preview = preview .. " " .. icon
    end
    return ColorizeChannelText(preview:gsub("[\r\n]+", " "), TargetMarkerAssistantDB.announceChannel or "SAY")
end

local SendConfiguredChatMessage

local function GetUsableAnnounceChannel()
    local channel = TargetMarkerAssistantDB.announceChannel or "SAY"
    if channel == "SAY" or channel == "YELL" then return channel end
    if channel == "PARTY" then return IsInGroup() and "PARTY" or "SAY" end
    if channel == "RAID" then return IsInRaid() and "RAID" or (IsInGroup() and "PARTY" or "SAY") end
    if channel == "INSTANCE_CHAT" then
        if TMA.IsChallengeModeActiveSafe() and IsInGroup() then return "PARTY" end
        return IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or (IsInRaid() and "RAID" or (IsInGroup() and "PARTY" or "SAY"))
    end
    if channel == "RAID_WARNING" then return IsInRaid() and "RAID_WARNING" or (IsInGroup() and "PARTY" or "SAY") end
    return channel
end

local function GetAutomaticAnnounceChannel()
    local channel = TargetMarkerAssistantDB.announceChannel or "SAY"
    if channel == "SAY" or channel == "YELL" then
        if TMA.IsChallengeModeActiveSafe() and IsInGroup() then return "PARTY" end
        if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then return "INSTANCE_CHAT" end
        if IsInRaid() then return "RAID" end
        if IsInGroup() then return "PARTY" end
    end

    return GetUsableAnnounceChannel()
end

local function EscapeMacroChatMessage(message)
    message = tostring(message or ""):gsub("[\r\n]+", " ")
    message = message:gsub("^%s+", ""):gsub("%s+$", "")
    return message
end

local function GetMacroAnnounceChatLine(marker)
    if not TargetMarkerAssistantDB.enableAnnounce then return "" end
    if not marker or marker <= 0 then return "" end

    local slash = announceSlashCommands[GetUsableAnnounceChannel()] or "/s"
    local message = GetConfiguredAnnounceMessage() .. FormatAnnounceSubject(marker)
    message = TMA.SanitizeChatPlainText and TMA.SanitizeChatPlainText(message, "") or message
    message = EscapeMacroChatMessage(message)
    if message == "" then return "" end

    return "\n" .. slash .. " " .. message
end

local function FindUnitByGUID(guid)
    if not guid then return end

    for _, unit in ipairs({ "mouseover", "focus", "target" }) do
        if UnitExists(unit) and UnitGUID(unit) == guid then
            return unit
        end
    end

    for i = 1, 8 do
        local unit = "boss" .. i
        if UnitExists(unit) and UnitGUID(unit) == guid then
            return unit
        end
    end

    for i = 1, 40 do
        local unit = "nameplate" .. i
        if UnitExists(unit) and UnitGUID(unit) == guid then
            return unit
        end
    end
end

local function AnnounceMarkedUnit(unit)
    if not TargetMarkerAssistantDB.enableAnnounce then return end
    if not unit then return end

    local actualMarker = GetRaidTargetIndex(unit) or 0
    local marker = actualMarker > 0 and actualMarker or TargetMarkerAssistantDB.selectedMarker or 0
    if marker <= 0 then return false end

    local message = FormatMacroAnnounceMessage(unit, marker)
    SendConfiguredChatMessage(message)
    return true
end

local pendingAnnounceToken = 0

local function AnnouncePreparedUnit(unitGUID, unitName, fallbackMarker)
    if not TargetMarkerAssistantDB.enableAnnounce then return end

    local unit = FindUnitByGUID(unitGUID)
    local actualMarker = unit and (GetRaidTargetIndex(unit) or 0) or 0
    local marker = actualMarker > 0 and actualMarker or fallbackMarker or TargetMarkerAssistantDB.selectedMarker or 0
    local name = (unit and UnitName(unit)) or unitName

    if marker <= 0 or not name or name == "" then return false end

    local message = FormatAnnounceMessageByName(name, marker)
    SendConfiguredChatMessage(message)
    return true
end

-- 兼容已有用户宏；实际安全点击逻辑由 TargetMarkerAssistantButton 承担。
function TMA_T(unit)
    unit = unit or "mouseover"
    if UnitExists(unit) then
        return true
    end
end

function TargetMarkerAssistant_MacroAnnounce()
    if not UnitExists("mouseover") then return end
    AnnounceMarkedUnit("mouseover")
end

function TargetMarkerAssistant_PrepareAnnounce()
    if not TargetMarkerAssistantDB.enableAnnounce then return end
    if not UnitExists("mouseover") then
        return
    end

    pendingAnnounceToken = pendingAnnounceToken + 1
    local token = pendingAnnounceToken
    local unitGUID = UnitGUID("mouseover")
    local unitName = UnitName("mouseover")
    local fallbackMarker = TargetMarkerAssistantDB.selectedMarker or 0

    C_Timer.After(0.08, function()
        if token == pendingAnnounceToken then
            AnnouncePreparedUnit(unitGUID, unitName, fallbackMarker)
        end
    end)
end

SendConfiguredChatMessage = function(message, automatic, suppressLocal, channelOverride)
    local channel = channelOverride or (automatic and GetAutomaticAnnounceChannel() or GetUsableAnnounceChannel())
    local sendMessage = TMA.SanitizeChatPlainText and TMA.SanitizeChatPlainText(message, "") or tostring(message or "")
    if sendMessage == "" then
        return false
    end

    if TMA.IsAddonChatRestricted() then
        if not suppressLocal then
            local blockedFormat = L["CHAT_LOCKDOWN_SEND_BLOCKED"] or "Chat is currently restricted. Not sent automatically: %s"
            TMA.PrintLocalMessage(string.format(blockedFormat, TMA.SafeDebugText(sendMessage, "")))
        end
        return false
    end

    if not suppressLocal then
        TMA.PrintLocalMessage(sendMessage)
    end

    if SendChatMessage then
        local ok, err = pcall(SendChatMessage, sendMessage, channel, nil, nil)
        if ok then return true end
    end

    if C_ChatInfo and C_ChatInfo.SendChatMessage then
        local ok, err = pcall(C_ChatInfo.SendChatMessage, sendMessage, channel)
        if ok then return true end
    end
    return false
end

local lastFocusCastAlertSig
TMA.FocusCastAlertState = TMA.FocusCastAlertState or {}

local function GetCastAlertInfoForUnit(unit, quiet)
    if not UnitExists(unit) then return end

    local snapshot = TMA.ReadUnitCastingInfoSnapshot and TMA.ReadUnitCastingInfoSnapshot(unit)
    if snapshot and snapshot.active then
        local spellID = TMA.SafeNumber(snapshot.spellID)
        local spellName = (spellID and TMA.GetPublicSpellNameByID(spellID)) or L["UNKNOWN_SPELL"] or "spell"
        local startMS = TMA.SafeNumber(snapshot.startTimeMS) or 0
        local endMS = TMA.SafeNumber(snapshot.endTimeMS) or 0
        local castID = TMA.SafeNumber(snapshot.castID) or 0
        return spellName, "cast:" .. tostring(spellID or 0) .. ":" .. tostring(castID) .. ":" .. tostring(startMS) .. ":" .. tostring(endMS), unit
    end

    snapshot = TMA.ReadUnitChannelInfoSnapshot and TMA.ReadUnitChannelInfoSnapshot(unit)
    if snapshot and snapshot.active then
        local spellID = TMA.SafeNumber(snapshot.spellID)
        local spellName = (spellID and TMA.GetPublicSpellNameByID(spellID)) or L["UNKNOWN_SPELL"] or "spell"
        local startMS = TMA.SafeNumber(snapshot.startTimeMS) or 0
        local endMS = TMA.SafeNumber(snapshot.endTimeMS) or 0
        return spellName, "channel:" .. tostring(spellID or 0) .. ":" .. tostring(startMS) .. ":" .. tostring(endMS), unit
    end

    return
end

local function PlayCastAlertSound()
    local sound = GetCastAlertSound(TargetMarkerAssistantDB.castAlertSound)
    local channel = GetSoundChannel(TargetMarkerAssistantDB.castAlertChannel).value
    if sound and sound.file then
        local ok, played = pcall(PlaySoundFile, sound.file, channel)
        if ok and played then return end
    end

    pcall(PlaySoundFile, "Interface\\AddOns\\TargetMarkerAssistant\\Sounds\\Glass.aiff", channel)
end

function TMA.PlayRestrictedDanmakuSound(force)
    if TMA.EnsureRestrictedInterruptDanmakuDefaults then
        TMA.EnsureRestrictedInterruptDanmakuDefaults()
    end

    local cfg = TargetMarkerAssistantDB and TargetMarkerAssistantDB.restrictedInterruptDanmaku
    if not cfg or (not force and cfg.soundEnabled ~= true) then
        return false
    end

    local function trySoundFile(file)
        if not file or file == "" or not PlaySoundFile then
            return false
        end
        local ok, played = pcall(PlaySoundFile, file, "Master")
        return ok and played == true
    end

    local sound = TMA.GetDanmakuAlertSound(cfg.sound)
    if sound and trySoundFile(sound.file) then
        return true
    end

    return trySoundFile("Interface\\AddOns\\TargetMarkerAssistant\\Sounds\\Glass.aiff")
end

local function GetFocusCastAlertInfo(quiet)
    if not TargetMarkerAssistantDB.enableCastAlert then
        return
    end

    if not UnitExists("focus") then
        return
    end

    local spellName, sig, castUnit = GetCastAlertInfoForUnit("focus", quiet)
    if spellName then return spellName, sig, castUnit end
end

local function ClearFocusCastAlertSig()
    local state = TMA.FocusCastAlertState
    local now = GetTime and GetTime() or 0
    if lastFocusCastAlertSig then
        state.stoppedSig = lastFocusCastAlertSig
        state.stoppedAt = now
    end
    lastFocusCastAlertSig = nil
    state.active = nil
    state.seenAt = nil
end

local function RefreshFocusCastAlert(quiet)
    local state = TMA.FocusCastAlertState
    local now = GetTime and GetTime() or 0
    local spellName, sig, castUnit = GetFocusCastAlertInfo(quiet)
    if not spellName or not sig then
        if state.active and state.seenAt and now - state.seenAt > 1 then
            ClearFocusCastAlertSig()
        end
        return
    end

    state.seenAt = now
    sig = tostring(castUnit or "focus") .. ":" .. tostring(sig)
    if state.active then
        lastFocusCastAlertSig = lastFocusCastAlertSig or sig
        return
    end

    if state.stoppedSig
        and TMA.SafeValueEquals(state.stoppedSig, sig)
        and state.stoppedAt
        and now - state.stoppedAt < 1
    then
        return
    end

    if state.playedAt and now - state.playedAt < 0.8 then
        state.active = true
        lastFocusCastAlertSig = sig
        return
    end

    state.active = true
    lastFocusCastAlertSig = sig
    state.playedAt = now
    PlayCastAlertSound()
end

local function ScheduleFocusCastAlertRefresh()
    RefreshFocusCastAlert()
    C_Timer.After(0.05, RefreshFocusCastAlert)
end

local castAlertTicker = CreateFrame("Frame")
local castAlertElapsed = 0
castAlertTicker:SetScript("OnUpdate", function(_, elapsed)
    castAlertElapsed = castAlertElapsed + elapsed
    if castAlertElapsed < 0.2 then return end
    castAlertElapsed = 0

    if TargetMarkerAssistantDB and TargetMarkerAssistantDB.enableCastAlert then
        RefreshFocusCastAlert(true)
    elseif lastFocusCastAlertSig then
        ClearFocusCastAlertSig()
    end
end)

local function GetFocusMacro()
    local macro = ""
    if TargetMarkerAssistantDB.allowClearFocus then
        macro = macro .. "/tar [@mouseover,noexists] focus"
        macro = macro .. "\n/tm [@mouseover,noexists] 0"
        macro = macro .. "\n/clearfocus [@mouseover,noexists]"
        macro = macro .. "\n/stopmacro [@mouseover,noexists]"
    end

    macro = macro .. "\n/focus [@mouseover,exists]"
    if TargetMarkerAssistantDB.enableMarking and TargetMarkerAssistantDB.selectedMarker and TargetMarkerAssistantDB.selectedMarker > 0 then
        local marker = TargetMarkerAssistantDB.selectedMarker
        macro = macro .. "\n/tm [@mouseover]" .. marker
        macro = macro .. GetMacroAnnounceChatLine(marker)
    end

    return macro
end

local function InitMouseoverFocus()
    if InCombatLockdown() then
        pendingSecureHotkeyRefresh = true
        return
    end

    if not TargetMarkerAssistantButton then
        TargetMarkerAssistantButton = CreateFrame("CheckButton", "TargetMarkerAssistantButton", UIParent, "SecureActionButtonTemplate")
        TargetMarkerAssistantButton:RegisterForClicks("AnyDown", "AnyUp")
        TargetMarkerAssistantButton:SetAttribute("type", "macro")
    end
    
    if TargetMarkerAssistantButton then
        local mod, btn = GetConfig()
        local bindingKey = string.upper(mod or "shift") .. "-BUTTON" .. tostring(btn or "1")
        local ok, err = pcall(function()
            TargetMarkerAssistantButton:SetScript("PreClick", nil)
            TargetMarkerAssistantButton:SetAttribute("type", "macro")
            TargetMarkerAssistantButton:SetAttribute("macrotext1", GetFocusMacro())
            ClearOverrideBindings(TargetMarkerAssistantButton)
            SetOverrideBindingClick(TargetMarkerAssistantButton, true, bindingKey, "TargetMarkerAssistantButton", "LeftButton")
        end)

        if ok then
            pendingSecureHotkeyRefresh = false
        else
            pendingSecureHotkeyRefresh = true
            RecordRuntimeError("SECURE_HOTKEY_REFRESH", err)
        end
    end
end

local function InitAll()
    if InCombatLockdown() then
        pendingSecureHotkeyRefresh = true
        return
    end

    -- Keep secure click handling on TMA-owned frames only. Writing attributes
    -- onto Blizzard or third-party unit frames can taint replacement UIs.
    InitMouseoverFocus()
end

local f = CreateFrame("Frame", "TargetMarkerAssistantEventFrame")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
f:RegisterEvent("SPELLS_CHANGED")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("CHALLENGE_MODE_START")
f:RegisterEvent("PLAYER_DIFFICULTY_CHANGED")
f:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
f:RegisterEvent("RAID_TARGET_UPDATE")
f:RegisterEvent("PLAYER_FOCUS_CHANGED")
f:RegisterEvent("SPELL_UPDATE_COOLDOWN")
f:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN")
f:RegisterEvent("CHAT_MSG_ADDON")
f:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player", "pet", "vehicle")
f:RegisterUnitEvent("UNIT_SPELLCAST_SENT", "player")

TMA.GroupInterruptSpellcastUnitSets = TMA.GroupInterruptSpellcastUnitSets or {
    { "party1", "party2", "party3", "party4" },
    { "partypet1", "partypet2", "partypet3", "partypet4" },
}

TMA.GroupInterruptSpellcastFrames = TMA.GroupInterruptSpellcastFrames or {}

function TMA.HandleGroupInterruptSpellcastUnitEvent(_, event, unit, castGUID, spellID, ...)
    if event ~= "UNIT_SPELLCAST_SUCCEEDED" then return end
    if TMA.IsOwnSpellcastUnit(unit) then return end

    local resolvedSpellID, resolvedSpellName = TMA.ResolveSucceededSpell(unit, castGUID, spellID, ...)
    TMA.HandleGroupInterruptSpellcast(unit, castGUID, resolvedSpellID, resolvedSpellName)
end

function TMA.RegisterGroupInterruptSpellcastFrames()
    local frames = TMA.GroupInterruptSpellcastFrames or {}
    TMA.GroupInterruptSpellcastFrames = frames

    for index, units in ipairs(TMA.GroupInterruptSpellcastUnitSets or {}) do
        local frame = frames[index]
        if not frame then
            frame = CreateFrame("Frame")
            frames[index] = frame
            frame:SetScript("OnEvent", TMA.HandleGroupInterruptSpellcastUnitEvent)
        end

        if frame.UnregisterAllEvents then
            pcall(frame.UnregisterAllEvents, frame)
        end

        local ok, err = pcall(
            frame.RegisterUnitEvent,
            frame,
            "UNIT_SPELLCAST_SUCCEEDED",
            units[1],
            units[2],
            units[3],
            units[4]
        )
        if ok then
            frame:SetScript("OnEvent", TMA.HandleGroupInterruptSpellcastUnitEvent)
        else
            TMA.DebugInterrupt("队友施法事件注册失败：" .. TMA.SafeDebugText(err, "不可用"))
        end
    end
end

TMA.RegisterGroupInterruptSpellcastFrames()

local function RegisterInterruptCastEvent(eventName)
    -- RegisterUnitEvent only accepts up to four unit tokens. RegisterEvent receives
    -- all spellcast unit events; HandleMainEvent filters to visible unit tokens.
    f:RegisterEvent(eventName)
end

RegisterInterruptCastEvent("UNIT_SPELLCAST_START")
RegisterInterruptCastEvent("UNIT_SPELLCAST_CHANNEL_START")
RegisterInterruptCastEvent("UNIT_SPELLCAST_INTERRUPTIBLE")
RegisterInterruptCastEvent("UNIT_SPELLCAST_DELAYED")
RegisterInterruptCastEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
RegisterInterruptCastEvent("UNIT_SPELLCAST_INTERRUPTED")
RegisterInterruptCastEvent("UNIT_SPELLCAST_FAILED")
RegisterInterruptCastEvent("UNIT_SPELLCAST_STOP")
RegisterInterruptCastEvent("UNIT_SPELLCAST_CHANNEL_STOP")

local function InitializeInterruptSpellRuntime()
    TMA.EnsureDBAliases()
    if TargetMarkerAssistantDB and TargetMarkerAssistantDB.interruptSpellMode == "auto" then
        C_Timer.After(0.5, function() TMA.AutoDetectInterruptSpell(true) end)
    elseif TargetMarkerAssistantDB and TargetMarkerAssistantDB.interruptSpellMode == "manual" then
        local spellID, spellName = TMA.ResolveConfiguredInterruptSpell(TargetMarkerAssistantDB.interruptSpellInput)
        if spellID then
            TMA.SaveInterruptSpell(spellID, spellName, TargetMarkerAssistantDB.interruptSpellInput)
        end
    end
end

local function ScheduleInterruptCooldownReadyCheck(reason, delay)
    if not TMA.TrackConfiguredInterruptCooldownReady then return end
    if TMA.IsChallengeModeActiveSafe and TMA.IsChallengeModeActiveSafe() then return end
    local now = GetTime and GetTime() or 0
    delay = tonumber(delay) or 0
    if delay > 0 and delay < 0.12 then
        delay = 0.12
    end

    TMA.InterruptCooldownReadyCheckBuckets = TMA.InterruptCooldownReadyCheckBuckets or {}
    for bucket, scheduledAt in pairs(TMA.InterruptCooldownReadyCheckBuckets) do
        if not scheduledAt or now - scheduledAt > 2 then
            TMA.InterruptCooldownReadyCheckBuckets[bucket] = nil
        end
    end

    local runAt = now + delay
    local bucket = tostring(math.floor(runAt * 2))
    if TMA.InterruptCooldownReadyCheckBuckets[bucket] then
        return
    end
    TMA.InterruptCooldownReadyCheckBuckets[bucket] = runAt

    local function runCheck()
        if not TMA.SafeValueEquals(TMA.InterruptCooldownReadyCheckBuckets[bucket], runAt) then return end
        TMA.InterruptCooldownReadyCheckBuckets[bucket] = nil
        if TMA.IsChallengeModeActiveSafe and TMA.IsChallengeModeActiveSafe() then return end
        TMA.LastInterruptCooldownReadyCheckAt = GetTime and GetTime() or 0
        TMA.TrackConfiguredInterruptCooldownReady(reason)
    end

    if delay > 0 and C_Timer and C_Timer.After then
        C_Timer.After(delay, runCheck)
    else
        runCheck()
    end
end

function TMA.ScheduleInterruptCooldownReadyCheckSeries(reason)
    local label = tostring(reason or "INTERRUPT_COOLDOWN_READY")
    ScheduleInterruptCooldownReadyCheck(label, 0.18)
    ScheduleInterruptCooldownReadyCheck(label .. ":retry1", 0.75)
    ScheduleInterruptCooldownReadyCheck(label .. ":retry2", 1.55)
    ScheduleInterruptCooldownReadyCheck(label .. ":retry3", 2.65)
end

function TMA.ShouldScheduleInterruptCooldownReadyCheckSeries(spellID, spellName)
    if TMA.IsConfiguredInterruptSpell and TMA.IsConfiguredInterruptSpell(spellID, spellName) then
        return true
    end
    spellID = TMA.SafeNumber and TMA.SafeNumber(spellID) or tonumber(spellID)
    return spellID and TMA.IsInterruptSpellID and TMA.IsInterruptSpellID(spellID)
end

local function GetInterruptCombatLogDisabledReason()
    if not TargetMarkerAssistantDB or not TargetMarkerAssistantDB.enableInterruptAnnounce then
        return "成功打断通报开关未开启。"
    end
    if not TMA.IsInterruptAnnounceContextValid() then
        return "玩家不在队伍或团队中。"
    end
    if TMA.IsChallengeModeActiveSafe() then
        return "当前处于大秘境状态。"
    end
end

local function RefreshInterruptCombatLogFrame(reason)
    TMA.CombatLogEventDisabled = true
    TMA.CombatLogEventRegistered = false

    local blockedReason = GetInterruptCombatLogDisabledReason()
    if blockedReason then
        if not TMA.SafeValueEquals(TMA.CombatLogDisabledReasonLogged, blockedReason) then
            TMA.CombatLogDisabledReasonLogged = blockedReason
            TMA.DebugInterrupt("战斗日志直读未启用：" .. tostring(blockedReason))
        end
        return
    end

    if not TMA.CombatLogRegistrationRestrictedNotified then
        TMA.CombatLogRegistrationRestrictedNotified = true
        TMA.DebugInterrupt("战斗日志直读已停用：当前环境会拦截 COMBAT_LOG_EVENT_UNFILTERED 注册，改用队友施法事件与插件同步兜底。")
    end
end

TMA.RefreshInterruptCombatLogFrame = RefreshInterruptCombatLogFrame

local function RegisterInterruptCombatLogFrame()
    RefreshInterruptCombatLogFrame("PLAYER_LOGIN")
end

local function HandleMainEvent(_, event, unit, castGUID, spellID, ...)
    if event == "PLAYER_LOGIN" then
        InitializeInterruptSpellRuntime()
        TMA.RegisterInterruptAddonSyncPrefix()
        C_Timer.After(2, RegisterInterruptCombatLogFrame)
        ScheduleInterruptCooldownReadyCheck("PLAYER_LOGIN", 0.75)
    elseif event == "CHAT_MSG_ADDON" then
        if TMA.HandleInterruptAddonSyncMessage then
            TMA.HandleInterruptAddonSyncMessage(unit, castGUID, spellID, ...)
        end
    elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
        RefreshInterruptCombatLogFrame(event)
        local resolvedSpellID, resolvedSpellName = TMA.ResolveSucceededSpell(unit, castGUID, spellID, ...)
        if TMA.IsOwnSpellcastUnit(unit) then
            TMA.RememberInterruptAttempt(unit, resolvedSpellID, resolvedSpellName, true)
            local shouldTrackInterruptCooldown = TMA.ShouldScheduleInterruptCooldownReadyCheckSeries(resolvedSpellID, resolvedSpellName)
            if shouldTrackInterruptCooldown then
                local handledByMythicPath = TMA.ScheduleMythicInterruptCooldownReadyAlert
                    and TMA.ScheduleMythicInterruptCooldownReadyAlert(resolvedSpellID, resolvedSpellName, "UNIT_SPELLCAST_SUCCEEDED")
                if not handledByMythicPath then
                    TMA.ScheduleInterruptCooldownReadyCheckSeries("UNIT_SPELLCAST_SUCCEEDED")
                end
            end
            if unit == "player" and shouldTrackInterruptCooldown then
                C_Timer.After(0.05, function() TMA.ScanConfiguredInterruptCooldown("UNIT_SPELLCAST_SUCCEEDED") end)
            end
        else
            TMA.HandleGroupInterruptSpellcast(unit, castGUID, resolvedSpellID, resolvedSpellName)
        end
    elseif event == "UNIT_SPELLCAST_SENT" then
        RefreshInterruptCombatLogFrame(event)
        local resolvedSpellID, resolvedSpellName = TMA.ResolveSucceededSpell(unit, castGUID, spellID, ...)
        if resolvedSpellID or resolvedSpellName then
            TMA.RememberInterruptAttempt(unit, resolvedSpellID, resolvedSpellName)
        end
        if TMA.ShouldScheduleInterruptCooldownReadyCheckSeries(resolvedSpellID, resolvedSpellName)
            and not (TMA.IsChallengeModeActiveSafe and TMA.IsChallengeModeActiveSafe())
        then
            TMA.ScheduleInterruptCooldownReadyCheckSeries("UNIT_SPELLCAST_SENT")
            C_Timer.After(0.05, function() TMA.ScanConfiguredInterruptCooldown("UNIT_SPELLCAST_SENT") end)
        end
    elseif event == "SPELL_UPDATE_COOLDOWN" or event == "ACTIONBAR_UPDATE_COOLDOWN" then
        RefreshInterruptCombatLogFrame(event)
        TMA.ScanConfiguredInterruptCooldown(event)
        ScheduleInterruptCooldownReadyCheck(event, 0.12)
    elseif event == "GROUP_ROSTER_UPDATE" or event == "CHALLENGE_MODE_START" or event == "PLAYER_DIFFICULTY_CHANGED" then
        RefreshInterruptCombatLogFrame(event)
        TMA.RegisterGroupInterruptSpellcastFrames()
        if event == "CHALLENGE_MODE_START" or event == "PLAYER_DIFFICULTY_CHANGED" then
            TMA.ScheduleInterruptCooldownReadyCheckSeries(event)
        end
    elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_SPECIALIZATION_CHANGED" or event == "SPELLS_CHANGED" then
        if event == "PLAYER_ENTERING_WORLD" then
            TMA.RegisterGroupInterruptSpellcastFrames()
        end
        local function updateInterruptSpell()
            if TargetMarkerAssistantDB and TargetMarkerAssistantDB.interruptSpellMode == "auto" then
                TMA.AutoDetectInterruptSpell(true)
                if RefreshOptionsPanel then RefreshOptionsPanel() end
            end
        end
        C_Timer.After(0.5, updateInterruptSpell)
        ScheduleInterruptCooldownReadyCheck(event, 0.65)
        if event == "PLAYER_ENTERING_WORLD" and not InCombatLockdown() then
            C_Timer.After(2, InitAll)
        end
        RefreshInterruptCombatLogFrame(event)
    elseif event == "NAME_PLATE_UNIT_REMOVED" then
        TMA.ClearTrackedUnitCast(unit)
    elseif event == "RAID_TARGET_UPDATE" then
        C_Timer.After(0.05, function() RefreshFocusCastAlert(true) end)
    elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_CHANNEL_START" then
        if not TMA.IsInterruptCastUnitToken(unit) then return end
        if unit == "focus" then
            RefreshFocusCastAlert()
        end
    elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" or event == "UNIT_SPELLCAST_DELAYED" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
        if not TMA.IsInterruptCastUnitToken(unit) then return end
        if unit == "focus" then
            RefreshFocusCastAlert()
        end
    elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
        if not TMA.IsInterruptCastUnitToken(unit) then return end
        TMA.ClearTrackedUnitCast(unit)
        if unit == "focus" then
            ClearFocusCastAlertSig()
        end
    elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_CHANNEL_STOP" or event == "UNIT_SPELLCAST_FAILED" then
        if not TMA.IsInterruptCastUnitToken(unit) then return end
        TMA.ClearTrackedUnitCast(unit)
        if unit == "focus" then
            ClearFocusCastAlertSig()
        end
    elseif event == "PLAYER_FOCUS_CHANGED" then
        ClearFocusCastAlertSig()
        ScheduleFocusCastAlertRefresh()
    elseif event == "PLAYER_REGEN_ENABLED" then
        if pendingSecureHotkeyRefresh then
            InitAll()
        end
        if pendingInterruptFocusMacroUpdate and TMA.QueueInterruptFocusMacroUpdate then
            TMA.QueueInterruptFocusMacroUpdate()
        end
    else
        TMA.DebugInterrupt("未处理的事件: " .. tostring(event) .. " unit=" .. tostring(unit))
    end
end
f:SetScript("OnEvent", function(...)
    SafeCall("MAIN_EVENT", HandleMainEvent, ...)
end)

-- 独立设置面板
local function CreateOptionsFrame()
    if not TMA_Ui or not TMA_Ui.CreateOptionsShell then
        error("TargetMarkerAssistant UI module is not loaded.")
    end
    if not TMA_Config_Mark or not TMA_Config_Mark.Create then
        error("TargetMarkerAssistant mark settings module is not loaded.")
    end
    if not TMA_Config_Notice or not TMA_Config_Notice.Create then
        error("TargetMarkerAssistant notice settings module is not loaded.")
    end
    if not TMA_Config_Notify or not TMA_Config_Notify.Create then
        error("TargetMarkerAssistant notify settings module is not loaded.")
    end

    local uiContext = TMA_Ui.CreateOptionsShell({
        L = L,
        addonVersion = addonVersion,
        addonAuthor = addonAuthor,
        minimapIconPath = minimapIconPath,
    })
    optionsFrame = uiContext.optionsFrame

    uiContext.InitAll = InitAll
    uiContext.RequestRefresh = function()
        if RefreshOptionsPanel then RefreshOptionsPanel() end
    end

    uiContext.markerTextures = markerTextures
    uiContext.nameToIndex = nameToIndex
    uiContext.optionsOrder = optionsOrder
    uiContext.GetAnnounceChannelText = GetAnnounceChannelText
    uiContext.GetAnnounceChannelItems = GetAnnounceChannelItems
    uiContext.FormatAnnouncePreviewMessage = FormatAnnouncePreviewMessage

    uiContext.ScheduleFocusCastAlertRefresh = ScheduleFocusCastAlertRefresh
    uiContext.castAlertSounds = castAlertSounds
    uiContext.danmakuAlertSounds = TMA.danmakuAlertSounds
    uiContext.soundChannels = soundChannels
    uiContext.GetCastAlertSound = GetCastAlertSound
    uiContext.GetDanmakuAlertSound = TMA.GetDanmakuAlertSound
    uiContext.GetSoundChannel = GetSoundChannel
    uiContext.PlayCastAlertSound = PlayCastAlertSound
    uiContext.PlayDanmakuAlertSound = TMA.PlayRestrictedDanmakuSound

    uiContext.SetConfiguredInterruptSpellInput = TMA.SetConfiguredInterruptSpellInput
    uiContext.SanitizeChatPlainText = TMA.SanitizeChatPlainText
    uiContext.GetBaseOrFallbackInterruptCooldown = TMA.GetBaseOrFallbackInterruptCooldown
    uiContext.GetPlayerInterruptCooldownDuration = TMA.GetPlayerInterruptCooldownDuration
    uiContext.GetObservedInterruptCooldownDuration = TMA.GetObservedInterruptCooldownDuration
    uiContext.GetDefaultInterruptChatTemplate = TMA.GetDefaultInterruptChatTemplate
    uiContext.FormatInterruptChatPreview = TMA.FormatInterruptChatPreview
    uiContext.FormatInterruptDanmakuPreviewMessage = TMA.FormatInterruptDanmakuPreviewMessage
    uiContext.ColorizeChannelText = TMA.ColorizeChannelText
    uiContext.GetShortPlayerName = TMA.GetShortPlayerName
    uiContext.ColorizePlayerName = TMA.ColorizePlayerName

    local markPanel = TMA_Config_Mark.Create(uiContext)
    local noticePanel = TMA_Config_Notice.Create(uiContext)
    local notifyPanel = TMA_Config_Notify.Create(uiContext)

    RefreshOptionsPanel = function()
        markPanel.Refresh()
        noticePanel.Refresh()
        notifyPanel.Refresh()
    end

    local currentOptionsPage = "MARKER"

    ShowOptionsPage = function(page)
        currentOptionsPage = page or "MARKER"
        if optionsFrame then
            optionsFrame.TMA_CurrentPage = currentOptionsPage
        end
        TMA_Ui.ShowPage(uiContext, currentOptionsPage)
    end

    local function SelectOptionsPageFromTab(page)
        if currentOptionsPage ~= page and TMA.PlayOptionsTabSound then
            TMA.PlayOptionsTabSound()
        end
        ShowOptionsPage(page)
    end

    uiContext.markerTab:SetScript("OnClick", function()
        SelectOptionsPageFromTab("MARKER")
    end)

    uiContext.castAlertTab:SetScript("OnClick", function()
        SelectOptionsPageFromTab("ALERT")
    end)

    uiContext.combatTab:SetScript("OnClick", function()
        SelectOptionsPageFromTab("COMBAT")
    end)

    uiContext.resetButton:SetScript("OnClick", function()
        if currentOptionsPage == "ALERT" then
            ResetKeysToDefaults(interruptAlertPageDefaultKeys)
        elseif currentOptionsPage == "COMBAT" then
            ResetKeysToDefaults(interruptAnnouncePageDefaultKeys)
        else
            ResetKeysToDefaults(settingsPageDefaultKeys)
        end
        RefreshOptionsPanel()
        ShowOptionsPage(currentOptionsPage)
        InitAll()
    end)

    optionsFrame:SetScript("OnShow", function(self)
        if self.SetFrameStrata then self:SetFrameStrata("MEDIUM") end
        if self.SetFrameLevel then self:SetFrameLevel(10) end
        if self.SetToplevel then self:SetToplevel(true) end
        if TMA.PlayOptionsPanelSound then TMA.PlayOptionsPanelSound("open") end
        CenterOptionsFrame()
        RefreshOptionsPanel()
        ShowOptionsPage(self.TMA_OpenPage or "MARKER")
        self.TMA_OpenPage = nil
    end)
    optionsFrame:SetScript("OnHide", function(self)
        self.TMA_OpenPage = nil
        if TMA.PlayOptionsPanelSound then TMA.PlayOptionsPanelSound("close") end
    end)
    ShowOptionsPage("MARKER")
end

CreateOptionsFrame()

TMA.SettingsPanel = CreateFrame("Frame", "TargetMarkerAssistantSettingsPanel")
TMA.SettingsPanel.name = L["ADDON_TITLE"]

TMA.SettingsPanel.Icon = TMA.SettingsPanel:CreateTexture(nil, "ARTWORK")
TMA.SettingsPanel.Icon:SetPoint("TOPLEFT", 24, -24)
TMA.SettingsPanel.Icon:SetSize(46, 46)
TMA.SettingsPanel.Icon:SetTexture(addonIconPath)

TMA.SettingsPanel.Title = TMA.SettingsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
TMA.SettingsPanel.Title:SetPoint("TOPLEFT", TMA.SettingsPanel.Icon, "TOPRIGHT", 14, -2)
TMA.SettingsPanel.Title:SetText(L["ADDON_TITLE"])
TMA.SettingsPanel.Title:SetTextColor(1, 0.82, 0)

TMA.SettingsPanel.TitleVersion = TMA.SettingsPanel:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
TMA.SettingsPanel.TitleVersion:SetPoint("TOPLEFT", TMA.SettingsPanel.Title, "BOTTOMLEFT", 0, -6)
TMA.SettingsPanel.TitleVersion:SetText(string.format(L["SETTINGS_VERSION_FORMAT"], addonVersion))
TMA.SettingsPanel.TitleVersion:SetTextColor(0.72, 0.72, 0.72)

local function CreateSettingsInfoBlock(parent, title, value, width, valueHeight)
    valueHeight = valueHeight or 28

    local block = CreateFrame("Frame", nil, parent)
    block:SetSize(width or 560, valueHeight + 24)

    block.Title = block:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    block.Title:SetPoint("TOPLEFT")
    block.Title:SetText(title)
    block.Title:SetTextColor(1, 0.82, 0)

    block.Value = block:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    block.Value:SetPoint("TOPLEFT", block.Title, "BOTTOMLEFT", 0, -5)
    block.Value:SetSize(width or 560, valueHeight)
    block.Value:SetJustifyH("LEFT")
    block.Value:SetJustifyV("TOP")
    if block.Value.SetWordWrap then
        block.Value:SetWordWrap(true)
    end
    if block.Value.SetNonSpaceWrap then
        block.Value:SetNonSpaceWrap(true)
    end
    block.Value:SetText(value)

    return block
end

local settingsInfoWidth = 560

TMA.SettingsPanel.IntroBlock = CreateSettingsInfoBlock(TMA.SettingsPanel, L["SETTINGS_INTRO_TITLE"], L["SETTINGS_INTRO_DESC"], settingsInfoWidth, 44)
TMA.SettingsPanel.IntroBlock:SetPoint("TOPLEFT", TMA.SettingsPanel.Icon, "BOTTOMLEFT", 0, -28)

TMA.SettingsPanel.ShortcutBlock = CreateSettingsInfoBlock(TMA.SettingsPanel, L["SETTINGS_SHORTCUT_TITLE"], L["SETTINGS_SHORTCUT_VALUE"], settingsInfoWidth)
TMA.SettingsPanel.ShortcutBlock:SetPoint("TOPLEFT", TMA.SettingsPanel.IntroBlock, "BOTTOMLEFT", 0, -20)
TMA.SettingsPanel.ShortcutBlock.Value:SetTextColor(0.52, 0.76, 1)

TMA.SettingsPanel.VersionBlock = CreateSettingsInfoBlock(TMA.SettingsPanel, L["SETTINGS_VERSION_TITLE"], string.format(L["SETTINGS_VERSION_FORMAT"], addonVersion), settingsInfoWidth)
TMA.SettingsPanel.VersionBlock:SetPoint("TOPLEFT", TMA.SettingsPanel.ShortcutBlock, "BOTTOMLEFT", 0, -18)
TMA.SettingsPanel.VersionBlock.Value:SetTextColor(0.86, 0.86, 0.86)

TMA.SettingsPanel.AuthorBlock = CreateSettingsInfoBlock(TMA.SettingsPanel, L["SETTINGS_AUTHOR_TITLE"], L["SETTINGS_AUTHOR_VALUE"], settingsInfoWidth)
TMA.SettingsPanel.AuthorBlock:SetPoint("TOPLEFT", TMA.SettingsPanel.VersionBlock, "BOTTOMLEFT", 0, -18)
TMA.SettingsPanel.AuthorBlock.Value:SetTextColor(0.78, 0.72, 0.62)

TMA.SettingsPanel.HomepageBlock = CreateSettingsInfoBlock(TMA.SettingsPanel, L["SETTINGS_AUTHOR_HOMEPAGE_TITLE"], L["SETTINGS_AUTHOR_HOMEPAGE_VALUE"], settingsInfoWidth)
TMA.SettingsPanel.HomepageBlock:SetPoint("TOPLEFT", TMA.SettingsPanel.AuthorBlock, "BOTTOMLEFT", 0, -18)
TMA.SettingsPanel.HomepageBlock.Value:SetTextColor(0.52, 0.76, 1)

TMA.SettingsPanel.OpenButton = CreateFrame("Button", nil, TMA.SettingsPanel, "UIPanelButtonTemplate")
TMA.SettingsPanel.OpenButton:SetSize(148, 26)
TMA.SettingsPanel.OpenButton:SetPoint("TOPLEFT", TMA.SettingsPanel.HomepageBlock, "BOTTOMLEFT", 0, -20)
TMA.SettingsPanel.OpenButton:SetText(L["SETTINGS_OPEN_PANEL"])
TMA.SettingsPanel.OpenButton:SetScript("OnClick", function()
    ShowSettingsPanel()
end)

local function RegisterTMASettingsCategory()
    if Settings and Settings.RegisterCanvasLayoutCategory and Settings.RegisterAddOnCategory then
        local ok, category = pcall(Settings.RegisterCanvasLayoutCategory, TMA.SettingsPanel, L["ADDON_TITLE"])
        if ok and category then
            category.ID = ADDON_NAME
            local registerOk = pcall(Settings.RegisterAddOnCategory, category)
            if registerOk then
                TMA.SettingsCategory = category
                return
            end
        end
    end

    if InterfaceOptions_AddCategory then
        pcall(InterfaceOptions_AddCategory, TMA.SettingsPanel)
    end
end

RegisterTMASettingsCategory()

local MINIMAP_BUTTON_SIZE = 31
local MINIMAP_BUTTON_ICON_SIZE = 17
local MINIMAP_BUTTON_ICON_OFFSET_X = -0.5
local MINIMAP_BUTTON_ICON_OFFSET_Y = 1
local MINIMAP_BUTTON_EDGE_OFFSET = 5
local DEFAULT_MINIMAP_BUTTON_ANGLE = 225

local function GetMinimapButtonAngle()
    local angle = TargetMarkerAssistantDB and tonumber(TargetMarkerAssistantDB.minimapButtonAngle)
    if not angle then
        angle = DEFAULT_MINIMAP_BUTTON_ANGLE
    end
    return angle % 360
end

local function GetMinimapButtonRadius()
    local width = Minimap and Minimap.GetWidth and Minimap:GetWidth() or 140
    local height = Minimap and Minimap.GetHeight and Minimap:GetHeight() or width
    return (math.min(width, height) / 2) + MINIMAP_BUTTON_EDGE_OFFSET
end

local function GetMinimapDragAngle()
    local centerX, centerY = Minimap:GetCenter()
    local cursorX, cursorY = GetCursorPosition()
    local scale = Minimap:GetEffectiveScale() or 1
    cursorX = cursorX / scale
    cursorY = cursorY / scale

    local dx = cursorX - centerX
    local dy = cursorY - centerY
    if dx == 0 and dy == 0 then
        return GetMinimapButtonAngle()
    end
    if dx == 0 then
        return dy > 0 and 90 or 270
    end

    local angle = math.deg(math.atan2(dy, dx))

    return angle % 360
end

local function SaveMinimapButtonAngle(angle)
    if not TargetMarkerAssistantDB then return end
    TargetMarkerAssistantDB.minimapButtonAngle = angle % 360
end

local minimapButton = CreateFrame("Button", "TargetMarkerAssistantMinimapButton", Minimap)
minimapButton:SetSize(MINIMAP_BUTTON_SIZE, MINIMAP_BUTTON_SIZE)
minimapButton:SetFrameStrata("MEDIUM")
minimapButton:RegisterForDrag("LeftButton")
minimapButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")

minimapButton.Background = minimapButton:CreateTexture(nil, "ARTWORK", nil, 1)
minimapButton.Background:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
minimapButton.Background:SetSize(22, 22)
minimapButton.Background:SetPoint("TOPLEFT", minimapButton, "TOPLEFT", 5, -5)

local minimapIconShadow = minimapButton:CreateTexture(nil, "ARTWORK", nil, 2)
minimapIconShadow:SetTexture(minimapIconPath)
minimapIconShadow:SetSize(MINIMAP_BUTTON_ICON_SIZE, MINIMAP_BUTTON_ICON_SIZE)
minimapIconShadow:SetPoint("CENTER", minimapButton, "CENTER", 1 + MINIMAP_BUTTON_ICON_OFFSET_X, -1 + MINIMAP_BUTTON_ICON_OFFSET_Y)
minimapIconShadow:SetTexCoord(0.08, 0.92, 0.08, 0.92)
minimapIconShadow:SetVertexColor(0, 0, 0, 0.4)

local minimapIcon = minimapButton:CreateTexture(nil, "ARTWORK", nil, 3)
minimapIcon:SetTexture(minimapIconPath)
minimapIcon:SetSize(MINIMAP_BUTTON_ICON_SIZE, MINIMAP_BUTTON_ICON_SIZE)
minimapIcon:SetPoint("CENTER", minimapButton, "CENTER", MINIMAP_BUTTON_ICON_OFFSET_X, MINIMAP_BUTTON_ICON_OFFSET_Y)
minimapIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

minimapButton.Border = minimapButton:CreateTexture(nil, "ARTWORK", nil, 4)
minimapButton.Border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
minimapButton.Border:SetSize(50, 50)
minimapButton.Border:SetPoint("TOPLEFT")

local minimapHighlight = minimapButton:CreateTexture(nil, "HIGHLIGHT")
minimapHighlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
minimapHighlight:SetBlendMode("ADD")
minimapHighlight:SetAllPoints(minimapButton)

local function UpdateMinimapButtonPosition()
    local angle = math.rad(GetMinimapButtonAngle())
    local radius = GetMinimapButtonRadius()
    minimapButton:ClearAllPoints()
    minimapButton:SetPoint("CENTER", Minimap, "CENTER", math.cos(angle) * radius, math.sin(angle) * radius)
end

local function UpdateMinimapButtonPositionFromCursor()
    SaveMinimapButtonAngle(GetMinimapDragAngle())
    UpdateMinimapButtonPosition()
end

TMA.SetMinimapButtonPressed = function(pressed)
    local offsetX = pressed and 1 or 0
    local offsetY = pressed and -1 or 0
    minimapButton.Background:ClearAllPoints()
    minimapButton.Background:SetPoint("TOPLEFT", minimapButton, "TOPLEFT", 5 + offsetX, -5 + offsetY)

    minimapIconShadow:ClearAllPoints()
    minimapIconShadow:SetPoint("CENTER", minimapButton, "CENTER", 1 + MINIMAP_BUTTON_ICON_OFFSET_X + offsetX, -1 + MINIMAP_BUTTON_ICON_OFFSET_Y + offsetY)

    minimapIcon:ClearAllPoints()
    minimapIcon:SetPoint("CENTER", minimapButton, "CENTER", MINIMAP_BUTTON_ICON_OFFSET_X + offsetX, MINIMAP_BUTTON_ICON_OFFSET_Y + offsetY)
    if pressed then
        minimapButton:SetAlpha(0.88)
    else
        minimapButton:SetAlpha(1)
    end
end

UpdateMinimapButtonPosition()

minimapButton:SetScript("OnClick", function(self, button)
    if self.ignoreNextClick then
        self.ignoreNextClick = false
        return
    end
    if button == "RightButton" then
        ToggleSettingsPanelFromMinimap("COMBAT")
    else
        ToggleSettingsPanelFromMinimap("MARKER")
    end
end)
minimapButton:SetScript("OnMouseDown", function(_, button)
    if button == "LeftButton" or button == "RightButton" then
        TMA.SetMinimapButtonPressed(true)
    end
end)
minimapButton:SetScript("OnMouseUp", function()
    TMA.SetMinimapButtonPressed(false)
end)
minimapButton:SetScript("OnEnter", function(self)
    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
    GameTooltip:SetText(L["ADDON_TITLE"], 1, 1, 1)
    GameTooltip:AddLine(L["MINIMAP_TOOLTIP_LEFT"], 1, 0.82, 0, true)
    GameTooltip:AddLine(L["MINIMAP_TOOLTIP_RIGHT"], 1, 0.82, 0, true)
    GameTooltip:Show()
end)
minimapButton:SetScript("OnLeave", function()
    TMA.SetMinimapButtonPressed(false)
    GameTooltip:Hide()
end)
minimapButton:SetScript("OnDragStart", function(self)
    self.ignoreNextClick = true
    TMA.SetMinimapButtonPressed(true)
    self:SetScript("OnUpdate", UpdateMinimapButtonPositionFromCursor)
end)
minimapButton:SetScript("OnDragStop", function(self)
    UpdateMinimapButtonPositionFromCursor()
    self:SetScript("OnUpdate", nil)
    TMA.SetMinimapButtonPressed(false)
    C_Timer.After(0.1, function()
        self.ignoreNextClick = false
    end)
end)

SLASH_TARGETMARKERASSISTANT1 = "/tma"
SLASH_TARGETMARKERASSISTANT2 = "/TMA"
SLASH_TARGETMARKERASSISTANT3 = "/mb"
SLASH_TARGETMARKERASSISTANT4 = "/MB"
SLASH_TARGETMARKERASSISTANTRELOAD1 = "/rl"
SlashCmdList["TARGETMARKERASSISTANTRELOAD"] = function()
    ReloadUI()
end

SlashCmdList["TARGETMARKERASSISTANT"] = function(msg)
    TMA.EnsureDBAliases()
    msg = tostring(msg or ""):gsub("^%s+", ""):gsub("%s+$", "")
    local command = msg:lower()

    if command == "debug on" then
        TargetMarkerAssistantDB.debugInterruptAnnounce = true
        TMA.DB = TargetMarkerAssistantDB
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[TMA-打断调试]|r 已开启打断通报调试输出。")
        end
        if TMA.RefreshInterruptCombatLogFrame then
            TMA.RefreshInterruptCombatLogFrame("DEBUG_ON")
        end
        return
    elseif command == "debug off" then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[TMA-打断调试]|r 已关闭打断通报调试输出。")
        end
        TargetMarkerAssistantDB.debugInterruptAnnounce = false
        TMA.DB = TargetMarkerAssistantDB
        return
    elseif command == "debug status" then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[TMA-打断调试]|r 当前打断通报调试开关：" .. tostring(TargetMarkerAssistantDB.debugInterruptAnnounce))
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[TMA-打断调试]|r 战斗日志直读：已停用；通报改用队友施法事件与插件同步。")
        end
        if TMA.RefreshInterruptCombatLogFrame then
            TMA.RefreshInterruptCombatLogFrame("DEBUG_STATUS")
        end
        return
    elseif command == "castdebug" or command == "debug cast" then
        TMA.RefreshTargetFocusCastCache("slash-castdebug")
        TMA.CaptureBestVisibleInterruptCast("slash-castdebug")
        local candidate = TMA.GetRecentTrackedCastCandidate and TMA.GetRecentTrackedCastCandidate()
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[TMA-打断调试]|r 可见读条快照：" .. TMA.GetVisibleInterruptCastDebugSnapshot())
            DEFAULT_CHAT_FRAME:AddMessage(
                "|cff00ccff[TMA-打断调试]|r 最近缓存读条："
                .. TMA.SafeDebugText(candidate and candidate.spellName, "无")
                .. "，单位=" .. TMA.SafeDebugText(candidate and candidate.unit, "无")
            )
        end
        return
    elseif command == "testinterrupt" then
        TMA.DispatchTestInterruptNotice()
        return
    end

    OpenSettingsPanel()
end
