local addonName, NS = ...
local GR = _G["GRS_MainFrame"]

-- =========================================================================
-- COMMAND CORE: CONSTANTS
-- =========================================================================
local COMM_PREFIX = "GRS_MAP_DAT" -- Data Channel
local HS_PREFIX   = "GRS_MAP_HS"  -- Heartbeat/Control Channel
local MAX_PACKET_LEN = 240        -- Safe packet size for Chat
local TICK_RATE = 0.4             -- Throttle Speed

-- Header/Separator
local STRAT_DELIMITER = "||STR||" 

-- =========================================================================
-- SESSION STATE & QUEUES
-- =========================================================================
GR.Session = {
    isLive = false,
    sequenceID = 0,        
    lastReceivedID = 0,    
    
    queues = {
        lane1 = {}, -- Alert (Stop/Start)
        lane2 = {}, -- Normal (Sync)
        lane3 = {}  -- Bulk (Mouse Drag)
    },
    
    reassembly = {},       
    pushTimer = nil        
}

-- =========================================================================
-- 1. UTILS: PLAIN TEXT SERIALIZER (ROBUST)
-- =========================================================================
-- FORMAT:  ID : X : Y : ScaleX : ScaleY : Rot ;
-- EXAMPLE: 100:0.5:0.5:1.0:1.0:0.0;
-- LABEL:   T:ID:Text:X:Y:R:G:B:Size:Bold;

local function Round(num, numDecimalPlaces)
    local mult = 10^(numDecimalPlaces or 0)
    return math.floor(num * mult + 0.5) / mult
end

function GR:SerializeData(canvasData)
    local stream = ""
    
    for _, d in ipairs(canvasData) do
        if d.isLabel then
            -- LABEL FORMAT: T:ID:Text:X:Y:R:G:B:Size:Bold
            local safeText = (d.text or "Label"):gsub("[:;|]", "")
            
            stream = stream .. string.format("T:%d:%s:%.3f:%.3f:%.2f:%.2f:%.2f:%d:%d;",
                d.id or 200, -- Default ID for Text
                safeText, 
                d.normX or 0.5, d.normY or 0.5,
                d.r or 1, d.g or 1, d.b or 1,
                d.fontSize or 14,
                d.isBold and 1 or 0
            )
        else
            -- ICON FORMAT: ID:X:Y:SX:SY:R
            local rot = d.rotation or 0
            rot = Round(rot, 2)
            
            -- [FIX] Serializing ScaleY is mandatory for Shapes (Box/Arrow)
            stream = stream .. string.format("%d:%.3f:%.3f:%.2f:%.2f:%.2f;",
                d.id or 1, 
                d.normX or 0.5,
                d.normY or 0.5,
                d.scaleX or 1.0,
                d.scaleY or (d.scale or 1.0), -- Fallback for legacy uniform scale
                rot
            )
        end
    end
    
    return stream
end

function GR:DeserializeData(stream)
    local t = {}
    
    -- Iterate over each object (separated by ;)
    for chunk in string.gmatch(stream, "([^;]+)") do
        
        -- Check if it's a Label (Starts with "T:")
        if string.sub(chunk, 1, 2) == "T:" then
            -- Format: T:ID:Text:X:Y:R:G:B:Size:Bold
            local parts = { strsplit(":", chunk) }
            
            if #parts >= 10 then
                local id = tonumber(parts[2])
                table.insert(t, {
                    id = id, -- [FIX] ID is now explicitly restored
                    type = "text", path = "A", name = "Text", isLabel = true,
                    text = parts[3],
                    normX = tonumber(parts[4]), normY = tonumber(parts[5]),
                    r = tonumber(parts[6]), g = tonumber(parts[7]), b = tonumber(parts[8]),
                    fontSize = tonumber(parts[9]),
                    isBold = (parts[10] == "1"),
                    isValid = true, scale = 1.0
                })
            end
        else
            -- Format: ID:X:Y:SX:SY:R
            local parts = { strsplit(":", chunk) }
            
            if #parts >= 6 then
                local id = tonumber(parts[1])
                local reg = GR.ToolRegistry and GR.ToolRegistry[id]
                
                -- [FIX] Fallback definitions for utility icons (201-203) when registry unavailable
                local UTILITY_FALLBACK = {
                    [201] = { type = "texture", path = "Interface\\AddOns\\GuildRecruiterManagement\\RaidRoster\\Resources\\Icons\\Box.tga", name = "Box", canStretch = true },
                    [202] = { type = "texture", path = "Interface\\AddOns\\GuildRecruiterManagement\\RaidRoster\\Resources\\Icons\\Circle.tga", name = "ShapeCircle", canStretch = true },
                    [203] = { type = "texture", path = "Interface\\AddOns\\GuildRecruiterManagement\\RaidRoster\\Resources\\Icons\\Arrow.tga", name = "Arrow", canStretch = true },
                }
                
                local source = reg or UTILITY_FALLBACK[id]
                
                if source then
                    table.insert(t, {
                        id = id,
                        type = source.type, path = source.path, name = source.name,
                        canStretch = source.canStretch,
                        normX = tonumber(parts[2]),
                        normY = tonumber(parts[3]),
                        scaleX = tonumber(parts[4]), 
                        scaleY = tonumber(parts[5]), -- [FIX] ScaleY explicitly restored
                        scale = tonumber(parts[4]), -- For uniform fallback logic
                        rotation = tonumber(parts[6]),
                        isValid = true, isLabel = false
                    })
                end
            end
        end
    end
    
    return t
end

-- =========================================================================
-- 2. THE TRAFFIC CONTROLLER (PRIORITY QUEUE)
-- =========================================================================
local CommTicker

local function FlushQueue()
    local channel = IsInRaid() and "RAID" or (IsInGroup() and "PARTY")
    if not channel then GR.Session.queues = {lane1={}, lane2={}, lane3={}} return end

    local packet = nil
    
    -- Priority 1: ALERT
    if #GR.Session.queues.lane1 > 0 then
        packet = table.remove(GR.Session.queues.lane1, 1)
        
    -- Priority 2: NORMAL
    elseif #GR.Session.queues.lane2 > 0 then
        packet = table.remove(GR.Session.queues.lane2, 1)
        
    -- Priority 3: BULK
    elseif #GR.Session.queues.lane3 > 0 then
        if #GR.Session.queues.lane3 > 3 then
            local newest = table.remove(GR.Session.queues.lane3)
            GR.Session.queues.lane3 = { newest }
            packet = newest
        else
            packet = table.remove(GR.Session.queues.lane3, 1)
        end
    end
    
    if packet then
        if C_ChatInfo and C_ChatInfo.SendAddonMessage then
            C_ChatInfo.SendAddonMessage(COMM_PREFIX, packet, channel)
        elseif SendAddonMessage then
            SendAddonMessage(COMM_PREFIX, packet, channel)
        end
    end
end

function GR:BroadcastData(cmd, lane, payload)
    if not GR:CanEditMap() then return end
    
    GR.Session.sequenceID = GR.Session.sequenceID + 1
    local seq = GR.Session.sequenceID
    local raid = DB_Rmap_Tools.lastRaid or "NIL"
    local boss = DB_Rmap_Tools.lastBoss or "NIL"
    
    local fullMsg = string.format("%d~%s~%s~%s~%s", seq, cmd, raid, boss, payload)
    
    local len = #fullMsg
    local parts = math.ceil(len / MAX_PACKET_LEN)
    
    for i=1, parts do
        local chunk = fullMsg:sub( (i-1)*MAX_PACKET_LEN + 1, i*MAX_PACKET_LEN )
        local packet = string.format("%d\t%d\t%d\t%s", seq, i, parts, chunk)
        
        if lane == "ALERT" then table.insert(GR.Session.queues.lane1, packet)
        elseif lane == "BULK" then table.insert(GR.Session.queues.lane3, packet)
        else table.insert(GR.Session.queues.lane2, packet) end
    end
    
    if not CommTicker then
        if C_Timer and C_Timer.NewTicker then
            CommTicker = C_Timer.NewTicker(TICK_RATE, FlushQueue)
        else
            local fqFrame = CreateFrame("Frame"); local fqEl = 0
            CommTicker = { Cancel = function() fqFrame:SetScript("OnUpdate", nil) end }
            fqFrame:SetScript("OnUpdate", function(s, dt) fqEl = fqEl + dt; if fqEl >= TICK_RATE then fqEl = 0; FlushQueue() end end)
        end
    end
end

-- =========================================================================
-- 3. INCOMING HANDLER
-- =========================================================================
function GR:InitMapComms()
    if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
        C_ChatInfo.RegisterAddonMessagePrefix(COMM_PREFIX)
        C_ChatInfo.RegisterAddonMessagePrefix(HS_PREFIX)
    elseif RegisterAddonMessagePrefix then
        RegisterAddonMessagePrefix(COMM_PREFIX)
        RegisterAddonMessagePrefix(HS_PREFIX)
    end
    
    local f = CreateFrame("Frame")
    f:RegisterEvent("CHAT_MSG_ADDON")
    f:SetScript("OnEvent", function(_, _, prefix, text, channel, sender)
        if Ambiguate(sender, "none") == UnitName("player") then return end
        
        if prefix == COMM_PREFIX then
            local seqStr, partStr, totalStr, chunk = strsplit("\t", text, 4)
            local seq, part, total = tonumber(seqStr), tonumber(partStr), tonumber(totalStr)
            
            if not seq or not chunk then return end
            
            if GR.Session.lastReceivedID > 0 and seq > (GR.Session.lastReceivedID + 1) then
                 if (seq - GR.Session.lastReceivedID) > 5 then
                     GR.Session.lastReceivedID = seq - 1
                 else
                     return 
                 end
            end
            
            if not GR.Session.reassembly[seq] then GR.Session.reassembly[seq] = { parts = {}, count = 0 } end
            GR.Session.reassembly[seq].parts[part] = chunk
            GR.Session.reassembly[seq].count = GR.Session.reassembly[seq].count + 1
            
            if GR.Session.reassembly[seq].count == total then
                local fullMsg = table.concat(GR.Session.reassembly[seq].parts)
                GR.Session.reassembly[seq] = nil 
                GR.Session.lastReceivedID = seq 
                GR:ProcessMapMessage(fullMsg)
            end
        end
    end)
end

function GR:ProcessMapMessage(fullMsg)
    local parts = { strsplit("~", fullMsg, 5) }
    local seq, cmd, raid, boss, payload = parts[1], parts[2], parts[3], parts[4], parts[5]
    
    if cmd == "LIVE_STOP" then
        GR.Session.isLive = false  -- [FIX] Clear isLive on receiving clients
        GR:ShowMapToast("Live Stream Ended", true)
        if GR.MapWindow and GR.MapWindow.liveSlot then GR.MapWindow.liveSlot.state = "OFF" end
        -- [FIX] Reset the Live button text when session ends
        if GR.MapWindow and GR.MapWindow.btnLive then GR.MapWindow.btnLive:SetText("Live") end
        if GR.selectedSaveSlot == 11 then GR.selectedSaveSlot = nil end
        if GR.ReflowSaveSlots then GR:ReflowSaveSlots() end
        return
    end
    
    -- [STRAT_SYNC] Immediate strategist permission update
    if cmd == "STRAT_SYNC" then
        GR:UnpackStrategists(payload)
        GR:ShowMapToast("Permissions Updated", false)
        if GR.ReflowSaveSlots then GR:ReflowSaveSlots() end
        return
    end

    if cmd == "LIVE_START" or cmd == "PUSH" then
        -- [FIX] Set isLive on receiving clients so strategists can broadcast
        -- [FIX] Always set isLive (member may join after LIVE_START)
        GR.Session.isLive = true
        
        GR:RR_OpenMapWindow()
        
        -- [FIX] Update the Live button UI to show "STOP" so members see they are in live mode
        if GR.MapWindow and GR.MapWindow.btnLive then
            GR.MapWindow.btnLive:SetText("STOP")
        end
        DB_Rmap_Tools.lastRaid = raid
        DB_Rmap_Tools.lastBoss = boss
        
        local mapData = GR.RAID_MAP_DATA and GR.RAID_MAP_DATA[raid]
        if mapData then
             for _, b in ipairs(mapData) do
                 if b.name == boss then
                     if GR.mapTex then GR.mapTex:SetTexture("Interface\\AddOns\\GuildRecruiterManagement\\RaidRoster\\Resources\\Midnight\\" .. b.file) end
                     break
                 end
             end
        end
        if GR.mapTex then GR.mapTex:Show() end
        local overlay = GR.mapOverlay or (GR.MapWindow and GR.MapWindow.mapOverlay)
        if overlay then overlay:Show() end
        
        local actualMapDataStr = payload
        local stratDataStr = nil
        local splitStart, splitEnd = string.find(payload, STRAT_DELIMITER, 1, true)
        if splitStart then
            actualMapDataStr = string.sub(payload, 1, splitStart - 1)
            stratDataStr = string.sub(payload, splitEnd + 1)
        end
        
        if stratDataStr then GR:UnpackStrategists(stratDataStr) end
        
        if not GR.RR then GR.RR = {} end
        GR.RR.LiveBuffer = { raid = raid, boss = boss, data = GR:DeserializeData(actualMapDataStr) }
        
        if GR.selectedSaveSlot == 11 or cmd == "LIVE_START" then
            GR:RenderLiveBuffer()
            if GR.MapWindow.liveSlot then GR.MapWindow.liveSlot.state = "ON" end
            GR.selectedSaveSlot = 11
            GR:ReflowSaveSlots()
        else
            GR:ShowMapToast("Update Received", false)
        end
    end
end

-- =========================================================================
-- 5. API HOOKS & UTILITIES
-- =========================================================================
function GR:PackStrategists()
    if not DB_Rmap_Tools or not DB_Rmap_Tools.settings.strategists then return "" end
    local s = {}
    for k, v in pairs(DB_Rmap_Tools.settings.strategists) do
        if type(k) == "number" and type(v) == "string" then table.insert(s, v) end
    end
    return table.concat(s, "^")
end

-- [STRAT_SYNC] Broadcast strategist list immediately to all members
function GR:BroadcastStrategists()
    if not UnitIsGroupLeader("player") then return end
    local stratStr = GR:PackStrategists()
    GR:BroadcastData("STRAT_SYNC", "ALERT", stratStr)
end

function GR:UnpackStrategists(str)
    if not DB_Rmap_Tools then return end
    DB_Rmap_Tools.settings.strategists = {}
    if str and str ~= "" then
        for name in string.gmatch(str, "([^^]+)") do table.insert(DB_Rmap_Tools.settings.strategists, name) end
    end
end

function GR:TriggerLiveUpdate()
    -- [FIX] Don't re-broadcast while receiving remote data (prevents ping-pong)
    if GR.Session.isReceiving then return end
    if not GR.Session.isLive or not GR:CanEditMap() then return end
    if GR.Session.pushTimer then GR.Session.pushTimer:Cancel() end
    if C_Timer and C_Timer.NewTimer then
        GR.Session.pushTimer = C_Timer.NewTimer(0.2, function() 
            GR:BroadcastMap("PUSH")
            GR.Session.pushTimer = nil
        end)
    elseif NS and NS.NewTimer then
        GR.Session.pushTimer = NS.NewTimer(0.2, function() 
            GR:BroadcastMap("PUSH")
            GR.Session.pushTimer = nil
        end)
    else
        local ptFrame = CreateFrame("Frame"); local ptEl = 0
        GR.Session.pushTimer = { Cancel = function() ptFrame:SetScript("OnUpdate", nil) end }
        ptFrame:SetScript("OnUpdate", function(s, dt) ptEl = ptEl + dt; if ptEl >= 0.2 then s:SetScript("OnUpdate", nil); GR:BroadcastMap("PUSH"); GR.Session.pushTimer = nil end end)
    end
end

function GR:BroadcastMap(cmd)
    if not GR:CanEditMap() then return end
    
    local mapStr = GR:SerializeData(DB_Rmap_Tools.canvasData)
    local stratStr = GR:PackStrategists()
    local payload = mapStr .. STRAT_DELIMITER .. stratStr
    
    local lane = "NORMAL"
    if cmd == "LIVE_STOP" or cmd == "LIVE_START" then lane = "ALERT" end
    if cmd == "PUSH" then lane = "BULK" end
    
    GR:BroadcastData(cmd, lane, payload)
end

function GR:CanEditMap()
    if not IsInGroup() then return true end
    if UnitIsGroupLeader("player") or UnitIsGroupAssistant("player") then return true end
    -- [FIX] Compare using both name formats (with and without realm)
    local nameShort = UnitName("player")
    local nameFull = GetUnitName("player", true)
    local strats = DB_Rmap_Tools.settings.strategists
    if strats then
        for k, v in pairs(strats) do 
            if v == nameShort or v == nameFull then return true end 
        end
    end
    return false
end

function GR:RenderLiveBuffer()
    -- [FIX] Set flag to prevent TriggerLiveUpdate during remote data render
    GR.Session.isReceiving = true
    GR:WipeCanvasData(true)
    if GR.RR.LiveBuffer and GR.RR.LiveBuffer.data then
        DB_Rmap_Tools.canvasData = CopyTable(GR.RR.LiveBuffer.data)
        GR:RefreshCanvasIcons()
        GR:ShowMapToast("Live Feed", false)
    end
    GR.Session.isReceiving = false
end

GR:InitMapComms()