local MAJOR = "NetEaseAPI-1.0"
local MINOR = 2026062301

local NetEaseAPI, oldMinor = LibStub:NewLibrary(MAJOR, MINOR)
if not NetEaseAPI then
    return
end

local DEFAULT_PROTOCOL_VERSION = "12.1.8"
local DEFAULT_SOCKET_PREFIX = "NERB"
local DEFAULT_SERVER_PREFIX = "S1"

local SOCKET_STATUS_CONNECT = 2
local SOCKET_STATUS_READY = 3

local SUCCESS_TTL = 24 * 60 * 60
local QUERY_COOLDOWN = 60
local FAILED_COOLDOWN = 10 * 60
local RESPONSE_TIMEOUT = 30
local BATCH_DELAY = 0.5
local RETRY_DELAY = 5
local LOGIN_DELIVERY_TIMEOUT = 5
local LOGIN_QUERY_GRACE = 2
local CACHE_MAX_ENTRIES = 1000
local MAX_BATCH_SIZE = 100

local type = type
local pairs = pairs
local ipairs = ipairs
local tostring = tostring
local tonumber = tonumber
local select = select
local tinsert = table.insert
local tconcat = table.concat
local sort = table.sort
local rawget = rawget

local function now()
    if type(time) == "function" then
        return time()
    end
    return os.time()
end

local function wipeTable(tbl)
    if type(wipe) == "function" then
        return wipe(tbl)
    end
    for key in pairs(tbl) do
        tbl[key] = nil
    end
    return tbl
end

local function trim(value)
    return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end

local function copyTable(value)
    if type(value) ~= "table" then
        return value
    end

    local copied = {}
    for key, item in pairs(value) do
        copied[key] = copyTable(item)
    end
    return copied
end

local function safeCall(fn, ...)
    if type(fn) ~= "function" then
        return nil
    end

    local ok, a, b, c, d, e = pcall(fn, ...)
    if ok then
        return a, b, c, d, e
    end
end

local function getMetadata(addonName, key)
    if not addonName or not C_AddOns or type(C_AddOns.GetAddOnMetadata) ~= "function" then
        return nil
    end
    return safeCall(C_AddOns.GetAddOnMetadata, addonName, key)
end

local function isAddonLoaded(addonName)
    if not addonName or not C_AddOns or type(C_AddOns.IsAddOnLoaded) ~= "function" then
        return false
    end
    return safeCall(C_AddOns.IsAddOnLoaded, addonName)
end

local function getAddonSource()
    local sourceMap = {
        { "BigFoot", 1 },
        { "!!!163UI!!!", 2 },
        { "Duowan", 4 },
        { "ElvUI", 8 },
    }

    for _, item in ipairs(sourceMap) do
        if isAddonLoaded(item[1]) then
            return item[2]
        end
    end
    return 0
end

local function getBattleTag()
    local _, battleTag = safeCall(BNGetInfo)
    return battleTag
end

local function getPlayerGuid()
    return safeCall(UnitGUID, "player")
end

local function getRealmName()
    local realmName = safeCall(GetRealmName)
    if realmName and realmName ~= "" then
        return realmName
    end
end

local function compareVersions(left, right)
    left = tostring(left or "")
    right = tostring(right or "")

    local leftParts = {}
    local rightParts = {}
    for part in left:gmatch("%d+") do
        tinsert(leftParts, tonumber(part) or 0)
    end
    for part in right:gmatch("%d+") do
        tinsert(rightParts, tonumber(part) or 0)
    end

    local count = #leftParts
    if #rightParts > count then
        count = #rightParts
    end
    if count < 3 then
        count = 3
    end

    for index = 1, count do
        local leftValue = leftParts[index] or 0
        local rightValue = rightParts[index] or 0
        if leftValue > rightValue then
            return 1
        end
        if leftValue < rightValue then
            return -1
        end
    end
    return 0
end

local function isSupportedProtocolVersion(version)
    return version and version ~= "" and compareVersions(version, DEFAULT_PROTOCOL_VERSION) >= 0
end

local function getFactionGroup()
    local faction = safeCall(UnitFactionGroup, "player")
    if faction and faction ~= "" then
        return faction
    end
end

local function safeAfter(delay, callback)
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(delay, callback)
        return true
    end
    return false
end

local function summarizeMapKeys(value)
    if type(value) ~= "table" then
        return 0, nil
    end

    local keys = {}
    for key in pairs(value) do
        tinsert(keys, safeCall(tostring, key) or "<unreadable>")
    end
    sort(keys)
    return #keys, tconcat(keys, ",")
end

local function getSocketHandler(owner)
    local socket = LibStub("NetEaseSocket-2.0", true)
    local handlers = socket and socket.handlers
    if type(handlers) == "table" then
        return rawget(handlers, owner)
    end
end

local function getSocketStatusText(status)
    if status == SOCKET_STATUS_CONNECT then
        return "connecting"
    end
    if status == SOCKET_STATUS_READY then
        return "ready"
    end
    if status == 1 then
        return "normal"
    end
    return status and "unknown" or nil
end

local function getConnectionMessage(reason)
    if reason == "waiting_connection" then
        return "网易 API 离线"
    end
    if reason == "timeout" then
        return "网易 API 无响应"
    end
    if reason == "fault" or reason == "unsupported" then
        return "网易 API 故障"
    end
    if reason == "not_started" then
        return "网易 API 初始化中"
    end
    if reason == "socket_not_listening" then
        return "网易服务连接未初始化"
    end
    if reason == "missing-socket-handler" then
        return "网易服务连接处理器未建立"
    end
    if reason == "connected" then
        return "网易服务已连接"
    end
    return reason
end

local function getServiceMessage(status)
    if status == "starting" then
        return "网易 API 初始化中"
    end
    if status == "offline" then
        return "网易 API 离线"
    end
    if status == "no_response" then
        return "网易 API 无响应"
    end
    if status == "fault" then
        return "网易 API 故障"
    end
    if status == "ok" then
        return "网易 API 正常"
    end
    return nil
end

local function recordOutbound(api, cmd, distribution, target, firstArg, argCount, currentTime)
    if not api then
        return
    end

    local count, keys = summarizeMapKeys(firstArg)
    currentTime = currentTime or now()

    api.lastOutboundAt = currentTime
    api.lastOutboundDistribution = distribution
    api.lastOutboundTarget = target
    api.lastOutboundCommand = cmd
    api.lastOutboundArgCount = argCount or 0
    api.lastOutboundFirstArgType = type(firstArg)
    api.lastOutboundFirstArgKeyCount = count
    api.lastOutboundFirstArgKeys = keys

    if cmd == "SLOGIN" then
        api.lastOutboundSLOGINAt = currentTime
        api.lastOutboundSLOGINTarget = target
        api.lastOutboundSLOGINArgCount = argCount or 0
        api.lastOutboundSLOGINVersion = firstArg
    elseif cmd == "CQTB" then
        api.lastOutboundCQTBAt = currentTime
        api.lastOutboundCQTBTarget = target
        api.lastOutboundCQTBArgCount = argCount or 0
    elseif cmd == "CQGLIB" then
        api.lastOutboundCQGLIBAt = currentTime
        api.lastOutboundCQGLIBTarget = target
        api.lastOutboundCQGLIBArgCount = argCount or 0
        api.lastOutboundCQGLIBNames = firstArg
    end
end

local function handleServerCommandDelivery(callbackArg, sent, total)
    local api = LibStub(MAJOR, true)
    if not api or type(callbackArg) ~= "table" then
        return
    end

    local cmd = callbackArg.cmd
    local currentTime = now()

    api.lastDeliveryProgressAt = currentTime
    api.lastDeliveryProgressCommand = cmd
    api.lastDeliveryProgressSent = sent
    api.lastDeliveryProgressTotal = total
    api.lastDeliveryProgressTarget = callbackArg.target

    if sent and total and sent < total then
        return
    end

    api.lastDeliveredAt = currentTime
    api.lastDeliveredCommand = cmd
    api.lastDeliveredSent = sent
    api.lastDeliveredTotal = total
    api.lastDeliveredTarget = callbackArg.target
    api.lastDeliveredDistribution = callbackArg.distribution

    if cmd == "SLOGIN" then
        api.lastDeliveredSLOGINAt = currentTime
        api.lastDeliveredSLOGINVersion = callbackArg.firstArg
        api.lastLoginDeliveredAt = currentTime
        if api.lastLoginDeliveryHandledId ~= callbackArg.sendId then
            api.lastLoginDeliveryHandledId = callbackArg.sendId
            api.lastCQTBAt = now()
            api:SendServerCommand("CQTB")
            api:ScheduleFlush(api.options.loginQueryGrace or LOGIN_QUERY_GRACE)
        end
    elseif cmd == "CQTB" then
        api.lastDeliveredCQTBAt = currentTime
    elseif cmd == "CQGLIB" then
        api.lastDeliveredCQGLIBAt = currentTime
        api.lastDeliveredCQGLIBNames = callbackArg.firstArg
    end
end

NetEaseAPI.major = MAJOR
NetEaseAPI.minor = MINOR
NetEaseAPI.oldMinor = oldMinor

NetEaseAPI.options = NetEaseAPI.options or {}
NetEaseAPI.cache = NetEaseAPI.cache or {}
NetEaseAPI.failed = NetEaseAPI.failed or {}
NetEaseAPI.pending = NetEaseAPI.pending or {}
NetEaseAPI.queue = NetEaseAPI.queue or {}
NetEaseAPI.lastQueryAt = NetEaseAPI.lastQueryAt or {}
NetEaseAPI.status = NetEaseAPI.status or {}

NetEaseAPI.protocolVersion = NetEaseAPI.protocolVersion or DEFAULT_PROTOCOL_VERSION
NetEaseAPI.socketPrefix = NetEaseAPI.socketPrefix or DEFAULT_SOCKET_PREFIX
NetEaseAPI.serverPrefix = NetEaseAPI.serverPrefix or DEFAULT_SERVER_PREFIX
NetEaseAPI.emptyLoginQueryData = NetEaseAPI.emptyLoginQueryData or {}

local CallbackHandler = LibStub("CallbackHandler-1.0", true)
if CallbackHandler and not NetEaseAPI.callbacks then
    NetEaseAPI.callbacks = CallbackHandler:New(NetEaseAPI, "RegisterCallback", "UnregisterCallback", "UnregisterAllCallbacks")
end

local function normalizeCallbackRegistration(selfOrOwner, ownerOrEventName, eventNameOrMethod, ...)
    if selfOrOwner == NetEaseAPI then
        if type(ownerOrEventName) == "string" then
            return NetEaseAPI, ownerOrEventName, eventNameOrMethod, ...
        end
        return ownerOrEventName, eventNameOrMethod, ...
    end
    return selfOrOwner, ownerOrEventName, eventNameOrMethod, ...
end

local function normalizeCallbackUnregister(selfOrOwner, ownerOrEventName, eventName)
    if selfOrOwner == NetEaseAPI then
        if type(ownerOrEventName) == "string" then
            return NetEaseAPI, ownerOrEventName
        end
        return ownerOrEventName, eventName
    end
    return selfOrOwner, ownerOrEventName
end

local function normalizeCallbackOwner(selfOrOwner, owner)
    if selfOrOwner == NetEaseAPI then
        return owner or NetEaseAPI
    end
    return selfOrOwner
end

if CallbackHandler and not NetEaseAPI.callbackFacadeInstalled then
    NetEaseAPI.callbackRegisterRaw = NetEaseAPI.RegisterCallback
    NetEaseAPI.callbackUnregisterRaw = NetEaseAPI.UnregisterCallback
    NetEaseAPI.callbackUnregisterAllRaw = NetEaseAPI.UnregisterAllCallbacks

    function NetEaseAPI.RegisterCallback(...)
        return NetEaseAPI.callbackRegisterRaw(normalizeCallbackRegistration(...))
    end

    function NetEaseAPI.UnregisterCallback(...)
        return NetEaseAPI.callbackUnregisterRaw(normalizeCallbackUnregister(...))
    end

    function NetEaseAPI.UnregisterAllCallbacks(...)
        return NetEaseAPI.callbackUnregisterAllRaw(normalizeCallbackOwner(...))
    end

    NetEaseAPI.callbackFacadeInstalled = true
end

if not CallbackHandler and not NetEaseAPI.RegisterCallback then
    NetEaseAPI._simpleCallbacks = NetEaseAPI._simpleCallbacks or {}

    function NetEaseAPI.RegisterCallback(...)
        local owner, eventName, method = normalizeCallbackRegistration(...)
        if type(eventName) ~= "string" or type(method) ~= "function" then
            return false
        end
        NetEaseAPI._simpleCallbacks[eventName] = NetEaseAPI._simpleCallbacks[eventName] or {}
        tinsert(NetEaseAPI._simpleCallbacks[eventName], {
            owner = owner,
            method = method,
        })
        return true
    end

    function NetEaseAPI.UnregisterAllCallbacks(...)
        local owner = normalizeCallbackOwner(...)
        if owner == NetEaseAPI or owner == nil then
            wipeTable(NetEaseAPI._simpleCallbacks)
            return
        end

        for _, callbacks in pairs(NetEaseAPI._simpleCallbacks) do
            for index = #callbacks, 1, -1 do
                if callbacks[index].owner == owner then
                    table.remove(callbacks, index)
                end
            end
        end
    end
end

function NetEaseAPI:FireCallback(eventName, ...)
    if self.callbacks and self.callbacks.Fire then
        self.callbacks:Fire(eventName, ...)
        return
    end

    local callbacks = self._simpleCallbacks and self._simpleCallbacks[eventName]
    if not callbacks then
        return
    end

    for _, callback in ipairs(callbacks) do
        safeCall(callback.method, eventName, ...)
    end
end

function NetEaseAPI:SetOption(key, value)
    if type(key) ~= "string" then
        return false
    end
    self.options[key] = value
    return true
end

function NetEaseAPI:SetOptions(options)
    if type(options) ~= "table" then
        return false
    end

    for key, value in pairs(options) do
        self.options[key] = value
    end
    return true
end

function NetEaseAPI:SetClientInfo(addonName, version, source, loginQueryData)
    if type(addonName) == "table" then
        local info = addonName
        addonName = info.addonName or info.name
        version = info.version
        source = info.source
        loginQueryData = info.loginQueryData
    end

    self.clientAddonName = addonName or self.clientAddonName
    self.clientVersion = version or self.clientVersion
    self.clientSource = source or self.clientSource
    self.loginQueryData = loginQueryData or self.loginQueryData
end

function NetEaseAPI:SetStorage(storage)
    if type(storage) ~= "table" then
        return false
    end

    storage.identities = storage.identities or {}
    storage.failed = storage.failed or {}
    storage.meta = storage.meta or {}
    storage.meta.major = MAJOR
    storage.meta.minor = MINOR

    if self.cache and self.cache ~= storage.identities then
        for name, data in pairs(self.cache) do
            if storage.identities[name] == nil then
                storage.identities[name] = data
            end
        end
    end

    if self.failed and self.failed ~= storage.failed then
        for name, data in pairs(self.failed) do
            if storage.failed[name] == nil then
                storage.failed[name] = data
            end
        end
    end

    self.storage = storage
    self.cache = storage.identities
    self.failed = storage.failed

    return true
end

function NetEaseAPI:EnsureStorage()
    if self.storage then
        return self.storage
    end

    _G.NetEaseAPIDB = type(_G.NetEaseAPIDB) == "table" and _G.NetEaseAPIDB or {}
    self:SetStorage(_G.NetEaseAPIDB)
    return self.storage
end

function NetEaseAPI:GetMinorVersion()
    return MINOR
end

function NetEaseAPI:GetVersion()
    return MAJOR, MINOR
end

function NetEaseAPI:GetClientVersion()
    if isSupportedProtocolVersion(self.clientVersion) then
        return self.clientVersion
    end

    local version = getMetadata(self.clientAddonName, "Version")
    if isSupportedProtocolVersion(version) then
        return version
    end

    if isSupportedProtocolVersion(self.protocolVersion) then
        return self.protocolVersion
    end

    return DEFAULT_PROTOCOL_VERSION
end

function NetEaseAPI:GetAddonSource()
    if self.clientSource ~= nil then
        return self.clientSource
    end
    return getAddonSource()
end

function NetEaseAPI:GetLoginQueryData()
    if type(self.loginQueryData) == "table" then
        return self.loginQueryData
    end
    return self.emptyLoginQueryData
end

function NetEaseAPI:GetDefaultServerTarget()
    local faction = getFactionGroup()
    if not faction then
        return nil
    end
    return (self.serverPrefix or DEFAULT_SERVER_PREFIX) .. faction
end

function NetEaseAPI:NormalizeName(name)
    name = trim(name)
    if name == "" then
        return nil
    end

    local ambiguous = safeCall(Ambiguate, name, "none")
    if ambiguous and ambiguous ~= "" then
        name = ambiguous
    end

    if not name:find("-", 1, true) then
        local realmName = getRealmName()
        if not realmName then
            return nil
        end
        name = name .. "-" .. realmName
    end

    return name
end

function NetEaseAPI:IsQueryEnvironmentAvailable()
    if self.serverUnsupported then
        return false, self.unsupportedReason or "unsupported"
    end

    if type(IsTrialAccount) == "function" and IsTrialAccount() then
        return false, "trial"
    end

    if not LibStub("NetEaseSocket-2.0", true) then
        return false, "missing-NetEaseSocket-2.0"
    end

    if not getFactionGroup() then
        return false, "missing-faction"
    end

    return true
end

function NetEaseAPI:PrepareSocket()
    if self.socketEmbedded then
        return true
    end

    local socket = LibStub("NetEaseSocket-2.0", true)
    if not socket then
        self.unsupportedReason = "missing-NetEaseSocket-2.0"
        return false, self.unsupportedReason
    end

    socket:Embed(self)
    self.socketEmbedded = true
    return true
end

function NetEaseAPI:RegisterServerHandlers()
    if self.serverHandlersRegistered then
        return true
    end

    if not self.RegisterServer then
        return false
    end

    self:RegisterServer("SERVER_CONNECTED", "OnServerConnected")
    self:RegisterServer("SERVER_DISCONNECTED", "OnServerDisconnected")
    self:RegisterServer("SVERSION", "OnServerVersion")
    self:RegisterServer("SQTB", "OnSQTB")
    self:RegisterServer("SQGLIB", "OnSQGLIB")
    self.serverHandlersRegistered = true
    return true
end

function NetEaseAPI:Start()
    if self.started then
        return true
    end

    self:EnsureStorage()

    local available, reason = self:IsQueryEnvironmentAvailable()
    if not available then
        self.unsupportedReason = reason
        self:FireCallback("PlayerIdentityFailed", nil, reason)
        return false, reason
    end

    local ok, socketReason = self:PrepareSocket()
    if not ok then
        self:FireCallback("PlayerIdentityFailed", nil, socketReason)
        return false, socketReason
    end

    local serverTarget = self.serverTarget or self:GetDefaultServerTarget()
    if not serverTarget then
        self.unsupportedReason = "missing-server-target"
        self:FireCallback("PlayerIdentityFailed", nil, self.unsupportedReason)
        return false, self.unsupportedReason
    end

    self.started = true
    self.connected = false
    self.connectionStartedAt = now()
    self.unsupportedReason = nil

    self:ListenSocket(self.socketPrefix or DEFAULT_SOCKET_PREFIX, serverTarget)

    if not self:RegisterServerHandlers() then
        self.unsupportedReason = "missing-register-server"
        self:FireCallback("PlayerIdentityFailed", nil, self.unsupportedReason)
        return false, self.unsupportedReason
    end

    self:ConnectServer()
    self:ScheduleFlush(RETRY_DELAY)

    return true
end

function NetEaseAPI:Stop()
    self.connected = false
    self.stopped = true
end

function NetEaseAPI:OnServerConnected()
    self.connected = true
    self.connectedAt = now()
    self:FireCallback("ServerStatusChanged", true)
    self:SendLogin()
    self:ScheduleFlush(BATCH_DELAY)
end

function NetEaseAPI:OnServerDisconnected()
    self.connected = false
    self.connectionStartedAt = now()
    self:FireCallback("ServerStatusChanged", false)
end

function NetEaseAPI:OnServerVersion(eventName, ...)
    local isSupported = select(3, ...)
    if isSupported ~= nil and not isSupported then
        self.serverUnsupported = true
        self.unsupportedReason = "server-unsupported"
        self:FireCallback("PlayerIdentityFailed", nil, self.unsupportedReason)
    end
end

function NetEaseAPI:OnSQTB(eventName, isDeal)
    self.lastSQTBAt = now()
    self.lastSQTBIsDeal = isDeal
end

function NetEaseAPI:SendServerCommand(cmd, ...)
    local handler = getSocketHandler(self)
    local target = handler and handler.target
    if not target or target == "" then
        self.lastSendServerCommandError = "missing-target"
        return false, self.lastSendServerCommandError
    end

    local aceComm = LibStub("AceComm-3.0", true)
    local serializer = LibStub("AceSerializer-3.0", true)
    if not aceComm or type(aceComm.SendCommMessage) ~= "function" then
        self.lastSendServerCommandError = "missing-AceComm-3.0"
        return false, self.lastSendServerCommandError
    end
    if not serializer or type(serializer.Serialize) ~= "function" then
        self.lastSendServerCommandError = "missing-AceSerializer-3.0"
        return false, self.lastSendServerCommandError
    end

    local ok, payload = pcall(serializer.Serialize, serializer, cmd, ...)
    if not ok or type(payload) ~= "string" then
        self.lastSendServerCommandError = ok and "serialize-failed" or payload
        return false, self.lastSendServerCommandError
    end

    self.sendSequence = (self.sendSequence or 0) + 1
    local sendId = self.sendSequence
    local firstArg = ...
    local argCount = select("#", ...)
    local distribution = "WHISPER"
    local currentTime = now()

    recordOutbound(self, cmd, distribution, target, firstArg, argCount, currentTime)

    local callbackArg = {
        cmd = cmd,
        sendId = sendId,
        firstArg = firstArg,
        target = target,
        distribution = distribution,
    }
    ok, payload = pcall(
        aceComm.SendCommMessage,
        aceComm,
        self.socketPrefix or DEFAULT_SOCKET_PREFIX,
        payload,
        distribution,
        target,
        "NORMAL",
        handleServerCommandDelivery,
        callbackArg
    )
    if not ok then
        self.lastSendServerCommandError = payload
        return false, payload
    end

    self.lastSendServerCommandError = nil
    self.lastSendServerCommandAt = currentTime
    self.lastSendServerCommand = cmd
    self.lastSendServerCommandSendId = sendId
    if cmd == "SLOGIN" then
        self.lastSLOGINSendId = sendId
    elseif cmd == "CQTB" then
        self.lastCQTBSendId = sendId
    elseif cmd == "CQGLIB" then
        self.lastCQGLIBSendId = sendId
    end

    return true, nil, sendId
end

function NetEaseAPI:SendLogin()
    if not self.connected then
        return false
    end

    local loginQueryData = self:GetLoginQueryData()
    local loginQueryDataCount = 0
    for _ in pairs(loginQueryData) do
        loginQueryDataCount = loginQueryDataCount + 1
    end
    local playerGuid = getPlayerGuid()
    local addonSource = self:GetAddonSource()
    local battleTag = getBattleTag()

    self.lastLoginAt = now()
    self.lastLoginVersion = self:GetClientVersion()
    self.lastLoginAddonSource = addonSource
    self.lastLoginHasPlayerGuid = playerGuid ~= nil
    self.lastLoginHasBattleTag = battleTag ~= nil
    self.lastLoginQueryDataType = type(loginQueryData)
    self.lastLoginQueryDataCount = loginQueryDataCount
    self.lastLoginDeliveredAt = nil
    self.lastLoginDeliveryHandledId = nil

    return self:SendServerCommand(
        "SLOGIN",
        self.lastLoginVersion,
        playerGuid,
        addonSource,
        battleTag,
        loginQueryData
    )
end

function NetEaseAPI:GetQuerySendDelay()
    if not self.lastLoginAt then
        return 0
    end

    local currentTime = now()
    local loginDeliveryTimeout = self.options.loginDeliveryTimeout or LOGIN_DELIVERY_TIMEOUT
    local loginQueryGrace = self.options.loginQueryGrace or LOGIN_QUERY_GRACE

    if not self.lastLoginDeliveredAt or self.lastLoginDeliveredAt < self.lastLoginAt then
        local elapsed = currentTime - self.lastLoginAt
        if elapsed < loginDeliveryTimeout then
            return loginDeliveryTimeout - elapsed
        end
        self.lastLoginDeliveryTimeoutAt = currentTime
        return 0
    end

    local elapsed = currentTime - self.lastLoginDeliveredAt
    if elapsed < loginQueryGrace then
        return loginQueryGrace - elapsed
    end

    return 0
end

function NetEaseAPI:IsCacheFresh(data)
    return type(data) == "table"
        and type(data.updatedAt) == "number"
        and now() - data.updatedAt < (self.options.successTTL or SUCCESS_TTL)
end

function NetEaseAPI:GetPlayerIdentityStatus(name)
    self:EnsureStorage()

    local normalized = self:NormalizeName(name)
    if not normalized then
        return "missing"
    end

    local available = self:IsQueryEnvironmentAvailable()
    if not available and self.cache[normalized] == nil then
        return "unsupported"
    end

    if self.pending[normalized] then
        local diagnostic = self:GetConnectionDiagnostic()
        if diagnostic.serviceConnectionMissing then
            return "pending", diagnostic.reason
        end
        return "pending"
    end

    local data = self.cache[normalized]
    if self:IsCacheFresh(data) then
        return "fresh"
    end

    if data then
        return "stale"
    end

    local failed = self.failed[normalized]
    if failed and now() - (failed.updatedAt or 0) < (self.options.failedCooldown or FAILED_COOLDOWN) then
        return "failed", failed.reason
    end

    return "missing"
end

function NetEaseAPI:GetPlayerIdentity(name)
    self:EnsureStorage()
    local normalized = self:NormalizeName(name)
    if not normalized then
        return nil
    end
    return self.cache[normalized]
end

function NetEaseAPI:QueuePlayer(name, force)
    local normalized = self:NormalizeName(name)
    if not normalized then
        return nil, "bad-name"
    end

    local currentTime = now()
    local data = self.cache[normalized]
    if not force and self:IsCacheFresh(data) then
        return data, "fresh"
    end

    local failed = self.failed[normalized]
    if not force and failed and currentTime - (failed.updatedAt or 0) < (self.options.failedCooldown or FAILED_COOLDOWN) then
        return data, data and "stale" or "failed", failed.reason
    end

    local lastQueryAt = self.lastQueryAt[normalized]
    if not force and lastQueryAt and currentTime - lastQueryAt < (self.options.queryCooldown or QUERY_COOLDOWN) then
        if self.pending[normalized] then
            return data, "pending"
        end
        return data, data and "stale" or "cooldown"
    end

    self.pending[normalized] = self.pending[normalized] or {}
    self.pending[normalized].queuedAt = self.pending[normalized].queuedAt or currentTime
    self.queue[normalized] = true
    self.lastQueryAt[normalized] = currentTime
    self.failed[normalized] = nil

    self:FireCallback("PlayerIdentityQueued", normalized)
    return data, "pending"
end

function NetEaseAPI:QueryPlayerIdentity(name, force)
    self:EnsureStorage()

    local available, reason = self:IsQueryEnvironmentAvailable()
    if not available then
        return self:GetPlayerIdentity(name), "unsupported", reason
    end

    local data, status, queueReason = self:QueuePlayer(name, force)
    if status == "pending" then
        self:Start()
        self:ScheduleFlush(BATCH_DELAY)
        local diagnostic = self:GetConnectionDiagnostic()
        if diagnostic.serviceConnectionMissing then
            return data, status, diagnostic.reason
        end
    end
    return data, status, queueReason
end

function NetEaseAPI:QueryPlayerIdentities(names, force)
    if type(names) ~= "table" then
        return 0
    end

    self:EnsureStorage()

    local count = 0
    local available = self:IsQueryEnvironmentAvailable()
    if not available then
        return count
    end

    for _, name in ipairs(names) do
        local _, status = self:QueuePlayer(name, force)
        if status == "pending" then
            count = count + 1
        end
    end

    if count > 0 then
        self:Start()
        self:ScheduleFlush(BATCH_DELAY)
    end

    return count
end

function NetEaseAPI:ScheduleFlush(delay)
    if self.flushScheduled then
        return
    end

    self.flushScheduled = true

    local scheduled = safeAfter(delay or BATCH_DELAY, function()
        local lib = LibStub(MAJOR, true)
        if not lib then
            return
        end
        lib.flushScheduled = false
        lib:FlushQueue()
    end)

    if not scheduled then
        self.flushScheduled = false
    end
end

function NetEaseAPI:ScheduleTimeoutCheck()
    if self.timeoutCheckScheduled then
        return
    end

    self.timeoutCheckScheduled = true

    local scheduled = safeAfter(self.options.responseTimeout or RESPONSE_TIMEOUT, function()
        local lib = LibStub(MAJOR, true)
        if not lib then
            return
        end
        lib.timeoutCheckScheduled = false
        lib:CheckTimeouts()
    end)

    if not scheduled then
        self.timeoutCheckScheduled = false
    end
end

function NetEaseAPI:FlushQueue()
    if not self.started then
        return false
    end

    if not self.connected then
        if next(self.queue) then
            self:ScheduleFlush(RETRY_DELAY)
        end
        return false
    end

    local querySendDelay = self:GetQuerySendDelay()
    if querySendDelay and querySendDelay > 0 then
        self.lastQuerySendDelay = querySendDelay
        self.lastQuerySendDelayedAt = now()
        self:ScheduleFlush(querySendDelay)
        return false
    end
    self.lastQuerySendDelay = 0

    local names = {}
    local currentTime = now()
    local maxBatchSize = self.options.maxBatchSize or MAX_BATCH_SIZE

    for name in pairs(self.queue) do
        tinsert(names, name)

        if #names >= maxBatchSize then
            break
        end
    end

    if #names == 0 then
        return true
    end

    local queryNames = tconcat(names, ",")
    self.lastCQGLIBAt = currentTime
    self.lastCQGLIBNames = queryNames
    self.lastCQGLIBCount = #names

    local ok, reason, sendId = self:SendServerCommand("CQGLIB", queryNames)
    if not ok then
        self.lastCQGLIBSendFailedAt = currentTime
        self.lastCQGLIBSendFailedReason = reason
        self:ScheduleFlush(RETRY_DELAY)
        return false
    end
    self.lastCQGLIBSendFailedAt = nil
    self.lastCQGLIBSendFailedReason = nil

    for _, name in ipairs(names) do
        self.queue[name] = nil
        self.pending[name] = self.pending[name] or {}
        self.pending[name].sentAt = currentTime
        self.pending[name].sendId = sendId
    end

    self:ScheduleTimeoutCheck()

    if next(self.queue) then
        self:ScheduleFlush(BATCH_DELAY)
    end

    return true
end

function NetEaseAPI:CheckTimeouts()
    local currentTime = now()
    local timeout = self.options.responseTimeout or RESPONSE_TIMEOUT
    local hasPending = false
    local timedOutNames

    for name, pending in pairs(self.pending) do
        if pending.sentAt and currentTime - pending.sentAt >= timeout then
            timedOutNames = timedOutNames or {}
            tinsert(timedOutNames, name)
            self.pending[name] = nil
            self.failed[name] = {
                reason = "timeout",
                updatedAt = currentTime,
            }
            self:FireCallback("PlayerIdentityFailed", name, "timeout")
        else
            hasPending = true
        end
    end

    if timedOutNames then
        sort(timedOutNames)
        self.lastTimeoutAt = currentTime
        self.lastTimeoutNames = tconcat(timedOutNames, ",")
    end

    if hasPending then
        self:ScheduleTimeoutCheck()
    end
end

function NetEaseAPI:BuildIdentity(name, data)
    if type(data) ~= "table" then
        return nil
    end

    local currentTime = now()
    local newbieFlag = tonumber(data.n) or 0
    local remainCount = tonumber(data.r) or 0

    return {
        name = name,
        level = data.l,
        starLevel = data.s,
        isNewbie = newbieFlag == 1,
        newbieExpireTime = newbieFlag == 1 and currentTime + remainCount * 30 * 60 or nil,
        bgID = data.b or 0,
        roomID = data.c,
        medalMap = {
            acc_exp = data.ae,
            next_star_level_threshold = data.ne,
            medal = copyTable(data.m),
        },
        updatedAt = currentTime,
        raw = {
            l = data.l,
            s = data.s,
            n = data.n,
            r = data.r,
            b = data.b,
            c = data.c,
            ae = data.ae,
            ne = data.ne,
            m = copyTable(data.m),
        },
    }
end

function NetEaseAPI:OnSQGLIB(eventName, maps)
    local currentTime = now()
    self.lastSQGLIBAt = currentTime
    self.lastSQGLIBType = type(maps)
    self.lastSQGLIBCount = 0
    self.lastSQGLIBKeys = nil
    self.lastSQGLIBAcceptedCount = 0
    self.lastSQGLIBBuildFailedCount = 0
    self.lastSQGLIBBuildFailedKeys = nil

    if type(maps) ~= "table" then
        return
    end

    self:EnsureStorage()

    local changed = {}
    local keys = {}
    local buildFailedKeys = {}
    for name, data in pairs(maps) do
        tinsert(keys, safeCall(tostring, name) or "<unreadable>")
        local normalized = self:NormalizeName(name) or name
        local identity = self:BuildIdentity(normalized, data)
        if identity then
            self.cache[normalized] = identity
            self.pending[normalized] = nil
            self.queue[normalized] = nil
            self.failed[normalized] = nil
            tinsert(changed, normalized)
            self:FireCallback("PlayerIdentityUpdated", normalized, identity)
        else
            tinsert(buildFailedKeys, safeCall(tostring, normalized) or "<unreadable>")
        end
    end

    sort(keys)
    self.lastSQGLIBCount = #keys
    self.lastSQGLIBKeys = tconcat(keys, ",")
    self.lastSQGLIBAcceptedCount = #changed
    if #buildFailedKeys > 0 then
        sort(buildFailedKeys)
        self.lastSQGLIBBuildFailedCount = #buildFailedKeys
        self.lastSQGLIBBuildFailedKeys = tconcat(buildFailedKeys, ",")
    end

    self:PruneCache()

    if #changed > 0 then
        self:FireCallback("PlayerIdentityBatchUpdated", changed)
    end
end

function NetEaseAPI:PruneCache()
    local maxEntries = self.options.cacheMaxEntries or CACHE_MAX_ENTRIES
    if maxEntries <= 0 then
        return
    end

    local count = 0
    local entries = {}
    for name, data in pairs(self.cache) do
        count = count + 1
        entries[count] = {
            name = name,
            updatedAt = type(data) == "table" and data.updatedAt or 0,
        }
    end

    if count <= maxEntries then
        return
    end

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

    for index = 1, count - maxEntries do
        self.cache[entries[index].name] = nil
    end
end

function NetEaseAPI:IsNewbie(name)
    local data = self:GetPlayerIdentity(name)
    return data and data.isNewbie or false
end

function NetEaseAPI:GetLocomotiveLevel(name)
    local data = self:GetPlayerIdentity(name)
    return data and data.level or nil
end

function NetEaseAPI:GetStarLevel(name)
    local data = self:GetPlayerIdentity(name)
    return data and data.starLevel or nil
end

function NetEaseAPI:GetMedals(name)
    local data = self:GetPlayerIdentity(name)
    return data and data.medalMap and data.medalMap.medal or nil
end

function NetEaseAPI:ClearPlayerIdentity(name)
    local normalized = self:NormalizeName(name)
    if not normalized then
        return false
    end

    self.cache[normalized] = nil
    self.failed[normalized] = nil
    self.pending[normalized] = nil
    self.queue[normalized] = nil
    self.lastQueryAt[normalized] = nil
    return true
end

function NetEaseAPI:ClearCache()
    self:EnsureStorage()
    wipeTable(self.cache)
    wipeTable(self.failed)
    wipeTable(self.pending)
    wipeTable(self.queue)
    wipeTable(self.lastQueryAt)
end

function NetEaseAPI:GetConnectionDiagnostic()
    local currentTime = now()
    local available, unavailableReason = self:IsQueryEnvironmentAvailable()
    local handler = getSocketHandler(self)
    local statusTable = handler and handler.statusTable
    local socketStatus = statusTable and statusTable.status or nil
    local socketStatusText = getSocketStatusText(socketStatus)
    local target = handler and handler.target or nil
    local isReady = socketStatus == SOCKET_STATUS_READY or target ~= nil
    local serviceConnected = self.connected == true or isReady
    local connectionStatus
    local reason

    if not available then
        connectionStatus = "unsupported"
        reason = unavailableReason or self.unsupportedReason or "unsupported"
    elseif serviceConnected then
        connectionStatus = "connected"
        reason = "connected"
    elseif not self.started then
        connectionStatus = "not_started"
        reason = "not_started"
    elseif not handler then
        connectionStatus = "missing_handler"
        reason = "missing-socket-handler"
    elseif not handler.prefix or not handler.connectTarget then
        connectionStatus = "not_listening"
        reason = "socket_not_listening"
    elseif socketStatus == SOCKET_STATUS_CONNECT then
        connectionStatus = "connecting"
        reason = "waiting_connection"
    else
        connectionStatus = "connecting"
        reason = "waiting_connection"
    end

    return {
        status = connectionStatus,
        reason = reason,
        message = getConnectionMessage(reason),
        started = self.started and true or false,
        connected = serviceConnected and true or false,
        apiConnected = self.connected and true or false,
        socketEmbedded = self.socketEmbedded and true or false,
        handlerAvailable = handler and true or false,
        socketPrefix = self.socketPrefix or DEFAULT_SOCKET_PREFIX,
        serverTarget = self.serverTarget or self:GetDefaultServerTarget(),
        prefix = handler and handler.prefix or nil,
        connectTarget = handler and handler.connectTarget or nil,
        target = target,
        socketStatus = socketStatus,
        socketStatusText = socketStatusText,
        serviceConnectionMissing = connectionStatus == "connecting" and reason == "waiting_connection",
        waitingSeconds = self.connectionStartedAt and math.max(0, currentTime - self.connectionStartedAt) or nil,
        unsupportedReason = self.unsupportedReason,
    }
end

function NetEaseAPI:GetServiceStatus(name)
    self:EnsureStorage()

    local currentTime = now()
    local diagnostic = self:GetConnectionDiagnostic()

    if diagnostic.status == "unsupported" then
        return "fault", getServiceMessage("fault"), diagnostic.reason, diagnostic
    end

    if diagnostic.status == "not_started" then
        return "starting", getServiceMessage("starting"), diagnostic.reason, diagnostic
    end

    if diagnostic.serviceConnectionMissing
        or diagnostic.status == "connecting"
        or diagnostic.status == "missing_handler"
        or diagnostic.status == "not_listening"
    then
        return "offline", getServiceMessage("offline"), diagnostic.reason, diagnostic
    end

    local normalized = name and self:NormalizeName(name)
    if normalized then
        local failed = self.failed[normalized]
        if failed and currentTime - (failed.updatedAt or 0) < (self.options.failedCooldown or FAILED_COOLDOWN) then
            if failed.reason == "timeout" then
                return "no_response", getServiceMessage("no_response"), failed.reason, diagnostic
            end
            return "fault", getServiceMessage("fault"), failed.reason, diagnostic
        end
    end

    if self.lastTimeoutAt
        and currentTime - self.lastTimeoutAt < (self.options.failedCooldown or FAILED_COOLDOWN)
        and (not self.lastSQGLIBAt or self.lastSQGLIBAt < self.lastTimeoutAt)
    then
        return "no_response", getServiceMessage("no_response"), "timeout", diagnostic
    end

    if self.lastCQGLIBSendFailedReason then
        return "fault", getServiceMessage("fault"), self.lastCQGLIBSendFailedReason, diagnostic
    end

    if self.lastSQGLIBBuildFailedCount and self.lastSQGLIBBuildFailedCount > 0 then
        return "fault", getServiceMessage("fault"), "invalid-response", diagnostic
    end

    return "ok", getServiceMessage("ok"), diagnostic.reason, diagnostic
end

function NetEaseAPI:GetStats()
    self:EnsureStorage()

    local cacheCount = 0
    local pendingCount = 0
    local failedCount = 0

    for _ in pairs(self.cache) do
        cacheCount = cacheCount + 1
    end
    for _ in pairs(self.pending) do
        pendingCount = pendingCount + 1
    end
    for _ in pairs(self.failed) do
        failedCount = failedCount + 1
    end

    local diagnostic = self:GetConnectionDiagnostic()
    local serviceStatus, serviceMessage, serviceReason = self:GetServiceStatus()

    return {
        major = MAJOR,
        minor = MINOR,
        started = self.started and true or false,
        connected = self.connected and true or false,
        connectionStatus = diagnostic.status,
        connectionReason = diagnostic.reason,
        connectionMessage = diagnostic.message,
        serviceStatus = serviceStatus,
        serviceMessage = serviceMessage,
        serviceReason = serviceReason,
        serviceConnected = diagnostic.connected,
        serviceConnectionMissing = diagnostic.serviceConnectionMissing,
        socketStatus = diagnostic.socketStatus,
        socketStatusText = diagnostic.socketStatusText,
        connectTarget = diagnostic.connectTarget,
        target = diagnostic.target,
        waitingSeconds = diagnostic.waitingSeconds,
        lastLoginAt = self.lastLoginAt,
        lastLoginVersion = self.lastLoginVersion,
        lastLoginAddonSource = self.lastLoginAddonSource,
        lastLoginHasPlayerGuid = self.lastLoginHasPlayerGuid,
        lastLoginHasBattleTag = self.lastLoginHasBattleTag,
        lastLoginQueryDataType = self.lastLoginQueryDataType,
        lastLoginQueryDataCount = self.lastLoginQueryDataCount,
        lastLoginDeliveredAt = self.lastLoginDeliveredAt,
        lastLoginDeliveryTimeoutAt = self.lastLoginDeliveryTimeoutAt,
        lastCQTBAt = self.lastCQTBAt,
        lastDeliveredCQTBAt = self.lastDeliveredCQTBAt,
        lastSQTBAt = self.lastSQTBAt,
        lastSQTBIsDeal = self.lastSQTBIsDeal,
        lastSendServerCommandAt = self.lastSendServerCommandAt,
        lastSendServerCommand = self.lastSendServerCommand,
        lastSendServerCommandSendId = self.lastSendServerCommandSendId,
        lastSendServerCommandError = self.lastSendServerCommandError,
        lastDeliveryProgressAt = self.lastDeliveryProgressAt,
        lastDeliveryProgressCommand = self.lastDeliveryProgressCommand,
        lastDeliveryProgressSent = self.lastDeliveryProgressSent,
        lastDeliveryProgressTotal = self.lastDeliveryProgressTotal,
        lastDeliveryProgressTarget = self.lastDeliveryProgressTarget,
        lastDeliveredAt = self.lastDeliveredAt,
        lastDeliveredCommand = self.lastDeliveredCommand,
        lastDeliveredSent = self.lastDeliveredSent,
        lastDeliveredTotal = self.lastDeliveredTotal,
        lastDeliveredTarget = self.lastDeliveredTarget,
        lastDeliveredDistribution = self.lastDeliveredDistribution,
        lastDeliveredSLOGINAt = self.lastDeliveredSLOGINAt,
        lastDeliveredSLOGINVersion = self.lastDeliveredSLOGINVersion,
        lastDeliveredCQGLIBAt = self.lastDeliveredCQGLIBAt,
        lastDeliveredCQGLIBNames = self.lastDeliveredCQGLIBNames,
        lastQuerySendDelay = self.lastQuerySendDelay,
        lastQuerySendDelayedAt = self.lastQuerySendDelayedAt,
        lastOutboundAt = self.lastOutboundAt,
        lastOutboundDistribution = self.lastOutboundDistribution,
        lastOutboundTarget = self.lastOutboundTarget,
        lastOutboundCommand = self.lastOutboundCommand,
        lastOutboundArgCount = self.lastOutboundArgCount,
        lastOutboundFirstArgType = self.lastOutboundFirstArgType,
        lastOutboundFirstArgKeyCount = self.lastOutboundFirstArgKeyCount,
        lastOutboundFirstArgKeys = self.lastOutboundFirstArgKeys,
        lastOutboundSLOGINAt = self.lastOutboundSLOGINAt,
        lastOutboundSLOGINTarget = self.lastOutboundSLOGINTarget,
        lastOutboundSLOGINArgCount = self.lastOutboundSLOGINArgCount,
        lastOutboundSLOGINVersion = self.lastOutboundSLOGINVersion,
        lastOutboundCQTBAt = self.lastOutboundCQTBAt,
        lastOutboundCQTBTarget = self.lastOutboundCQTBTarget,
        lastOutboundCQTBArgCount = self.lastOutboundCQTBArgCount,
        lastOutboundCQGLIBAt = self.lastOutboundCQGLIBAt,
        lastOutboundCQGLIBTarget = self.lastOutboundCQGLIBTarget,
        lastOutboundCQGLIBArgCount = self.lastOutboundCQGLIBArgCount,
        lastOutboundCQGLIBNames = self.lastOutboundCQGLIBNames,
        lastCQGLIBAt = self.lastCQGLIBAt,
        lastCQGLIBNames = self.lastCQGLIBNames,
        lastCQGLIBCount = self.lastCQGLIBCount,
        lastCQGLIBSendFailedAt = self.lastCQGLIBSendFailedAt,
        lastCQGLIBSendFailedReason = self.lastCQGLIBSendFailedReason,
        lastSQGLIBAt = self.lastSQGLIBAt,
        lastSQGLIBType = self.lastSQGLIBType,
        lastSQGLIBCount = self.lastSQGLIBCount,
        lastSQGLIBKeys = self.lastSQGLIBKeys,
        lastSQGLIBAcceptedCount = self.lastSQGLIBAcceptedCount,
        lastSQGLIBBuildFailedCount = self.lastSQGLIBBuildFailedCount,
        lastSQGLIBBuildFailedKeys = self.lastSQGLIBBuildFailedKeys,
        lastTimeoutAt = self.lastTimeoutAt,
        lastTimeoutNames = self.lastTimeoutNames,
        cacheCount = cacheCount,
        pendingCount = pendingCount,
        failedCount = failedCount,
        unsupportedReason = self.unsupportedReason,
    }
end

function NetEaseAPI:Debug(...)
    if self.options.debug and type(print) == "function" then
        print("|cff00aaffNetEaseAPI|r", ...)
    end
end

NetEaseAPI:EnsureStorage()
