local pluginName, pluginShared = ...

-- The player network: AppearanceMatic clients in your guild and group trade sightings the game
-- only hands out locally, over addon messages (retail carries them on GUILD, RAID, PARTY and
-- INSTANCE_CHAT only). Each client broadcasts what it saw itself; received news merges newest-wins
-- into the same stores the panel reads, so one guildie near Tol Barad lights the badge for all.
local Network = {}
pluginShared.Network = Network

local PREFIX = "APPRMATICNET"
local WEEK = 7 * 86400

Network.peersHeard = {}
Network.mergedCount = 0
Network.sentCount = 0
Network.lastMergeFrom = nil
Network.lastMergeAt = nil
local lastSent = {}

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 type(sender) ~= "string" 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 globalStore()
    local db = pluginShared.db
    return db and db.global or nil
end

-- The moment this week began, from the client's own reset timer; nil when it cannot say.
function Network.WeekStart()
    if not (type(C_DateAndTime) == "table" and type(C_DateAndTime.GetSecondsUntilWeeklyReset) == "function") then
        return nil
    end
    local ok, seconds = pcall(C_DateAndTime.GetSecondsUntilWeeklyReset)
    if not (ok and type(seconds) == "number") then
        return nil
    end
    return time() + seconds - WEEK
end

function Network.IsFresh(seenAt)
    local start = Network.WeekStart()
    return type(seenAt) == "number" and start ~= nil and seenAt >= start
end

function Network.EncodeControl(zoneMapID, holder, seenAt, battleAt)
    return string.format("C1|%d|%s|%d|%d", zoneMapID, holder == "Alliance" and "A" or "H", seenAt, battleAt or 0)
end

function Network.EncodeBosses(set, seenAt)
    local ids = {}
    for encounterID in pairs(set) do
        ids[#ids + 1] = encounterID
    end
    table.sort(ids)
    return "W1|" .. table.concat(ids, ",") .. "|" .. seenAt
end

-- "C", zone, holder, seenAt, battleAt for control; "W", idSet, seenAt for bosses; nil for junk.
function Network.Decode(message)
    if type(message) ~= "string" then return nil end
    local zone, letter, seenAt, battleAt = message:match("^C1|(%d+)|([AH])|(%d+)|(%d+)$")
    if zone then
        return "C", tonumber(zone), letter == "A" and "Alliance" or "Horde", tonumber(seenAt), tonumber(battleAt)
    end
    local list, at = message:match("^W1|([%d,]+)|(%d+)$")
    if list then
        local set = {}
        for id in list:gmatch("%d+") do
            set[tonumber(id)] = true
        end
        return "W", set, tonumber(at)
    end
    return nil
end

-- A peer's control sighting: kept only when newer than what is already known. true when it landed.
function Network.MergeControl(zoneMapID, holder, seenAt, battleAt, from)
    local store = globalStore()
    if not store then return false end
    store.zoneControl = store.zoneControl or {}
    local current = store.zoneControl[zoneMapID]
    if current and type(current.seenAt) == "number" and current.seenAt >= seenAt then
        return false
    end
    store.zoneControl[zoneMapID] = {
        holder = holder,
        seenAt = seenAt,
        battleAt = battleAt and battleAt > 0 and battleAt or nil,
        via = from,
    }
    return true
end

-- A peer's active-boss scan: per boss, the newest sighting this week wins; last week's news is out.
function Network.MergeBosses(set, seenAt, from)
    if not Network.IsFresh(seenAt) then return false end
    local store = globalStore()
    if not store then return false end
    store.bossNetwork = store.bossNetwork or {}
    local changed = false
    for encounterID in pairs(set) do
        local known = store.bossNetwork[encounterID]
        if not known or known < seenAt then
            store.bossNetwork[encounterID] = seenAt
            changed = true
        end
    end
    return changed
end

local function channels()
    local out = {}
    if IsInGuild and IsInGuild() then
        out[#out + 1] = "GUILD"
    end
    -- battlegrounds, dungeon finder and raid finder are instance groups: their traffic rides
    -- INSTANCE_CHAT (Blizzard's own broadcasters test the same way)
    if IsInGroup and LE_PARTY_CATEGORY_INSTANCE and IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        out[#out + 1] = "INSTANCE_CHAT"
    elseif IsPartyLFG and IsPartyLFG() then
        out[#out + 1] = "INSTANCE_CHAT"
    elseif IsInRaid and IsInRaid() then
        out[#out + 1] = "RAID"
    elseif IsInGroup and IsInGroup() then
        out[#out + 1] = "PARTY"
    end
    return out
end

local function sendAll(key, message)
    if not (type(C_ChatInfo) == "table" and type(C_ChatInfo.SendAddonMessage) == "function") then return end
    for _, channel in ipairs(channels()) do
        local slot = key .. channel
        if lastSent[slot] ~= message then
            lastSent[slot] = message
            pcall(C_ChatInfo.SendAddonMessage, PREFIX, message, channel)
            Network.sentCount = Network.sentCount + 1
        end
    end
end

-- Broadcast what this client saw itself: its own control sightings and its own boss scan. News
-- merged from peers stays quiet, so the mesh cannot echo.
function Network:BroadcastOwn()
    local store = globalStore()
    if store and type(store.zoneControl) == "table" then
        for zoneMapID, seen in pairs(store.zoneControl) do
            if type(seen) == "table" and seen.holder and type(seen.seenAt) == "number"
                and (seen.via == nil or seen.via == "local") then
                sendAll("C" .. zoneMapID, Network.EncodeControl(zoneMapID, seen.holder, seen.seenAt, seen.battleAt))
            end
        end
    end
    local lockouts = pluginShared.Lockouts
    local set = lockouts and lockouts.activeBossSet
    if type(set) == "table" and next(set) then
        local ids = {}
        for encounterID in pairs(set) do
            ids[#ids + 1] = encounterID
        end
        table.sort(ids)
        local fingerprint = table.concat(ids, ",")
        for _, channel in ipairs(channels()) do
            local slot = "W" .. channel
            if lastSent[slot] ~= fingerprint then
                lastSent[slot] = fingerprint
                if type(C_ChatInfo) == "table" and type(C_ChatInfo.SendAddonMessage) == "function" then
                    pcall(C_ChatInfo.SendAddonMessage, PREFIX, Network.EncodeBosses(set, time()), channel)
                    Network.sentCount = Network.sentCount + 1
                end
            end
        end
    end
end

function Network:OnAddonMessage(prefix, message, _, sender)
    if prefix ~= PREFIX then return end
    local from = normalize(sender)
    if not from or from == selfTag() then return end
    local kind, a, b, c, d = Network.Decode(message)
    if not kind then return end
    self.peersHeard[from] = time()
    local lockouts = pluginShared.Lockouts
    local landed = false
    if kind == "C" then
        landed = Network.MergeControl(a, b, c, d, from)
        if landed and lockouts and lockouts.InvalidateGates then
            lockouts:InvalidateGates()
        end
    elseif kind == "W" then
        landed = Network.MergeBosses(a, b, from)
        if landed and lockouts then
            -- the next scan re-unions the shared bosses
            lockouts.activeBossSet, lockouts.activeBossAskedAt = nil, nil
        end
    end
    if landed then
        self.mergedCount = self.mergedCount + 1
        self.lastMergeFrom, self.lastMergeAt = from, time()
        if lockouts and lockouts.ScheduleRebuild then
            lockouts:ScheduleRebuild()
        end
    end
end

function Network:DiagText()
    local peers = 0
    for _ in pairs(self.peersHeard) do
        peers = peers + 1
    end
    local last = ""
    if self.lastMergeFrom then
        last = string.format(" last=%s %dm ago", self.lastMergeFrom, math.floor((time() - (self.lastMergeAt or 0)) / 60))
    end
    return string.format("peers=%d sent=%d merged=%d%s", peers, self.sentCount, self.mergedCount, last)
end

if type(C_ChatInfo) == "table" and type(C_ChatInfo.RegisterAddonMessagePrefix) == "function" then
    pcall(C_ChatInfo.RegisterAddonMessagePrefix, PREFIX)
end
if type(CreateFrame) == "function" then
    local frame = CreateFrame("Frame")
    frame:RegisterEvent("CHAT_MSG_ADDON")
    frame:RegisterEvent("PLAYER_ENTERING_WORLD")
    frame:SetScript("OnEvent", function(_, event, ...)
        if event == "CHAT_MSG_ADDON" then
            Network:OnAddonMessage(...)
        elseif event == "PLAYER_ENTERING_WORLD" then
            if type(C_Timer) == "table" and type(C_Timer.After) == "function" then
                C_Timer.After(15, function() Network:BroadcastOwn() end)
            end
        end
    end)
    if type(C_Timer) == "table" and type(C_Timer.NewTicker) == "function" then
        C_Timer.NewTicker(60, function() Network:BroadcastOwn() end)
    end
end
