
local CallbackHandler = LibStub('CallbackHandler-1.0')
local SocketMiddleware = LibStub('NetEaseSocketMiddleware-2.0')
local BroadMiddleware = LibStub('NetEaseBroadMiddleware-2.0')
local AceTimer = LibStub('AceTimer-3.0')
local AceEvent = LibStub('AceEvent-3.0')

local MAJOR, MINOR = 'SocketHandler-2.0', 21
local SocketHandler,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not SocketHandler then return end

local random = fastrandom or random or math.random
local CanAccessValue = canaccessvalue
local IsSecretValue = issecretvalue

local SOCKET_NORMAL  = 1
local SOCKET_CONNECT = 2
local SOCKET_READY   = 3

local CONNECT_DELAY = 3
local RETRY_DELAY = 3

local NOT_FOUND_MATCH = ERR_CHAT_PLAYER_NOT_FOUND_S and ERR_CHAT_PLAYER_NOT_FOUND_S:format('(.+)') or nil

local SOCKET_BASE_CMD = {
    NETEASE_CONNECT_SUCCESS = true,
    NETEASE_CHANNEL_OWNER = true,
}

SocketHandler.EventHandler = SocketHandler.EventHandler or {}

SocketHandler.objects = SocketHandler.objects or {}
SocketHandler.channels = SocketHandler.channels or {}
SocketHandler.connectQueue = SocketHandler.connectQueue or {}
SocketHandler.connectStatus = SocketHandler.connectStatus or {}
SocketHandler.suppressNotFound = SocketHandler.suppressNotFound or {}

SocketHandler.status = SOCKET_NORMAL
SocketHandler.isLoggedIn = false
SocketHandler._meta = {__index = SocketHandler}

local objects = SocketHandler.objects
local channels = SocketHandler.channels
local connectQueue = SocketHandler.connectQueue
local EventHandler = SocketHandler.EventHandler

local function IsSocketHandlerSecretValue(value)
    if type(CanAccessValue) == 'function' then
        local ok, canAccess = pcall(CanAccessValue, value)
        if not ok or not canAccess then
            return true
        end
    end

    if type(IsSecretValue) == 'function' then
        local ok, isSecret = pcall(IsSecretValue, value)
        if not ok or isSecret then
            return true
        end
    end

    return false
end

local function IsSocketHandlerSafeString(value)
    if value == nil or IsSocketHandlerSecretValue(value) then
        return false
    end
    return type(value) == 'string' and value ~= ''
end

local function IsSocketHandlerSafeCommand(cmd)
    return IsSocketHandlerSafeString(cmd) and #cmd <= 64
end

local function IsSocketHandlerValidChannelName(channelName)
    return IsSocketHandlerSafeString(channelName)
        and not channelName:find('%z')
        and not channelName:find('-', 1, true)
end

local function IsTimerActive(owner, timer)
    if not (owner and timer and owner.TimeLeft) then
        return false
    end
    local ok, remaining = pcall(owner.TimeLeft, owner, timer)
    return ok and tonumber(remaining) and tonumber(remaining) > 0
end

local function SafeCancelTimer(owner, timer)
    if owner and timer and owner.CancelTimer then
        pcall(owner.CancelTimer, owner, timer)
    end
end

local function IsHomeRaid()
    if not IsInRaid then
        return false
    end
    local ok, inRaid = pcall(IsInRaid, LE_PARTY_CATEGORY_HOME)
    return ok and inRaid == true
end

local function IsHomeGroup()
    if not IsInGroup then
        return false
    end
    local ok, inGroup = pcall(IsInGroup, LE_PARTY_CATEGORY_HOME)
    return ok and inGroup == true
end

local function IsInstanceGroup()
    if not IsInGroup then
        return false
    end
    local ok, inGroup = pcall(IsInGroup, LE_PARTY_CATEGORY_INSTANCE)
    return ok and inGroup == true
end

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

local function QueueContains(queue, value)
    for _, item in ipairs(queue) do
        if item == value then
            return true
        end
    end
    return false
end

local function OnUsed(registry, target, cmd)
    if target and IsSocketHandlerSafeCommand(cmd) then
        target:RegisterCallback(cmd, 'OnSocket')
    end
end

local _server
local function getServer()
    if not IsSocketHandlerSafeString(_server) then
        if type(GetNormalizedRealmName) == 'function' then
            local ok, realm = pcall(GetNormalizedRealmName)
            if ok then
                _server = realm
            end
        end

        if not IsSocketHandlerSafeString(_server) and type(GetRealmName) == 'function' then
            local ok, realmName = pcall(GetRealmName)
            if ok and IsSocketHandlerSafeString(realmName) then
                _server = realmName:gsub('%s+', '')
            end
        end
    end

    return IsSocketHandlerSafeString(_server) and _server or nil
end

local function NormalizePlayerName(name)
    if not IsSocketHandlerSafeString(name) then
        return nil
    end

    local ok, normalized = pcall(Ambiguate, name, 'none')
    if ok and IsSocketHandlerSafeString(normalized) then
        return normalized
    end
end

local function formatTarget(target)
    if not IsSocketHandlerSafeString(target) then
        return nil
    end

    if target:find('-', 1, true) then
        return NormalizePlayerName(target)
    end

    local server = getServer()
    if not server then
        return NormalizePlayerName(target)
    end

    return NormalizePlayerName(target .. '-' .. server)
end

local function getChannelId(channelName)
    if not IsSocketHandlerValidChannelName(channelName) then
        return nil
    end
    if type(GetChannelName) ~= 'function' then
        return nil
    end
    local ok, id = pcall(GetChannelName, channelName)
    if ok and type(id) == 'number' and id > 0 then
        return id
    end
end

local function ClearTable(tbl)
    if not tbl then
        return
    end
    if wipe then
        wipe(tbl)
        return
    end
    for key in pairs(tbl) do
        tbl[key] = nil
    end
end

local function HasCallback(registry, cmd)
    local events = registry and registry.events
    local bucket = events and events[cmd]
    return bucket and next(bucket) ~= nil
end

function SocketHandler:New()
    local obj = setmetatable({}, self._meta)

    local socketRegistry = CallbackHandler:New(obj, 'RegisterSocket', 'UnregisterSocket', 'UnregisterAllSocket')
    local serverRegistry = CallbackHandler:New(obj, 'RegisterServer', 'UnregisterServer', 'UnregisterAllServer')

    local function OnUnused(registry, target, cmd)
        if SOCKET_BASE_CMD[cmd] then
            return
        end
        if HasCallback(socketRegistry, cmd) or HasCallback(serverRegistry, cmd) then
            return
        end
        if target and target.UnregisterCallback then
            target:UnregisterCallback(cmd)
        end
    end

    socketRegistry.OnUsed = OnUsed
    socketRegistry.OnUnused = OnUnused
    serverRegistry.OnUsed = OnUsed
    serverRegistry.OnUnused = OnUnused

    obj.FireSocket = socketRegistry.Fire
    obj.FireServer = serverRegistry.Fire

    SocketMiddleware:Embed(obj)
    AceTimer:Embed(obj)

    tinsert(objects, obj)

    return obj
end

function SocketHandler:PreServer(cmd, distribution, sender, ...)
    if not IsSocketHandlerSafeCommand(cmd) then
        return nil
    end

    if cmd == 'NETEASE_CONNECT_SUCCESS' then
        local key, channelName = ...
        local statusTable = self.statusTable
        local connectKey = statusTable and statusTable.connectKey
        if IsSocketHandlerSafeString(sender)
            and statusTable
            and statusTable.status == SOCKET_CONNECT
            and IsSocketHandlerSafeString(connectKey)
            and IsSocketHandlerSafeString(key)
            and connectKey == key
            and (channelName == nil or IsSocketHandlerValidChannelName(channelName))
        then
            SafeCancelTimer(self, statusTable.retryConnectTimer)
            self.target = sender
            statusTable.status = SOCKET_READY
            statusTable.retryConnectTimer = nil

            self:SetChannel(channelName)

            self:FireServer('SERVER_CONNECTED')
        end
        return true
    end

    if cmd == 'NETEASE_CHANNEL_OWNER' then
        local channelName = ...
        if self:IsReady()
            and self:IsServer(sender)
            and IsSocketHandlerSafeString(sender)
            and IsSocketHandlerValidChannelName(channelName)
            and channelName == self.channelName
            and type(SetChannelOwner) == 'function'
        then
            pcall(SetChannelOwner, channelName, sender)
        end
        return true
    end
end

function SocketHandler:OnSocket(cmd, distribution, sender, ...)
    if not IsSocketHandlerSafeCommand(cmd) then
        return
    end
    if self:PreServer(cmd, distribution, sender, ...) then
        return
    elseif self:IsServer(sender) and self:IsReady() then
        self:FireServer(cmd, ...)
    elseif not SOCKET_BASE_CMD[cmd] then
        self:FireSocket(cmd, sender, ...)
    end
end

function SocketHandler:ListenSocket(prefix, target)
    self.prefix = prefix
    self.connectTarget = formatTarget(target)
    self:UpdateStatusTable()
    return self:Listen(prefix, 4)
end

function SocketHandler:CheckSendDistribution(target)
    if target == '@GROUP' then
        if IsInstanceGroup() then
            return 'INSTANCE_CHAT'
        elseif IsHomeRaid() then
            return 'RAID'
        elseif IsHomeGroup() then
            return 'PARTY'
        end
    elseif target == '@CHANNEL' then
        if type(self.channelId) == 'number' and self.channelId > 0 then
            return 'CHANNEL', self.channelId
        end
    elseif target == '@GUILD' then
        return 'GUILD'
    elseif target == '@BATTLEGROUND' then
        if IsInstanceGroup() then
            return 'INSTANCE_CHAT'
        end
    elseif IsSocketHandlerSafeString(target) then
        return 'WHISPER', target
    end
end

function SocketHandler:SendSocket(target, cmd, ...)
    if not IsSocketHandlerSafeCommand(cmd) then
        return false, 'INVALID_CMD'
    end
    local distribution, sendTarget = self:CheckSendDistribution(target)
    if not distribution then
        return false, 'INVALID_DISTRIBUTION'
    end
    return self:Send(distribution, sendTarget, cmd, ...)
end

function SocketHandler:ConnectServer(target)
    if not IsSocketHandlerSafeString(self.prefix) then
        error('Can`t found prefix in object', 4)
    end
    self.connectTarget = formatTarget(target) or self.connectTarget
    if not IsSocketHandlerSafeString(self.connectTarget) then
        error('Can`t found connectTarget in object', 4)
    end

    self:UpdateStatusTable()
    if not self.statusTable then
        error('Can`t found statusTable in object', 4)
    end
    SafeCancelTimer(self, self.statusTable.retryConnectTimer)
    self.statusTable.retryConnectTimer = nil
    self.statusTable.status = SOCKET_CONNECT

    self:RegisterCallback('NETEASE_CONNECT_SUCCESS', 'OnSocket')
    self:RegisterCallback('NETEASE_CHANNEL_OWNER', 'OnSocket')

    return self:TryConnect()
end

function SocketHandler:DisconnectServer()
    self.target = nil
    local statusTable = self.statusTable
    if statusTable then
        statusTable.status = SOCKET_NORMAL
        SafeCancelTimer(self, statusTable.retryConnectTimer)
        statusTable.retryConnectTimer = nil
    end
    self:ScheduleTimer('FireServer', 1, 'SERVER_DISCONNECTED')
    return true
end

function SocketHandler:ConnectChannel()
    if not IsSocketHandlerValidChannelName(self.channelName) then
        self.channelId = nil
        return false, 'INVALID_CHANNEL'
    end

    local id = getChannelId(self.channelName)
    if id then
        self.channelId = id
        self:FireServer('CHANNEL_CONNECTED')
        return true
    end

    self.channelId = nil
    if type(JoinTemporaryChannel) == 'function' then
        local ok, reason = pcall(JoinTemporaryChannel, self.channelName)
        if not ok then
            return false, reason
        end
        return true
    end
    return false, 'NO_JOIN_CHANNEL'
end

function SocketHandler:SendServer(cmd, ...)
    if not IsSocketHandlerSafeString(self.target) then
        return false, 'NO_TARGET'
    end
    if not IsSocketHandlerSafeCommand(cmd) then
        return false, 'INVALID_CMD'
    end
    return self:Send('WHISPER', self.target, cmd, ...)
end

function SocketHandler:TryConnect()
    local statusTable = self.statusTable
    if not statusTable then
        return false, 'NO_STATUS'
    end
    if statusTable.status == SOCKET_READY then
        return true
    end
    if IsTimerActive(self, statusTable.retryConnectTimer) then
        return false, 'RETRY_PENDING'
    end
    if not IsSocketHandlerSafeString(self.connectTarget) then
        return false, 'NO_CONNECT_TARGET'
    end

    if SocketHandler.isLoggedIn then
        SocketHandler.suppressNotFound[self.connectTarget] = (GetTime and GetTime() or 0) + 5
        local ok, reason = self:Send('WHISPER', self.connectTarget, 'NETEASE_CONNECT', self:GetConnectKey())
        statusTable.retryConnectTimer = self:ScheduleTimer('TryConnect', RETRY_DELAY)
        return ok, reason
    end

    if not QueueContains(connectQueue, self) then
        tinsert(connectQueue, self)
    end
    return false, 'WAIT_LOGIN'
end

function SocketHandler:UpdateStatusTable()
    if IsSocketHandlerSafeString(self.prefix) and IsSocketHandlerSafeString(self.connectTarget) then
        local key = self.prefix .. '.' .. self.connectTarget
        self.connectStatus[key] = self.connectStatus[key] or {}
        self.statusTable = self.connectStatus[key]
    else
        self.statusTable = nil
    end
end

function SocketHandler:GetConnectKey()
    local statusTable = self.statusTable
    if not statusTable then
        return nil
    end
    if not statusTable.connectKey then
        statusTable.connectKey = tostring(random(0x100000, 0xFFFFFF))
    end
    return statusTable.connectKey
end

function SocketHandler:SetChannel(channelName)
    if channelName ~= nil and not IsSocketHandlerValidChannelName(channelName) then
        return false, 'INVALID_CHANNEL'
    end

    local oldChannel = self.channelName
    if oldChannel == channelName then
        return true
    end

    if IsSocketHandlerValidChannelName(oldChannel) and channels[oldChannel] then
        channels[oldChannel][self] = nil
        if not next(channels[oldChannel]) then
            channels[oldChannel] = nil
            if type(LeaveChannelByName) == 'function' and not IsInProtectedLockdown() then
                pcall(LeaveChannelByName, oldChannel)
            end
        end
    end

    self.channelName = channelName
    if channelName == nil then
        self.channelId = nil
        return true
    end

    channels[channelName] = channels[channelName] or {}
    channels[channelName][self] = true

    BroadMiddleware.Listen(self, channelName)
    self:ConnectChannel()
    return true
end

function SocketHandler:IsReady()
    local statusTable = self.statusTable
    return statusTable and statusTable.status == SOCKET_READY
end

function SocketHandler:IsServer(sender)
    if not IsSocketHandlerSafeString(sender) then
        return false
    end
    return self.target == sender or self.connectTarget == sender
end

function SocketHandler:YOU_JOINED()
    if not IsSocketHandlerValidChannelName(self.channelName) then
        return
    end
    self.channelId = getChannelId(self.channelName)
    self:ScheduleTimer('FireServer', 1, 'CHANNEL_CONNECTED', true)
end

function SocketHandler:YOU_LEFT()
    self.channelId = nil
    self:ScheduleTimer('FireServer', 1, 'CHANNEL_DISCONNECTED')
end

function SocketHandler:WRONG_PASSWORD()
    self.channelId = nil

    if IsSocketHandlerValidChannelName(self.channelName) then
        self:SendServer('SCJF', self.channelName)
        self:ScheduleTimer('ConnectChannel', 10)
    end
end

SocketHandler.YOU_CHANGED = SocketHandler.YOU_JOINED
SocketHandler.BANNED = SocketHandler.WRONG_PASSWORD

---- EventHandler

AceEvent:Embed(EventHandler)
AceTimer:Embed(EventHandler)

local function RefreshVisibleChannelFrame()
    if type(ChannelList_Update) == 'function' then
        ChannelList_Update()
    elseif ChannelFrame and type(ChannelFrame.MarkDirty) == 'function' and type(ChannelFrame.Update) == 'function' then
        ChannelFrame:MarkDirty('UpdateAll')
        ChannelFrame:Update()
    elseif ChannelFrame and type(ChannelFrame.Update) == 'function' then
        ChannelFrame:Update()
    end
end

function EventHandler:CHAT_MSG_CHANNEL_NOTICE(_, event, _, _, _, _, _, _, id, channelName)
    local callback = IsSocketHandlerSafeString(event) and SocketHandler[event] or nil
    local channelHandlers = IsSocketHandlerValidChannelName(channelName) and channels[channelName] or nil
    if type(callback) == 'function' and type(channelHandlers) == 'table' then
        for handler in pairs(channelHandlers) do
            callback(handler)
        end
    end

    if ChannelFrame and type(ChannelFrame.IsShown) == 'function' and ChannelFrame:IsShown() then
        RefreshVisibleChannelFrame()
    end
end

function EventHandler:PLAYER_LOGOUT()
    self:UnregisterAllEvents()
    self:CancelAllTimers()

    SocketHandler.isLoggedIn = false
    ClearTable(connectQueue)
    ClearTable(SocketHandler.suppressNotFound)
    for channelName, handlers in pairs(channels) do
        ClearTable(handlers)
        channels[channelName] = nil
    end
end

function EventHandler:PLAYER_LOGIN()
    SocketHandler.isLoggedIn = true

    self:RegisterEvent('PLAYER_LOGOUT')
    self:RegisterEvent('CHAT_MSG_SYSTEM')
    self:RegisterEvent('CHAT_MSG_CHANNEL_NOTICE')

    for _, handler in ipairs(connectQueue) do
        handler:TryConnect()
    end
end

function EventHandler:CHAT_MSG_SYSTEM(_, msg)
    if not IsSocketHandlerSafeString(msg) or not NOT_FOUND_MATCH then
        return
    end

    if InCombatLockdown and InCombatLockdown() then
        return
    end

    local ok, name = pcall(string.match, msg, NOT_FOUND_MATCH)
    if not ok or not IsSocketHandlerSafeString(name) then
        return
    end

    name = NormalizePlayerName(name)
    if not name then
        return
    end

    for _, handler in pairs(objects) do
        if type(handler) == 'table' and handler.target == name and handler:IsReady() then
            handler:DisconnectServer()
        end
    end
end

EventHandler:CancelAllTimers()
EventHandler:UnregisterAllEvents()

if IsLoggedIn() then
    EventHandler:ScheduleTimer('PLAYER_LOGIN', CONNECT_DELAY)
else
    EventHandler:RegisterEvent('PLAYER_LOGIN', function()
        EventHandler:UnregisterEvent('PLAYER_LOGIN')
        EventHandler:ScheduleTimer('PLAYER_LOGIN', CONNECT_DELAY)
    end)
end

---- ChatFilter

if not SocketHandler.chatFilter then
    ChatFrame_AddMessageEventFilter('CHAT_MSG_SYSTEM', function(_, _, msg)
        if not IsSocketHandlerSafeString(msg) or not NOT_FOUND_MATCH then
            return
        end
        local ok, name = pcall(string.match, msg, NOT_FOUND_MATCH)
        if not ok or not IsSocketHandlerSafeString(name) then
            return
        end

        name = NormalizePlayerName(name)
        if not name then
            return
        end

        local expiresAt = SocketHandler.suppressNotFound[name]
        local now = GetTime and GetTime() or 0
        if expiresAt and expiresAt > now then
            SocketHandler.suppressNotFound[name] = nil
            return true
        elseif expiresAt then
            SocketHandler.suppressNotFound[name] = nil
        end
    end)
    SocketHandler.chatFilter = true
end

---- hook
if not SocketHandler.hooked then
    SocketHandler.hooked = true

    local orig_GetChannelDisplayInfo = GetChannelDisplayInfo
    function GetChannelDisplayInfo(id)
        local name, header, collapsed, channelNumber, count, active, category, channelType = orig_GetChannelDisplayInfo(id)
        if IsSocketHandlerSafeString(name) and channels[name] then
            active = nil
        end
        return name, header, collapsed, channelNumber, count, active, category, channelType
    end

    hooksecurefunc('CreateChatChannelList', function()
        if type(CHAT_CONFIG_CHANNEL_LIST) ~= 'table' then
            return
        end
        for i = #CHAT_CONFIG_CHANNEL_LIST, 1, -1 do
            local v = CHAT_CONFIG_CHANNEL_LIST[i]
            if type(v) == 'table' and IsSocketHandlerSafeString(v.channelName) and channels[v.channelName] then
                tremove(CHAT_CONFIG_CHANNEL_LIST, i)
            end
        end
    end)
end
