-- =====================================================================
-- LiveStrat — Sync.lua
-- Synchronisation en direct par canal addon (RAID/PARTY).
-- Principe : on ne transporte que des OPÉRATIONS vectorielles compactes,
-- signées par leur émetteur (le canal addon donne le nom, infalsifiable).
-- Un client n'AFFICHE que les opérations d'un émetteur autorisé (chef ou
-- liste de droits diffusée par le chef) — la sécurité est côté récepteur.
--
-- Protocole (messages "LIVESTRAT", champs séparés par "\1") :
--   OP  \1 <opSérialisée>            — une opération de dessin (Board)
--   DEL \1 <opId>                    — suppression d'un élément
--   CLR                              — tout effacer
--   ACL \1 nom1,nom2,…               — liste des autorisés (chef seul)
--   REQ                              — « envoyez-moi le tableau »
--   FUL \1 <i>/<n> \1 <paquetOps>    — état complet, morcelé
-- =====================================================================
local ADDON, ns = ...
local L = ns.L

local Sync = {}
ns.modules.Sync = Sync

local PREFIX = "LIVESTRAT"
local SEP = "\1"

-- File d'envoi throttlée (~8 messages/s : sous la limite client, et un
-- trait à main levée tient en 2-3 messages grâce au vectoriel).
local queue, sending = {}, false
local function pump()
    if #queue == 0 then sending = false return end
    sending = true
    local msg = table.remove(queue, 1)
    local channel = IsInRaid() and "RAID" or (IsInGroup() and "PARTY" or nil)
    if channel then
        C_ChatInfo.SendAddonMessage(PREFIX, msg, channel)
    end
    C_Timer.After(0.13, pump)
end

local function send(msg)
    if not IsInGroup() then return end   -- solo : tout reste local
    queue[#queue + 1] = msg
    if not sending then pump() end
end

function Sync:Initialize()
    C_ChatInfo.RegisterAddonMessagePrefix(PREFIX)
    ns.Core:RegisterEvent("CHAT_MSG_ADDON")
    ns.Core:HookScript("OnEvent", function(_, event, prefix, text, _, sender)
        if event == "CHAT_MSG_ADDON" and prefix == PREFIX then
            Sync:OnMessage(text, sender)
        end
    end)
end

-- ---------------------------------------------------------------------
-- Émission
-- ---------------------------------------------------------------------
function Sync:SendOp(serialized) send("OP" .. SEP .. serialized) end
function Sync:SendDelete(opId) send("DEL" .. SEP .. opId) end
function Sync:SendClear() send("CLR") end

function Sync:SendACL()
    local names = {}
    for n in pairs(ns.acl) do names[#names + 1] = n end
    send("ACL" .. SEP .. table.concat(names, ","))
end

function Sync:RequestBoard() send("REQ") end

-- ---------------------------------------------------------------------
-- RECENSEMENT : qui a l'addon ? « WHO » → chaque client répond « HI »
-- (délai aléatoire 0-1,5 s : pas de rafale à 20 réponses simultanées).
-- ns.present[nom] = version ; l'UI des droits affiche vert/gris.
-- ---------------------------------------------------------------------
ns.present = {}
ns.viewing = {}   -- [nom] = true si son TABLEAU est OUVERT (le « live »)

function Sync:Census()
    wipe(ns.present)
    wipe(ns.viewing)
    ns.present[ns.me] = true
    local Board = ns.modules.Board
    ns.viewing[ns.me] = Board and Board:IsShown() or nil
    send("WHO")
end

function Sync:_answerCensus()
    C_Timer.After(math.random() * 1.5, function()
        local Board = ns.modules.Board
        send("HI" .. SEP .. ((Board and Board:IsShown()) and "1" or "0"))
    end)
end

-- Annonce d'ouverture/fermeture du tableau (la colonne « en direct »).
function Sync:SendViewing(on)
    send("VIS" .. SEP .. (on and "1" or "0"))
end

-- Invitation à OUVRIR le tableau (popup chez les porteurs de l'addon).
function Sync:SendOpenInvite() send("OPN") end

-- CONTEXTE de carte : le chef impose instance/boss/étage — sans ça, un
-- invité voyait les dessins SANS la carte (vécu).
function Sync:SendMapContext()
    local B = ns.modules.Board
    if not B then return end
    send(("MAP%s%d%s%d%s%d%s%d"):format(
        SEP, B._mapID or 0,
        SEP, (B._instance and B._instance.id) or 0,
        SEP, (B._boss and B._boss.encID) or 0,
        SEP, B._page or 1))
end

-- VUE du chef (zoom/panoramique) suivie par le groupe.
function Sync:SendView(zoom, ox, oy)
    send(("VW%s%d%s%d%s%d"):format(SEP,
        math.floor(zoom * 100 + 0.5), SEP,
        math.floor(ox + 0.5), SEP, math.floor(oy + 0.5)))
end

-- Annonce de l'EMPREINTE du tableau (« qui est à jour ? ») — débouncée :
-- un trait à main levée déclenche des dizaines d'ApplyOp.
ns.boardState = {}   -- [nom] = empreinte annoncée
function Sync:AnnounceState()
    if self._statePending then return end
    self._statePending = true
    C_Timer.After(1.5, function()
        self._statePending = nil
        local Board = ns.modules.Board
        if Board then send("ST" .. SEP .. Board:Checksum()) end
    end)
end

-- État complet, morcelé sous la limite de 250 octets par message.
function Sync:SendFullBoard()
    local Board = ns.modules.Board
    if not Board then return end
    local packets, cur = {}, ""
    for _, op in ipairs(ns.board.ops) do
        local s = Board:SerializeOp(op)
        if s then
            if #cur + #s + 1 > 220 then packets[#packets + 1] = cur; cur = "" end
            cur = (cur == "") and s or (cur .. "\2" .. s)
        end
    end
    if cur ~= "" then packets[#packets + 1] = cur end
    if #packets == 0 then packets[1] = "" end
    for i, p in ipairs(packets) do
        send("FUL" .. SEP .. i .. "/" .. #packets .. SEP .. p)
    end
end

-- ---------------------------------------------------------------------
-- Réception
-- ---------------------------------------------------------------------
function Sync:OnMessage(text, sender)
    local Board = ns.modules.Board
    if not Board then return end
    if sender == ns.me then return end   -- écho de nos propres messages
    local kind, rest = text:match("^(%u+)" .. SEP .. "?(.*)$")
    if not kind then return end
    ns.Log("reçu", kind, "de", sender, "(", #text, "octets )")

    if kind == "ACL" then
        -- Seul le CHEF fait autorité sur les droits.
        if sender ~= ns.GroupLeaderName() then return end
        wipe(ns.acl)
        for n in rest:gmatch("[^,]+") do ns.acl[n] = true end
        if Board:IsShown() then Board:UpdatePermissionUI() end
        return
    end

    if kind == "WHO" then
        ns.present[sender] = true      -- qui demande a forcément l'addon
        self:_answerCensus()
        return
    end
    if kind == "HI" then
        ns.present[sender] = true
        ns.viewing[sender] = (rest == "1") or nil
        if Board:IsShown() then Board:UpdatePermissionUI() end
        return
    end
    if kind == "VIS" then
        ns.present[sender] = true
        ns.viewing[sender] = (rest == "1") or nil
        if Board:IsShown() then Board:UpdatePermissionUI() end
        return
    end
    if kind == "ST" then
        ns.present[sender] = true
        ns.boardState[sender] = rest
        if Board:IsShown() then Board:UpdatePermissionUI() end
        -- AUTO-RATTRAPAGE (vécu : clics rapides = client à la traîne,
        -- pour toujours) : si le plan du CHEF diffère encore du mien
        -- après 2,5 s de calme, je redemande tout — au plus 1×/8 s.
        if sender == ns.GroupLeaderName()
            and not UnitIsGroupLeader("player")
            and rest ~= Board:Checksum()
            and not self._healPending then
            self._healPending = true
            C_Timer.After(2.5, function()
                self._healPending = nil
                local leader = ns.GroupLeaderName()
                local lst = leader and ns.boardState[leader]
                if lst and lst ~= Board:Checksum()
                    and (not self._lastHeal or GetTime() - self._lastHeal > 8) then
                    self._lastHeal = GetTime()
                    ns.Log("plan du chef différent → resynchronisation auto")
                    self:RequestBoard()
                end
            end)
        end
        return
    end
    if kind == "REQ" then
        -- Le CHEF répond (un seul répondeur → pas de tempête) — avec le
        -- CONTEXTE de carte d'abord, sinon les dessins sans la carte.
        if UnitIsGroupLeader("player") then
            self:SendACL()
            self:SendMapContext()
            self:SendFullBoard()
            if ns.modules.Board then ns.modules.Board:_broadcastView() end
            self:AnnounceState()
        end
        return
    end
    if kind == "OPN" then
        if ns.CanDraw(sender) then
            local B2 = ns.modules.Board
            if not (B2 and B2:IsShown()) then
                ns.ShowOpenInvitePopup(sender)
            end
        end
        return
    end
    if kind == "MAP" then
        if ns.CanDraw(sender) then
            local m, inst, enc, pg = rest:match("^(%d+)" .. SEP .. "(%d+)"
                .. SEP .. "(%d+)" .. SEP .. "(%d+)$")
            if not m then   -- ancien format sans page
                m, inst, enc = rest:match("^(%d+)" .. SEP .. "(%d+)" .. SEP .. "(%d+)$")
            end
            if m then
                Board:ApplyMapContext(tonumber(m), tonumber(inst),
                    tonumber(enc), tonumber(pg) or 1)
            end
        end
        return
    end
    if kind == "VW" then
        -- Seule la vue du CHEF fait autorité (pas de bataille de zooms).
        if sender == ns.GroupLeaderName() then
            local z, ox, oy = rest:match("^(%d+)" .. SEP .. "(-?%d+)" .. SEP .. "(-?%d+)$")
            if z then
                Board:ApplyRemoteView(tonumber(z) / 100, tonumber(ox), tonumber(oy))
            end
        end
        return
    end

    -- Toute opération de dessin exige un émetteur AUTORISÉ.
    if not ns.CanDraw(sender) then
        ns.Log("op ignorée (non autorisé) :", sender)
        return
    end

    if kind == "OP" then
        local op = Board:DeserializeOp(rest, sender)
        if op then Board:ApplyOp(op) end
    elseif kind == "DEL" then
        Board:RemoveOp(rest)
    elseif kind == "CLR" then
        Board:ClearAll(true)
        print(("LiveStrat : " .. L["Tableau effacé par %s."]):format(sender))
    elseif kind == "FUL" then
        local i, n, packet = rest:match("^(%d+)/(%d+)" .. SEP .. "(.*)$")
        if not i then return end
        if tonumber(i) == 1 then Board:ClearAll(true, true) end
        local count = 0
        for s in packet:gmatch("[^\2]+") do
            local op = Board:DeserializeOp(s, sender)
            if op then Board:ApplyOp(op); count = count + 1 end
        end
        if tonumber(i) == tonumber(n) then
            print(("LiveStrat : " .. L["Tableau reçu de %s (%d éléments)."])
                :format(sender, #ns.board.ops))
        end
    end
end
