local MAJOR, MINOR = "LibMatic-Broadcast-1.0", 3
assert(LibStub, MAJOR .. " requires LibStub")
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

lib.profiles = lib.profiles or {}
lib.peers = lib.peers or {}
lib.recentSends = lib.recentSends or {}

local SEP = "\31"
local PRESENCE = "P"
local DIRECTIVE = "A"
local STALE_SECONDS = 300

local function fetchAceComm()
    return LibStub and LibStub("AceComm-3.0", true)
end

local function ensureComm()
    local AceComm = fetchAceComm()
    if not AceComm then return nil end
    if not lib.commObject then
        lib.commObject = {}
        AceComm:Embed(lib.commObject)
    end
    return lib.commObject
end

local function trimRealm(realm)
    if not realm then return "" end
    return (realm:gsub("%s+", ""))
end

local function selfTag()
    local name = (UnitName and UnitName("player")) or "Unknown"
    local realm = trimRealm(GetRealmName and GetRealmName())
    if realm == "" then return name end
    return name .. "-" .. realm
end

local function normalize(sender)
    if not sender or sender == "" then return nil end
    if sender:find("-", 1, true) then return sender end
    local realm = trimRealm(GetRealmName and GetRealmName())
    if realm == "" then return sender end
    return sender .. "-" .. realm
end

local function resolveHandle(handleOrName)
    if type(handleOrName) == "table" then return handleOrName end
    if type(handleOrName) == "string" then return lib.profiles[handleOrName] end
    return nil
end

local function groupContext()
    local inRaid = IsInRaid and IsInRaid() and true or false
    local inGroup = IsInGroup and IsInGroup() and true or false
    local lfg = IsPartyLFG and IsPartyLFG() and true or false
    return inRaid, inGroup, lfg
end

local function chatChannel(inRaid, inGroup, lfg)
    if lfg then return "INSTANCE_CHAT" end
    if inRaid then return "RAID" end
    if inGroup then return "PARTY" end
    return nil
end

local function commChannel(inRaid, lfg)
    if lfg then return "INSTANCE_CHAT" end
    if inRaid then return "RAID" end
    return "PARTY"
end

local function doSend(handle, fullText, channel, dedupeKey)
    if type(SendChatMessage) ~= "function" then return end
    local now = (GetTime and GetTime()) or 0
    local recent = lib.recentSends[handle.name]
    if not recent then
        recent = {}
        lib.recentSends[handle.name] = recent
    end
    local last = recent[dedupeKey]
    if last and (now - last) < handle.dedupeSeconds then return end
    -- Prune entries past the dedupe window so this per-handle table can't grow
    -- unbounded over a session of unique broadcast strings.
    for key, t in pairs(recent) do
        if (now - t) >= handle.dedupeSeconds then
            recent[key] = nil
        end
    end
    recent[dedupeKey] = now
    SendChatMessage(fullText, channel)
end

local function rosterSet()
    local set = {}
    set[selfTag()] = true
    local n = (GetNumGroupMembers and GetNumGroupMembers()) or 0
    local unitPrefix = (IsInRaid and IsInRaid()) and "raid" or "party"
    local count = (unitPrefix == "raid") and n or (n - 1)
    for i = 1, count do
        local name, realm = UnitName(unitPrefix .. i)
        if name then
            realm = trimRealm(realm)
            if realm ~= "" then
                set[name .. "-" .. realm] = true
            else
                set[normalize(name)] = true
            end
        end
    end
    return set
end

local function sendPresence(handle)
    local comm = ensureComm()
    if not comm then return end
    local inRaid, inGroup, lfg = groupContext()
    if not inGroup then return end
    comm:SendCommMessage(handle.commPrefix, PRESENCE, commChannel(inRaid, lfg))
end

local function onComm(handle, prefix, message, distribution, sender)
    if type(message) ~= "string" or not sender then return end
    local from = normalize(sender)
    if not from or from == selfTag() then return end
    local kind = message:sub(1, 1)
    if kind == PRESENCE then
        local registry = lib.peers[handle.name]
        if registry then
            registry[from] = (GetTime and GetTime()) or 0
        end
    elseif kind == DIRECTIVE then
        local chosen, channel, dedupeKey, fullText =
            message:match("^A" .. SEP .. "([^" .. SEP .. "]*)" .. SEP
                .. "([^" .. SEP .. "]*)" .. SEP
                .. "([^" .. SEP .. "]*)" .. SEP .. "(.+)$")
        if chosen and selfTag() == chosen then
            doSend(handle, fullText, channel, dedupeKey)
        end
    end
end

local function ensureFrame()
    if lib.frame then return end
    lib.frame = CreateFrame("Frame")
    lib.frame:RegisterEvent("GROUP_ROSTER_UPDATE")
    lib.frame:SetScript("OnEvent", function()
        for _, handle in pairs(lib.profiles) do
            sendPresence(handle)
        end
    end)
end

local function seedRandom()
    if type(math.randomseed) == "function" then
        local seed = (GetTime and math.floor(GetTime() * 1000)) or 0
        pcall(math.randomseed, seed)
    end
end

local function sendDirective(handle, chosen, channel, dedupeKey, fullText, comm, dist)
    local payload = DIRECTIVE .. SEP .. chosen .. SEP .. channel .. SEP .. dedupeKey .. SEP .. fullText
    comm:SendCommMessage(handle.commPrefix, payload, dist)
    if selfTag() == chosen then
        doSend(handle, fullText, channel, dedupeKey)
    end
end

function lib:Register(config)
    assert(type(config) == "table", MAJOR .. ":Register requires a config table")
    assert(type(config.name) == "string" and config.name ~= "", MAJOR .. ":Register requires config.name")
    assert(type(config.commPrefix) == "string" and config.commPrefix ~= "", MAJOR .. ":Register requires config.commPrefix")

    local name = config.name
    local handle = lib.profiles[name] or {}
    lib.profiles[name] = handle
    handle.name = name
    handle.commPrefix = config.commPrefix
    handle.electionEnabled = config.electionEnabled and true or false
    handle.dedupeSeconds = config.dedupeSeconds or 30
    lib.peers[name] = lib.peers[name] or {}
    lib.recentSends[name] = lib.recentSends[name] or {}

    local comm = ensureComm()
    if comm then
        comm:RegisterComm(handle.commPrefix, function(prefix, message, distribution, sender)
            onComm(handle, prefix, message, distribution, sender)
        end)
        sendPresence(handle)
    end
    ensureFrame()

    return handle
end

function lib:Get(name)
    return lib.profiles[name]
end

function lib:Broadcast(handleOrName, opts)
    local handle = resolveHandle(handleOrName)
    if not handle then return end
    opts = opts or {}
    assert(type(opts.text) == "string" and opts.text ~= "", MAJOR .. ":Broadcast requires opts.text")
    assert(type(opts.marker) == "number" and opts.marker >= 1 and opts.marker <= 8,
        MAJOR .. ":Broadcast requires opts.marker between 1 and 8")

    local fullText = "{rt" .. opts.marker .. "} " .. opts.text

    local election = opts.electionEnabled
    if election == nil then election = handle.electionEnabled end

    local inRaid, inGroup, lfg = groupContext()
    if not opts.channel and not inGroup then return end
    local channel = opts.channel or chatChannel(inRaid, inGroup, lfg)
    if not channel then return end

    -- Key dedupe by channel as well as text. The same announcement is often sent
    -- to a group channel and then to GUILD in one pass; a text-only key would let
    -- the second send be swallowed as a duplicate of the first.
    local dedupeKey = opts.dedupeKey or (opts.text .. SEP .. channel)

    if election and inRaid and not lfg then
        local leader = UnitIsGroupLeader and UnitIsGroupLeader("player")
        local assist = UnitIsGroupAssistant and UnitIsGroupAssistant("player")
        if leader or assist then
            doSend(handle, fullText, "RAID_WARNING", dedupeKey)
            return
        end
    end

    if election and inRaid then
        local comm = ensureComm()
        if comm then
            local peers = lib:GetPeers(handle)
            if #peers > 0 then
                seedRandom()
                local chosen = peers[math.random(1, #peers)]
                sendDirective(handle, chosen, channel, dedupeKey, fullText, comm, commChannel(inRaid, lfg))
                return
            end
        end
    end

    doSend(handle, fullText, channel, dedupeKey)
end

function lib:GetPeers(handleOrName)
    local handle = resolveHandle(handleOrName)
    if not handle then return {} end
    local registry = lib.peers[handle.name]
    if not registry then return {} end
    local roster = rosterSet()
    local now = (GetTime and GetTime()) or 0
    local me = selfTag()
    local out = {}
    for name, seen in pairs(registry) do
        if roster[name] and name ~= me and (now - seen) <= STALE_SECONDS then
            out[#out + 1] = name
        elseif not roster[name] and (now - seen) > STALE_SECONDS then
            registry[name] = nil
        end
    end
    return out
end

function lib:SetElectionEnabled(handleOrName, enabled)
    local handle = resolveHandle(handleOrName)
    if not handle then return end
    handle.electionEnabled = enabled and true or false
end
