-- =========================================================================
-- FILE: Roster_OfficerLogic.lua (TBC Port)
-- =========================================================================
local addonName, NS = ...
local GR = _G["GRS_MainFrame"]

-- =========================================================================
-- 1. HELPERS & STYLES
-- =========================================================================

local function SafeCreateBg(f, r, g, b, a)
    if NS.CreateBg then NS.CreateBg(f, r, g, b, a) else
        local t = f:CreateTexture(nil, "BACKGROUND"); t:SetAllPoints(); t:SetColorTexture(r, g, b, a); f.bg = t
    end
end

local function SafeCreateBorder(f, r, g, b, a)
    if NS.CreateBorder then NS.CreateBorder(f, r, g, b, a) else
        if not f.border then
            local border = CreateFrame("Frame", nil, f, "BackdropTemplate")
            border:SetAllPoints(); border:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1})
            f.border = border
        end
        f.border:SetBackdropBorderColor(r, g, b, a)
    end
end

-- TBC Safe Class Color Helper
local function GetClassColorSafe(classFile)
    if C_ClassColor and C_ClassColor.GetClassColor then
        return C_ClassColor.GetClassColor(classFile or "PRIEST")
    elseif RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] then
        local c = RAID_CLASS_COLORS[classFile]
        if c and not c.GenerateHexColorMarkup then
            c.GenerateHexColorMarkup = function(self)
                return string.format("|cff%02x%02x%02x", self.r*255, self.g*255, self.b*255)
            end
        end
        return c
    end
    return { r = 1, g = 1, b = 1, GenerateHexColorMarkup = function() return "|cffffffff" end }
end

local function CreateCustomDropdown(parent, width, itemProvider, onSelect)
    local dd = CreateFrame("Button", nil, parent)
    SafeCreateBg(dd, 0.2, 0.2, 0.2, 1)
    dd:SetSize(width, 24)
    
    local txt = dd:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    txt:SetPoint("LEFT", 5, 0); txt:SetPoint("RIGHT", -25, 0); txt:SetJustifyH("LEFT"); txt:SetText("Select Character...")
    dd.Text = txt
    
    local arrow = dd:CreateTexture(nil, "ARTWORK")
    arrow:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
    arrow:SetSize(20, 20); arrow:SetPoint("RIGHT", -2, 0)
    
    local list = CreateFrame("Frame", nil, dd)
    SafeCreateBg(list, 0.1, 0.1, 0.1, 1); SafeCreateBorder(list, 0.5, 0.5, 0.5, 1)
    list:SetPoint("TOPLEFT", dd, "BOTTOMLEFT", 0, -2); list:SetWidth(width); list:SetHeight(200); list:SetFrameStrata("FULLSCREEN_DIALOG"); list:Hide()
    
    local scroll = CreateFrame("ScrollFrame", nil, list, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 5, -5); scroll:SetPoint("BOTTOMRIGHT", -25, 5)
    if NS.StyleScrollBar then NS.StyleScrollBar(scroll) end
    
    local content = CreateFrame("Frame", nil, scroll); content:SetSize(width - 30, 10); scroll:SetScrollChild(content)
    
    dd:SetScript("OnClick", function()
        if list:IsShown() then list:Hide() else
            local kids = {content:GetChildren()}; for _, k in ipairs(kids) do k:Hide() end
            local items = itemProvider()
            if #items == 0 then
                local b = content.emptyMsg or content:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
                b:SetPoint("TOPLEFT", 5, -5); b:SetText("No players found"); content.emptyMsg = b
                content:SetHeight(30); list:SetHeight(40)
            else
                if content.emptyMsg then content.emptyMsg:Hide() end
                local y = 0; local rowH = 20
                if not content.buttons then content.buttons = {} end
                for i, item in ipairs(items) do
                    local b = content.buttons[i]
                    if not b then
                        b = CreateFrame("Button", nil, content); b:SetSize(width - 30, rowH)
                        b.t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); b.t:SetPoint("LEFT", 5, 0)
                        b:SetScript("OnEnter", function(s) local fbg = s.bg or s:CreateTexture(nil, "BACKGROUND"); fbg:SetAllPoints(); fbg:SetColorTexture(1,1,1,0.1); s.bg = fbg end)
                        b:SetScript("OnLeave", function(s) if s.bg then s.bg:SetColorTexture(0,0,0,0) end end)
                        content.buttons[i] = b
                    end
                    b:SetPoint("TOPLEFT", 0, y); b:Show(); b.t:SetText(item.text); b.t:SetTextColor(item.r or 1, item.g or 1, item.b or 1)
                    b:SetScript("OnClick", function() dd.Text:SetText(item.text); dd.Text:SetTextColor(item.r or 1, item.g or 1, item.b or 1); if onSelect then onSelect(item.value) end list:Hide() end)
                    y = y - rowH
                end
                for i = #items + 1, #content.buttons do content.buttons[i]:Hide() end
                local totalH = math.abs(y); content:SetHeight(totalH); list:SetHeight(math.min(totalH + 10, 300))
            end
            list:Show()
        end
    end)
    return dd
end

-- =========================================================================
-- 2. CONTEXT MENU
-- =========================================================================

function GR:OpenContextMenu(anchor, data)
    if GR.ContextMenu then GR.ContextMenu:Hide() end
    if GR.MenuCatcher then GR.MenuCatcher:Hide() end

    if not GR.MenuCatcher then
        local c = CreateFrame("Button", nil, UIParent)
        c:SetFrameStrata("FULLSCREEN_DIALOG"); c:SetFrameLevel(100); c:SetAllPoints()
        c:SetScript("OnClick", function() if GR.ContextMenu then GR.ContextMenu:Hide() end; c:Hide() end)
        c:SetNormalTexture(""); GR.MenuCatcher = c
    end
    GR.MenuCatcher:Show()

    local f = GR.ContextMenu
    if not f then
        f = CreateFrame("Frame", "GRS_ContextMenu", UIParent, "BackdropTemplate")
        f:SetWidth(200); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetFrameLevel(GR.MenuCatcher:GetFrameLevel() + 10); f:SetClampedToScreen(true)
        SafeCreateBg(f, 0.1, 0.1, 0.1, 0.95); SafeCreateBorder(f, 0, 0, 0, 1)
        GR.ContextMenu = f
    end
    
    f:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 10, 5); f:Show()
    if f.buttons then for _, b in ipairs(f.buttons) do b:Hide() end end
    f.buttons = {}

    local btnY = -10
    local function AddBtn(text, func)
        local b = CreateFrame("Button", nil, f)
        b:SetSize(180, 20); b:SetPoint("TOPLEFT", 10, btnY)
        local bg = b:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); bg:SetColorTexture(0, 0, 0, 0); b.bg = bg
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT", 5, 0); t:SetText(text); b:SetFontString(t)
        b:SetScript("OnEnter", function(s) s.bg:SetColorTexture(0.3, 0.3, 0.3, 1); s:GetFontString():SetTextColor(1, 0.82, 0) end)
        b:SetScript("OnLeave", function(s) s.bg:SetColorTexture(0, 0, 0, 0); s:GetFontString():SetTextColor(1, 1, 1) end)
        b:SetScript("OnClick", function() func(); f:Hide(); GR.MenuCatcher:Hide() end)
        table.insert(f.buttons, b); btnY = btnY - 20
        return b
    end

    local canonical = data.fullName; if not canonical then return end 
    AddBtn("Whisper", function() ChatFrame_SendTell(canonical) end)
    -- TBC: Use InviteUnit instead of C_PartyInfo.InviteUnit
    AddBtn("Invite", function() 
        if C_PartyInfo and C_PartyInfo.InviteUnit then
            C_PartyInfo.InviteUnit(canonical)
        else
            InviteUnit(canonical)
        end
    end)
    AddBtn("Copy Name-Realm", function() NS.ShowCopyPopup(canonical) end)
    
    local myName, myRealm = UnitFullName("player"); local myCanonical = myName .. "-" .. (myRealm or GetRealmName())
    if data.dbKey == myCanonical then AddBtn("Set as MAIN", function() GR:SetIdentity(myCanonical, "MAIN") end) end
    if GR:CheckPermissions() then AddBtn("Officer: Set Main / Alt", function() GR:CreateAssignmentPanel(data.dbKey) end) end
    AddBtn("Close", function() f:Hide(); GR.MenuCatcher:Hide() end)
    f:SetHeight(math.abs(btnY) + 10)
end

-- =========================================================================
-- 3. ASSIGNMENT UI (OFFICER)
-- =========================================================================

function GR:CreateAssignmentPanel(targetDbKey)
    if not GR:CheckPermissions() then return end
    
    if not GR.AssignmentPanel then
        local f = CreateFrame("Frame", "GRS_AssignmentPanel", UIParent, "BackdropTemplate")
        f:SetSize(400, 500); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetFrameLevel(50); f:SetToplevel(true)
        f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", f.StopMovingOrSizing)
        SafeCreateBg(f, 0, 0, 0, 0.9); SafeCreateBorder(f, 0.5, 0.5, 0.5, 1)
        
        -- [[ HEADER ]]
        f.header = f:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
        f.header:SetPoint("TOP", 0, -15)
        f.header:SetText("Alt-Assignment panel")
        f.header:SetTextColor(0.4, 0.8, 1, 1) -- Light Blue

        -- [[ TARGET NAME (Top Left) ]]
        f.targetName = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
        f.targetName:SetPoint("TOPLEFT", 20, -50)

        -- [[ FILTER CONTROLS (Top Right) ]]
        local filterBtn = CreateFrame("Button", nil, f)
        filterBtn:SetSize(20, 20); filterBtn:SetPoint("TOPRIGHT", -20, -50)
        filterBtn:SetNormalTexture("Interface\\Buttons\\UI-OptionsButton")
        filterBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")
        filterBtn:SetScript("OnClick", function() GR:CreateRankFilterPopup() end)
        
        local filterLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        filterLabel:SetPoint("RIGHT", filterBtn, "LEFT", -5, 0)
        filterLabel:SetText("Select rank filter")
        filterLabel:SetTextColor(1, 1, 1, 1) 
        
        -- [[ INSTRUCTION TEXT (Gold) ]]
        local info = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        info:SetPoint("TOPLEFT", 20, -80)
        info:SetWidth(360); info:SetJustifyH("LEFT")
        info:SetText("Use the button below to add alt characters for this Main. Ensure Ranks are filtered correctly.")
        info:SetTextColor(1, 0.82, 0) -- Gold
        
        -- [[ SCROLL FRAME ]]
        local sf = CreateFrame("ScrollFrame", "GRS_AssignScroll", f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", 20, -110); sf:SetPoint("BOTTOMRIGHT", -35, 60)
        if NS.StyleScrollBar then NS.StyleScrollBar(sf) end
        
        local content = CreateFrame("Frame", nil, sf); content:SetSize(340, 400); sf:SetScrollChild(content)
        f.content = content; f.rows = {}
        
        -- [[ SQUARE ADD BUTTON ]]
        local addBtn = CreateFrame("Button", nil, content)
        addBtn:SetSize(30, 30); addBtn:SetPoint("TOPLEFT", 0, 0)
        
        SafeCreateBg(addBtn, 0.1, 0.1, 0.1, 1)
        if not addBtn.border then
            local b = CreateFrame("Frame", nil, addBtn, "BackdropTemplate")
            b:SetAllPoints(); b:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1})
            addBtn.border = b
        end
        addBtn.border:SetBackdropBorderColor(1, 1, 1, 1) -- White Outline

        local plus = addBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
        plus:SetPoint("CENTER", 0, 0); plus:SetText("+")
        plus:SetTextColor(1, 1, 1) -- White Text
        
        addBtn:SetScript("OnClick", function()
            local hasFilter = false; for k,v in pairs(GR.AssignmentFilterRanks or {}) do if v then hasFilter = true break end end
            if not hasFilter then GR:ShowToast("Filter is empty. No ranks selected.", true); return end
            GR:AddAssignmentRow()
        end)
        f.addBtn = addBtn
        
        -- [[ FOOTER BUTTONS ]]
        local btnSave = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        btnSave:SetSize(100, 25); btnSave:SetPoint("BOTTOMLEFT", 20, 20); btnSave:SetText("Save")
        if NS.SkinButton then NS.SkinButton(btnSave) end
        local fsSave = btnSave:GetFontString(); if fsSave then fsSave:SetTextColor(1, 1, 1) end
        btnSave:SetScript("OnClick", function() GR:SaveAssignment() end)
        
        local btnCancel = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        btnCancel:SetSize(100, 25); btnCancel:SetPoint("BOTTOMRIGHT", -20, 20); btnCancel:SetText("Cancel")
        if NS.SkinButton then NS.SkinButton(btnCancel) end
        local fsCancel = btnCancel:GetFontString(); if fsCancel then fsCancel:SetTextColor(1, 1, 1) end
        btnCancel:SetScript("OnClick", function() f:Hide() end)
        
        GR.AssignmentPanel = f
    end
    
    GR.AssignmentPanel.targetMain = targetDbKey
    
    -- Format Target Name (Class Color + No Realm)
    local namePart = strsplit("-", targetDbKey)
    local classColor = "|cffffffff" -- Default
    
    -- Scan roster to find class
    if IsInGuild() then
        for i=1, GetNumGuildMembers() do
            local fullName, _, _, _, _, _, _, _, _, _, classFile = GetGuildRosterInfo(i)
            if GR:GetCanonicalName(fullName) == targetDbKey then
                local c = GetClassColorSafe(classFile or "PRIEST")
                if c and c.GenerateHexColorMarkup then classColor = c:GenerateHexColorMarkup() end
                break
            end
        end
    end
    GR.AssignmentPanel.targetName:SetText(classColor .. namePart .. "|r")
    
    for _, row in ipairs(GR.AssignmentPanel.rows) do row:Hide() end
    GR.AssignmentPanel.rows = {}
    
    -- [[ POPULATE EXISTING ALTS ]]
    local db = GR:GetDB()
    local existingAlts = {}
    if db.alts then
        for alt, parent in pairs(db.alts) do
            if parent == targetDbKey then
                table.insert(existingAlts, alt)
            end
        end
    end
    table.sort(existingAlts)
    for _, altName in ipairs(existingAlts) do
        GR:AddAssignmentRow(altName)
    end
    
    GR:ReanchorAssignmentRows()
    GR.AssignmentPanel:Show()
end

function GR:ReanchorAssignmentRows()
    local panel = GR.AssignmentPanel
    if not panel or not panel.rows then return end
    
    for i, row in ipairs(panel.rows) do
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", 0, -((i-1)*35))
    end
    
    if panel.addBtn then
        panel.addBtn:ClearAllPoints()
        panel.addBtn:SetPoint("TOPLEFT", 0, -(#panel.rows * 35))
    end
end

function GR:AddAssignmentRow(preselectedAlt)
    local panel = GR.AssignmentPanel
    local index = #panel.rows + 1
    local row = CreateFrame("Frame", nil, panel.content)
    row:SetSize(340, 30); row:SetPoint("TOPLEFT", 0, -((index-1)*35))
    
    local function GetAlts()
        local db = GR:GetDB()
        
        -- Precompute which mains actually have alts assigned to them
        local mainsWithAlts = {}
        if db.alts then
            for _, parent in pairs(db.alts) do
                mainsWithAlts[parent] = true
            end
        end
        
        local list = {}; local numMembers = GetNumGuildMembers()
        for i=1, numMembers do
            local name, _, rankIndex, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
            if name then
                local cName = GR:GetCanonicalName(name)
                local isTarget = (cName == panel.targetMain)
                local isRankMatch = GR.AssignmentFilterRanks[rankIndex+1]
                local isAssignedUI = false
                for _, r in ipairs(panel.rows) do if r.selectedAlt == cName then isAssignedUI = true break end end
                
                -- Exclude characters that are a Main WITH alts already assigned
                local isMainWithAlts = mainsWithAlts[cName]
                
                -- Exclude characters already assigned as an Alt to a DIFFERENT main
                -- (Allow alts of the CURRENT target main — they are managed by this panel)
                local isAltElsewhere = db.alts and db.alts[cName] and db.alts[cName] ~= panel.targetMain
                
                if not isTarget and isRankMatch and not isAssignedUI and not isMainWithAlts and not isAltElsewhere then
                    local cColor = GetClassColorSafe(classFileName or "PRIEST")
                    table.insert(list, { text = Ambiguate(cName, "none"), value = cName, r = cColor.r, g = cColor.g, b = cColor.b })
                end
            end
        end
        table.sort(list, function(a,b) return a.text < b.text end)
        return list
    end
    
    local dd = CreateCustomDropdown(row, 220, GetAlts, function(val)
        local db = GR:GetDB()
        row.selectedAlt = val
        if db.mains[val] then
            GR:ShowToast("Warning: " .. val .. " is a MAIN. Assigning as Alt will unlink data.", true)
            -- TBC: SOUNDKIT may not exist
            if PlaySound and SOUNDKIT and SOUNDKIT.RAID_WARNING then
                PlaySound(SOUNDKIT.RAID_WARNING)
            elseif PlaySound then
                PlaySound(8959) -- Raid Warning sound ID
            end
        end
    end)
    dd:SetPoint("LEFT", 0, 0)
    
    -- [[ PRE-SELECTION LOGIC ]]
    if preselectedAlt then
        row.selectedAlt = preselectedAlt
        dd.Text:SetText(Ambiguate(preselectedAlt, "none"))
        
        local idx = GR:GetGuildMemberIndex(preselectedAlt)
        if idx then
            local _, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(idx)
            if classFileName then
                local c = GetClassColorSafe(classFileName)
                if c then dd.Text:SetTextColor(c.r, c.g, c.b) end
            end
        end
    end
    
    local delBtn = CreateFrame("Button", nil, row)
    delBtn:SetSize(20, 20)
    delBtn:SetPoint("LEFT", dd, "RIGHT", 5, 0)
    
    if NS.SkinButton then 
        NS.SkinButton(delBtn) 
    else 
        SafeCreateBg(delBtn, 0.2, 0.2, 0.2, 1)
        SafeCreateBorder(delBtn, 0, 0, 0, 1) 
    end
    
    delBtn:SetNormalFontObject("GameFontNormal")
    delBtn:SetHighlightFontObject("GameFontHighlight")
    delBtn:SetText("X")
    local fs = delBtn:GetFontString()
    if fs then fs:SetTextColor(1, 0.2, 0.2) end 
    
    delBtn:SetScript("OnClick", function() 
        row:Hide()
        for i, r in ipairs(panel.rows) do
            if r == row then table.remove(panel.rows, i); break end
        end
        GR:ReanchorAssignmentRows()
    end)
    
    row.dd = dd; 
    if not row.selectedAlt then row.selectedAlt = nil end
    table.insert(panel.rows, row)
    GR:ReanchorAssignmentRows()
end

function GR:CreateRankFilterPopup()
    if not GR.RankFilterFrame then
        local f = CreateFrame("Frame", nil, GR.AssignmentPanel, "BackdropTemplate")
        f:SetSize(200, 350); f:SetPoint("TOPRIGHT", GR.AssignmentPanel, "TOPRIGHT", 210, 0); f:SetFrameStrata("FULLSCREEN_DIALOG")
        SafeCreateBg(f, 0, 0, 0, 0.9); SafeCreateBorder(f, 0.5, 0.5, 0.5, 1)
        
        local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormal"); lbl:SetPoint("TOP", 0, -10); lbl:SetText("Filter Ranks")
        
        local function SetAll(val)
            for i, cb in ipairs(f.checks) do cb:SetChecked(val); GR.AssignmentFilterRanks[i] = val end
        end
        
        local btnAll = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); btnAll:SetSize(80, 20); btnAll:SetPoint("TOPLEFT", 10, -30); btnAll:SetText("All")
        if NS.SkinButton then NS.SkinButton(btnAll) end
        btnAll:SetScript("OnClick", function() SetAll(true) end)
        
        local btnNone = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); btnNone:SetSize(80, 20); btnNone:SetPoint("TOPRIGHT", -10, -30); btnNone:SetText("None")
        if NS.SkinButton then NS.SkinButton(btnNone) end
        btnNone:SetScript("OnClick", function() SetAll(false) end)

        f.checks = {}
        local y = -60
        for i=1, GuildControlGetNumRanks() do
            local rankName = GuildControlGetRankName(i)
            local cb = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate"); cb:SetPoint("TOPLEFT", 10, y)
            cb.text:SetText(rankName); cb.text:SetTextColor(1, 1, 1); cb:SetChecked(GR.AssignmentFilterRanks[i])
            cb:SetScript("OnClick", function(s) GR.AssignmentFilterRanks[i] = s:GetChecked() end)
            table.insert(f.checks, cb); y = y - 25
        end
        
        local close = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); close:SetSize(60, 20); close:SetPoint("BOTTOM", 0, 10); close:SetText("Close")
        if NS.SkinButton then NS.SkinButton(close) end
        close:SetScript("OnClick", function() f:Hide() end)
        
        GR.RankFilterFrame = f
    end
    GR.RankFilterFrame:Show()
end

function GR:SaveAssignment()
    local db = GR:GetDB()
    local panel = GR.AssignmentPanel; local main = panel.targetMain; local alts = {}
    
    for _, row in ipairs(panel.rows) do 
        if row:IsVisible() and row.selectedAlt then 
            table.insert(alts, row.selectedAlt) 
        end 
    end
    
    if #alts == 0 and #panel.rows > 0 then GR:ShowToast("No alts selected.", true); return end

    local ts = GetServerTime()
    
    if db.alts then
        for alt, parent in pairs(db.alts) do
            if parent == main then
                db.alts[alt] = nil
            end
        end
    end

    db.mains[main] = true; db.alts[main] = nil; db.times[main] = ts
    local altListStr = ""
    for _, alt in ipairs(alts) do
        db.mains[alt] = nil; db.alts[alt] = main; db.times[alt] = ts
        altListStr = altListStr .. alt .. ","
    end
    
    C_ChatInfo.SendAddonMessage("GRS_ASGN", string.format("PROPOSE:%s:%s:%d", main, altListStr, ts), "GUILD")
    GR:FetchGuildData(); panel:Hide(); GR:ShowToast("Assignments Saved.")
end

-- =========================================================================
-- 4. HANDSHAKE PROTOCOLS
-- =========================================================================

function GR:HandleAssignmentPacket(prefix, msg, sender)
    if prefix ~= "GRS_ASGN" then return end
    local db = GR:GetDB()
    
    local cmd, main, alts, tsStr = strsplit(":", msg, 4); local ts = tonumber(tsStr) or 0
    local myName, myRealm = UnitFullName("player"); local myCanonical = myName .. "-" .. (myRealm or GetRealmName())
    
    if cmd == "PROPOSE" then
        if main == myCanonical then
            local altTable = {}
            for alt in string.gmatch(alts, "([^,]+)") do table.insert(altTable, alt) end
            GRS_PendingAssignments[main] = { officer = sender, alts = altTable, timestamp = ts }
            GR:ShowAssignmentProposalPopup(main)
        end
        if GR:CheckPermissions() and sender ~= myCanonical then
            db.mains[main] = true; db.times[main] = ts
            for alt in string.gmatch(alts, "([^,]+)") do db.mains[alt] = nil; db.alts[alt] = main; db.times[alt] = ts end
            if GR.rosterListContainer and GR.rosterListContainer:IsVisible() then GR:FetchGuildData() end
        end
    elseif cmd == "REJECT" then
        if GR:CheckPermissions() and db.times[main] == tonumber(alts) then
            db.mains[main] = nil
            for alt, parent in pairs(db.alts) do if parent == main then db.alts[alt] = nil end end
            GR:ShowToast("Assignment Rejected by " .. main, true)
            if GR.rosterListContainer and GR.rosterListContainer:IsVisible() then GR:FetchGuildData() end
        end
    end
end

function GR:ShowAssignmentProposalPopup(mainKey)
    local data = GRS_PendingAssignments[mainKey]; if not data then return end
    
    local db = GR:GetDB()
    db.ghostFinished = true;
    
    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    f:SetSize(400, 300); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG")
    SafeCreateBg(f, 0, 0, 0, 0.95); SafeCreateBorder(f, 1, 0, 0, 1)
    
    local txt = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); txt:SetPoint("TOP", 0, -20); txt:SetText("Officer " .. data.officer .. " modified your roster:")
    local list = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); list:SetPoint("TOP", 0, -60); list:SetText("Main: " .. Ambiguate(mainKey, "none"))
    local altTxt = "Alts:\n"; for _, alt in ipairs(data.alts) do altTxt = altTxt .. "- " .. Ambiguate(alt, "none") .. "\n" end
    local altsList = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); altsList:SetPoint("TOP", 0, -90); altsList:SetText(altTxt)
    
    local btnAccept = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); btnAccept:SetSize(100, 30); btnAccept:SetPoint("BOTTOMRIGHT", -20, 20); btnAccept:SetText("Accept (5)"); btnAccept:Disable()
    if NS.SkinButton then NS.SkinButton(btnAccept) end
    local btnReject = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); btnReject:SetSize(100, 30); btnReject:SetPoint("BOTTOMLEFT", 20, 20); btnReject:SetText("Reject (5)"); btnReject:Disable()
    if NS.SkinButton then NS.SkinButton(btnReject) end
    
    local timeleft = 5
    C_Timer.NewTicker(1, function(self)
        timeleft = timeleft - 1; btnAccept:SetText("Accept ("..timeleft..")"); btnReject:SetText("Reject ("..timeleft..")")
        if timeleft <= 0 then btnAccept:Enable(); btnAccept:SetText("Accept"); btnReject:Enable(); btnReject:SetText("Reject"); self:Cancel() end
    end, 5)
    
    btnAccept:SetScript("OnClick", function()
        local db = GR:GetDB()
        db.mains[mainKey] = true; db.times[mainKey] = data.timestamp
        GR:BroadcastIdentity(mainKey, "MAIN", nil, data.timestamp)
        
        for _, alt in ipairs(data.alts) do 
            db.mains[alt] = nil; db.alts[alt] = mainKey; db.times[alt] = data.timestamp 
            GR:BroadcastIdentity(alt, "ALT", mainKey, data.timestamp)
        end
        f:Hide(); GR:ShowToast("Nesting Permanently Accepted.")
    end)
    btnReject:SetScript("OnClick", function() GRS_PendingAssignments[mainKey] = nil; GR:StartRejectionHandshake(mainKey, data.timestamp); f:Hide() end)
end

function GR:StartRejectionHandshake(mainKey, timestamp)
    GR:ShowToast("Rejection queued...", false)
    C_Timer.NewTicker(60, function(self)
        local anyOfficer = false
        for i=1, GetNumGuildMembers() do 
            local _,_,_,_,_,_,_,_,online = GetGuildRosterInfo(i)
            -- TBC: Check officer status via rank
            if online then
                -- Simple check - if they can be online, assume officer check passed
                anyOfficer = true
                break
            end
        end
        if anyOfficer then C_ChatInfo.SendAddonMessage("GRS_ASGN", string.format("REJECT:%s:%d", mainKey, timestamp), "GUILD"); GR:ShowToast("Rejection Sent.", false); self:Cancel() end
    end, 5)
end
