--------------------------------------------------------------------------------
-- S-Frames: Chat takeover skin (Chat.lua)
-- Keeps Blizzard chat backend while replacing visuals and tab/filter workflow.
--------------------------------------------------------------------------------

SFrames.Chat = SFrames.Chat or {}

local DEFAULTS = {
    enable = true,
    showBorder = false,
    borderClassColor = false,
    showPlayerLevel = true,
    width = 360,
    height = 220,
    scale = 1,
    fontSize = 12,
    sidePadding = 10,
    topPadding = 30,
    bottomPadding = 8,
    bgAlpha = 0.45,
    activeTab = 1,
    editBoxPosition = "bottom",
    editBoxX = 0,
    editBoxY = 200,
}

local DEFAULT_COMBAT_TAB_NAME = COMBATLOG or COMBAT_LOG or "Combat"

local HIDDEN_OBJECTS = {
    "ChatFrameMenuButton",
    "ChatFrameChannelButton",
    "ChatFrameToggleVoiceMuteButton",
    "ChatFrameToggleVoiceDeafenButton",
    "ChatFrameToggleVoiceSelfMuteButton",
    "ChatFrameToggleVoiceSelfDeafenButton",
    "ChatFrameUpButton",
    "ChatFrameDownButton",
    "ChatFrameBottomButton",
}

local EDITBOX_TEXTURES = {
    "ChatFrameEditBoxLeft",
    "ChatFrameEditBoxMid",
    "ChatFrameEditBoxRight",
    "ChatFrameEditBoxFocusLeft",
    "ChatFrameEditBoxFocusMid",
    "ChatFrameEditBoxFocusRight",
    "ChatFrame1EditBoxLeft",
    "ChatFrame1EditBoxMid",
    "ChatFrame1EditBoxRight",
    "ChatFrame1EditBoxFocusLeft",
    "ChatFrame1EditBoxFocusMid",
    "ChatFrame1EditBoxFocusRight",
}

local FILTER_DEFS = {
    { key = "say",     label = "说话" },
    { key = "yell",    label = "大喊" },
    { key = "emote",   label = "动作" },
    { key = "guild",   label = "公会" },
    { key = "party",   label = "小队" },
    { key = "raid",    label = "团队" },
    { key = "whisper", label = "密语" },
    { key = "system",  label = "系统" },
    { key = "loot",    label = "拾取" },
    { key = "money",   label = "获益" },
}
local DEFAULT_FILTERS = {
    say = true,
    yell = true,
    emote = true,
    guild = true,
    party = true,
    raid = true,
    whisper = true,
    system = true,
    loot = true,
    money = true,
}

local AUTO_TRANSLATE_TARGET_LANG = "zh"

local TRANSLATE_FILTER_KEYS = {
    say = true,
    yell = true,
    emote = true,
    guild = true,
    party = true,
    raid = true,
    whisper = true,
}

local TRANSLATE_FILTER_ORDER = {
    "say",
    "yell",
    "emote",
    "guild",
    "party",
    "raid",
    "whisper",
}

local FILTER_GROUPS = {
    say = { "SAY" },
    yell = { "YELL" },
    emote = { "EMOTE", "TEXT_EMOTE", "MONSTER_EMOTE" },
    guild = { "GUILD", "OFFICER" },
    party = { "PARTY", "PARTY_LEADER" },
    raid = { "RAID", "RAID_LEADER", "RAID_WARNING", "BATTLEGROUND", "BATTLEGROUND_LEADER" },
    whisper = { "WHISPER", "WHISPER_INFORM", "REPLY", "AFK", "DND", "IGNORED", "MONSTER_WHISPER", "MONSTER_BOSS_WHISPER" },
    channel = { "CHANNEL", "CHANNEL_JOIN", "CHANNEL_LEAVE", "CHANNEL_LIST", "CHANNEL_NOTICE", "BG_HORDE", "BG_ALLIANCE" },
    system = { "SYSTEM", "MONSTER_SAY", "MONSTER_YELL", "MONSTER_BOSS_EMOTE" },
    loot = { "LOOT", "OPENING", "TRADESKILLS" },
    money = { "MONEY", "COMBAT_XP_GAIN", "COMBAT_HONOR_GAIN", "COMBAT_FACTION_CHANGE", "SKILL", "PET_INFO", "COMBAT_MISC_INFO" },
}

local ALL_MESSAGE_GROUPS = {
    "SYSTEM",
    "SAY",
    "YELL",
    "EMOTE",
    "TEXT_EMOTE",
    "MONSTER_SAY",
    "MONSTER_YELL",
    "MONSTER_EMOTE",
    "MONSTER_WHISPER",
    "MONSTER_BOSS_EMOTE",
    "MONSTER_BOSS_WHISPER",
    "WHISPER",
    "WHISPER_INFORM",
    "REPLY",
    "AFK",
    "DND",
    "IGNORED",
    "GUILD",
    "OFFICER",
    "PARTY",
    "PARTY_LEADER",
    "RAID",
    "RAID_LEADER",
    "RAID_WARNING",
    "BATTLEGROUND",
    "BATTLEGROUND_LEADER",
    "CHANNEL",
    "CHANNEL_JOIN",
    "CHANNEL_LEAVE",
    "CHANNEL_LIST",
    "CHANNEL_NOTICE",
    "BG_HORDE",
    "BG_ALLIANCE",
    "LOOT",
    "OPENING",
    "TRADESKILLS",
    "MONEY",
    "COMBAT_XP_GAIN",
    "COMBAT_HONOR_GAIN",
    "COMBAT_FACTION_CHANGE",
    "SKILL",
    "PET_INFO",
    "COMBAT_MISC_INFO",
}

local TRANSLATE_EVENT_FILTERS = {
    CHAT_MSG_SAY = "say",
    CHAT_MSG_YELL = "yell",
    CHAT_MSG_EMOTE = "emote",
    CHAT_MSG_TEXT_EMOTE = "emote",
    CHAT_MSG_GUILD = "guild",
    CHAT_MSG_OFFICER = "guild",
    CHAT_MSG_PARTY = "party",
    CHAT_MSG_PARTY_LEADER = "party",
    CHAT_MSG_RAID = "raid",
    CHAT_MSG_RAID_LEADER = "raid",
    CHAT_MSG_RAID_WARNING = "raid",
    CHAT_MSG_BATTLEGROUND = "raid",
    CHAT_MSG_BATTLEGROUND_LEADER = "raid",
    CHAT_MSG_WHISPER = "whisper",
    CHAT_MSG_REPLY = "whisper",
    CHAT_MSG_MONSTER_WHISPER = "whisper",
    CHAT_MSG_MONSTER_BOSS_WHISPER = "whisper",
    CHAT_MSG_CHANNEL = "channel",
}

local function Clamp(value, minValue, maxValue)
    value = tonumber(value) or minValue
    if value < minValue then return minValue end
    if value > maxValue then return maxValue end
    return value
end

-- ============================================================
-- 共享：玩家职业颜色工具（供 Chat / Roll 等模块复用）
-- ============================================================
SFrames.ClassColorHex = SFrames.ClassColorHex or {
    WARRIOR   = "c79c6e",
    PALADIN   = "f58cba",
    HUNTER    = "abd473",
    ROGUE     = "fff569",
    PRIEST    = "ffffff",
    SHAMAN    = "0070de",
    MAGE      = "69ccf0",
    WARLOCK   = "9482c9",
    DRUID     = "ff7d0a",
}

SFrames.PlayerClassColorCache = SFrames.PlayerClassColorCache or {}
SFrames.PlayerLevelCache = SFrames.PlayerLevelCache or {}

local function EnsureGlobalClassCache()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.classColorCache then SFramesGlobalDB.classColorCache = {} end
    return SFramesGlobalDB.classColorCache
end

local function EnsureGlobalLevelCache()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.levelCache then SFramesGlobalDB.levelCache = {} end
    return SFramesGlobalDB.levelCache
end

local function LoadPersistentClassCache()
    local persistent = EnsureGlobalClassCache()
    local runtime = SFrames.PlayerClassColorCache
    for name, hex in pairs(persistent) do
        if not runtime[name] then
            runtime[name] = hex
        end
    end
    local persistentLvl = EnsureGlobalLevelCache()
    local runtimeLvl = SFrames.PlayerLevelCache
    for name, lvl in pairs(persistentLvl) do
        if not runtimeLvl[name] then
            runtimeLvl[name] = lvl
        end
    end
end

local function PersistClassCache()
    local persistent = EnsureGlobalClassCache()
    for name, hex in pairs(SFrames.PlayerClassColorCache) do
        persistent[name] = hex
    end
    local persistentLvl = EnsureGlobalLevelCache()
    for name, lvl in pairs(SFrames.PlayerLevelCache) do
        persistentLvl[name] = lvl
    end
end

do
    local reverseClassMap = nil
    function SFrames:LocalizedClassToEN(localizedName)
        if not localizedName then return nil end
        if not reverseClassMap then
            reverseClassMap = {}
            local classTable = LOCALIZED_CLASS_NAMES_MALE or {}
            for en, loc in pairs(classTable) do
                reverseClassMap[loc] = en
                reverseClassMap[string.upper(loc)] = en
            end
            local classTableF = LOCALIZED_CLASS_NAMES_FEMALE or {}
            for en, loc in pairs(classTableF) do
                reverseClassMap[loc] = en
                reverseClassMap[string.upper(loc)] = en
            end
            local fallback = {
                ["战士"] = "WARRIOR", ["圣骑士"] = "PALADIN", ["猎人"] = "HUNTER",
                ["盗贼"] = "ROGUE", ["牧师"] = "PRIEST", ["萨满祭司"] = "SHAMAN",
                ["法师"] = "MAGE", ["术士"] = "WARLOCK", ["德鲁伊"] = "DRUID",
            }
            for loc, en in pairs(fallback) do
                if not reverseClassMap[loc] then reverseClassMap[loc] = en end
            end
        end
        return reverseClassMap[localizedName] or reverseClassMap[string.upper(localizedName)]
    end
end

function SFrames:RefreshClassColorCache()
    local now = GetTime()
    if self._lastClassCacheRefresh and (now - self._lastClassCacheRefresh) < 2 then
        return
    end
    self._lastClassCacheRefresh = now

    local cache = self.PlayerClassColorCache
    local lvlCache = self.PlayerLevelCache
    local hex   = self.ClassColorHex
    if UnitName and UnitClass then
        local selfName = UnitName("player")
        local _, selfClass = UnitClass("player")
        if selfName and selfClass and hex[selfClass] then
            cache[selfName] = hex[selfClass]
        end
        if selfName and UnitLevel then
            local selfLevel = UnitLevel("player")
            if selfLevel and selfLevel > 0 then
                lvlCache[selfName] = selfLevel
            end
        end
    end
    if GetNumRaidMembers then
        local count = GetNumRaidMembers()
        for i = 1, count do
            local rName, _, _, rLevel, _, rClass = GetRaidRosterInfo(i)
            if rName and rClass and hex[rClass] then
                cache[rName] = hex[rClass]
            end
            if rName and rLevel and rLevel > 0 then
                lvlCache[rName] = rLevel
            end
        end
    end
    if GetNumPartyMembers and UnitName and UnitClass then
        local count = GetNumPartyMembers()
        for i = 1, count do
            local unit = "party" .. i
            local pName = UnitName(unit)
            local _, pClass = UnitClass(unit)
            if pName and pClass and hex[pClass] then
                cache[pName] = hex[pClass]
            end
            if pName and UnitLevel then
                local pLevel = UnitLevel(unit)
                if pLevel and pLevel > 0 then
                    lvlCache[pName] = pLevel
                end
            end
        end
    end
    if GetNumGuildMembers then
        local count = GetNumGuildMembers()
        for i = 1, count do
            local gName, _, _, gLevel, gClassLoc, _, _, _, _, _, classEN = GetGuildRosterInfo(i)
            if gName then
                if not classEN or classEN == "" then
                    classEN = self:LocalizedClassToEN(gClassLoc)
                end
                if classEN and hex[string.upper(classEN)] then
                    cache[gName] = hex[string.upper(classEN)]
                end
                if gLevel and gLevel > 0 then
                    lvlCache[gName] = gLevel
                end
            end
        end
    end
    if GetNumFriends then
        local count = GetNumFriends()
        for i = 1, count do
            local fName, fLevel, fClass, fArea, fConnected = GetFriendInfo(i)
            if fName and fClass then
                local classEN = self:LocalizedClassToEN(fClass)
                if classEN and hex[classEN] then
                    cache[fName] = hex[classEN]
                end
            end
            if fName and fLevel and fLevel > 0 then
                lvlCache[fName] = fLevel
            end
        end
    end
    if GetNumWhoResults then
        local count = GetNumWhoResults()
        for i = 1, count do
            local wName, wGuild, wLevel, wRace, wClassEN = GetWhoInfo(i)
            if wName and wClassEN and wClassEN ~= "" and hex[string.upper(wClassEN)] then
                cache[wName] = hex[string.upper(wClassEN)]
            end
            if wName and wLevel and wLevel > 0 then
                lvlCache[wName] = wLevel
            end
        end
    end
    PersistClassCache()
end

function SFrames:GetClassHexForName(name)
    if not name or name == "" then return nil end
    local cache = self.PlayerClassColorCache
    if cache[name] then return cache[name] end
    self:RefreshClassColorCache()
    return cache[name]
end

function SFrames:GetLevelForName(name)
    if not name or name == "" then return nil end
    local cache = self.PlayerLevelCache
    if cache[name] then return cache[name] end
    return nil
end

-- 本地别名，供 Chat.lua 内部使用
local function GetClassHexForName(name)
    return SFrames:GetClassHexForName(name)
end

local function GetLevelForName(name)
    return SFrames:GetLevelForName(name)
end

-- 将聊天文本中所有 |Hplayer:NAME|h[NAME]|h 替换为职业颜色版本，并可选附加等级
local function ColorPlayerNamesInText(text)
    if not text or text == "" then return text end
    local showLevel = SFramesDB and SFramesDB.Chat and SFramesDB.Chat.showPlayerLevel ~= false
    local result = string.gsub(text, "(|Hplayer:([^|]+)|h%[([^%]]+)%]|h)", function(full, linkName, displayName)
        local baseName = string.gsub(linkName, "%-.*", "")
        local hex = GetClassHexForName(baseName)
        local level = showLevel and GetLevelForName(baseName) or nil
        local levelSuffix = ""
        if level then
            levelSuffix = "|cff888888:" .. level .. "|r"
        end
        if hex then
            return "|Hplayer:" .. linkName .. "|h|cff" .. hex .. "[" .. displayName .. "]|r|h" .. levelSuffix
        end
        if level then
            return full .. levelSuffix
        end
        return full
    end)
    return result
end

local function Trim(text)
    text = tostring(text or "")
    text = string.gsub(text, "^%s+", "")
    text = string.gsub(text, "%s+$", "")
    return text
end

local function Dummy() end

local function CopyTable(src)
    local out = {}
    for k, v in pairs(src) do
        if type(v) == "table" then
            out[k] = CopyTable(v)
        else
            out[k] = v
        end
    end
    return out
end

local function BuildDefaultTranslateFilters()
    local out = {}
    for key in pairs(TRANSLATE_FILTER_KEYS) do
        out[key] = false
    end
    return out
end

local function ShortText(text, maxLen)
    text = tostring(text or "")
    local n = maxLen or 10
    if string.len(text) <= n then return text end
    return string.sub(text, 1, n - 1) .. "."
end

local function GetFilterLabel(key)
    for i = 1, table.getn(FILTER_DEFS) do
        local def = FILTER_DEFS[i]
        if def and def.key == key then
            return def.label or key
        end
    end
    return key
end

local function NormalizeChannelName(name)
    local clean = Trim(name)
    if clean == "" then return "" end
    clean = string.gsub(clean, "|Hchannel:[^|]+|h%[([^%]]+)%]|h", "%1")
    clean = string.gsub(clean, "^%[", "")
    clean = string.gsub(clean, "%]$", "")
    clean = string.gsub(clean, "^%d+%s*%.%s*", "")
    clean = string.gsub(clean, "^%d+%s*:%s*", "")
    clean = Trim(clean)
    return clean
end

local function ChannelKey(name)
    local clean = NormalizeChannelName(name)
    if clean == "" then return "" end
    return string.lower(clean)
end

-- Groups of channel aliases that should be treated as the same logical channel.
-- If a user enables any one key in a group, all keys in that group are considered enabled.
local CHANNEL_ALIAS_GROUPS = {
    { "hc", "hardcore", "hard core", "hard-core", "h", "硬核" },
    { "lfg", "lft", "lookingforgroup", "looking for group", "group" },
}

-- Build a reverse lookup: channel key -> alias group index
local CHANNEL_ALIAS_GROUP_INDEX = {}
for gIdx, group in ipairs(CHANNEL_ALIAS_GROUPS) do
    for _, alias in ipairs(group) do
        CHANNEL_ALIAS_GROUP_INDEX[alias] = gIdx
    end
end

-- Returns all alias keys for the group that `name` belongs to (or nil if not in any group).
local function GetChannelAliasKeys(name)
    local key = ChannelKey(name)
    if key == "" then return nil end
    -- Check exact match first
    local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
    if not gIdx then
        -- Check substring match for each alias in each group.
        -- Only use aliases 3+ chars long to avoid false positives (e.g. "h" matching "whisper").
        for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
            for _, alias in ipairs(group) do
                if string.len(alias) >= 3 and string.find(key, alias, 1, true) then
                    gIdx = i
                    break
                end
            end
            if gIdx then break end
        end
    end
    if not gIdx then return nil end
    return CHANNEL_ALIAS_GROUPS[gIdx]
end

local function IsIgnoredChannelByDefault(name)
    local key = ChannelKey(name)
    if key == "" then return false end
    -- Check via alias groups
    local aliases = GetChannelAliasKeys(name)
    if aliases then return true end
    return false
end

-- Channels discovered at runtime from actual chat messages / join events.
-- Keys are normalized names, values are true.
local discoveredChannels = {}

local function TrackDiscoveredChannel(name)
    if type(name) ~= "string" or name == "" then return end
    local clean = NormalizeChannelName(name)
    if clean ~= "" then
        discoveredChannels[clean] = true
    end
end

local function UntrackDiscoveredChannel(name)
    if type(name) ~= "string" or name == "" then return end
    local clean = NormalizeChannelName(name)
    if clean ~= "" then
        discoveredChannels[clean] = nil
    end
end

local function GetJoinedChannels()
    local out = {}
    if not GetChannelList then return out end

    local raw = { GetChannelList() }
    local i = 1
    local seen = {}
    while i <= table.getn(raw) do
        local id = raw[i]
        local name = raw[i + 1]
        if type(id) == "number" and type(name) == "string" and Trim(name) ~= "" then
            table.insert(out, { id = id, name = name })
            local key = ChannelKey(name)
            if key ~= "" then
                seen[key] = true
            end
        end
        i = i + 3
    end

    table.sort(out, function(a, b)
        if a.id == b.id then
            return string.lower(a.name) < string.lower(b.name)
        end
        return a.id < b.id
    end)
    
    -- Add one representative per alias group that isn't already present,
    -- plus individual channels that are not aliases.
    -- For alias groups (like hc/hardcore/硬核), only add ONE representative
    -- so we don't create duplicate conflicting entries.
    local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world" }
    local seenAliasGroups = {}
    for _, cname in ipairs(customChannels) do
        local key = ChannelKey(cname)
        if key == "" then
            -- skip
        elseif seen[key] then
            -- already in the joined list
            local aliases = GetChannelAliasKeys(cname)
            if aliases then
                -- Mark the whole group as seen so no other alias gets added
                local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
                if gIdx then seenAliasGroups[gIdx] = true end
            end
        else
            local aliases = GetChannelAliasKeys(cname)
            if aliases then
                -- This is an alias channel - check if any alias is already seen/added
                local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
                -- Also check substring aliases
                if not gIdx then
                    for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
                        for _, a in ipairs(group) do
                            if string.find(key, a, 1, true) then
                                gIdx = i
                                break
                            end
                        end
                        if gIdx then break end
                    end
                end
                local alreadyInJoined = false
                if gIdx then
                    for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do
                        if seen[a] then
                            alreadyInJoined = true
                            break
                        end
                    end
                end
                if not alreadyInJoined and (not gIdx or not seenAliasGroups[gIdx]) then
                    table.insert(out, { id = 99, name = cname })
                    seen[key] = true
                    if gIdx then seenAliasGroups[gIdx] = true end
                end
            else
                table.insert(out, { id = 99, name = cname })
                seen[key] = true
            end
        end
    end

    -- Include dynamically discovered channels (from actual chat messages).
    -- Verify each with GetChannelName() to confirm the player is still in it.
    for dName, _ in pairs(discoveredChannels) do
        local key = ChannelKey(dName)
        if key ~= "" and not seen[key] then
            local aliasKeys = GetChannelAliasKeys(dName)
            local aliasAlreadySeen = false
            if aliasKeys then
                for _, a in ipairs(aliasKeys) do
                    if seen[a] then aliasAlreadySeen = true; break end
                end
            end
            if not aliasAlreadySeen then
                if GetChannelName then
                    local chId, chRealName = GetChannelName(dName)
                    if type(chId) == "number" and chId > 0 then
                        table.insert(out, { id = chId, name = chRealName or dName })
                        seen[key] = true
                    end
                else
                    table.insert(out, { id = 99, name = dName })
                    seen[key] = true
                end
            end
        end
    end

    return out
end

local function MatchJoinedChannelName(rawName)
    local key = ChannelKey(rawName)
    if key == "" then return "" end

    local channels = GetJoinedChannels()

    -- First: exact key match
    for i = 1, table.getn(channels) do
        local name = channels[i] and channels[i].name
        if ChannelKey(name) == key then
            return name
        end
    end

    -- Second: alias-group match (e.g. "Hardcore" -> joined "hc")
    local aliases = GetChannelAliasKeys(rawName)
    if aliases then
        for _, alias in ipairs(aliases) do
            local aliasKey = ChannelKey(alias)
            if aliasKey ~= "" then
                for i = 1, table.getn(channels) do
                    local name = channels[i] and channels[i].name
                    if ChannelKey(name) == aliasKey then
                        return name
                    end
                end
            end
        end
    end

    return NormalizeChannelName(rawName)
end

local function GetChannelNameFromMessageEvent(channelString, channelBaseName, channelName, fallback)
    local candidates = {
        channelName,
        channelBaseName,
        channelString,
        arg9,
        arg8,
        arg4,
        fallback,
    }

    for i = 1, table.getn(candidates) do
        local candidate = candidates[i]
        if type(candidate) == "string" and candidate ~= "" then
            local matched = MatchJoinedChannelName(candidate)
            if matched ~= "" then
                return matched
            end
        end
    end

    for i = 1, table.getn(candidates) do
        local candidate = candidates[i]
        if type(candidate) == "string" and candidate ~= "" then
            local normalized = NormalizeChannelName(candidate)
            if normalized ~= "" then
                return normalized
            end
        end
    end

    return ""
end

local function GetChannelNameFromChatLine(text)
    if type(text) ~= "string" or text == "" then return nil end

    local _, _, label = string.find(text, "|Hchannel:[^|]+|h%[([^%]]+)%]|h")
    if not label then
        if string.byte(text, 1) ~= 124 and string.byte(text, 1) ~= 91 then
            return nil
        end
        local raw = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
        raw = string.gsub(raw, "|r", "")
        raw = string.gsub(raw, "^%s+", "")
        _, _, label = string.find(raw, "^%[([^%]]+)%]")
    end
    if not label or label == "" then return nil end

    label = string.gsub(label, "^%d+%s*%.%s*", "")
    return label
end

local function GetTranslateFilterKeyForEvent(event)
    return TRANSLATE_EVENT_FILTERS[event]
end

local function ParseHardcoreDeathMessage(text)
    if type(text) ~= "string" or text == "" then return nil end
    local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
    clean = string.gsub(clean, "|r", "")
    
    -- Check for Hardcore death signatures
    if string.find(string.lower(clean), "hc news") or string.find(clean, "硬核") or string.find(clean, "死亡") or string.find(string.lower(clean), "has fallen") or string.find(string.lower(clean), "died") or string.find(string.lower(clean), "slain") then
        -- Turtle English "Level 14"
        local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
        if lvlStr then return tonumber(lvlStr) end
        
        -- Chinese "14级"
        local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
        if lvlStr2 then return tonumber(lvlStr2) end
        
        -- Fallback
        local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
        if lvlStr3 then return tonumber(lvlStr3) end
        
        -- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter.
        if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then
            return 1
        end
    end
    return nil
end

local function CleanTextForTranslation(text)
    if type(text) ~= "string" then return "" end
    local clean = text
    clean = string.gsub(clean, "|c%x%x%x%x%x%x%x%x", "")
    clean = string.gsub(clean, "|r", "")
    clean = string.gsub(clean, "|H.-|h(.-)|h", "%1")
    clean = string.gsub(clean, "^%s+", "")
    clean = string.gsub(clean, "%s+$", "")
    return clean
end

local function ForceHide(object)
    if not object then return end
    object:Hide()
    if object.SetAlpha then object:SetAlpha(0) end
    if object.EnableMouse then object:EnableMouse(false) end
    object.Show = Dummy
end

local function ForceInvisible(object)
    if not object then return end
    object:Hide()
    if object.SetAlpha then object:SetAlpha(0) end
    if object.EnableMouse then object:EnableMouse(false) end
end

local function CreateFont(parent, size, justify)
    if SFrames and SFrames.CreateFontString then
        return SFrames:CreateFontString(parent, size, justify)
    end

    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont("Fonts\\ARIALN.TTF", size or 11, "OUTLINE")
    fs:SetJustifyH(justify or "LEFT")
    fs:SetTextColor(1, 1, 1)
    return fs
end

local configWidgetId = 0

local function NextConfigWidget(prefix)
    configWidgetId = configWidgetId + 1
    return "SFramesChatCfg" .. prefix .. tostring(configWidgetId)
end

local CFG_THEME = SFrames.ActiveTheme

local function EnsureCfgBackdrop(frame)
    if not frame then return end
    if frame.sfCfgBackdrop then return end
    if SFrames and SFrames.CreateBackdrop then
        SFrames:CreateBackdrop(frame)
    elseif frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end
    frame.sfCfgBackdrop = true
end

local function HideCfgTexture(tex)
    if not tex then return end
    if tex.SetTexture then tex:SetTexture(nil) end
    tex:Hide()
end

local function StyleCfgButton(btn)
    if not btn or btn.sfCfgStyled then return btn end
    btn.sfCfgStyled = true

    btn:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    HideCfgTexture(btn.GetNormalTexture and btn:GetNormalTexture())
    HideCfgTexture(btn.GetPushedTexture and btn:GetPushedTexture())
    HideCfgTexture(btn.GetHighlightTexture and btn:GetHighlightTexture())
    HideCfgTexture(btn.GetDisabledTexture and btn:GetDisabledTexture())

    local name = btn:GetName() or ""
    for _, suffix in ipairs({ "Left", "Right", "Middle" }) do
        local tex = _G[name .. suffix]
        if tex then tex:SetAlpha(0) tex:Hide() end
    end

    local function SetVisual(state)
        local bg = CFG_THEME.buttonBg
        local border = CFG_THEME.buttonBorder
        local text = CFG_THEME.buttonText
        if state == "hover" then
            bg = CFG_THEME.buttonHoverBg
            border = { CFG_THEME.buttonBorder[1] * 1.5, CFG_THEME.buttonBorder[2] * 1.5, CFG_THEME.buttonBorder[3] * 1.5, 0.98 }
            text = { 1, 0.92, 0.96 }
        elseif state == "down" then
            bg = CFG_THEME.buttonDownBg
        elseif state == "disabled" then
            bg = CFG_THEME.buttonDisabledBg
            text = CFG_THEME.buttonDisabledText
        end
        if btn.SetBackdropColor then
            btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
        end
        if btn.SetBackdropBorderColor then
            btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
        end
        local fs = btn.GetFontString and btn:GetFontString()
        if fs then
            fs:SetTextColor(text[1], text[2], text[3])
        end
    end

    btn.RefreshVisual = function()
        if btn.IsEnabled and not btn:IsEnabled() then
            SetVisual("disabled")
        else
            SetVisual("normal")
        end
    end

    local oldEnter = btn:GetScript("OnEnter")
    local oldLeave = btn:GetScript("OnLeave")
    local oldDown = btn:GetScript("OnMouseDown")
    local oldUp = btn:GetScript("OnMouseUp")

    btn:SetScript("OnEnter", function()
        if oldEnter then oldEnter() end
        if this.IsEnabled and this:IsEnabled() then
            SetVisual("hover")
        end
    end)
    btn:SetScript("OnLeave", function()
        if oldLeave then oldLeave() end
        if this.IsEnabled and this:IsEnabled() then
            SetVisual("normal")
        end
    end)
    btn:SetScript("OnMouseDown", function()
        if oldDown then oldDown() end
        if this.IsEnabled and this:IsEnabled() then
            SetVisual("down")
        end
    end)
    btn:SetScript("OnMouseUp", function()
        if oldUp then oldUp() end
        if this.IsEnabled and this:IsEnabled() then
            SetVisual("hover")
        end
    end)

    btn:RefreshVisual()
    return btn
end

local function StyleCfgCheck(cb)
    if not cb or cb.sfCfgStyled then return cb end
    cb.sfCfgStyled = true

    local box = CreateFrame("Frame", nil, cb)
    box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2)
    box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2)
    local boxLevel = (cb:GetFrameLevel() or 1) - 1
    if boxLevel < 0 then boxLevel = 0 end
    box:SetFrameLevel(boxLevel)
    EnsureCfgBackdrop(box)
    if box.SetBackdropColor then
        box:SetBackdropColor(CFG_THEME.checkBg[1], CFG_THEME.checkBg[2], CFG_THEME.checkBg[3], CFG_THEME.checkBg[4])
    end
    if box.SetBackdropBorderColor then
        box:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4])
    end
    cb.sfCfgBox = box

    HideCfgTexture(cb.GetNormalTexture and cb:GetNormalTexture())
    HideCfgTexture(cb.GetPushedTexture and cb:GetPushedTexture())
    HideCfgTexture(cb.GetHighlightTexture and cb:GetHighlightTexture())
    HideCfgTexture(cb.GetDisabledTexture and cb:GetDisabledTexture())

    if cb.SetCheckedTexture then
        cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8")
    end
    local checked = cb.GetCheckedTexture and cb:GetCheckedTexture()
    if checked then
        checked:ClearAllPoints()
        checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
        checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
        if checked.SetDrawLayer then
            checked:SetDrawLayer("OVERLAY", 7)
        end
        if checked.SetAlpha then
            checked:SetAlpha(1)
        end
        checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4])
    end

    if cb.SetDisabledCheckedTexture then
        cb:SetDisabledCheckedTexture("Interface\\Buttons\\WHITE8X8")
    end
    local disChecked = cb.GetDisabledCheckedTexture and cb:GetDisabledCheckedTexture()
    if disChecked then
        disChecked:ClearAllPoints()
        disChecked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
        disChecked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
        if disChecked.SetDrawLayer then
            disChecked:SetDrawLayer("OVERLAY", 6)
        end
        disChecked:SetVertexColor(0.5, 0.5, 0.55, 0.8)
    end

    local label = _G[cb:GetName() .. "Text"]
    if label then
        label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
    end

    local oldEnter = cb:GetScript("OnEnter")
    local oldLeave = cb:GetScript("OnLeave")
    cb:SetScript("OnEnter", function()
        if oldEnter then oldEnter() end
        if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then
            this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkHoverBorder[1], CFG_THEME.checkHoverBorder[2], CFG_THEME.checkHoverBorder[3], CFG_THEME.checkHoverBorder[4])
        end
    end)
    cb:SetScript("OnLeave", function()
        if oldLeave then oldLeave() end
        if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then
            this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4])
        end
    end)
    return cb
end

local function StyleCfgSlider(slider, low, high, text)
    if not slider or slider.sfCfgStyled then return slider end
    slider.sfCfgStyled = true

    local regions = { slider:GetRegions() }
    for i = 1, table.getn(regions) do
        local region = regions[i]
        if region and region.GetObjectType and region:GetObjectType() == "Texture" then
            region:SetTexture(nil)
        end
    end

    local track = slider:CreateTexture(nil, "BACKGROUND")
    track:SetTexture("Interface\\Buttons\\WHITE8X8")
    track:SetPoint("LEFT",  slider, "LEFT",  0, 0)
    track:SetPoint("RIGHT", slider, "RIGHT", 0, 0)
    track:SetHeight(4)
    track:SetVertexColor(CFG_THEME.sliderTrack[1], CFG_THEME.sliderTrack[2], CFG_THEME.sliderTrack[3], CFG_THEME.sliderTrack[4])
    slider.sfTrack = track

    local fill = slider:CreateTexture(nil, "ARTWORK")
    fill:SetTexture("Interface\\Buttons\\WHITE8X8")
    fill:SetPoint("LEFT", track, "LEFT", 0, 0)
    fill:SetPoint("TOP", track, "TOP", 0, 0)
    fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0)
    fill:SetWidth(1)
    fill:SetVertexColor(CFG_THEME.sliderFill[1], CFG_THEME.sliderFill[2], CFG_THEME.sliderFill[3], CFG_THEME.sliderFill[4])
    slider.sfFill = fill

    if slider.SetThumbTexture then
        slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
    end
    local thumb = slider.GetThumbTexture and slider:GetThumbTexture()
    if thumb then
        thumb:SetWidth(8)
        thumb:SetHeight(16)
        thumb:SetVertexColor(CFG_THEME.sliderThumb[1], CFG_THEME.sliderThumb[2], CFG_THEME.sliderThumb[3], CFG_THEME.sliderThumb[4])
    end

    local function UpdateFill()
        local minValue, maxValue = slider:GetMinMaxValues()
        local value = slider:GetValue() or minValue
        local pct = 0
        if maxValue > minValue then
            pct = (value - minValue) / (maxValue - minValue)
        end
        pct = Clamp(pct, 0, 1)
        local width = math.floor((slider:GetWidth() or 1) * pct + 0.5)
        if width < 1 then width = 1 end
        slider.sfFill:SetWidth(width)
    end

    local oldChanged = slider:GetScript("OnValueChanged")
    slider:SetScript("OnValueChanged", function()
        if oldChanged then oldChanged() end
        UpdateFill()
    end)
    UpdateFill()

    if low then
        low:SetTextColor(0.74, 0.72, 0.8)
        low:ClearAllPoints()
        low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, 0)
    end
    if high then
        high:SetTextColor(0.74, 0.72, 0.8)
        high:ClearAllPoints()
        high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, 0)
    end
    if text then
        text:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
        text:ClearAllPoints()
        text:SetPoint("BOTTOM", slider, "TOP", 0, 2)
    end
    return slider
end

local function StyleCfgEditBox(eb)
    if not eb or eb.sfCfgStyled then return eb end
    eb.sfCfgStyled = true

    local name = eb:GetName()
    if name and name ~= "" then
        HideCfgTexture(_G[name .. "Left"])
        HideCfgTexture(_G[name .. "Middle"])
        HideCfgTexture(_G[name .. "Right"])
    end

    local bg = CreateFrame("Frame", nil, eb)
    bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3)
    bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3)
    bg:SetFrameLevel((eb:GetFrameLevel() or 1) - 1)
    EnsureCfgBackdrop(bg)
    if bg.SetBackdropColor then
        bg:SetBackdropColor(0.13, 0.14, 0.17, 0.94)
    end
    if bg.SetBackdropBorderColor then
        bg:SetBackdropBorderColor(0.52, 0.5, 0.58, 0.88)
    end
    eb.sfCfgBg = bg
    if eb.SetTextInsets then
        eb:SetTextInsets(4, 4, 0, 0)
    end
    if eb.SetTextColor then
        eb:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
    end
    return eb
end

local function CreateCfgSection(parent, title, x, y, width, height, font)
    local sec = CreateFrame("Frame", NextConfigWidget("Section"), parent)
    sec:SetWidth(width)
    sec:SetHeight(height)
    sec:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    EnsureCfgBackdrop(sec)
    if sec.SetBackdropColor then
        sec:SetBackdropColor(CFG_THEME.sectionBg[1], CFG_THEME.sectionBg[2], CFG_THEME.sectionBg[3], CFG_THEME.sectionBg[4])
    end
    if sec.SetBackdropBorderColor then
        sec:SetBackdropBorderColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], CFG_THEME.sectionBorder[4])
    end

    local header = sec:CreateFontString(nil, "OVERLAY")
    header:SetFont(font or "Fonts\\ARIALN.TTF", 11, "OUTLINE")
    header:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -8)
    header:SetText(title or "")
    header:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

    local div = sec:CreateTexture(nil, "ARTWORK")
    div:SetTexture("Interface\\Buttons\\WHITE8X8")
    div:SetHeight(1)
    div:SetPoint("TOPLEFT",  sec, "TOPLEFT",  8, -24)
    div:SetPoint("TOPRIGHT", sec, "TOPRIGHT", -8, -24)
    div:SetVertexColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], 0.6)

    return sec
end

local function CreateCfgButton(parent, text, x, y, width, height, onClick)
    local btn = CreateFrame("Button", NextConfigWidget("Button"), parent, "UIPanelButtonTemplate")
    btn:SetWidth(width)
    btn:SetHeight(height)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    btn:SetText(text or "")
    btn:SetScript("OnClick", onClick)
    StyleCfgButton(btn)
    return btn
end

local function AddBtnIcon(btn, iconKey, size, align)
    if not (SFrames and SFrames.CreateIcon) then return end
    local sz = size or 12
    local gap = 3
    local ico = SFrames:CreateIcon(btn, iconKey, sz)
    ico:SetDrawLayer("OVERLAY")
    btn.nanamiIcon = ico
    local fs = btn:GetFontString()
    if fs then
        fs:ClearAllPoints()
        if align == "left" then
            ico:SetPoint("LEFT", btn, "LEFT", 8, 0)
            fs:SetPoint("LEFT", ico, "RIGHT", gap, 0)
            fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
            fs:SetJustifyH("LEFT")
        else
            fs:SetPoint("CENTER", btn, "CENTER", (sz + gap) / 2, 0)
            ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0)
        end
    else
        ico:SetPoint("CENTER", btn, "CENTER", 0, 0)
    end
end

local function CreateCfgCheck(parent, text, x, y, getter, setter, onChanged)
    local cb = CreateFrame("CheckButton", NextConfigWidget("Check"), parent, "UICheckButtonTemplate")
    cb:SetWidth(20)
    cb:SetHeight(20)
    cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    StyleCfgCheck(cb)
    local label = _G[cb:GetName() .. "Text"]
    if label then
        label:SetText(text or "")
        label:SetJustifyH("LEFT")
    end

    local internal = false
    cb:SetScript("OnClick", function()
        if internal then return end
        local checked = (this:GetChecked() and true or false)
        if setter then setter(checked) end
        if onChanged then onChanged(checked) end
    end)

    cb.Refresh = function()
        internal = true
        cb:SetChecked(getter and getter() and true or false)
        internal = false
    end

    cb:Refresh()
    return cb
end

local function CreateCfgSlider(parent, labelText, x, y, width, minValue, maxValue, step, getter, setter, formatter, onChanged)
    local sliderName = NextConfigWidget("Slider")
    local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
    slider:SetWidth(width)
    slider:SetHeight(26)
    slider:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    slider:SetMinMaxValues(minValue, maxValue)
    slider:SetValueStep(step)
    if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end

    local low = _G[sliderName .. "Low"]
    local high = _G[sliderName .. "High"]
    local text = _G[sliderName .. "Text"]
    if low then low:SetText(tostring(minValue)) end
    if high then high:SetText(tostring(maxValue)) end

    local internal = false
    local function UpdateLabel(value)
        local display = formatter and formatter(value) or value
        if text then text:SetText((labelText or "") .. ": " .. tostring(display)) end
    end

    slider:SetScript("OnValueChanged", function()
        if internal then return end
        local raw = this:GetValue() or minValue or 0
        local safeStep = step or 1
        local value
        if safeStep >= 1 then
            value = math.floor(raw + 0.5)
        else
            if safeStep == 0 then safeStep = 1 end
            value = math.floor(raw / safeStep + 0.5) * safeStep
        end
        value = Clamp(value, minValue, maxValue)
        if setter then setter(value) end
        UpdateLabel(value)
        if onChanged then onChanged(value) end
    end)

    slider.Refresh = function()
        local value = tonumber(getter and getter() or minValue) or minValue
        value = Clamp(value, minValue, maxValue)
        internal = true
        slider:SetValue(value)
        internal = false
        UpdateLabel(value)
    end

    StyleCfgSlider(slider, low, high, text)
    slider:Refresh()
    return slider
end

local function CreateCfgEditBox(parent, x, y, width, height, getter, setter)
    local eb = CreateFrame("EditBox", NextConfigWidget("Edit"), parent, "InputBoxTemplate")
    eb:SetWidth(width)
    eb:SetHeight(height)
    eb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    eb:SetAutoFocus(false)
    StyleCfgEditBox(eb)
    eb:SetScript("OnEnterPressed", function()
        if setter then setter(this:GetText() or "") end
        this:ClearFocus()
    end)
    eb:SetScript("OnEscapePressed", function()
        this:ClearFocus()
        if getter then this:SetText(getter() or "") end
    end)
    eb.Refresh = function()
        if getter then eb:SetText(getter() or "") end
    end
    eb:Refresh()
    return eb
end

local function BuildDefaultTab(id, name)
    return {
        id = id,
        name = name or ("标签" .. tostring(id)),
        filters = CopyTable(DEFAULT_FILTERS),
        channelFilters = {},
        translateFilters = BuildDefaultTranslateFilters(),
        channelTranslateFilters = {},
    }
end

local function BuildCombatTab(id, name)
    local tab = BuildDefaultTab(id, name or DEFAULT_COMBAT_TAB_NAME)
    tab.kind = "combat"
    tab.filters.say = false
    tab.filters.yell = false
    tab.filters.emote = false
    tab.filters.guild = false
    tab.filters.party = false
    tab.filters.raid = false
    tab.filters.whisper = false
    tab.filters.channel = false
    tab.filters.system = true
    tab.filters.loot = true
    tab.filters.money = true
    return tab
end

local function IsCombatTab(tab)
    if type(tab) ~= "table" then return false end
    if tab.kind == "combat" then return true end

    local nameKey = ChannelKey(tab.name)
    if nameKey == "combat" then return true end
    if nameKey == "combat log" then return true end

    local localizedKey = ChannelKey(DEFAULT_COMBAT_TAB_NAME)
    if localizedKey ~= "" and nameKey == localizedKey then return true end
    return false
end

local function SanitizeTab(tab, fallbackId, fallbackName)
    if type(tab) ~= "table" then tab = {} end
    if type(tab.id) ~= "number" then tab.id = fallbackId or 1 end
    if type(tab.kind) ~= "string" then tab.kind = nil end

    if type(tab.name) ~= "string" or tab.name == "" then
        tab.name = fallbackName or ("标签" .. tostring(tab.id))
    else
        tab.name = Trim(tab.name)
        if tab.name == "" then
            tab.name = fallbackName or ("标签" .. tostring(tab.id))
        end
    end

    if type(tab.filters) ~= "table" then tab.filters = {} end
    for key, defaultValue in pairs(DEFAULT_FILTERS) do
        if tab.filters[key] == nil then
            tab.filters[key] = defaultValue
        else
            tab.filters[key] = (tab.filters[key] == true)
        end
    end

    if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
    -- Pass 1: normalize all keys and collect alias group values
    local cfGroupValues = {} -- gIdx -> value (last-write wins: true beats false)
    for key, value in pairs(tab.channelFilters) do
        if type(key) ~= "string" then
            tab.channelFilters[key] = nil
        else
            local normalized = ChannelKey(key)
            if normalized == "" then
                tab.channelFilters[key] = nil
            else
                local aliases = GetChannelAliasKeys(normalized)
                if aliases then
                    -- Find this key's group index
                    local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized]
                    if not gIdx then
                        for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do
                            for _, a in ipairs(group) do
                                if string.find(normalized, a, 1, true) then gIdx = gi break end
                            end
                            if gIdx then break end
                        end
                    end
                    if gIdx then
                        local enabled = (value == true)
                        -- true overrides false (if any alias is enabled, keep enabled)
                        if cfGroupValues[gIdx] == nil or enabled then
                            cfGroupValues[gIdx] = enabled
                        end
                    end
                    tab.channelFilters[key] = nil
                else
                    local enabled = (value == true)
                    if normalized ~= key then
                        tab.channelFilters[key] = nil
                        tab.channelFilters[normalized] = enabled
                    else
                        tab.channelFilters[key] = enabled
                    end
                end
            end
        end
    end
    -- Pass 2: write canonical key for each alias group that has a saved value
    for gi, enabled in pairs(cfGroupValues) do
        local group = CHANNEL_ALIAS_GROUPS[gi]
        if group then
            local canonicalKey = ChannelKey(group[1] or "")
            if canonicalKey ~= "" then
                tab.channelFilters[canonicalKey] = enabled
            end
        end
    end

    if type(tab.translateFilters) ~= "table" then tab.translateFilters = {} end
    for key in pairs(TRANSLATE_FILTER_KEYS) do
        tab.translateFilters[key] = (tab.translateFilters[key] == true)
    end
    for key, value in pairs(tab.translateFilters) do
        if not TRANSLATE_FILTER_KEYS[key] then
            tab.translateFilters[key] = nil
        else
            tab.translateFilters[key] = (value == true)
        end
    end

    if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
    local ctfGroupValues = {}
    for key, value in pairs(tab.channelTranslateFilters) do
        if type(key) ~= "string" then
            tab.channelTranslateFilters[key] = nil
        else
            local normalized = ChannelKey(key)
            if normalized == "" then
                tab.channelTranslateFilters[key] = nil
            else
                local aliases = GetChannelAliasKeys(normalized)
                if aliases then
                    local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized]
                    if not gIdx then
                        for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do
                            for _, a in ipairs(group) do
                                if string.find(normalized, a, 1, true) then gIdx = gi break end
                            end
                            if gIdx then break end
                        end
                    end
                    if gIdx then
                        local enabled = (value == true)
                        if ctfGroupValues[gIdx] == nil or enabled then
                            ctfGroupValues[gIdx] = enabled
                        end
                    end
                    tab.channelTranslateFilters[key] = nil
                else
                    local enabled = (value == true)
                    if normalized ~= key then
                        tab.channelTranslateFilters[key] = nil
                        tab.channelTranslateFilters[normalized] = enabled
                    else
                        tab.channelTranslateFilters[key] = enabled
                    end
                end
            end
        end
    end
    for gi, enabled in pairs(ctfGroupValues) do
        local group = CHANNEL_ALIAS_GROUPS[gi]
        if group then
            local canonicalKey = ChannelKey(group[1] or "")
            if canonicalKey ~= "" then
                tab.channelTranslateFilters[canonicalKey] = enabled
            end
        end
    end
    return tab
end

local function EnsureProtectedTabs(db, maxId)
    local tabs = db.tabs
    local generalIdx = nil
    local combatIdx = nil

    for i = 1, table.getn(tabs) do
        if IsCombatTab(tabs[i]) then
            if not combatIdx then combatIdx = i end
        else
            if not generalIdx then generalIdx = i end
        end
    end

    local function AllocTabId()
        local id = db.nextTabId
        if type(id) ~= "number" or id <= maxId then
            id = maxId + 1
        end
        db.nextTabId = id + 1
        maxId = id
        return id
    end

    if not generalIdx then
        local tab = BuildDefaultTab(AllocTabId(), GENERAL or "General")
        tab.kind = "general"
        table.insert(tabs, 1, tab)
        generalIdx = 1
        if combatIdx then combatIdx = combatIdx + 1 end
    end

    if not combatIdx then
        local tab = BuildCombatTab(AllocTabId(), DEFAULT_COMBAT_TAB_NAME)
        table.insert(tabs, tab)
        combatIdx = table.getn(tabs)
    end

    for i = 1, table.getn(tabs) do
        local tab = tabs[i]
        if i == generalIdx then
            tab.kind = "general"
            tab.locked = true
            if Trim(tab.name) == "" then tab.name = GENERAL or "General" end
        elseif i == combatIdx then
            tab.kind = "combat"
            tab.locked = true
            if Trim(tab.name) == "" then tab.name = DEFAULT_COMBAT_TAB_NAME end
        else
            if tab.kind == "general" or tab.kind == "combat" then tab.kind = nil end
            tab.locked = nil
        end
    end

    return maxId
end

local function EnsureDB()
    if not SFramesDB then SFramesDB = {} end
    if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end

    local db = SFramesDB.Chat
    if db.enable == nil then db.enable = DEFAULTS.enable end
    if db.showBorder == nil then db.showBorder = DEFAULTS.showBorder end
    if db.borderClassColor == nil then db.borderClassColor = DEFAULTS.borderClassColor end
    if db.showPlayerLevel == nil then db.showPlayerLevel = DEFAULTS.showPlayerLevel end
    if type(db.width) ~= "number" then db.width = DEFAULTS.width end
    if type(db.height) ~= "number" then db.height = DEFAULTS.height end
    if type(db.scale) ~= "number" then db.scale = DEFAULTS.scale end
    if type(db.fontSize) ~= "number" then db.fontSize = DEFAULTS.fontSize end
    if type(db.sidePadding) ~= "number" then db.sidePadding = DEFAULTS.sidePadding end
    if type(db.topPadding) ~= "number" then db.topPadding = DEFAULTS.topPadding end
    if type(db.bottomPadding) ~= "number" then db.bottomPadding = DEFAULTS.bottomPadding end
    if type(db.bgAlpha) ~= "number" then db.bgAlpha = DEFAULTS.bgAlpha end
    if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end
    if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end
    if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end
    if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
    if db.layoutVersion < 2 then
        db.topPadding = DEFAULTS.topPadding
        db.layoutVersion = 2
    end
    if db.layoutVersion < 3 then
        db.showBorder = false
        db.layoutVersion = 3
    end

    if type(db.tabs) ~= "table" or table.getn(db.tabs) == 0 then
        db.tabs = {
            BuildDefaultTab(1, GENERAL or "General"),
            BuildCombatTab(2, DEFAULT_COMBAT_TAB_NAME),
        }
    end

    local maxId = 0
    for i = 1, table.getn(db.tabs) do
        db.tabs[i] = SanitizeTab(db.tabs[i], i, "标签" .. tostring(i))
        if IsCombatTab(db.tabs[i]) then
            db.tabs[i].kind = "combat"
        end
        if db.tabs[i].id > maxId then maxId = db.tabs[i].id end
    end

    if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then
        db.nextTabId = maxId + 1
    end

    if db.layoutVersion < 4 then
        local hasCombat = false
        for i = 1, table.getn(db.tabs) do
            if IsCombatTab(db.tabs[i]) then
                hasCombat = true
                break
            end
        end

        if not hasCombat then
            local id = db.nextTabId
            if type(id) ~= "number" or id <= maxId then
                id = maxId + 1
            end
            table.insert(db.tabs, BuildCombatTab(id, DEFAULT_COMBAT_TAB_NAME))
            db.nextTabId = id + 1
        end

        db.layoutVersion = 4
    end

    if type(db.activeTab) ~= "number" then db.activeTab = DEFAULTS.activeTab end
    db.activeTab = Clamp(math.floor(db.activeTab + 0.5), 1, table.getn(db.tabs))

    if db.layoutVersion < 5 then
        local activeTab = db.tabs[db.activeTab]
        if IsCombatTab(activeTab) then
            for i = 1, table.getn(db.tabs) do
                if not IsCombatTab(db.tabs[i]) then
                    db.activeTab = i
                    break
                end
            end
        end
        db.layoutVersion = 5
    end

    maxId = EnsureProtectedTabs(db, maxId)
    if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then
        db.nextTabId = maxId + 1
    end
    if db.layoutVersion < 6 then
        db.layoutVersion = 6
    end

    db.activeTab = Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs))
    return db
end

local popupFrameCache = {}

local function RememberPopupFrame(whichKey, popup)
    if type(whichKey) ~= "string" or whichKey == "" then return end
    if type(popup) ~= "table" then return end
    popupFrameCache[whichKey] = popup
end

local function GetPopupEditBox(popup)
    if not popup then return nil end
    if popup.editBox then return popup.editBox end
    if popup.GetName then
        local eb = _G[popup:GetName() .. "EditBox"]
        if eb then return eb end
    end
    return nil
end

local function GetPopupEditText(popup)
    local eb = GetPopupEditBox(popup)
    if eb and eb.GetText then
        return eb:GetText() or ""
    end
    return ""
end

local function SetPopupEditText(popup, text)
    local eb = GetPopupEditBox(popup)
    if eb and eb.SetText then
        eb:SetText(text or "")
    end
end

local function FocusPopupEdit(popup)
    local eb = GetPopupEditBox(popup)
    if not eb then return end
    if eb.SetFocus then eb:SetFocus() end
    if eb.HighlightText then eb:HighlightText() end
end

local function SkinPopupEditBox(popup)
    local eb = GetPopupEditBox(popup)
    if not eb or eb._sfSkinned then return end
    eb._sfSkinned = true

    local name = eb:GetName() or ""
    if name ~= "" then
        for _, suffix in ipairs({"Left", "Middle", "Mid", "Right"}) do
            local tex = _G[name .. suffix]
            if tex and tex.SetAlpha then tex:SetAlpha(0) end
        end
    end

    local bg = CreateFrame("Frame", nil, eb)
    bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3)
    bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3)
    bg:SetFrameLevel(math.max((eb:GetFrameLevel() or 1) - 1, 0))
    bg:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    bg:SetBackdropColor(0.08, 0.06, 0.1, 0.95)
    bg:SetBackdropBorderColor(0.5, 0.4, 0.55, 0.8)
    eb._sfBg = bg

    if eb.SetTextInsets then eb:SetTextInsets(6, 6, 2, 2) end
    if eb.SetTextColor then eb:SetTextColor(1, 1, 1) end
end

local function ResolvePopupFrame(whichKey, dialog)
    if dialog and dialog.GetParent then
        local parent = dialog:GetParent()
        if parent and parent.which == whichKey and GetPopupEditBox(parent) then
            RememberPopupFrame(whichKey, parent)
            return parent
        end
    end

    if dialog and dialog.editBox then
        RememberPopupFrame(whichKey, dialog)
        return dialog
    end

    if this then
        if this.editBox then
            RememberPopupFrame(whichKey, this)
            return this
        end

        if this.GetParent then
            local parent = this:GetParent()
            if parent and parent.which == whichKey and GetPopupEditBox(parent) then
                RememberPopupFrame(whichKey, parent)
                return parent
            end
        end
    end

    local cached = popupFrameCache[whichKey]
    if cached and cached.which == whichKey and GetPopupEditBox(cached) then
        return cached
    end

    if StaticPopup_FindVisible then
        local popup = StaticPopup_FindVisible(whichKey)
        if popup then
            RememberPopupFrame(whichKey, popup)
            return popup
        end
    end

    for i = 1, 4 do
        local popup = _G["StaticPopup" .. tostring(i)]
        if popup and popup.which == whichKey then
            RememberPopupFrame(whichKey, popup)
            return popup
        end
    end

    return dialog
end

local function EnsurePopupDialogs()
    if not StaticPopupDialogs then return end

    if not StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] then
        StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] = {
            text = "请输入新标签标题",
            button1 = ACCEPT or "确定",
            button2 = CANCEL or "取消",
            hasEditBox = 1,
            maxLetters = 32,
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            preferredIndex = 3,
            OnShow = function(dialog)
                local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog)
                if not popup then return end
                SkinPopupEditBox(popup)
                local suggested = "Tab"
                if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then
                    suggested = SFrames.Chat:GetNextTabName()
                end
                SetPopupEditText(popup, suggested)
                FocusPopupEdit(popup)
            end,
            OnAccept = function(dialog)
                local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog)
                local text = ""
                if popup then
                    text = Trim(GetPopupEditText(popup))
                end
                if text == "" then
                    if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then
                        text = SFrames.Chat:GetNextTabName()
                    else
                        text = "Tab"
                    end
                end
                if SFrames and SFrames.Chat then
                    SFrames.Chat:AddTab(text)
                end
            end,
        }
    end

    if not StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] then
        StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] = {
            text = "重命名标签",
            button1 = ACCEPT or "确定",
            button2 = CANCEL or "取消",
            hasEditBox = 1,
            maxLetters = 32,
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            preferredIndex = 3,
            OnShow = function(dialog, data)
                local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog)
                if not popup then return end
                SkinPopupEditBox(popup)
                local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex))
                local name = ""
                if SFrames and SFrames.Chat then
                    local tab = nil
                    if type(idx) == "number" then
                        tab = SFrames.Chat:GetTab(idx)
                    else
                        tab = SFrames.Chat:GetActiveTab()
                    end
                    if tab and tab.name then
                        name = tab.name
                    end
                end
                SetPopupEditText(popup, name)
                FocusPopupEdit(popup)
            end,
            OnAccept = function(dialog, data)
                local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog)
                if not popup then return end
                local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex))
                local text = GetPopupEditText(popup)
                if not (SFrames and SFrames.Chat) then return end
                if type(idx) == "number" then
                    SFrames.Chat:RenameTab(idx, text)
                else
                    SFrames.Chat:RenameActiveTab(text)
                end
                SFrames.Chat.pendingRenameIndex = nil
            end,
        }
    end

    if not StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] then
        StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] = {
            text = "更改聊天界面的启用状态需要重载界面(Reload UI)。\n确定要现在重载吗？",
            button1 = ACCEPT or "确定",
            button2 = CANCEL or "取消",
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            OnAccept = function()
                ReloadUI()
            end,
            OnCancel = function()
                -- 恢复之前的勾选状态
                local db = SFrames.Chat:EnsureDB()
                db.enable = not db.enable
                SFrames.Chat:RefreshConfigFrame()
            end,
        }
    end
end

local function NewDropDownInfo()
    if UIDropDownMenu_CreateInfo then
        return UIDropDownMenu_CreateInfo()
    end
    return {}
end

SFrames.Chat.FilterDefs = FILTER_DEFS

function SFrames.Chat:EnsureDB()
    return EnsureDB()
end

function SFrames.Chat:GetConfig()
    local db = EnsureDB()
    local editBoxPosition = tostring(db.editBoxPosition or DEFAULTS.editBoxPosition)
    if editBoxPosition ~= "top" and editBoxPosition ~= "bottom" and editBoxPosition ~= "free" then
        editBoxPosition = DEFAULTS.editBoxPosition
    end

    return {
        enable = db.enable ~= false,
        showBorder = db.showBorder ~= false,
        borderClassColor = db.borderClassColor == true,
        width = math.floor(Clamp(db.width, 320, 900) + 0.5),
        height = math.floor(Clamp(db.height, 120, 460) + 0.5),
        scale = Clamp(db.scale, 0.75, 1.4),
        fontSize = math.floor(Clamp(db.fontSize, 10, 18) + 0.5),
        sidePadding = math.floor(Clamp(db.sidePadding, 6, 20) + 0.5),
        topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
        bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
        bgAlpha = Clamp(db.bgAlpha, 0, 1),
        editBoxPosition = editBoxPosition,
        editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
        editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
    }
end

function SFrames.Chat:GetTabs()
    return EnsureDB().tabs
end

function SFrames.Chat:GetActiveTabIndex()
    local db = EnsureDB()
    return Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs))
end

function SFrames.Chat:GetActiveTab()
    local db = EnsureDB()
    return db.tabs[self:GetActiveTabIndex()]
end

function SFrames.Chat:GetTab(index)
    local db = EnsureDB()
    index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs))
    return db.tabs[index], index
end

function SFrames.Chat:GetNextTabName()
    local db = EnsureDB()
    local id = db.nextTabId or (table.getn(db.tabs) + 1)
    return "标签" .. tostring(id)
end

function SFrames.Chat:IsTabProtected(index)
    local tab = self:GetTab(index)
    if not tab then return false end
    if tab.locked == true then return true end
    if tab.kind == "general" or tab.kind == "combat" then return true end
    if IsCombatTab(tab) then return true end
    return false
end

function SFrames.Chat:GetJoinedChannels()
    return GetJoinedChannels()
end

function SFrames.Chat:GetTabChannelFilter(index, channelName)
    local tab = self:GetTab(index)
    if not tab then return true end
    if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
    local key = ChannelKey(channelName)
    if key == "" then return true end

    -- Direct lookup first
    local saved = tab.channelFilters[key]
    if saved ~= nil then
        return saved == true
    end

    -- Alias-aware lookup: if this channel belongs to an alias group,
    -- check if any alias key has been explicitly saved.
    local aliases = GetChannelAliasKeys(channelName)
    if aliases then
        for _, alias in ipairs(aliases) do
            local aliasKey = ChannelKey(alias)
            if aliasKey ~= "" and aliasKey ~= key then
                local aliasSaved = tab.channelFilters[aliasKey]
                if aliasSaved ~= nil then
                    return aliasSaved == true
                end
            end
        end
        -- No explicit save found for any alias; default to blocked
        return false
    end

    -- Not an ignored channel; default to shown
    return true
end

function SFrames.Chat:GetTabTranslateFilter(index, key)
    local tab = self:GetTab(index)
    if not tab or not TRANSLATE_FILTER_KEYS[key] then return false end
    if type(tab.translateFilters) ~= "table" then
        tab.translateFilters = BuildDefaultTranslateFilters()
    end
    return tab.translateFilters[key] == true
end

function SFrames.Chat:SetTabTranslateFilter(index, key, enabled)
    local tab = self:GetTab(index)
    if not tab or not TRANSLATE_FILTER_KEYS[key] then return end
    if type(tab.translateFilters) ~= "table" then
        tab.translateFilters = BuildDefaultTranslateFilters()
    end
    tab.translateFilters[key] = (enabled == true)
    if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
        self:RefreshTranslateConfigFrame()
    end
    self:NotifyConfigUI()
end

function SFrames.Chat:SetActiveTabTranslateFilter(key, enabled)
    self:SetTabTranslateFilter(self:GetActiveTabIndex(), key, enabled)
end

function SFrames.Chat:GetTabChannelTranslateFilter(index, channelName)
    local tab = self:GetTab(index)
    if not tab then return false end
    if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
    local key = ChannelKey(channelName)
    if key == "" then return false end
    if not self:GetTabChannelFilter(index, channelName) then
        return false
    end
    -- Direct lookup
    local saved = tab.channelTranslateFilters[key]
    if saved ~= nil then return saved == true end
    -- Alias-aware lookup
    local aliases = GetChannelAliasKeys(channelName)
    if aliases then
        for _, alias in ipairs(aliases) do
            local ak = ChannelKey(alias)
            if ak ~= "" and ak ~= key then
                local aliasSaved = tab.channelTranslateFilters[ak]
                if aliasSaved ~= nil then return aliasSaved == true end
            end
        end
    end
    return false
end

function SFrames.Chat:SetTabChannelTranslateFilter(index, channelName, enabled)
    local tab = self:GetTab(index)
    if not tab then return end
    if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
    local key = ChannelKey(channelName)
    if key == "" then return end
    -- Save under canonical alias key and clear other alias keys
    local canonicalKey = key
    local aliases = GetChannelAliasKeys(channelName)
    if aliases then
        local firstAlias = ChannelKey(aliases[1] or "")
        if firstAlias ~= "" then canonicalKey = firstAlias end
        for _, alias in ipairs(aliases) do
            local ak = ChannelKey(alias)
            if ak ~= "" and ak ~= canonicalKey then
                tab.channelTranslateFilters[ak] = nil
            end
        end
    end
    tab.channelTranslateFilters[canonicalKey] = (enabled == true)
    if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
        self:RefreshTranslateConfigFrame()
    end
    self:NotifyConfigUI()
end

function SFrames.Chat:SetActiveTabChannelTranslateFilter(channelName, enabled)
    self:SetTabChannelTranslateFilter(self:GetActiveTabIndex(), channelName, enabled)
end

function SFrames.Chat:GetTabIndexForChatFrame(frame)
    if not frame then return nil end
    if self.chatFrameToTabIndex and self.chatFrameToTabIndex[frame] then
        return self.chatFrameToTabIndex[frame]
    end
    local db = EnsureDB()
    for i = 1, table.getn(db.tabs) do
        local chatFrame = self:GetChatFrameForTab(db.tabs[i])
        if chatFrame == frame then
            if not self.chatFrameToTabIndex then self.chatFrameToTabIndex = {} end
            self.chatFrameToTabIndex[frame] = i
            return i
        end
    end
    return nil
end

function SFrames.Chat:FindTabIndexById(tabId)
    if type(tabId) ~= "number" then return nil end
    local db = EnsureDB()
    for i = 1, table.getn(db.tabs) do
        local tab = db.tabs[i]
        if tab and tab.id == tabId then
            return i
        end
    end
    return nil
end

function SFrames.Chat:ShouldAutoTranslateForTab(index, filterKey, channelName)
    local tab = self:GetTab(index)
    if not tab then return false end
    if filterKey == "channel" then
        return self:GetTabChannelTranslateFilter(index, channelName)
    end
    if not TRANSLATE_FILTER_KEYS[filterKey] then
        return false
    end
    if tab.filters and tab.filters[filterKey] == false then
        return false
    end
    if not self:GetTabTranslateFilter(index, filterKey) then
        return false
    end
    return true
end

function SFrames.Chat:SetTabChannelFilter(index, channelName, enabled)
    local tab, idx = self:GetTab(index)
    if not tab then return end
    if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
    local key = ChannelKey(channelName)
    if key == "" then return end
    -- Save under the canonical key only (first alias in the group, or the key itself)
    local canonicalKey = key
    local aliases = GetChannelAliasKeys(channelName)
    if aliases then
        -- Use the first alias as the canonical key, but only if it's a simple exact key
        local firstAlias = ChannelKey(aliases[1] or "")
        if firstAlias ~= "" then canonicalKey = firstAlias end
        -- Also clear any previously-saved keys for other aliases in the same group
        for _, alias in ipairs(aliases) do
            local ak = ChannelKey(alias)
            if ak ~= "" and ak ~= canonicalKey then
                tab.channelFilters[ak] = nil
            end
        end
    end
    tab.channelFilters[canonicalKey] = (enabled == true)
    if self:GetActiveTabIndex() == idx then
        self:ApplyTabChannels(idx)
    end
    if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
        self:RefreshTranslateConfigFrame()
    end
    self:NotifyConfigUI()
end

function SFrames.Chat:SetActiveTabChannelFilter(channelName, enabled)
    self:SetTabChannelFilter(self:GetActiveTabIndex(), channelName, enabled)
end

function SFrames.Chat:NotifyConfigUI()
    if SFrames and SFrames.ConfigUI and SFrames.ConfigUI.activePage == "chat" and SFrames.ConfigUI.RefreshChatPage then
        SFrames.ConfigUI:RefreshChatPage()
    end
end

function SFrames.Chat:CanUseAutoTranslateAPI()
    if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then
        return false
    end
    return _G.STranslateAPI
        and _G.STranslateAPI.IsReady
        and _G.STranslateAPI.IsReady()
        and _G.STranslateAPI.Translate
end

function SFrames.Chat:RequestAutoTranslation(text, callback)
    local cleanText = CleanTextForTranslation(text)
    if cleanText == "" then
        if callback then callback(nil, "EMPTY_TEXT") end
        return false
    end
    if not self:CanUseAutoTranslateAPI() then
        if callback then callback(nil, "API_UNAVAILABLE") end
        return false
    end

    if not self.translationCache then self.translationCache = {} end
    if not self.translationPending then self.translationPending = {} end

    local cacheKey = AUTO_TRANSLATE_TARGET_LANG .. "\031" .. cleanText
    local cached = self.translationCache[cacheKey]
    if cached and cached ~= "" then
        if callback then callback(cached, nil, true) end
        return true
    end

    local pending = self.translationPending[cacheKey]
    if pending then
        if callback then table.insert(pending, callback) end
        return true
    end

    self.translationPending[cacheKey] = {}
    if callback then
        table.insert(self.translationPending[cacheKey], callback)
    end

    local chat = self
    _G.STranslateAPI.Translate(cleanText, "auto", AUTO_TRANSLATE_TARGET_LANG, function(result, err, meta)
        local callbacks = chat.translationPending and chat.translationPending[cacheKey]
        if chat.translationPending then
            chat.translationPending[cacheKey] = nil
        end

        if result and result ~= "" then
            if not chat.translationCache then chat.translationCache = {} end
            chat.translationCache[cacheKey] = result
        end

        if callbacks then
            for i = 1, table.getn(callbacks) do
                local cb = callbacks[i]
                if cb then
                    cb(result, err, meta)
                end
            end
        end
    end, "Nanami-UI")
    return true
end

function SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, sourceText, translatedText, senderName)
    if type(translatedText) ~= "string" or translatedText == "" then return end

    local tabIndex = self:FindTabIndexById(tabId)
    if not tabIndex then return end
    if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then
        return
    end

    local cleanSource = CleanTextForTranslation(sourceText)
    local cleanTranslated = CleanTextForTranslation(translatedText)
    if cleanSource == "" or cleanTranslated == "" or cleanSource == cleanTranslated then
        return
    end

    local tab = self:GetTab(tabIndex)
    local chatFrame = self:GetChatFrameForTab(tab)
    if not chatFrame then return end

    if not chatFrame.AddMessage then return end

    local prefix = "|cff6ecf6e[AI]|r "
    if type(channelName) == "string" and channelName ~= "" then
        prefix = prefix .. "|cff8cb4d8[" .. channelName .. "]|r "
    end
    if type(senderName) == "string" and senderName ~= "" then
        prefix = prefix .. "|cffdab777" .. senderName .. ":|r "
    end
    local fullText = prefix .. "|cffe8dcc8" .. cleanTranslated .. "|r"
    chatFrame:AddMessage(fullText)
    if type(senderName) == "string" and senderName ~= "" and SFrames.Chat.MessageIndex then
        if not SFrames.Chat.MessageSenders then SFrames.Chat.MessageSenders = {} end
        SFrames.Chat.MessageSenders[SFrames.Chat.MessageIndex] = senderName
    end
end

function SFrames.Chat:QueueAutoTranslationForFrame(frame, tabIndex, event, messageText, channelName)
    if not frame or type(messageText) ~= "string" or messageText == "" then
        return
    end

    local filterKey = GetTranslateFilterKeyForEvent(event)
    if not filterKey then
        return
    end
    if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then
        return
    end

    local tab = self:GetTab(tabIndex)
    if not tab or type(tab.id) ~= "number" then
        return
    end

    local cleanText = CleanTextForTranslation(messageText)
    if cleanText == "" then
        return
    end

    self:RequestAutoTranslation(cleanText, function(result, err, meta)
        if result and result ~= "" then
            SFrames.Chat:AppendAutoTranslatedLine(tab.id, filterKey, channelName, cleanText, result)
        end
    end)
end

function SFrames.Chat:SavePosition()
    if not (self.frame and self.frame.GetPoint) then return end
    if not SFramesDB then SFramesDB = {} end
    if not SFramesDB.Positions then SFramesDB.Positions = {} end

    local point, _, relativePoint, xOfs, yOfs = self.frame:GetPoint()
    SFramesDB.Positions["ChatFrame"] = {
        point = point,
        relativePoint = relativePoint,
        xOfs = xOfs,
        yOfs = yOfs,
    }
end

function SFrames.Chat:SaveSizeFromFrame()
    if not self.frame then return end
    local db = EnsureDB()
    db.width = math.floor(Clamp(self.frame:GetWidth() or DEFAULTS.width, 320, 900) + 0.5)
    db.height = math.floor(Clamp(self.frame:GetHeight() or DEFAULTS.height, 120, 460) + 0.5)
    self:NotifyConfigUI()
end

function SFrames.Chat:ResetPosition()
    if not self.frame then return end
    if not SFramesDB then SFramesDB = {} end
    if not SFramesDB.Positions then SFramesDB.Positions = {} end
    SFramesDB.Positions["ChatFrame"] = nil
    self.frame:ClearAllPoints()
    self.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
    self:SavePosition()
    if SFrames and SFrames.Print then
        SFrames:Print("Chat frame position reset.")
    end
end

function SFrames.Chat:SetTabFilter(index, key, enabled)
    local tab = self:GetTab(index)
    if not tab or tab.filters[key] == nil then return end
    tab.filters[key] = (enabled == true)
    if self:GetActiveTabIndex() == index then
        self:ApplyTabFilters(index)
        -- Restore cached messages after filter change cleared the frame
        local chatFrame = self:GetChatFrameForTab(tab)
        if chatFrame then
            self:RestoreCachedMessages(chatFrame)
        end
    end
    self:NotifyConfigUI()
end

function SFrames.Chat:SetActiveTabFilter(key, enabled)
    self:SetTabFilter(self:GetActiveTabIndex(), key, enabled)
end

function SFrames.Chat:RenameTab(index, name)
    local tab, idx = self:GetTab(index)
    if not tab then return false end
    local clean = Trim(name)
    if clean == "" then return false end
    tab.name = clean
    if self.frame then self:RefreshTabButtons() end
    self:NotifyConfigUI()
    if idx == self:GetActiveTabIndex() then
        self:SwitchActiveChatFrame(self:GetActiveTab())
    end
    return true
end

function SFrames.Chat:RenameActiveTab(name)
    return self:RenameTab(self:GetActiveTabIndex(), name)
end

function SFrames.Chat:PromptNewTab()
    EnsurePopupDialogs()
    if StaticPopup_Show then
        local popup = StaticPopup_Show("SFRAMES_CHAT_NEW_TAB")
        if not popup then
            local fallbackName = self:GetNextTabName()
            self:AddTab(fallbackName)
        end
    else
        self:AddTab(self:GetNextTabName())
    end
end

function SFrames.Chat:PromptRenameTab(index)
    local _, idx = self:GetTab(index or self:GetActiveTabIndex())
    self.pendingRenameIndex = idx
    EnsurePopupDialogs()
    if StaticPopup_Show then
        StaticPopup_Show("SFRAMES_CHAT_RENAME_TAB", nil, nil, idx)
    end
end

function SFrames.Chat:AddTab(name)
    local db = EnsureDB()
    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    if table.getn(db.tabs) >= maxWindows then
        if SFrames and SFrames.Print then SFrames:Print("最多只能创建 " .. maxWindows .. " 个聊天标签。") end
        return false
    end

    local clean = Trim(name)
    if clean == "" then clean = self:GetNextTabName() end
    local id = db.nextTabId or (table.getn(db.tabs) + 1)
    db.nextTabId = id + 1
    table.insert(db.tabs, BuildDefaultTab(id, clean))
    db.activeTab = table.getn(db.tabs)

    self:RefreshTabButtons()
    self:ApplyAllTabsSetup()
    self:NotifyConfigUI()
    return true
end

function SFrames.Chat:DeleteTab(index)
    local db = EnsureDB()
    local tab, idx = self:GetTab(index)
    if not tab then return false end

    if self:IsTabProtected(idx) then
        if SFrames and SFrames.Print then
            SFrames:Print("Default General/Combat tabs cannot be deleted.")
        end
        return false
    end

    if table.getn(db.tabs) <= 2 then
        if SFrames and SFrames.Print then
            SFrames:Print("Keep at least default General and Combat tabs.")
        end
        return false
    end

    table.remove(db.tabs, idx)
    if idx <= db.activeTab then db.activeTab = db.activeTab - 1 end
    if db.activeTab < 1 then db.activeTab = 1 end
    if db.activeTab > table.getn(db.tabs) then db.activeTab = table.getn(db.tabs) end

    self:RefreshTabButtons()
    self:ApplyAllTabsSetup()
    self:NotifyConfigUI()
    return true
end

function SFrames.Chat:DeleteActiveTab()
    return self:DeleteTab(self:GetActiveTabIndex())
end

function SFrames.Chat:OpenFilterConfigForTab(index)
    if index then
        self:SetActiveTab(index)
    end
    self:OpenConfigFrame()
end

function SFrames.Chat:SetActiveTab(index)
    local db = EnsureDB()
    index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs))
    if db.activeTab == index then return end
    db.activeTab = index
    self:RefreshTabButtons()
    self:SwitchActiveChatFrame(self:GetActiveTab())
    self:NotifyConfigUI()
end

function SFrames.Chat:StepTab(delta)
    local db = EnsureDB()
    local count = table.getn(db.tabs)
    local target = db.activeTab + (delta or 1)
    if target < 1 then target = count end
    if target > count then target = 1 end
    self:SetActiveTab(target)
end

function SFrames.Chat:EnsureTabContextMenu()
    if self.tabContextMenu then return end
    if not (CreateFrame and UIDropDownMenu_Initialize and UIDropDownMenu_AddButton) then return end

    local menu = CreateFrame("Frame", "SFramesChatTabContextMenu", UIParent, "UIDropDownMenuTemplate")
    UIDropDownMenu_Initialize(menu, function()
        local idx = SFrames.Chat.contextMenuTabIndex or SFrames.Chat:GetActiveTabIndex()
        local tab = SFrames.Chat:GetTab(idx)
        if not tab then return end
        local cfg = SFrames.Chat:GetConfig()

        local info = NewDropDownInfo()
        info.text = tab.name or ("标签 " .. tostring(idx))
        info.isTitle = 1
        info.notCheckable = 1
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "重命名标签"
        info.notCheckable = 1
        info.func = function()
            SFrames.Chat:PromptRenameTab(idx)
        end
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "删除标签"
        info.notCheckable = 1
        info.disabled = (SFrames.Chat:IsTabProtected(idx) or table.getn(SFrames.Chat:GetTabs()) <= 2) and 1 or nil
        info.func = function()
            SFrames.Chat:DeleteTab(idx)
        end
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "过滤设置"
        info.notCheckable = 1
        info.func = function()
            SFrames.Chat:OpenFilterConfigForTab(idx)
        end
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "当前字号: " .. tostring(cfg.fontSize)
        info.notCheckable = 1
        info.disabled = 1
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "增大字号 +1"
        info.notCheckable = 1
        info.disabled = (cfg.fontSize >= 18) and 1 or nil
        info.func = function()
            SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) + 1)
        end
        UIDropDownMenu_AddButton(info)

        info = NewDropDownInfo()
        info.text = "减小字号 -1"
        info.notCheckable = 1
        info.disabled = (cfg.fontSize <= 10) and 1 or nil
        info.func = function()
            SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) - 1)
        end
        UIDropDownMenu_AddButton(info)
    end, "MENU")

    self.tabContextMenu = menu
end

function SFrames.Chat:OpenTabContextMenu(index)
    self.contextMenuTabIndex = index
    self:EnsureTabContextMenu()

    if self.tabContextMenu and ToggleDropDownMenu then
        ToggleDropDownMenu(1, nil, self.tabContextMenu, "cursor", 0, 0)
    else
        self:PromptRenameTab(index)
    end
end

local function BoolText(v)
    if v then return "ON" end
    return "OFF"
end

local function GetPlayerClassColor()
    if not UnitClass then return nil end
    local _, classFile = UnitClass("player")
    if not classFile then return nil end

    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    if not colors then return nil end

    local classColor = colors[classFile]
    if not classColor then return nil end

    local r = classColor.r or classColor[1]
    local g = classColor.g or classColor[2]
    local b = classColor.b or classColor[3]
    if r and g and b then
        return r, g, b
    end
    return nil
end

function SFrames.Chat:GetBorderColorRGB()
    local cfg = self:GetConfig()
    if cfg.borderClassColor then
        local r, g, b = GetPlayerClassColor()
        if r and g and b then
            return r, g, b
        end
    end
    return 0.92, 0.56, 0.78
end

function SFrames.Chat:SetWindowSize(width, height)
    local db = EnsureDB()
    db.width = math.floor(Clamp(width or db.width, 320, 900) + 0.5)
    db.height = math.floor(Clamp(height or db.height, 120, 460) + 0.5)
    self:ApplyConfig()
end

function SFrames.Chat:SetWindowScale(scale)
    local db = EnsureDB()
    db.scale = Clamp(scale or db.scale, 0.75, 1.4)
    self:ApplyConfig()
end

function SFrames.Chat:SetFontSize(size)
    local db = EnsureDB()
    db.fontSize = math.floor(Clamp(size or db.fontSize, 10, 18) + 0.5)
    self:ApplyConfig()
end

function SFrames.Chat:PrintFilters()
    local tab = self:GetActiveTab()
    if not tab then return end
    if SFrames and SFrames.Print then
        SFrames:Print("闁荤喐绮庢晶妤呭箰閸涘﹥娅犻柣妯款嚙閸愨偓闂佹悶鍎弲鈺呭礉? " .. tostring(tab.name))
        for i = 1, table.getn(FILTER_DEFS) do
            local def = FILTER_DEFS[i]
            SFrames:Print(" - " .. def.key .. ": " .. BoolText(tab.filters[def.key] ~= false))
        end
    end
end

function SFrames.Chat:PrintHelp()
    if not (SFrames and SFrames.Print) then return end
    SFrames:Print("/nui chat (闂備胶鎳撻悘姘跺箰閸濄儲顫曢柟杈鹃檮閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鈷戦柡澶庢硶鑲栭梺?")
    SFrames:Print("/nui chat ui")
    SFrames:Print("/nui chat size <w> <h>")
    SFrames:Print("/nui chat scale <0.75-1.4>")
    SFrames:Print("/nui chat font <10-18>")
    SFrames:Print("/nui chat reset")
    SFrames:Print("/nui chat tab new <name>")
    SFrames:Print("/nui chat tab del")
    SFrames:Print("/nui chat tab next|prev|<index>")
    SFrames:Print("/nui chat tab rename <name>")
    SFrames:Print("/nui chat filter <key> on|off")
    SFrames:Print("/nui chat filters")
end

function SFrames.Chat:GetConfigFrameActiveTabName()
    local tab = self:GetActiveTab()
    if tab and tab.name and tab.name ~= "" then
        return tab.name
    end
    return "Tab"
end

function SFrames.Chat:RefreshConfigFrame()
    if not self.configFrame then return end

    if self.cfgCurrentTabText then
        self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
    end

    if self.cfgChannelHint then
        local channels = self:GetJoinedChannels()
        self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
    end

    if self.cfgRenameBox and self.cfgRenameBox.Refresh then
        self.cfgRenameBox:Refresh()
    end

    if self.cfgDeleteTabButton then
        local protected = self:IsTabProtected(self:GetActiveTabIndex())
        if protected and self.cfgDeleteTabButton.Disable then
            self.cfgDeleteTabButton:Disable()
        elseif (not protected) and self.cfgDeleteTabButton.Enable then
            self.cfgDeleteTabButton:Enable()
        end
    end

    if self.configControls then
        for i = 1, table.getn(self.configControls) do
            local ctrl = self.configControls[i]
            if ctrl and ctrl.Refresh then
                ctrl:Refresh()
            end
        end
    end

    if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
        self:RefreshTranslateConfigFrame()
    end
end

function SFrames.Chat:RefreshTranslateConfigFrame()
    if not self.translateConfigFrame then return end

    if self.translateCurrentTabText then
        self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
    end

    if self.translateChannelHint then
        local channels = self:GetJoinedChannels()
        self.translateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
    end

    if self.translateConfigControls then
        for i = 1, table.getn(self.translateConfigControls) do
            local ctrl = self.translateConfigControls[i]
            if ctrl and ctrl.Refresh then
                ctrl:Refresh()
            end
        end
    end
end

function SFrames.Chat:EnsureTranslateConfigFrame()
    if self.translateConfigFrame then return end

    local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
    local panel = CreateFrame("Frame", "SFramesChatTranslateConfigPanel", UIParent)
    panel:SetWidth(540)
    panel:SetHeight(520)
    panel:SetPoint("CENTER", UIParent, "CENTER", 180, 0)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:SetClampedToScreen(true)
    panel:SetFrameStrata("DIALOG")
    panel:SetFrameLevel(151)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", function() this:StartMoving() end)
    panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)

    table.insert(UISpecialFrames, "SFramesChatTranslateConfigPanel")

    if SFrames and SFrames.CreateBackdrop then
        SFrames:CreateBackdrop(panel)
    else
        panel:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = true, tileSize = 32, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end
    panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
    panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])

    local title = panel:CreateFontString(nil, "OVERLAY")
    title:SetFont(fontPath, 14, "OUTLINE")
    title:SetPoint("TOP", panel, "TOP", 0, -12)
    title:SetText("聊天 AI 翻译")
    title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

    local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)

    local controls = {}
    local function AddControl(ctrl)
        table.insert(controls, ctrl)
    end

    local tabSection = CreateCfgSection(panel, "标签页", 10, -36, 520, 92, fontPath)
    self.translateCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
    self.translateCurrentTabText:SetFont(fontPath, 11, "OUTLINE")
    self.translateCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28)
    self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())

    CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function()
        SFrames.Chat:StepTab(-1)
        SFrames.Chat:RefreshConfigFrame()
        SFrames.Chat:RefreshTranslateConfigFrame()
    end)
    CreateCfgButton(tabSection, "下一个", 112, -48, 92, 22, function()
        SFrames.Chat:StepTab(1)
        SFrames.Chat:RefreshConfigFrame()
        SFrames.Chat:RefreshTranslateConfigFrame()
    end)

    local filterSection = CreateCfgSection(panel, "消息翻译", 10, -134, 520, 126, fontPath)
    for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do
        local key = TRANSLATE_FILTER_ORDER[i]
        local col = math.mod(i - 1, 2)
        local row = math.floor((i - 1) / 2)
        local x = 14 + col * 240
        local y = -28 - row * 24

        AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y,
            function()
                return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key)
            end,
            function(checked)
                SFrames.Chat:SetActiveTabTranslateFilter(key, checked)
            end,
            function()
                SFrames.Chat:RefreshTranslateConfigFrame()
            end
        ))
    end

    local channelSection = CreateCfgSection(panel, "频道翻译", 10, -266, 520, 198, fontPath)
    self.translateChannelChecks = {}
    self.translateChannelHint = channelSection:CreateFontString(nil, "OVERLAY")
    self.translateChannelHint:SetFont(fontPath, 10, "OUTLINE")
    self.translateChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8)
    self.translateChannelHint:SetTextColor(0.84, 0.8, 0.86)
    self.translateChannelHint:SetText("已加入频道:")

    local maxChannelChecks = 15
    for i = 1, maxChannelChecks do
        local slot = i
        local col = math.mod(i - 1, 3)
        local row = math.floor((i - 1) / 3)
        local x = 14 + col * 165
        local y = -24 - row * 24

        local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), channelSection, "UICheckButtonTemplate")
        cb:SetWidth(20)
        cb:SetHeight(20)
        cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y)
        StyleCfgCheck(cb)

        local label = _G[cb:GetName() .. "Text"]
        if label then
            label:SetText("")
            label:SetWidth(140)
            label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
        end

        cb:SetScript("OnClick", function()
            local name = this.channelName
            if not name or name == "" then return end
            SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false)
            SFrames.Chat:RefreshTranslateConfigFrame()
        end)

        cb.Refresh = function()
            local channels = SFrames.Chat:GetJoinedChannels()
            local info = channels[slot]
            if info then
                local activeIndex = SFrames.Chat:GetActiveTabIndex()
                local enabled = SFrames.Chat:GetTabChannelFilter(activeIndex, info.name)
                    and SFrames.Chat:GetTabTranslateFilter(activeIndex, "channel")
                cb.channelName = info.name
                if label then
                    label:SetText(ShortText(info.name, 24))
                    if enabled then
                        label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
                    else
                        label:SetTextColor(0.45, 0.45, 0.45)
                    end
                end
                cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
                if enabled then
                    cb:Enable()
                else
                    cb:Disable()
                end
                cb:Show()
            else
                cb.channelName = nil
                cb:SetChecked(false)
                if label then label:SetText("") end
                cb:Hide()
            end
        end

        AddControl(cb)
        table.insert(self.translateChannelChecks, cb)
    end

    local tip = channelSection:CreateFontString(nil, "OVERLAY")
    tip:SetFont(fontPath, 10, "OUTLINE")
    tip:SetPoint("TOPLEFT", channelSection, "TOPLEFT", 14, -150)
    tip:SetText("仅已启用接收的频道可自动翻译")
    tip:SetTextColor(0.7, 0.7, 0.74)

    local close = CreateCfgButton(panel, "关闭", 200, -474, 140, 26, function()
        SFrames.Chat.translateConfigFrame:Hide()
    end)
    StyleCfgButton(close)

    self.translateConfigControls = controls
    self.translateConfigFrame = panel
end

function SFrames.Chat:OpenTranslateConfigFrame()
    self:EnsureTranslateConfigFrame()
    if not self.translateConfigFrame then return end
    self:RefreshTranslateConfigFrame()
    self.translateConfigFrame:Show()
    self.translateConfigFrame:Raise()
end

function SFrames.Chat:ToggleTranslateConfigFrame()
    self:EnsureTranslateConfigFrame()
    if not self.translateConfigFrame then return end
    if self.translateConfigFrame:IsShown() then
        self.translateConfigFrame:Hide()
    else
        self:OpenTranslateConfigFrame()
    end
end

function SFrames.Chat:EnsureConfigFrame()
    if self.configFrame then return end

    local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
    local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent)
    panel:SetWidth(540)
    panel:SetHeight(786)
    panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:SetClampedToScreen(true)
    panel:SetFrameStrata("DIALOG")
    panel:SetFrameLevel(150)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", function() this:StartMoving() end)
    panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
    
    table.insert(UISpecialFrames, "SFramesChatConfigPanel")

    if SFrames and SFrames.CreateBackdrop then
        SFrames:CreateBackdrop(panel)
    else
        panel:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = true, tileSize = 32, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end
    panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
    panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])

    local title = panel:CreateFontString(nil, "OVERLAY")
    title:SetFont(fontPath, 14, "OUTLINE")
    title:SetPoint("TOP", panel, "TOP", 0, -12)
    title:SetText("Nanami 聊天设置")
    title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

    local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)

    local controls = {}

    local function AddControl(ctrl)
        table.insert(controls, ctrl)
    end

    local windowSection = CreateCfgSection(panel, "窗口", 10, -36, 520, 226, fontPath)
    AddControl(CreateCfgSlider(windowSection, "宽度", 16, -46, 235, 320, 900, 1,
        function() return EnsureDB().width end,
        function(v) EnsureDB().width = v end,
        function(v) return tostring(math.floor(v + 0.5)) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgSlider(windowSection, "高度", 270, -46, 235, 120, 460, 1,
        function() return EnsureDB().height end,
        function(v) EnsureDB().height = v end,
        function(v) return tostring(math.floor(v + 0.5)) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgSlider(windowSection, "缩放", 16, -108, 235, 0.75, 1.40, 0.05,
        function() return EnsureDB().scale end,
        function(v) EnsureDB().scale = v end,
        function(v) return string.format("%.2f", v) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgSlider(windowSection, "字号", 270, -108, 235, 10, 18, 1,
        function() return EnsureDB().fontSize end,
        function(v) EnsureDB().fontSize = v end,
        function(v) return tostring(math.floor(v + 0.5)) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgCheck(windowSection, "启用聊天", 360, -122,
        function() return EnsureDB().enable ~= false end,
        function(checked)
            local oldState = EnsureDB().enable ~= false
            if oldState ~= checked then
                EnsureDB().enable = (checked == true)
                SFrames.Chat:EnsureConfigFrame()
                if StaticPopup_Show then
                    StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT")
                else
                    ReloadUI()
                end
            end
        end,
        function() SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgCheck(windowSection, "显示边框", 360, -142,
        function() return EnsureDB().showBorder ~= false end,
        function(checked) EnsureDB().showBorder = (checked == true) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgCheck(windowSection, "边框职业色", 360, -162,
        function() return EnsureDB().borderClassColor == true end,
        function(checked) EnsureDB().borderClassColor = (checked == true) end,
        function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
    ))
    AddControl(CreateCfgCheck(windowSection, "显示等级", 360, -182,
        function() return EnsureDB().showPlayerLevel ~= false end,
        function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
        function() SFrames.Chat:RefreshConfigFrame() end
    ))

    CreateCfgButton(windowSection, "重置位置", 16, -198, 110, 22, function()
        SFrames.Chat:ResetPosition()
    end)
    CreateCfgButton(windowSection, "解锁", 132, -198, 110, 22, function()
        if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
    end)
    CreateCfgButton(windowSection, "锁定", 248, -198, 110, 22, function()
        if SFrames and SFrames.LockFrames then SFrames:LockFrames() end
    end)
    
    local posLabel = windowSection:CreateFontString(nil, "OVERLAY")
    posLabel:SetFont(fontPath, 13, "OUTLINE")
    posLabel:SetPoint("TOPLEFT", windowSection, "TOPLEFT", 16, -151)
    posLabel:SetText("输入框位置:")
    posLabel:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
    
    CreateCfgButton(windowSection, "底部", 90, -148, 50, 20, function() EnsureDB().editBoxPosition = "bottom"; SFrames.Chat:StyleEditBox() end)
    CreateCfgButton(windowSection, "顶部", 144, -148, 50, 20, function() EnsureDB().editBoxPosition = "top"; SFrames.Chat:StyleEditBox() end)
    CreateCfgButton(windowSection, "自由拖动 (Alt)", 198, -148, 100, 20, function()
        EnsureDB().editBoxPosition = "free"
        SFrames.Chat:StyleEditBox()
        DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 键可以自由拖动输入框。")
    end)

    local tabSection = CreateCfgSection(panel, "标签", 10, -268, 520, 118, fontPath)
    self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
    self.cfgCurrentTabText:SetFont(fontPath, 11, "OUTLINE")
    self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28)
    self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
    self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())

    CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function()
        SFrames.Chat:StepTab(-1)
        SFrames.Chat:RefreshConfigFrame()
    end)
    CreateCfgButton(tabSection, "下一个", 112, -48, 92, 22, function()
        SFrames.Chat:StepTab(1)
        SFrames.Chat:RefreshConfigFrame()
    end)
    CreateCfgButton(tabSection, "新建", 210, -48, 92, 22, function()
        SFrames.Chat:PromptNewTab()
        SFrames.Chat:RefreshConfigFrame()
    end)
    self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 308, -48, 92, 22, function()
        SFrames.Chat:DeleteActiveTab()
        SFrames.Chat:RefreshConfigFrame()
    end)

    CreateCfgButton(tabSection, "AI", 406, -48, 92, 22, function()
        SFrames.Chat:OpenTranslateConfigFrame()
    end)

    self.cfgRenameBox = CreateCfgEditBox(tabSection, 14, -80, 188, 20,
        function() return SFrames.Chat:GetConfigFrameActiveTabName() end,
        function(text)
            SFrames.Chat:RenameActiveTab(text)
            SFrames.Chat:RefreshConfigFrame()
        end
    )
    CreateCfgButton(tabSection, "重命名", 210, -80, 92, 22, function()
        SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "")
        SFrames.Chat:RefreshConfigFrame()
    end)

    local filterSection = CreateCfgSection(panel, "消息过滤（当前标签）", 10, -392, 520, 186, fontPath)
    for i = 1, table.getn(FILTER_DEFS) do
        local def = FILTER_DEFS[i]
        local col = math.mod(i - 1, 2)
        local row = math.floor((i - 1) / 2)
        local x = 14 + col * 240
        local y = -28 - row * 24

        AddControl(CreateCfgCheck(filterSection, def.label, x, y,
            function()
                local tab = SFrames.Chat:GetActiveTab()
                return tab and tab.filters and tab.filters[def.key] ~= false
            end,
            function(checked)
                SFrames.Chat:SetActiveTabFilter(def.key, checked)
            end,
            function()
                SFrames.Chat:ApplyConfig()
                SFrames.Chat:RefreshConfigFrame()
            end
        ))
    end

    local channelSection = CreateCfgSection(panel, "频道过滤（当前标签）", 10, -584, 520, 172, fontPath)
    self.cfgChannelChecks = {}
    self.cfgChannelHint = channelSection:CreateFontString(nil, "OVERLAY")
    self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE")
    self.cfgChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8)
    self.cfgChannelHint:SetTextColor(0.84, 0.8, 0.86)
    self.cfgChannelHint:SetText("已加入频道:")

    local maxChannelChecks = 15
    for i = 1, maxChannelChecks do
        local slot = i
        local col = math.mod(i - 1, 3)
        local row = math.floor((i - 1) / 3)
        local x = 14 + col * 165
        local y = -24 - row * 24

        local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), channelSection, "UICheckButtonTemplate")
        cb:SetWidth(20)
        cb:SetHeight(20)
        cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y)
        StyleCfgCheck(cb)

        local label = _G[cb:GetName() .. "Text"]
        if label then
            label:SetText("")
            label:SetWidth(140)
            label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
        end

        cb:SetScript("OnClick", function()
            local name = this.channelName
            if not name or name == "" then return end
            SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false)
            SFrames.Chat:ApplyConfig()
            SFrames.Chat:RefreshConfigFrame()
        end)

        cb.Refresh = function()
            local channels = SFrames.Chat:GetJoinedChannels()
            local info = channels[slot]
            if info then
                cb.channelName = info.name
                if label then label:SetText(ShortText(info.name, 24)) end
                cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
                cb:Show()
            else
                cb.channelName = nil
                cb:SetChecked(false)
                if label then label:SetText("") end
                cb:Hide()
            end
        end

        AddControl(cb)
        table.insert(self.cfgChannelChecks, cb)
    end


    local okBtn = CreateCfgButton(panel, "保存", 115, -730, 140, 26, function()
        SFrames.Chat.configFrame:Hide()
    end)
    StyleCfgButton(okBtn)
    AddBtnIcon(okBtn, "save")

    local reloadBtn = CreateCfgButton(panel, "保存并重载", 285, -730, 140, 26, function()
        ReloadUI()
    end)
    StyleCfgButton(reloadBtn)
    AddBtnIcon(reloadBtn, "save")

    panel.controls = controls
    self.configControls = controls
    self.configFrame = panel
end

function SFrames.Chat:OpenConfigFrame()
    self:EnsureConfigFrame()
    if not self.configFrame then return end
    self:RefreshConfigFrame()
    self.configFrame:Show()
    self.configFrame:Raise()
end

function SFrames.Chat:ToggleConfigFrame()
    self:EnsureConfigFrame()
    if not self.configFrame then return end
    if self.configFrame:IsShown() then
        self.configFrame:Hide()
    else
        self:OpenConfigFrame()
    end
end

local CONFIG_PAGE_ORDER = {
    { key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" },
    { key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" },
    { key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" },
    { key = "translate", label = "AI翻译", title = "AI 翻译", desc = "为当前标签配置自动翻译范围和频道翻译。", icon = "ai" },
    { key = "hc", label = "硬核设置", title = "硬核生存选项", desc = "全局硬核控制、死亡通报过滤及等级限制。", icon = "skull" },
}

local CONFIG_PAGE_MAP = {}
for i = 1, table.getn(CONFIG_PAGE_ORDER) do
    local info = CONFIG_PAGE_ORDER[i]
    CONFIG_PAGE_MAP[info.key] = info
end

local function GetConfigPageInfo(pageKey)
    return CONFIG_PAGE_MAP[pageKey] or CONFIG_PAGE_MAP.window
end

local function GetEditBoxPositionText(mode)
    if mode == "top" then return "顶部" end
    if mode == "free" then return "自由拖动" end
    return "底部"
end

local function SetConfigNavButtonActive(btn, active)
    if not btn then return end
    local bg = CFG_THEME.buttonBg
    local border = CFG_THEME.buttonBorder
    local text = CFG_THEME.buttonText
    if active then
        bg = { 0.32, 0.16, 0.26, 0.98 }
        border = CFG_THEME.btnHoverBd or { 0.80, 0.48, 0.64, 0.98 }
        text = { 1, 0.92, 0.96 }
    end
    if btn.SetBackdropColor then
        btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
    end
    if btn.SetBackdropBorderColor then
        btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
    end
    local fs = btn.GetFontString and btn:GetFontString()
    if fs then
        fs:SetTextColor(text[1], text[2], text[3])
    end
end

function SFrames.Chat:RefreshConfigNavButtons()
    if not self.configNavButtons then return end
    local activePage = self.configActivePage or "window"
    for key, btn in pairs(self.configNavButtons) do
        SetConfigNavButtonActive(btn, key == activePage)
    end
end

function SFrames.Chat:ShowConfigPage(pageKey)
    if not self.configFrame then return end
    local info = GetConfigPageInfo(pageKey)
    self.configActivePage = info.key

    if self.configPages then
        for key, page in pairs(self.configPages) do
            if page then
                if key == info.key then
                    page:Show()
                else
                    page:Hide()
                end
            end
        end
    end

    if self.configPageTitle then
        self.configPageTitle:SetText(info.title or "")
    end
    if self.configPageDesc then
        self.configPageDesc:SetText(info.desc or "")
    end

    self:RefreshConfigNavButtons()
end

function SFrames.Chat:RefreshConfigFrame()
    if not self.configFrame then return end

    if not self.configActivePage then
        self.configActivePage = "window"
    end

    if self.configSidebarTabText then
        self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName())
    end

    local pageInfo = GetConfigPageInfo(self.configActivePage)
    if self.configPageTitle then
        self.configPageTitle:SetText(pageInfo.title or "")
    end
    if self.configPageDesc then
        self.configPageDesc:SetText(pageInfo.desc or "")
    end

    if self.cfgCurrentTabText then
        self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
    end
    if self.cfgFilterTabText then
        self.cfgFilterTabText:SetText("过滤目标: " .. self:GetConfigFrameActiveTabName())
    end
    if self.cfgTranslateTabText then
        self.cfgTranslateTabText:SetText("翻译目标: " .. self:GetConfigFrameActiveTabName())
    end

    if self.cfgChannelHint then
        local channels = self:GetJoinedChannels()
        self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
    end
    if self.cfgTranslateChannelHint then
        local channels = self:GetJoinedChannels()
        self.cfgTranslateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
    end

    if self.cfgRenameBox and self.cfgRenameBox.Refresh then
        self.cfgRenameBox:Refresh()
    end

    if self.cfgDeleteTabButton then
        local protected = self:IsTabProtected(self:GetActiveTabIndex())
        if protected and self.cfgDeleteTabButton.Disable then
            self.cfgDeleteTabButton:Disable()
        elseif (not protected) and self.cfgDeleteTabButton.Enable then
            self.cfgDeleteTabButton:Enable()
        end
    end

    if self.cfgTabProtectedText then
        if self:IsTabProtected(self:GetActiveTabIndex()) then
            self.cfgTabProtectedText:SetText("当前标签受保护，不能删除。")
            self.cfgTabProtectedText:SetTextColor(0.95, 0.72, 0.72)
        else
            self.cfgTabProtectedText:SetText("当前标签可自由调整过滤和翻译设置。")
            self.cfgTabProtectedText:SetTextColor(0.72, 0.88, 0.76)
        end
    end

    if self.cfgInputModeText then
        self.cfgInputModeText:SetText("当前输入框位置: " .. GetEditBoxPositionText(EnsureDB().editBoxPosition))
    end

    if self.cfgWindowSummaryText then
        local db = EnsureDB()
        self.cfgWindowSummaryText:SetText("当前尺寸: " .. tostring(db.width) .. " x " .. tostring(db.height) .. "    缩放: " .. string.format("%.2f", db.scale) .. "    背景: " .. string.format("%.0f%%", (db.bgAlpha or DEFAULTS.bgAlpha) * 100))
    end

    if self.cfgTranslateStatusText then
        if self:CanUseAutoTranslateAPI() then
            self.cfgTranslateStatusText:SetText("STranslate API 已就绪，自动翻译可用。")
            self.cfgTranslateStatusText:SetTextColor(0.72, 0.9, 0.78)
        else
            self.cfgTranslateStatusText:SetText("未检测到可用的 STranslate API，开启后也不会发起翻译请求。")
            self.cfgTranslateStatusText:SetTextColor(0.95, 0.76, 0.62)
        end
    end

    if self.configControls then
        for i = 1, table.getn(self.configControls) do
            local ctrl = self.configControls[i]
            if ctrl and ctrl.Refresh then
                ctrl:Refresh()
            end
        end
    end

    self:RefreshConfigNavButtons()
end

function SFrames.Chat:RefreshTranslateConfigFrame()
    self:RefreshConfigFrame()
end

function SFrames.Chat:EnsureTranslateConfigFrame()
    self:EnsureConfigFrame()
end

function SFrames.Chat:OpenTranslateConfigFrame()
    self:OpenConfigFrame("translate")
end

function SFrames.Chat:ToggleTranslateConfigFrame()
    self:ToggleConfigFrame("translate")
end

function SFrames.Chat:EnsureConfigFrame()
    if self.configFrame then return end

    local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
    local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent)
    panel:SetWidth(780)
    panel:SetHeight(630)
    panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:SetClampedToScreen(true)
    panel:SetFrameStrata("DIALOG")
    panel:SetFrameLevel(150)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", function() this:StartMoving() end)
    panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)

    table.insert(UISpecialFrames, "SFramesChatConfigPanel")

    if SFrames and SFrames.CreateBackdrop then
        SFrames:CreateBackdrop(panel)
    else
        panel:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = true, tileSize = 32, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end
    panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
    panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])

    local title = panel:CreateFontString(nil, "OVERLAY")
    title:SetFont(fontPath, 15, "OUTLINE")
    title:SetPoint("TOPLEFT", panel, "TOPLEFT", 18, -14)
    title:SetText("Nanami 聊天设置")
    title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

    local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)

    local sidebar = CreateCfgSection(panel, "导航", 12, -42, 154, 532, fontPath)

    local sideLabel = sidebar:CreateFontString(nil, "OVERLAY")
    sideLabel:SetFont(fontPath, 10, "OUTLINE")
    sideLabel:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -32)
    sideLabel:SetText("当前标签")
    sideLabel:SetTextColor(0.72, 0.72, 0.78)

    self.configSidebarTabText = sidebar:CreateFontString(nil, "OVERLAY")
    self.configSidebarTabText:SetFont(fontPath, 14, "OUTLINE")
    self.configSidebarTabText:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -50)
    self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName())
    self.configSidebarTabText:SetTextColor(0.96, 0.94, 0.98)

    local sideTip = sidebar:CreateFontString(nil, "OVERLAY")
    sideTip:SetFont(fontPath, 10, "OUTLINE")
    sideTip:SetPoint("BOTTOMLEFT", sidebar, "BOTTOMLEFT", 12, 14)
    sideTip:SetWidth(130)
    sideTip:SetJustifyH("LEFT")
    sideTip:SetText("把窗口、标签、过滤和翻译拆成独立页面，避免一屏堆满。")
    sideTip:SetTextColor(0.72, 0.72, 0.78)

    self.configPageTitle = panel:CreateFontString(nil, "OVERLAY")
    self.configPageTitle:SetFont(fontPath, 14, "OUTLINE")
    self.configPageTitle:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -48)
    self.configPageTitle:SetText("")
    self.configPageTitle:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

    self.configPageDesc = panel:CreateFontString(nil, "OVERLAY")
    self.configPageDesc:SetFont(fontPath, 10, "OUTLINE")
    self.configPageDesc:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -68)
    self.configPageDesc:SetText("")
    self.configPageDesc:SetTextColor(0.78, 0.78, 0.84)

    self.configNavButtons = {}
    local navY = -88
    for i = 1, table.getn(CONFIG_PAGE_ORDER) do
        local info = CONFIG_PAGE_ORDER[i]
        local btn = CreateCfgButton(sidebar, info.label, 12, navY, 130, 28, function()
            SFrames.Chat:ShowConfigPage(info.key)
            SFrames.Chat:RefreshConfigFrame()
        end)
        if info.icon then AddBtnIcon(btn, info.icon, nil, "left") end
        btn.pageKey = info.key
        local oldLeave = btn:GetScript("OnLeave")
        btn:SetScript("OnLeave", function()
            if oldLeave then oldLeave() end
            if this.pageKey and SFrames and SFrames.Chat and SFrames.Chat.configActivePage == this.pageKey then
                SetConfigNavButtonActive(this, true)
            end
        end)
        self.configNavButtons[info.key] = btn
        navY = navY - 36
    end

    local content = CreateFrame("Frame", nil, panel)
    content:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -92)
    content:SetWidth(584)
    content:SetHeight(484)
    panel.content = content

    local controls = {}
    local function AddControl(ctrl)
        table.insert(controls, ctrl)
        return ctrl
    end

    local pages = {}
    local function CreatePage(key)
        local page = CreateFrame("Frame", nil, content)
        page:SetAllPoints(content)
        page:Hide()
        pages[key] = page
        return page
    end

    local windowPage = CreatePage("window")
    do
        local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
        AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
            function() return EnsureDB().width end,
            function(v) EnsureDB().width = v end,
            function(v) return tostring(math.floor(v + 0.5)) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgSlider(appearance, "高度", 308, -46, 260, 120, 460, 1,
            function() return EnsureDB().height end,
            function(v) EnsureDB().height = v end,
            function(v) return tostring(math.floor(v + 0.5)) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgSlider(appearance, "缩放", 16, -108, 260, 0.75, 1.40, 0.05,
            function() return EnsureDB().scale end,
            function(v) EnsureDB().scale = v end,
            function(v) return string.format("%.2f", v) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgSlider(appearance, "字号", 308, -108, 260, 10, 18, 1,
            function() return EnsureDB().fontSize end,
            function(v) EnsureDB().fontSize = v end,
            function(v) return tostring(math.floor(v + 0.5)) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgSlider(appearance, "背景透明度", 16, -170, 260, 0, 1, 0.05,
            function() return EnsureDB().bgAlpha end,
            function(v) EnsureDB().bgAlpha = v end,
            function(v) return string.format("%.0f%%", v * 100) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgCheck(appearance, "启用聊天", 16, -218,
            function() return EnsureDB().enable ~= false end,
            function(checked)
                local oldState = EnsureDB().enable ~= false
                if oldState ~= checked then
                    EnsureDB().enable = (checked == true)
                    if StaticPopup_Show then
                        StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT")
                    else
                        ReloadUI()
                    end
                end
            end,
            function() SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgCheck(appearance, "显示边框", 144, -218,
            function() return EnsureDB().showBorder ~= false end,
            function(checked) EnsureDB().showBorder = (checked == true) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgCheck(appearance, "边框职业色", 288, -218,
            function() return EnsureDB().borderClassColor == true end,
            function(checked) EnsureDB().borderClassColor = (checked == true) end,
            function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
        ))
        AddControl(CreateCfgCheck(appearance, "显示等级", 432, -218,
            function() return EnsureDB().showPlayerLevel ~= false end,
            function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
            function() SFrames.Chat:RefreshConfigFrame() end
        ))

        self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY")
        self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
        self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
        self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8)

        local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath)
        self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY")
        self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
        self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
        self.cfgInputModeText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
        self.cfgInputModeText:SetText("")

        CreateCfgButton(inputSection, "底部", 16, -52, 92, 22, function()
            EnsureDB().editBoxPosition = "bottom"
            SFrames.Chat:StyleEditBox()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(inputSection, "顶部", 114, -52, 92, 22, function()
            EnsureDB().editBoxPosition = "top"
            SFrames.Chat:StyleEditBox()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(inputSection, "自由拖动", 212, -52, 108, 22, function()
            EnsureDB().editBoxPosition = "free"
            SFrames.Chat:StyleEditBox()
            SFrames.Chat:RefreshConfigFrame()
            DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 可拖动输入框。")
        end)

        local inputTip = inputSection:CreateFontString(nil, "OVERLAY")
        inputTip:SetFont(fontPath, 10, "OUTLINE")
        inputTip:SetPoint("BOTTOMLEFT", inputSection, "BOTTOMLEFT", 16, 10)
        inputTip:SetWidth(540)
        inputTip:SetJustifyH("LEFT")
        inputTip:SetText("建议优先使用顶部或底部模式；自由拖动适合特殊布局。")
        inputTip:SetTextColor(0.74, 0.74, 0.8)

        local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath)
        CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
            SFrames.Chat:ResetPosition()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(actionSection, "解锁", 132, -32, 108, 24, function()
            if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
        end)
        CreateCfgButton(actionSection, "锁定", 248, -32, 108, 24, function()
            if SFrames and SFrames.LockFrames then SFrames:LockFrames() end
        end)
        CreateCfgButton(actionSection, "重置私聊图标位置", 364, -32, 160, 24, function()
            if SFramesDB then SFramesDB.whisperBtnPos = nil end
            if SFrames and SFrames.Chat and SFrames.Chat.frame then
                local f = SFrames.Chat.frame
                if f.whisperButton and f.configButton then
                    f.whisperButton:ClearAllPoints()
                    f.whisperButton:SetPoint("RIGHT", f.configButton, "LEFT", -6, 0)
                end
            end
            SFrames.Chat:RefreshConfigFrame()
        end)
    end

    local tabsPage = CreatePage("tabs")
    do
        local tabSection = CreateCfgSection(tabsPage, "当前标签", 0, 0, 584, 118, fontPath)
        self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
        self.cfgCurrentTabText:SetFont(fontPath, 12, "OUTLINE")
        self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 16, -30)
        self.cfgCurrentTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])

        CreateCfgButton(tabSection, "上一个", 16, -54, 92, 22, function()
            SFrames.Chat:StepTab(-1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(tabSection, "下一个", 114, -54, 92, 22, function()
            SFrames.Chat:StepTab(1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(tabSection, "新建", 212, -54, 92, 22, function()
            SFrames.Chat:PromptNewTab()
            SFrames.Chat:RefreshConfigFrame()
        end)
        self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 310, -54, 92, 22, function()
            SFrames.Chat:DeleteActiveTab()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(tabSection, "前往过滤", 408, -54, 112, 22, function()
            SFrames.Chat:ShowConfigPage("filters")
            SFrames.Chat:RefreshConfigFrame()
        end)

        local renameSection = CreateCfgSection(tabsPage, "重命名", 0, -134, 584, 84, fontPath)
        self.cfgRenameBox = CreateCfgEditBox(renameSection, 16, -38, 250, 20,
            function() return SFrames.Chat:GetConfigFrameActiveTabName() end,
            function(text)
                SFrames.Chat:RenameActiveTab(text)
                SFrames.Chat:RefreshConfigFrame()
            end
        )
        CreateCfgButton(renameSection, "应用名称", 280, -36, 108, 22, function()
            SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "")
            SFrames.Chat:RefreshConfigFrame()
        end)

        local infoSection = CreateCfgSection(tabsPage, "状态与快捷入口", 0, -234, 584, 116, fontPath)
        self.cfgTabProtectedText = infoSection:CreateFontString(nil, "OVERLAY")
        self.cfgTabProtectedText:SetFont(fontPath, 11, "OUTLINE")
        self.cfgTabProtectedText:SetPoint("TOPLEFT", infoSection, "TOPLEFT", 16, -30)
        self.cfgTabProtectedText:SetWidth(540)
        self.cfgTabProtectedText:SetJustifyH("LEFT")

        CreateCfgButton(infoSection, "消息过滤", 16, -66, 110, 22, function()
            SFrames.Chat:ShowConfigPage("filters")
            SFrames.Chat:RefreshConfigFrame()
        end)
        local aiBtn = CreateCfgButton(infoSection, "AI 翻译", 132, -66, 110, 22, function()
            SFrames.Chat:ShowConfigPage("translate")
            SFrames.Chat:RefreshConfigFrame()
        end)
        AddBtnIcon(aiBtn, "ai")

        local infoTip = infoSection:CreateFontString(nil, "OVERLAY")
        infoTip:SetFont(fontPath, 10, "OUTLINE")
        infoTip:SetPoint("BOTTOMLEFT", infoSection, "BOTTOMLEFT", 16, 12)
        infoTip:SetWidth(540)
        infoTip:SetJustifyH("LEFT")
        infoTip:SetText("默认综合/战斗标签可能受保护。自定义标签建议先设置频道过滤，再配置 AI 翻译。")
        infoTip:SetTextColor(0.74, 0.74, 0.8)
    end

    local filtersPage = CreatePage("filters")
    do
        local headerSection = CreateCfgSection(filtersPage, "当前过滤目标", 0, 0, 584, 92, fontPath)
        self.cfgFilterTabText = headerSection:CreateFontString(nil, "OVERLAY")
        self.cfgFilterTabText:SetFont(fontPath, 12, "OUTLINE")
        self.cfgFilterTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30)
        self.cfgFilterTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])

        CreateCfgButton(headerSection, "上一个", 16, -52, 92, 22, function()
            SFrames.Chat:StepTab(-1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(headerSection, "下一个", 114, -52, 92, 22, function()
            SFrames.Chat:StepTab(1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(headerSection, "标签管理", 212, -52, 108, 22, function()
            SFrames.Chat:ShowConfigPage("tabs")
            SFrames.Chat:RefreshConfigFrame()
        end)

        local filterSection = CreateCfgSection(filtersPage, "消息与频道接收", 0, -108, 584, 380, fontPath)
        
        CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function()
            for _, def in ipairs(FILTER_DEFS) do
                SFrames.Chat:SetActiveTabFilter(def.key, true)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, true)
            end
            SFrames.Chat:ApplyConfig()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function()
            for _, def in ipairs(FILTER_DEFS) do
                SFrames.Chat:SetActiveTabFilter(def.key, false)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, false)
            end
            SFrames.Chat:ApplyConfig()
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function()
            local actIdx = SFrames.Chat:GetActiveTabIndex()
            local tab = SFrames.Chat:GetActiveTab()
            for _, def in ipairs(FILTER_DEFS) do
                local state = tab and tab.filters and tab.filters[def.key] ~= false
                SFrames.Chat:SetActiveTabFilter(def.key, not state)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                local state = SFrames.Chat:GetTabChannelFilter(actIdx, channels[i].name)
                SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, not state)
            end
            SFrames.Chat:ApplyConfig()
            SFrames.Chat:RefreshConfigFrame()
        end)

        local nextIndex = 0
        for i = 1, table.getn(FILTER_DEFS) do
            local def = FILTER_DEFS[i]
            local col = math.mod(nextIndex, 3)
            local row = math.floor(nextIndex / 3)
            local x = 16 + col * 182
            local y = -60 - row * 24

            AddControl(CreateCfgCheck(filterSection, def.label, x, y,
                function()
                    local tab = SFrames.Chat:GetActiveTab()
                    return tab and tab.filters and tab.filters[def.key] ~= false
                end,
                function(checked)
                    SFrames.Chat:SetActiveTabFilter(def.key, checked)
                end,
                function()
                    SFrames.Chat:ApplyConfig()
                    SFrames.Chat:RefreshConfigFrame()
                end
            ))
            nextIndex = nextIndex + 1
        end

        -- Channel sub-header: separator, hint, refresh button
        local chSepRow = math.ceil(nextIndex / 3)
        local chSepY = -60 - chSepRow * 24

        local chSepLine = filterSection:CreateTexture(nil, "ARTWORK")
        chSepLine:SetTexture(1, 1, 1, 0.15)
        chSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY + 6)
        chSepLine:SetWidth(540)
        chSepLine:SetHeight(1)

        local chSepLabel = filterSection:CreateFontString(nil, "OVERLAY")
        chSepLabel:SetFont(fontPath, 11, "OUTLINE")
        chSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY - 4)
        chSepLabel:SetText("频道过滤")
        chSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

        self.cfgChannelHint = filterSection:CreateFontString(nil, "OVERLAY")
        self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE")
        self.cfgChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, chSepY - 5)
        self.cfgChannelHint:SetTextColor(0.84, 0.80, 0.86)
        self.cfgChannelHint:SetText("已加入频道:")

        CreateCfgButton(filterSection, "刷新频道列表", 420, chSepY - 2, 100, 18, function()
            SFrames.Chat:RefreshConfigFrame()
        end)

        nextIndex = (chSepRow + 1) * 3

        self.cfgChannelChecks = {}
        for i = 1, 15 do
            local slot = i
            local col = math.mod(nextIndex, 3)
            local row = math.floor(nextIndex / 3)
            local x = 16 + col * 182
            local y = -60 - row * 24

            local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), filterSection, "UICheckButtonTemplate")
            cb:SetWidth(20)
            cb:SetHeight(20)
            cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y)
            StyleCfgCheck(cb)

            local label = _G[cb:GetName() .. "Text"]
            if label then
                label:SetText("")
                label:SetWidth(150)
                label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
            end

            cb:SetScript("OnClick", function()
                local name = this.channelName
                if not name or name == "" then return end
                SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false)
                SFrames.Chat:ApplyConfig()
                SFrames.Chat:RefreshConfigFrame()
            end)

            cb.Refresh = function()
                local channels = SFrames.Chat:GetJoinedChannels()
                local info = channels[slot]
                if info then
                    cb.channelName = info.name
                    if label then label:SetText(ShortText(info.name, 24)) end
                    cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
                    cb:Show()
                else
                    cb.channelName = nil
                    cb:SetChecked(false)
                    if label then label:SetText("") end
                    cb:Hide()
                end
            end

            AddControl(cb)
            table.insert(self.cfgChannelChecks, cb)
            nextIndex = nextIndex + 1
        end
    end

    local translatePage = CreatePage("translate")
    do
        local headerSection = CreateCfgSection(translatePage, "当前翻译目标", 0, 0, 584, 104, fontPath)
        self.cfgTranslateTabText = headerSection:CreateFontString(nil, "OVERLAY")
        self.cfgTranslateTabText:SetFont(fontPath, 12, "OUTLINE")
        self.cfgTranslateTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30)
        self.cfgTranslateTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])

        self.cfgTranslateStatusText = headerSection:CreateFontString(nil, "OVERLAY")
        self.cfgTranslateStatusText:SetFont(fontPath, 10, "OUTLINE")
        self.cfgTranslateStatusText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -50)
        self.cfgTranslateStatusText:SetWidth(540)
        self.cfgTranslateStatusText:SetJustifyH("LEFT")

        CreateCfgButton(headerSection, "上一个", 16, -72, 92, 22, function()
            SFrames.Chat:StepTab(-1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(headerSection, "下一个", 114, -72, 92, 22, function()
            SFrames.Chat:StepTab(1)
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(headerSection, "频道过滤", 212, -72, 108, 22, function()
            SFrames.Chat:ShowConfigPage("filters")
            SFrames.Chat:RefreshConfigFrame()
        end)

        local filterSection = CreateCfgSection(translatePage, "消息与频道翻译", 0, -120, 584, 360, fontPath)
        
        CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function()
            for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
                SFrames.Chat:SetActiveTabTranslateFilter(key, true)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, true)
            end
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function()
            for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
                SFrames.Chat:SetActiveTabTranslateFilter(key, false)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, false)
            end
            SFrames.Chat:RefreshConfigFrame()
        end)
        CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function()
            local actIdx = SFrames.Chat:GetActiveTabIndex()
            for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
                local state = SFrames.Chat:GetTabTranslateFilter(actIdx, key)
                SFrames.Chat:SetActiveTabTranslateFilter(key, not state)
            end
            local channels = SFrames.Chat:GetJoinedChannels()
            for i = 1, table.getn(channels) do
                local state = SFrames.Chat:GetTabChannelTranslateFilter(actIdx, channels[i].name)
                SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, not state)
            end
            SFrames.Chat:RefreshConfigFrame()
        end)

        local nextIndex = 0
        for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do
            local key = TRANSLATE_FILTER_ORDER[i]
            local col = math.mod(nextIndex, 3)
            local row = math.floor(nextIndex / 3)
            local x = 16 + col * 182
            local y = -60 - row * 24

            AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y,
                function()
                    return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key)
                end,
                function(checked)
                    SFrames.Chat:SetActiveTabTranslateFilter(key, checked)
                end,
                function()
                    SFrames.Chat:RefreshConfigFrame()
                end
            ))
            nextIndex = nextIndex + 1
        end

        -- Channel sub-header for translate page
        local tchSepRow = math.ceil(nextIndex / 3)
        local tchSepY = -60 - tchSepRow * 24

        local tchSepLine = filterSection:CreateTexture(nil, "ARTWORK")
        tchSepLine:SetTexture(1, 1, 1, 0.15)
        tchSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY + 6)
        tchSepLine:SetWidth(540)
        tchSepLine:SetHeight(1)

        local tchSepLabel = filterSection:CreateFontString(nil, "OVERLAY")
        tchSepLabel:SetFont(fontPath, 11, "OUTLINE")
        tchSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY - 4)
        tchSepLabel:SetText("频道翻译")
        tchSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])

        self.cfgTranslateChannelHint = filterSection:CreateFontString(nil, "OVERLAY")
        self.cfgTranslateChannelHint:SetFont(fontPath, 10, "OUTLINE")
        self.cfgTranslateChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, tchSepY - 5)
        self.cfgTranslateChannelHint:SetTextColor(0.84, 0.80, 0.86)
        self.cfgTranslateChannelHint:SetText("已加入频道:")

        CreateCfgButton(filterSection, "刷新频道列表", 420, tchSepY - 2, 100, 18, function()
            SFrames.Chat:RefreshConfigFrame()
        end)

        nextIndex = (tchSepRow + 1) * 3

        self.translateChannelChecks = {}
        for i = 1, 15 do
            local slot = i
            local col = math.mod(nextIndex, 3)
            local row = math.floor(nextIndex / 3)
            local x = 16 + col * 182
            local y = -60 - row * 24

            local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), filterSection, "UICheckButtonTemplate")
            cb:SetWidth(20)
            cb:SetHeight(20)
            cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y)
            StyleCfgCheck(cb)

            local label = _G[cb:GetName() .. "Text"]
            if label then
                label:SetText("")
                label:SetWidth(150)
                label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
            end

            cb:SetScript("OnClick", function()
                local name = this.channelName
                if not name or name == "" then return end
                SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false)
                SFrames.Chat:RefreshConfigFrame()
            end)

            cb.Refresh = function()
                local channels = SFrames.Chat:GetJoinedChannels()
                local info = channels[slot]
                if info then
                    cb.channelName = info.name
                    if label then
                        label:SetText(ShortText(info.name, 24))
                        label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
                    end
                    cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
                    cb:Enable()
                    cb:Show()
                else
                    cb.channelName = nil
                    cb:SetChecked(false)
                    if label then label:SetText("") end
                    cb:Hide()
                end
            end

            AddControl(cb)
            table.insert(self.translateChannelChecks, cb)
            nextIndex = nextIndex + 1
        end
    end

    local hcPage = CreatePage("hc")
    do
        local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath)
        
        local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
        hcStatusText:SetFont(fontPath, 10, "OUTLINE")

        AddControl(CreateCfgCheck(hcControls, "全局彻底关闭硬核频道接收", 16, -30,
            function() return EnsureDB().hcGlobalDisable == true end,
            function(checked) EnsureDB().hcGlobalDisable = (checked == true) end,
            function(checked)
                SendChatMessage(".hcc", "SAY")
                if checked then
                    hcStatusText:SetText("HC Chat is now |cffff4444OFF|r")
                    hcStatusText:SetTextColor(1, 0.4, 0.4)
                else
                    hcStatusText:SetText("HC Chat is now |cff44ff44ON|r")
                    hcStatusText:SetTextColor(0.4, 1, 0.4)
                end
                SFrames.Chat:RefreshConfigFrame()
            end
        ))

        hcStatusText:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 230, -32)
        hcStatusText:SetWidth(200)
        hcStatusText:SetJustifyH("LEFT")
        hcStatusText:SetText("")

        local hcTip = hcControls:CreateFontString(nil, "OVERLAY")
        hcTip:SetFont(fontPath, 10, "OUTLINE")
        hcTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -56)
        hcTip:SetWidth(540)
        hcTip:SetJustifyH("LEFT")
        hcTip:SetText("彻底无视HC频道的强制聊天推送。勾选后，所有标签都不会收到硬核频道内容。(即时生效)")
        hcTip:SetTextColor(0.8, 0.7, 0.7)

        AddControl(CreateCfgCheck(hcControls, "全局屏蔽玩家死亡/满级信息", 16, -86,
            function() return EnsureDB().hcDeathDisable == true end,
            function(checked) EnsureDB().hcDeathDisable = (checked == true) end,
            function() SFrames.Chat:RefreshConfigFrame() end
        ))

        local deathTip = hcControls:CreateFontString(nil, "OVERLAY")
        deathTip:SetFont(fontPath, 10, "OUTLINE")
        deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
        deathTip:SetWidth(540)
        deathTip:SetJustifyH("LEFT")
        deathTip:SetText("关闭那些“某某在XX级死亡”的系统提示。")
        deathTip:SetTextColor(0.8, 0.7, 0.7)

        AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1,
            function() return EnsureDB().hcDeathLevelMin or 10 end,
            function(v) EnsureDB().hcDeathLevelMin = v end,
            function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
            function() SFrames.Chat:RefreshConfigFrame() end
        ))
    end

    local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function()
        SFrames.Chat.configFrame:Hide()
        local db = EnsureDB()
        
        if db.hcDeathDisable then
            SendChatMessage(".hcm 60", "SAY")
        elseif db.hcDeathLevelMin then
            SendChatMessage(".hcm " .. tostring(db.hcDeathLevelMin), "SAY")
        else
            SendChatMessage(".hcm 0", "SAY")
        end
    end)
    StyleCfgButton(close)
    AddBtnIcon(close, "save")

    local reload = CreateCfgButton(panel, "保存并重载", 598, -588, 150, 28, function()
        ReloadUI()
    end)
    StyleCfgButton(reload)
    AddBtnIcon(reload, "save")

    self.configControls = controls
    self.configPages = pages
    self.configFrame = panel
    self.translateConfigFrame = panel

    -- Auto-refresh channel list while panel is visible
    local channelPollTimer = 0
    local lastChannelCount = 0
    panel:SetScript("OnUpdate", function()
        channelPollTimer = channelPollTimer + (arg1 or 0)
        if channelPollTimer >= 3 then
            channelPollTimer = 0
            if not this:IsShown() then return end
            local channels = SFrames.Chat:GetJoinedChannels()
            local n = table.getn(channels)
            if n ~= lastChannelCount then
                lastChannelCount = n
                SFrames.Chat:RefreshConfigFrame()
            end
        end
    end)

    self:ShowConfigPage(self.configActivePage or "window")
end

function SFrames.Chat:OpenConfigFrame(pageKey)
    self:EnsureConfigFrame()
    if not self.configFrame then return end
    self:ShowConfigPage(pageKey or self.configActivePage or "window")
    self:RefreshConfigFrame()
    self.configFrame:Show()
    self.configFrame:Raise()
end

function SFrames.Chat:ToggleConfigFrame(pageKey)
    self:EnsureConfigFrame()
    if not self.configFrame then return end

    local targetPage = pageKey or self.configActivePage or "window"
    if self.configFrame:IsShown() then
        if pageKey and self.configActivePage ~= targetPage then
            self:ShowConfigPage(targetPage)
            self:RefreshConfigFrame()
            self.configFrame:Raise()
        else
            self.configFrame:Hide()
        end
    else
        self:OpenConfigFrame(targetPage)
    end
end

function SFrames.Chat:HandleSlash(input)
    local text = Trim(input)
    local _, _, cmd, rest = string.find(text, "^(%S*)%s*(.-)$")
    cmd = string.lower(cmd or "")
    rest = rest or ""

    if cmd == "" or cmd == "ui" or cmd == "config" or cmd == "panel" then
        self:ToggleConfigFrame()
        return
    end

    if cmd == "help" then
        self:PrintHelp()
        return
    end

    if cmd == "reset" then
        self:ResetPosition()
        return
    end

    if cmd == "size" then
        local _, _, w, h = string.find(rest, "^(%d+)%s+(%d+)$")
        w = tonumber(w)
        h = tonumber(h)
        if not w or not h then
            if SFrames and SFrames.Print then
                SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat size <width> <height>")
            end
            return
        end
        self:SetWindowSize(w, h)
        if SFrames and SFrames.Print then
            SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁喚娈曟俊娴嬪亾闂? " .. tostring(math.floor(w + 0.5)) .. "x" .. tostring(math.floor(h + 0.5)))
        end
        return
    end

    if cmd == "scale" then
        local v = tonumber(rest)
        if not v then
            if SFrames and SFrames.Print then
                SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat scale <0.75-1.4>")
            end
            return
        end
        self:SetWindowScale(v)
        if SFrames and SFrames.Print then
            SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁啰鎳呯紒杈ㄥ哺閺岋繝鍩€? " .. string.format("%.2f", Clamp(v, 0.75, 1.4)))
        end
        return
    end

    if cmd == "font" then
        local v = tonumber(rest)
        if not v then
            if SFrames and SFrames.Print then
                SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat font <10-18>")
            end
            return
        end
        self:SetFontSize(v)
        if SFrames and SFrames.Print then
            SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛澶囬崑鎾斥槈濞嗗秳娌紓? " .. tostring(math.floor(Clamp(v, 10, 18) + 0.5)))
        end
        return
    end

    if cmd == "tab" then
        local _, _, sub, subArgs = string.find(rest, "^(%S*)%s*(.-)$")
        sub = string.lower(sub or "")
        subArgs = subArgs or ""

        if sub == "new" then
            if Trim(subArgs) == "" then
                self:PromptNewTab()
            else
                self:AddTab(subArgs)
            end
            return
        elseif sub == "del" or sub == "delete" then
            self:DeleteActiveTab()
            return
        elseif sub == "next" then
            self:StepTab(1)
            return
        elseif sub == "prev" then
            self:StepTab(-1)
            return
        elseif sub == "rename" then
            local ok = self:RenameActiveTab(subArgs)
            if (not ok) and SFrames and SFrames.Print then
                SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab rename <name>")
            end
            return
        else
            local idx = tonumber(sub)
            if idx then
                self:SetActiveTab(idx)
                return
            end
        end

        if SFrames and SFrames.Print then
            SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab new|del|next|prev|rename|<index>")
        end
        return
    end

    if cmd == "filters" then
        self:PrintFilters()
        return
    end

    if cmd == "filter" then
        local _, _, key, state = string.find(rest, "^(%S+)%s*(%S*)$")
        key = string.lower(key or "")
        state = string.lower(state or "")

        local matched = nil
        for i = 1, table.getn(FILTER_DEFS) do
            local def = FILTER_DEFS[i]
            if key == def.key then
                matched = def.key
                break
            end
        end

        if not matched then
            if SFrames and SFrames.Print then
                SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柍鈺佸暞娴溿倝鏌涢妷锝呭濠殿喓鍨归—鍐Χ閸涱垳鏆涢梺閫炲苯澧柛濠佺矙椤㈡岸顢楁担鐟邦€撻柣鐘充航閸斿秹寮?/nui chat filters")
            end
            return
        end

        local enabled = nil
        if state == "on" or state == "1" or state == "true" then
            enabled = true
        elseif state == "off" or state == "0" or state == "false" then
            enabled = false
        end

        if enabled == nil then
            if SFrames and SFrames.Print then
                SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat filter <key> on|off")
            end
            return
        end

        self:SetActiveTabFilter(matched, enabled)
        self:ApplyConfig()
        if SFrames and SFrames.Print then
            SFrames:Print("闂佸搫顦弲娑樏洪敃鍌氱?" .. matched .. " = " .. BoolText(enabled))
        end
        return
    end

    if SFrames and SFrames.Print then
        SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柨鐔哄У閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鐓曟繛鎴炵懃閻忓﹪鏌熼煬鎻掆偓婵囦繆閹绢喖纾兼繝鍨姇濞堝弶绻涙潏鍓хК婵炲娲熷?/nui chat help")
    end
end

function SFrames.Chat:CreateContainer()
    if self.frame then return end

    local f = CreateFrame("Frame", "SFramesChatContainer", UIParent)
    f:SetWidth(DEFAULTS.width)
    f:SetHeight(DEFAULTS.height)
    f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetClampedToScreen(true)
    f:SetFrameStrata("LOW")
    f:SetResizable(true)
    if f.SetMinResize then f:SetMinResize(320, 120) end
    if f.SetMaxResize then f:SetMaxResize(900, 460) end

    f:SetScript("OnDragStart", function()
        if IsAltKeyDown() or (SFrames and SFrames.isUnlocked) then
            this:StartMoving()
        end
    end)

    f:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        if SFrames and SFrames.Chat then
            SFrames.Chat:SavePosition()
        end
    end)

    f:SetScript("OnSizeChanged", function()
        if SFrames and SFrames.Chat then
            SFrames.Chat:SaveSizeFromFrame()
            SFrames.Chat:RefreshChatBounds()
            SFrames.Chat:RefreshTabButtons()
        end
    end)

    f:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.9)
    f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.95)
    local chatShadow = CreateFrame("Frame", nil, f)
    chatShadow:SetPoint("TOPLEFT", f, "TOPLEFT", -5, 5)
    chatShadow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 5, -5)
    chatShadow:SetFrameLevel(math.max(f:GetFrameLevel() - 1, 0))
    chatShadow:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    chatShadow:SetBackdropColor(0, 0, 0, 0.55)
    chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)

    local topGlow = f:CreateTexture(nil, "BACKGROUND")
    topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
    topGlow:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
    topGlow:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
    topGlow:SetHeight(20)
    topGlow:SetVertexColor(1, 0.58, 0.82, 0.2)
    topGlow:Hide()
    f.topGlow = topGlow

    local topLine = f:CreateTexture(nil, "BORDER")
    topLine:SetTexture("Interface\\Buttons\\WHITE8X8")
    topLine:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -22)
    topLine:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -22)
    topLine:SetHeight(1)
    topLine:SetVertexColor(1, 0.69, 0.88, 0.85)
    topLine:Hide()
    f.topLine = topLine

    local title = CreateFont(f, 11, "LEFT")
    title:SetPoint("TOPLEFT", f, "TOPLEFT", 26, -7)
    title:SetText("Nanami")
    title:SetTextColor(1, 0.82, 0.93)
    f.title = title
    local configButton = CreateFrame("Button", nil, f)
    configButton:SetWidth(14)
    configButton:SetHeight(14)
    configButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -7)
    configButton:SetHitRectInsets(-4, -4, -4, -4)
    configButton:SetFrameStrata("HIGH")
    configButton:SetFrameLevel(f:GetFrameLevel() + 20)
    configButton:RegisterForClicks("LeftButtonUp")

    local cfgIcon = SFrames:CreateIcon(configButton, "settings", 12)
    cfgIcon:SetPoint("CENTER", configButton, "CENTER", 0, 0)
    configButton.cfgIcon = cfgIcon

    configButton:SetScript("OnClick", function()
        if SFrames and SFrames.Chat then
            SFrames.Chat:ToggleConfigFrame()
        end
    end)
    configButton:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_TOP")
        GameTooltip:ClearLines()
        GameTooltip:AddLine("聊天设置", 1, 0.84, 0.94)
        GameTooltip:AddLine("打开聊天配置面板", 0.85, 0.85, 0.85)
        GameTooltip:Show()
    end)
    configButton:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    f.configButton = configButton

    local whisperButton = CreateFrame("Button", nil, f)
    whisperButton:SetFrameStrata("HIGH")
    whisperButton:SetWidth(14)
    whisperButton:SetHeight(14)
    if SFramesDB and SFramesDB.whisperBtnPos then
        local pos = SFramesDB.whisperBtnPos
        whisperButton:SetPoint(pos.p, f, pos.rp, pos.x, pos.y)
    else
        whisperButton:SetPoint("RIGHT", configButton, "LEFT", -6, 0)
    end
    whisperButton:SetHitRectInsets(-4, -4, -4, -4)
    whisperButton:SetFrameLevel(f:GetFrameLevel() + 20)
    whisperButton:RegisterForClicks("LeftButtonUp")
    whisperButton:RegisterForDrag("LeftButton")
    whisperButton:SetMovable(true)
    whisperButton:SetScript("OnDragStart", function()
        if IsAltKeyDown() then
            this:StartMoving()
        end
    end)
    whisperButton:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        if not SFramesDB then SFramesDB = {} end
        -- Convert to position relative to parent frame f to ensure correct restore after reload
        local absX, absY = this:GetCenter()
        local fX, fY = f:GetCenter()
        local fW, fH = f:GetWidth(), f:GetHeight()
        local relX = absX - fX
        local relY = absY - fY
        SFramesDB.whisperBtnPos = { p = "CENTER", rp = "CENTER", x = relX, y = relY }
    end)

    if SFrames and SFrames.CreateBackdrop then
        SFrames:CreateBackdrop(whisperButton)
    elseif whisperButton.SetBackdrop then
        whisperButton:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end
    if whisperButton.SetBackdropColor then
        whisperButton:SetBackdropColor(0.1, 0.08, 0.12, 0.9)
    end
    if whisperButton.SetBackdropBorderColor then
        whisperButton:SetBackdropBorderColor(0.5, 0.8, 1.0, 0.92)
    end

    local whisperIcon = SFrames:CreateIcon(whisperButton, "chat", 12)
    whisperIcon:SetDrawLayer("ARTWORK")
    whisperIcon:SetPoint("CENTER", whisperButton, "CENTER", 0, 0)
    whisperIcon:SetVertexColor(0.6, 0.85, 1, 0.95)

    local flashFrame = CreateFrame("Frame", nil, whisperButton)
    flashFrame:SetAllPoints(whisperButton)
    flashFrame:SetFrameLevel(whisperButton:GetFrameLevel() + 1)
    local flashTex = flashFrame:CreateTexture(nil, "OVERLAY")
    flashTex:SetTexture("Interface\\Buttons\\WHITE8X8")
    flashTex:SetAllPoints(flashFrame)
    flashTex:SetVertexColor(1, 0.8, 0.2, 0.4)
    flashTex:SetBlendMode("ADD")
    flashFrame.tex = flashTex
    flashFrame:Hide()
    whisperButton.flashFrame = flashFrame

    flashFrame:SetScript("OnUpdate", function()
        if not this:IsShown() then return end
        this.elapsed = (this.elapsed or 0) + arg1
        local alpha = math.abs(math.sin(this.elapsed * 4)) * 0.5 + 0.1
        this.tex:SetAlpha(alpha)
    end)

    whisperButton:SetScript("OnClick", function()
        if SFrames and SFrames.Whisper then
            SFrames.Whisper:Toggle()
        end
        this.hasUnread = false
        if this.flashFrame then this.flashFrame:Hide() end
    end)
    whisperButton:SetScript("OnEnter", function()
        if this.SetBackdropColor then this:SetBackdropColor(0.16, 0.12, 0.2, 0.96) end
        GameTooltip:SetOwner(this, "ANCHOR_TOP")
        GameTooltip:ClearLines()
        GameTooltip:AddLine("私聊对话管理器", 0.6, 0.8, 1)
        if this.hasUnread then
            GameTooltip:AddLine("你有未读的私聊消息！", 1, 0.8, 0.2)
        end
        GameTooltip:AddLine("Alt + 拖动 可移动图标", 0.6, 0.6, 0.6)
        GameTooltip:Show()
    end)
    whisperButton:SetScript("OnLeave", function()
        if this.SetBackdropColor then this:SetBackdropColor(0.1, 0.08, 0.12, 0.9) end
        GameTooltip:Hide()
    end)

    f.whisperButton = whisperButton

    local hint = CreateFont(f, 10, "RIGHT")
    hint:SetPoint("RIGHT", whisperButton, "LEFT", -8, 0)
    hint:SetText("")
    hint:SetTextColor(0.86, 0.78, 0.85)
    hint:Hide()
    f.hint = hint

    local titleBtn = CreateFrame("Button", nil, f)
    titleBtn:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -2)
    titleBtn:SetHeight(20)
    titleBtn:SetFrameStrata("HIGH")
    titleBtn:SetFrameLevel(f:GetFrameLevel() + 20)
    titleBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    local titleBtnThrottle = 0
    titleBtn:SetScript("OnUpdate", function()
        titleBtnThrottle = titleBtnThrottle + arg1
        if titleBtnThrottle < 0.5 then return end
        titleBtnThrottle = 0
        local tw = title:GetStringWidth() or 40
        this:SetWidth(tw + 28)
    end)
    titleBtn:SetScript("OnClick", function()
        if arg1 == "RightButton" then
            if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then
                SFrames.Movers:ToggleLayoutMode()
            end
        else
            if SFrames and SFrames.ConfigUI then
                SFrames.ConfigUI:OpenUI()
            end
        end
    end)
    titleBtn:SetScript("OnEnter", function()
        title:SetTextColor(1, 0.92, 1)
        if f.leftCat then f.leftCat:SetVertexColor(1, 0.92, 1, 1) end
        GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
        GameTooltip:ClearLines()
        GameTooltip:AddLine("Nanami UI 设置", 1, 0.84, 0.94)
        GameTooltip:AddLine("左键打开主设置面板", 0.85, 0.85, 0.85)
        GameTooltip:AddLine("右键开启布局模式", 0.85, 0.85, 0.85)
        GameTooltip:Show()
    end)
    titleBtn:SetScript("OnLeave", function()
        title:SetTextColor(1, 0.82, 0.93)
        if f.leftCat then f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8) end
        GameTooltip:Hide()
    end)
    f.titleBtn = titleBtn

    local leftCat = SFrames:CreateIcon(titleBtn, "logo", 14)
    leftCat:SetDrawLayer("OVERLAY")
    leftCat:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -5)
    leftCat:SetVertexColor(1, 0.82, 0.9, 0.8)
    f.leftCat = leftCat

    local watermark = SFrames:CreateIcon(f, "logo", 62)
    watermark:SetDrawLayer("BACKGROUND")
    watermark:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
    watermark:SetVertexColor(1, 0.78, 0.9, 0.08)
    f.watermark = watermark

    local shade = f:CreateTexture(nil, "BACKGROUND")
    shade:SetTexture("Interface\\Buttons\\WHITE8X8")
    shade:SetVertexColor(0, 0, 0, 0.2)
    shade:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -30)
    shade:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
    f.innerShade = shade

    local tabBar = CreateFrame("Frame", nil, f)
    tabBar:SetPoint("LEFT", title, "RIGHT", 10, -1)
    tabBar:SetPoint("RIGHT", configButton, "LEFT", -28, -1)
    tabBar:SetHeight(18)
    f.tabBar = tabBar

    local inner = CreateFrame("Frame", nil, f)
    inner:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
    inner:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -20, 8)
    f.inner = inner

    local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"

    local scrollUpBtn = CreateFrame("Button", nil, f)
    scrollUpBtn:SetWidth(16)
    scrollUpBtn:SetHeight(16)
    scrollUpBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -30)
    scrollUpBtn:SetFrameStrata("HIGH")
    local upTxt = scrollUpBtn:CreateFontString(nil, "OVERLAY")
    upTxt:SetFont(fontPath, 11, "OUTLINE")
    upTxt:SetPoint("CENTER", scrollUpBtn, "CENTER", 1, 1)
    upTxt:SetText("▲")
    upTxt:SetTextColor(0.5, 0.55, 0.6)
    scrollUpBtn:SetScript("OnEnter", function() upTxt:SetTextColor(0.9, 0.85, 1) end)
    scrollUpBtn:SetScript("OnLeave", function() upTxt:SetTextColor(0.5, 0.55, 0.6) end)
    scrollUpBtn:SetScript("OnClick", function()
        if SFrames.Chat.chatFrame then
            SFrames.Chat.chatFrame:ScrollToTop()
        end
    end)
    
    local scrollDownBtn = CreateFrame("Button", nil, f)
    scrollDownBtn:SetWidth(16)
    scrollDownBtn:SetHeight(16)
    scrollDownBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 20)
    scrollDownBtn:SetFrameStrata("HIGH")
    local dnTxt = scrollDownBtn:CreateFontString(nil, "OVERLAY")
    dnTxt:SetFont(fontPath, 11, "OUTLINE")
    dnTxt:SetPoint("CENTER", scrollDownBtn, "CENTER", 1, -1)
    dnTxt:SetText("▼")
    dnTxt:SetTextColor(0.5, 0.55, 0.6)
    scrollDownBtn:SetScript("OnEnter", function() dnTxt:SetTextColor(0.9, 0.85, 1) end)
    scrollDownBtn:SetScript("OnLeave", function() dnTxt:SetTextColor(0.5, 0.55, 0.6) end)
    scrollDownBtn:SetScript("OnClick", function()
        if SFrames.Chat.chatFrame then
            SFrames.Chat.chatFrame:ScrollToBottom()
        end
    end)
    
    local scrollTrack = f:CreateTexture(nil, "BACKGROUND")
    scrollTrack:SetTexture("Interface\\Buttons\\WHITE8X8")
    scrollTrack:SetPoint("TOP", scrollUpBtn, "BOTTOM", 0, -2)
    scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
    scrollTrack:SetWidth(4)
    scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9)

    local resize = CreateFrame("Button", nil, f)
    resize:SetWidth(16)
    resize:SetHeight(16)
    resize:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
    resize:RegisterForDrag("LeftButton")
    resize:EnableMouse(true)
    resize:SetFrameStrata("HIGH")

    local resizeTex = resize:CreateTexture(nil, "ARTWORK")
    resizeTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    resizeTex:SetAllPoints(resize)
    resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92)

    resize:SetScript("OnEnter", function()
        resizeTex:SetVertexColor(1, 0.86, 0.94, 1)
    end)
    resize:SetScript("OnLeave", function()
        resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92)
    end)
    resize:SetScript("OnMouseDown", function()
        if not (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) then return end
        f:StartSizing("BOTTOMRIGHT")
    end)
    resize:SetScript("OnMouseUp", function()
        f:StopMovingOrSizing()
        if SFrames and SFrames.Chat then
            SFrames.Chat:SaveSizeFromFrame()
            SFrames.Chat:ApplyConfig()
        end
    end)

    f.resizeHandle = resize
    self.frame = f

    if not self.hiddenConfigButton then
        local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
        hiddenConfigButton:SetWidth(74)
        hiddenConfigButton:SetHeight(22)
        hiddenConfigButton:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 14, 132)
        hiddenConfigButton:SetFrameStrata("DIALOG")
        hiddenConfigButton:SetFrameLevel(220)
        hiddenConfigButton:SetText("聊天设置")
        hiddenConfigButton:SetScript("OnClick", function()
            if SFrames and SFrames.Chat then
                SFrames.Chat:ToggleConfigFrame()
            end
        end)
        hiddenConfigButton:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT")
            GameTooltip:ClearLines()
            GameTooltip:AddLine("聊天设置", 1, 0.84, 0.94)
            GameTooltip:AddLine("聊天界面隐藏时显示此按钮", 0.86, 0.86, 0.86)
            GameTooltip:AddLine("点击打开 Nanami 聊天配置", 0.86, 0.86, 0.86)
            GameTooltip:Show()
        end)
        hiddenConfigButton:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        StyleCfgButton(hiddenConfigButton)
        hiddenConfigButton:Hide()
        self.hiddenConfigButton = hiddenConfigButton
    end

    local db = EnsureDB()
    local saved = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ChatFrame"]
    if saved then
        f:ClearAllPoints()
        f:SetPoint(saved.point, UIParent, saved.relativePoint, saved.xOfs, saved.yOfs)
    else
        f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
    end
    f:SetWidth(Clamp(db.width, 320, 900))
    f:SetHeight(Clamp(db.height, 120, 460))

    -- Background alpha: always show at configured bgAlpha
    local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
    f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
end

function SFrames.Chat:HideDefaultChrome()
    for _, objectName in ipairs(HIDDEN_OBJECTS) do
        ForceHide(_G[objectName])
    end
end

function SFrames.Chat:HideTabChrome()
    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7

    for i = 1, maxWindows do
        local prefix = "ChatFrame" .. i
        local elements = {
            prefix .. "Tab",
            prefix .. "ButtonFrame",
            prefix .. "UpButton",
            prefix .. "DownButton",
            prefix .. "BottomButton",
            prefix .. "TabLeft",
            prefix .. "TabMiddle",
            prefix .. "TabRight",
            prefix .. "TabSelectedLeft",
            prefix .. "TabSelectedMiddle",
            prefix .. "TabSelectedRight",
            prefix .. "TabHighlightLeft",
            prefix .. "TabHighlightMiddle",
            prefix .. "TabHighlightRight",
            prefix .. "TabFlash",
            prefix .. "TabGlow",
        }

        for _, objectName in ipairs(elements) do
            ForceHide(_G[objectName])
        end

        local text = _G[prefix .. "TabText"]
        if text then
            ForceInvisible(text)
            text.Show = Dummy
        end
    end

    local dock = _G["GeneralDockManager"]
    if dock then
        if dock.SetAlpha then dock:SetAlpha(0) end
        if dock.EnableMouse then dock:EnableMouse(false) end
    end
end

function SFrames.Chat:IsManagedChatWindow(chatFrame)
    if not chatFrame then return false end
    local name = chatFrame.GetName and chatFrame:GetName()
    if type(name) ~= "string" then return false end
    if not string.find(name, "^ChatFrame%d+$") then return false end
    if self.frame and self.frame.inner and chatFrame.GetParent and chatFrame:GetParent() == self.frame.inner then
        return true
    end
    if self.GetTabIndexForChatFrame and self:GetTabIndexForChatFrame(chatFrame) then
        return true
    end
    return false
end

function SFrames.Chat:HookWindowDragBlocker()
    if self.dragBlockerHooked then return end
    
    local function ResolveObjectToChatFrame(object)
        local probe = object
        local steps = 0
        while probe and steps < 10 do
            if probe.chatFrame then
                probe = probe.chatFrame
            end

            if probe and probe.GetName then
                local name = probe:GetName()
                if type(name) == "string" then
                    local _, _, idx = string.find(name, "^ChatFrame(%d+)Tab$")
                    if idx then
                        local cf = _G["ChatFrame" .. idx]
                        if cf then
                            return cf
                        end
                    end
                    if string.find(name, "^ChatFrame%d+$") then
                        return probe
                    end
                end
            end

            if not (probe and probe.GetParent) then
                break
            end
            probe = probe:GetParent()
            steps = steps + 1
        end
        return nil
    end

    local function ResolveTarget(frame)
        local direct = ResolveObjectToChatFrame(frame)
        if direct then return direct end
        return ResolveObjectToChatFrame(this)
    end

    local function Wrap(name)
        local orig = _G[name]
        if type(orig) ~= "function" then return false end
        _G[name] = function(frame, a, b, c, d, e)
            local target = ResolveTarget(frame)
            if SFrames and SFrames.Chat and SFrames.Chat.IsManagedChatWindow and SFrames.Chat:IsManagedChatWindow(target) then
                if target and target.StopMovingOrSizing then
                    target:StopMovingOrSizing()
                end
                return
            end
            return orig(frame, a, b, c, d, e)
        end
        return true
    end

    local hookedAny = false
    if Wrap("FCF_StartWindowDrag") then hookedAny = true end
    if Wrap("FCF_StartMoving") then hookedAny = true end
    if Wrap("FCF_StartDrag") then hookedAny = true end
    if Wrap("FloatingChatFrame_OnMouseDown") then hookedAny = true end

    if hookedAny then
        self.dragBlockerHooked = true
    end
end

function SFrames.Chat:EnforceChatWindowLock(chatFrame)
    if not chatFrame then return end
    if FCF_SetLocked then
        pcall(function() FCF_SetLocked(chatFrame, 1) end)
    end
    if FCF_SetWindowLocked then
        pcall(function() FCF_SetWindowLocked(chatFrame, 1) end)
    end
    if chatFrame.SetMovable then
        chatFrame:SetMovable(false)
    end
    if not chatFrame.sfRegisterForDragBlocked and chatFrame.RegisterForDrag then
        pcall(function() chatFrame:RegisterForDrag("Button4") end)
        chatFrame.sfRegisterForDragBlocked = true
        chatFrame.sfOriginalRegisterForDrag = chatFrame.RegisterForDrag
        chatFrame.RegisterForDrag = function() end
    end
    if not chatFrame.sfStartMovingBlocked and chatFrame.StartMoving then
        chatFrame.sfStartMovingBlocked = true
        chatFrame.StartMoving = function(frame)
            if frame and frame.StopMovingOrSizing then
                frame:StopMovingOrSizing()
            end
        end
    end
    if not chatFrame.sfStartSizingBlocked and chatFrame.StartSizing then
        chatFrame.sfStartSizingBlocked = true
        chatFrame.StartSizing = function(frame)
            if frame and frame.StopMovingOrSizing then
                frame:StopMovingOrSizing()
            end
        end
    end
    if chatFrame.SetScript then
        chatFrame:SetScript("OnMouseDown", function()
            if this and this.StopMovingOrSizing then
                this:StopMovingOrSizing()
            end
            if (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) and SFrames.Chat and SFrames.Chat.frame then
                SFrames.Chat.frame:StartMoving()
                SFrames.Chat._contentDragging = true
            end
        end)
        chatFrame:SetScript("OnMouseUp", function()
            if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then
                SFrames.Chat.frame:StopMovingOrSizing()
                SFrames.Chat:SavePosition()
                SFrames.Chat._contentDragging = nil
            end
            if this and this.StopMovingOrSizing then
                this:StopMovingOrSizing()
            end
        end)
        chatFrame:SetScript("OnDragStart", function()
            if this and this.StopMovingOrSizing then
                this:StopMovingOrSizing()
            end
        end)
        chatFrame:SetScript("OnDragStop", function()
            if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then
                SFrames.Chat.frame:StopMovingOrSizing()
                SFrames.Chat:SavePosition()
                SFrames.Chat._contentDragging = nil
            end
            if this and this.StopMovingOrSizing then
                this:StopMovingOrSizing()
            end
        end)
    end
    if chatFrame.SetUserPlaced then
        chatFrame:SetUserPlaced(false)
    end
    if chatFrame.StopMovingOrSizing then
        chatFrame:StopMovingOrSizing()
    end
end

function SFrames.Chat:StartPositionEnforcer()
    if self._positionEnforcerRunning then return end
    self._positionEnforcerRunning = true
    if not self._positionEnforcer then
        self._positionEnforcer = CreateFrame("Frame")
    end
    local enforcer = self._positionEnforcer
    enforcer.elapsed = 0
    enforcer:SetScript("OnUpdate", function()
        this.elapsed = this.elapsed + arg1
        if this.elapsed < 0.3 then return end
        this.elapsed = 0
        if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.inner and SFrames.Chat.frame:IsShown()) then
            return
        end
        local inner = SFrames.Chat.frame.inner
        local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
        for i = 1, maxWindows do
            local cf = _G["ChatFrame" .. tostring(i)]
            if cf and SFrames.Chat:IsManagedChatWindow(cf) then
                local needsFix = false
                if cf.GetParent and cf:GetParent() ~= inner then
                    cf:SetParent(inner)
                    needsFix = true
                end
                if cf.GetPoint then
                    local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
                    if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or (xOfs and xOfs ~= 0) or (yOfs and yOfs ~= 0) then
                        needsFix = true
                    end
                    if cf.GetNumPoints and cf:GetNumPoints() ~= 1 then
                        needsFix = true
                    end
                end
                if needsFix then
                    cf:ClearAllPoints()
                    cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0)
                    SFrames.Chat:EnforceChatWindowLock(cf)
                    SFrames.Chat:RefreshChatBounds()
                end
            end
        end
    end)
end

function SFrames.Chat:RefreshChatBounds()
    if not (self.chatFrame and self.frame and self.frame.inner) then return end

    -- In WoW 1.12, GetWidth/GetHeight can return 0 for frames sized entirely by anchors.
    -- ChatFrame internal scrolling breaks if its explicit size is 0, causing it to render
    -- only 1 line at the bottom. We manually calculate the intended size.
    local cfg = self:GetConfig()
    local parentWidth = self.frame:GetWidth() or cfg.width or 400
    local parentHeight = self.frame:GetHeight() or cfg.height or 200

    local width = parentWidth - (cfg.sidePadding * 2)
    local height = parentHeight - cfg.topPadding - cfg.bottomPadding

    if width < 80 or height < 10 then return end

    self.chatFrame:SetWidth(width + 1)
    self.chatFrame:SetWidth(width)
    self.chatFrame:SetHeight(height + 1)
    self.chatFrame:SetHeight(height)
    if self.chatFrame.UpdateScrollRegion then
        pcall(function() self.chatFrame:UpdateScrollRegion() end)
    end
end

function SFrames.Chat:StartStabilizer()
    if not self.stabilizer then
        self.stabilizer = CreateFrame("Frame")
    end

    local stabilize = self.stabilizer
    stabilize.elapsed = 0
    stabilize.total = 0

    stabilize:SetScript("OnUpdate", function()
        this.elapsed = this.elapsed + arg1
        this.total = this.total + arg1

        if this.elapsed >= 0.12 then
            this.elapsed = 0
            if SFrames and SFrames.Chat then
                SFrames.Chat:HideDefaultChrome()
                SFrames.Chat:HideTabChrome()
                SFrames.Chat:RefreshTabButtons()
                -- Only refresh bounds if anchors actually drifted (avoids +1/-1 flicker)
                local chat = SFrames.Chat
                if chat.chatFrame and chat.frame and chat.frame.inner then
                    local inner = chat.frame.inner
                    local point, relativeTo = chat.chatFrame:GetPoint(1)
                    if point ~= "TOPLEFT" or relativeTo ~= inner then
                        chat:ReanchorChatFrames()
                        chat:RefreshChatBounds()
                    end
                end
            end
        end

        if this.total >= 2 then
            this:SetScript("OnUpdate", nil)
        end
    end)
end

function SFrames.Chat:EnsureChromeWatcher()
    if not self.chromeWatcher then
        self.chromeWatcher = CreateFrame("Frame")
    end

    local watcher = self.chromeWatcher
    watcher.elapsed = 0
    watcher.remaining = math.max(watcher.remaining or 0, 2.5)
    if watcher.sfRunning then
        return
    end

    watcher.sfRunning = true
    watcher:SetScript("OnUpdate", function()
        this.elapsed = this.elapsed + arg1
        this.remaining = (this.remaining or 0) - arg1

        if this.elapsed < 0.25 then
            if this.remaining <= 0 then
                this.sfRunning = false
                this:SetScript("OnUpdate", nil)
            end
            return
        end
        this.elapsed = 0

        if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame:IsShown()) then
            if this.remaining <= 0 then
                this.sfRunning = false
                this:SetScript("OnUpdate", nil)
            end
            return
        end

        local didReanchor = false
        SFrames.Chat:HideDefaultChrome()
        SFrames.Chat:HideTabChrome()
        local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
        for i = 1, maxWindows do
            local cf = _G["ChatFrame" .. tostring(i)]
            if cf then
                SFrames.Chat:EnforceChatWindowLock(cf)
                if SFrames.Chat:IsManagedChatWindow(cf) and SFrames.Chat.frame and SFrames.Chat.frame.inner then
                    if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then
                        cf:SetParent(SFrames.Chat.frame.inner)
                        didReanchor = true
                    end
                    if cf.GetPoint then
                        local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
                        local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0)
                        if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then
                            cf:ClearAllPoints()
                            cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0)
                            cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0)
                            didReanchor = true
                        end
                    end
                end
            end
        end
        if SFrames.Chat.chatFrame and SFrames.Chat.frame and SFrames.Chat.frame.inner then
            local cf = SFrames.Chat.chatFrame
            if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then
                cf:SetParent(SFrames.Chat.frame.inner)
                didReanchor = true
            end
            if cf.GetPoint then
                local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
                local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0)
                if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then
                    cf:ClearAllPoints()
                    cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0)
                    cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0)
                    didReanchor = true
                end
            end
        end

        if didReanchor then
            SFrames.Chat:RefreshChatBounds()
        end

        if this.remaining <= 0 then
            this.sfRunning = false
            this:SetScript("OnUpdate", nil)
        end
    end)
end

function SFrames.Chat:ReanchorChatFrames()
    if not (self.frame and self.frame.inner) then return end
    local inner = self.frame.inner
    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    for i = 1, maxWindows do
        local cf = _G["ChatFrame" .. tostring(i)]
        if cf and self:IsManagedChatWindow(cf) then
            if cf.GetParent and cf:GetParent() ~= inner then
                cf:SetParent(inner)
            end
            local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
            if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0 then
                cf:ClearAllPoints()
                cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0)
                cf:SetPoint("BOTTOMRIGHT", inner, "BOTTOMRIGHT", 0, 0)
            end
        end
    end
    self:HideDefaultChrome()
    self:HideTabChrome()
end

function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
    if not chatFrame then return end
    if chatFrame.SetFrameStrata then
        chatFrame:SetFrameStrata("MEDIUM")
    end
    if chatFrame.SetFrameLevel and self.frame and self.frame.inner then
        chatFrame:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6)
    end
    if chatFrame.SetAlpha then chatFrame:SetAlpha(1) end
    chatFrame:SetFading(false)
    if chatFrame.SetTimeVisible then
        chatFrame:SetTimeVisible(999999)
    end
    if chatFrame.EnableMouseWheel then
        chatFrame:EnableMouseWheel(true)
        if not chatFrame.sfScrollHooked then
            chatFrame.sfScrollHooked = true
            chatFrame:SetScript("OnMouseWheel", function()
                if arg1 > 0 then
                    if IsShiftKeyDown() then
                        this:ScrollToTop()
                    else
                        this:ScrollUp()
                        this:ScrollUp()
                        this:ScrollUp()
                    end
                elseif arg1 < 0 then
                    if IsShiftKeyDown() then
                        this:ScrollToBottom()
                    else
                        this:ScrollDown()
                        this:ScrollDown()
                        this:ScrollDown()
                    end
                end
            end)
        end
    end
    chatFrame:SetJustifyH("LEFT")
    if chatFrame.SetSpacing then chatFrame:SetSpacing(1) end
    if chatFrame.SetMaxLines then
        chatFrame:SetMaxLines((isCombat and 4096) or 1024)
    end
    if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
    if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
    if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end
    if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end

    self:EnforceChatWindowLock(chatFrame)
    if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
        chatFrame.sfDragLockHooked = true
        chatFrame:HookScript("OnDragStart", function()
            chatFrame:StopMovingOrSizing()
        end)
        chatFrame:HookScript("OnDragStop", function()
            chatFrame:StopMovingOrSizing()
        end)
    end

    -- Force disable fading completely
    chatFrame:SetFading(false)
    if chatFrame.SetTimeVisible then
        chatFrame:SetTimeVisible(999999)
    end
    if not chatFrame.sfFadingHooked then
        chatFrame.sfFadingHooked = true
        chatFrame.SetFading = function() end
        chatFrame.SetTimeVisible = function() end
    end
end

function SFrames.Chat:GetChatFrameForTab(tab)
    if not tab then return ChatFrame1, false end
    if tab.kind == "combat" and ChatFrame2 then
        return ChatFrame2, true
    end
    
    local index = 1
    local db = EnsureDB()
    for i=1, table.getn(db.tabs) do
        if db.tabs[i] == tab then
            index = i
            break
        end
    end
    
    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    if index > maxWindows then index = maxWindows end
    
    local cfName = "ChatFrame" .. tostring(index)
    local cf = _G[cfName]
    
    if not cf then
        return ChatFrame1, false
    end
    
    return cf, false
end

function SFrames.Chat:EnsureCombatLogFrame()
    if not ChatFrame2 then return end

    if (not self.combatLogAddonLoaded) and LoadAddOn and IsAddOnLoaded then
        if not IsAddOnLoaded("Blizzard_CombatLog") then
            local loadable = false
            if GetAddOnInfo then
                local name, _, _, enabled, lod, reason = GetAddOnInfo("Blizzard_CombatLog")
                loadable = name and name ~= "" and reason ~= "MISSING" and reason ~= "DISABLED"
            end
            if loadable then
                local ok = pcall(LoadAddOn, "Blizzard_CombatLog")
                if not ok then
                    DEFAULT_CHAT_FRAME:AddMessage("|cffff9900Nanami-UI:|r Blizzard_CombatLog not available, skipping.")
                end
            end
        end
        self.combatLogAddonLoaded = true
    end

    if FCF_SetCombatLog then
        pcall(function()
            FCF_SetCombatLog(ChatFrame2)
        end)
    end

    if FCF_SetWindowName then
        pcall(function()
            FCF_SetWindowName(ChatFrame2, DEFAULT_COMBAT_TAB_NAME)
        end)
    end

    if ChatFrame2.SetFading then ChatFrame2:SetFading(false) end
    if ChatFrame2.SetMaxLines then ChatFrame2:SetMaxLines(4096) end
    self.combatLogPrepared = true
end

function SFrames.Chat:SetupChatFrameForTab(index)
    local tab = self:GetTab(index)
    if not tab then return end
    if not (self.frame and self.frame.inner) then return end

    local chatFrame, isCombat = self:GetChatFrameForTab(tab)
    if not chatFrame then return end
    if isCombat then
        self:EnsureCombatLogFrame()
    end

    if not self.chatUndockedFrames then
        self.chatUndockedFrames = {}
    end
    if not self.chatFrameToTabIndex then
        self.chatFrameToTabIndex = {}
    end
    local frameID = chatFrame.GetID and chatFrame:GetID() or 1
    if not self.chatUndockedFrames[frameID] and FCF_UnDockFrame then
        pcall(function() FCF_UnDockFrame(chatFrame) end)
        self.chatUndockedFrames[frameID] = true
    end
    self.chatFrameToTabIndex[chatFrame] = index

    chatFrame:ClearAllPoints()
    chatFrame:SetParent(self.frame.inner)
    chatFrame:SetPoint("TOPLEFT", self.frame.inner, "TOPLEFT", 0, 0)

    local cfg = self:GetConfig()
    local width = (self.frame:GetWidth() or cfg.width or 400) - (cfg.sidePadding * 2)
    local height = (self.frame:GetHeight() or cfg.height or 180) - cfg.topPadding - cfg.bottomPadding
    if width < 80 then width = 80 end
    if height < 10 then height = 10 end
    chatFrame:SetWidth(width)
    chatFrame:SetHeight(height)

    self:ApplyChatFrameBaseStyle(chatFrame, isCombat)
    if chatFrame.SetUserPlaced then chatFrame:SetUserPlaced(false) end
    if chatFrame.SetMaxLines then chatFrame:SetMaxLines((isCombat and 4096) or 1024) end
    if chatFrame.UpdateScrollRegion then pcall(function() chatFrame:UpdateScrollRegion() end) end
end

function SFrames.Chat:SwitchActiveChatFrame(tab)
    if not (self.frame and self.frame.inner) then return end

    local activeChatFrame, isCombat = self:GetChatFrameForTab(tab)
    if not activeChatFrame then return end

    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    for i = 1, maxWindows do
        local cf = _G["ChatFrame"..tostring(i)]
        if cf then
            self:EnforceChatWindowLock(cf)
            if cf == activeChatFrame then
                if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end
                cf.fadeInfo = nil
                if cf.SetAlpha then cf:SetAlpha(1) end
                if cf.EnableMouse then cf:EnableMouse(true) end
                if cf.SetFrameLevel and self.frame and self.frame.inner then
                    cf:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6)
                end
                cf:Show()
                self.chatFrame = cf
                self.chatFrameIsCombat = isCombat
                self:RefreshChatBounds()
            else
                if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end
                cf.fadeInfo = nil
                cf:Hide()
                if cf.SetAlpha then cf:SetAlpha(0) end
                if cf.EnableMouse then cf:EnableMouse(false) end
                if cf.SetFrameLevel and self.frame and self.frame.inner then
                    cf:SetFrameLevel(self.frame.inner:GetFrameLevel() or 1)
                end
            end
        end
    end
end

function SFrames.Chat:AttachChatFrame()
    if not (self.frame and self.frame.inner and ChatFrame1) then return end
    self:SwitchActiveChatFrame(self:GetActiveTab())
end

function SFrames.Chat:RestoreCachedMessages(targetChatFrame)
    if not targetChatFrame then return end
    local cache = EnsureDB().messageCache
    if not cache or table.getn(cache) == 0 then return end
    
    -- Find which frame index this is
    local targetIdx = nil
    for fi = 1, 7 do
        if targetChatFrame == _G["ChatFrame" .. fi] then
            targetIdx = fi
            break
        end
    end
    
    local origAM = targetChatFrame.origAddMessage or targetChatFrame.AddMessage
    if not origAM then return end
    
    for idx = 1, table.getn(cache) do
        local entry = cache[idx]
        if entry and entry.text then
            local frameIdx = entry.frame or 1
            -- Only restore messages that belong to this frame
            if frameIdx == targetIdx then
                local msgID = entry.id or 0
                if not SFrames.Chat.MessageHistory then
                    SFrames.Chat.MessageHistory = {}
                end
                SFrames.Chat.MessageHistory[msgID] = entry.text
                local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text
                origAM(targetChatFrame, modified, entry.r, entry.g, entry.b)
            end
        end
    end
end

function SFrames.Chat:ApplyAllTabsSetup()
    local db = EnsureDB()
    self.chatFrameToTabIndex = {}
    for i = 1, table.getn(db.tabs) do
        self:SetupChatFrameForTab(i)
        self:ApplyTabFilters(i)
        self:ApplyTabChannels(i)
    end
    -- Restore cached messages after filters cleared the frames
    for i = 1, table.getn(db.tabs) do
        local tab = db.tabs[i]
        if tab then
            local chatFrame = self:GetChatFrameForTab(tab)
            if chatFrame then
                self:RestoreCachedMessages(chatFrame)
            end
        end
    end
    self.cacheRestorePrimed = true
    self:SwitchActiveChatFrame(self:GetActiveTab())
end

function SFrames.Chat:ApplyTabFilters(index)
    local tab, idx = self:GetTab(index)
    if not tab then return end

    local chatFrame, isCombat = self:GetChatFrameForTab(tab)
    if not chatFrame then return end

    if tab.kind == "combat" and isCombat then
        return
    end
    if not tab.filters then return end

    for _, group in ipairs(ALL_MESSAGE_GROUPS) do
        pcall(function()
            ChatFrame_RemoveMessageGroup(chatFrame, group)
        end)
    end

    local applied = {}
    local anyEnabled = false
    for _, def in ipairs(FILTER_DEFS) do
        if tab.filters[def.key] ~= false then
            anyEnabled = true
            local groups = FILTER_GROUPS[def.key] or {}
            for _, group in ipairs(groups) do
                if not applied[group] then
                    applied[group] = true
                    pcall(function()
                        ChatFrame_AddMessageGroup(chatFrame, group)
                    end)
                end
            end
        end
    end

    if not anyEnabled then
        tab.filters = CopyTable(DEFAULT_FILTERS)
        applied = {}
        for _, def in ipairs(FILTER_DEFS) do
            if tab.filters[def.key] ~= false then
                local groups = FILTER_GROUPS[def.key] or {}
                for _, group in ipairs(groups) do
                    if not applied[group] then
                        applied[group] = true
                        pcall(function()
                            ChatFrame_AddMessageGroup(chatFrame, group)
                        end)
                    end
                end
            end
        end
        self:NotifyConfigUI()
    end
end

function SFrames.Chat:ApplyTabChannels(index)
    local tab, idx = self:GetTab(index)
    if not tab then return end

    local chatFrame, isCombat = self:GetChatFrameForTab(tab)
    if not chatFrame then return end
    if tab.kind == "combat" and isCombat then return end

    local channels = self:GetJoinedChannels()
    self:ApplyBlockedChannelsGlobally(channels)
    local addSupported = (ChatFrame_AddChannel ~= nil)
    local removeSupported = (ChatFrame_RemoveChannel ~= nil)
    if not (addSupported or removeSupported) then return end
    if table.getn(channels) == 0 then return end

    for i = 1, table.getn(channels) do
        local name = channels[i].name
        local enabled = self:GetTabChannelFilter(idx, name)
        if enabled then
            if addSupported then
                pcall(function()
                    ChatFrame_AddChannel(chatFrame, name)
                end)
            end
        else
            if removeSupported then
                pcall(function()
                    ChatFrame_RemoveChannel(chatFrame, name)
                end)
            end
        end
    end
end

function SFrames.Chat:ApplyAllTabChannels()
    local db = EnsureDB()
    for i = 1, table.getn(db.tabs) do
        self:ApplyTabChannels(i)
    end
end

function SFrames.Chat:ApplyBlockedChannelsGlobally(channels)
    if not ChatFrame_RemoveChannel then return end
    channels = channels or self:GetJoinedChannels()
    if table.getn(channels) == 0 then return end

    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    local db = EnsureDB()
    for i = 1, table.getn(channels) do
        local info = channels[i]
        local name = info and info.name
        if IsIgnoredChannelByDefault(name) then
            local anyTabEnabled = false
            for t = 1, table.getn(db.tabs) do
                if self:GetTabChannelFilter(t, name) then
                    anyTabEnabled = true
                    break
                end
            end
            if not anyTabEnabled then
                for w = 1, maxWindows do
                    local frame = _G["ChatFrame" .. tostring(w)]
                    if frame then
                        pcall(function()
                            ChatFrame_RemoveChannel(frame, name)
                        end)
                    end
                end
            end
        end
    end
end

function SFrames.Chat:EnsureChannelMessageFilter()
    return
end

function SFrames.Chat:EnsureAddMessageChannelFilter()
    return
end

function SFrames.Chat:StartBlockedChannelWatcher()
    local watcher = self.blockedChannelWatcher
    if watcher and type(watcher) ~= "boolean" and watcher.SetScript then
        watcher:SetScript("OnUpdate", nil)
    end
    self.blockedChannelWatcher = true
end

function SFrames.Chat:RefreshTabButtons()
    if not (self.frame and self.frame.tabBar) then return end

    local db = EnsureDB()
    local tabs = db.tabs
    local count = table.getn(tabs)
    local activeIndex = self:GetActiveTabIndex()
    local cfg = self:GetConfig()
    local showBorder = (cfg.showBorder ~= false)
    local borderR, borderG, borderB = self:GetBorderColorRGB()
    local inactiveBorderA = showBorder and 0.72 or 0
    local activeBorderA = showBorder and 0.95 or 0
    local addBorderA = showBorder and 0.9 or 0

    if not self.tabButtons then self.tabButtons = {} end
    for i = 1, table.getn(self.tabButtons) do
        self.tabButtons[i]:Hide()
    end

    local function EnsureButtonSkin(btn)
        if btn.sfSkinned then return end

        btn:SetBackdrop({
            bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 12,
            insets = { left = 2, right = 2, top = 2, bottom = 2 },
        })

        if btn.SetBackdropColor then
            btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92)
        end
        if btn.SetBackdropBorderColor then
            btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA)
        end

        local bg = btn:CreateTexture(nil, "BACKGROUND")
        bg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
        bg:SetPoint("TOPLEFT", btn, "TOPLEFT", 3, -3)
        bg:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -3, 3)
        bg:SetVertexColor(1, 0.62, 0.84, 0.12)
        btn.sfBg = bg

        local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
        local label = btn:CreateFontString(nil, "OVERLAY")
        label:SetFont(fontPath, 9, "OUTLINE")
        label:SetPoint("CENTER", btn, "CENTER", 0, 0)
        label:SetTextColor(0.92, 0.84, 0.9)
        btn.sfText = label

        btn.sfSkinned = true
    end

    if not self.addTabButton then
        local addBtn = CreateFrame("Button", nil, self.frame.tabBar)
        addBtn:SetHeight(20)
        addBtn:SetWidth(28)
        addBtn:RegisterForClicks("LeftButtonUp")
        EnsureButtonSkin(addBtn)
        addBtn.sfText:SetText("+")
        addBtn:SetScript("OnClick", function()
            if SFrames and SFrames.Chat then
                SFrames.Chat:PromptNewTab()
            end
        end)
        addBtn:SetScript("OnMouseDown", function()
            if this.sfBg then this.sfBg:SetVertexColor(1, 0.4, 0.7, 0.6) end
            if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnDownBg[1], CFG_THEME.btnDownBg[2], CFG_THEME.btnDownBg[3], 0.96) end
        end)
        addBtn:SetScript("OnMouseUp", function()
            if MouseIsOver and MouseIsOver(this) then
                if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end
                if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end
            else
                if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
                if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
            end
        end)
        addBtn:SetScript("OnEnter", function()
            if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end
            if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            GameTooltip:ClearLines()
            GameTooltip:AddLine("新建标签", 1, 0.84, 0.94)
            GameTooltip:AddLine("创建聊天标签页", 0.85, 0.85, 0.85)
            GameTooltip:Show()
        end)
        addBtn:SetScript("OnLeave", function()
            if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
            if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
            GameTooltip:Hide()
        end)
        self.addTabButton = addBtn
    else
        EnsureButtonSkin(self.addTabButton)
        if self.addTabButton.sfText then self.addTabButton.sfText:SetText("+") end
    end

    local gap = 3
    local addWidth = 28
    local barWidth = self.frame.tabBar:GetWidth() or 0
    if barWidth <= 0 then
        barWidth = (self.frame:GetWidth() or DEFAULTS.width) - 180
    end
    local available = barWidth - addWidth - gap * count - 2
    if available < 20 then available = 20 end
    local buttonWidth = math.floor(available / math.max(1, count))
    buttonWidth = Clamp(buttonWidth, 14, 96)
    local maxChars = math.max(3, math.floor((buttonWidth - 8) / 5))

    local x = 0
    for i = 1, count do
        local tab = tabs[i]
        local btn = self.tabButtons[i]
        if not btn then
            btn = CreateFrame("Button", nil, self.frame.tabBar)
            btn:SetHeight(18)
            btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
            self.tabButtons[i] = btn
        end

        EnsureButtonSkin(btn)

        btn:ClearAllPoints()
        btn:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0)
        btn:SetWidth(buttonWidth)
        if btn.sfText then
            btn.sfText:SetText(ShortText(tab.name or ("标签" .. tostring(i)), maxChars))
        end

        local idx = i
        btn:SetScript("OnClick", function()
            if not (SFrames and SFrames.Chat) then return end
            if arg1 == "RightButton" then
                SFrames.Chat:OpenTabContextMenu(idx)
            else
                SFrames.Chat:SetActiveTab(idx)
            end
        end)

        btn:SetScript("OnEnter", function()
            if idx ~= activeIndex then
                if this.sfBg then this.sfBg:SetVertexColor(1, 0.68, 0.89, 0.25) end
                if this.sfText then this.sfText:SetTextColor(0.92, 0.84, 0.92) end
                if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92) end
                this:SetAlpha(0.96)
            end
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            GameTooltip:ClearLines()
            GameTooltip:AddLine(tab.name or ("标签" .. tostring(idx)), 1, 0.84, 0.94)
            GameTooltip:AddLine("左键: 切换标签", 0.82, 0.82, 0.82)
            GameTooltip:AddLine("右键: 打开菜单", 1, 0.68, 0.79)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function()
            if idx ~= activeIndex then
                if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end
                if this.sfText then this.sfText:SetTextColor(0.72, 0.64, 0.72) end
                if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end
                this:SetAlpha(0.82)
            end
            GameTooltip:Hide()
        end)

        if idx == activeIndex then
            local aBg = CFG_THEME.tabActiveBg or CFG_THEME.buttonActiveBg or CFG_THEME.btnBg
            local aBd = CFG_THEME.tabActiveBorder or CFG_THEME.buttonActiveBorder
            if btn.SetBackdropColor then btn:SetBackdropColor(aBg[1], aBg[2], aBg[3], 0.98) end
            if btn.SetBackdropBorderColor then
                if aBd then
                    btn:SetBackdropBorderColor(aBd[1], aBd[2], aBd[3], 1)
                else
                    btn:SetBackdropBorderColor(borderR, borderG, borderB, 1)
                end
            end
            if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.64, 0.86, 0.45) end
            if btn.sfText then btn.sfText:SetTextColor(1, 1, 1) end
            btn:SetAlpha(1)
        else
            if btn.SetBackdropColor then btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end
            if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA) end
            if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end
            if btn.sfText then btn.sfText:SetTextColor(0.72, 0.64, 0.72) end
            btn:SetAlpha(0.82)
        end

        btn:Show()
        x = x + buttonWidth + gap
    end

    self.addTabButton:ClearAllPoints()
    self.addTabButton:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0)
    if self.addTabButton.SetBackdropColor then self.addTabButton:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
    if self.addTabButton.SetBackdropBorderColor then self.addTabButton:SetBackdropBorderColor(borderR, borderG, borderB, addBorderA) end
    if self.addTabButton.sfBg then self.addTabButton.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
    if self.addTabButton.sfText then self.addTabButton.sfText:SetTextColor(1, 0.9, 0.98) end
    self.addTabButton:Show()
end



function SFrames.Chat:StyleChatFont()
    if not (SFrames and SFrames.GetFont) then return end
    local cfg = self:GetConfig()
    local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
    local fontPath = SFrames:GetFont()

    local styled = {}
    local function Apply(frame)
        if not (frame and frame.SetFont) then return end
        if styled[frame] then return end
        styled[frame] = true
        pcall(function()
            frame:SetFont(fontPath, cfg.fontSize, outline)
        end)
    end

    if ChatFontNormal and ChatFontNormal.SetFont then
        pcall(function()
            ChatFontNormal:SetFont(fontPath, cfg.fontSize, outline)
        end)
    end

    Apply(self.chatFrame)
    local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
    for i = 1, maxWindows do
        local cf = _G["ChatFrame"..tostring(i)]
        if cf then Apply(cf) end
    end
end

function SFrames.Chat:HookChatEditColoring()
    if ChatEdit_UpdateHeader and not self._colorHooked then
        local orig = ChatEdit_UpdateHeader
        ChatEdit_UpdateHeader = function(editBox)
            orig(editBox)
            
            -- Re-apply insets to prevent buttons overlapping text and header
            local header = _G[editBox:GetName().."Header"]
            local hw = (header and header:GetWidth()) or 0
            if editBox and editBox.SetTextInsets then
                -- Left: 20px (for cat icon) + header width + 5px padding
                -- Right: 45px (for right shortcut buttons)
                editBox:SetTextInsets(hw + 25, 45, 0, 0)
            end

            local ctype = editBox.chatType
            if not ctype and editBox.GetAttribute then
                ctype = editBox:GetAttribute("chatType")
            end
            
            local info = ChatTypeInfo[ctype]
            if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
                if info then
                    SFrames.Chat.editBackdrop:SetBackdropBorderColor(info.r, info.g, info.b, 1)
                    if SFrames.Chat.editBackdrop.topLine then
                            SFrames.Chat.editBackdrop.topLine:SetVertexColor(info.r, info.g, info.b, 0.85)
                    end
                    if SFrames.Chat.editBackdrop.catIcon then
                            SFrames.Chat.editBackdrop.catIcon:SetVertexColor(info.r, info.g, info.b, 0.9)
                    end
                else
                    -- Default coloring
                    local cfg = SFrames.Chat:GetConfig()
                    local r, g, b = SFrames.Chat:GetBorderColorRGB()
                    SFrames.Chat.editBackdrop:SetBackdropBorderColor(r, g, b, (cfg.showBorder ~= false) and 0.98 or 0)
                    if SFrames.Chat.editBackdrop.topLine then
                            SFrames.Chat.editBackdrop.topLine:SetVertexColor(r, g, b, 0.85)
                    end
                    if SFrames.Chat.editBackdrop.catIcon then
                            SFrames.Chat.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9)
                    end
                end
            end
        end
        self._colorHooked = true
    end
end

function SFrames.Chat:StyleEditBox()
    if not self.frame then return end

    local editBox = ChatFrameEditBox or ChatFrame1EditBox
    if not editBox then return end
    self.editBox = editBox

    local cfg = self:GetConfig()
    local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
    local fontPath = "Fonts\\ARIALN.TTF"
    if SFrames and SFrames.GetFont then
        local customFont = SFrames:GetFont()
        if type(customFont) == "string" and customFont ~= "" then
            fontPath = customFont
        end
    end
    local editText = _G[editBox:GetName() .. "Text"]

    local ename = editBox:GetName()
    if ename then
        local suffixes = {"Left", "Mid", "Middle", "Right", "FocusLeft", "FocusMid", "FocusMiddle", "FocusRight", "Focus"}
        for _, suf in ipairs(suffixes) do
            local tex = _G[ename .. suf]
            if tex then
                tex:SetTexture(nil)
                tex:Hide()
                tex.Show = Dummy
            end
        end
    end

    for _, texName in ipairs(EDITBOX_TEXTURES) do
        local tex = _G[texName]
        if tex then
            tex:SetTexture(nil)
            tex:Hide()
            tex.Show = Dummy
        end
    end

    if editBox.SetBackdrop then
        editBox:SetBackdrop(nil)
    end
    local regions = { editBox:GetRegions() }
    for _, region in ipairs(regions) do
        if region and region:GetObjectType() == "Texture" and region ~= editText then
            local layer = region:GetDrawLayer()
            if layer == "BACKGROUND" or layer == "BORDER" then
                region:SetTexture(nil)
                region:Hide()
            end
        end
    end

    -- We will anchor editBox slightly later inside editBackdrop, but we set its basic properties here.
    editBox:ClearAllPoints()
    editBox:SetHeight(20)
    if editBox.SetFrameStrata then editBox:SetFrameStrata("DIALOG") end
    if editBox.SetFrameLevel then editBox:SetFrameLevel((self.frame:GetFrameLevel() or 1) + 20) end
    if editBox.SetAltArrowKeyMode then editBox:SetAltArrowKeyMode(false) end
    -- Let WoW manage text insets via ChatEdit_UpdateHeader dynamically
    -- if editBox.SetTextInsets then editBox:SetTextInsets(20, 6, 0, 0) end
    if editBox.SetJustifyH then editBox:SetJustifyH("LEFT") end

    local header = _G[editBox:GetName() .. "Header"]
    if header then
        header:ClearAllPoints()
        header:SetPoint("LEFT", editBox, "LEFT", 20, 0)
        header:Show()
    end

    local function ApplyFontStyle(target)
        if not target then return end
        if target.SetFont then
            pcall(function()
                target:SetFont(fontPath, cfg.fontSize, outline)
            end)
        end
        if target.SetFontObject and ChatFontNormal then
            target:SetFontObject(ChatFontNormal)
        end
        if target.SetTextColor then target:SetTextColor(1, 1, 1) end
        if target.SetShadowColor then target:SetShadowColor(0, 0, 0, 1) end
        if target.SetShadowOffset then target:SetShadowOffset(1, -1) end
        if target.SetAlpha then target:SetAlpha(1) end
    end

    local function ApplyEditTextStyle()
        ApplyFontStyle(editBox)
        ApplyFontStyle(editText)
        if header then ApplyFontStyle(header) end
        if editText and editText.SetDrawLayer then editText:SetDrawLayer("OVERLAY", 7) end
        if editText and editText.SetParent then editText:SetParent(editBox) end
        if editText and editText.Show then editText:Show() end
    end
    ApplyEditTextStyle()

    if not self.editBackdrop then
        local bg = CreateFrame("Frame", "SFramesChatEditBackdrop", self.frame)
        bg:SetFrameStrata("DIALOG")
        bg:SetFrameLevel(editBox:GetFrameLevel() - 1)
        bg:SetBackdrop({
            bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 14,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        })
        bg:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96)
        bg:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98)

        local icon = SFrames:CreateIcon(bg, "logo", 13)
        icon:SetPoint("LEFT", bg, "LEFT", 4, 0)
        icon:SetVertexColor(1, 0.84, 0.94, 0.9)
        bg.catIcon = icon

        local line = bg:CreateTexture(nil, "OVERLAY")
        line:SetTexture("Interface\\Buttons\\WHITE8X8")
        line:SetPoint("TOPLEFT", bg, "TOPLEFT", 1, -1)
        line:SetPoint("TOPRIGHT", bg, "TOPRIGHT", -1, -1)
        line:SetHeight(1)
        line:SetVertexColor(1, 0.76, 0.9, 0.85)
        line:Hide()
        bg.topLine = line

        self.editBackdrop = bg
    end

    -- Add shortcut buttons inside the edit backdrop's right side.
    local buttonParent = self.editBackdrop or self.frame
    local btnLevel = (buttonParent:GetFrameLevel() or editBox:GetFrameLevel() or 1) + 3
    if not self.rollButton then
        local rb = CreateFrame("Button", "SFramesChatRollButton", buttonParent)
        rb:SetWidth(18)
        rb:SetHeight(18)
        rb:SetFrameStrata("DIALOG")
        rb:SetFrameLevel(btnLevel)
        
        local tex = rb:CreateTexture(nil, "ARTWORK")
        tex:SetTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Up")
        tex:SetTexCoord(0.06, 0.94, 0.06, 0.94)
        tex:SetAllPoints(rb)
        rb:SetNormalTexture(tex)
        
        rb:SetScript("OnClick", function() RandomRoll(1, 100) end)
        rb:SetScript("OnEnter", function() 
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            GameTooltip:SetText("掷骰子 (1-100)")
            GameTooltip:Show()
        end)
        rb:SetScript("OnLeave", function() GameTooltip:Hide() end)
        self.rollButton = rb
    end

    if not self.emoteButton then
        local eb = CreateFrame("Button", "SFramesChatEmoteButton", buttonParent)
        eb:SetWidth(18)
        eb:SetHeight(18)
        eb:SetFrameStrata("DIALOG")
        eb:SetFrameLevel(btnLevel)
        
        local tex = eb:CreateTexture(nil, "ARTWORK")
        tex:SetTexture("Interface\\Icons\\Ability_Warrior_BattleShout")
        tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        tex:SetAllPoints(eb)
        eb:SetNormalTexture(tex)
        
        eb:SetScript("OnClick", function()
            if not SFrames.Chat._emoteDropdown then
                local dd = CreateFrame("Frame", "SFramesChatEmoteDropdown", UIParent, "UIDropDownMenuTemplate")
                SFrames.Chat._emoteDropdown = dd
            end
            UIDropDownMenu_Initialize(SFrames.Chat._emoteDropdown, function()
                local emotes = {
                    { text = "/大笑",  cmd = "LAUGH"  },
                    { text = "/微笑",  cmd = "SMILE"  },
                    { text = "/挥手",  cmd = "WAVE"   },
                    { text = "/跳舞",  cmd = "DANCE"  },
                    { text = "/鞠躬",  cmd = "BOW"    },
                    { text = "/欢呼",  cmd = "CHEER"  },
                    { text = "/感谢",  cmd = "THANK"  },
                    { text = "/惊讶",  cmd = "GASP"   },
                    { text = "/眨眼",  cmd = "WINK"   },
                    { text = "/叹气",  cmd = "SIGH"   },
                    { text = "/哭泣",  cmd = "CRY"    },
                    { text = "/生气",  cmd = "ANGRY"  },
                    { text = "/飞吻",  cmd = "KISS"   },
                    { text = "/鼓掌",  cmd = "APPLAUD"},
                    { text = "/点头",  cmd = "YES"    },
                    { text = "/摇头",  cmd = "NO"     },
                    { text = "/强壮",  cmd = "FLEX"   },
                    { text = "/哭喊",  cmd = "WHINE"  },
                }
                for _, e in ipairs(emotes) do
                    local info = NewDropDownInfo()
                    info.text = e.text
                    info.notCheckable = 1
                    local cmd = e.cmd
                    info.func = function()
                        DoEmote(cmd)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end, "MENU")
            ToggleDropDownMenu(1, nil, SFrames.Chat._emoteDropdown, "SFramesChatEmoteButton", 0, 0)
        end)
        eb:SetScript("OnEnter", function() 
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            GameTooltip:SetText("表情动作")
            GameTooltip:Show()
        end)
        eb:SetScript("OnLeave", function() GameTooltip:Hide() end)
        self.emoteButton = eb
    end

    if not self.langButton then
        if RDbItems and RDbItems.TriStateToggle then
            local lb = CreateFrame("Button", "SFramesChatLangButton", buttonParent)
            lb:SetWidth(18)
            lb:SetHeight(18)
            lb:SetFrameStrata("DIALOG")
            lb:SetFrameLevel(btnLevel)

            local fs = lb:CreateFontString(nil, "ARTWORK")
            fs:SetFont(cfg.fontPath or "Fonts\\ARIALN.TTF", 11, "OUTLINE")
            fs:SetAllPoints(lb)
            fs:SetJustifyH("CENTER")
            fs:SetJustifyV("MIDDLE")
            lb.fontString = fs

            local function UpdateLangButtonText()
                local mode = RDbItemsCfg and RDbItemsCfg.mode
                if mode == "en" then
                    fs:SetText("E")
                    fs:SetTextColor(0.4, 0.8, 1.0)
                elseif mode == "cn" then
                    fs:SetText("中")
                    fs:SetTextColor(1.0, 0.82, 0.4)
                else
                    fs:SetText("--")
                    fs:SetTextColor(0.6, 0.6, 0.6)
                end
            end

            lb:SetScript("OnClick", function()
                RDbItems.TriStateToggle(this)
                UpdateLangButtonText()
            end)
            lb:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_TOP")
                local mode = RDbItemsCfg and RDbItemsCfg.mode
                if mode == "en" then
                    GameTooltip:SetText("物品链接语言: 英文")
                elseif mode == "cn" then
                    GameTooltip:SetText("物品链接语言: 中文")
                else
                    GameTooltip:SetText("物品链接语言: 自动")
                end
                GameTooltip:AddLine("点击切换 Shift+点击物品时的链接语言", 0.7, 0.7, 0.7)
                GameTooltip:Show()
            end)
            lb:SetScript("OnLeave", function() GameTooltip:Hide() end)

            lb:EnableMouse(true)
            local hl = lb:CreateTexture(nil, "HIGHLIGHT")
            hl:SetTexture("Interface\\Buttons\\WHITE8X8")
            hl:SetAllPoints(lb)
            hl:SetAlpha(0.15)

            UpdateLangButtonText()
            lb._UpdateText = UpdateLangButtonText
            self.langButton = lb
        end
    elseif self.langButton and self.langButton._UpdateText then
        self.langButton._UpdateText()
    end

    -- Re-parent in case buttons were created by an older version.
    self.rollButton:SetParent(self.editBackdrop)
    self.emoteButton:SetParent(self.editBackdrop)
    if self.rollButton.SetFrameLevel then
        self.rollButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
    end
    if self.emoteButton.SetFrameLevel then
        self.emoteButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
    end
    if self.langButton then
        self.langButton:SetParent(self.editBackdrop)
        if self.langButton.SetFrameLevel then
            self.langButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
        end
    end

    -- Re-anchor buttons every call so they track editBackdrop position
    self.rollButton:ClearAllPoints()
    self.rollButton:SetPoint("RIGHT", self.editBackdrop, "RIGHT", -4, 0)
    self.emoteButton:ClearAllPoints()
    self.emoteButton:SetPoint("RIGHT", self.rollButton, "LEFT", -2, 0)
    if self.langButton then
        self.langButton:ClearAllPoints()
        self.langButton:SetPoint("RIGHT", self.emoteButton, "LEFT", -2, 0)
    end

    local function SaveFreeEditBoxPosition()
        if EnsureDB().editBoxPosition ~= "free" then return end
        if not self.editBackdrop then return end
        local x = self.editBackdrop:GetLeft()
        local y = self.editBackdrop:GetBottom()
        if x and y then
            EnsureDB().editBoxX = x
            EnsureDB().editBoxY = y
        end
    end

    self.editBackdrop:ClearAllPoints()
    if cfg.editBoxPosition == "bottom" then
        self.editBackdrop:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", cfg.sidePadding - 4, -5)
        self.editBackdrop:SetPoint("TOPRIGHT", self.frame, "BOTTOMRIGHT", -(cfg.sidePadding - 4), -5)
    elseif cfg.editBoxPosition == "free" then
        self.editBackdrop:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cfg.editBoxX or 0, cfg.editBoxY or 200)
        self.editBackdrop:SetWidth(cfg.width - cfg.sidePadding * 2 + 8)
    else -- "top" or default
        self.editBackdrop:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 4, 5)
        self.editBackdrop:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cfg.sidePadding - 4), 5)
    end
    self.editBackdrop:SetHeight(26)
    
    self.editBackdrop:EnableMouse(true)
    self.editBackdrop:SetMovable(true)
    if self.editBackdrop.SetClampedToScreen then self.editBackdrop:SetClampedToScreen(true) end
    self.editBackdrop:RegisterForDrag("LeftButton")
    self.editBackdrop:SetScript("OnDragStart", function()
        if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" then
            this:StartMoving()
        end
    end)
    self.editBackdrop:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        SaveFreeEditBoxPosition()
    end)

    editBox:SetParent(self.editBackdrop)
    editBox:ClearAllPoints()
    editBox:SetPoint("TOPLEFT", self.editBackdrop, "TOPLEFT", 4, -3)
    local rightPad = self.langButton and -65 or -45
    editBox:SetPoint("BOTTOMRIGHT", self.editBackdrop, "BOTTOMRIGHT", rightPad, 3)
    editBox:EnableMouse(true)
    self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96)
    local borderR, borderG, borderB = self:GetBorderColorRGB()
    self.editBackdrop:SetBackdropBorderColor(borderR, borderG, borderB, (cfg.showBorder ~= false) and 0.98 or 0)
    if self.editBackdrop.topLine then
        self.editBackdrop.topLine:Hide()
    end

    if not editBox.sfNanamiHooked then
        local oldOnShow = editBox:GetScript("OnShow")
        local oldOnHide = editBox:GetScript("OnHide")
        local oldOnTextChanged = editBox:GetScript("OnTextChanged")

        editBox:SetScript("OnShow", function()
            if oldOnShow then oldOnShow() end
            ApplyEditTextStyle()
            if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:Show()
            end
        end)

        editBox:SetScript("OnHide", function()
            if oldOnHide then oldOnHide() end
            if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:Hide()
            end
        end)

        editBox:SetScript("OnTextChanged", function()
            if oldOnTextChanged then oldOnTextChanged() end
            ApplyEditTextStyle()
        end)

        local origShow = editBox.Show
        editBox.Show = function(self)
            if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:Show()
            end
            if origShow then origShow(self) end
        end

        local origHide = editBox.Hide
        editBox.Hide = function(self)
            if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:Hide()
            end
            if origHide then origHide(self) end
        end

        editBox.sfNanamiHooked = true
    end

    if not editBox.sfAltFreeDragHooked then
        local oldMouseDown = editBox:GetScript("OnMouseDown")
        local oldMouseUp = editBox:GetScript("OnMouseUp")
        local oldDragStart = editBox:GetScript("OnDragStart")
        local oldDragStop = editBox:GetScript("OnDragStop")

        editBox:RegisterForDrag("LeftButton")
        editBox:SetScript("OnMouseDown", function()
            if oldMouseDown then oldMouseDown() end
            if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then
                SFrames.Chat._editBoxAltDragging = true
                SFrames.Chat.editBackdrop:StartMoving()
            end
        end)
        editBox:SetScript("OnMouseUp", function()
            if oldMouseUp then oldMouseUp() end
            if SFrames.Chat and SFrames.Chat._editBoxAltDragging and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:StopMovingOrSizing()
                SFrames.Chat._editBoxAltDragging = nil
                SaveFreeEditBoxPosition()
            end
        end)
        editBox:SetScript("OnDragStart", function()
            if oldDragStart then oldDragStart() end
            if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then
                SFrames.Chat._editBoxAltDragging = true
                SFrames.Chat.editBackdrop:StartMoving()
            end
        end)
        editBox:SetScript("OnDragStop", function()
            if oldDragStop then oldDragStop() end
            if SFrames.Chat and SFrames.Chat.editBackdrop then
                SFrames.Chat.editBackdrop:StopMovingOrSizing()
                SFrames.Chat._editBoxAltDragging = nil
                SaveFreeEditBoxPosition()
            end
        end)
        editBox.sfAltFreeDragHooked = true
    end

    if editBox:IsShown() then
        self.editBackdrop:Show()
    else
        self.editBackdrop:Hide()
    end
end

function SFrames.Chat:ApplyFrameBorderStyle()
    if not self.frame then return end

    local cfg = self:GetConfig()
    local showBorder = (cfg.showBorder ~= false)
    local borderR, borderG, borderB = self:GetBorderColorRGB()
    if showBorder then
        local alpha = (SFrames and SFrames.isUnlocked) and 1 or 0.95
        self.frame:SetBackdropBorderColor(borderR, borderG, borderB, alpha)
    else
        self.frame:SetBackdropBorderColor(borderR, borderG, borderB, 0)
    end

    if self.frame.topLine then
        self.frame.topLine:Hide()
    end
    if self.frame.topGlow then
        self.frame.topGlow:Hide()
    end
end

function SFrames.Chat:SetUnlocked(unlocked)
    if not self.frame then return end
    if self.frame.hint then
        self.frame.hint:SetText("")
        self.frame.hint:Hide()
    end
    self:ApplyFrameBorderStyle()
    if not unlocked then
        self:ApplyAllTabsSetup()
        self:RefreshChatBounds()
    end
end

function SFrames.Chat:ApplyConfig()
    if not self.frame then return end

    self:HookWindowDragBlocker()

    local db = EnsureDB()
    local cfg = self:GetConfig()

    if cfg.enable == false then
        if self.hiddenConfigButton then
            self.hiddenConfigButton:Show()
        end
        self.frame:Hide()
        if self.editBackdrop then self.editBackdrop:Hide() end
        return
    end

    if self.hiddenConfigButton then
        self.hiddenConfigButton:Hide()
    end
    self.frame:Show()
    self.frame:SetWidth(cfg.width)
    self.frame:SetHeight(cfg.height)
    self.frame:SetScale(cfg.scale)

    db.width = cfg.width
    db.height = cfg.height
    db.scale = cfg.scale
    db.fontSize = cfg.fontSize
    db.showBorder = cfg.showBorder
    db.borderClassColor = cfg.borderClassColor
    db.sidePadding = cfg.sidePadding
    db.topPadding = cfg.topPadding
    db.bottomPadding = cfg.bottomPadding

    if self.frame.tabBar and self.frame.title and self.frame.configButton then
        self.frame.tabBar:ClearAllPoints()
        self.frame.tabBar:SetPoint("LEFT", self.frame.title, "RIGHT", 10, -1)
        self.frame.tabBar:SetPoint("RIGHT", self.frame.configButton, "LEFT", -28, -1)
        self.frame.tabBar:SetHeight(18)
    end

    if self.frame.inner then
        self.frame.inner:ClearAllPoints()
        self.frame.inner:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding, -cfg.topPadding)
        self.frame.inner:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding, cfg.bottomPadding)
    end

    if self.frame.innerShade then
        self.frame.innerShade:ClearAllPoints()
        self.frame.innerShade:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 2, -cfg.topPadding + 2)
        self.frame.innerShade:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding + 2, cfg.bottomPadding + 2)
    end

    local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
    self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)

    self:AttachChatFrame()
    self:HideDefaultChrome()
    self:HideTabChrome()
    self:StyleChatFont()
    self:RefreshTabButtons()
    self:ApplyAllTabsSetup()
    self:StyleEditBox()
    self:RefreshChatBounds()
    self:EnsureChromeWatcher()
    self:StartStabilizer()
    self:SetUnlocked(SFrames and SFrames.isUnlocked)
    self:RefreshConfigFrame()
end

function SFrames.Chat:Initialize()
    EnsureDB()
    self:CreateContainer()
    self:HookWindowDragBlocker()
    self:ApplyConfig()
    self:StartPositionEnforcer()
    self:EnsureAddMessageChannelFilter()
    self:EnsureChannelMessageFilter()
    self:ApplyBlockedChannelsGlobally()
    self:StartBlockedChannelWatcher()
    self:HookChatEditColoring()

    -- Hook ChatFrame_MessageEventHandler to suppress ignored channels (lft/lfg/etc)
    if ChatFrame_MessageEventHandler and not SFrames.Chat._lftHooked then
        local origHandler = ChatFrame_MessageEventHandler
        ChatFrame_MessageEventHandler = function(frameArg, eventArg)
            local frame = frameArg or this
            local ev = eventArg or event

            if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" or ev == "CHAT_MSG_CHANNEL_LEAVE" or ev == "CHAT_MSG_CHANNEL_NOTICE" then
                local chanName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2)
                if chanName and chanName ~= "" then
                    if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" then
                        TrackDiscoveredChannel(chanName)
                    elseif ev == "CHAT_MSG_CHANNEL_LEAVE" then
                        UntrackDiscoveredChannel(chanName)
                    end
                end
                if frame and frame.GetName then
                    local frameName = frame:GetName()
                    if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then
                        local matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(frame)
                        if matchedTabIdx then
                            local tab = EnsureDB().tabs[matchedTabIdx]
                            local allowChannelMessages = not (tab.filters and tab.filters.channel == false)
                            if not allowChannelMessages or not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
                                return
                            end
                        end
                    end
                end
            end

            return origHandler(frame, ev)
        end
        SFrames.Chat._lftHooked = true
    end

    -- Direct event-based auto-translation hook (always installed as primary translation trigger)
    if not SFrames.Chat._directTranslateHooked then
        SFrames.Chat._directTranslateHooked = true
        local translateEvFrame = CreateFrame("Frame", "SFramesChatTranslateEvents", UIParent)
        for evName, _ in pairs(TRANSLATE_EVENT_FILTERS) do
            translateEvFrame:RegisterEvent(evName)
        end
        translateEvFrame:SetScript("OnEvent", function()
            if not (SFrames and SFrames.Chat) then return end
            local filterKey = GetTranslateFilterKeyForEvent(event)
            if not filterKey then return end
            local messageText = arg1
            if type(messageText) ~= "string" or messageText == "" then return end

            local channelName = nil
            if filterKey == "channel" then
                channelName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2)
                -- Note: do NOT skip ignored channels here — user may have explicitly
                -- enabled them (e.g. hc/hardcore). ShouldAutoTranslateForTab will
                -- correctly return false if the channel is not enabled for this tab.
            end

            local db = EnsureDB()
            local translated = false
            for i = 1, table.getn(db.tabs) do
                if not translated and SFrames.Chat:ShouldAutoTranslateForTab(i, filterKey, channelName) then
                    local tab = db.tabs[i]
                    if tab and type(tab.id) == "number" then
                        local cleanText = CleanTextForTranslation(messageText)
                        if cleanText ~= "" then
                            local tabId = tab.id
                            local senderName = arg2
                            SFrames.Chat:RequestAutoTranslation(cleanText, function(result, err)
                                if result and result ~= "" then
                                    SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, cleanText, result, senderName)
                                end
                            end)
                            translated = true
                        end
                    end
                end
            end
        end)
    end

    SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
        if SFrames and SFrames.Chat then
            SFrames.Chat:ApplyConfig()
            SFrames.Chat:EnsureAddMessageChannelFilter()
            SFrames.Chat:ApplyBlockedChannelsGlobally()
        end
        LoadPersistentClassCache()
        if IsInGuild and IsInGuild() and GuildRoster then
            GuildRoster()
        end
        SFrames:RefreshClassColorCache()
    end)

    -- 团队成员变化时更新职业缓存
    SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() SFrames:RefreshClassColorCache() end)
    SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end)
    SFrames:RegisterEvent("FRIENDLIST_UPDATE", function() SFrames:RefreshClassColorCache() end)
    SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end)
    SFrames:RegisterEvent("WHO_LIST_UPDATE", function() SFrames:RefreshClassColorCache() end)

    -- 监听公会/队伍聊天，未命中缓存时即时刷新名单
    if not SFrames.Chat._classRefreshHooked then
        SFrames.Chat._classRefreshHooked = true
        local classRefreshFrame = CreateFrame("Frame", "SFramesChatClassRefresh", UIParent)
        classRefreshFrame:RegisterEvent("CHAT_MSG_GUILD")
        classRefreshFrame:RegisterEvent("CHAT_MSG_OFFICER")
        classRefreshFrame:RegisterEvent("CHAT_MSG_PARTY")
        classRefreshFrame:RegisterEvent("CHAT_MSG_RAID")
        classRefreshFrame:RegisterEvent("CHAT_MSG_RAID_LEADER")
        classRefreshFrame:SetScript("OnEvent", function()
            local sender = arg2
            if sender and sender ~= "" and not SFrames.PlayerClassColorCache[sender] then
                if event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER" then
                    if GuildRoster then GuildRoster() end
                else
                    SFrames:RefreshClassColorCache()
                end
            end
        end)
    end

    SFrames:RegisterEvent("UPDATE_CHAT_WINDOWS", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:HideDefaultChrome()
            SFrames.Chat:HideTabChrome()
            SFrames.Chat:RefreshChatBounds()
            SFrames.Chat:RefreshTabButtons()
            SFrames.Chat:StyleEditBox()
            SFrames.Chat:EnsureAddMessageChannelFilter()
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
        end
    end)

    SFrames:RegisterEvent("UPDATE_CHAT_COLOR", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:StyleChatFont()
            SFrames.Chat:StyleEditBox()
        end
    end)
    
    if not SFrames.Chat._msgHooked then
        SFrames.Chat._msgHooked = true
        
        local MAX_CACHE = 200
        
        -- Initialize from persistent storage
        local db = EnsureDB()
        if type(db.messageCache) ~= "table" then
            db.messageCache = {}
        end
        
        -- MessageHistory is the live runtime lookup (msgID -> raw text), seeded from cache
        SFrames.Chat.MessageHistory = {}
        SFrames.Chat.MessageSenders = {}
        SFrames.Chat.MessageIndex = 0
        
        -- Seed runtime lookup from persistent cache
        for i = 1, table.getn(db.messageCache) do
            local entry = db.messageCache[i]
            if entry and entry.id and entry.text then
                SFrames.Chat.MessageHistory[entry.id] = entry.text
                if entry.id > SFrames.Chat.MessageIndex then
                    SFrames.Chat.MessageIndex = entry.id
                end
            end
        end

        -- Helper: save a message to the persistent cache ring buffer
        local function PersistMessage(msgID, text, r, g, b, frameIndex)
            local cache = EnsureDB().messageCache
            if not cache then
                EnsureDB().messageCache = {}
                cache = EnsureDB().messageCache
            end
            table.insert(cache, {
                id = msgID,
                text = text,
                r = r,
                g = g,
                b = b,
                frame = frameIndex or 1,
                time = date("%H:%M:%S"),
            })
            -- Trim to MAX_CACHE
            while table.getn(cache) > MAX_CACHE do
                table.remove(cache, 1)
            end
        end

        for i = 1, 7 do
            local cf = _G["ChatFrame" .. i]
            if cf and cf.AddMessage then
                local origAddMessage = cf.AddMessage
                cf.origAddMessage = origAddMessage
                cf.AddMessage = function(self, text, r, g, b, alpha, holdTime)
                    if not text or text == "" then
                        origAddMessage(self, text, r, g, b, alpha, holdTime)
                        return
                    end
                    if string.sub(text, 1, 10) ~= "|Hsfchat:" and string.sub(text, 1, 12) ~= "|cff888888|H" then
                        local db = EnsureDB()
                        -- Universal catch for Turtle WoW custom chat channels like [硬核]
                        local chanName = GetChannelNameFromChatLine(text)
                        
                        if chanName and IsIgnoredChannelByDefault(chanName) then
                            -- Global HC kill switch override check
                            if db.hcGlobalDisable then
                                local lowerChan = string.lower(chanName)
                                if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then
                                    return
                                end
                            end

                            local frameName = self:GetName()
                            local matchedTabIdx = nil
                            if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then
                                matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self)
                                if not matchedTabIdx then
                                    local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$")
                                    local frameNum = tonumber(frameNumStr)
                                    if frameNum then
                                        local db = EnsureDB()
                                        if frameNum <= table.getn(db.tabs) then
                                            matchedTabIdx = frameNum
                                        end
                                    end
                                end
                            end
                            if matchedTabIdx then
                                local tab = EnsureDB().tabs[matchedTabIdx]
                                if tab then
                                    if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
                                        return
                                    end
                                    -- If it passed channel filter, allow translation for these bypassed channels
                                    local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName)
                                    if shouldTranslate then
                                        local cleanText = CleanTextForTranslation(text)
                                        -- Remove the channel prefix from translation text to be clean
                                        cleanText = string.gsub(cleanText, "^%[.-%]%s*", "")
                                        -- It might also have [PlayerName]: 
                                        local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")
                                        if senderName then
                                            cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "")
                                        end
                                        if cleanText ~= "" then
                                            local tabId = tab.id
                                            SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
                                                if result and result ~= "" then
                                                    SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
                                                end
                                            end)
                                        end
                                    end
                                end
                            else
                                return
                            end
                        end
                        
                        -- Hardcore Death Event Overrides
                        if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then
                            local deathLvl = ParseHardcoreDeathMessage(text)
                            if deathLvl then
                                if db.hcDeathDisable then return end
                                if db.hcDeathLevelMin and deathLvl < db.hcDeathLevelMin then return end
                            end
                        end
                        
                        SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1
                        local msgID = SFrames.Chat.MessageIndex
                        
                        -- Store in runtime lookup
                        SFrames.Chat.MessageHistory[msgID] = text
                        
                        -- Persist to SavedVariables
                        local frameIdx = nil
                        for fi = 1, 7 do
                            if self == _G["ChatFrame" .. fi] then
                                frameIdx = fi
                                break
                            end
                        end
                        PersistMessage(msgID, text, r, g, b, frameIdx)
                        
                        -- Apply class color to player names in message
                        local coloredText = ColorPlayerNamesInText(text)
                        -- Insert the clickable button [+] at the beginning
                        local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText
                        origAddMessage(self, modifiedText, r, g, b, alpha, holdTime)

                        -- Legacy auto-translate disabled; handled by per-tab routing above.
                        if false then
                        if self == ChatFrame1 and not string.find(text, "%[翻译%]") then
                            if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then
                                local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
                                cleanText = string.gsub(cleanText, "|r", "")
                                cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
                                
                                if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
                                    _G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
                                        if result then
                                            DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
                                        end
                                    end, "Nanami-UI")
                                elseif _G.STranslate and _G.STranslate.SendIO then
                                    _G.STranslate:SendIO(cleanText, "IN", "auto", "zh")
                                end
                            end
                        end
                        end
                    else
                        origAddMessage(self, text, r, g, b, alpha, holdTime)
                    end
                end
            end
        end
        
        -- Restore cached messages after a short delay so chat frames are fully ready
        if not SFrames.Chat._cacheRestored then
            SFrames.Chat._cacheRestored = true
            local restoreFrame = CreateFrame("Frame", nil, UIParent)
            restoreFrame.elapsed = 0
            restoreFrame:SetScript("OnUpdate", function()
                this.elapsed = this.elapsed + arg1
                if this.elapsed < 1.5 then return end
                this:SetScript("OnUpdate", nil)
                this:Hide()

                if SFrames.Chat.cacheRestorePrimed then
                    return
                end
                
                local cache = EnsureDB().messageCache
                if not cache or table.getn(cache) == 0 then return end
                
                -- Split cache restoration across multiple frames to prevent WoW from freezing
                local currentIdx = 1
                local totalCount = table.getn(cache)
                
                this:SetScript("OnUpdate", function()
                    local chunkEnd = math.min(currentIdx + 15, totalCount)
                    for idx = currentIdx, chunkEnd do
                        local entry = cache[idx]
                        if entry and entry.text then
                            local frameIdx = entry.frame or 1
                            local cf = _G["ChatFrame" .. frameIdx]
                            if not cf then cf = ChatFrame1 end
                            if cf and cf.AddMessage then
                                local msgID = entry.id or 0
                                SFrames.Chat.MessageHistory[msgID] = entry.text
                                local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text
                                -- Call with the raw origAddMessage to avoid re-persisting
                                local origAM = cf.origAddMessage or cf.AddMessage
                                origAM(cf, modified, entry.r, entry.g, entry.b)
                            end
                        end
                    end
                    currentIdx = chunkEnd + 1
                    if currentIdx > totalCount then
                        this:SetScript("OnUpdate", nil)
                        this:Hide()
                        SFrames.Chat.cacheRestorePrimed = true
                    end
                end)
            end)
        end
    end
    
    if not SFrames.Chat._itemRefHooked then
        SFrames.Chat._itemRefHooked = true
        local origSetItemRef = SetItemRef
        SetItemRef = function(link, text, button)
            if link and string.sub(link, 1, 7) == "sfchat:" then
                local msgID = tonumber(string.sub(link, 8))
                if msgID and SFrames.Chat.MessageHistory[msgID] then
                    local messageText = SFrames.Chat.MessageHistory[msgID]
                    local sender = nil
                    
                    -- Check MessageSenders lookup first (for AI translated messages)
                    if SFrames.Chat.MessageSenders and SFrames.Chat.MessageSenders[msgID] then
                        sender = SFrames.Chat.MessageSenders[msgID]
                    else
                        -- Attempt to extract sender name from raw text like: |Hplayer:Name|h[Name]|h or [Name]:
                        local _, _, extractedName = string.find(messageText, "|Hplayer:(.-)|h")
                        if extractedName then
                            sender = string.gsub(extractedName, ":.*", "")
                        else
                            _, _, sender = string.find(messageText, "%[(.-)%]")
                        end
                    end
                    -- `this` inside SetItemRef is usually the ChatFrame that was clicked
                    local clickedFrame = this
                    if type(clickedFrame) ~= "table" or not clickedFrame.AddMessage then
                        local activeIdx = SFrames.Chat:GetActiveTabIndex()
                        local tab = SFrames.Chat:GetTab(activeIdx)
                        clickedFrame = tab and SFrames.Chat:GetChatFrameForTab(tab) or DEFAULT_CHAT_FRAME
                    end

                    SFrames.Chat:OpenMessageContextMenu(msgID, messageText, sender, clickedFrame)
                end
                return
            end
            if origSetItemRef then
                origSetItemRef(link, text, button)
            end
        end
    end

    if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
        SFrames.Movers:RegisterMover("ChatFrame", self.frame, "聊天框",
            "BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 0, 0)
    end
end

function SFrames.Chat:ShowCopyDialog(text)
    if not self.copyFrame then
        local f = CreateFrame("Frame", "SFramesChatCopyFrame", UIParent)
        f:SetWidth(400)
        f:SetHeight(150)
        f:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
        f:SetFrameStrata("FULLSCREEN_DIALOG")
        f:EnableMouse(true)
        f:SetMovable(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", function() this:StartMoving() end)
        f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
        
        if SFrames and SFrames.CreateBackdrop then
            SFrames:CreateBackdrop(f)
        else
            f:SetBackdrop({
                bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
                edgeFile = "Interface\\Buttons\\WHITE8X8",
                tile = true, tileSize = 32, edgeSize = 1,
                insets = { left = 1, right = 1, top = 1, bottom = 1 },
            })
        end
        f:SetBackdropColor(0.08, 0.07, 0.1, 0.96)
        f:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
        
        local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
        close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2)
        
        local l = f:CreateFontString(nil, "OVERLAY")
        l:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE")
        l:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10)
        l:SetText("复制聊天内容 (Ctrl+C)")
        
        local sf = CreateFrame("ScrollFrame", "SFramesChatCopyScroll", f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
        sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 10)
        
        local edit = CreateFrame("EditBox", "SFramesChatCopyEdit", sf)
        edit:SetWidth(350)
        edit:SetHeight(90)
        edit:SetMultiLine(true)
        edit:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE")
        edit:SetAutoFocus(false)
        edit:SetScript("OnEscapePressed", function() f:Hide() end)
        sf:SetScrollChild(edit)
        
        f.edit = edit
        self.copyFrame = f
    end
    
    local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
    cleanText = string.gsub(cleanText, "|r", "")
    cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
    
    self.copyFrame.edit:SetText(cleanText)
    self.copyFrame:Show()
    self.copyFrame.edit:HighlightText()
    self.copyFrame.edit:SetFocus()
end

function SFrames.Chat:OpenMessageContextMenu(msgID, text, sender, targetFrame)
    if not self._msgDropdown then
        self._msgDropdown = CreateFrame("Frame", "SFramesChatMessageDropdown", UIParent, "UIDropDownMenuTemplate")
        self._msgDropdown:SetFrameStrata("FULLSCREEN_DIALOG")
    end
    UIDropDownMenu_Initialize(self._msgDropdown, function()
        local level = UIDROPDOWNMENU_MENU_LEVEL or 1
        
        local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
        cleanText = string.gsub(cleanText, "|r", "")
        cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")

        if level == 2 and UIDROPDOWNMENU_MENU_VALUE == "STranslate_Langs" then
            local langs = nil
            if _G.STranslateAPI and _G.STranslateAPI.GetSupportedLanguages then
                langs = _G.STranslateAPI.GetSupportedLanguages()
            end
            
            if type(langs) ~= "table" or table.getn(langs) == 0 then
                langs = {
                    { code = "zh", name = "中文" },
                    { code = "en", name = "英语" },
                    { code = "es", name = "西班牙语" },
                    { code = "fr", name = "法语" },
                    { code = "pl", name = "波兰语" },
                    { code = "de", name = "德语" },
                    { code = "ru", name = "俄语" },
                    { code = "ko", name = "韩语" },
                }
            end

            local langNames = {
                zh = "中文", 
                en = "英语", 
                es = "西班牙语", 
                fr = "法语", 
                pl = "波兰语", 
                de = "德语", 
                ru = "俄语", 
                ko = "韩语",
                ja = "日语",
                auto = "自动检测"
            }

            for _, lang in ipairs(langs) do
                local info = NewDropDownInfo()
                info.text = langNames[lang.code] or lang.name or lang.code
                info.notCheckable = 1
                
                -- Capture the primitive value correctly for Lua 5.0 loops
                local currentLangCode = lang.code
                
                info.func = function()
                    local outputFrame = targetFrame or DEFAULT_CHAT_FRAME
                    if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
                        _G.STranslateAPI.Translate(cleanText, "auto", currentLangCode, function(result, err, meta)
                            if err then
                                outputFrame:AddMessage("|cffff3333[Nanami-UI] 翻译失败:|r " .. tostring(err))
                                return
                            end
                            if not result then return end
                            outputFrame:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
                        end, "Nanami-UI")
                    elseif _G.STranslate and _G.STranslate.SendIO then
                        _G.STranslate:SendIO(cleanText, "IN", "auto", currentLangCode)
                    end
                    CloseDropDownMenus()
                end
                UIDropDownMenu_AddButton(info, level)
            end
            return
        end

        if level == 1 then
            if sender and sender ~= "" then
                local replySender = sender
                local infoReply = NewDropDownInfo()
                infoReply.text = "回复 " .. replySender
                infoReply.notCheckable = 1
                infoReply.func = function()
                    if ChatFrameEditBox then
                        ChatFrameEditBox:Show()
                        ChatFrameEditBox:SetText("/w " .. replySender .. " ")
                        ChatFrameEditBox:SetFocus()
                    end
                    CloseDropDownMenus()
                end
                UIDropDownMenu_AddButton(infoReply, level)

                local infoName = NewDropDownInfo()
                infoName.text = "复制玩家姓名"
                infoName.notCheckable = 1
                infoName.func = function()
                    SFrames.Chat:ShowCopyDialog(replySender)
                end
                UIDropDownMenu_AddButton(infoName, level)
            end

            local info = NewDropDownInfo()
            info.text = "复制内容"
            info.notCheckable = 1
            info.func = function()
                SFrames.Chat:ShowCopyDialog(text)
            end
            UIDropDownMenu_AddButton(info, level)
            
            if (_G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady()) or (_G.STranslate and _G.STranslate.SendIO) then
                local t = NewDropDownInfo()
                t.text = "翻译为..."
                t.hasArrow = 1
                t.value = "STranslate_Langs"
                t.notCheckable = 1
                UIDropDownMenu_AddButton(t, level)
            end
        end
    end, "MENU")
    ToggleDropDownMenu(1, nil, self._msgDropdown, "cursor", 3, -3)
end
    SFrames:RegisterEvent("CHANNEL_UI_UPDATE", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
            local delayFrame = CreateFrame("Frame")
            local elapsed = 0
            delayFrame:SetScript("OnUpdate", function()
                elapsed = elapsed + (arg1 or 0)
                if elapsed >= 0.5 then
                    this:SetScript("OnUpdate", nil)
                    if SFrames and SFrames.Chat then
                        SFrames.Chat:RefreshConfigFrame()
                    end
                end
            end)
        end
    end)

    SFrames:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            -- Track channel join/leave from notice events (arg9 = channel name)
            local noticeName = arg9 or arg4 or ""
            if noticeName ~= "" then
                if arg1 == "YOU_JOINED" then
                    TrackDiscoveredChannel(noticeName)
                elseif arg1 == "YOU_LEFT" then
                    UntrackDiscoveredChannel(noticeName)
                end
            end
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
            -- Delayed re-refresh: GetChannelList() may lag behind the event
            local delayFrame = CreateFrame("Frame")
            local elapsed = 0
            delayFrame:SetScript("OnUpdate", function()
                elapsed = elapsed + (arg1 or 0)
                if elapsed >= 0.5 then
                    this:SetScript("OnUpdate", nil)
                    if SFrames and SFrames.Chat then
                        SFrames.Chat:RefreshConfigFrame()
                    end
                end
            end)
        end
    end)

    SFrames:RegisterEvent("ZONE_CHANGED", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
            SFrames.Chat:StartStabilizer()
        end
    end)

    SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
            SFrames.Chat:StartStabilizer()
        end
    end)

    SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:ApplyBlockedChannelsGlobally()
            SFrames.Chat:RefreshConfigFrame()
        end
    end)

    local function ChatCombatReanchor()
        if SFrames and SFrames.Chat then
            if EnsureDB().enable == false then return end
            SFrames.Chat:ReanchorChatFrames()
        end
    end
    SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
    SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor)

