local addonName, TOAST = ...

-- A single conservative queue for every outgoing TOAST message. Guild and
-- whisper traffic share Blizzard's per-prefix allowance, so no feature may
-- send directly around this queue.

local MIN_SEND_INTERVAL = 1.05
local MAX_QUEUE_LENGTH = 160

local PRIORITY = {
    CONTROL = 500,
    SELF_STATE = 400,
    SNAPSHOT = 300,
    STATE_RESPONSE = 200,
    BACKGROUND = 100,
}

local queue = {}
local serial = 0
local lastSentAt = -math.huge
local recentWhisperTargets = {}
local WHISPER_ERROR_WINDOW = 5

local function NormalizeTarget(value)
    if type(value) ~= "string" then return nil end
    local normalized = value:gsub("[%s%-']", ""):lower()
    return normalized ~= "" and normalized or nil
end

local function RememberWhisperTarget(target)
    local normalized = NormalizeTarget(target)
    if not normalized then return end
    local expiresAt = GetTime() + WHISPER_ERROR_WINDOW
    recentWhisperTargets[normalized] = expiresAt

    -- Blizzard may phrase a cross-realm target error with just the character
    -- name. Remember both forms, while still limiting suppression to a TOAST
    -- whisper made in the last few seconds.
    local short = normalized:match("^([^%-]+)")
    if short then recentWhisperTargets[short] = expiresAt end
end

local function EscapePattern(value)
    return value:gsub("([^%w])", "%%%1")
end

local function PlayerNotFoundTarget(message)
    if type(message) ~= "string" or type(ERR_CHAT_PLAYER_NOT_FOUND) ~= "string" then return nil end
    local before, after = ERR_CHAT_PLAYER_NOT_FOUND:match("^(.-)%%s(.-)$")
    if not before then return nil end
    return message:match("^" .. EscapePattern(before) .. "(.+)" .. EscapePattern(after) .. "$")
end

-- A directed addon whisper can race a player logging out after C_Club marked
-- them online. Hide only that exact, recent TOAST failure from chat; manual
-- player whispers and unrelated system messages are left untouched.
if ChatFrame_AddMessageEventFilter then
    ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", function(_, _, message)
        local target = NormalizeTarget(PlayerNotFoundTarget(message))
        if target and (recentWhisperTargets[target] or 0) >= GetTime() then
            return true
        end
    end)
end

local function ResultCode(...)
    return select(-1, ...)
end

local function IsSuccess(result)
    local enum = Enum and Enum.SendAddonMessageResult
    return result == 0 or (enum and result == enum.Success)
end

local function IsThrottle(result)
    local enum = Enum and Enum.SendAddonMessageResult
    return result == 3 or result == 8
        or (enum and (result == enum.AddonMessageThrottle or result == enum.ChannelThrottle))
end

local function IsLockdown(result)
    local enum = Enum and Enum.SendAddonMessageResult
    return result == 11 or (enum and result == enum.AddOnMessageLockdown)
end

local function IsTargetOffline(result)
    local enum = Enum and Enum.SendAddonMessageResult
    return result == 12 or (enum and result == enum.TargetOffline)
end

local function RemoveAt(index)
    local entry = queue[index]
    table.remove(queue, index)
    return entry
end

local function BestReadyIndex(now)
    local bestIndex, best
    for index, entry in ipairs(queue) do
        if entry.notBefore <= now then
            if not best
                or entry.priority > best.priority
                or (entry.priority == best.priority and entry.serial < best.serial) then
                bestIndex, best = index, entry
            end
        end
    end
    return bestIndex, best
end

local function Finish(entry, status, result)
    if type(entry.callback) == "function" then
        entry.callback(status, result, entry)
    end
end

local function TraceOutgoing(entry, result)
    if TOAST.LogCommsTrace then
        TOAST.LogCommsTrace("OUT", entry.channel, entry.message, entry.target or "TIDE", result)
    end
end

local function SendQueuedMessage(entry)
    -- Target-not-found is emitted synchronously as UI_ERROR_MESSAGE by the
    -- client. Temporarily unhook only Blizzard's error frame for this single
    -- TOAST whisper; do not mute the user's normal errors or any other addon.
    local errorsFrame = entry.channel == "WHISPER" and UIErrorsFrame or nil
    local restoreErrors = errorsFrame and errorsFrame.IsEventRegistered
        and errorsFrame:IsEventRegistered("UI_ERROR_MESSAGE")
    if restoreErrors then errorsFrame:UnregisterEvent("UI_ERROR_MESSAGE") end

    local result = ResultCode(C_ChatInfo.SendAddonMessage(
        entry.prefix, entry.message, entry.channel, entry.target
    ))

    if restoreErrors then errorsFrame:RegisterEvent("UI_ERROR_MESSAGE") end
    return result
end

local function Pump()
    if #queue == 0 then return end
    if InCombatLockdown and InCombatLockdown() then return end

    local now = GetTime()
    if now - lastSentAt < MIN_SEND_INTERVAL then return end

    local index, entry = BestReadyIndex(now)
    if not entry then return end

    if entry.channel == "WHISPER" then RememberWhisperTarget(entry.target) end
    local result = SendQueuedMessage(entry)

    if IsSuccess(result) then
        RemoveAt(index)
        lastSentAt = now
        TraceOutgoing(entry, "sent")
        Finish(entry, "sent", result)
        return
    end

    if IsThrottle(result) or IsLockdown(result) then
        entry.attempts = entry.attempts + 1
        entry.notBefore = now + math.min(30, 1.5 * entry.attempts)
        TraceOutgoing(entry, IsThrottle(result) and "throttled" or "combat deferred")
        return
    end

    RemoveAt(index)
    if IsTargetOffline(result) then
        TraceOutgoing(entry, "target offline")
        Finish(entry, "target_offline", result)
    else
        TraceOutgoing(entry, "failed")
        Finish(entry, "failed", result)
    end
end

local frame = CreateFrame("Frame")
frame:SetScript("OnUpdate", function()
    Pump()
end)

function TOAST.QueueAddonMessage(priority, message, channel, target, callback)
    if type(message) ~= "string" or message == "" or #message > 210 then
        return false, "invalid_message"
    end
    if type(channel) ~= "string" or channel == "" then
        return false, "invalid_channel"
    end
    if channel == "WHISPER" and (type(target) ~= "string" or target == "") then
        return false, "missing_target"
    end
    if #queue >= MAX_QUEUE_LENGTH then
        return false, "queue_full"
    end

    serial = serial + 1
    queue[#queue + 1] = {
        prefix = "TOAST",
        message = message,
        channel = channel,
        target = target,
        callback = callback,
        priority = tonumber(priority) or PRIORITY.BACKGROUND,
        serial = serial,
        attempts = 0,
        notBefore = GetTime(),
    }
    return true
end

function TOAST.GetAddonQueueStatus()
    return {
        pending = #queue,
        lastSentAt = lastSentAt,
    }
end

-- Transfers must be either queued in full or not started at all.  A partial
-- begin/chunk/end sequence only burns the receiver's timeout window.
function TOAST.CanQueueAddonMessages(count)
    count = tonumber(count)
    return count ~= nil and count >= 0 and #queue + math.floor(count) <= MAX_QUEUE_LENGTH
end

TOAST.AddonMessagePriority = PRIORITY
