local addonName, addonTable = ...

local AceSerializer = LibStub("AceSerializer-3.0")
local NetEaseSocket = LibStub("NetEaseSocket-2.0", true)

local COMM_PREFIX = "NERB"
local CONNECT_RETRY_SECONDS = 3
local CACHE_TTL_SECONDS = 24 * 60 * 60
local QUERY_DEBOUNCE_SECONDS = 0.5
local NATIVE_QUERY_RETRY_SECONDS = 1
local NATIVE_QUERY_RETRY_LIMIT = 5
local SERVER_QUERY_RESEND_SECONDS = 5
local SERVER_LOGIN_QUERY_DELAY_SECONDS = 1.5
local ACCOUNT_TYPE_SERVER_BATCH_SIZE = 8
local ACCOUNT_TYPE_CHARACTER_QUERY_BATCH_SIZE = 4
local ACCOUNT_TYPE_CHARACTER_QUERY_DELAY_SECONDS = 0.20
local ACCOUNT_TYPE_NEGATIVE_CACHE_TTL_SECONDS = 10 * 60
local ACCOUNT_TYPE_MAX_QUERY_RETRIES = 1
local ACCOUNT_TYPE_UI_REFRESH_DEBOUNCE_SECONDS = 0.50
local ACCOUNT_TYPE_DISPLAY_PENDING_TIMEOUT_SECONDS = 6
local ACCOUNT_TYPE_NATIVE_PARALLEL_FALLBACK_DELAY_SECONDS = 0.75
local SERVER_LOGIN_SOURCE_FALLBACK = 0
local ACCOUNT_TYPE_CACHE_MAX_ENTRIES = 500
local ACCOUNT_TYPE_FORCE_REFRESH_SECONDS = 10
local ACCOUNT_TYPE_QUERY_TIMEOUT_SECONDS = SERVER_QUERY_RESEND_SECONDS
local MEETINGSTONE_PROTOCOL_ADDON_NAME = "MeetingStone"
local MEETINGSTONE_PROTOCOL_VERSION_FALLBACK = "12.1.6"
local NATIVE_MEETINGSTONE_ENV_NAMES = {
    "MeetingStone",
    "MeetingStoneEX",
    "MeetingStoneEx",
}
local ADDON_SOURCE_HOSTS = {
    { name = "BigFoot", source = 1 },
    { name = "!!!163UI!!!", source = 2 },
    { name = "Duowan", source = 4 },
    { name = "ElvUI", source = 8 },
}
local ADDON_ICON_PATH = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Icon\\"
local NEWBIE_ICON_TEXTURE = ADDON_ICON_PATH .. "Newbie.png"
local VETERAN_ICON_TEXTURE = ADDON_ICON_PATH .. "Veteran.png"
local STAR_ICON_TEXTURE = ADDON_ICON_PATH .. "Star.png"
local ACCOUNT_TYPE_ICON_SIZE = 18
local ACCOUNT_TYPE_GREEN_TEXT_COLOR = { 0.18, 1, 0.22, 1 }
local ACCOUNT_TYPE_GOLD_TEXT_COLOR = { 1, 0.82, 0, 1 }
local ACCOUNT_TYPE_RED_TEXT_COLOR = { 1, 0.08, 0.05, 1 }
local ACCOUNT_TYPE_PENDING_TEXT_COLOR = { 0.62, 0.75, 1, 1 }
local LEADER_ICON_TEXTURE = ADDON_ICON_PATH .. "Leader.png"

local MeetingStoneData = addonTable.MeetingStoneData or {}
addonTable.MeetingStoneData = MeetingStoneData

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

local function IsMeetingStoneDataCombatPaused(self)
    return type(self) == "table" and (self.combatPaused == true or IsInCombatLockdown())
end

local function TrimString(value)
    if type(value) ~= "string" then
        return nil
    end

    local trimmed = value:match("^%s*(.-)%s*$")
    if not trimmed or trimmed == "" then
        return nil
    end

    return trimmed
end

local function NormalizeRealmName(realm)
    local normalizedRealm = TrimString(realm)
    if not normalizedRealm then
        return nil
    end

    normalizedRealm = normalizedRealm:gsub("%s+", "")
    return normalizedRealm ~= "" and normalizedRealm or nil
end

local function SplitLeaderName(name)
    local leader = TrimString(name)
    if not leader then
        return nil, nil
    end

    local shortName, realm = leader:match("^([^-]+)%-(.+)$")
    if shortName and realm then
        return TrimString(shortName), TrimString(realm)
    end

    return leader, nil
end

local function FormatLeaderName(name, realm)
    local shortName = TrimString(name)
    if not shortName then
        return nil
    end

    local resolvedRealm = TrimString(realm)
    if resolvedRealm then
        return shortName .. "-" .. resolvedRealm
    end

    return shortName
end

local function FormatLeaderNameWithRawRealm(name, realm)
    local shortName = TrimString(name)
    local rawRealm = TrimString(realm)
    if not shortName then
        return nil
    end
    if rawRealm then
        return shortName .. "-" .. rawRealm
    end
    return shortName
end

local function GetAddonMetadata(targetAddonName, fieldName)
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        local ok, value = pcall(C_AddOns.GetAddOnMetadata, targetAddonName, fieldName)
        if ok and type(value) == "string" and value ~= "" then
            return value
        end
    end
    if GetAddOnMetadata then
        local ok, value = pcall(GetAddOnMetadata, targetAddonName, fieldName)
        if ok and type(value) == "string" and value ~= "" then
            return value
        end
    end
    return nil
end

local function GetMeetingStoneProtocolVersion()
    return GetAddonMetadata(MEETINGSTONE_PROTOCOL_ADDON_NAME, "Version")
        or MEETINGSTONE_PROTOCOL_VERSION_FALLBACK
end

local function IsAddonLoaded(addon)
    if C_AddOns and C_AddOns.IsAddOnLoaded then
        local ok, loaded = pcall(C_AddOns.IsAddOnLoaded, addon)
        return ok and loaded == true
    end
    if IsAddOnLoaded then
        local ok, loaded = pcall(IsAddOnLoaded, addon)
        return ok and loaded == true
    end
    return false
end

local function GetAddonSource()
    for _, host in ipairs(ADDON_SOURCE_HOSTS) do
        if IsAddonLoaded(host.name) then
            return host.source, false
        end
    end

    return SERVER_LOGIN_SOURCE_FALLBACK, true
end

local function GetServerLoginQueryData()
    local dataCache = _G.DataCache
    if dataCache and type(dataCache.GetQueryData) == "function" then
        local ok, queryData = pcall(dataCache.GetQueryData, dataCache)
        if ok and type(queryData) == "table" then
            return queryData
        end
    end

    return {}
end

local function GetServerRealm()
    local realm = TrimString(GetRealmName and GetRealmName())
    if realm then
        return realm
    end

    local realms = GetAutoCompleteRealms and GetAutoCompleteRealms()
    return realms and TrimString(realms[1]) or nil
end

local function GetAddonServer()
    local prefix = _G.NETEASE_SERVER_PREFIX or "S1"
    local faction = UnitFactionGroup and UnitFactionGroup("player") or "Neutral"
    return prefix .. tostring(faction or "Neutral")
end

local function GetNativeMeetingStoneEnv()
    local bridge = addonTable.MeetingStoneBridge
    if type(bridge) == "table" and type(bridge.GetEnv) == "function" then
        local ok, env = pcall(bridge.GetEnv)
        if ok and type(env) == "table" then
            return env
        end
    end

    local libStub = _G.LibStub
    if not libStub then
        return nil
    end

    local ok, envLib = pcall(libStub, "NetEaseEnv-1.0")
    if not ok or type(envLib) ~= "table" or type(envLib._NSList) ~= "table" then
        return nil
    end

    for _, envName in ipairs(NATIVE_MEETINGSTONE_ENV_NAMES) do
        local env = envLib._NSList[envName]
        if type(env) == "table" then
            return env
        end
    end

    return nil
end

local function GetSocketClientHandler(client)
    local socket = NetEaseSocket or LibStub("NetEaseSocket-2.0", true)
    local handlers = socket and socket.handlers
    return client and type(handlers) == "table" and handlers[client] or nil
end

local function IsSocketClientReady(client)
    if not client then
        return false
    end

    if client.IsReady then
        return client:IsReady() == true
    end

    local handler = GetSocketClientHandler(client)
    if handler and handler.IsReady then
        return handler:IsReady() == true
    end

    return false
end

local function GetSocketClientTarget(client)
    if not client then
        return nil
    end

    if type(client.target) == "string" and client.target ~= "" then
        return client.target
    end

    local handler = GetSocketClientHandler(client)
    if type(handler) == "table" then
        if type(handler.target) == "string" and handler.target ~= "" then
            return handler.target
        end
        if type(handler.connectTarget) == "string" and handler.connectTarget ~= "" then
            return handler.connectTarget
        end
    end

    if type(client.connectTarget) == "string" and client.connectTarget ~= "" then
        return client.connectTarget
    end

    return nil
end

local function GetSocketClientServerTarget(client)
    if not client then
        return nil
    end

    if type(client.target) == "string" and client.target ~= "" then
        return client.target
    end

    local handler = GetSocketClientHandler(client)
    if type(handler) == "table" and type(handler.target) == "string" and handler.target ~= "" then
        return handler.target
    end

    return nil
end

local function GetDebugTime()
    return GetTime and GetTime() or time()
end

local function GetServerLoginQueryDelayRemaining(self)
    if type(self.lastSLOGINAt) ~= "number" then
        return 0
    end

    local elapsed = GetDebugTime() - self.lastSLOGINAt
    if elapsed >= SERVER_LOGIN_QUERY_DELAY_SECONDS then
        return 0
    end

    return SERVER_LOGIN_QUERY_DELAY_SECONDS - elapsed
end

local function CountTableValues(values)
    if type(values) ~= "table" then
        return 0
    end

    local count = 0
    for _ in pairs(values) do
        count = count + 1
    end
    return count
end

local function IsActiveNewbieAccountTypeData(data)
    if type(data) ~= "table" or data.isNewbie ~= true then
        return false
    end

    local expireTime = tonumber(data.newbieExpireTime)
    return expireTime ~= nil and time() < expireTime
end

local function PreserveActiveNewbieAccountTypeData(existing, incoming)
    if type(incoming) ~= "table" then
        return incoming
    end
    if not IsActiveNewbieAccountTypeData(existing) or IsActiveNewbieAccountTypeData(incoming) then
        return incoming
    end

    local merged = {}
    for key, value in pairs(incoming) do
        merged[key] = value
    end
    merged.isNewbie = true
    merged.newbieExpireTime = existing.newbieExpireTime
    merged.newbiePreservedAt = time()
    return merged
end

local function FormatTableKeys(values, limit)
    if type(values) ~= "table" then
        return nil
    end

    local keys = {}
    for key in pairs(values) do
        keys[#keys + 1] = tostring(key)
    end
    table.sort(keys)

    limit = tonumber(limit) or #keys
    if #keys > limit then
        local remaining = #keys - limit
        for index = #keys, limit + 1, -1 do
            keys[index] = nil
        end
        keys[#keys + 1] = "+" .. tostring(remaining)
    end

    return table.concat(keys, ",")
end

local function EnsureAccountTypeQueryStates(self)
    if type(self.accountTypeQueryStates) ~= "table" then
        self.accountTypeQueryStates = {}
    end
    return self.accountTypeQueryStates
end

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

    local states = EnsureAccountTypeQueryStates(self)
    local state = states[leader]
    if type(state) ~= "table" then
        return nil
    end

    local now = GetDebugTime()
    if state.status == "sent" and type(state.sentAt) == "number" and (now - state.sentAt) >= ACCOUNT_TYPE_QUERY_TIMEOUT_SECONDS then
        state.status = "retry"
        state.retryAt = now
        state.failureReason = "timeout"
    elseif state.status == "pending" and type(state.queuedAt) == "number" and (now - state.queuedAt) >= ACCOUNT_TYPE_QUERY_TIMEOUT_SECONDS then
        state.status = "retry"
        state.retryAt = now
        state.failureReason = "timeout"
    end

    return state
end

local function IsAccountTypeQueryPending(state)
    return type(state) == "table" and (state.status == "pending" or state.status == "sent")
end

local function IsAccountTypeQueryActive(state)
    return IsAccountTypeQueryPending(state) or (type(state) == "table" and state.status == "retry")
end

local function IsAccountTypeSendFailureReason(reason)
    return reason == "connection_failed" or reason == "send_failed"
end

local function IsAccountTypeQueryDisplayPending(state)
    if not IsAccountTypeQueryPending(state) then
        return false
    end

    local startedAt = tonumber(state.sentAt) or tonumber(state.queuedAt)
    if not startedAt then
        return true
    end

    return (GetDebugTime() - startedAt) < ACCOUNT_TYPE_DISPLAY_PENDING_TIMEOUT_SECONDS
end

local EnsureState
local MarkAccountTypeQueryPending
local MarkAccountTypeQueryDone
local MarkAccountTypeQueryRetry
local NormalizeLeaderName
local IsAccountTypeCacheForLeader
local StoreNoResultAccountTypeData

local function ScheduleMeetingStoneDataRefresh(self, reason, detail)
    if not (self and self.owner and self.owner.IsMainWindowShown and self.owner.RefreshMainWindow) then
        return
    end
    if not self.owner:IsMainWindowShown() then
        return
    end

    self.pendingAccountTypeRefreshReason = reason or self.pendingAccountTypeRefreshReason or "MeetingStoneData"
    self.pendingAccountTypeRefreshDetail = detail or self.pendingAccountTypeRefreshDetail
    if self.accountTypeRefreshScheduled then
        return
    end

    self.accountTypeRefreshScheduled = true
    local function runRefresh()
        MeetingStoneData.accountTypeRefreshScheduled = false
        local owner = MeetingStoneData.owner
        if owner and owner.IsMainWindowShown and owner:IsMainWindowShown() and owner.RefreshMainWindow then
            local refreshReason = MeetingStoneData.pendingAccountTypeRefreshReason or "MeetingStoneData"
            local refreshDetail = MeetingStoneData.pendingAccountTypeRefreshDetail
            MeetingStoneData.pendingAccountTypeRefreshReason = nil
            MeetingStoneData.pendingAccountTypeRefreshDetail = nil
            owner:RefreshMainWindow("MeetingStoneData", refreshDetail or refreshReason)
        end
    end

    if C_Timer and C_Timer.After then
        C_Timer.After(ACCOUNT_TYPE_UI_REFRESH_DEBOUNCE_SECONDS, runRefresh)
    else
        runRefresh()
    end
end

local function ScheduleAccountTypeQueryStateRefresh(self)
    if not (self and C_Timer and C_Timer.After) then
        return
    end
    if self.accountTypeQueryStateRefreshScheduled then
        return
    end

    self.accountTypeQueryStateRefreshScheduled = true
    C_Timer.After(ACCOUNT_TYPE_QUERY_TIMEOUT_SECONDS + 0.1, function()
        MeetingStoneData.accountTypeQueryStateRefreshScheduled = false
        if IsMeetingStoneDataCombatPaused(MeetingStoneData) then
            MeetingStoneData.combatDeferred = true
            return
        end

        ScheduleMeetingStoneDataRefresh(MeetingStoneData, "AccountTypeQueryTimeout")

        for leader in pairs(MeetingStoneData.accountTypeQueryStates or {}) do
            local state = GetAccountTypeQueryState(MeetingStoneData, leader)
            if state and state.status == "retry" then
                -- 超时必须消耗一次重试次数；旧逻辑只是把 retry 又改回 pending，
                -- 服务器无回包时会无限循环，UI 就一直显示“查询中”。
                MarkAccountTypeQueryRetry(MeetingStoneData, leader, state.failureReason or "timeout")
            elseif IsAccountTypeQueryPending(state) then
                ScheduleAccountTypeQueryStateRefresh(MeetingStoneData)
            end
        end
    end)
end

MarkAccountTypeQueryPending = function(self, leader)
    if type(leader) ~= "string" or leader == "" then
        return nil
    end

    local states = EnsureAccountTypeQueryStates(self)
    local now = GetDebugTime()
    local state = states[leader]
    if type(state) ~= "table" then
        state = {}
        states[leader] = state
    elseif IsAccountTypeQueryPending(state) then
        return state
    end

    local wasRetry = state.status == "retry"
    state.status = "pending"
    state.queuedAt = now
    state.queuedWallTime = time()
    if not wasRetry then
        state.retryCount = nil
    end
    state.sentAt = nil
    state.sentWallTime = nil
    state.retryAt = nil
    state.failureReason = nil
    ScheduleAccountTypeQueryStateRefresh(self)
    return state
end

local function MarkAccountTypeQuerySent(self, leader, sentAt, method)
    if type(leader) ~= "string" or leader == "" then
        return
    end

    local state = EnsureAccountTypeQueryStates(self)[leader]
    if type(state) ~= "table" then
        state = MarkAccountTypeQueryPending(self, leader)
    end

    state.status = "sent"
    state.sentAt = sentAt or GetDebugTime()
    state.sentWallTime = time()
    state.sendMethod = method
    ScheduleAccountTypeQueryStateRefresh(self)
end

MarkAccountTypeQueryDone = function(self, leader)
    if type(leader) ~= "string" or leader == "" then
        return
    end

    local state = EnsureAccountTypeQueryStates(self)[leader]
    if type(state) ~= "table" then
        state = {}
        self.accountTypeQueryStates[leader] = state
    end

    state.status = "done"
    state.doneAt = GetDebugTime()
    state.retryAt = nil
    state.failureReason = nil
    if type(self.pendingNames) == "table" then
        self.pendingNames[leader] = nil
    end
    if type(self.nativeMeetingStonePendingNames) == "table" then
        self.nativeMeetingStonePendingNames[leader] = nil
    end
end

MarkAccountTypeQueryRetry = function(self, leader, reason)
    if type(leader) ~= "string" or leader == "" then
        return
    end

    local state = EnsureAccountTypeQueryStates(self)[leader]
    if type(state) ~= "table" then
        state = {}
        self.accountTypeQueryStates[leader] = state
    end

    if state.status == "retry"
        and self.pendingNames
        and self.pendingNames[leader] == true
        and not IsAccountTypeSendFailureReason(reason)
    then
        state.failureReason = state.failureReason or reason or "retry"
        ScheduleAccountTypeQueryStateRefresh(self)
        if self.ready then
            self:ScheduleFlush()
        else
            self:Connect()
        end
        return
    end

    state.retryCount = (tonumber(state.retryCount) or 0) + 1
    state.failureReason = reason or "retry"
    if state.retryCount > ACCOUNT_TYPE_MAX_QUERY_RETRIES then
        if StoreNoResultAccountTypeData then
            StoreNoResultAccountTypeData(self, leader, state.failureReason or "retry_limit")
        else
            MarkAccountTypeQueryDone(self, leader)
        end
        ScheduleMeetingStoneDataRefresh(self, "AccountTypeNoResult", leader)
        return
    end

    state.status = "retry"
    state.retryAt = GetDebugTime()
    self.pendingNames = self.pendingNames or {}
    self.pendingNames[leader] = true
    ScheduleAccountTypeQueryStateRefresh(self)
    if self.ready then
        self:ScheduleFlush()
    else
        self:Connect()
    end
end

local function SplitQueryNames(queryNames)
    local names = {}
    if type(queryNames) ~= "string" or queryNames == "" then
        return names
    end

    for name in queryNames:gmatch("[^,]+") do
        local trimmed = TrimString(name)
        if trimmed then
            names[#names + 1] = trimmed
        end
    end

    return names
end

local function FormatTarget(target)
    local realm = GetServerRealm()
    if type(target) ~= "string" or target == "" or type(realm) ~= "string" or realm == "" then
        return nil
    end

    local fullName = target .. "-" .. realm
    return Ambiguate and Ambiguate(fullName, "none") or fullName
end

NormalizeLeaderName = function(name, realm)
    local shortName, embeddedRealm = SplitLeaderName(name)
    if not shortName then
        return nil
    end

    return FormatLeaderName(shortName, realm or embeddedRealm or GetServerRealm())
end

local function IsSameLeaderName(left, right)
    local leftName, leftRealm = SplitLeaderName(left)
    local rightName, rightRealm = SplitLeaderName(right)
    if not leftName or not rightName or leftName ~= rightName then
        return false
    end

    local normalizedLeftRealm = NormalizeRealmName(leftRealm)
    local normalizedRightRealm = NormalizeRealmName(rightRealm)
    if normalizedLeftRealm and normalizedRightRealm then
        return normalizedLeftRealm == normalizedRightRealm
    end

    return true
end

local function DoesLeaderListContainLeader(leaderNames, leader)
    if type(leaderNames) ~= "string" or leaderNames == "" or type(leader) ~= "string" or leader == "" then
        return false
    end

    for _, returnedLeader in ipairs(SplitQueryNames(leaderNames)) do
        if IsSameLeaderName(leader, returnedLeader) then
            return true
        end
    end
    return false
end

local function AddSetValue(values, value)
    if type(value) == "string" and value ~= "" then
        values[value] = true
    end
end

local function AddLeaderLookupKeys(keys, name, realm)
    local shortName, embeddedRealm = SplitLeaderName(name)
    if not shortName then
        return
    end

    local resolvedRealm = TrimString(realm) or embeddedRealm or GetServerRealm()
    if not resolvedRealm then
        AddSetValue(keys, TrimString(name))
        return
    end

    AddSetValue(keys, FormatLeaderName(shortName, resolvedRealm))
    AddSetValue(keys, FormatLeaderName(shortName, NormalizeRealmName(resolvedRealm)))
    AddSetValue(keys, FormatLeaderNameWithRawRealm(shortName, resolvedRealm))
end

local function TrackLeaderAlias(self, leader)
    local shortName = SplitLeaderName(leader)
    if not shortName then
        return
    end

    self.pendingLeaderAliases = self.pendingLeaderAliases or {}
    self.pendingLeaderAliases[shortName] = self.pendingLeaderAliases[shortName] or {}
    self.pendingLeaderAliases[shortName][leader] = true
end

local function GetTrackedLeaderAliases(self, shortName)
    return self.pendingLeaderAliases and self.pendingLeaderAliases[shortName] or nil
end

local function BuildLeaderCacheKeys(self, leaderName)
    local keys = {}
    local shortName, embeddedRealm = SplitLeaderName(leaderName)
    if not shortName then
        return keys
    end

    AddSetValue(keys, TrimString(leaderName))
    AddSetValue(keys, FormatLeaderName(shortName, embeddedRealm))
    AddSetValue(keys, FormatLeaderName(shortName, NormalizeRealmName(embeddedRealm)))

    local trackedAliases = GetTrackedLeaderAliases(self, shortName)
    if trackedAliases then
        for trackedLeader in pairs(trackedAliases) do
            AddSetValue(keys, trackedLeader)
            local trackedShortName, trackedRealm = SplitLeaderName(trackedLeader)
            AddSetValue(keys, FormatLeaderName(trackedShortName, NormalizeRealmName(trackedRealm)))
        end
        self.pendingLeaderAliases[shortName] = nil
    elseif not embeddedRealm then
        AddSetValue(keys, NormalizeLeaderName(shortName))
    end

    return keys
end

local function GetNativeMeetingStoneRegimentCache()
    local nativeEnv = GetNativeMeetingStoneEnv()
    local profile = (nativeEnv and nativeEnv.Profile) or _G.Profile
    local cache = profile
        and profile.gdb
        and profile.gdb.global
        and profile.gdb.global.LocomotiveData
    return type(cache) == "table" and cache or nil
end

local function GetNativeMeetingStoneRegimentData(leader, name)
    local keys = {}
    AddLeaderLookupKeys(keys, leader)
    AddLeaderLookupKeys(keys, name)

    local cache = GetNativeMeetingStoneRegimentCache()
    if cache then
        for cacheKey in pairs(keys) do
            local data = cache[cacheKey]
            if type(data) == "table" then
                return data
            end
        end
    end

    local nativeEnv = GetNativeMeetingStoneEnv()
    local nativeUtils = nativeEnv and nativeEnv.MSUtils
    local nativeSystemUtils = nativeUtils and nativeUtils.System
    if type(nativeSystemUtils) == "table" and type(nativeSystemUtils.getRegimentData) == "function" then
        for cacheKey in pairs(keys) do
            local ok, data = pcall(nativeSystemUtils.getRegimentData, cacheKey)
            if ok and type(data) == "table" then
                return data
            end
        end
    end

    if nativeEnv and type(nativeEnv.GetRegimentData) == "function" then
        for cacheKey in pairs(keys) do
            local ok, data = pcall(nativeEnv.GetRegimentData, cacheKey)
            if ok and type(data) == "table" then
                return data
            end
        end
    end

    if type(_G.GetRegimentData) == "function" then
        for cacheKey in pairs(keys) do
            local ok, data = pcall(_G.GetRegimentData, cacheKey)
            if ok and type(data) == "table" then
                return data
            end
        end
    end

    return nil
end

local function GetPersistentAccountTypeCache(self)
    local global = self
        and self.owner
        and self.owner.db
        and self.owner.db.global
    if type(global) ~= "table" then
        return nil
    end

    if type(global.meetingStoneAccountTypeCache) ~= "table" then
        global.meetingStoneAccountTypeCache = {}
    end
    return global.meetingStoneAccountTypeCache
end

local function AttachPersistentAccountTypeCache(self)
    local persistentCache = GetPersistentAccountTypeCache(self)
    if not persistentCache or self.cache == persistentCache then
        return
    end

    if type(self.cache) == "table" then
        for key, value in pairs(self.cache) do
            if type(value) == "table" and type(persistentCache[key]) ~= "table" then
                persistentCache[key] = value
            end
        end
    end
    self.cache = persistentCache
end

local function IsAccountTypeCacheNewEnough(data, minMedalTime)
    if type(minMedalTime) ~= "number" then
        return true
    end

    local medalTime = type(data) == "table" and tonumber(data.medalTime) or nil
    return medalTime ~= nil and medalTime >= minMedalTime
end

local function IsPositiveRawLevel(value)
    local level = tonumber(value)
    return level ~= nil and level > 0
end

local function IsNoResultAccountTypeData(data)
    return type(data) == "table"
        and (data.noAccountTypeResult == true or data.noAccountTypeReason ~= nil or data.lastNoResultAt ~= nil)
end

local function IsFreshNoResultAccountTypeData(data)
    if not IsNoResultAccountTypeData(data) then
        return false
    end

    local noResultAt = tonumber(data.lastNoResultAt) or tonumber(data.medalTime)
    return noResultAt ~= nil and (time() - noResultAt) < ACCOUNT_TYPE_NEGATIVE_CACHE_TTL_SECONDS
end

local function IsLegacyNoResultAccountTypeData(data)
    if type(data) ~= "table" then
        return false
    end

    if IsNoResultAccountTypeData(data) then
        return false
    end

    if data.source ~= nil
        and data.isNewbie == false
        and type(data.medalMap) ~= "table"
        and not IsPositiveRawLevel(data.level)
        and not IsPositiveRawLevel(data.starLevel)
    then
        return true
    end

    return false
end

IsAccountTypeCacheForLeader = function(data, leader, name)
    if type(data) ~= "table" then
        return false
    end

    local cachedLeader = TrimString(data.accountTypeLeaderName)
    if cachedLeader then
        return IsSameLeaderName(leader, cachedLeader) or IsSameLeaderName(name, cachedLeader)
    end

    -- 旧版本写入的本插件缓存没有所属角色名，可能已经被单人查询回包污染。
    if data.medalTime ~= nil
        and (data.source ~= nil or data.sourceFallback ~= nil or data.noAccountTypeResult ~= nil or data.medalMap ~= nil)
    then
        return false
    end

    return true
end

local function GetCachedLeaderData(self, leader, name, skipNativeCache, minMedalTime)
    if not leader then
        return nil
    end

    local cache = type(self.cache) == "table" and self.cache or nil
    if cache then
        local data = cache[leader]
        if type(data) == "table" and IsNoResultAccountTypeData(data) then
            if IsFreshNoResultAccountTypeData(data) and IsAccountTypeCacheNewEnough(data, minMedalTime) and IsAccountTypeCacheForLeader(data, leader, name) then
                return data
            end
            cache[leader] = nil
        elseif type(data) == "table" and IsLegacyNoResultAccountTypeData(data) then
            cache[leader] = nil
        elseif type(data) == "table" and IsAccountTypeCacheNewEnough(data, minMedalTime) and IsAccountTypeCacheForLeader(data, leader, name) then
            return data
        elseif type(data) == "table" and not IsAccountTypeCacheForLeader(data, leader, name) then
            cache[leader] = nil
        end

        local rawName = TrimString(name)
        data = rawName and rawName:find("-", 1, true) and cache[rawName] or nil
        if type(data) == "table" and IsNoResultAccountTypeData(data) then
            if IsFreshNoResultAccountTypeData(data) and IsAccountTypeCacheNewEnough(data, minMedalTime) and IsAccountTypeCacheForLeader(data, leader, rawName) then
                return data
            end
            cache[rawName] = nil
        elseif type(data) == "table" and IsLegacyNoResultAccountTypeData(data) then
            cache[rawName] = nil
        elseif type(data) == "table" and IsAccountTypeCacheNewEnough(data, minMedalTime) and IsAccountTypeCacheForLeader(data, leader, rawName) then
            return data
        elseif type(data) == "table" and not IsAccountTypeCacheForLeader(data, leader, rawName) then
            cache[rawName] = nil
        end
    end

    if skipNativeCache then
        return nil
    end

    local nativeData = GetNativeMeetingStoneRegimentData(leader, name)
    if type(nativeData) == "table"
        and not IsLegacyNoResultAccountTypeData(nativeData)
        and IsAccountTypeCacheNewEnough(nativeData, minMedalTime)
    then
        return nativeData
    end
    return nil
end

local function GetCharacterNameAndRealm(characterEntry)
    local character = characterEntry and characterEntry.data
    if type(character) ~= "table" then
        return nil, nil
    end

    local name = character.name or characterEntry.key
    local realm = character.realm
    local shortName, embeddedRealm = SplitLeaderName(name)
    return shortName, TrimString(realm) or embeddedRealm
end

local function IsCacheFresh(data)
    if IsFreshNoResultAccountTypeData(data) then
        return true
    end

    if not (type(data) == "table" and type(data.medalTime) == "number" and (time() - data.medalTime) < CACHE_TTL_SECONDS) then
        return false
    end

    if IsLegacyNoResultAccountTypeData(data) then
        return false
    end

    if data.isNewbie == true then
        local expireTime = tonumber(data.newbieExpireTime)
        if not expireTime or time() >= expireTime then
            return false
        end
    end

    local _, currentSourceFallback = GetAddonSource()
    return not (data.sourceFallback == true and currentSourceFallback ~= true)
end

local function IsAccountTypeForceRefreshActive(self)
    local expiresAt = self and self.forceAccountTypeRefreshUntil
    if type(expiresAt) == "number" and GetDebugTime() < expiresAt then
        return true
    end

    if self then
        self.forceAccountTypeRefreshUntil = nil
        self.forceAccountTypeRefreshStartedAt = nil
    end
    return false
end

local function GetMinimumNativeAccountTypeMedalTime(self, queryState, forceRefresh)
    local minMedalTime = nil
    if IsAccountTypeQueryPending(queryState) or (queryState and queryState.status == "retry") then
        minMedalTime = tonumber(queryState.queuedWallTime) or tonumber(queryState.sentWallTime)
    end
    if forceRefresh then
        local forceStartedAt = tonumber(self and self.forceAccountTypeRefreshStartedAt)
        if forceStartedAt and (not minMedalTime or forceStartedAt > minMedalTime) then
            minMedalTime = forceStartedAt
        end
    end
    return minMedalTime
end

EnsureState = function(self)
    AttachPersistentAccountTypeCache(self)
    if type(self.cache) ~= "table" then
        self.cache = {}
    end
    if type(self.pendingNames) ~= "table" then
        self.pendingNames = {}
    end
    if type(self.pendingLeaderAliases) ~= "table" then
        self.pendingLeaderAliases = {}
    end
    if type(self.nativeMeetingStonePendingNames) ~= "table" then
        self.nativeMeetingStonePendingNames = {}
    end
    if type(self.characterAccountTypeQueryQueue) ~= "table" then
        self.characterAccountTypeQueryQueue = {}
    end
    if type(self.characterAccountTypeQuerySet) ~= "table" then
        self.characterAccountTypeQuerySet = {}
    end
    EnsureAccountTypeQueryStates(self)
end

local function IsLeaderAwaitingAccountTypeResponse(self, leader)
    if type(leader) ~= "string" or leader == "" then
        return false
    end
    if self.pendingNames and self.pendingNames[leader] == true then
        return true
    end

    local lastSentAt = self.lastCQGLIBSentAt
    if type(lastSentAt) ~= "number" or (GetDebugTime() - lastSentAt) >= SERVER_QUERY_RESEND_SECONDS then
        return false
    end
    if type(self.lastSQGLIBAt) == "number" and self.lastSQGLIBAt >= lastSentAt then
        return false
    end

    for _, name in ipairs(SplitQueryNames(self.lastCQGLIBNames)) do
        if name == leader then
            return true
        end
    end
    return false
end

local function PruneAccountTypeCache(self)
    local cache = type(self.cache) == "table" and self.cache or nil
    if not cache then
        return
    end

    local entries = {}
    for key, data in pairs(cache) do
        entries[#entries + 1] = {
            key = key,
            medalTime = type(data) == "table" and tonumber(data.medalTime) or 0,
        }
    end
    if #entries <= ACCOUNT_TYPE_CACHE_MAX_ENTRIES then
        return
    end

    table.sort(entries, function(left, right)
        return (left.medalTime or 0) < (right.medalTime or 0)
    end)

    for index = 1, #entries - ACCOUNT_TYPE_CACHE_MAX_ENTRIES do
        cache[entries[index].key] = nil
    end
end

local function NormalizeLeaderLevel(value)
    local level = tonumber(value)
    if not level then
        return nil
    end

    level = math.floor(level)
    if level <= 0 then
        return nil
    end

    return level
end

local function NormalizeStarLevel(value)
    local level = tonumber(value)
    if not level then
        return nil
    end

    level = math.floor(level)
    if level <= 0 then
        return nil
    end

    return level
end

local function GetAccountTypeNoResponseText(reason)
    if reason == "connection_failed"
        or reason == "connect_failed"
        or reason == "send_failed"
        or reason == "not_ready"
        or reason == "socket_unavailable"
    then
        return "连接失败"
    end

    return "查询失败"
end

local function HasKnownAccountTypeData(data)
    if type(data) ~= "table" then
        return false
    end

    if IsNoResultAccountTypeData(data) then
        return false
    end

    if type(data.medalTime) == "number" then
        return true
    end
    if data.isNewbie ~= nil then
        return true
    end
    if NormalizeLeaderLevel(data.level) then
        return true
    end
    if NormalizeStarLevel(data.starLevel) then
        return true
    end

    return false
end

local function StoreAccountTypeData(self, leaderName, data, extraKey)
    if type(self) ~= "table" or type(data) ~= "table" then
        return false
    end

    local storedData = {}
    for key, value in pairs(data) do
        storedData[key] = value
    end
    storedData.accountTypeLeaderName = NormalizeLeaderName(leaderName) or TrimString(leaderName)

    local cacheKeys = BuildLeaderCacheKeys(self, leaderName)
    AddSetValue(cacheKeys, extraKey)
    local stored = false
    for cacheKey in pairs(cacheKeys) do
        local existing = self.cache[cacheKey]
        if not IsAccountTypeCacheForLeader(existing, leaderName, leaderName) then
            existing = nil
        end
        self.cache[cacheKey] = PreserveActiveNewbieAccountTypeData(existing, storedData)
        MarkAccountTypeQueryDone(self, cacheKey)
        stored = true
    end
    return stored
end

StoreNoResultAccountTypeData = function(self, leaderName, reason)
    if type(self) ~= "table" or type(leaderName) ~= "string" or leaderName == "" then
        return false
    end

    EnsureState(self)
    local now = time()
    local noResultData = {
        noAccountTypeResult = true,
        noAccountTypeReason = reason or "no_result",
        lastNoResultAt = now,
        medalTime = now,
        accountTypeLeaderName = NormalizeLeaderName(leaderName) or TrimString(leaderName),
    }

    local cacheKeys = BuildLeaderCacheKeys(self, leaderName)
    local stored = false
    for cacheKey in pairs(cacheKeys) do
        self.cache[cacheKey] = noResultData
        MarkAccountTypeQueryDone(self, cacheKey)
        stored = true
    end
    PruneAccountTypeCache(self)
    return stored
end

local function ResolvePendingNativeAccountTypeData(self)
    EnsureState(self)

    local changed = false
    local resolvedNames = {}
    for leader, state in pairs(self.accountTypeQueryStates or {}) do
        if IsAccountTypeQueryPending(state) or (state and state.status == "retry") then
            local minNativeMedalTime = GetMinimumNativeAccountTypeMedalTime(self, state, false)
            local data = GetNativeMeetingStoneRegimentData(leader)
            if HasKnownAccountTypeData(data)
                and not IsLegacyNoResultAccountTypeData(data)
                and IsAccountTypeCacheNewEnough(data, minNativeMedalTime)
                and StoreAccountTypeData(self, leader, data)
            then
                resolvedNames[#resolvedNames + 1] = leader
                changed = true
            end
        end
    end
    if changed then
        self.lastSQGLIBAt = GetDebugTime()
        self.lastSQGLIBCount = #resolvedNames
        self.lastSQGLIBChanged = true
        self.lastSQGLIBLeaderNames = table.concat(resolvedNames, ",")
    end
    return changed
end

local function AddAccountTypeBadge(badges, badge)
    if type(badges) ~= "table" or type(badge) ~= "table" or not badge.key then
        return
    end

    badges[#badges + 1] = badge
end

local function ApplyPrimaryAccountTypeBadge(info, badges)
    if type(info) ~= "table" or type(badges) ~= "table" or #badges == 0 then
        return
    end

    local primary = badges[1]
    info.status = primary.key
    info.texture = primary.texture
    info.text = primary.text
    info.compactText = primary.compactText
    info.textColor = primary.textColor
    info.width = primary.width
    info.height = primary.height
end

local function AddVeteranAccountTypeBadge(badges)
    AddAccountTypeBadge(badges, {
        key = "veteran",
        texture = VETERAN_ICON_TEXTURE,
        text = "老兵",
        compactText = "老兵",
        textColor = ACCOUNT_TYPE_GOLD_TEXT_COLOR,
        width = ACCOUNT_TYPE_ICON_SIZE,
        height = ACCOUNT_TYPE_ICON_SIZE,
    })
end

local function GetAccountTypeDebugText(accountTypeInfo)
    if type(accountTypeInfo) ~= "table" then
        return nil
    end

    if type(accountTypeInfo.badges) == "table" then
        local parts = {}
        for _, badge in ipairs(accountTypeInfo.badges) do
            if type(badge) == "table" and type(badge.text) == "string" and badge.text ~= "" then
                parts[#parts + 1] = badge.text
            end
        end
        if #parts > 0 then
            return table.concat(parts, "+")
        end
    end

    return accountTypeInfo.text or accountTypeInfo.status
end

local function GetNativeMeetingStoneLogic()
    local bridge = addonTable.MeetingStoneBridge
    if type(bridge) == "table" and type(bridge.GetRuntime) == "function" then
        local ok, _, logic = pcall(bridge.GetRuntime)
        if ok and type(logic) == "table" then
            return logic
        end
    end

    local nativeEnv = GetNativeMeetingStoneEnv()
    if nativeEnv and type(nativeEnv.Logic) == "table" then
        return nativeEnv.Logic
    end

    local libStub = _G.LibStub
    if libStub then
        local ok, aceAddon = pcall(libStub, "AceAddon-3.0")
        if ok and aceAddon and type(aceAddon.GetAddon) == "function" then
            local addonOk, nativeAddon = pcall(aceAddon.GetAddon, aceAddon, "MeetingStone", true)
            if addonOk and type(nativeAddon) == "table" and type(nativeAddon.GetModule) == "function" then
                local moduleOk, logic = pcall(nativeAddon.GetModule, nativeAddon, "Logic", true)
                if moduleOk and type(logic) == "table" then
                    return logic
                end
            end
        end
    end

    local logic = _G.Logic
    return type(logic) == "table" and logic or nil
end

local ScheduleNativeMeetingStoneRetry

local function IsNativeMeetingStoneLogicReady(logic)
    if type(logic) ~= "table" then
        return false
    end

    if logic.IsConnectServer == true then
        return true
    end

    if type(logic.IsReady) == "function" then
        local ok, ready = pcall(logic.IsReady, logic)
        if ok and ready == true then
            return true
        end
    end

    local socketTarget = GetSocketClientServerTarget(logic)
    return IsSocketClientReady(logic) == true and socketTarget ~= nil
end

local function CollectNativePendingNames(self, limit)
    local names = {}
    if type(self) ~= "table" or type(self.nativeMeetingStonePendingNames) ~= "table" then
        return names
    end

    for leader in pairs(self.nativeMeetingStonePendingNames) do
        names[#names + 1] = leader
    end
    table.sort(names)
    limit = tonumber(limit)
    if limit and #names > limit then
        for index = #names, limit + 1, -1 do
            names[index] = nil
        end
    end
    return names
end

local function MarkNativeMeetingStoneQueriesSent(self, names)
    if type(self) ~= "table" or type(names) ~= "table" or #names == 0 then
        return
    end

    local sentAt = GetDebugTime()
    self.lastCQGLIBNames = table.concat(names, ",")
    self.lastCQGLIBSentAt = sentAt
    self.lastCQGLIBSendMethod = "netease"
    self.lastCQGLIBSendSucceeded = true
    for _, leader in ipairs(names) do
        MarkAccountTypeQuerySent(self, leader, sentAt, "netease")
    end
end

local function FlushNativeMeetingStoneQueries(self)
    EnsureState(self)
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return false
    end

    local logic = GetNativeMeetingStoneLogic()
    if not logic then
        if next(self.nativeMeetingStonePendingNames) then
            ScheduleNativeMeetingStoneRetry(self)
        end
        return false
    end

    local names = CollectNativePendingNames(self, ACCOUNT_TYPE_SERVER_BATCH_SIZE)
    if #names > 0 and type(logic.InsertServerCQGLIB) == "function" then
        for _, leader in ipairs(names) do
            pcall(logic.InsertServerCQGLIB, logic, leader)
        end
    end

    if not IsNativeMeetingStoneLogicReady(logic) then
        if next(self.nativeMeetingStonePendingNames) then
            ScheduleNativeMeetingStoneRetry(self)
        end
        return false
    end

    if #names == 0 then
        if type(logic.SendServerCQGLIB) == "function" then
            pcall(logic.SendServerCQGLIB, logic)
        end
        return true
    end

    local sent = false
    if type(logic.SendServerCQGLIB) == "function" then
        local ok = pcall(logic.SendServerCQGLIB, logic)
        sent = ok == true
    elseif type(logic.SendServer) == "function" then
        local ok = pcall(logic.SendServer, logic, "CQGLIB", table.concat(names, ","))
        sent = ok == true
    end

    if sent then
        for _, leader in ipairs(names) do
            self.nativeMeetingStonePendingNames[leader] = nil
        end
        MarkNativeMeetingStoneQueriesSent(self, names)
        ResolvePendingNativeAccountTypeData(self)
        if next(self.nativeMeetingStonePendingNames) then
            ScheduleNativeMeetingStoneRetry(self)
        end
        return true
    end

    if next(self.nativeMeetingStonePendingNames) then
        ScheduleNativeMeetingStoneRetry(self)
    end
    return false
end

local function ScheduleNativeMeetingStoneFlush(self)
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return
    end

    if self.nativeMeetingStoneFlushScheduled then
        return
    end

    self.nativeMeetingStoneFlushScheduled = true
    if C_Timer and C_Timer.After then
        C_Timer.After(QUERY_DEBOUNCE_SECONDS, function()
            MeetingStoneData.nativeMeetingStoneFlushScheduled = false
            FlushNativeMeetingStoneQueries(MeetingStoneData)
        end)
    else
        self.nativeMeetingStoneFlushScheduled = false
        FlushNativeMeetingStoneQueries(self)
    end
end

local function RememberNativeMeetingStonePendingName(self, leader)
    EnsureState(self)

    local isNewName = self.nativeMeetingStonePendingNames[leader] ~= true
    self.nativeMeetingStonePendingNames[leader] = true
    if isNewName then
        self.nativeMeetingStoneRetryCount = 0
    end
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return
    end
    ScheduleNativeMeetingStoneRetry(self)
end

local function QueueNativeMeetingStoneQuery(self, leader)
    EnsureState(self)
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return false
    end

    local logic = GetNativeMeetingStoneLogic()
    if not (logic and type(logic.InsertServerCQGLIB) == "function") then
        if GetNativeMeetingStoneEnv() then
            RememberNativeMeetingStonePendingName(self, leader)
        end
        return false
    end

    local ok = pcall(logic.InsertServerCQGLIB, logic, leader)
    if not ok then
        RememberNativeMeetingStonePendingName(self, leader)
        return false
    end

    self.nativeMeetingStonePendingNames[leader] = true
    self.nativeMeetingStoneRetryCount = 0
    ScheduleNativeMeetingStoneFlush(self)
    return true
end

function MeetingStoneData:FlushNativeMeetingStonePendingQueries()
    EnsureState(self)
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return false
    end

    local names = {}
    for leader in pairs(self.nativeMeetingStonePendingNames) do
        names[#names + 1] = leader
    end
    table.sort(names)
    if #names > ACCOUNT_TYPE_SERVER_BATCH_SIZE then
        for index = #names, ACCOUNT_TYPE_SERVER_BATCH_SIZE + 1, -1 do
            names[index] = nil
        end
    end

    if #names == 0 then
        return true
    end

    local logic = GetNativeMeetingStoneLogic()
    if not (logic and type(logic.InsertServerCQGLIB) == "function") then
        local _, sourceFallback = GetAddonSource()
        if sourceFallback == true then
            wipe(self.nativeMeetingStonePendingNames)
            self.nativeMeetingStoneRetryCount = 0
            self.nativeMeetingStoneRetryScheduled = false
            return false
        end
        ScheduleNativeMeetingStoneRetry(self)
        return false
    end

    local queued = false
    for _, leader in ipairs(names) do
        local ok = pcall(logic.InsertServerCQGLIB, logic, leader)
        if ok then
            self.nativeMeetingStonePendingNames[leader] = nil
            queued = true
        end
    end

    if queued then
        self.nativeMeetingStoneRetryCount = 0
        ScheduleNativeMeetingStoneFlush(self)
    end

    if next(self.nativeMeetingStonePendingNames) then
        ScheduleNativeMeetingStoneRetry(self)
    end

    return queued
end

ScheduleNativeMeetingStoneRetry = function(self)
    EnsureState(self)
    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        return
    end

    if self.nativeMeetingStoneRetryScheduled or not next(self.nativeMeetingStonePendingNames) then
        return
    end

    local retryCount = tonumber(self.nativeMeetingStoneRetryCount) or 0
    if retryCount >= NATIVE_QUERY_RETRY_LIMIT then
        return
    end

    if not (C_Timer and C_Timer.After) then
        return
    end

    self.nativeMeetingStoneRetryScheduled = true
    C_Timer.After(NATIVE_QUERY_RETRY_SECONDS, function()
        MeetingStoneData.nativeMeetingStoneRetryScheduled = false
        MeetingStoneData.nativeMeetingStoneRetryCount = (tonumber(MeetingStoneData.nativeMeetingStoneRetryCount) or 0) + 1
        MeetingStoneData:FlushNativeMeetingStonePendingQueries()
    end)
end

local function CreateSocketClient(self)
    local socket = NetEaseSocket or LibStub("NetEaseSocket-2.0", true)
    if not socket or (IsTrialAccount and IsTrialAccount()) then
        return nil
    end

    local client = {
        owner = self,
    }

    socket:Embed(client)

    function client:SERVER_CONNECTED()
        if self.owner and self.owner.HandleSocketServerConnected then
            self.owner:HandleSocketServerConnected()
        end
    end

    function client:SERVER_DISCONNECTED()
        if self.owner and self.owner.HandleSocketServerDisconnected then
            self.owner:HandleSocketServerDisconnected()
        end
    end

    function client:SVERSION(eventName, version, url, isSupport, changeLog)
        if self.owner and self.owner.RecordServerMessage then
            self.owner:RecordServerMessage("SVERSION")
        end
        if self.owner and self.owner.HandleServerVersion then
            self.owner:HandleServerVersion(version, url, isSupport, changeLog)
        end
    end

    function client:SQTB(eventName, isDeal)
        if self.owner and self.owner.RecordServerMessage then
            self.owner:RecordServerMessage("SQTB")
        end
        if self.owner and self.owner.HandleSQTB then
            self.owner:HandleSQTB(isDeal)
        end
    end

    function client:SQGLIB(eventName, maps)
        if self.owner and self.owner.RecordServerMessage then
            self.owner:RecordServerMessage("SQGLIB")
        end
        if self.owner and self.owner.HandleSQGLIB then
            self.owner:HandleSQGLIB(type(eventName) == "table" and eventName or maps)
        end
    end

    local ok = pcall(client.ListenSocket, client, COMM_PREFIX, GetAddonServer())
    if not ok then
        return nil
    end

    client:RegisterServer("SERVER_CONNECTED")
    client:RegisterServer("SERVER_DISCONNECTED")
    client:RegisterServer("SVERSION")
    client:RegisterServer("SQTB")
    client:RegisterServer("SQGLIB")

    return client
end

function MeetingStoneData:GetSocketClient()
    if self.socketClient then
        return self.socketClient
    end

    self.socketClient = CreateSocketClient(self)
    return self.socketClient
end

function MeetingStoneData:HandleSocketServerConnected()
    self.ready = true
    self.connecting = false
    self.target = GetSocketClientTarget(self.socketClient) or self.target
    self.lastSocketConnectedAt = GetDebugTime()
    self:RecordServerMessage("连接成功", self.target, "WHISPER")
    if self:IsCombatPaused() then
        self.loginDeferred = true
        self.combatDeferred = true
        return
    end

    self:SendLogin()
    self:ScheduleFlush(SERVER_LOGIN_QUERY_DELAY_SECONDS)
end

function MeetingStoneData:HandleSocketServerDisconnected()
    self.ready = false
    self.connecting = false
    self.lastSocketDisconnectedAt = GetDebugTime()

    EnsureState(self)
    if next(self.pendingNames) then
        if self:IsCombatPaused() then
            self.combatDeferred = true
        else
            self:Connect()
        end
    end
end

function MeetingStoneData:Initialize(owner)
    self.owner = owner
    EnsureState(self)
    PruneAccountTypeCache(self)

    if self.initialized then
        return
    end

    self.initialized = true
    self.connectKey = tostring((fastrandom or random)(0x100000, 0xFFFFFF))
    self:GetSocketClient()

    if owner and owner.RegisterComm then
        owner:RegisterComm(COMM_PREFIX, function(prefix, message, distribution, sender)
            MeetingStoneData:OnCommReceived(prefix, message, distribution, sender)
        end)
    end

    if owner and owner.RegisterMessage then
        owner:RegisterMessage("MEETINGSTONE_SQGLIB_UPDATED", function()
            ResolvePendingNativeAccountTypeData(MeetingStoneData)
            ScheduleMeetingStoneDataRefresh(MeetingStoneData, "MEETINGSTONE_SQGLIB_UPDATED")
        end)
    end

    if not self.nativeMeetingStoneEventFrame and CreateFrame then
        self.nativeMeetingStoneEventFrame = CreateFrame("Frame")
        self.nativeMeetingStoneEventFrame:RegisterEvent("ADDON_LOADED")
        self.nativeMeetingStoneEventFrame:SetScript("OnEvent", function(_, _, loadedAddonName)
            if loadedAddonName == "MeetingStone" or loadedAddonName == "MeetingStoneEX" then
                if C_Timer and C_Timer.After then
                    C_Timer.After(0, function()
                        MeetingStoneData:FlushNativeMeetingStonePendingQueries()
                    end)
                else
                    MeetingStoneData:FlushNativeMeetingStonePendingQueries()
                end
            end
        end)
    end
end

function MeetingStoneData:Start()
    EnsureState(self)
    if self:IsCombatPaused() then
        self.combatDeferred = true
        self:QueueCombatDeferredQueries()
        return
    end

    self:Connect()
    self:FlushNativeMeetingStonePendingQueries()
end

function MeetingStoneData:IsCombatPaused()
    return IsMeetingStoneDataCombatPaused(self)
end

function MeetingStoneData:QueueCombatDeferredQueries()
    EnsureState(self)
    for _, leader in ipairs(SplitQueryNames(self.lastCQGLIBNames)) do
        local queryState = GetAccountTypeQueryState(self, leader)
        if IsAccountTypeQueryPending(queryState) then
            self.pendingNames[leader] = true
        end
    end
end

function MeetingStoneData:SetCombatPaused(paused)
    EnsureState(self)

    local wasPaused = self.combatPaused == true
    self.combatPaused = paused == true

    if IsMeetingStoneDataCombatPaused(self) then
        self.combatDeferred = true
        self:QueueCombatDeferredQueries()
        return
    end

    if not wasPaused and not self.combatDeferred then
        return
    end

    self.combatDeferred = nil
    self.connecting = false

    if self.loginDeferred == true and self.ready == true then
        self.loginDeferred = nil
        self:SendLogin()
        self:ScheduleFlush(SERVER_LOGIN_QUERY_DELAY_SECONDS)
    else
        self.loginDeferred = nil
    end

    self:FlushNativeMeetingStonePendingQueries()

    if next(self.pendingNames) or not self.ready then
        self:Connect()
    end
    if next(self.pendingNames) then
        self:ScheduleFlush()
    end

    ScheduleAccountTypeQueryStateRefresh(self)
end

function MeetingStoneData:GetTarget()
    if self.target and self.target ~= "" then
        return self.target
    end
    self.connectTarget = self.connectTarget or FormatTarget(GetAddonServer())
    return self.connectTarget
end

function MeetingStoneData:Send(target, cmd, ...)
    if not self.owner or not self.owner.SendCommMessage or type(target) ~= "string" or target == "" then
        return false
    end

    local ok = pcall(self.owner.SendCommMessage, self.owner, COMM_PREFIX, AceSerializer:Serialize(cmd, ...), "WHISPER", target)
    return ok == true
end

function MeetingStoneData:RecordServerSend(cmd, sent, method, target)
    self.lastServerSendAt = GetDebugTime()
    self.lastServerSendCmd = cmd
    self.lastServerSendSucceeded = sent == true
    self.lastServerSendMethod = method
    self.lastServerSendTarget = target or self.target
end

function MeetingStoneData:RecordServerMessage(cmd, sender, distribution)
    self.lastServerMessageAt = GetDebugTime()
    self.lastServerMessageCmd = cmd
    self.lastServerMessageSender = sender or self.lastServerMessageSender
    self.lastServerMessageDistribution = distribution or self.lastServerMessageDistribution
end

function MeetingStoneData:SendServer(cmd, ...)
    if self:IsCombatPaused() then
        self.combatDeferred = true
        self:RecordServerSend(cmd, false, "combat_paused", self.target)
        return false, "combat_paused"
    end

    local socketClient = self:GetSocketClient()
    local socketTarget = GetSocketClientServerTarget(socketClient)
    if IsSocketClientReady(socketClient) and socketTarget then
        self.target = socketTarget
        local ok = socketClient.SendServer and pcall(socketClient.SendServer, socketClient, cmd, ...)
        local sent = ok == true
        self:RecordServerSend(cmd, sent, "socket", socketTarget)
        return sent, "socket"
    end

    if not self.ready or not self.target then
        self:RecordServerSend(cmd, false, "not_ready", self.target)
        return false, "not_ready"
    end

    local sent = self:Send(self.target, cmd, ...)
    self:RecordServerSend(cmd, sent, "acecomm", self.target)
    return sent, "acecomm"
end

function MeetingStoneData:Connect()
    EnsureState(self)

    if self:IsCombatPaused() then
        self.combatDeferred = true
        return
    end

    if self.ready or self.connecting then
        return
    end

    local socketClient = self:GetSocketClient()
    if socketClient and socketClient.ConnectServer then
        local socketTarget = GetSocketClientServerTarget(socketClient)
        if IsSocketClientReady(socketClient) and socketTarget then
            self.ready = true
            self.connecting = false
            self.target = socketTarget
            self:SendLogin()
            self:ScheduleFlush(SERVER_LOGIN_QUERY_DELAY_SECONDS)
            return
        end

        self.connecting = true
        local ok = pcall(socketClient.ConnectServer, socketClient, GetAddonServer())
        if not ok then
            self.connecting = false
            return
        end

        C_Timer.After(CONNECT_RETRY_SECONDS, function()
            if MeetingStoneData.ready then
                return
            end

            local delayedSocketTarget = GetSocketClientServerTarget(MeetingStoneData.socketClient)
            if IsSocketClientReady(MeetingStoneData.socketClient) and delayedSocketTarget then
                MeetingStoneData.ready = true
                MeetingStoneData.connecting = false
                MeetingStoneData.target = delayedSocketTarget
                if MeetingStoneData:IsCombatPaused() then
                    MeetingStoneData.loginDeferred = true
                    MeetingStoneData.combatDeferred = true
                    return
                end

                MeetingStoneData:SendLogin()
                MeetingStoneData:ScheduleFlush(SERVER_LOGIN_QUERY_DELAY_SECONDS)
                return
            end

            local fallbackTarget = MeetingStoneData:GetTarget()
            if fallbackTarget then
                local sent = MeetingStoneData:Send(fallbackTarget, "NETEASE_CONNECT", MeetingStoneData.connectKey)
                MeetingStoneData:RecordServerSend("NETEASE_CONNECT", sent, "acecomm_connect_fallback", fallbackTarget)
            end

            MeetingStoneData.connecting = false
            EnsureState(MeetingStoneData)
            if next(MeetingStoneData.pendingNames) then
                MeetingStoneData:Connect()
            end
        end)
        return
    end

    local target = self:GetTarget()
    if not target then
        return
    end

    self.connecting = true
    local sent = self:Send(target, "NETEASE_CONNECT", self.connectKey)
    self:RecordServerSend("NETEASE_CONNECT", sent, "acecomm_connect", target)

    C_Timer.After(CONNECT_RETRY_SECONDS, function()
        if MeetingStoneData.ready then
            return
        end

        MeetingStoneData.connecting = false
        EnsureState(MeetingStoneData)
        if next(MeetingStoneData.pendingNames) then
            MeetingStoneData:Connect()
        end
    end)
end

function MeetingStoneData:SendLogin()
    local battleTag = nil
    if BNGetInfo then
        battleTag = select(2, BNGetInfo())
    end

    local version = GetMeetingStoneProtocolVersion()
    local source, sourceFallback = GetAddonSource()
    local queryData = GetServerLoginQueryData()
    self.lastSLOGINAt = GetDebugTime()
    self.lastSLOGINVersion = version
    self.lastSLOGINSource = source
    self.lastSLOGINSourceFallback = sourceFallback == true
    self.lastSLOGINQueryDataType = type(queryData)
    self.lastSLOGINQueryDataCount = CountTableValues(queryData)
    self.lastSLOGINQueryDataKeys = FormatTableKeys(queryData, 8)
    local sent, method = self:SendServer("SLOGIN", version, UnitGUID and UnitGUID("player") or nil, source, battleTag, queryData)
    self.lastSLOGINSendSucceeded = sent == true
    self.lastSLOGINSendMethod = method
    self.lastSLOGINSendTarget = self.lastServerSendTarget
    local cqtbSent, cqtbMethod = self:SendServer("CQTB")
    self.lastCQTBQuerySendSucceeded = cqtbSent == true
    self.lastCQTBQuerySendMethod = cqtbMethod
end

function MeetingStoneData:HandleServerVersion(version, url, isSupport, changeLog)
    self.lastSVERSIONAt = GetDebugTime()
    self.lastSVERSIONVersion = version
    self.lastSVERSIONUrl = url
    self.lastSVERSIONSupported = isSupport == true
    self.lastSVERSIONChangeLog = changeLog
end

function MeetingStoneData:HandleSQTB(isDeal)
    self.lastSQTBAt = GetDebugTime()
    self.lastSQTBIsDeal = isDeal == true
end

function MeetingStoneData:OnCommReceived(prefix, message, distribution, sender)
    if prefix ~= COMM_PREFIX or type(message) ~= "string" then
        return
    end

    local ok, cmd, arg1, arg2, arg3, arg4 = AceSerializer:Deserialize(message)
    if not ok then
        return
    end

    self:RecordServerMessage(cmd, sender, distribution)

    if cmd == "NETEASE_CONNECT_SUCCESS" then
        if arg1 ~= self.connectKey then
            return
        end

        self.target = sender
        self.ready = true
        self.connecting = false
        if self:IsCombatPaused() then
            self.loginDeferred = true
            self.combatDeferred = true
            return
        end

        self:SendLogin()
        self:ScheduleFlush(SERVER_LOGIN_QUERY_DELAY_SECONDS)
        return
    end

    if cmd == "SVERSION" then
        self:HandleServerVersion(arg1, arg2, arg3, arg4)
        return
    end

    if cmd == "SQTB" then
        self:HandleSQTB(arg1)
        return
    end

    if cmd == "SQGLIB" then
        local maps = type(arg1) == "table" and arg1 or arg2
        self:HandleSQGLIB(maps)
    end
end

function MeetingStoneData:HandleSQGLIB(maps)
    if type(maps) ~= "table" then
        EnsureState(self)
        self.lastSQGLIBInvalidAt = GetDebugTime()
        self.lastSQGLIBInvalidType = type(maps)
        for _, queryName in ipairs(SplitQueryNames(self.lastCQGLIBNames)) do
            MarkAccountTypeQueryRetry(self, queryName, "invalid_response")
        end
        ScheduleMeetingStoneDataRefresh(self, "SQGLIBInvalid")
        return
    end

    EnsureState(self)

    local changed = false
    local queryStateChanged = false
    local receivedCount = 0
    local receivedLeaderNames = {}
    local lastQueryNames = SplitQueryNames(self.lastCQGLIBNames)
    local singleQueryName = #lastQueryNames == 1 and lastQueryNames[1] or nil
    local resolvedQueryNames = {}
    local mismatchedQueryNames = {}
    local mismatchedQueryNameSet = {}
    for leaderName, data in pairs(maps) do
        receivedCount = receivedCount + 1
        if type(leaderName) == "string" and type(data) == "table" then
            receivedLeaderNames[#receivedLeaderNames + 1] = leaderName
            local newbieFlag = data.n or data["n"] or 0
            local remainCount = data.r or data["r"] or 0
            local cacheKeys = BuildLeaderCacheKeys(self, leaderName)
            if singleQueryName then
                if IsSameLeaderName(singleQueryName, leaderName) then
                    AddSetValue(cacheKeys, singleQueryName)
                else
                    mismatchedQueryNames[#mismatchedQueryNames + 1] = singleQueryName .. "->" .. leaderName
                    mismatchedQueryNameSet[singleQueryName] = true
                end
            end
            for cacheKey in pairs(cacheKeys) do
                self.pendingNames[cacheKey] = nil
                self.nativeMeetingStonePendingNames[cacheKey] = nil
            end
            local regimentData = nil
            for cacheKey in pairs(cacheKeys) do
                if type(self.cache[cacheKey]) == "table" and IsAccountTypeCacheForLeader(self.cache[cacheKey], leaderName, leaderName) then
                    regimentData = self.cache[cacheKey]
                    break
                end
            end
            regimentData = regimentData or {}
            local keepExistingNewbie = IsActiveNewbieAccountTypeData(regimentData) and newbieFlag ~= 1

            regimentData.level = data.l or data["l"]
            regimentData.starLevel = data.s or data["s"]
            regimentData.roomID = data.c or data["c"]
            regimentData.bgID = data.b or data["b"] or 0
            regimentData.source = self.lastSLOGINSource
            regimentData.sourceFallback = self.lastSLOGINSourceFallback == true
            regimentData.medalMap = {
                acc_exp = data.ae,
                next_star_level_threshold = data.ne,
                medal = data.m,
            }
            regimentData.medalTime = time()
            regimentData.accountTypeLeaderName = NormalizeLeaderName(leaderName) or leaderName
            if newbieFlag == 1 then
                regimentData.isNewbie = true
                regimentData.newbieExpireTime = time() + remainCount * 30 * 60
            elseif keepExistingNewbie then
                regimentData.isNewbie = true
                regimentData.newbiePreservedAt = time()
            else
                regimentData.isNewbie = false
                regimentData.newbieExpireTime = nil
            end

            for cacheKey in pairs(cacheKeys) do
                self.cache[cacheKey] = regimentData
                resolvedQueryNames[cacheKey] = true
                MarkAccountTypeQueryDone(self, cacheKey)
                queryStateChanged = true
                changed = true
            end
        end
    end

    for _, queryName in ipairs(lastQueryNames) do
        if not resolvedQueryNames[queryName] then
            MarkAccountTypeQueryRetry(self, queryName, mismatchedQueryNameSet[queryName] and "mismatched_response" or "missing_response")
            queryStateChanged = true
        end
    end

    self.lastSQGLIBAt = GetDebugTime()
    self.lastSQGLIBCount = receivedCount
    self.lastSQGLIBChanged = changed
    self.lastSQGLIBLeaderNames = table.concat(receivedLeaderNames, ",")
    self.lastSQGLIBMismatchedNames = #mismatchedQueryNames > 0 and table.concat(mismatchedQueryNames, ",") or nil
    PruneAccountTypeCache(self)

    if changed or queryStateChanged then
        ScheduleMeetingStoneDataRefresh(self, "SQGLIB")
    end
end

function MeetingStoneData:ScheduleFlush(delaySeconds)
    EnsureState(self)
    if self:IsCombatPaused() then
        self.combatDeferred = true
        return
    end

    if self.flushScheduled then
        return
    end

    delaySeconds = tonumber(delaySeconds) or QUERY_DEBOUNCE_SECONDS
    if delaySeconds < QUERY_DEBOUNCE_SECONDS then
        delaySeconds = QUERY_DEBOUNCE_SECONDS
    end

    self.flushScheduled = true
    C_Timer.After(delaySeconds, function()
        MeetingStoneData.flushScheduled = false
        if MeetingStoneData:IsCombatPaused() then
            MeetingStoneData.combatDeferred = true
            return
        end

        MeetingStoneData:FlushQueries()
    end)
end

function MeetingStoneData:QueryLeader(name, realm)
    EnsureState(self)

    local leader = NormalizeLeaderName(name, realm)
    if not leader then
        return
    end

    local forceRefresh = IsAccountTypeForceRefreshActive(self)
    local queryState = GetAccountTypeQueryState(self, leader)
    local minNativeMedalTime = GetMinimumNativeAccountTypeMedalTime(self, queryState, forceRefresh)
    local cached = GetCachedLeaderData(self, leader, name, forceRefresh and not minNativeMedalTime, minNativeMedalTime)
    if not forceRefresh and IsCacheFresh(cached) then
        return
    end

    TrackLeaderAlias(self, leader)
    if self:IsCombatPaused() then
        MarkAccountTypeQueryPending(self, leader)
        self.pendingNames[leader] = true
        self.combatDeferred = true
        return
    end

    if queryState and queryState.status == "retry" then
        if self.pendingNames and self.pendingNames[leader] == true then
            return
        end
        MarkAccountTypeQueryRetry(self, leader, queryState.failureReason or "timeout")
        return
    end

    if IsLeaderAwaitingAccountTypeResponse(self, leader) then
        MarkAccountTypeQueryPending(self, leader)
        return
    end
    MarkAccountTypeQueryPending(self, leader)

    local queuedNative = QueueNativeMeetingStoneQuery(self, leader)
    if queuedNative then
        self.lastNativeCQGLIBNames = leader
        self.lastNativeCQGLIBQueuedAt = GetDebugTime()
    end

    -- 不能只依赖集合石原生 InsertServerCQGLIB 路径：原生库有时不会把回包事件广播给本插件，
    -- 之前这里直接 return 会让状态一直停在 pending/sent。现在把原生路径当作并行加速/缓存来源，
    -- 同时仍通过本插件自己的受控批处理 Socket 兜底。
    self.pendingNames[leader] = true
    if self.ready then
        self:ScheduleFlush(queuedNative and ACCOUNT_TYPE_NATIVE_PARALLEL_FALLBACK_DELAY_SECONDS or nil)
    else
        self:Connect()
    end
end

local ScheduleCharacterAccountTypeQueryBatch

local function ShouldQueueCharacterAccountTypeQuery(self, name, realm, leader)
    local forceRefresh = IsAccountTypeForceRefreshActive(self)
    local queryState = GetAccountTypeQueryState(self, leader)
    local minNativeMedalTime = GetMinimumNativeAccountTypeMedalTime(self, queryState, forceRefresh)
    local cached = GetCachedLeaderData(self, leader, name, forceRefresh and not minNativeMedalTime, minNativeMedalTime)
    if not forceRefresh and IsCacheFresh(cached) then
        return false
    end
    if IsAccountTypeQueryPending(queryState) or IsLeaderAwaitingAccountTypeResponse(self, leader) then
        return false
    end
    if self.characterAccountTypeQuerySet and self.characterAccountTypeQuerySet[leader] == true then
        return false
    end
    return true
end

local function ProcessCharacterAccountTypeQueryBatch(self)
    EnsureState(self)

    local queue = self.characterAccountTypeQueryQueue or {}
    local index = tonumber(self.characterAccountTypeQueryIndex) or 1
    local processed = 0
    while processed < ACCOUNT_TYPE_CHARACTER_QUERY_BATCH_SIZE and index <= #queue do
        local entry = queue[index]
        index = index + 1
        processed = processed + 1

        local leader = type(entry) == "table" and entry.leader or nil
        if leader and self.characterAccountTypeQuerySet then
            self.characterAccountTypeQuerySet[leader] = nil
        end
        if type(entry) == "table" and entry.name then
            self:QueryLeader(entry.name, entry.realm)
        end
    end

    if index <= #queue then
        self.characterAccountTypeQueryIndex = index
        ScheduleCharacterAccountTypeQueryBatch(self)
        return
    end

    self.characterAccountTypeQueryQueue = {}
    self.characterAccountTypeQuerySet = {}
    self.characterAccountTypeQueryIndex = nil
end

ScheduleCharacterAccountTypeQueryBatch = function(self)
    if type(self) ~= "table" or self.characterAccountTypeQueryScheduled then
        return
    end

    self.characterAccountTypeQueryScheduled = true
    local function runBatch()
        MeetingStoneData.characterAccountTypeQueryScheduled = false
        ProcessCharacterAccountTypeQueryBatch(MeetingStoneData)
    end

    if C_Timer and C_Timer.After then
        C_Timer.After(ACCOUNT_TYPE_CHARACTER_QUERY_DELAY_SECONDS, runBatch)
    else
        runBatch()
    end
end

function MeetingStoneData:QueryCharacters(characterEntries)
    if type(characterEntries) ~= "table" then
        return
    end

    EnsureState(self)
    local queried = {}
    local queuedCount = 0
    for _, characterEntry in ipairs(characterEntries) do
        local name, realm = GetCharacterNameAndRealm(characterEntry)
        local leader = NormalizeLeaderName(name, realm)
        if leader and not queried[leader] and ShouldQueueCharacterAccountTypeQuery(self, name, realm, leader) then
            queried[leader] = true
            self.characterAccountTypeQuerySet[leader] = true
            self.characterAccountTypeQueryQueue[#self.characterAccountTypeQueryQueue + 1] = {
                name = name,
                realm = realm,
                leader = leader,
            }
            queuedCount = queuedCount + 1
        end
    end

    if queuedCount > 0 then
        ScheduleCharacterAccountTypeQueryBatch(self)
    end
    return queuedCount
end

function MeetingStoneData:FlushQueries()
    EnsureState(self)

    if self:IsCombatPaused() then
        self.combatDeferred = true
        return
    end

    if not self.ready then
        self:Connect()
        return
    end

    local names = {}
    for name in pairs(self.pendingNames) do
        names[#names + 1] = name
    end

    if #names == 0 then
        return
    end

    local loginDelayRemaining = GetServerLoginQueryDelayRemaining(self)
    if loginDelayRemaining > 0 then
        self:ScheduleFlush(loginDelayRemaining)
        return
    end

    table.sort(names)
    local batchNames = {}
    local batchLimit = math.max(1, tonumber(ACCOUNT_TYPE_SERVER_BATCH_SIZE) or 8)
    for index = 1, math.min(#names, batchLimit) do
        batchNames[#batchNames + 1] = names[index]
    end

    local queryNames = table.concat(batchNames, ",")
    local sent, method = self:SendServer("CQGLIB", queryNames)
    self.lastCQGLIBNames = queryNames
    self.lastCQGLIBSentAt = GetDebugTime()
    self.lastCQGLIBSendMethod = method
    self.lastCQGLIBSendSucceeded = sent == true
    if sent then
        for _, name in ipairs(batchNames) do
            MarkAccountTypeQuerySent(self, name, self.lastCQGLIBSentAt, method)
            self.pendingNames[name] = nil
        end
        if next(self.pendingNames) then
            self:ScheduleFlush(QUERY_DEBOUNCE_SECONDS)
        end
    else
        local failureReason = method == "not_ready" and "connection_failed" or "send_failed"
        for _, name in ipairs(batchNames) do
            MarkAccountTypeQueryRetry(self, name, failureReason)
        end
    end
end

function MeetingStoneData:ClearAccountTypeCache()
    EnsureState(self)

    local clearedCount = CountTableValues(self.cache)
    if type(self.cache) == "table" then
        wipe(self.cache)
    else
        self.cache = {}
    end

    if type(self.pendingNames) == "table" then
        wipe(self.pendingNames)
    end
    if type(self.pendingLeaderAliases) == "table" then
        wipe(self.pendingLeaderAliases)
    end
    if type(self.nativeMeetingStonePendingNames) == "table" then
        wipe(self.nativeMeetingStonePendingNames)
    end
    if type(self.characterAccountTypeQueryQueue) == "table" then
        wipe(self.characterAccountTypeQueryQueue)
    end
    if type(self.characterAccountTypeQuerySet) == "table" then
        wipe(self.characterAccountTypeQuerySet)
    end
    self.characterAccountTypeQueryIndex = nil
    self.characterAccountTypeQueryScheduled = false
    if type(self.accountTypeQueryStates) == "table" then
        wipe(self.accountTypeQueryStates)
    end

    self.lastCQGLIBNames = nil
    self.lastCQGLIBSentAt = nil
    self.lastCQGLIBSendMethod = nil
    self.lastCQGLIBSendSucceeded = nil
    self.lastSQGLIBAt = nil
    self.lastSQGLIBCount = nil
    self.lastSQGLIBChanged = nil
    self.lastSQGLIBLeaderNames = nil
    self.lastSQGLIBMismatchedNames = nil
    self.forceAccountTypeRefreshUntil = GetDebugTime() + ACCOUNT_TYPE_FORCE_REFRESH_SECONDS
    self.forceAccountTypeRefreshStartedAt = time()

    return clearedCount
end

function MeetingStoneData:GetAccountTypeInfo(name, realm)
    EnsureState(self)

    local leader = NormalizeLeaderName(name, realm)
    local queryState = GetAccountTypeQueryState(self, leader)
    local forceRefresh = IsAccountTypeForceRefreshActive(self)
    local minNativeMedalTime = GetMinimumNativeAccountTypeMedalTime(self, queryState, forceRefresh)
    local data = GetCachedLeaderData(self, leader, name, forceRefresh and not minNativeMedalTime, minNativeMedalTime)
    local hasKnownData = HasKnownAccountTypeData(data)
    local noResultData = IsFreshNoResultAccountTypeData(data) and data or nil
    local isQueryPending = IsAccountTypeQueryActive(queryState)
    local displayQueryPending = isQueryPending or IsAccountTypeQueryDisplayPending(queryState)
    if hasKnownData and leader then
        MarkAccountTypeQueryDone(self, leader)
    end
    data = (hasKnownData or noResultData) and data or {}
    local now = time()
    local newbieExpireTime = tonumber(data.newbieExpireTime)
    local newbieExpired = data.isNewbie == true and (not newbieExpireTime or now >= newbieExpireTime)
    local isNewbie = data.isNewbie == true and newbieExpireTime ~= nil and now < newbieExpireTime
    local leaderLevel = NormalizeLeaderLevel(data.level)
    local starLevel = NormalizeStarLevel(data.starLevel)
    local badges = {}
    local info = {
        level = leaderLevel,
        starLevel = starLevel,
        isNewbie = isNewbie,
        newbieExpired = newbieExpired == true,
        queryPending = displayQueryPending == true,
        activeQueryPending = isQueryPending == true,
        badges = badges,
    }

    if not leader then
        info.status = "unknown"
        info.text = ""
        info.compactText = ""
        info.textColor = ACCOUNT_TYPE_PENDING_TEXT_COLOR
    elseif not hasKnownData and displayQueryPending then
        info.status = "pending"
        info.text = "···"
        info.compactText = "···"
        info.textColor = ACCOUNT_TYPE_PENDING_TEXT_COLOR
    elseif not hasKnownData and noResultData then
        local failureText = GetAccountTypeNoResponseText(noResultData.noAccountTypeReason)
        info.status = "no_response"
        info.text = failureText
        info.compactText = failureText
        info.textColor = ACCOUNT_TYPE_RED_TEXT_COLOR
        info.failureReason = noResultData.noAccountTypeReason
    elseif not hasKnownData then
        info.status = "unknown"
        info.text = ""
        info.compactText = ""
        info.textColor = ACCOUNT_TYPE_PENDING_TEXT_COLOR
    else
        if isNewbie then
            AddAccountTypeBadge(badges, {
                key = "newbie",
                texture = NEWBIE_ICON_TEXTURE,
                text = "大秘新兵",
                compactText = "新兵",
                textColor = ACCOUNT_TYPE_GREEN_TEXT_COLOR,
                width = ACCOUNT_TYPE_ICON_SIZE,
                height = ACCOUNT_TYPE_ICON_SIZE,
            })
        else
            if leaderLevel then
                AddAccountTypeBadge(badges, {
                    key = "leader",
                    texture = LEADER_ICON_TEXTURE,
                    text = "火车头 Lv" .. tostring(leaderLevel),
                    compactText = "Lv" .. tostring(leaderLevel),
                    textColor = ACCOUNT_TYPE_GOLD_TEXT_COLOR,
                    width = ACCOUNT_TYPE_ICON_SIZE,
                    height = ACCOUNT_TYPE_ICON_SIZE,
                    level = leaderLevel,
                })
            end
            if starLevel then
                AddAccountTypeBadge(badges, {
                    key = "star",
                    texture = STAR_ICON_TEXTURE,
                    text = "星团长 Lv" .. tostring(starLevel),
                    compactText = "Lv" .. tostring(starLevel),
                    textColor = ACCOUNT_TYPE_GOLD_TEXT_COLOR,
                    width = ACCOUNT_TYPE_ICON_SIZE,
                    height = ACCOUNT_TYPE_ICON_SIZE,
                    level = starLevel,
                })
            end
            AddVeteranAccountTypeBadge(badges)
        end
        ApplyPrimaryAccountTypeBadge(info, badges)
    end

    info.width = ACCOUNT_TYPE_ICON_SIZE
    info.height = ACCOUNT_TYPE_ICON_SIZE
    return info
end

function MeetingStoneData:DebugQueryLeader(name, realm)
    EnsureState(self)

    local leader = NormalizeLeaderName(name, realm)
    if leader then
        TrackLeaderAlias(self, leader)
        local now = GetDebugTime()
        local lastSentAt = self.lastCQGLIBSentAt
        local lastNames = self.lastCQGLIBNames
        local hasCurrentSQGLIB = type(self.lastSQGLIBAt) == "number"
            and type(lastSentAt) == "number"
            and self.lastSQGLIBAt >= lastSentAt
        local recentlySentSameQuery = lastNames == leader
            and type(lastSentAt) == "number"
            and (now - lastSentAt) < SERVER_QUERY_RESEND_SECONDS
            and not hasCurrentSQGLIB

        if not recentlySentSameQuery then
            self:QueryLeader(name, realm)
        end
    end

    return self:GetDebugInfo(name, realm)
end

function MeetingStoneData:GetDebugInfo(name, realm)
    EnsureState(self)

    local leader = NormalizeLeaderName(name, realm)
    local queryState = GetAccountTypeQueryState(self, leader)
    local forceRefresh = IsAccountTypeForceRefreshActive(self)
    local minNativeMedalTime = GetMinimumNativeAccountTypeMedalTime(self, queryState, forceRefresh)
    local data = GetCachedLeaderData(self, leader, name, forceRefresh and not minNativeMedalTime, minNativeMedalTime)
    local accountTypeInfo = self:GetAccountTypeInfo(name, realm)
    queryState = GetAccountTypeQueryState(self, leader)
    local socketClient = self:GetSocketClient()
    local socketTarget = GetSocketClientTarget(socketClient)

    return {
        leader = leader,
        socketAvailable = socketClient ~= nil,
        socketReady = IsSocketClientReady(socketClient) == true,
        ready = self.ready == true,
        connecting = self.connecting == true,
        target = self.target or socketTarget or self.connectTarget,
        pending = (leader and self.pendingNames[leader] == true) or IsAccountTypeQueryActive(queryState),
        displayPending = IsAccountTypeQueryDisplayPending(queryState) == true,
        queryStatus = queryState and queryState.status,
        queryFailureReason = queryState and queryState.failureReason,
        pendingCount = CountTableValues(self.pendingNames),
        nativePendingCount = CountTableValues(self.nativeMeetingStonePendingNames),
        lastSocketConnectedAt = self.lastSocketConnectedAt,
        lastSocketDisconnectedAt = self.lastSocketDisconnectedAt,
        lastServerSendAt = self.lastServerSendAt,
        lastServerSendCmd = self.lastServerSendCmd,
        lastServerSendSucceeded = self.lastServerSendSucceeded == true,
        lastServerSendMethod = self.lastServerSendMethod,
        lastServerSendTarget = self.lastServerSendTarget,
        lastServerMessageAt = self.lastServerMessageAt,
        lastServerMessageCmd = self.lastServerMessageCmd,
        lastServerMessageSender = self.lastServerMessageSender,
        lastServerMessageDistribution = self.lastServerMessageDistribution,
        lastSLOGINAt = self.lastSLOGINAt,
        lastSLOGINVersion = self.lastSLOGINVersion,
        lastSLOGINSource = self.lastSLOGINSource,
        lastSLOGINSourceFallback = self.lastSLOGINSourceFallback == true,
        lastSLOGINQueryDataType = self.lastSLOGINQueryDataType,
        lastSLOGINQueryDataCount = self.lastSLOGINQueryDataCount,
        lastSLOGINQueryDataKeys = self.lastSLOGINQueryDataKeys,
        lastSLOGINSendSucceeded = self.lastSLOGINSendSucceeded == true,
        lastSLOGINSendMethod = self.lastSLOGINSendMethod,
        lastSLOGINSendTarget = self.lastSLOGINSendTarget,
        lastCQTBQuerySendSucceeded = self.lastCQTBQuerySendSucceeded == true,
        lastCQTBQuerySendMethod = self.lastCQTBQuerySendMethod,
        lastSVERSIONAt = self.lastSVERSIONAt,
        lastSVERSIONVersion = self.lastSVERSIONVersion,
        lastSVERSIONSupported = self.lastSVERSIONSupported,
        lastSQTBAt = self.lastSQTBAt,
        lastSQTBIsDeal = self.lastSQTBIsDeal,
        lastCQGLIBSentAt = self.lastCQGLIBSentAt,
        lastCQGLIBNames = self.lastCQGLIBNames,
        lastCQGLIBSendMethod = self.lastCQGLIBSendMethod,
        lastCQGLIBSendSucceeded = self.lastCQGLIBSendSucceeded == true,
        lastSQGLIBAt = self.lastSQGLIBAt,
        lastSQGLIBCount = self.lastSQGLIBCount,
        lastSQGLIBChanged = self.lastSQGLIBChanged == true,
        lastSQGLIBLeaderNames = self.lastSQGLIBLeaderNames,
        lastSQGLIBMismatchedNames = self.lastSQGLIBMismatchedNames,
        lastSQGLIBMatchesQuery = DoesLeaderListContainLeader(self.lastSQGLIBLeaderNames, leader),
        lastSQGLIBInvalidAt = self.lastSQGLIBInvalidAt,
        lastSQGLIBInvalidType = self.lastSQGLIBInvalidType,
        cached = type(data) == "table",
        cacheFresh = IsCacheFresh(data) == true,
        cachedLeaderName = data and data.accountTypeLeaderName,
        level = data and data.level,
        starLevel = data and data.starLevel,
        cacheSource = data and data.source,
        cacheSourceFallback = data and data.sourceFallback == true,
        isNewbie = data and data.isNewbie == true,
        accountStatus = GetAccountTypeDebugText(accountTypeInfo),
    }
end
