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

-- =========================================================================
-- 1. CONSTANTS & CONFIGURATION
-- =========================================================================
local ROSTER_BG_PATH = "Interface\\AddOns\\GuildRecruiterManagement\\Media\\RaidRosterBackground.tga"
local INSPECT_BG_PATH = "Interface\\AddOns\\GuildRecruiterManagement\\Media\\Inspection.tga"
local COMM_PREFIX = "GRS_RR"
local SYNC_INTERVAL = 900 
local MAX_ROWS_PER_COLUMN = 10
local COMM_DELIM = "^"

-- Data chunking for AddOn Comm (Avoid disconnects)
local MAX_COMM_CHUNK = 200

local ROLE_TEX = {
    ["TANK"] = "Interface\\Icons\\Ul_70_tank_icon",
    ["HEALER"] = "Interface\\Icons\\Ul_70_healer_icon",
    ["DPS"] = "Interface\\Icons\\Ul_70_dps_icon",
    ["BENCH"] = "Interface\\Icons\\Spell_ChargePositive",
    ["ABSENT"] = "Interface\\Icons\\Spell_ChargeNegative"
}

local CLASS_ICON_TCOORDS = {
    ["WARRIOR"] = {0, 0.25, 0, 0.25}, ["MAGE"] = {0.25, 0.5, 0, 0.25},
    ["ROGUE"] = {0.5, 0.75, 0, 0.25}, ["DRUID"] = {0.75, 1, 0, 0.25},
    ["HUNTER"] = {0, 0.25, 0.25, 0.5}, ["SHAMAN"] = {0.25, 0.5, 0.25, 0.5},
    ["PRIEST"] = {0.5, 0.75, 0.25, 0.5}, ["WARLOCK"] = {0.75, 1, 0.25, 0.5},
    ["PALADIN"] = {0, 0.25, 0.5, 0.75}, ["DEATHKNIGHT"] = {0.25, 0.5, 0.5, 0.75},
    ["MONK"] = {0.5, 0.75, 0.5, 0.75}, ["DEMONHUNTER"] = {0.75, 1, 0.5, 0.75},
    ["EVOKER"] = {0, 0.25, 0.75, 1},
}

-- DB Initialization (Pre-Load)
if not GRS_RaidRosterDB then GRS_RaidRosterDB = {} end
if not GRS_RaidRosterDB.events then GRS_RaidRosterDB.events = {} end

-- Module State
GR.RR = {
    selectedEvent = nil,
    viewMode = "MEMBER", 
    cache = {},
    lastSync = 0,
    multiSelect = {},
    inspectSelection = nil,
    inviteSelection = {} -- [Name] = {name=Name, role=Role, ...} (Stores full data object now for ease)
}

-- =========================================================================
-- 2. LOCAL UI HELPERS
-- =========================================================================

local function CreateBg(f, r, g, b, a)
    local t = f:CreateTexture(nil, "BACKGROUND")
    t:SetAllPoints()
    t:SetColorTexture(r, g, b, a)
    f.bg = t
end

local function CreateBorder(f, r, g, b, a)
    if f.border then f.border:Hide() end
    local border = f:CreateTexture(nil, "BORDER")
    border:SetPoint("TOPLEFT", -1, 1)
    border:SetPoint("BOTTOMRIGHT", 1, -1)
    border:SetColorTexture(r, g, b, a)
    f.border = border
end

local function SkinButton(btn)
    if not btn then return end
    btn:SetNormalTexture("")
    btn:SetHighlightTexture("")
    btn:SetPushedTexture("")
    btn:SetDisabledTexture("")
    CreateBg(btn, 0.2, 0.2, 0.2, 1) 
    CreateBorder(btn, 0, 0, 0, 1)
    btn:HookScript("OnEnter", function(s) if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end end)
    btn:HookScript("OnLeave", function(s) if s.bg then s.bg:SetColorTexture(0.2, 0.2, 0.2, 1) end end)
    local fs = btn:GetFontString()
    if fs then fs:SetTextColor(1, 1, 1) end
end

local function CreateCustomScrollbar(parent, targetScrollFrame)
    local slider = CreateFrame("Slider", nil, parent)
    slider:SetWidth(12)
    slider:SetOrientation("VERTICAL")
    local track = slider:CreateTexture(nil, "BACKGROUND")
    track:SetAllPoints()
    track:SetColorTexture(0, 0, 0, 0.5)
    local thumb = slider:CreateTexture(nil, "ARTWORK")
    thumb:SetSize(12, 24)
    thumb:SetColorTexture(0.4, 0.4, 0.4, 1) 
    slider:SetThumbTexture(thumb)
    slider:SetScript("OnValueChanged", function(s, v)
        targetScrollFrame:SetVerticalScroll(v)
    end)
    return slider
end

local function ParseDate(dateStr)
    local d, m, y = string.match(dateStr, "(%d+)[%p](%d+)[%p](%d+)")
    if not d then return nil end
    return time({year=y, month=m, day=d, hour=12, min=0, sec=0})
end

local function FormatDate(timestamp)
    return date("%d-%m-%Y", timestamp)
end

local function Sanitize(str)
    if not str then return "" end
    str = string.gsub(str, "%^", "") -- Remove delimiter
    return str
end

local function GetRegionName()
    local regionID = GetCurrentRegion()
    if regionID == 1 then return "us"
    elseif regionID == 2 then return "kr"
    elseif regionID == 3 then return "eu"
    elseif regionID == 4 then return "tw"
    elseif regionID == 5 then return "cn"
    else return "us" end 
end

local function GetFactionID()
    local factionGroup = UnitFactionGroup("player")
    if factionGroup == "Alliance" then return 1 else return 2 end 
end

local function ScoreColor(s)
    if not s or s == 0 then return "|cffffffff" end
    if s >= 3000 then return "|cffff8000" -- Legendary
    elseif s >= 2500 then return "|cffa335ee" -- Epic
    elseif s >= 2000 then return "|cff0070dd" -- Rare
    elseif s >= 1000 then return "|cff1eff00" -- Green
    else return "|cffffffff" end
end

local function RaidProgressColor(str)
    if not str or str == "-" then return "|cff808080" end
    local kills = tonumber(string.match(str, "^(%d+)")) or 0
    if kills == 0 then return "|cff808080"
    elseif kills <= 4 then return "|cff00ff00"
    elseif kills <= 6 then return "|cffa335ee"
    else return "|cffff8000" end
end

-- =========================================================================
-- Name Normalization Helper
-- Ensure we always work with "Name-Realm" format for consistency
-- =========================================================================
local function GetFullPlayerName()
    local name, realm = UnitFullName("player")
    if not realm then realm = GetRealmName() end
    return name .. "-" .. realm
end

-- =========================================================================
-- 3. INITIALIZATION & COMM
-- =========================================================================

function GR:RR_Init()
    GR.RR.Frame = GR.views[6]
    
    if GR.RR.Lite and GR.RR.Lite.Init then GR.RR.Lite:Init() end

    if not GRS_RaidRosterDB then GRS_RaidRosterDB = {} end
    if not GRS_RaidRosterDB.events then GRS_RaidRosterDB.events = {} end

    C_ChatInfo.RegisterAddonMessagePrefix(COMM_PREFIX)
    GR.RR.Frame:RegisterEvent("CHAT_MSG_ADDON")
    GR.RR.Frame:RegisterEvent("GUILD_ROSTER_UPDATE")
    
    GR.RR.Frame:SetScript("OnEvent", function(s, event, ...)
        if event == "CHAT_MSG_ADDON" then GR:RR_OnComm(...)
        elseif event == "GUILD_ROSTER_UPDATE" then
            if GR.RR.viewMode == "OFFICER" and s:IsVisible() then GR:RR_UpdateOfficerRightPane() end
        end
    end)
    
    C_Timer.NewTicker(SYNC_INTERVAL, function() GR:RR_RequestSync() end)
    
    if IsInGuild() and (C_GuildInfo.CanEditOfficerNote() or IsGuildLeader()) then
        GR.RR.viewMode = "OFFICER"
    else
        GR.RR.viewMode = "MEMBER"
    end
    
    GR:RR_BuildUI()
    GR:RR_ScanDB()
    GR:RR_RequestSync()
end

function GR:RR_BroadcastEvent(eventData)
    if not IsInGuild() then return end
    local timeStr = (eventData.startTime or "00:00") .. "-" .. (eventData.endTime or "00:00")
    local str = table.concat({
        "EVT", eventData.id, Sanitize(eventData.name), eventData.date, timeStr, 
        eventData.type, eventData.limit or 99, eventData.rankLimit or 99, Sanitize(eventData.def or "")
    }, COMM_DELIM)
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, str, "GUILD")
    
    -- If broadcasting the event creation/edit, also broadcast the roster state immediately
    GR:RR_BroadcastRosterFull(eventData.id)
end

function GR:RR_BroadcastDelete(idList)
    if not IsInGuild() or #idList == 0 then return end
    local str = "DEL" .. COMM_DELIM .. table.concat(idList, ",")
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, str, "GUILD")
end

function GR:RR_BroadcastSignUp(eventId, role, status)
    if not IsInGuild() then return end
    local str = table.concat({"SIGNUP", eventId, role, status, GetServerTime()}, COMM_DELIM)
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, str, "GUILD")
end

function GR:RR_BroadcastModStatus(eventId, targetName, targetRole, newStatus)
    if not IsInGuild() then return end
    local str = table.concat({"MOD_STATUS", eventId, targetName, targetRole, newStatus, GetServerTime()}, COMM_DELIM)
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, str, "GUILD")
end

function GR:RR_BroadcastRemove(eventId, targetName)
    if not IsInGuild() then return end
    local str = table.concat({"REMOVE_PLAYER", eventId, targetName}, COMM_DELIM)
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, str, "GUILD")
end

function GR:RR_BroadcastRosterFull(eventId)
    if not IsInGuild() then return end
    local evt = GRS_RaidRosterDB.events[eventId]
    if not evt then return end
    
    -- 1. Send Clear
    C_ChatInfo.SendAddonMessage(COMM_PREFIX, "ROSTER_CLEAR"..COMM_DELIM..eventId, "GUILD")
    
    -- 2. Build Bulk Data
    local buffer = ""
    local count = 0
    
    local function AddToBuffer(name, role, status, timeVal)
        -- Format: Name:Role:Status:Time
        local entry = table.concat({name, role, status, timeVal or 0}, ":")
        if #buffer + #entry + 1 > MAX_COMM_CHUNK then
            -- Flush
            C_ChatInfo.SendAddonMessage(COMM_PREFIX, "ROSTER_DAT"..COMM_DELIM..eventId..COMM_DELIM..buffer, "GUILD")
            buffer = entry
        else
            if buffer == "" then buffer = entry else buffer = buffer .. ";" .. entry end
        end
    end
    
    for roleKey, list in pairs(evt.roster) do
        for _, member in ipairs(list) do
            AddToBuffer(member.name, member.role, member.status, member.time)
        end
    end
    
    if buffer ~= "" then
         C_ChatInfo.SendAddonMessage(COMM_PREFIX, "ROSTER_DAT"..COMM_DELIM..eventId..COMM_DELIM..buffer, "GUILD")
    end
end

function GR:RR_RequestSync()
    if IsInGuild() then C_ChatInfo.SendAddonMessage(COMM_PREFIX, "REQ_SYNC", "GUILD") end
end

function GR:RR_OnComm(prefix, text, channel, sender)
    if prefix ~= COMM_PREFIX then return end
    
    local senderName, senderRealm = strsplit("-", sender)
    if not senderRealm then senderRealm = GetRealmName() end
    local fullSender = senderName .. "-" .. senderRealm
    
    if fullSender == GetFullPlayerName() then return end
    
    local parts = { strsplit(COMM_DELIM, text) }
    local op = parts[1]
    
    if op == "REQ_SYNC" then
        if GRS_RaidRosterDB and GRS_RaidRosterDB.events then
            for _, evt in pairs(GRS_RaidRosterDB.events) do 
                GR:RR_BroadcastEvent(evt) 
            end
        end
        
    elseif op == "EVT" then
        local id = parts[2]
        if not id then return end
        
        if not GRS_RaidRosterDB then GRS_RaidRosterDB = {} end
        if not GRS_RaidRosterDB.events then GRS_RaidRosterDB.events = {} end

        local evt = GRS_RaidRosterDB.events[id]
        if not evt then
            evt = { roster = { TANK={}, HEALER={}, DPS={}, BENCH={}, TENTATIVE={}, ABSENT={} } }
            GRS_RaidRosterDB.events[id] = evt
        end
        
        evt.id = id; evt.name = parts[3]; evt.date = parts[4]
        local ts, te = strsplit("-", parts[5] or "")
        evt.startTime = ts; evt.endTime = te
        evt.time = parts[5]
        
        evt.type = parts[6]; evt.limit = tonumber(parts[7])
        evt.rankLimit = tonumber(parts[8]); evt.def = parts[9]
        GR:RR_ScanDB()
        
    elseif op == "ROSTER_CLEAR" then
        local eid = parts[2]
        local evt = GRS_RaidRosterDB.events[eid]
        if evt then
            evt.roster = { TANK={}, HEALER={}, DPS={}, BENCH={}, TENTATIVE={}, ABSENT={} }
            if GR.RR.selectedEvent and GR.RR.selectedEvent.id == eid then GR:RR_SelectEvent(evt) end
        end
        
    elseif op == "ROSTER_DAT" then
        local eid = parts[2]
        local payload = parts[3]
        local evt = GRS_RaidRosterDB.events[eid]
        
        if evt and payload then
            for entry in string.gmatch(payload, "([^;]+)") do
                local name, role, status, ts = strsplit(":", entry)
                ts = tonumber(ts) or 0
                GR:RR_UpdateMemberStatus(eid, name, role, status, ts)
            end
        end
        
    elseif op == "DEL" then
        local ids = parts[2]
        if ids then
            for id in string.gmatch(ids, "([^,]+)") do
                GRS_RaidRosterDB.events[id] = nil
            end
            GR:RR_ScanDB()
            if GR.RR.selectedEvent and not GRS_RaidRosterDB.events[GR.RR.selectedEvent.id] then
                GR:RR_SelectEvent(nil)
            end
        end
        
    elseif op == "SIGNUP" or op == "MOD_STATUS" then
        local eid, target, role, status, ts
        
        if op == "SIGNUP" then
            eid = parts[2]; target = sender; role = parts[3]; status = parts[4]; ts = tonumber(parts[5])
        else
            eid = parts[2]; target = parts[3]; role = parts[4]; status = parts[5]; ts = tonumber(parts[6])
        end
        GR:RR_UpdateMemberStatus(eid, target, role, status, ts)
        
    elseif op == "REMOVE_PLAYER" then
        local eid, target = parts[2], parts[3]
        GR:RR_RemoveMember(eid, target)
    end
end

-- Shared logic for updating a member state (Used by Comm and Local actions)
function GR:RR_UpdateMemberStatus(eid, target, role, status, ts)
    local evt = GRS_RaidRosterDB.events[eid]
    if not evt then return end
    
    if not evt.roster["TENTATIVE"] then evt.roster["TENTATIVE"] = {} end
    if not evt.roster["BENCH"] then evt.roster["BENCH"] = {} end
    if not evt.roster["ABSENT"] then evt.roster["ABSENT"] = {} end

    -- Remove from all lists first (Anti-Duplication)
    for r, list in pairs(evt.roster) do
        for i=#list, 1, -1 do
            if list[i].name == target then table.remove(list, i) end
        end
    end
    
    -- Determine destination list
    local targetList = evt.roster[status]
    if not targetList then
        if status == "Present" then targetList = evt.roster[role] -- Return to role list
        elseif status == "Tentative" then targetList = evt.roster["TENTATIVE"]
        elseif status == "BENCH" then targetList = evt.roster["BENCH"]
        elseif status == "ABSENT" then targetList = evt.roster["ABSENT"]
        else targetList = evt.roster[role] end -- Default
    end
    
    if targetList then
        table.insert(targetList, {name=target, role=role, status=status, time=ts})
        table.sort(targetList, function(a,b) return (a.time or 0) < (b.time or 0) end)
    end
    
    -- UI Refresh
    if GR.RR.selectedEvent and GR.RR.selectedEvent.id == eid then
        GR:RR_SelectEvent(evt)
    end
    if GRS_RR_InspectDlg and GRS_RR_InspectDlg:IsShown() and GR.RR.selectedEvent and GR.RR.selectedEvent.id == eid then
        GR:RR_OpenInspectDialog() -- Refresh Inspect
    end
end

function GR:RR_RemoveMember(eid, target)
    local evt = GRS_RaidRosterDB.events[eid]
    if evt then
        for r, list in pairs(evt.roster) do
            for i=#list, 1, -1 do
                if list[i].name == target then table.remove(list, i) end
            end
        end
        
        if GR.RR.selectedEvent and GR.RR.selectedEvent.id == eid then
            GR:RR_SelectEvent(evt)
        end
        if GRS_RR_InspectDlg and GRS_RR_InspectDlg:IsShown() and GR.RR.selectedEvent and GR.RR.selectedEvent.id == eid then
            GR:RR_OpenInspectDialog()
        end
    end
end

-- =========================================================================
-- 4. DATABASE & UI LOGIC (MAIN ROSTER)
-- =========================================================================

function GR:RR_ScanDB()
    if not GRS_RaidRosterDB then GRS_RaidRosterDB = {} end
    if not GRS_RaidRosterDB.events then GRS_RaidRosterDB.events = {} end

    GR.RR.cache = {}
    local list = {}
    for id, evt in pairs(GRS_RaidRosterDB.events) do
        local counts = { TANK=0, HEALER=0, DPS=0, BENCH=0, TENTATIVE=0, ABSENT=0 }
        for role, members in pairs(evt.roster) do counts[role] = #members end
        evt.counts = counts
        table.insert(list, evt)
    end
    table.sort(list, function(a, b) 
        local tA = ParseDate(a.date) or 0
        local tB = ParseDate(b.date) or 0
        return tA < tB 
    end)
    GR.RR.cache = list
    GR:RR_UpdateLeftPane()
end

function GR:RR_SelectEvent(eventData)
    GR.RR.selectedEvent = eventData
    if not eventData then 
        GR.RR.currentRoster = nil
        GR:RR_UpdateLeftPaneCounters()
        GR:RR_UpdateRaster(nil)
        return 
    end
    GR.RR.currentRoster = eventData.roster
    if GR.RR.viewMode == "OFFICER" then GR:RR_UpdateOfficerRightPane() else GR:RR_UpdateMemberRightPane() end
    GR:RR_UpdateLeftPaneCounters()
end

function GR:RR_BuildUI()
    local p = GR.RR.Frame
    local bgSolid = p:CreateTexture(nil, "BACKGROUND", nil, -1)
    bgSolid:SetAllPoints(); bgSolid:SetColorTexture(0, 0, 0, 0.35) 
    
    local bg = p:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(); bg:SetTexture(ROSTER_BG_PATH); bg:SetVertexColor(1, 1, 1, 1)
    
    local left = CreateFrame("Frame", "GRS_RR_Left", p)
    left:SetWidth(180); left:SetPoint("TOPLEFT", 10, -60); left:SetPoint("BOTTOMLEFT", 10, 10)
    CreateBg(left, 0, 0, 0, 0.5)
    GR.RR.Left = left
    
    local right = CreateFrame("Frame", "GRS_RR_Right", p)
    right:SetPoint("TOPLEFT", left, "TOPRIGHT", 10, 0); right:SetPoint("BOTTOMRIGHT", -10, 10)
    GR.RR.Right = right
    
    GR:RR_BuildLeftPane()
    if GR.RR.viewMode == "OFFICER" then GR:RR_BuildOfficerRightPane() else GR:RR_BuildMemberRightPane() end
end

-- =========================================================================
-- 5. LEFT PANE (LIST & BUTTONS)
-- =========================================================================
function GR:RR_BuildLeftPane()
    local p = GR.RR.Left
    local t = p:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    t:SetPoint("TOP", 0, -10); t:SetText("Sign-Ups")
    
    local scroll = CreateFrame("ScrollFrame", "GRS_RR_Scroll", p, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 5, -40); scroll:SetPoint("BOTTOMRIGHT", -25, 160) 
    
    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(150, 500); scroll:SetScrollChild(content)
    GR.RR.EventListContent = content; GR.RR.EventCards = {}
    
    local btnArea = CreateFrame("Frame", nil, p)
    btnArea:SetPoint("TOPLEFT", scroll, "BOTTOMLEFT", 0, -10); btnArea:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", -5, 5)
    
    local function MkBtn(txt, parent, w, h)
        local b = CreateFrame("Button", nil, parent); b:SetSize(w, h); SkinButton(b)
        local fs = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); fs:SetPoint("CENTER"); fs:SetText(txt); b:SetFontString(fs)
        return b
    end
    
    if GR.RR.viewMode == "OFFICER" then
        local btnCreate = MkBtn("Create", btnArea, 80, 22); btnCreate:SetPoint("TOPLEFT", 0, 0)
        btnCreate:SetScript("OnClick", function() GR:RR_OpenEventDialog("CREATE") end)
        
        local btnEdit = MkBtn("Edit", btnArea, 80, 22); btnEdit:SetPoint("LEFT", btnCreate, "RIGHT", 5, 0)
        btnEdit:SetScript("OnClick", function() 
            if GR.RR.selectedEvent then GR:RR_OpenEventDialog("EDIT", GR.RR.selectedEvent) end 
        end)
        
        local btnDel = MkBtn("Delete", btnArea, 80, 22); btnDel:SetPoint("TOPLEFT", btnCreate, "BOTTOMLEFT", 0, -5)
        GR:RR_SetupDeleteButton(btnDel)
        
        local btnRefresh = MkBtn("Refresh", btnArea, 80, 22); btnRefresh:SetPoint("LEFT", btnDel, "RIGHT", 5, 0)
        btnRefresh:SetScript("OnClick", function() GR:RR_RequestSync() end)

        local btnInspect = MkBtn("Inspect", btnArea, 165, 22); btnInspect:SetPoint("TOPLEFT", btnDel, "BOTTOMLEFT", 0, -5)
        btnInspect:SetScript("OnClick", function() GR:RR_OpenInspectDialog() end)

        local btnSignUp = MkBtn("Sign-Up", btnArea, 165, 22); btnSignUp:SetPoint("TOPLEFT", btnInspect, "BOTTOMLEFT", 0, -5)
        btnSignUp:SetScript("OnClick", function() GR:RR_OpenSignUpDialog() end)
    else
        local btnSignUp = MkBtn("Sign-Up", btnArea, 165, 30); btnSignUp:SetPoint("TOP", 0, -10)
        btnSignUp:SetScript("OnClick", function() GR:RR_OpenSignUpDialog() end)
    end
end

function GR:RR_UpdateLeftPane()
    local content = GR.RR.EventListContent; local list = GR.RR.cache
    for _, card in pairs(GR.RR.EventCards) do card:Hide() end
    local y = 0
    for i, event in ipairs(list) do
        local card = GR.RR.EventCards[i]
        if not card then
            card = CreateFrame("Button", nil, content, "BackdropTemplate"); card:SetSize(150, 60)
            CreateBg(card, 0, 0, 0, 0.5); CreateBorder(card, 0.3, 0.3, 0.3, 1)
            
            card.name = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightMedium")
            card.name:SetPoint("TOPLEFT", 5, -5); card.name:SetJustifyH("LEFT"); card.name:SetWidth(120)
            
            card.dt = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            card.dt:SetPoint("TOPLEFT", 5, -35); card.dt:SetJustifyH("LEFT")
            
            if GR.RR.viewMode == "OFFICER" then
                local chk = CreateFrame("CheckButton", nil, card, "UICheckButtonTemplate")
                chk:SetSize(20, 20); chk:SetPoint("RIGHT", -2, 0)
                chk:SetScript("OnClick", function(s)
                    if s:GetChecked() then GR.RR.multiSelect[card.data.id] = true
                    else GR.RR.multiSelect[card.data.id] = nil end
                end)
                card.chk = chk
            end
            
            card:SetScript("OnClick", function(s) GR:RR_SelectEvent(s.data) end)
            GR.RR.EventCards[i] = card
        end
        
        card.data = event; card:SetPoint("TOP", 0, -y); card:Show()
        card.name:SetText(event.name)
        card.dt:SetText((event.date or "") .. "\n" .. (event.time or ""))
        
        if card.chk then card.chk:SetChecked(GR.RR.multiSelect[event.id]) end
        
        if GR.RR.selectedEvent and GR.RR.selectedEvent.id == event.id then
            card.bg:SetColorTexture(0.3, 0.3, 0.5, 0.8)
        else
            card.bg:SetColorTexture(0, 0, 0, 0.5)
        end
        
        y = y + 65
    end
    content:SetHeight(math.max(y, 10))
end

function GR:RR_UpdateLeftPaneCounters() GR:RR_UpdateLeftPane() end

-- =========================================================================
-- 6. OFFICER RIGHT PANE & CLASS ICONS
-- =========================================================================

function GR:RR_BuildOfficerRightPane()
    local p = GR.RR.Right
    local availFrame = CreateFrame("Frame", nil, p)
    
    availFrame:SetPoint("TOPLEFT", 10, 0); availFrame:SetPoint("RIGHT", -10, 0); availFrame:SetHeight(70)
    CreateBg(availFrame, 0, 0, 0, 0.4)
    
    local classes = { "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "MONK", "DRUID", "DEMONHUNTER", "EVOKER" }
    GR.RR.ClassButtons = {}
    local x, y = 10, -10
    for i, cls in ipairs(classes) do
        local btn = CreateFrame("Button", nil, availFrame)
        btn:SetSize(24, 24); btn:SetPoint("TOPLEFT", x, y)
        local icon = btn:CreateTexture(nil, "ARTWORK"); icon:SetAllPoints()
        local coords = CLASS_ICON_TCOORDS[cls]
        icon:SetTexture("Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes")
        if coords then icon:SetTexCoord(unpack(coords)) else icon:SetTexCoord(0, 1, 0, 1) end
        local hl = btn:CreateTexture(nil, "HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1,1,1,0.2)
        btn:SetScript("OnClick", function() if GR.RR.Lite then GR.RR.Lite:ShowPopup(cls) end end)
        local cnt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); cnt:SetPoint("LEFT", btn, "RIGHT", 2, 0); cnt:SetText("0")
        btn.cnt = cnt; GR.RR.ClassButtons[cls] = btn
        x = x + 55; if i == 7 then x = 10; y = -40 end
    end
    
    GR:RR_BuildRaster(p)
    GR.RR.RasterFrame:SetPoint("TOPLEFT", availFrame, "BOTTOMLEFT", 0, -10)
    GR.RR.RasterFrame:SetPoint("BOTTOMRIGHT", -10, 0)
end

function GR:RR_UpdateOfficerRightPane()
    if not IsInGuild() then return end
    local roster = GR.RR.Lite:FetchData()
    local counts = {}
    for _, member in ipairs(roster) do counts[member.class] = (counts[member.class] or 0) + 1 end
    if GR.RR.ClassButtons then
        for cls, btn in pairs(GR.RR.ClassButtons) do btn.cnt:SetText(counts[cls] or 0) end
    end
    GR:RR_UpdateRaster(nil)
end

function GR:RR_BuildMemberRightPane()
    local p = GR.RR.Right; GR:RR_BuildRaster(p); GR.RR.RasterFrame:SetPoint("TOPLEFT", 10, 0)
    GR.RR.RasterFrame:SetPoint("BOTTOMRIGHT", -10, 0)
end

function GR:RR_UpdateMemberRightPane() GR:RR_UpdateRaster() end

-- =========================================================================
-- 7. RASTER DISPLAY (GRID)
-- =========================================================================

function GR:RR_BuildRaster(parent)
    local f = CreateFrame("ScrollFrame", "GRS_RR_RasterScroll", parent, "UIPanelScrollFrameTemplate")
    CreateBg(f, 0, 0, 0, 0.4)
    local content = CreateFrame("Frame", nil, f); content:SetSize(500, 400); f:SetScrollChild(content)
    GR.RR.RasterFrame = f; GR.RR.RasterContent = content; GR.RR.RasterCols = {}
end

function GR:RR_UpdateRaster(override)
    local content = GR.RR.RasterContent; local roster = GR.RR.currentRoster
    for _, col in pairs(GR.RR.RasterCols) do col:Hide() end
    if not roster then return end
    local xOff = 10; local colIdx = 1
    
    local function RenderRole(roleName, list, color)
        local f = GR.RR.RasterCols[colIdx]
        if not f then
            f = CreateFrame("Frame", nil, content, "BackdropTemplate"); f:SetSize(140, 200)
            f:SetBackdrop({bgFile="Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile="", tile=true, tileSize=16})
            f:SetBackdropColor(0,0,0,0.5); f.rows = {}; f.head = f:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall")
            f.head:SetPoint("TOP",0,-5); GR.RR.RasterCols[colIdx] = f
        end
        f:ClearAllPoints(); f:SetPoint("TOPLEFT", xOff, -10); f:Show()
        f.head:SetText(roleName); f.head:SetTextColor(unpack(color))
        
        for r=1, MAX_ROWS_PER_COLUMN do
            if not f.rows[r] then local t=f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall"); t:SetPoint("TOPLEFT",10,-25-((r-1)*15)); f.rows[r]=t end
            if list and list[r] then f.rows[r]:SetText(list[r].name); f.rows[r]:Show() else f.rows[r]:Hide() end
        end
        xOff = xOff + 145; colIdx = colIdx + 1
    end
    
    RenderRole("TANKS", roster["TANK"], {0.2,0.6,1})
    RenderRole("HEALERS", roster["HEALER"], {0.2,1,0.4})
    RenderRole("DPS", roster["DPS"], {1,0.3,0.3})
    
    RenderRole("BENCH", roster["BENCH"], {0.4, 0.8, 1})
    RenderRole("TENTATIVE", roster["TENTATIVE"] or {}, {1, 0.8, 0})
    RenderRole("ABSENT", roster["ABSENT"] or {}, {0.6, 0.6, 0.6})
    
    content:SetWidth(xOff+20)
end

-- =========================================================================
-- 8. DIALOGS (CREATE / EDIT / DELETE)
-- =========================================================================

function GR:RR_OpenEventDialog(mode, existingData)
    if C_GuildInfo and C_GuildInfo.GuildRoster then C_GuildInfo.GuildRoster() end

    local f = GR:CreateDialog("GRS_RR_EventDlg", mode == "EDIT" and "Edit Event" or "Create Event", 500, 450)
    f:Show()
    
    if not f.inputs then
        f.inputs = {}
        
        local xL, y = 20, -40
        
        local function MkInp(lbl, w, def, parent, pAnchor, px, py)
            local l = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            if parent then l:SetPoint("TOPLEFT", parent, pAnchor, px, py) 
            else l:SetPoint("TOPLEFT", xL, y) end
            l:SetText(lbl)
            
            local eb = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); eb:SetSize(w, 20); eb:SetPoint("LEFT", l, "RIGHT", 10, 0); eb:SetAutoFocus(false)
            if def then eb:SetText(def) end
            return eb
        end
        
        f.inputs.name = MkInp("Name:", 150); y = y - 40
        f.inputs.date = MkInp("Date:", 80); y = y - 40
        f.inputs.startT = MkInp("Start:", 50)
        f.inputs.endT = MkInp("End:", 50, nil, f.inputs.startT, "TOPRIGHT", 20, 0)
        
        local dl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); dl:SetPoint("TOPLEFT", 20, y-40); dl:SetText("Desc:")
        f.inputs.def = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); f.inputs.def:SetSize(200, 20); f.inputs.def:SetPoint("LEFT", dl, "RIGHT", 10, 0); f.inputs.def:SetAutoFocus(false)
        
        local xR, yR = 260, -40
        local function MkDrop(lbl, offY)
            local l = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); l:SetPoint("TOPLEFT", xR, yR + (offY or 0)); l:SetText(lbl)
            local d = CreateFrame("Frame", nil, f, "UIDropDownMenuTemplate"); d:SetPoint("TOPLEFT", xR-15, yR + (offY or 0) - 20)
            UIDropDownMenu_SetWidth(d, 120)
            return d
        end
        
        f.inputs.typeDrop = MkDrop("Event Type:")
        f.inputs.rankDrop = MkDrop("Min Rank:", -60)
        
        local limLbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); limLbl:SetPoint("TOPLEFT", xR, -160); limLbl:SetText("Limit:")
        f.inputs.limit = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); f.inputs.limit:SetSize(40, 20); f.inputs.limit:SetPoint("LEFT", limLbl, "RIGHT", 10, 0); f.inputs.limit:SetAutoFocus(false)
        
        f.inputs.repeatChk = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate")
        f.inputs.repeatChk:SetPoint("TOPLEFT", xR, -200)
        f.inputs.repeatChk.text:SetText("Weekly Repeat")
        
        f.saveBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); f.saveBtn:SetSize(100,25); f.saveBtn:SetPoint("BOTTOMLEFT", 20, 20); f.saveBtn:SetText("Save")
        f.cancelBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); f.cancelBtn:SetSize(100,25); f.cancelBtn:SetPoint("BOTTOMRIGHT", -20, 20); f.cancelBtn:SetText("Cancel")
        f.cancelBtn:SetScript("OnClick", function() f:Hide() end)
    end
    
    local version, _, _, buildInfo = GetBuildInfo()
    local isMidnight = (tonumber(string.match(version, "^(%d+)")) or 0) >= 12
    local raidList = isMidnight 
        and {"The Voidspire", "The Dreamrift", "March on Quel'Danas", "Mythic+"}
        or {"Nerub-ar Palace", "Liberation of Undermine", "Manaforge Omega", "Mythic+"}
        
    UIDropDownMenu_Initialize(f.inputs.typeDrop, function(self, level)
        for _, r in ipairs(raidList) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = r
            info.func = function(self) 
                UIDropDownMenu_SetSelectedID(f.inputs.typeDrop, self:GetID()) 
                f.selectedType = r
                UIDropDownMenu_SetText(f.inputs.typeDrop, r)
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end)
    
    UIDropDownMenu_Initialize(f.inputs.rankDrop, function(self, level)
        local numRanks = GuildControlGetNumRanks()
        for i=1, numRanks do
            local rName = GuildControlGetRankName(i)
            local info = UIDropDownMenu_CreateInfo()
            info.text = rName
            info.func = function(self)
                UIDropDownMenu_SetSelectedID(f.inputs.rankDrop, i)
                f.selectedRank = i-1
                UIDropDownMenu_SetText(f.inputs.rankDrop, rName)
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end)
    
    if mode == "EDIT" and existingData then
        f.inputs.name:SetText(existingData.name)
        f.inputs.date:SetText(existingData.date)
        f.inputs.startT:SetText(existingData.startTime or "")
        f.inputs.endT:SetText(existingData.endTime or "")
        f.inputs.def:SetText(existingData.def or "")
        f.inputs.limit:SetText(existingData.limit or 99)
        f.inputs.repeatChk:SetChecked(false); f.inputs.repeatChk:Disable()
        f.selectedType = existingData.type; UIDropDownMenu_SetText(f.inputs.typeDrop, existingData.type)
        
        f.selectedRank = existingData.rankLimit; 
        local storedRankName = GuildControlGetRankName((existingData.rankLimit or 0) + 1)
        UIDropDownMenu_SetText(f.inputs.rankDrop, storedRankName)
    else
        f.inputs.name:SetText(""); f.inputs.date:SetText(date("%d-%m-%Y")); f.inputs.limit:SetText("20")
        f.inputs.repeatChk:Enable(); f.inputs.repeatChk:SetChecked(false)
        f.selectedType = raidList[1]; UIDropDownMenu_SetText(f.inputs.typeDrop, raidList[1])
        f.selectedRank = 99; UIDropDownMenu_SetText(f.inputs.rankDrop, "Any")
    end
    
    f.saveBtn:SetScript("OnClick", function()
        local name = f.inputs.name:GetText()
        local dateStr = f.inputs.date:GetText()
        if name == "" or not ParseDate(dateStr) then return end
        
        -- Capture inputs before loop to handle delay safely
        local sTime, eTime = f.inputs.startT:GetText(), f.inputs.endT:GetText()
        local defTxt, typeVal = f.inputs.def:GetText(), f.selectedType
        local limVal, rankVal = tonumber(f.inputs.limit:GetText()), f.selectedRank
        local doRepeat = (mode == "CREATE" and f.inputs.repeatChk:GetChecked())
        
        local function SaveOne(dStr, extraSalt)
            -- Safer ID generation to prevent collision on fast loops
            local uniqueSuffix = tostring(GetTime()) .. tostring(math.random(10000)) .. (extraSalt or "")
            local id = (mode == "EDIT" and existingData.id) or uniqueSuffix
            
            local rosterStruct = {TANK={},HEALER={},DPS={},BENCH={},TENTATIVE={},ABSENT={}}
            if existingData and existingData.roster then
                 rosterStruct = existingData.roster
                 if not rosterStruct.TENTATIVE then rosterStruct.TENTATIVE = {} end
                 if not rosterStruct.BENCH then rosterStruct.BENCH = {} end
                 if not rosterStruct.ABSENT then rosterStruct.ABSENT = {} end
            end

            local evt = {
                id = id, name = name, date = dStr,
                startTime = sTime, endTime = eTime,
                def = defTxt, type = typeVal,
                limit = limVal, rankLimit = rankVal,
                roster = rosterStruct
            }
            GRS_RaidRosterDB.events[id] = evt
            GR:RR_BroadcastEvent(evt)
        end
        
        -- Save first event immediately
        SaveOne(dateStr, "_MAIN")
        
        -- Throttle the repeats using C_Timer to prevent comms overflow/loss
        if doRepeat then
            local t = ParseDate(dateStr)
            local repeatCount = 0
            C_Timer.NewTicker(0.5, function()
                repeatCount = repeatCount + 1
                t = t + (7 * 86400)
                SaveOne(date("%d-%m-%Y", t), "_R"..repeatCount)
                GR:RR_ScanDB() -- Refresh list after each add
            end, 4)
        end
        
        GR:RR_ScanDB()
        f:Hide()
    end)
end

function GR:RR_SetupDeleteButton(btn)
    btn:SetScript("OnClick", function()
        local count = 0
        local ids = {}
        for id, _ in pairs(GR.RR.multiSelect) do count = count + 1; table.insert(ids, id) end
        if GR.RR.selectedEvent and count == 0 then table.insert(ids, GR.RR.selectedEvent.id); count = 1 end
        
        if count > 0 then
            local dlg = GR:CreateDialog("GRS_RR_DelDlg", "Delete Events", 300, 150)
            if not dlg.lbl then dlg.lbl = dlg:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); dlg.lbl:SetPoint("CENTER", 0, 20) end
            dlg.lbl:SetText("Delete " .. count .. " events?")
            
            if not dlg.ok then 
                dlg.ok = CreateFrame("Button", nil, dlg, "UIPanelButtonTemplate"); dlg.ok:SetSize(80, 25); dlg.ok:SetPoint("BOTTOMLEFT", 60, 20)
                dlg.cancel = CreateFrame("Button", nil, dlg, "UIPanelButtonTemplate"); dlg.cancel:SetSize(80, 25); dlg.cancel:SetPoint("BOTTOMRIGHT", -60, 20); dlg.cancel:SetText("Cancel")
                dlg.cancel:SetScript("OnClick", function() dlg:Hide() end)
            end
            
            dlg:Show()
            dlg.ok:Disable()
            dlg.timeLeft = 5
            dlg:SetScript("OnUpdate", function(s, elapsed)
                s.timeLeft = s.timeLeft - elapsed
                if s.timeLeft <= 0 then 
                    s.ok:Enable(); s.ok:SetText("Delete")
                    s:SetScript("OnUpdate", nil)
                else 
                    s.ok:SetText(math.ceil(s.timeLeft)) 
                end
            end)
            
            dlg.ok:SetScript("OnClick", function()
                for _, id in ipairs(ids) do GRS_RaidRosterDB.events[id] = nil end
                GR:RR_BroadcastDelete(ids)
                GR.RR.multiSelect = {}
                GR:RR_ScanDB(); GR:RR_SelectEvent(nil)
                dlg:Hide()
            end)
        end
    end)
end

-- =========================================================================
-- 9. INSPECT DIALOG (UPDATED: MASS ACTIONS SUPPORT)
-- =========================================================================

function GR:RR_OpenInspectDialog()
    local evt = GR.RR.selectedEvent
    if not evt then return end
    
    GR.RR.inspectSelection = nil
    GR.RR.inviteSelection = {} -- Reset checkbox selections
    
    local f = GR:CreateDialog("GRS_RR_InspectDlg", "Inspect: " .. evt.name, 900, 450)
    
    local bg = f:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(); bg:SetTexture(INSPECT_BG_PATH); bg:SetVertexColor(1, 1, 1, 1)
    bg:SetDrawLayer("BACKGROUND", 1)

    -- [UPDATED] Helper to get list of target players
    -- Returns list of {name=..., role=...}
    local function GetTargets()
        local t = {}
        -- 1. Priority: Checkboxes
        local hasChecks = false
        for name, data in pairs(GR.RR.inviteSelection) do
            if data then 
                table.insert(t, data)
                hasChecks = true 
            end
        end
        -- 2. Fallback: Single Selection (only if no checks)
        if not hasChecks and GR.RR.inspectSelection then
            table.insert(t, GR.RR.inspectSelection)
        end
        return t
    end

    if not f.content then
        f.scroll = CreateFrame("ScrollFrame", "GRS_RR_InspectScroll", f)
        f.scroll:SetPoint("TOPLEFT", 10, -30); f.scroll:SetPoint("BOTTOMRIGHT", -30, 60)
        
        local slider = CreateCustomScrollbar(f, f.scroll)
        slider:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -30)
        slider:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 60)
        f.scroll.slider = slider
        f.scroll:SetScript("OnMouseWheel", function(s, d) 
            local cur = slider:GetValue()
            slider:SetValue(cur - (d * 20)) 
        end)
        
        f.content = CreateFrame("Frame", nil, f.scroll); f.content:SetSize(860, 360)
        f.scroll:SetScrollChild(f.content)
        f.cols = {}
        
        -- [[ ACTION BAR ]]
        f.actionBar = CreateFrame("Frame", nil, f)
        f.actionBar:SetPoint("BOTTOMLEFT", 10, 10)
        f.actionBar:SetPoint("BOTTOMRIGHT", -10, 10)
        f.actionBar:SetHeight(40)
        
        local function MkActBtn(txt, w, x, func)
            local b = CreateFrame("Button", nil, f.actionBar, "UIPanelButtonTemplate")
            b:SetSize(w, 25); b:SetPoint("LEFT", x, 0); b:SetText(txt)
            b:SetScript("OnClick", func)
            b:Disable() -- Default disabled
            return b
        end
        
        -- [UPDATED] Action Logic to iterate targets
        f.btnPresent = MkActBtn("Set Present", 90, 0, function()
            local targets = GetTargets()
            for _, s in ipairs(targets) do
                GR:RR_BroadcastModStatus(evt.id, s.name, s.role, "Present")
                GR:RR_UpdateMemberStatus(evt.id, s.name, s.role, "Present", 0)
            end
        end)
        
        f.btnBench = MkActBtn("Set Bench", 90, 95, function()
            local targets = GetTargets()
            for _, s in ipairs(targets) do
                GR:RR_BroadcastModStatus(evt.id, s.name, s.role, "BENCH")
                GR:RR_UpdateMemberStatus(evt.id, s.name, s.role, "BENCH", 0)
            end
        end)
        
        f.btnTentative = MkActBtn("Set Tentative", 90, 190, function()
            local targets = GetTargets()
            for _, s in ipairs(targets) do
                GR:RR_BroadcastModStatus(evt.id, s.name, s.role, "Tentative")
                GR:RR_UpdateMemberStatus(evt.id, s.name, s.role, "Tentative", 0)
            end
        end)
        
        f.btnAbsent = MkActBtn("Set Absent", 90, 285, function()
            local targets = GetTargets()
            for _, s in ipairs(targets) do
                GR:RR_BroadcastModStatus(evt.id, s.name, s.role, "ABSENT")
                GR:RR_UpdateMemberStatus(evt.id, s.name, s.role, "ABSENT", 0)
            end
        end)
        
        f.btnRemove = MkActBtn("Remove", 90, 380, function()
            local targets = GetTargets()
            for _, s in ipairs(targets) do
                GR:RR_BroadcastRemove(evt.id, s.name)
                GR:RR_RemoveMember(evt.id, s.name)
            end
            -- Clear selection after remove to avoid ghost data
            GR.RR.inspectSelection = nil
            GR.RR.inviteSelection = {}
            f.UpdateListDisplay()
        end)

        f.btnInvite = MkActBtn("Mass Invite", 120, 500, function()
             local count = 0
             -- Invite ONLY checked checkboxes (InviteSelection)
             -- We do NOT fallback to single select for Mass Invite usually, 
             -- but consistent with above logic, we use GetTargets()
             local targets = GetTargets()
             for _, s in ipairs(targets) do
                 C_PartyInfo.InviteUnit(s.name)
                 count = count + 1
             end
             print("Inviting " .. count .. " players...")
        end)
        f.btnInvite:Enable() 
        
        if GR.RR.viewMode ~= "OFFICER" then f.actionBar:Hide() end
    end
    f:Show()
    
    -- [[ UPDATED: CLEAN LIST DISPLAY WITH CHECKBOXES ]]
    f.UpdateListDisplay = function()
        local colCounter = 1
        local x = 0
        
        -- [[ FIX: BUTTON STATE LOGIC ]]
        -- Enable buttons if EITHER single selection exists OR checkboxes exist
        if f.btnPresent then
            local targets = GetTargets()
            if #targets > 0 then
                f.btnPresent:Enable(); f.btnBench:Enable(); f.btnTentative:Enable()
                f.btnAbsent:Enable(); f.btnRemove:Enable()
            else
                f.btnPresent:Disable(); f.btnBench:Disable(); f.btnTentative:Disable()
                f.btnAbsent:Disable(); f.btnRemove:Disable()
            end
        end

        local function AddCol(header, list, color)
            local count = list and #list or 0
            local numCols = math.ceil(count / 10)
            if numCols == 0 then numCols = 1 end
            
            for c=1, numCols do
                local frame = f.cols[colCounter]
                if not frame then
                    frame = CreateFrame("Frame", nil, f.content, "BackdropTemplate")
                    frame:SetBackdrop({bgFile="Interface\\DialogFrame\\UI-DialogBox-Background", tile=true, tileSize=16})
                    frame:SetBackdropColor(0,0,0,0.5)
                    frame:SetSize(135, 250)
                    frame.head = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
                    frame.head:SetPoint("TOP",0,-5)
                    frame.rows = {}
                    f.cols[colCounter] = frame 
                end
                
                frame:ClearAllPoints(); frame:SetPoint("TOPLEFT", x, 0); frame:Show()
                local hTxt = header
                if c > 1 then hTxt = hTxt .. " (Ext" .. (c-1) .. ")" end
                frame.head:SetText(hTxt); frame.head:SetTextColor(unpack(color))
                
                for _, btn in pairs(frame.rows) do btn:Hide() end
                
                for r=1, 10 do
                    local btn = frame.rows[r]
                    if not btn then
                        btn = CreateFrame("Button", nil, frame)
                        btn:SetSize(130, 18)
                        btn:SetPoint("TOPLEFT", 2, -25-((r-1)*20))
                        
                        btn.hl = btn:CreateTexture(nil, "BACKGROUND")
                        btn.hl:SetAllPoints(); btn.hl:SetColorTexture(1, 1, 0, 0.3)
                        btn.hl:Hide()
                        
                        local cb = CreateFrame("CheckButton", nil, btn, "UICheckButtonTemplate")
                        cb:SetSize(16, 16)
                        cb:SetPoint("LEFT", 2, 0)
                        btn.cb = cb

                        local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                        fs:SetPoint("LEFT", cb, "RIGHT", 2, 0); fs:SetWidth(105); fs:SetJustifyH("LEFT")
                        btn:SetFontString(fs)
                        
                        btn:SetScript("OnEnter", function(s) s.fs:SetTextColor(1, 1, 1) end)
                        btn:SetScript("OnLeave", function(s) s.fs:SetTextColor(0.8, 0.8, 0.8) end)
                        
                        btn.fs = fs
                        frame.rows[r] = btn
                    end
                    
                    local idx = (c-1)*10 + r
                    if list and list[idx] then 
                        local d = list[idx]
                        btn:Show()
                        btn:SetText(d.name)
                        
                        -- Update Checkbox state
                        -- Store FULL data object in inviteSelection if checked
                        btn.cb:SetChecked(GR.RR.inviteSelection[d.name] ~= nil)
                        btn.cb:SetScript("OnClick", function(s)
                            if s:GetChecked() then
                                GR.RR.inviteSelection[d.name] = d -- Save Data
                            else
                                GR.RR.inviteSelection[d.name] = nil
                            end
                            -- Refresh buttons immediately
                            f.UpdateListDisplay()
                        end)

                        if GR.RR.inspectSelection and GR.RR.inspectSelection.name == d.name then
                            btn.hl:Show()
                        else
                            btn.hl:Hide()
                        end
                        
                        if GR.RR.viewMode == "OFFICER" then
                            btn:SetScript("OnClick", function() 
                                GR.RR.inspectSelection = d
                                f.UpdateListDisplay() 
                            end)
                        else
                            btn:SetScript("OnClick", nil)
                            btn.cb:Hide()
                        end
                    else 
                        btn:Hide() 
                    end
                end
                x = x + 140
                colCounter = colCounter + 1
            end
        end
        
        AddCol("Tank", evt.roster["TANK"], {0.2,0.6,1})
        AddCol("Healer", evt.roster["HEALER"], {0.2,1,0.4})
        AddCol("DPS", evt.roster["DPS"], {1,0.3,0.3})
        AddCol("Bench", evt.roster["BENCH"], {0.4, 0.8, 1})
        AddCol("Tentative", evt.roster["TENTATIVE"] or {}, {1, 0.8, 0})
        AddCol("Absent", evt.roster["ABSENT"] or {}, {0.6, 0.6, 0.6})
        
        for i = colCounter, #f.cols do
            f.cols[i]:Hide()
        end
        
        f.content:SetWidth(x + 20)
        local maxScroll = math.max(0, f.content:GetHeight() - f.scroll:GetHeight())
        f.scroll.slider:SetMinMaxValues(0, maxScroll)
    end
    
    f.UpdateListDisplay()
end

-- =========================================================================
-- 10. SIGN-UP DIALOG
-- =========================================================================
function GR:RR_OpenSignUpDialog()
    local evt = GR.RR.selectedEvent
    if not evt then return end
    
    local myRankIdx = C_GuildInfo.GetGuildRankOrder(UnitGUID("player"))
    if evt.rankLimit and myRankIdx > evt.rankLimit then
        print("You do not meet the rank requirements for this event.")
        return
    end
    
    local f = GR:CreateDialog("GRS_RR_SignUpDlg", "Sign Up", 300, 350)
    f:Show()
    
    if not f.inited then
        f.inited = true
        f.info = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); f.info:SetPoint("TOP", 0, -30)
        f.slots = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); f.slots:SetPoint("TOP", 0, -50); f.slots:SetTextColor(1, 0.82, 0)
        
        f.roles = {}
        local rx = 40
        for _, r in ipairs({"TANK", "HEALER", "DPS"}) do
            local cb = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate")
            cb:SetPoint("TOPLEFT", rx, -90); cb.text:SetText(r)
            cb:SetScript("OnClick", function(s) 
                for _, o in pairs(f.roles) do o:SetChecked(false) end
                s:SetChecked(true); f.selRole = r; f:CheckSave()
            end)
            f.roles[r] = cb; rx = rx + 80
        end
        
        f.att = {}
        local y = -140
        for _, a in ipairs({"Present", "Tentative", "BENCH", "ABSENT"}) do
            local cb = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate")
            cb:SetPoint("TOPLEFT", 80, y); cb.text:SetText(a)
            cb:SetScript("OnClick", function(s) 
                for _, o in pairs(f.att) do o:SetChecked(false) end
                s:SetChecked(true); f.selStatus = a; f:CheckSave()
            end)
            f.att[a] = cb; y = y - 30
        end
        
        f.save = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); f.save:SetSize(100,25); f.save:SetPoint("BOTTOMLEFT", 30, 20); f.save:SetText("Save")
        f.cancel = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); f.cancel:SetSize(100,25); f.cancel:SetPoint("BOTTOMRIGHT", -30, 20); f.cancel:SetText("Cancel")
        f.cancel:SetScript("OnClick", function() f:Hide() end)
        
        f.CheckSave = function(s)
            if s.selRole and s.selStatus then s.save:Enable() else s.save:Disable() end
        end
    end
    
    f.info:SetText(evt.name .. " - " .. (evt.startTime or "") .. " to " .. (evt.endTime or ""))
    
    local total = (evt.counts.TANK + evt.counts.HEALER + evt.counts.DPS)
    local remain = (evt.limit or 99) - total
    f.slots:SetText("Slots: " .. math.max(0, remain))
    
    local isFull = remain <= 0
    if isFull then
        f.att["Present"]:Disable(); f.att["Present"]:SetChecked(false)
        f.att["Tentative"]:Disable(); f.att["Tentative"]:SetChecked(false)
        f.att["BENCH"]:SetChecked(true); f.selStatus = "BENCH"
    else
        f.att["Present"]:Enable(); f.att["Tentative"]:Enable()
    end
    
    f.selRole = nil; f.selStatus = nil; f.save:Disable()
    for _, c in pairs(f.roles) do c:SetChecked(false) end
    for _, c in pairs(f.att) do c:SetChecked(false) end
    if isFull then f.att["BENCH"]:SetChecked(true); f.selStatus = "BENCH" end
    
    f.save:SetScript("OnClick", function()
        local fullName = GetFullPlayerName()
        GR:RR_BroadcastSignUp(evt.id, f.selRole, f.selStatus)
        GR:RR_UpdateMemberStatus(evt.id, fullName, f.selRole, f.selStatus, GetServerTime())
        f:Hide()
    end)
end

function GR:CreateDialog(name, title, w, h)
    local f = _G[name]
    if f then f:Show(); return f end
    
    f = CreateFrame("Frame", name, UIParent, "BackdropTemplate")
    f:SetSize(w, h); f:SetPoint("CENTER")
    
    f:SetFrameStrata("FULLSCREEN_DIALOG") 
    f:SetToplevel(true)
    
    f:EnableMouse(true); f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    
    CreateBg(f, 0, 0, 0, 0.95)
    CreateBorder(f, 0.5, 0.5, 0.5, 1)
    
    f.title = f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
    f.title:SetPoint("TOP", 0, -12); f.title:SetText(title)
    
    local c = CreateFrame("Button", nil, f)
    c:SetSize(24, 24); c:SetPoint("TOPRIGHT", -5, -5)
    SkinButton(c)
    local cfs = c:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    cfs:SetPoint("CENTER"); cfs:SetText("X")
    c:SetFontString(cfs)
    c:SetScript("OnClick", function() f:Hide() end)
    
    return f
end

hooksecurefunc(GR, "SwitchView", function(self, id) if id == 6 and not GR.RR.Initialized then GR:RR_Init(); GR.RR.Initialized = true end end)


-- =========================================================================
-- 9. LITE ROSTER MODULE (CONFLICT-FREE NAMESPACE)
-- =========================================================================

GR.RR.Lite = {}
local Lite = GR.RR.Lite

Lite.RosterData = {}
Lite.Rows = {} 
Lite.ViewList = {}
Lite.SortKey = "rankIndex"
Lite.SortAsc = true
Lite.CurrentFilter = nil 
Lite.ShowOffline = false 

local LITE_ROW_HEIGHT = 24
local LITE_COL_DEF = {
    { name = "Name", width = 130, key = "name", sort = "name" },
    { name = "Rank", width = 80, key = "rank", sort = "rankIndex" }, 
    { name = "Lvl", width = 35, key = "level", sort = "level" },
    { name = "ilvl", width = 40, key = "ilvl", sort = "ilvl" },
    { name = "Key", width = 50, key = "maxKey", sort = "maxKey" },
    { name = "Score", width = 60, key = "rio", sort = "rio" },
    { name = "N", width = 55, key = "raidN", sort = "raidN" },
    { name = "H", width = 55, key = "raidH", sort = "raidH" },
    { name = "M", width = 55, key = "raidM", sort = "raidM" },
}

function Lite:Init()
    if not self.Frame then
        local f = CreateFrame("Frame", "GRS_RR_PopupFrame", UIParent)
        f:SetSize(650, 500); f:SetPoint("CENTER"); f:Hide()
        f:SetFrameStrata("FULLSCREEN_DIALOG") 
        f:EnableMouse(true); f:SetMovable(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)
        
        CreateBg(f, 0, 0, 0, 0.95)
        CreateBorder(f, 0.5, 0.5, 0.5, 1)
        
        local t = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        t:SetPoint("TOP", 0, -15); t:SetText("Available Members")
        f.title = t
        
        local c = CreateFrame("Button", nil, f)
        c:SetSize(24, 24); c:SetPoint("TOPRIGHT", -5, -5)
        SkinButton(c)
        local cfs = c:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        cfs:SetPoint("CENTER"); cfs:SetText("X")
        c:SetFontString(cfs)
        c:SetScript("OnClick", function() f:Hide() end)
        
        local sf = CreateFrame("ScrollFrame", "GRS_RR_LiteScroll", f)
        sf:SetPoint("TOPLEFT", 10, -50); sf:SetPoint("BOTTOMRIGHT", -30, 40)
        
        local slider = CreateCustomScrollbar(sf, sf)
        slider:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -50)
        slider:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 40)
        sf.slider = slider
        
        local content = CreateFrame("Frame", nil, sf); content:SetSize(600, 500)
        sf:SetScrollChild(content)
        sf:EnableMouseWheel(true)
        sf:SetScript("OnMouseWheel", function(s, d) 
            local cur = slider:GetValue()
            slider:SetValue(cur - (d * 20)) 
        end)
        
        self.Frame = f
        self.Content = content
        
        local h = CreateFrame("Frame", nil, f)
        h:SetHeight(20); h:SetPoint("TOPLEFT", 10, -30); h:SetPoint("RIGHT", -10, 0)
        local x = 0
        for _, col in ipairs(LITE_COL_DEF) do
            local b = CreateFrame("Button", nil, h)
            b:SetSize(col.width, 20); b:SetPoint("LEFT", x, 0)
            local txt = b:CreateFontString(nil,"OVERLAY","GameFontNormalSmall"); txt:SetPoint("LEFT",5,0); txt:SetText(col.name)
            if col.sort then b:SetScript("OnClick", function() Lite:Sort(col.sort) end) end
            x = x + col.width
        end
        
        local chk = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate")
        chk:SetPoint("BOTTOMLEFT", 10, 10)
        chk.text:SetText("Show Offline Members")
        chk:SetScript("OnClick", function(s) 
            Lite.ShowOffline = s:GetChecked()
            Lite:FetchData()
        end)
        chk:SetChecked(Lite.ShowOffline)
        self.OfflineChk = chk
    end
end

function Lite:ShowPopup(classFilter)
    if not self.Frame then self:Init() end
    self.CurrentFilter = classFilter
    self.Frame.title:SetText(classFilter .. " - Available")
    self.Frame:Show()
    self:FetchData() 
end

function Lite:FetchData()
    if not IsInGuild() then return {} end
    C_GuildInfo.GuildRoster()
    local num = GetNumGuildMembers()
    local temp = {}
    
    for i=1, num do
        local fullName, rank, rankIndex, level, class, zone, note, onote, online, status, classFile = GetGuildRosterInfo(i)
        
        if fullName and (online or self.ShowOffline) then
            local rioScore, ilvl, rN, rH, rM, maxKey = 0, 0, "-", "-", "-", "-"
            
            -- Raider.IO Logic
            if _G.RaiderIO and _G.RaiderIO.GetProfile then
                local name, realm = strsplit("-", fullName)
                if not realm or realm == "" then realm = GetRealmName() end
                local region = GetRegionName()
                local factionID = GetFactionID()
                
                -- 1. Try Player's Faction
                local p = _G.RaiderIO.GetProfile(name, realm, factionID)
                
                -- 2. Fallback: Try Opposite Faction
                if not p then
                    local otherFaction = (factionID == 1) and 2 or 1
                    p = _G.RaiderIO.GetProfile(name, realm, otherFaction)
                end
                
                -- 3. Fallback: Region Scan
                if not p then 
                     p = _G.RaiderIO.GetProfile(name, realm, region) 
                end
                
                if p then
                    if p.mythicKeystoneProfile then 
                        rioScore = p.mythicKeystoneProfile.currentScore or 0 
                        if p.mythicKeystoneProfile.sortedDungeons then
                            local top = p.mythicKeystoneProfile.sortedDungeons[1]
                            if top then maxKey = "+"..top.level end
                        end
                    end
                    
                    if p.gear then
                        ilvl = p.gear.item_level_equipped or 0
                    end
                    
                    if p.raidProfile and p.raidProfile.progress then
                        local currentTierPriority = 0
                        for _, prog in ipairs(p.raidProfile.progress) do
                            local rName = string.lower(prog.raid and prog.raid.name or "")
                            
                            local tier = 0
                            if string.find(rName, "manaforge") or string.find(rName, "omega") then tier = 3
                            elseif string.find(rName, "liberation") or string.find(rName, "undermine") then tier = 2
                            elseif string.find(rName, "nerub") or string.find(rName, "palace") then tier = 1 
                            end

                            if tier > 0 and tier >= currentTierPriority then
                                if tier > currentTierPriority then
                                    rN, rH, rM = "-", "-", "-"
                                    currentTierPriority = tier
                                end

                                local kills = prog.progressCount or 0
                                local total = 8
                                if prog.raid and prog.raid.bossCount then total = prog.raid.bossCount end
                                local str = kills .. "/" .. total
                                
                                if prog.difficulty == 1 then rN = str end
                                if prog.difficulty == 2 then rH = str end
                                if prog.difficulty == 3 then rM = str end
                            end
                        end
                    end
                end
            end
            
            table.insert(temp, {
                name = fullName, rank = rank, rankIndex = rankIndex, level = level,
                class = classFile, online = online, ilvl = ilvl,
                rio = rioScore, maxKey = maxKey, raidN = rN, raidH = rH, raidM = rM
            })
        end
    end
    self.RosterData = temp
    self:BuildView()
    return temp
end

function Lite:Sort(key)
    if self.SortKey == key then self.SortAsc = not self.SortAsc else self.SortKey = key; self.SortAsc = true end
    self:BuildView()
end

function Lite:BuildView()
    if not self.Content then return end

    local data = {}
    for _, d in ipairs(self.RosterData) do
        if not self.CurrentFilter or d.class == self.CurrentFilter then
            table.insert(data, d)
        end
    end
    
    table.sort(data, function(a, b)
        local valA, valB = a[self.SortKey], b[self.SortKey]
        if self.SortAsc then return valA < valB else return valA > valB end
    end)
    
    for _, r in ipairs(self.Rows) do r:Hide() end
    local y = 0
    
    for i, d in ipairs(data) do
        local r = self.Rows[i]
        if not r then
            r = CreateFrame("Button", nil, self.Content)
            r:SetSize(600, LITE_ROW_HEIGHT)
            
            local bg = r:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            r.bg = bg
            
            local hl = r:CreateTexture(nil,"HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1,1,1,0.1)
            
            r.cols = {}
            local x = 0
            for _, def in ipairs(LITE_COL_DEF) do
                local t = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall")
                t:SetPoint("LEFT", x+5, 0); t:SetWidth(def.width-10); t:SetJustifyH("LEFT")
                r.cols[def.key] = t
                x = x + def.width
            end
            self.Rows[i] = r
        end
        
        r:SetPoint("TOPLEFT", 0, -y); r:Show()
        
        if (i % 2 == 0) then r.bg:SetColorTexture(0.1, 0.1, 0.1, 0.6) 
        else r.bg:SetColorTexture(0.15, 0.15, 0.15, 0.6) end
        
        local cColor = C_ClassColor.GetClassColor(d.class) or C_ClassColor.GetClassColor("PRIEST")
        local nameText = cColor:WrapTextInColorCode(d.name)
        if not d.online then nameText = "|cff808080" .. d.name .. "|r" end
        
        r.cols["name"]:SetText(nameText)
        r.cols["rank"]:SetText(d.rank)
        r.cols["level"]:SetText(d.level)
        r.cols["ilvl"]:SetText(d.ilvl > 0 and math.floor(d.ilvl) or "-")
        
        r.cols["maxKey"]:SetText(d.maxKey)
        r.cols["rio"]:SetText(d.rio > 0 and (ScoreColor(d.rio) .. d.rio .. "|r") or "")
        
        r.cols["raidN"]:SetText(RaidProgressColor(d.raidN) .. d.raidN .. "|r")
        r.cols["raidH"]:SetText(RaidProgressColor(d.raidH) .. d.raidH .. "|r")
        r.cols["raidM"]:SetText(RaidProgressColor(d.raidM) .. d.raidM .. "|r")
        
        y = y + LITE_ROW_HEIGHT
    end
    self.Content:SetHeight(math.max(y, 10))
    if self.Frame and self.Frame.title then 
        local scrollFrame = _G["GRS_RR_LiteScroll"]
        if scrollFrame and scrollFrame.slider then
            local maxScroll = math.max(0, self.Content:GetHeight() - scrollFrame:GetHeight())
            scrollFrame.slider:SetMinMaxValues(0, maxScroll)
        end
    end
end