local addonName, ns = ...
local L = ns.L

-- Midnight 12.x secret-value helpers. Keep addon logic from inspecting values
-- that Blizzard marks as secret on restricted/tainted execution paths.
local function IsSecretValue(value)
    return issecretvalue and issecretvalue(value) or false
end

local function HasAnySecretValue(...)
    if hasanysecretvalues then
        return hasanysecretvalues(...)
    end
    if not issecretvalue then
        return false
    end
    for i = 1, select("#", ...) do
        if issecretvalue(select(i, ...)) then
            return true
        end
    end
    return false
end

local function SafeUpdateChatHeader(editBox)
    if not editBox or not ChatEdit_UpdateHeader then return end
    if securecall then
        securecall(ChatEdit_UpdateHeader, editBox)
    else
        ChatEdit_UpdateHeader(editBox)
    end
end

-- ========================================================================
-- Optional keyboard channel cycling
-- ========================================================================
-- No TAB interception. The user can assign "Next ChatFloater channel" in
-- Blizzard's normal Key Bindings UI. This avoids competing with target-cycle
-- bindings and avoids replacing Blizzard chat functions.
do
    local cycleOrder = {
        { id = "Say",      chatType = "SAY",           cmd = "say",      use = function() return true end },
        { id = "Yell",     chatType = "YELL",          cmd = "yell",     use = function() return true end },
        { id = "Whisper",  chatType = "WHISPER",       cmd = nil,        use = function() return true end },
        { id = "Party",    chatType = "PARTY",         cmd = "party",    use = function() return IsInGroup() end },
        { id = "Raid",     chatType = "RAID",          cmd = "raid",     use = function() return IsInRaid() end },
        { id = "Instance", chatType = "INSTANCE_CHAT", cmd = "instance", use = function() return IsInGroup(LE_PARTY_CATEGORY_INSTANCE) end },
        { id = "Guild",    chatType = "GUILD",         cmd = "guild",    use = function() return IsInGuild() end },
    }

    local function GetCurrentChatType()
        local editBox = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
        if not editBox or not editBox.GetAttribute then return nil end
        local chatType = editBox:GetAttribute("chatType")
        if IsSecretValue(chatType) then return nil end
        return chatType
    end

    function ns.CycleChatChannel()
        local current = GetCurrentChatType()
        if not current then return end

        local start = 0
        for i, item in ipairs(cycleOrder) do
            if current == item.chatType or
               (item.chatType == "PARTY" and current == "PARTY_LEADER") or
               (item.chatType == "RAID" and (current == "RAID_LEADER" or current == "RAID_WARNING")) or
               (item.chatType == "INSTANCE_CHAT" and current == "INSTANCE_CHAT_LEADER") then
                start = i
                break
            end
        end

        local editBox = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
        if not editBox then return end

        for step = 1, #cycleOrder do
            local idx = ((start + step - 1) % #cycleOrder) + 1
            local item = cycleOrder[idx]
            if item.use() then
                -- Whisper needs a target; skip it unless already whispering.
                if item.chatType ~= "WHISPER" then
                    editBox:SetAttribute("chatType", item.chatType)
                    SafeUpdateChatHeader(editBox)
                    return
                end
            end
        end
    end
end

function ChatFloater_CycleChatChannel()
    if ns and ns.CycleChatChannel then ns.CycleChatChannel() end
end

-- ========================================================================
-- 【快捷聊天条】
-- ========================================================================
do
    local function SwitchToChannel(cmd)
        local cf = SELECTED_DOCK_FRAME or DEFAULT_CHAT_FRAME or ChatFrame1
        if not cf or not ChatFrame_OpenChat then return false end

        local active = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
        if active and cf.editBox and cf.editBox.GetText then
            local currentText = cf.editBox:GetText()
            if IsSecretValue(currentText) then
                -- Do not inspect/rebuild secret chat text. Let Blizzard own it.
                return false
            end
            if type(currentText) == "string" and currentText ~= "" then
                ChatFrame_OpenChat("/" .. tostring(cmd) .. " " .. currentText, cf)
                return true
            end
        end

        ChatFrame_OpenChat("/" .. tostring(cmd), cf)
        return true
    end

    local countdownEndTime = 0

    local buttonDefs = {
        {
            id = "Say",
            var = "isSay",
            shortName = "S",
            color = { 1, 1, 1 },
            tooltip = nil,
            onClick = function() SwitchToChannel("s") end,
        },
        {
            id = "Yell",
            var = "isYell",
            shortName = "Y",
            color = { 1, 0.25, 0.18 },
            tooltip = L["大喊"],
            onClick = function() SwitchToChannel("y") end,
        },
        {
            id = "Whisper",
            var = "isWhisper",
            shortName = "W",
            color = { 1.0, 0.50, 1.0 },
            tooltip = L["密语"],
            onClick = function()
                local cf = DEFAULT_CHAT_FRAME or ChatFrame1
                if ChatFrame_OpenChat then ChatFrame_OpenChat("/w ", cf) end
            end,
        },
        {
            id = "Party",
            var = "isParty",
            shortName = "P",
            color = { 0.45, 0.72, 1.0 },
            tooltip = L["队伍"],
            onClick = function() SwitchToChannel("p") end,
        },
        {
            id = "Raid",
            var = "isRaid",
            shortName = "R",
            color = { 1.0, 0.55, 0.10 },
            tooltip = L["左键：团队频道\n右键：团队通知"],
            onClick = function(_, button)
                if button == "RightButton" then
                    SwitchToChannel("rw")
                else
                    SwitchToChannel("raid")
                end
            end,
        },
        {
            id = "Instance",
            var = "isInstance",
            shortName = "I",
            color = { 0.72, 0.38, 1.0 },
            tooltip = nil,
            onClick = function() SwitchToChannel("instance") end,
        },
        {
            id = "Guild",
            var = "isGuild",
            shortName = "G",
            color = { 0.25, 1, 0.25 },
            tooltip = L["公会"],
            onClick = function() SwitchToChannel("g") end,
        },
        {
            id = "Dice",
            var = "isDiceButton",
            shortName = L["骰"],
            color = { 0.7, 0.7, 0.7 },
            tooltip = L["左键：投掷点数\n右键：掷骰记录"],
            onClick = function(_, button)
                if button == "LeftButton" then
                    RandomRoll(1, 100)
                elseif button == "RightButton" then
                    if ToggleLootHistoryFrame then
                        ToggleLootHistoryFrame()
                    elseif GroupLootHistoryFrame then
                        if GroupLootHistoryFrame:IsShown() then
                            HideUIPanel(GroupLootHistoryFrame)
                        else
                            ShowUIPanel(GroupLootHistoryFrame)
                        end
                    end
                end
            end,
        },
        {
            id = "Macro",
            var = "isMacroButton",
            shortName = L["宏"],
            color = { 0.2, 0.4, 1 },
            tooltip = L["宏界面"],
            onClick = function(_, button)
                if button == "LeftButton" then
                    if MacroFrame and MacroFrame:IsShown() then
                        HideUIPanel(MacroFrame)
                    else
                        ShowMacroFrame()
                    end
                end
            end,
        },
        {
            id = "ReadyCheck",
            var = "isReadyCheck",
            shortName = L["备"],
            color = { 0.4, 1, 0.6 },
            tooltip = L["左键：就位确认\n右键：倒数计时"],
            onClick = function(_, button)
                if button == "LeftButton" then
                    if C_PartyInfo and C_PartyInfo.DoReadyCheck then
                        C_PartyInfo.DoReadyCheck()
                    elseif DoReadyCheck then
                        DoReadyCheck()
                    end
                elseif button == "RightButton" then
                    if not C_PartyInfo or not C_PartyInfo.DoCountdown then return end
                    local secs = tonumber(ChatFloaterDB and ChatFloaterDB.readyCheckCountdown) or 5
                    secs = math.max(1, math.floor(secs))
                    local now = GetTime()
                    if now < countdownEndTime then
                        if C_PartyInfo.CancelCountdown then
                            C_PartyInfo.CancelCountdown()
                        else
                            C_PartyInfo.DoCountdown(0)
                        end
                        countdownEndTime = 0
                    else
                        C_PartyInfo.DoCountdown(secs)
                        countdownEndTime = now + secs
                    end
                end
            end,
        },
        {
            id = "LeaveReset",
            var = "isLeaveReset",
            shortName = L["退"],
            color = { 1, 1, 0 },
            tooltip = L["左键：退出队伍\n右键：重置副本"],
            onClick = function(_, button)
                if button == "LeftButton" then
                    if not C_PartyInfo or not C_PartyInfo.LeaveParty then return end

                    local wasInDelve = false
                    if C_PartyInfo.IsDelveInProgress then
                        local value = C_PartyInfo.IsDelveInProgress()
                        if not IsSecretValue(value) then
                            wasInDelve = value and true or false
                        end
                    end

                    C_PartyInfo.LeaveParty()

                    if wasInDelve and C_PartyInfo.DelveTeleportOut then
                        C_Timer.After(0.3, function()
                            if C_PartyInfo and C_PartyInfo.DelveTeleportOut then
                                C_PartyInfo.DelveTeleportOut()
                            end
                        end)
                    end
                elseif button == "RightButton" then
                    if ResetInstances then ResetInstances() end
                end
            end,
        },
        {
            id = "Reload",
            var = "isReload",
            shortName = L["RL"],
            color = { 0, 0.8, 1 },
            tooltip = L["左键：重载界面\n右键：重置伤害"],
            onClick = function(self, button)
                if button == "LeftButton" then
                    local mode = ChatFloaterDB.reloadClickMode or "single"
                    if mode == "single" then
                        ReloadUI()
                    else
                        local now = GetTime()
                        if self._lastClickTime and (now - self._lastClickTime) < 0.4 then
                            self._lastClickTime = nil
                            ReloadUI()
                        else
                            self._lastClickTime = now
                        end
                    end
                elseif button == "RightButton" then
                    if C_DamageMeter and C_DamageMeter.ResetAllCombatSessions then
                        C_DamageMeter.ResetAllCombatSessions()
                    end
                end
            end,
        },
    }

    local displayNames = {
        Say = L["说话频道"],
        Yell = L["大喊频道"],
        Whisper = L["密语"],
        Party = L["队伍频道"],
        Raid = L["团队频道"],
        Instance = "Instance",
        Guild = L["公会频道"],
        Dice = L["投掷"],
        Macro = L["宏界面"],
        ReadyCheck = L["就位确认"],
        LeaveReset = L["离开重置"],
        Reload = L["重载"],
    }
    for _, def in ipairs(buttonDefs) do
        def.displayName = displayNames[def.id] or def.tooltip or def.shortName
    end

    local chatBarFrame = nil
    local activeButtons = {}
    local manualActiveId = nil
    local activePollElapsed = 0

    local function GetLayout(enabled, direction, spacing, db)
        local padX, padY = ns.Visuals.GetPanelPadding(db)
        local metrics = {}
        local totalW, totalH = padX * 2, padY * 2
        local maxW, maxH = 0, 0

        for i, def in ipairs(enabled) do
            local w, h = ns.Visuals.GetButtonMetrics(def, db)
            metrics[i] = { w = w, h = h }
            if w > maxW then maxW = w end
            if h > maxH then maxH = h end
            if direction == "HORIZONTAL" then
                totalW = totalW + w
            else
                totalH = totalH + h
            end
        end

        if direction == "HORIZONTAL" then
            totalW = totalW + math.max(0, #enabled - 1) * spacing
            totalH = totalH + maxH
        else
            totalH = totalH + math.max(0, #enabled - 1) * spacing
            totalW = totalW + maxW
        end
        return metrics, totalW, totalH, padX, padY
    end

    local function ResolveActiveButtonId()
        local editBox = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
        if not editBox or not editBox.GetAttribute then
            return manualActiveId
        end

        local chatType = editBox:GetAttribute("chatType")
        if IsSecretValue(chatType) then return manualActiveId end

        if chatType == "SAY" then return "Say" end
        if chatType == "YELL" then return "Yell" end
        if chatType == "WHISPER" or chatType == "BN_WHISPER" then return "Whisper" end
        if chatType == "PARTY" or chatType == "PARTY_LEADER" then return "Party" end
        if chatType == "RAID" or chatType == "RAID_LEADER" or chatType == "RAID_WARNING" then return "Raid" end
        if chatType == "INSTANCE_CHAT" or chatType == "INSTANCE_CHAT_LEADER" then return "Instance" end
        if chatType == "GUILD" or chatType == "OFFICER" then return "Guild" end
        return manualActiveId
    end

    local function UpdateActiveChannelState()
        if not ChatFloaterDB or ChatFloaterDB.chatShowActiveChannel == false then
            for _, btn in pairs(activeButtons) do
                if btn and btn:IsShown() then ns.Visuals.SetActive(btn, false, ChatFloaterDB) end
            end
            return
        end
        local activeId = ResolveActiveButtonId()
        for id, btn in pairs(activeButtons) do
            if btn and btn:IsShown() then
                ns.Visuals.SetActive(btn, id == activeId and ns.Visuals.IsChannelButton(id), ChatFloaterDB)
            end
        end
    end

    local BeginBarMove, EndBarMove

    local function SetupMouseEvents(button, def)
        button:RegisterForDrag("LeftButton")

        button:SetScript("OnClick", function(self, mouseButton)
            if self.floatWasDragged then
                self.floatWasDragged = false
                return
            end
            def.onClick(self, mouseButton)
            if ns.Visuals.IsChannelButton(def.id) then
                manualActiveId = def.id
                UpdateActiveChannelState()
            end
        end)

        button:SetScript("OnEnter", function(self)
            ns.Visuals.SetHover(self, true, ChatFloaterDB)
            ns.Visuals.ShowTooltip(self, def)
        end)

        button:SetScript("OnLeave", function(self)
            ns.Visuals.HideTooltip()
            ns.Visuals.SetHover(self, false, ChatFloaterDB)
        end)

        button:SetScript("OnMouseDown", function(self)
            ns.Visuals.SetPressed(self, true, ChatFloaterDB)
        end)

        button:SetScript("OnMouseUp", function(self)
            ns.Visuals.SetPressed(self, false, ChatFloaterDB)
        end)

        button:SetScript("OnDragStart", function(self)
            if BeginBarMove() then
                self.floatWasDragged = true
            end
        end)

        button:SetScript("OnDragStop", function()
            EndBarMove()
        end)
    end

    local function SaveFreePosition()
        if not chatBarFrame or not ChatFloaterDB then return end
        local cx, cy = chatBarFrame:GetCenter()
        local ux, uy = UIParent:GetCenter()
        if not cx or not cy or not ux or not uy then return end

        ChatFloaterDB.chatBarAnchorMode = "FREE"
        ChatFloaterDB.chatBarPosition = ChatFloaterDB.chatBarPosition or {}
        ChatFloaterDB.chatBarPosition.FREE = ChatFloaterDB.chatBarPosition.FREE or { x = 0, y = 0 }
        ChatFloaterDB.chatBarPosition.FREE.x = math.floor((cx - ux) + 0.5)
        ChatFloaterDB.chatBarPosition.FREE.y = math.floor((cy - uy) + 0.5)

        if ns.RefreshGUI then ns.RefreshGUI() end
    end

    BeginBarMove = function()
        if not chatBarFrame or not ChatFloaterDB then return false end

        -- Shift+Left-drag is always available.
        -- The GUI move mode additionally allows ordinary Left-drag.
        local allowed = ChatFloaterDB.barUnlocked == true or (IsShiftKeyDown and IsShiftKeyDown())
        if not allowed then return false end

        ChatFloaterDB.chatBarAnchorMode = "FREE"
        chatBarFrame:SetMovable(true)
        chatBarFrame:StartMoving()
        chatBarFrame.floatMoving = true
        return true
    end

    EndBarMove = function()
        if not chatBarFrame then return end
        if chatBarFrame.floatMoving then
            chatBarFrame:StopMovingOrSizing()
            chatBarFrame.floatMoving = false
            SaveFreePosition()
        end
    end

    local function UpdatePosition()
        if not chatBarFrame then return end
        local db = ChatFloaterDB
        local mode = db.chatBarAnchorMode or "FREE"
        local pos = db.chatBarPosition and db.chatBarPosition[mode]
        local x = pos and pos.x or 0
        local y = pos and pos.y or 0
        chatBarFrame:ClearAllPoints()
        if mode == "ANCHOR" then
            local anchorTarget

            if ns.GetAdaptiveChatAnchor then
                anchorTarget = ns.GetAdaptiveChatAnchor()
            end

            if not anchorTarget then
                anchorTarget = GeneralDockManager
                if not anchorTarget or not anchorTarget:IsShown() then
                    anchorTarget = DEFAULT_CHAT_FRAME or ChatFrame1
                end
            end

            chatBarFrame:SetPoint("TOPLEFT", anchorTarget, "TOPLEFT", x, y)
        else
            chatBarFrame:SetPoint("CENTER", UIParent, "CENTER", x, y)
        end
    end

    local function RebuildButtons()
        if not chatBarFrame then return end
        local db = ChatFloaterDB
        if not db or not db.isChatBar then
            chatBarFrame:Hide()
            return
        end

        local enabled = {}
        for _, def in ipairs(buttonDefs) do
            if db[def.var] then
                enabled[#enabled + 1] = def
            end
        end

        if #enabled == 0 then
            chatBarFrame:Hide()
            return
        end

        local direction = db.chatLayoutDirection or "HORIZONTAL"
        local spacing = tonumber(db.chatButtonSpacing) or 0
        local metrics, totalW, totalH, padX, padY = GetLayout(enabled, direction, spacing, db)

        for _, btn in pairs(activeButtons) do
            btn:Hide()
            btn:ClearAllPoints()
        end

        local cursorX, cursorY = padX, -padY
        for i, def in ipairs(enabled) do
            local btnName = "ChatFloaterChatButton_" .. def.id
            local btn = activeButtons[def.id] or _G[btnName]
            if not btn then
                btn = CreateFrame("Button", btnName, chatBarFrame, "BackdropTemplate")
                btn:RegisterForClicks("AnyUp")
            end

            local w, h = metrics[i].w, metrics[i].h
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", chatBarFrame, "TOPLEFT", cursorX, cursorY)
            btn:SetSize(w, h)

            ns.Visuals.ApplyButton(btn, def, db)
            SetupMouseEvents(btn, def)
            btn:SetAlpha(1)
            btn:Show()
            activeButtons[def.id] = btn

            if direction == "HORIZONTAL" then
                cursorX = cursorX + w + spacing
            else
                cursorY = cursorY - h - spacing
            end
        end

        -- The final Minimal Float button does not need a trailing separator.
        local last = enabled[#enabled] and activeButtons[enabled[#enabled].id]
        if last and last.floatSeparator then last.floatSeparator:Hide() end

        chatBarFrame:SetSize(totalW, totalH)
        ns.Visuals.ApplyPanel(chatBarFrame, db)
        chatBarFrame:Show()
        UpdatePosition()
        UpdateActiveChannelState()
    end

    local function InitChatBar()
        local db = ChatFloaterDB
        if not db or not db.isChatBar then return end

        if not chatBarFrame then
            chatBarFrame = CreateFrame("Frame", "ChatFloaterChatBarFrame", UIParent, "BackdropTemplate")
            chatBarFrame:SetFrameStrata("MEDIUM")
            chatBarFrame:SetFrameLevel(200)
            chatBarFrame:SetSize(1, 1)
            chatBarFrame:SetClampedToScreen(true)
            chatBarFrame:SetMovable(true)
            chatBarFrame:EnableMouse(true)
            chatBarFrame:RegisterForDrag("LeftButton")
            chatBarFrame:SetScript("OnDragStart", function()
                BeginBarMove()
            end)
            chatBarFrame:SetScript("OnDragStop", EndBarMove)
            chatBarFrame:SetScript("OnUpdate", function(_, elapsed)
                activePollElapsed = activePollElapsed + elapsed
                if activePollElapsed < 0.15 then return end
                activePollElapsed = 0
                UpdateActiveChannelState()
            end)
        end
        RebuildButtons()
    end

    function ns.OnChatBarToggle(value)
        if value then
            InitChatBar()
        else
            if chatBarFrame then chatBarFrame:Hide() end
        end
    end

    function ns.OnChatBarChanged()
        if not ChatFloaterDB or not ChatFloaterDB.isChatBar then return end
        if not chatBarFrame then
            InitChatBar()
        else
            RebuildButtons()
        end
    end

    function ns.OnChatBarPositionChanged()
        UpdatePosition()
    end

    function ns.SetBarUnlocked(value)
        if not ChatFloaterDB then return end
        ChatFloaterDB.barUnlocked = value and true or false
        if chatBarFrame then
            chatBarFrame:EnableMouse(true)
        end
    end

    function ns.ResetBarPosition()
        if not ChatFloaterDB then return end
        ChatFloaterDB.chatBarAnchorMode = "FREE"
        ChatFloaterDB.chatBarPosition = ChatFloaterDB.chatBarPosition or {}
        ChatFloaterDB.chatBarPosition.FREE = { x = 0, y = 0 }
        UpdatePosition()
        if ns.RefreshGUI then ns.RefreshGUI() end
    end

    function ns.GetBarFrame()
        return chatBarFrame
    end

    local chatEventFrame = CreateFrame("Frame")
    chatEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    chatEventFrame:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" then
            local db = ChatFloaterDB
            if not db then return end
            if db.isChatBar then
                C_Timer.After(1, InitChatBar)
            end
        end
    end)
end

-- ========================================================================
-- 【框体渐隐系统】
-- ========================================================================
do
    local MODE_SHOW = 0
    local MODE_HIDE = 1
    local MODE_FADE = 2
    local MODE_FADE_COMBAT = 3

    -- -------------------------------------------------------------------
    -- 核心：FadeEngine + 工具函数 + 通用渐隐钩子
    -- -------------------------------------------------------------------
    local animQueue = {}
    local animFrame = CreateFrame("Frame")
    local animRunning = false

    local function StopAnim(key)
        animQueue[key] = nil
        if not next(animQueue) then
            animFrame:SetScript("OnUpdate", nil)
            animRunning = false
        end
    end

    local function PlayFade(key, frame, targetAlpha, duration)
        if not frame then return end
        local current = frame:GetAlpha()
        if math.abs(current - targetAlpha) < 0.001 then return end
        if duration <= 0 then
            StopAnim(key)
            frame:SetAlpha(targetAlpha)
            return
        end
        animQueue[key] = {
            frame = frame,
            startAlpha = current,
            targetAlpha = targetAlpha,
            startTime = GetTime(),
            duration = duration,
        }
        if not animRunning then
            animRunning = true
            animFrame:SetScript("OnUpdate", function()
                local now = GetTime()
                for k, a in pairs(animQueue) do
                    local t = math.min((now - a.startTime) / a.duration, 1)
                    a.frame:SetAlpha(a.startAlpha + (a.targetAlpha - a.startAlpha) * t)
                    if t >= 1 then animQueue[k] = nil end
                end
                if not next(animQueue) then
                    animFrame:SetScript("OnUpdate", nil)
                    animRunning = false
                end
            end)
        end
    end

    local function GetFadeIn() return ChatFloaterDB.fadeInDuration or 0.2 end
    local function GetFadeOut() return ChatFloaterDB.fadeOutDuration or 0.2 end
    local function InCombat() return UnitAffectingCombat("player") end

    local globalFadeEnabled = true
    local hookStates = {}

    local function HookFade(key, frame, buttons)
        local state = hookStates[key]
        if not state then
            state = {
                frame = frame,
                buttons = buttons,
                hookedButtons = {},
                leaveGeneration = 0,
            }
            hookStates[key] = state
        else
            state.frame = frame
            state.buttons = buttons
        end

        local function OnEnter()
            state.leaveGeneration = state.leaveGeneration + 1
            if not ChatFloaterDB then return end
            local mode = ChatFloaterDB[key]
            if mode ~= MODE_FADE and mode ~= MODE_FADE_COMBAT then return end
            if not globalFadeEnabled then return end
            PlayFade(key, state.frame, 1, GetFadeIn())
        end

        local function OnLeave()
            state.leaveGeneration = state.leaveGeneration + 1
            local generation = state.leaveGeneration
            if not ChatFloaterDB then return end
            local mode = ChatFloaterDB[key]
            if mode ~= MODE_FADE and mode ~= MODE_FADE_COMBAT then return end
            if not globalFadeEnabled then return end

            C_Timer.After(0.05, function()
                if generation ~= state.leaveGeneration then return end
                local currentFrame = state.frame
                if not currentFrame or currentFrame:IsMouseOver() then return end
                for _, btn in ipairs(state.buttons or {}) do
                    if btn and btn:IsMouseOver() then return end
                end
                if ChatFloaterDB and ChatFloaterDB[key] == MODE_FADE_COMBAT and InCombat() then return end
                PlayFade(key, currentFrame, 0, GetFadeOut())
            end)
        end

        if not state.frameHooked then
            frame:HookScript("OnEnter", OnEnter)
            frame:HookScript("OnLeave", OnLeave)
            state.frameHooked = true
        end

        for _, btn in ipairs(buttons) do
            if btn and not state.hookedButtons[btn] then
                btn:HookScript("OnEnter", OnEnter)
                btn:HookScript("OnLeave", OnLeave)
                state.hookedButtons[btn] = true
            end
        end
    end

    local function ApplyFadeState(key, frame, buttons, mode, dynamicButtons)
        HookFade(key, frame, buttons)
        if mode == MODE_FADE_COMBAT and InCombat() then
            StopAnim(key); frame:SetAlpha(1); return
        end
        if not globalFadeEnabled then
            PlayFade(key, frame, 1, GetFadeIn()); return
        end
        local hovered = frame:IsMouseOver()
        if not hovered then
            for _, btn in ipairs(buttons) do
                if btn and btn:IsMouseOver() then hovered = true; break end
            end
        end
        PlayFade(key, frame, hovered and 1 or 0, hovered and GetFadeIn() or GetFadeOut())
    end

    -- -------------------------------------------------------------------
    -- REGISTRY：框体数据表
    -- -------------------------------------------------------------------
    local REGISTRY = {}

    local chatBarButtonIds = {
        "Say", "Yell", "Whisper", "Party", "Raid", "Instance", "Guild",
        "Dice", "Macro", "ReadyCheck", "LeaveReset", "Reload",
    }

    REGISTRY[#REGISTRY + 1] = {
        key = "chatBar",
        noHide = true,
        retry = true,
        dynamicButtons = true,
        getFrame = function() return _G["ChatFloaterChatBarFrame"] end,
        getButtons = function()
            local btns = {}
            for _, id in ipairs(chatBarButtonIds) do
                local btn = _G["ChatFloaterChatButton_" .. id]
                if btn then btns[#btns + 1] = btn end
            end
            return btns
        end,
    }

    -- -------------------------------------------------------------------
    -- ApplyState：调度函数
    -- -------------------------------------------------------------------
    local function ApplyState(reg)
        if not ChatFloaterDB then return end
        local mode = ChatFloaterDB[reg.key] or MODE_SHOW
        local key = reg.key
        local frame = reg.getFrame()
        if not frame then return end
        local buttons = reg.getButtons()

        if mode == MODE_SHOW then
            StopAnim(key); frame:SetAlpha(1); return
        end

        if mode == MODE_HIDE then
            StopAnim(key); frame:SetAlpha(0); return
        end

        ApplyFadeState(key, frame, buttons, mode, reg.dynamicButtons)
    end

    -- -------------------------------------------------------------------
    -- 事件处理 + 初始化
    -- -------------------------------------------------------------------
    local function ApplyAllStates()
        for _, reg in ipairs(REGISTRY) do
            ApplyState(reg)
        end
    end

    local combatFrame = CreateFrame("Frame")
    combatFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
    combatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    combatFrame:SetScript("OnEvent", ApplyAllStates)

    function ns.OnUIFrameModeChanged(key, value)
        if not ChatFloaterDB then return end
        for _, reg in ipairs(REGISTRY) do
            if reg.key == key then ApplyState(reg); break end
        end
    end

    function ns.OnUIFadeTimerChanged() end

    local function Initialize()
        if ChatFloaterDB and ChatFloaterDB.globalFadeEnabled ~= nil then
            globalFadeEnabled = ChatFloaterDB.globalFadeEnabled
        end
        C_Timer.After(1, function()
            ApplyAllStates()
            for _, reg in ipairs(REGISTRY) do
                if reg.retry then
                    local retryCount = 0
                    C_Timer.NewTicker(0.5, function(t)
                        retryCount = retryCount + 1
                        ApplyState(reg)
                        if reg.getFrame() or retryCount >= 20 then t:Cancel() end
                    end)
                end
            end
        end)
    end

    EventUtil.ContinueOnAddOnLoaded(addonName, Initialize)

    local worldFrame = CreateFrame("Frame")
    worldFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    worldFrame:SetScript("OnEvent", function(_, _, isInitialLogin, isReloadingUI)
        if not isInitialLogin and not isReloadingUI then
            C_Timer.After(0.5, ApplyAllStates)
        end
    end)
end