-- ============================================================
-- Asylum Compagnon — Sync.lua
-- Synchro entre joueurs via messages d'addon (canal invisible).
-- Même principe que KickSync / Northern Sky Raid Tools (AceComm).
-- Protocole :
--   H  = « hello » : présence + méta (classe, niveau, ilvl, or, items)
--   G  = « get »   : envoie-moi ton code SimC
--   C  = « chunk » : morceau i/n du code SimC
-- ============================================================

local ADDON_NAME, NS = ...

local PREFIX     = "AsylumSync"
local CHUNK_SIZE = 200
local MAX_CHUNKS = 120 -- garde-fou (≈ 24 Ko, le profil inclut les sacs)

local Sync = {}
NS.Sync = Sync

-- Enregistrement du préfixe (indispensable pour recevoir les messages)
Sync.available = false
if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
    local res = C_ChatInfo.RegisterAddonMessagePrefix(PREFIX)
    Sync.available = (res == true) or (res == 0)
        or (Enum and Enum.RegisterAddonMessagePrefixResult
            and res == Enum.RegisterAddonMessagePrefixResult.Success)
end

-- ---------- Identité ----------
local myFull
local function MyFullName()
    if myFull then return myFull end
    local name, realm = UnitFullName("player")
    if not name then return nil end
    realm = (realm and realm ~= "" and realm)
        or (GetNormalizedRealmName and GetNormalizedRealmName()) or ""
    myFull = (realm ~= "") and (name .. "-" .. realm) or name
    return myFull
end

local function IsMe(sender)
    local me = MyFullName()
    if not me then return false end
    if sender == me then return true end
    return sender == (me:match("^(.-)%-") or me)
end

-- ---------- File d'envoi (anti-déconnexion / throttle Midnight) ----------
local queue = {}
local ticker
local function Pump()
    local item = table.remove(queue, 1)
    if not item then
        if ticker then ticker:Cancel(); ticker = nil end
        return
    end
    C_ChatInfo.SendAddonMessage(PREFIX, item.msg, item.channel, item.target)
end
local function Enqueue(msg, channel, target)
    if not Sync.available then return end
    queue[#queue + 1] = { msg = msg, channel = channel, target = target }
    if not ticker then
        ticker = C_Timer.NewTicker(0.3, Pump)
    end
end

-- ---------- Émission ----------
-- H \t reply \t version \t classe \t niveau \t specID \t ilvl \t ts \t or \t id:count/id:count…
local function BuildHello(wantReply)
    local p = NS.db and NS.db.myProfile
    local _, classToken = UnitClass("player")

    -- seuls les objets déjà possédés au moins une fois sont annoncés
    local items = {}
    for _, t in ipairs(NS.TRACKED_ITEMS or {}) do
        local pid = t.ids and t.ids[1]
        local v = p and p.items and pid and p.items[pid]
        if v ~= nil then
            items[#items + 1] = ("%d:%d"):format(pid, v)
        end
    end

    return ("H\t%d\t%s\t%s\t%d\t%d\t%d\t%d\t%d\t%s\t%s"):format(
        wantReply and 1 or 0,
        NS.VERSION or "?",
        (p and p.class) or classToken or "?",
        (p and p.level) or UnitLevel("player") or 0,
        (p and p.specID) or 0,
        (p and p.ilvl) or 0,
        (p and p.ts) or 0,
        (p and p.gold) or 0,
        table.concat(items, "/"),
        (p and p.flags) or "")
end

-- Annonce sa présence au groupe/raid et à la guilde
function Sync.BroadcastHello(wantReply)
    if not Sync.available or not NS.db then return end
    local msg = BuildHello(wantReply)
    if IsInGuild() then Enqueue(msg, "GUILD") end
    if IsInRaid() then
        Enqueue(msg, "RAID")
    elseif IsInGroup() then
        Enqueue(msg, "PARTY")
    end
end

-- Annonce (hello) au groupe/raid UNIQUEMENT — pas de guilde.
-- Utilisé par le check à l'appel : quand un ready check démarre, chaque
-- client rediffuse ses flags à jour (flacon/bouffe/huile) à ses coéquipiers.
function Sync.AnnounceGroup()
    if not Sync.available or not NS.db then return end
    local msg = BuildHello(false)
    if IsInRaid() then
        Enqueue(msg, "RAID")
    elseif IsInGroup() then
        Enqueue(msg, "PARTY")
    end
end

-- Diffuse l'ilvl par emplacement (aide au butin, message « E »).
-- payload = "inv:ilvl/inv:ilvl/…" construit par NS.Loot.
function Sync.BroadcastSlots(payload)
    if not Sync.available or not NS.db or not payload or payload == "" then return end
    local msg = "E\t" .. payload
    if #msg > 255 then msg = msg:sub(1, 255) end
    if IsInGuild() then Enqueue(msg, "GUILD") end
    if IsInRaid() then
        Enqueue(msg, "RAID")
    elseif IsInGroup() then
        Enqueue(msg, "PARTY")
    end
end

-- Diffuse clé M+ + coffre convoité (aide clés/vault, message « V »).
function Sync.BroadcastVault(payload)
    if not Sync.available or not NS.db or not payload or payload == "" then return end
    local msg = "V\t" .. payload
    if #msg > 255 then msg = msg:sub(1, 255) end
    if IsInGuild() then Enqueue(msg, "GUILD") end
    if IsInRaid() then
        Enqueue(msg, "RAID")
    elseif IsInGroup() then
        Enqueue(msg, "PARTY")
    end
end

-- Demande le code SimC d'un joueur (clé = "Nom-Royaume")
function Sync.RequestProfile(peerKey)
    if not Sync.available then return end
    Enqueue("G", "WHISPER", peerKey)
end

-- Kicks : messages courts et urgents → envoi direct (sans file),
-- au groupe/raid uniquement. Depuis Midnight les casts des autres
-- joueurs sont masqués côté client : chaque addon annonce donc ses
-- propres kicks, comme le font KickSync et consorts.
local function SendGroupNow(msg)
    if not Sync.available then return end
    local channel
    if IsInRaid() then
        channel = "RAID"
    elseif IsInGroup() then
        channel = "PARTY"
    end
    if channel then
        pcall(C_ChatInfo.SendAddonMessage, PREFIX, msg, channel)
    end
end

function Sync.BroadcastKick(cd, spellID)
    SendGroupNow(("K\t%d\t%d"):format(math.floor(cd or 0), math.floor(spellID or 0)))
end

function Sync.BroadcastKickResult(res)
    SendGroupNow("KR\t" .. tostring(res))
end

-- CD de l'interruption du joueur (rotation de kick) : « IC \t <cd> »
function Sync.BroadcastKickCD(cd)
    SendGroupNow(("IC\t%d"):format(math.floor(cd or 0)))
end

-- Cooldowns de groupe : « D \t spellID \t cd \t cur \t max \t recharge »
-- Pour un sort à charges, cd = durée de recharge d'UNE charge, cur = charges
-- restantes après le cast, max > 1, recharge = temps avant la prochaine charge.
-- Pour un sort normal, max <= 1 et cd = cooldown complet.
function Sync.BroadcastCooldown(spellID, cd, cur, max, recharge)
    SendGroupNow(("D\t%d\t%d\t%d\t%d\t%d"):format(
        math.floor(spellID or 0), math.floor(cd or 0),
        math.floor(cur or 0), math.floor(max or 0), math.floor(recharge or 0)))
end

-- Usage d'une potion de combat : « P \t spellID(effet) \t cd »
function Sync.BroadcastPotion(spellID, cd)
    SendGroupNow(("P\t%d\t%d"):format(math.floor(spellID or 0), math.floor(cd or 0)))
end

-- Compteur de résurrection de combat : « R \t charges \t secondes »
function Sync.BroadcastRes(charges, timeToNext)
    SendGroupNow(("R\t%d\t%d"):format(math.floor(charges or 0), math.floor(timeToNext or 0)))
end

-- Stats M+ : résumé du joueur « MS \t kh,km,off,def,ext,bl,br,pot »
function Sync.BroadcastStats(payload)
    SendGroupNow("MS\t" .. tostring(payload or ""))
end

-- Stats M+ détail des sorts (fin de donjon) « MSD \t o:sid*n,…;d:…;e:… »
function Sync.BroadcastStatsDetail(payload)
    SendGroupNow("MSD\t" .. tostring(payload or ""))
end

-- Alerte Bloodlust (Potion + Burst) : « BLA » (sans paramètre)
function Sync.BroadcastLustAlert()
    SendGroupNow("BLA")
end

-- Liste des CD réellement connus (spé + talents) : « CK \t sid/sid/… »
function Sync.BroadcastKnownCDs(payload)
    if not Sync.available or not NS.db then return end
    local msg = "CK\t" .. (payload or "")
    if #msg > 255 then msg = msg:sub(1, 255) end
    if IsInGuild() then Enqueue(msg, "GUILD") end
    if IsInRaid() then
        Enqueue(msg, "RAID")
    elseif IsInGroup() then
        Enqueue(msg, "PARTY")
    end
end

-- Envoie son code SimC en morceaux
local function StreamProfileTo(target)
    local prof = NS.db and NS.db.myProfile
    if (not prof or not prof.simc) and NS.UpdateMyProfile then
        NS.UpdateMyProfile(false)
        prof = NS.db and NS.db.myProfile
    end
    if not prof or not prof.simc then return end

    local code, ts = prof.simc, prof.ts or 0
    local total = math.ceil(#code / CHUNK_SIZE)
    if total > MAX_CHUNKS then total = MAX_CHUNKS end
    for i = 1, total do
        local part = code:sub((i - 1) * CHUNK_SIZE + 1, i * CHUNK_SIZE)
        Enqueue(("C\t%d\t%d\t%d\t%s"):format(ts, i, total, part), "WHISPER", target)
    end
end

-- ---------- Réception ----------
local function EnsurePeer(sender)
    NS.db.peers = NS.db.peers or {}
    local p = NS.db.peers[sender]
    if not p then p = {}; NS.db.peers[sender] = p end
    return p
end

local function OnHello(sender, msg)
    -- format v0.7 (avec flags buffs), puis v0.3 (or + items), puis v0.2
    local flags
    local reply, ver, class, level, specID, ilvl, ts, gold, itemsStr, flagsStr =
        msg:match("^H\t(%d)\t([^\t]*)\t([^\t]*)\t(%d+)\t(%d+)\t(%d+)\t(%d+)\t(%d+)\t([^\t]*)\t([^\t]*)$")
    if reply then
        flags = flagsStr
    else
        reply, ver, class, level, specID, ilvl, ts, gold, itemsStr =
            msg:match("^H\t(%d)\t([^\t]*)\t([^\t]*)\t(%d+)\t(%d+)\t(%d+)\t(%d+)\t(%d+)\t([^\t]*)$")
    end
    if not reply then
        reply, ver, class, level, specID, ilvl, ts =
            msg:match("^H\t(%d)\t([^\t]*)\t([^\t]*)\t(%d+)\t(%d+)\t(%d+)\t(%d+)$")
    end
    if not reply then return end

    local p = EnsurePeer(sender)
    p.ver, p.class = ver, class
    p.level  = tonumber(level)
    p.specID = tonumber(specID)
    p.ilvl   = tonumber(ilvl)
    p.profileTs = tonumber(ts) -- version du profil annoncée par le pair
    p.online = true
    p.lastSeen = (GetServerTime and GetServerTime()) or time()

    if gold then
        p.gold = tonumber(gold)
        p.items = {}
        if itemsStr then
            for id, count in itemsStr:gmatch("(%d+):(%d+)") do
                p.items[tonumber(id)] = tonumber(count)
            end
        end
    end
    if flags and flags ~= "" then p.flags = flags end

    if reply == "1" then
        Enqueue(BuildHello(false), "WHISPER", sender)
    end
    if NS.RefreshOptimUI then NS.RefreshOptimUI("roster", sender) end
    if NS.Kicks and NS.Kicks.RefreshRoster then NS.Kicks.RefreshRoster() end
    if NS.Cooldowns and NS.Cooldowns.InvalidateRoster then NS.Cooldowns.InvalidateRoster() end
    -- Échos (rediffusion de NOS slots / vault / CD connus) : UNIQUEMENT pour
    -- un hello « reply=1 » (connexion ou arrivée dans le groupe — le pair a
    -- vraiment besoin de nos données). Avant, chaque heartbeat (60 s × chaque
    -- pair) déclenchait un écho E+V+CK chez TOUS les clients, vers guilde +
    -- groupe : un bruit de fond permanent multiplié par le nombre d'amis.
    if reply == "1" then
        if NS.Loot and NS.Loot.OnPeerHello then NS.Loot.OnPeerHello() end
        if NS.Vault and NS.Vault.OnPeerHello then NS.Vault.OnPeerHello() end
        if NS.Cooldowns and NS.Cooldowns.OnPeerHello then NS.Cooldowns.OnPeerHello() end
    end
end

local incoming = {}
local function OnChunk(sender, msg)
    local ts, i, n, data = msg:match("^C\t(%d+)\t(%d+)\t(%d+)\t(.*)$")
    if not ts then return end
    i, n = tonumber(i), tonumber(n)
    if not i or not n or n < 1 or n > MAX_CHUNKS or i < 1 or i > n then return end

    local key = sender .. "#" .. ts
    local buf = incoming[key]
    if not buf then
        buf = { parts = {}, count = 0, n = n }
        incoming[key] = buf
    end
    buf.at = (GetServerTime and GetServerTime()) or time() -- pour la purge
    if not buf.parts[i] then
        buf.parts[i] = data
        buf.count = buf.count + 1
    end

    if buf.count >= buf.n then
        incoming[key] = nil
        local p = EnsurePeer(sender)
        p.simc = table.concat(buf.parts, "", 1, buf.n)
        p.ts = tonumber(ts)
        p.online = true
        if NS.RefreshOptimUI then NS.RefreshOptimUI("peerdata", sender) end
    end
end

local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_ADDON")
f:SetScript("OnEvent", function(_, _, prefix, msg, _, sender)
    if prefix ~= PREFIX or type(msg) ~= "string" or not sender then return end
    if not NS.db or IsMe(sender) then return end

    if msg:sub(1, 2) == "KR" then
        local res = msg:match("^KR\t(%a+)$")
        if res and NS.Kicks and NS.Kicks.OnPeerKickResult then
            NS.Kicks.OnPeerKickResult(sender, res)
        end
        return
    end
    -- « CK » (CD connus) avant le « C » de chunk, car même 1re lettre
    if msg:sub(1, 2) == "CK" then
        if NS.Cooldowns and NS.Cooldowns.OnPeerKnownCDs then
            NS.Cooldowns.OnPeerKnownCDs(sender, msg)
        end
        return
    end
    -- « MS » (stats M+) et « BLA » (alerte Bloodlust) — préfixes multi-lettres
    if msg:sub(1, 3) == "MSD" then
        if NS.Stats and NS.Stats.OnPeerStatsDetail then
            NS.Stats.OnPeerStatsDetail(sender, msg:match("^MSD\t(.*)$") or "")
        end
        return
    end
    if msg:sub(1, 2) == "MS" then
        if NS.Stats and NS.Stats.OnPeerStats then
            NS.Stats.OnPeerStats(sender, msg:match("^MS\t(.*)$") or "")
        end
        return
    end
    if msg == "BLA" then
        if NS.Stats and NS.Stats.OnLustAlert then NS.Stats.OnLustAlert(sender) end
        return
    end
    if msg:sub(1, 2) == "IC" then
        local cd = msg:match("^IC\t(%d+)$")
        if cd and NS.Kicks and NS.Kicks.OnPeerKickCD then
            NS.Kicks.OnPeerKickCD(sender, tonumber(cd))
        end
        return
    end

    local t = msg:sub(1, 1)
    if t == "H" then
        OnHello(sender, msg)
    elseif t == "E" then
        if NS.Loot and NS.Loot.OnPeerSlots then NS.Loot.OnPeerSlots(sender, msg) end
    elseif t == "V" then
        if NS.Vault and NS.Vault.OnPeerVault then NS.Vault.OnPeerVault(sender, msg) end
    elseif t == "G" then
        StreamProfileTo(sender)
    elseif t == "C" then
        OnChunk(sender, msg)
    elseif t == "K" then
        local cd, sid = msg:match("^K\t(%d+)\t(%d+)$")
        if not cd then cd = msg:match("^K\t(%d+)$") end
        if cd and NS.Kicks and NS.Kicks.OnPeerKick then
            NS.Kicks.OnPeerKick(sender, tonumber(cd), sid and tonumber(sid))
        end
    elseif t == "D" then
        local sid, cd, cur, mx, rc = msg:match("^D\t(%d+)\t(%d+)\t(%d+)\t(%d+)\t(%d+)$")
        if not sid then sid, cd = msg:match("^D\t(%d+)\t(%d+)$") end
        if sid and NS.Cooldowns and NS.Cooldowns.OnPeerCooldown then
            NS.Cooldowns.OnPeerCooldown(sender, tonumber(sid), tonumber(cd),
                cur and tonumber(cur), mx and tonumber(mx), rc and tonumber(rc))
        end
    elseif t == "P" then
        local sid, cd = msg:match("^P\t(%d+)\t(%d+)$")
        if sid and NS.Cooldowns and NS.Cooldowns.OnPeerPotion then
            NS.Cooldowns.OnPeerPotion(sender, tonumber(sid), tonumber(cd))
        end
    elseif t == "R" then
        local ch, nx = msg:match("^R\t(%d+)\t(%d+)$")
        if ch and NS.Cooldowns and NS.Cooldowns.OnPeerRes then
            NS.Cooldowns.OnPeerRes(sender, tonumber(ch), tonumber(nx))
        end
    end
end)

-- ============================================================
-- Présence en ligne : battement de cœur + détection hors ligne
-- Un addon ne reçoit AUCUN signal quand un autre joueur se déconnecte
-- (pas de journal de combat, les messages d'addon cessent simplement).
-- On procède donc par « heartbeat » : chaque client rediffuse un hello
-- toutes les HEARTBEAT s ; si on n'a plus de nouvelles d'un pair depuis
-- OFFLINE_AFTER s, on le repasse hors ligne. Avant, `online` n'était
-- JAMAIS remis à false → un perso vu une fois restait vert à vie.
-- ============================================================
local HEARTBEAT     = 60   -- s : fréquence de rediffusion du hello
local OFFLINE_AFTER = 150  -- s : sans nouvelles → hors ligne (2,5 min)

-- Marque tous les pairs comme hors ligne (à ta connexion : on ne sait
-- pas encore qui est là ; leurs hellos re-verdiront les vrais en ligne).
function Sync.MarkAllOffline()
    if not (NS.db and NS.db.peers) then return end
    for _, p in pairs(NS.db.peers) do p.online = false end
    if NS.RefreshOptimUI then NS.RefreshOptimUI("roster") end
end

-- Balayage périodique : repasse hors ligne les pairs trop silencieux.
local function SweepOffline()
    if not (NS.db and NS.db.peers) then return end
    local now = (GetServerTime and GetServerTime()) or time()
    local changed = false
    for _, p in pairs(NS.db.peers) do
        if p.online and (now - (p.lastSeen or 0)) > OFFLINE_AFTER then
            p.online = false
            changed = true
        end
    end
    if changed then
        if NS.RefreshOptimUI then NS.RefreshOptimUI("roster") end
        if NS.Kicks and NS.Kicks.RefreshRoster then NS.Kicks.RefreshRoster() end
    end
    -- transferts SimC interrompus (pair déconnecté en plein envoi) : on
    -- jette les morceaux incomplets après 3 min (sinon gardés à vie)
    for key, buf in pairs(incoming) do
        if (now - (buf.at or 0)) > 180 then incoming[key] = nil end
    end
end

-- Démarre les tickers (heartbeat + balayage). Idempotent.
local hbTicker, sweepTicker
function Sync.StartHeartbeat()
    if not Sync.available then return end
    if not hbTicker then
        hbTicker = C_Timer.NewTicker(HEARTBEAT, function()
            Sync.BroadcastHello(false)
        end)
    end
    if not sweepTicker then
        sweepTicker = C_Timer.NewTicker(20, SweepOffline)
    end
end
