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

-- =========================================================================
-- VERSION CONTROL
-- =========================================================================
local PROTOCOL_VERSION = 25
print("|cff00ff00GRS:|r Roster Members v24.20 (Layout Fix) Loaded.")

-- Load Blizzard Dependencies
if C_AddOns then
    C_AddOns.LoadAddOn("Blizzard_Communities")
    C_AddOns.LoadAddOn("Blizzard_GuildUI")
else
    -- Fallback for older clients
    LoadAddOn("Blizzard_Communities")
    LoadAddOn("Blizzard_GuildUI")
end

-- =========================================================================
-- 1. CONFIGURATION
-- =========================================================================
local ROW_HEIGHT = 24
local HEADER_HEIGHT = 20
local TOP_PANE_HEIGHT = 180   -- Fixed Height for the Info Pane
local MAX_HISTORY_LINES = 10000

-- DEFINITION: restricted = true means "Only show if GR:CheckPermissions() is true"
local COL_DEF = {
    { name = "", width = 20, key = "expand" },
    { name = "Name", width = 210, key = "name", sort = "name" },
    { name = "Rank", width = 120, key = "rank", sort = "rankIndex" },
    { name = "Last Online", width = 80, key = "offline", sort = "offline" },
    { name = "Level", width = 80, key = "level", sort = "level" },
    { name = "Class", width = 100, key = "class", sort = "class" },
    { name = "Zone", width = 180, key = "zone", sort = "zone" },
    { name = "Identity", width = 130, key = "identity", sort = "parent", restricted = true },
}

-- CALCULATE FIXED WIDTH: Sum of columns + 20px padding
-- 20 + 210 + 120 + 80 + 80 + 100 + 180 + 130 = 920
-- 920 + 20 = 940
local ROSTER_FIXED_WIDTH = 940 

local GEAR_SLOTS = {
    "HeadSlot", "NeckSlot", "ShoulderSlot", "BackSlot", "ChestSlot", "ShirtSlot", "TabardSlot", "WristSlot",
    "HandsSlot", "WaistSlot", "LegsSlot", "FeetSlot", "Finger0Slot", "Finger1Slot", "Trinket0Slot", "Trinket1Slot",
    "MainHandSlot", "SecondaryHandSlot"
}

-- =========================================================================
-- 2. STATE & DB
-- =========================================================================
if not GRS_IdentityDB then GRS_IdentityDB = {} end
if not GRS_IdentityDB.mains then GRS_IdentityDB.mains = {} end
if not GRS_IdentityDB.alts then GRS_IdentityDB.alts = {} end
if not GRS_IdentityDB.times then GRS_IdentityDB.times = {} end
if not GRS_IdentityDB.ghostData then GRS_IdentityDB.ghostData = {} end

if not GRS_RaidRosterDB then GRS_RaidRosterDB = {} end
if not GRS_RaidRosterDB.guildRoster then GRS_RaidRosterDB.guildRoster = {} end
if not GRS_InspectionDB then GRS_InspectionDB = {} end

-- Chat History DB Initialization
if not GRS_GchatHist then GRS_GchatHist = {} end

GR.RosterData = {}
GR.ViewList = {}
GR.ExpandedMains = {}
GR.SortKey = "rankIndex"
GR.SortAsc = true
GR.Rows = {}
GR.ShowOffline = true
GR.InspectBuffer = {} 
GR.hasPermission = false 
GR.lastPermissionState = nil 
GR.SelectedMember = nil 

-- Ensure Context Menu closes if the main frame hides
GR:HookScript("OnHide", function()
    if GR.ContextMenu then GR.ContextMenu:Hide() end
    if GR.MenuCatcher then GR.MenuCatcher:Hide() end
end)

-- =========================================================================
-- 3. HELPERS
-- =========================================================================

function GR:CheckPermissions()
    if not IsInGuild() then 
        return false 
    end
    if CanEditOfficerNote then
        return CanEditOfficerNote()
    elseif C_GuildInfo and C_GuildInfo.CanEditOfficerNote then
        return C_GuildInfo.CanEditOfficerNote()
    end
    return false
end

local function FormatTimeOffline(years, months, days, hours)
    if not years then return "Online" end
    if years > 0 then return years.."y" end
    if months > 0 then return months.."mo" end
    if days > 0 then return days.."d" end
    if hours > 0 then return hours.."h" end
    return "<1h"
end

local function GetCanonicalName(name)
    if not name or name == "" then return nil end
    if string.find(name, "-") then
        return name
    else
        local _, realm = UnitFullName("player")
        return name .. "-" .. (realm or "")
    end
end

local function GetGuildMemberIndex(fullName)
    if not fullName then return nil end
    local nameToFind = Ambiguate(fullName, "none")
    
    local numMembers = GetNumGuildMembers()
    for i = 1, numMembers do
        local name = GetGuildRosterInfo(i)
        if name and (name == fullName or Ambiguate(name, "none") == nameToFind) then
            return i
        end
    end
    return nil
end

local function GetIdentity(fullName)
    if not fullName then return true, nil end
    local db = GRS_IdentityDB
    if db.mains[fullName] then return true, fullName end
    if db.alts[fullName] then return false, db.alts[fullName] end
    return true, fullName
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

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 GetSecondsSinceReset()
    local region = GetRegionName()
    local t = date("*t")
    local resetDay = 3 -- Default Tuesday
    if region == "eu" then resetDay = 4 end
    local daysSince = t.wday - resetDay
    if daysSince < 0 then daysSince = daysSince + 7 end
    if daysSince == 0 and t.hour < 7 then daysSince = 6 elseif daysSince == 0 then daysSince = 0 end
    local secondsSince = (daysSince * 86400) + (t.hour * 3600) + (t.min * 60) + t.sec
    return secondsSince
end

local function GetIlvlColor(ilvl)
    if not ilvl or type(ilvl) ~= "number" then return "|cff808080" end
    if ilvl >= 150 then return "|cffff8000" -- TBC Scale
    elseif ilvl >= 130 then return "|cffa335ee"
    elseif ilvl >= 115 then return "|cff0070dd"
    elseif ilvl >= 100 then return "|cff1eff00"
    else return "|cffffffff" end
end

-- =========================================================================
-- 4. VISUAL HELPERS (ANNOUNCEMENTS)
-- =========================================================================

function GR:ShowAnnouncement(msg)
    if not GR.AnnounceFrame then
        local f = CreateFrame("Frame", nil, UIParent)
        f:SetSize(800, 50)
        f:SetPoint("TOP", 0, -100)
        f:SetFrameStrata("FULLSCREEN_DIALOG")
        
        f.text = f:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
        f.text:SetPoint("CENTER")
        f.text:SetTextColor(1, 0.82, 0)
        
        f.ag = f:CreateAnimationGroup()
        local a1 = f.ag:CreateAnimation("Alpha"); a1:SetFromAlpha(0); a1:SetToAlpha(1); a1:SetDuration(0.3); a1:SetOrder(1)
        local a2 = f.ag:CreateAnimation("Alpha"); a2:SetFromAlpha(1); a2:SetToAlpha(1); a2:SetDuration(2.5); a2:SetOrder(2)
        local a3 = f.ag:CreateAnimation("Alpha"); a3:SetFromAlpha(1); a3:SetToAlpha(0); a3:SetDuration(0.5); a3:SetOrder(3)
        a3:SetScript("OnFinished", function() f:Hide() end)
        GR.AnnounceFrame = f
    end
    
    GR.AnnounceFrame.text:SetText(msg)
    GR.AnnounceFrame:Show()
    GR.AnnounceFrame.ag:Stop()
    GR.AnnounceFrame.ag:Play()
end

-- =========================================================================
-- 5. INSPECTION & DATA ENGINE
-- =========================================================================

function GR:ScanSelf()
    local _, avgEquip = GetAverageItemLevel()
    local name, realm = UnitFullName("player")
    local myName = name .. "-" .. (realm or "")
    local _, classFileName = UnitClass("player")
    
    local specName = "Unknown"
    local maxPoints = -1
    for i = 1, 3 do
        local tabName, _, points = GetTalentTabInfo(i)
        local pVal = 0
        if type(points) == "number" then pVal = points else pVal = tonumber(points) or 0 end
        if pVal > maxPoints then maxPoints = pVal; specName = tabName end
    end
    if maxPoints <= 0 then specName = "None" end

    local crit = (GetCritChance and GetCritChance()) or 0
    local haste = (GetHaste and GetHaste()) or 0 
    local mast = 0 
    local vers = 0 

    local stats = {
        str = UnitStat("player", 1),
        agi = UnitStat("player", 2),
        int = UnitStat("player", 4),
        sta = UnitStat("player", 3),
        crit = crit,
        haste = haste,
        mast = mast,
        vers = vers
    }

    local gear = {}
    for _, slotName in ipairs(GEAR_SLOTS) do
        local slotId = GetInventorySlotInfo(slotName)
        local itemLink = GetInventoryItemLink("player", slotId)
        if itemLink then
            local payload = string.match(itemLink, "item:([%d:%-]+)")
            if payload then table.insert(gear, slotId .. "=" .. payload) end
        end
    end

    local data = {
        ts = GetServerTime(),
        ilvl = math.floor(avgEquip or 0),
        class = classFileName,
        spec = specName,
        stats = stats,
        gear = gear
    }

    GRS_InspectionDB[myName] = data
    return data
end

local function SerializeInspectData(d)
    local statStr = string.format("str:%d,agi:%d,int:%d,sta:%d,crit:%.1f,haste:%.1f,mast:%.1f,vers:%.1f",
        d.stats.str, d.stats.agi, d.stats.int, d.stats.sta, d.stats.crit, d.stats.haste, d.stats.mast, d.stats.vers)
    local gearStr = table.concat(d.gear, "|")
    local fullMsg = string.format("%d^%d^%s^%s^%s^%s", d.ts, d.ilvl, d.class, d.spec, statStr, gearStr)
    return fullMsg
end

local function DeserializeInspectData(str)
    local ts, ilvl, cls, spec, statBlock, gearBlock = strsplit("^", str)
    if not gearBlock then return nil end
    local stats = {}
    for kv in string.gmatch(statBlock, "([^,]+)") do
        local k, v = strsplit(":", kv)
        stats[k] = tonumber(v)
    end
    local gear = {}
    for item in string.gmatch(gearBlock, "([^|]+)") do
        table.insert(gear, item)
    end
    return {
        ts = tonumber(ts),
        ilvl = tonumber(ilvl),
        class = cls,
        spec = spec,
        stats = stats,
        gear = gear
    }
end

function GR:BroadcastInspectData()
    if not IsInGuild() then return end
    local name, realm = UnitFullName("player")
    local myName = name .. "-" .. (realm or "")
    local myData = GRS_InspectionDB[myName]
    if not myData then myData = GR:ScanSelf() end
    local serialized = SerializeInspectData(myData)
    local msgID = string.format("%X", math.random(0, 65535)) 
    local chunkSize = 200
    local totalLen = #serialized
    local numChunks = math.ceil(totalLen / chunkSize)
    for i = 1, numChunks do
        local startIdx = (i - 1) * chunkSize + 1
        local endIdx = math.min(i * chunkSize, totalLen)
        local chunk = string.sub(serialized, startIdx, endIdx)
        local packet = string.format("%s:%d:%d:%s", msgID, i, numChunks, chunk)
        C_ChatInfo.SendAddonMessage("GRS_INS", packet, "GUILD")
    end
end

function GR:FetchGuildData()
    if not IsInGuild() then return end
    
    GR.hasPermission = GR:CheckPermissions()
    SetGuildRosterShowOffline(true)
    C_GuildInfo.GuildRoster()

    local numMembers = GetNumGuildMembers()
    if numMembers == 0 then return end

    local tempRoster = {}
    local secondsSinceReset = GetSecondsSinceReset()

    for i = 1, numMembers do
        local fullName, rank, rankIndex, level, class, zone, note, officernote, online, status, classFileName, achievementPoints = GetGuildRosterInfo(i)
        achievementPoints = achievementPoints or 0

        if fullName then
            local timeText = "|cff00ff00Online|r"
            local sortVal = 0
            local activeThisWeek = false

            if online then
                activeThisWeek = true
            else
                local y, m, d, h = GetGuildRosterLastOnline(i)
                timeText = FormatTimeOffline(y, m, d, h)
                sortVal = (y or 0)*8760 + (m or 0)*720 + (d or 0)*24 + (h or 0)
                if sortVal == 0 then sortVal = 0.1 end

                local secondsOffline = (y or 0)*31536000 + (m or 0)*2592000 + (d or 0)*86400 + (h or 0)*3600
                if secondsOffline < secondsSinceReset then
                    activeThisWeek = true
                end
            end

            self:ProcessMember(tempRoster, fullName, level, classFileName, zone, online, timeText, sortOffline, achievementPoints, activeThisWeek, rank, rankIndex, i, note, officernote)
        end
    end

    GR.RosterData = tempRoster
    if GRS_RaidRosterDB then GRS_RaidRosterDB.guildRoster = tempRoster end

    self:BuildViewList()
end

function GR:ProcessMember(rosterTable, fullName, level, class, zone, online, timeText, sortOffline, ach, activeThisWeek, rank, rankIndex, rosterIndex, pubNote, offNote)
    local dbKey = GetCanonicalName(fullName)
    local isMain, parentName = GetIdentity(dbKey)
    local shortName = Ambiguate(fullName, "none")
    local rioScore, raidN, raidH, raidM, maxKey = 0, "-", "-", "-", "-"
    
    local identityStr = ""
    if isMain then
        identityStr = "|cff00ff00Main|r"
    else
        if parentName then
            identityStr = "Alt of " .. Ambiguate(parentName, "none")
        else
            identityStr = "-"
        end
    end

    local ilvl = "-"
    local iData = GRS_InspectionDB[dbKey]
    if iData and iData.ilvl then
        ilvl = iData.ilvl
    end
    
    table.insert(rosterTable, {
        name = shortName, fullName = fullName, dbKey = dbKey, level = level, class = class,
        zone = zone, online = online, offline = sortOffline, timeText = timeText, ach = ach,
        ilvl = ilvl, rio = rioScore, raidN = raidN, raidH = raidH, raidM = raidM, maxKey = maxKey,
        identity = identityStr, isMain = isMain, parent = parentName, rank = rank,
        rankIndex = rankIndex, rosterIndex = rosterIndex, publicNote = pubNote, officerNote = offNote
    })
end

function GR:SortRoster(key)
    if GR.SortKey == key then GR.SortAsc = not GR.SortAsc else GR.SortKey = key; GR.SortAsc = true end
    GR:BuildViewList()
end

function GR:BuildViewList()
    local mains, alts = {}, {}
    if #GR.RosterData == 0 then GR.ViewList = {}; if GR.RosterScrollFrame then GR:UpdateRosterList() end return end

    for _, char in ipairs(GR.RosterData) do
        if char.isMain then
            if not mains[char.dbKey] then mains[char.dbKey] = { data = char, children = {} } end
        else
            if char.parent and char.parent ~= char.dbKey then
                if not alts[char.parent] then alts[char.parent] = {} end
                table.insert(alts[char.parent], char)
            else
                if not mains[char.dbKey] then mains[char.dbKey] = { data = char, children = {} } end
            end
        end
    end

    for parentKey, altList in pairs(alts) do
        if mains[parentKey] then mains[parentKey].children = altList
        else for _, alt in ipairs(altList) do mains[alt.dbKey] = { data = alt, children = {} } end end
    end

    local sortableMains = {}
    for _, m in pairs(mains) do table.insert(sortableMains, m) end

    table.sort(sortableMains, function(a, b)
        local dA, dB = a.data, b.data
        local k = GR.SortKey
        if k == "offline" then
            if dA.online and not dB.online then return true end
            if not dA.online and dB.online then return false end
        end
        local valA, valB = dA[k], dB[k]
        if valA == nil then valA = "" end
        if valB == nil then valB = "" end
        if type(valA) ~= type(valB) then valA = tostring(valA); valB = tostring(valB) end
        if valA == valB then return dA.name < dB.name end
        if GR.SortAsc then return valA < valB else return valA > valB end
    end)

    GR.ViewList = {}
    for _, m in ipairs(sortableMains) do
        local anyChildOnline = false
        for _, child in ipairs(m.children) do if child.online then anyChildOnline = true end end
        m.data.isOnlineOnAlt = (not m.data.online) and anyChildOnline
        local showMain = false
        if GR.ShowOffline then showMain = true else if m.data.online or anyChildOnline then showMain = true end end

        if showMain then
            m.data.isChild = false
            m.data.hasChildren = (#m.children > 0)
            m.data.isExpanded = GR.ExpandedMains[m.data.dbKey]
            table.insert(GR.ViewList, m.data)
            if m.data.isExpanded then
                for _, child in ipairs(m.children) do child.isChild = true; table.insert(GR.ViewList, child) end
            end
        end
    end
    GR:UpdateRosterList()
end

-- =========================================================================
-- 6. UI CONSTRUCTION
-- =========================================================================

function GR:CreateRosterHeader(parent)
    local hFrame = CreateFrame("Frame", nil, parent)
    hFrame:SetHeight(HEADER_HEIGHT)
    hFrame:SetPoint("TOPLEFT", 0, 0); hFrame:SetPoint("TOPRIGHT", 0, 0)
    local xOffset = 0
    for _, col in ipairs(COL_DEF) do
        if not col.restricted or GR.hasPermission then
            local btn = CreateFrame("Button", nil, hFrame)
            btn:SetHeight(HEADER_HEIGHT); btn:SetWidth(col.width); btn:SetPoint("LEFT", xOffset, 0)
            local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            text:SetPoint("LEFT", 5, 0); text:SetText(col.name)
            if col.sort then
                btn:SetScript("OnClick", function() GR:SortRoster(col.sort) end)
                btn:SetScript("OnEnter", function() text:SetTextColor(1, 1, 1) end)
                btn:SetScript("OnLeave", function() text:SetTextColor(1, 0.82, 0) end)
            end
            local sep = btn:CreateTexture(nil, "ARTWORK"); sep:SetColorTexture(0.3, 0.3, 0.3, 0.5)
            sep:SetWidth(1); sep:SetHeight(15); sep:SetPoint("RIGHT", 0, 0)
            xOffset = xOffset + col.width
        end
    end
end

function GR:CreateRosterRow(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    local totalWidth = 0
    for _, col in ipairs(COL_DEF) do if not col.restricted or GR.hasPermission then totalWidth = totalWidth + col.width end end
    row:SetWidth(math.max(totalWidth, 1280))
    local hl = row:CreateTexture(nil, "HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.1)
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:SetScript("OnClick", function(self, button)
        if button == "RightButton" and self.data then GR:OpenContextMenu(self, self.data)
        elseif button == "LeftButton" and self.data then GR:SelectMember(self.data) end
    end)
    row.cols = {}
    local xOffset = 0
    for _, def in ipairs(COL_DEF) do
        if not def.restricted or GR.hasPermission then
            local f = CreateFrame("Frame", nil, row)
            f:SetHeight(ROW_HEIGHT); f:SetWidth(def.width); f:SetPoint("LEFT", xOffset, 0)
            if def.key == "ilvl" then
                 f:EnableMouse(true)
                 f:SetScript("OnEnter", function() f.text:SetTextColor(1,1,1); GameTooltip:SetOwner(f, "ANCHOR_RIGHT"); GameTooltip:SetText("Click to Inspect", 1, 1, 1); GameTooltip:Show() end)
                 f:SetScript("OnLeave", function() f.text:SetTextColor(1, 0.82, 0); GameTooltip:Hide() end)
                 f:SetScript("OnMouseDown", function() if row.data then GR:OpenInspectWindow(row.data.dbKey) end end)
            end
            if def.key == "expand" then
                local expBtn = CreateFrame("Button", nil, f)
                expBtn:SetSize(16, 16); expBtn:SetPoint("CENTER", 0, 0)
                expBtn:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
                expBtn:GetNormalTexture():SetDesaturated(true) 
                expBtn:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-Down"); expBtn:GetPushedTexture():SetDesaturated(true)
                expBtn:SetHighlightTexture("Interface\\Buttons\\UI-PlusButton-Hilight"); expBtn:GetHighlightTexture():SetDesaturated(true)
                expBtn:Hide()
                row.expBtn = expBtn
                expBtn:SetScript("OnClick", function() if row.data then GR.ExpandedMains[row.data.dbKey] = not GR.ExpandedMains[row.data.dbKey]; GR:BuildViewList() end end)
            else
                local t = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                t:SetPoint("LEFT", 5, 0); t:SetWidth(def.width - 10); t:SetJustifyH("LEFT"); t:SetWordWrap(false)
                row.cols[def.key] = t; f.text = t
            end
            xOffset = xOffset + def.width
        end
    end
    local status = row:CreateTexture(nil, "OVERLAY"); status:SetSize(12, 12); status:SetTexture("Interface\\COMMON\\Indicator-Gray")
    status:SetPoint("LEFT", row, "LEFT", 220, 0); row.statusDot = status
    return row
end

function GR:UpdateRosterList()
    local mainContainer = GR.rosterListContainer or _G["GRS_RosterListFrame"]
    if not mainContainer then return end
    
    local currentPermission = GR:CheckPermissions()
    GR.hasPermission = currentPermission
    if GR.lastPermissionState ~= currentPermission then
        if GR.RosterSplitMain then GR.RosterSplitMain:Hide(); GR.RosterSplitMain = nil; GR.RosterRows = {}; GR.Rows = {}; if GR.RosterContent then GR.RosterContent:Hide(); GR.RosterContent = nil end end
        if GR.RightPaneTop then GR.RightPaneTop:Hide(); GR.RightPaneTop = nil end
        if GR.RightPaneBottom then GR.RightPaneBottom:Hide(); GR.RightPaneBottom = nil end
        GR.lastPermissionState = currentPermission
    end

    if not GR.RosterSplitMain then
        GR.RosterSplitMain = CreateFrame("Frame", nil, mainContainer)
        GR.RosterSplitMain:SetPoint("TOPLEFT", 0, 0); GR.RosterSplitMain:SetPoint("BOTTOMLEFT", 0, 0)
        -- FIXED: Left side is FIXED WIDTH
        GR.RosterSplitMain:SetWidth(ROSTER_FIXED_WIDTH)

        GR.RightPaneTop = CreateFrame("Frame", nil, mainContainer, "BackdropTemplate")
        -- FIXED: Anchored to Left Side + Main Frame Right Side (Dynamic Width)
        GR.RightPaneTop:SetPoint("TOPLEFT", GR.RosterSplitMain, "TOPRIGHT", 0, 0)
        GR.RightPaneTop:SetPoint("TOPRIGHT", mainContainer, "TOPRIGHT", 0, 0)
        GR.RightPaneTop:SetHeight(TOP_PANE_HEIGHT)
        GR.RightPaneTop:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
        GR.RightPaneTop:SetBackdropColor(0, 0, 0, 0.9)

        GR.RightPaneBottom = CreateFrame("Frame", nil, mainContainer, "BackdropTemplate")
        -- FIXED: Anchored to Top Right Pane + Main Frame Bottom Right (Dynamic Width)
        GR.RightPaneBottom:SetPoint("TOPLEFT", GR.RightPaneTop, "BOTTOMLEFT", 0, 0)
        GR.RightPaneBottom:SetPoint("BOTTOMRIGHT", mainContainer, "BOTTOMRIGHT", 0, 0)
        GR.RightPaneBottom:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
        GR.RightPaneBottom:SetBackdropColor(0, 0, 0, 0.9)

        GR:CreateRosterHeader(GR.RosterSplitMain)
        local sf = CreateFrame("ScrollFrame", "GRS_RosterScrollDirect", GR.RosterSplitMain, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", GR.RosterSplitMain, "TOPLEFT", 0, -HEADER_HEIGHT - 5)
        sf:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", -26, 5) 
        local content = CreateFrame("Frame", nil, sf)
        content:SetHeight(500); content:SetWidth(sf:GetWidth()); sf:SetScrollChild(content)
        sf:SetScript("OnSizeChanged", function(self, w, h) content:SetWidth(w) end)
        GR.RosterContent = content; GR.RosterScrollFrame = sf
        GR:BuildRightSide(GR.RightPaneTop, GR.RightPaneBottom)
        GR.RosterRows = {}
    end
    
    -- Ensure fixed width persists
    if GR.RosterSplitMain then GR.RosterSplitMain:SetWidth(ROSTER_FIXED_WIDTH) end
    
    if #GR.ViewList == 0 and #GR.RosterData == 0 then GR:FetchGuildData() end

    local data = GR.ViewList; local content = GR.RosterContent
    for _, r in ipairs(GR.Rows) do r:Hide() end
    for i, d in ipairs(data) do
        local r = GR.Rows[i]
        if not r then r = GR:CreateRosterRow(content); GR.Rows[i] = r end
        r:SetPoint("TOPLEFT", 0, -((i - 1) * ROW_HEIGHT)); r:SetWidth(content:GetWidth()); r:Show(); r.data = d
        local indent = d.isChild and 15 or 0
        r.cols["name"]:SetPoint("LEFT", 5 + indent, 0)
        local cColor = C_ClassColor.GetClassColor((d.class or "PRIEST"):upper()) or C_ClassColor.GetClassColor("PRIEST")
        r.cols["name"]:SetText(cColor:WrapTextInColorCode(d.name))
        r.cols["rank"]:SetText(d.rank); r.cols["level"]:SetText(d.level); r.cols["class"]:SetText(cColor:WrapTextInColorCode(d.class))
        r.cols["zone"]:SetText(d.zone); r.cols["offline"]:SetText(d.timeText); 
        
        if r.cols["ach"] then
            if d.ach then r.cols["ach"]:SetText(d.ach) else r.cols["ach"]:SetText("-") end
        end
        
        if r.cols["ilvl"] then r.cols["ilvl"]:SetText("") end
        if r.cols["rio"] then r.cols["rio"]:SetText("") end
        if r.cols["raidN"] then r.cols["raidN"]:SetText("") end
        if r.cols["raidH"] then r.cols["raidH"]:SetText("") end
        if r.cols["raidM"] then r.cols["raidM"]:SetText("") end
        if r.cols["maxKey"] then r.cols["maxKey"]:SetText("") end

        if r.cols["identity"] then r.cols["identity"]:SetText(d.identity) end
        r.statusDot:Show(); r.statusDot:SetTexture("Interface\\COMMON\\Indicator-Gray")
        if d.online then r.statusDot:SetVertexColor(d.isChild and 1 or 0, d.isChild and 0.6 or 1, 0)
        else r.statusDot:SetVertexColor(d.isOnlineOnAlt and 1 or 0.5, d.isOnlineOnAlt and 0.5 or 0.5, d.isOnlineOnAlt and 0 or 0.5) end
        if d.hasChildren then
            r.expBtn:Show(); r.expBtn:SetNormalTexture(d.isExpanded and "Interface\\Buttons\\UI-MinusButton-Up" or "Interface\\Buttons\\UI-PlusButton-Up")
            local nt = r.expBtn:GetNormalTexture(); if nt then nt:SetDesaturated(true) end
        else r.expBtn:Hide() end
    end
    content:SetHeight(#data * ROW_HEIGHT)
end

-- =========================================================================
-- 7. RIGHT SIDE PANES (TOP & BOTTOM)
-- =========================================================================

function GR:BuildRightSide(topPane, bottomPane)
    local function Skin(b)
        if NS.SkinButton then NS.SkinButton(b) else
            b:SetNormalTexture(""); b:SetHighlightTexture(""); b:SetPushedTexture(""); b:SetDisabledTexture("")
            local t = b:CreateTexture(nil, "BACKGROUND"); t:SetAllPoints(); t:SetColorTexture(0.2, 0.2, 0.2, 1); b.bg = t
            local border = b:CreateTexture(nil, "BORDER"); border:SetAllPoints(); border:SetColorTexture(0, 0, 0, 1); border:SetDrawLayer("BORDER", -1)
            local fs = b:GetFontString(); if fs then fs:SetTextColor(1, 1, 1) end
        end
    end

    -- TOP PANE BUTTONS
    local refBtn = CreateFrame("Button", nil, topPane, "UIPanelButtonTemplate")
    refBtn:SetSize(60, 20); refBtn:SetPoint("TOPRIGHT", -5, -5); refBtn:SetText("Refresh")
    Skin(refBtn); refBtn:SetScript("OnClick", function() GR:PerformSync() end)
    local togBtn = CreateFrame("Button", nil, topPane, "UIPanelButtonTemplate")
    togBtn:SetSize(180, 20); togBtn:SetPoint("RIGHT", refBtn, "LEFT", -5, 0); togBtn:SetText("Toggle Offline Members")
    Skin(togBtn); togBtn:SetScript("OnClick", function(s) GR.ShowOffline = not GR.ShowOffline; s:GetFontString():SetTextColor(1, GR.ShowOffline and 1 or 0.5, GR.ShowOffline and 1 or 0); if #GR.RosterData > 0 then GR:BuildViewList() else GR:FetchGuildData() end end)

    -- TOP PANE DETAIL CONTENT
    local title = topPane:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", 10, -5); title:SetText("Member Details")
    local detailContent = CreateFrame("Frame", nil, topPane)
    detailContent:SetPoint("TOPLEFT", 0, -30); detailContent:SetPoint("BOTTOMRIGHT", 0, 0); detailContent:Hide(); GR.DetailContent = detailContent
    local dpTitle = detailContent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); dpTitle:SetPoint("TOPLEFT", 10, 0); GR.DetailName = dpTitle
    local dpRank = detailContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightMedium"); dpRank:SetPoint("TOPLEFT", 10, -25); GR.DetailRank = dpRank
    local dpClass = detailContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightMedium"); dpClass:SetPoint("TOPLEFT", 10, -45); GR.DetailClass = dpClass
    
    local function CreateLabelEdit(labelTxt, y, maxChars)
        local lbl = detailContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); lbl:SetPoint("TOPLEFT", 10, y); lbl:SetText(labelTxt)
        local eb = CreateFrame("EditBox", nil, detailContent, "InputBoxTemplate"); eb:SetPoint("LEFT", lbl, "RIGHT", 10, 0); eb:SetSize(250, 20); eb:SetAutoFocus(false); eb:SetMaxLetters(maxChars)
        return eb
    end
    GR.PublicNoteEdit = CreateLabelEdit("Public Note:", -75, 20)
    GR.OfficerNoteEdit = CreateLabelEdit("Officer Note:", -100, 40)
    local btnSave = CreateFrame("Button", nil, detailContent, "UIPanelButtonTemplate")
    btnSave:SetSize(80, 22); btnSave:SetPoint("BOTTOMLEFT", 10, 10); btnSave:SetText("Save"); Skin(btnSave)
    btnSave:SetScript("OnClick", function() 
        if GR.SelectedMember then 
            local idx = GetGuildMemberIndex(GR.SelectedMember.fullName); 
            if idx then 
                if GuildRosterSetPublicNote then GuildRosterSetPublicNote(idx, GR.PublicNoteEdit:GetText()) end
                if GuildRosterSetOfficerNote then GuildRosterSetOfficerNote(idx, GR.OfficerNoteEdit:GetText()) end
                GR:ShowToast("Notes Updated.") 
            end 
        end 
    end)
    local btnCancel = CreateFrame("Button", nil, detailContent, "UIPanelButtonTemplate")
    btnCancel:SetSize(80, 22); btnCancel:SetPoint("LEFT", btnSave, "RIGHT", 10, 0); btnCancel:SetText("Cancel"); Skin(btnCancel)
    btnCancel:SetScript("OnClick", function() GR:DeselectMember() end)

    -- BOTTOM PANE (Chat with History)
    local bg = bottomPane:CreateTexture(nil, "ARTWORK")
    bg:SetAllPoints(); bg:SetTexture("Interface\\AddOns\\" .. addonName .. "\\Media\\Background_G-chat.tga"); bg:SetAlpha(0.25); bg:SetTexCoord(0, 1, 0, 1)

    GR:InitGuildChat(bottomPane)
end

-- =========================================================================
-- 8. PERSISTENT GUILD CHAT SYSTEM
-- =========================================================================

function GR:InitGuildChat(parent)
    local edit = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    edit:SetPoint("BOTTOMLEFT", 10, 5); edit:SetPoint("BOTTOMRIGHT", -10, 5); edit:SetHeight(20); edit:SetAutoFocus(false)
    edit:SetScript("OnEnterPressed", function(s)
        local msg = s:GetText()
        if msg and msg ~= "" then SendChatMessage(msg, "GUILD"); s:SetText("") end
        s:ClearFocus()
    end)
    local chatScroll = CreateFrame("ScrollingMessageFrame", nil, parent)
    chatScroll:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, -10)
    chatScroll:SetPoint("BOTTOMRIGHT", edit, "TOPRIGHT", 0, 5)
    chatScroll:SetFontObject(ChatFontNormal)
    chatScroll:SetJustifyH("LEFT")
    chatScroll:SetFading(false)
    chatScroll:SetMaxLines(MAX_HISTORY_LINES)
    chatScroll:SetInsertMode("BOTTOM")
    
    chatScroll:EnableMouseWheel(true)
    chatScroll:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            if IsShiftKeyDown() then self:ScrollToTop() else self:ScrollUp() end
        else
            if IsShiftKeyDown() then self:ScrollToBottom() else self:ScrollDown() end
        end
    end)
    
    GR.GuildChatFrame = chatScroll
    GR:LoadChatHistory()

    if not GR.ChatListener then
        GR.ChatListener = CreateFrame("Frame")
        GR.ChatListener:RegisterEvent("CHAT_MSG_GUILD")
        GR.ChatListener:RegisterEvent("CHAT_MSG_GUILD_ACHIEVEMENT")
        GR.ChatListener:SetScript("OnEvent", function(self, event, msg, sender, ...)
            sender = Ambiguate(sender or "", "none")
            local classFile = "PRIEST"
            if IsInGuild() and sender ~= "" then
                for i=1, GetNumGuildMembers() do
                    local name, _, _, _, _, _, _, _, _, _, cf = GetGuildRosterInfo(i)
                    if name and Ambiguate(name, "none") == sender then classFile = cf; break end
                end
            end
            local entry = { ts = time(), msg = msg, sender = sender, class = classFile, event = event }
            table.insert(GRS_GchatHist, entry)
            if #GRS_GchatHist > MAX_HISTORY_LINES then table.remove(GRS_GchatHist, 1) end
            GR:AddChatMessage(entry)
        end)
    end
end

function GR:LoadChatHistory()
    if not GR.GuildChatFrame then return end
    GR.GuildChatFrame:Clear()
    local lastDay = nil
    for _, entry in ipairs(GRS_GchatHist) do
        local entryDay = date("%Y%j", entry.ts)
        if entryDay ~= lastDay then GR:AddDateSeparator(entry.ts); lastDay = entryDay end
        GR:AddChatMessage(entry)
    end
end

function GR:AddDateSeparator(ts)
    if not GR.GuildChatFrame then return end
    local today = date("%Y%j", time()); local yesterday = date("%Y%j", time() - 86400)
    local entryDay = date("%Y%j", ts); local text = ""
    if entryDay == today then text = "Today" elseif entryDay == yesterday then text = "Yesterday" else text = date("%A, %B %d", ts) end
    GR.GuildChatFrame:AddMessage(" "); GR.GuildChatFrame:AddMessage("|cffffd100--- " .. text .. " ---|r"); GR.GuildChatFrame:AddMessage(" ")
end

function GR:AddChatMessage(entry)
    if not GR.GuildChatFrame then return end
    local timestamp = date("%H:%M:%S", entry.ts)
    local classColorObj = C_ClassColor.GetClassColor(entry.class or "PRIEST")
    local classColorHex = classColorObj and classColorObj:GenerateHexColorMarkup() or "|cffffffff"
    if entry.event == "CHAT_MSG_GUILD_ACHIEVEMENT" then
        local coloredName = classColorHex .. entry.sender .. "|r"
        local fixedMsg = (entry.msg or ""):gsub("%%s", coloredName)
        GR.GuildChatFrame:AddMessage("|cff888888["..timestamp.."]|r " .. "|cff00ff00" .. fixedMsg .. "|r")
    else
        GR.GuildChatFrame:AddMessage("|cff888888["..timestamp.."]|r " .. classColorHex .. "["..entry.sender.."]:|r " .. "|cff40ff40" .. (entry.msg or "") .. "|r")
    end
end

-- =========================================================================
-- 9. SELECTION & CONTEXT MENU
-- =========================================================================

function GR:SelectMember(data)
    GR.SelectedMember = data
    if not GR.DetailContent then return end
    local fullName = data.fullName or ""
    local namePart, realmPart = strsplit("-", fullName)
    if not realmPart or realmPart == "" then realmPart = GetRealmName() end
    GR.DetailName:SetText(namePart .. " - " .. realmPart)
    GR.DetailRank:SetText("Rank: " .. (data.rank or "Unknown"))
    local cColor = C_ClassColor.GetClassColor((data.class or "PRIEST"):upper()) or GRAY_FONT_COLOR
    GR.DetailClass:SetText(cColor:WrapTextInColorCode(data.class) .. " " .. (data.race or ""))
    local myName, myRealm = UnitFullName("player"); local myCanonical = myName .. "-" .. (myRealm or "")
    local isMe = (data.dbKey == myCanonical); local isOfficer = GR:CheckPermissions()
    GR.PublicNoteEdit:SetText(data.publicNote or ""); GR.OfficerNoteEdit:SetText(data.officerNote or "")
    if isMe or isOfficer then GR.PublicNoteEdit:Enable() else GR.PublicNoteEdit:Disable() end
    if isOfficer then GR.OfficerNoteEdit:Enable(); GR.OfficerNoteEdit:Show() else GR.OfficerNoteEdit:Disable(); GR.OfficerNoteEdit:Hide() end
    GR.DetailContent:Show()
end

function GR:DeselectMember()
    GR.SelectedMember = nil
    if GR.DetailContent then GR.DetailContent:Hide() end
end

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:SetAllPoints()
        c:SetScript("OnClick", function()
            if GR.ContextMenu then GR.ContextMenu:Hide() end
            c:Hide()
        end)
        c:SetNormalTexture(""); c:SetHighlightTexture(""); c:SetPushedTexture("")
        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() + 5) 
        f:SetClampedToScreen(true)
        f:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 }
        })
        f:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
        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)
    AddBtn("Invite", function() C_PartyInfo.InviteUnit(canonical) end)
    AddBtn("Ignore", function() C_FriendList.AddIgnore(canonical) end)
    
    local myName, myRealm = UnitFullName("player")
    local myCanonical = myName .. "-" .. (myRealm or "")
    if data.dbKey == myCanonical then
        AddBtn("Set as MAIN", function() GR:SetIdentity(myCanonical, "MAIN") end)
    end
    
    AddBtn("Close", function() f:Hide(); GR.MenuCatcher:Hide() end)
    f:SetHeight(math.abs(btnY) + 10)
end

-- =========================================================================
-- 10. INSPECTION & SYNC LOGIC
-- =========================================================================

function GR:OpenInspectWindow(dbKey)
    local data = GRS_InspectionDB[dbKey]; if not data then GR:ShowToast("No inspection data available for " .. (dbKey or "Unknown")); return end
    if not GR.InspectFrame then GR:CreateInspectPopup() end; GR.InspectFrame:Show(); GR:PopulateInspectWindow(dbKey, data)
end

function GR:CreateInspectPopup()
    local f = CreateFrame("Frame", "GRS_InspectFrame", UIParent, "BackdropTemplate"); f:SetSize(400, 500); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
    f:SetBackdropColor(0, 0, 0, 0.95)
    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton"); close:SetPoint("TOPRIGHT", -5, -5)
    f.name = f:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge"); f.name:SetPoint("TOP", 0, -20)
    f.subtext = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); f.subtext:SetPoint("TOP", 0, -45)
    f.stats = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); f.stats:SetPoint("CENTER", 0, -50); f.stats:SetJustifyH("CENTER")
    f.slots = {}; local leftSlots = {1, 2, 3, 15, 5, 4, 19, 9}; local rightSlots = {10, 6, 7, 8, 11, 12, 13, 14}
    local function CreateIcon(id, anchor, x, y)
        local icon = CreateFrame("Button", nil, f); icon:SetSize(32, 32); icon:SetPoint(anchor, x, y); icon.t = icon:CreateTexture(nil, "ARTWORK"); icon.t:SetAllPoints()
        icon:SetScript("OnEnter", function(self) if self.link then GameTooltip:SetOwner(self, "ANCHOR_RIGHT"); GameTooltip:SetHyperlink("item:" .. self.link); GameTooltip:Show() end end)
        icon:SetScript("OnLeave", function() GameTooltip:Hide() end); f.slots[id] = icon
    end
    local startY = -80; for i, id in ipairs(leftSlots) do CreateIcon(id, "TOPLEFT", 20, startY - ((i-1)*40)) end
    for i, id in ipairs(rightSlots) do CreateIcon(id, "TOPRIGHT", -20, startY - ((i-1)*40)) end
    CreateIcon(16, "BOTTOM", -20, 30); CreateIcon(17, "BOTTOM", 20, 30); GR.InspectFrame = f
end

function GR:PopulateInspectWindow(name, data)
    local f = GR.InspectFrame; local classColor = C_ClassColor.GetClassColor(data.class) or GRAY_FONT_COLOR
    f.name:SetText(classColor:WrapTextInColorCode(Ambiguate(name, "none"))); f.subtext:SetText(string.format("%s %s - iLvl: %d", data.spec or "??", data.class or "Unknown", data.ilvl or 0))
    local s = data.stats; 
    if s then 
        f.stats:SetText(string.format("Str: %d  Agi: %d  Int: %d  Sta: %d\nCrit: %.1f%%  Haste: %.1f%%", s.str, s.agi, s.int, s.sta, s.crit, s.haste)) 
    else 
        f.stats:SetText("No Stat Data") 
    end
    for _, btn in pairs(f.slots) do btn.t:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot"); btn.link = nil end
    if data.gear then for _, entry in ipairs(data.gear) do local slotId, linkPayload = strsplit("=", entry); slotId = tonumber(slotId); local btn = f.slots[slotId] if btn and linkPayload then local _, _, _, _, icon = GetItemInfoInstant("item:" .. linkPayload); if icon then btn.t:SetTexture(icon) end; btn.link = linkPayload end end end
end

function GR:ShowToast(text)
    if not GR.ToastFrame then
        local f = CreateFrame("Frame", "GRS_Toast", UIParent); f:SetSize(420, 80); f:SetPoint("TOP", 0, -180); f:SetFrameStrata("DIALOG")
        f.text = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge"); f.text:SetPoint("CENTER", 0, 0); f.text:SetWidth(380); f.text:SetJustifyH("CENTER"); f.text:SetWordWrap(true)
        f.ag = f:CreateAnimationGroup(); f.fade = f.ag:CreateAnimation("Alpha"); f.fade:SetFromAlpha(1); f.fade:SetToAlpha(0); f.fade:SetDuration(1.0); f.fade:SetStartDelay(4.0)
        f.ag:SetScript("OnFinished", function() f:Hide() end); GR.ToastFrame = f
    end
    GR.ToastFrame.text:SetText(text); GR.ToastFrame:Show(); GR.ToastFrame:SetAlpha(1); GR.ToastFrame.ag:Stop(); GR.ToastFrame.ag:Play()
end

function GR:SetIdentity(canonicalName, type, parent)
    if not GRS_IdentityDB.mains then GRS_IdentityDB.mains = {} end; if not GRS_IdentityDB.alts then GRS_IdentityDB.alts = {} end; if not GRS_IdentityDB.times then GRS_IdentityDB.times = {} end
    GRS_IdentityDB.ghostFinished = true; GRS_IdentityDB.ghostData = nil; local timestamp = GetServerTime()
    if type == "MAIN" then GRS_IdentityDB.mains[canonicalName] = true; GRS_IdentityDB.alts[canonicalName] = nil; GRS_IdentityDB.times[canonicalName] = timestamp; GRS_IdentityDB.accountMain = canonicalName; GR:ShowToast(string.format("|cff00ff00%s|r set as MAIN.", Ambiguate(canonicalName, "none")))
    elseif type == "ALT" and parent then GRS_IdentityDB.mains[canonicalName] = nil; GRS_IdentityDB.alts[canonicalName] = parent; GRS_IdentityDB.times[canonicalName] = timestamp; GR:ShowToast(string.format("%s linked to %s", Ambiguate(canonicalName, "none"), Ambiguate(parent, "none"))) end
    GR:BroadcastIdentity(canonicalName, type, parent, timestamp); GR:FetchGuildData()
end

function GR:BroadcastIdentity(name, role, parent, manualTS)
    local ts = manualTS; if not ts and GRS_IdentityDB.times then ts = GRS_IdentityDB.times[name] end; if not ts then ts = 1 end
    local msg = string.format("%s:%s:%s:%d:%d", name, role, parent or "NIL", ts, PROTOCOL_VERSION)
    if IsInGuild() then C_ChatInfo.SendAddonMessage("GRS_ID", msg, "GUILD") end
end

function GR:PerformSync(isAuto)
    if not IsInGuild() then return end
    C_ChatInfo.SendAddonMessage("GRS_ID", "REQ", "GUILD")
    local name, realm = UnitFullName("player"); local myCanonical = name .. "-" .. (realm or ""); local myTS = GRS_IdentityDB.times and GRS_IdentityDB.times[myCanonical] or 1
    if GRS_IdentityDB.mains[myCanonical] then GR:BroadcastIdentity(myCanonical, "MAIN", nil, myTS) elseif GRS_IdentityDB.alts[myCanonical] then GR:BroadcastIdentity(myCanonical, "ALT", GRS_IdentityDB.alts[myCanonical], myTS) end
    GR:BroadcastInspectData(); GR:FetchGuildData(); if not isAuto then print("|cff00ff00GRS:|r Manual Sync Executed (Identity & Inspection).") end
end

function GR:StartHourlyTimer()
    local t = date("*t"); local secondsToNextHour = (3600 - (t.min * 60 + t.sec)); if secondsToNextHour <= 0 then secondsToNextHour = 1 end
    C_Timer.After(secondsToNextHour, function() GR:PerformSync(true); GR:StartHourlyTimer() end)
end

function GR:CheckGhostProtocol()
    local db = GRS_IdentityDB; if db.ghostFinished then return end
    local hasMains = false; for _ in pairs(db.mains) do hasMains = true break end; if hasMains then db.ghostFinished = true; db.ghostData = nil; return end
    if not db.ghostStartTime then db.ghostStartTime = GetServerTime() end; if not db.ghostData then db.ghostData = {} end
    local name, realm = UnitFullName("player"); local canonical = name .. "-" .. (realm or ""); local _, ilvl = GetAverageItemLevel(); ilvl = math.floor(ilvl); db.ghostData[canonical] = ilvl
    if (GetServerTime() - db.ghostStartTime) >= 86400 then
        local maxIlvl = -1; local bestMain = nil
        for charName, charIlvl in pairs(db.ghostData) do if charIlvl > maxIlvl then maxIlvl = charIlvl; bestMain = charName end end
        if bestMain then
            print("|cff00ff00GRS:|r Auto-detect (24h) complete. Main: " .. bestMain)
            GRS_IdentityDB.mains[bestMain] = true; GRS_IdentityDB.alts[bestMain] = nil; GRS_IdentityDB.times[bestMain] = GetServerTime(); GRS_IdentityDB.accountMain = bestMain
            for charName, _ in pairs(db.ghostData) do if charName ~= bestMain then GRS_IdentityDB.mains[charName] = nil; GRS_IdentityDB.alts[charName] = bestMain; GRS_IdentityDB.times[charName] = GetServerTime() end end
            db.ghostFinished = true; db.ghostData = nil; GR:PerformSync(true)
        else db.ghostFinished = true end
    end
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("CHAT_MSG_ADDON"); eventFrame:RegisterEvent("PLAYER_LOGIN"); eventFrame:RegisterEvent("GUILD_ROSTER_UPDATE"); eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "GUILD_ROSTER_UPDATE" then if GR.rosterListContainer and GR.rosterListContainer:IsVisible() then GR:FetchGuildData() end
    elseif event == "PLAYER_EQUIPMENT_CHANGED" then GR:ScanSelf(); GR:BroadcastInspectData(); GR:CheckGhostProtocol() 
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, channel, sender = ...
        if prefix == "GRS_INS" then
            sender = GetCanonicalName(sender); local msgID, idx, total, chunk = strsplit(":", msg, 4); idx = tonumber(idx); total = tonumber(total)
            if not GR.InspectBuffer[sender] then GR.InspectBuffer[sender] = {} end; if not GR.InspectBuffer[sender][msgID] then GR.InspectBuffer[sender][msgID] = { parts = {}, count = 0 } end
            local buf = GR.InspectBuffer[sender][msgID]
            if not buf.parts[idx] then buf.parts[idx] = chunk; buf.count = buf.count + 1; if buf.count == total then local fullStr = table.concat(buf.parts); local data = DeserializeInspectData(fullStr); if data then GRS_InspectionDB[sender] = data; if GR.rosterListContainer and GR.rosterListContainer:IsVisible() then GR:FetchGuildData() end end; GR.InspectBuffer[sender][msgID] = nil end end
        elseif prefix == "GRS_ID" then
            if msg == "REQ" then
                local name, realm = UnitFullName("player"); local myCanonical = name .. "-" .. (realm or ""); local myTS = GRS_IdentityDB.times and GRS_IdentityDB.times[myCanonical] or 1
                if GRS_IdentityDB.mains[myCanonical] then GR:BroadcastIdentity(myCanonical, "MAIN", nil, myTS) elseif GRS_IdentityDB.alts[myCanonical] then GR:BroadcastIdentity(myCanonical, "ALT", GRS_IdentityDB.alts[myCanonical], myTS) end
                local queue = {}; if GRS_IdentityDB.alts then for altName, parentName in pairs(GRS_IdentityDB.alts) do if altName ~= myCanonical then table.insert(queue, {name = altName, role = "ALT", parent = parentName}) end end end
                if GRS_IdentityDB.mains then for mainName, _ in pairs(GRS_IdentityDB.mains) do if mainName ~= myCanonical then table.insert(queue, {name = mainName, role = "MAIN", parent = nil}) end end end
                if #queue > 0 then local idx = 1; C_Timer.NewTicker(0.25, function() local entry = queue[idx]; if entry then local storedTS = GRS_IdentityDB.times and GRS_IdentityDB.times[entry.name] or 1; GR:BroadcastIdentity(entry.name, entry.role, entry.parent, storedTS) end; idx = idx + 1 end, #queue) end
            else
                local name, role, parent, remoteTS, remoteVer = strsplit(":", msg); remoteTS = tonumber(remoteTS) or 0
                if name and role then
                    if not GRS_IdentityDB.mains then GRS_IdentityDB.mains = {} end; if not GRS_IdentityDB.alts then GRS_IdentityDB.alts = {} end; if not GRS_IdentityDB.times then GRS_IdentityDB.times = {} end
                    GRS_IdentityDB.ghostFinished = true; GRS_IdentityDB.ghostData = nil; local localTS = GRS_IdentityDB.times[name] or 0
                    if remoteTS > localTS or localTS == 0 then if role == "MAIN" then GRS_IdentityDB.mains[name] = true; GRS_IdentityDB.alts[name] = nil elseif role == "ALT" and parent then GRS_IdentityDB.mains[name] = nil; GRS_IdentityDB.alts[name] = parent end; GRS_IdentityDB.times[name] = remoteTS
                    elseif localTS > remoteTS then local myselfName, myselfRealm = UnitFullName("player"); local myselfCanonical = myselfName .. "-" .. (myselfRealm or ""); if GetCanonicalName(sender) ~= myselfCanonical then local correctParent = GRS_IdentityDB.alts[name]; if GRS_IdentityDB.mains[name] then GR:BroadcastIdentity(name, "MAIN", nil, localTS) elseif correctParent then GR:BroadcastIdentity(name, "ALT", correctParent, localTS) end end end
                end
            end
        end
    elseif event == "PLAYER_LOGIN" then
        C_ChatInfo.RegisterAddonMessagePrefix("GRS_ID"); C_ChatInfo.RegisterAddonMessagePrefix("GRS_INS")
        if not GRS_IdentityDB.mains then GRS_IdentityDB.mains = {} end; if not GRS_IdentityDB.alts then GRS_IdentityDB.alts = {} end; if not GRS_IdentityDB.times then GRS_IdentityDB.times = {} end; if not GRS_InspectionDB then GRS_InspectionDB = {} end
        if not GRS_GchatHist then GRS_GchatHist = {} end 
        C_Timer.After(4, function() GR:ScanSelf(); local name, realm = UnitFullName("player"); local myCanonical = name .. "-" .. (realm or ""); local knownMain = GRS_IdentityDB.accountMain; if knownMain and IsInGuild() and knownMain ~= "" and knownMain ~= myCanonical then local currentParent = GRS_IdentityDB.alts[myCanonical]; if currentParent ~= knownMain then GR:SetIdentity(myCanonical, "ALT", knownMain) end end; GR:PerformSync(true); GR:StartHourlyTimer(); GR:CheckGhostProtocol() end)
    end
end)

local dataTicker = C_Timer.NewTicker(2.0, function() local box = _G["GRS_RosterListFrame"]; if box then GR.rosterListContainer = box; if IsInGuild() then GR:FetchGuildData() end end end)