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

-- =========================================================================
-- VERSION CONTROL
-- =========================================================================
local PROTOCOL_VERSION = 21
print("|cff00ff00GRS:|r Roster Members v21.2 (UI Fixes) Loaded.")

-- These are harmless to load, but we will NOT rely on them for actions in this release build.
C_AddOns.LoadAddOn("Blizzard_Communities")
C_AddOns.LoadAddOn("Blizzard_GuildUI")

-- =========================================================================
-- 0. RELEASE-SAFE NOTICE POPUP
-- =========================================================================
local DEV_NOTICE_KEY = "GRS_DEV_NOTICE_POPUP_V1"

if not StaticPopupDialogs[DEV_NOTICE_KEY] then
    StaticPopupDialogs[DEV_NOTICE_KEY] = {
        text =
            "Currently under development. Blizzard has decided to overcomplicate this.\n\n" ..
            "For now, please use the native Guild Roster to perform Promotions, demotions and notes.\n\n" ..
            "Thank you in advance for your patience.",
        button1 = OKAY,
        timeout = 0,
        whileDead = 1,
        hideOnEscape = 1,
        preferredIndex = 3,
    }
end

function GR:ShowDevNotice()
    if StaticPopup_Show then
        StaticPopup_Show(DEV_NOTICE_KEY)
    end
end

-- =========================================================================
-- 1. CONFIGURATION
-- =========================================================================
local ROW_HEIGHT = 24
local HEADER_HEIGHT = 20
local CHAT_WIDTH = 500

local COL_DEF = {
    { name = "", width = 20, key = "expand" },
    { name = "Name", width = 210, key = "name", sort = "name" },
    { name = "Rank", width = 80, key = "rank", sort = "rankIndex" },
    { name = "Last Online", width = 80, key = "offline", sort = "offline" },
    { name = "Lvl", width = 35, key = "level", sort = "level" },
    { name = "Class", width = 80, key = "class", sort = "class" },
    { name = "Zone", width = 110, key = "zone", sort = "zone" },
    { name = "Achv", width = 50, key = "ach", sort = "ach" },
    { name = "ilvl", width = 45, key = "ilvl", sort = "ilvl" },
    { name = "M+ Sc", 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" },
    { name = "Max Key", width = 60, key = "maxKey", sort = "maxKey" },
}

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

-- =========================================================================
-- 2. STATE & DB
-- =========================================================================
-- FIXED: Safe Initialization Pattern to prevent 'Table Exists' trap
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
-- Timestamp Table for Authority Protocol
if not GRS_IdentityDB.times then GRS_IdentityDB.times = {} end
-- accountMain remains nil by default until set

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

-- Inspection Database
if not GRS_InspectionDB then GRS_InspectionDB = {} end

GR.RosterData = {}
GR.ViewList = {}
GR.ExpandedMains = {}
GR.SortKey = "rankIndex"
GR.SortAsc = true
GR.Rows = {}
GR.ShowOffline = true
GR.InspectBuffer = {} -- For reassembling chunked messages

-- =========================================================================
-- 3. HELPERS
-- =========================================================================
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 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 -- Grey
    local kills = tonumber(string.match(str, "^(%d+)")) or 0

    if kills == 0 then return "|cff808080" -- Grey
    elseif kills <= 4 then return "|cff00ff00" -- Green
    elseif kills <= 6 then return "|cffa335ee" -- Purple
    else return "|cffff8000" end -- Orange
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 >= 635 then return "|cffff8000" -- Legendary/High Mythic
    elseif ilvl >= 620 then return "|cffa335ee" -- Epic/Heroic+
    elseif ilvl >= 600 then return "|cff0070dd" -- Rare/Normal
    elseif ilvl >= 580 then return "|cff1eff00" -- Green
    else return "|cffffffff" end
end

-- =========================================================================
-- 4. INSPECTION & DATA ENGINE
-- =========================================================================

-- 4a. Self Scan Logic
function GR:ScanSelf()
    local _, avgEquip = GetAverageItemLevel()
    local name, realm = UnitFullName("player")
    local myName = name .. "-" .. (realm or "")
    
    local _, classFileName = UnitClass("player")
    local specID = GetSpecialization()
    local specName = specID and select(2, GetSpecializationInfo(specID)) or "Unknown"

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

    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),
        class = classFileName,
        spec = specName,
        stats = stats,
        gear = gear
    }

    GRS_InspectionDB[myName] = data
    return data
end

-- 4b. Serialization
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

-- 4c. Communication
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

-- 4d. Guild Data Fetch
function GR:FetchGuildData()
    if not IsInGuild() then return end

    SetGuildRosterShowOffline(true)
    C_GuildInfo.GuildRoster()

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

    local tempRoster = {}
    local seen = {}
    local secondsSinceReset = GetSecondsSinceReset()

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

        if fullName then
            seen[fullName] = true
            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)
        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)
    local dbKey = GetCanonicalName(fullName)
    local isMain, parentName = GetIdentity(dbKey)
    local shortName = Ambiguate(fullName, "none")
    local rioScore, raidN, raidH, raidM, maxKey = 0, "-", "-", "-", "-"
    
    local ilvl = "-"
    local iData = GRS_InspectionDB[dbKey]
    if iData and iData.ilvl then
        ilvl = iData.ilvl
    end

    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 faction = GetFactionID()

        local p = _G.RaiderIO.GetProfile(name, realm, faction)
        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
            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
                            raidN, raidH, raidM = "-", "-", "-"
                            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 progressStr = kills .. "/" .. total

                        if prog.difficulty == 1 then raidN = progressStr end
                        if prog.difficulty == 2 then raidH = progressStr end
                        if prog.difficulty == 3 then raidM = progressStr end
                    end
                end
            end
            
            if p.mythicKeystoneProfile and p.mythicKeystoneProfile.sortedDungeons then
                local top = p.mythicKeystoneProfile.sortedDungeons[1]
                if top then maxKey = "+"..top.level end
            end
        end
    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,
        isMain = isMain,
        parent = parentName,
        rank = rank,
        rankIndex = rankIndex,
        rosterIndex = rosterIndex,
    })
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 = {}
    local 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
            local visibleChildren = m.children 
            
            m.data.hasChildren = (#visibleChildren > 0)
            m.data.isExpanded = GR.ExpandedMains[m.data.dbKey]

            table.insert(GR.ViewList, m.data)

            if m.data.isExpanded then
                for _, child in ipairs(visibleChildren) do
                    child.isChild = true
                    table.insert(GR.ViewList, child)
                end
            end
        end
    end

    GR:UpdateRosterList()
end

-- =========================================================================
-- 5. 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
        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

    -- BUTTONS REMOVED FROM HERE
end

function GR:CreateRosterRow(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    row:SetWidth(1280)

    local hl = row:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints()
    hl:SetColorTexture(1, 1, 1, 0.1)

    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    row.cols = {}
    local xOffset = 0
    for _, def in ipairs(COL_DEF) do
        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
        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

    local status = row:CreateTexture(nil, "OVERLAY")
    status:SetSize(12, 12) 
    status:SetTexture("Interface\\COMMON\\Indicator-Gray")
    status:SetPoint("LEFT", row.cols["name"], "RIGHT", -5, 0)
    row.statusDot = status

    return row
end

function GR:UpdateRosterList()
    local mainContainer = GR.rosterListContainer or _G["GRS_RosterListFrame"]
    if not mainContainer then return end

    if not GR.RosterSplitMain then
        GR.RosterSplitMain = CreateFrame("Frame", nil, mainContainer)
        GR.RosterSplitMain:SetPoint("TOPLEFT", 0, 0)
        GR.RosterSplitMain:SetPoint("BOTTOMLEFT", 0, 0)
        GR.RosterSplitMain:SetWidth(mainContainer:GetWidth() - CHAT_WIDTH)

        GR.ChatSplitMain = CreateFrame("Frame", nil, mainContainer, "BackdropTemplate")
        GR.ChatSplitMain:SetPoint("TOPRIGHT", 0, 0)
        GR.ChatSplitMain:SetPoint("BOTTOMRIGHT", 0, 0)
        GR.ChatSplitMain:SetWidth(CHAT_WIDTH)
        GR.ChatSplitMain: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.ChatSplitMain:SetBackdropColor(0, 0, 0, 0.9)

        GR:CreateRosterHeader(GR.RosterSplitMain)

        local sf = CreateFrame("ScrollFrame", "GRS_RosterScrollDirect", GR.RosterSplitMain, "UIPanelScrollFrameTemplate")
        -- FIXED SCROLLFRAME ANCHORS (Correctly snaps to right edge of roster list, with space for bar)
        sf:SetPoint("TOPLEFT", 0, -(HEADER_HEIGHT) -10)
        sf:SetPoint("BOTTOMRIGHT", -25, 10) 

        local content = CreateFrame("Frame", nil, sf)
        content:SetSize(GR.RosterSplitMain:GetWidth() - 30, 500)
        sf:SetScrollChild(content)

        GR.RosterContent = content
        GR.RosterScrollFrame = sf

        self:BuildChatPane(GR.ChatSplitMain)
        GR.RosterRows = {}
    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

        -- Right Click Logic UPDATE
        r:SetScript("OnClick", function(self, button)
            if button == "RightButton" then
                local name, realm = UnitFullName("player")
                local myCanonical = name .. "-" .. (realm or "")
                
                if self.data and self.data.dbKey == myCanonical then
                    MenuUtil.CreateContextMenu(self, function(owner, root)
                        root:CreateTitle(self.data.name)
                        root:CreateDivider()
                        root:CreateButton("Set this character as MAIN", function()
                            GR:SetIdentity(myCanonical, "MAIN")
                        end)
                    end)
                else
                    GR:ShowDevNotice()
                end
            end
        end)

        r.expBtn:SetScript("OnClick", function()
            if r.data then
                GR.ExpandedMains[r.data.dbKey] = not GR.ExpandedMains[r.data.dbKey]
                GR:BuildViewList()
            end
        end)

        local indent = d.isChild and 0 or 0
        r.cols["name"]:SetPoint("LEFT", 5 + indent, 0)

        local cColor = C_ClassColor.GetClassColor((d.class or "PRIEST"):upper())
        if not cColor then cColor = C_ClassColor.GetClassColor("PRIEST") end

        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)
        r.cols["ach"]:SetText(d.ach)
        
        local ilvlVal = tonumber(d.ilvl) or 0
        if ilvlVal > 0 then
            r.cols["ilvl"]:SetText(GetIlvlColor(ilvlVal) .. d.ilvl .. "|r")
        else
            r.cols["ilvl"]:SetText("-")
        end

        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")
        r.cols["maxKey"]:SetText(d.maxKey)

        r.statusDot:Show()
        r.statusDot:SetTexture("Interface\\COMMON\\Indicator-Gray")

        if d.online then
            if d.isChild then
                r.statusDot:SetVertexColor(1, 0.6, 0) 
            else
                r.statusDot:SetVertexColor(0, 1, 0) 
            end
        else
            if d.isOnlineOnAlt then
                r.statusDot:SetVertexColor(1, 0.5, 0) -- Orange
            else
                r.statusDot:SetVertexColor(0.5, 0.5, 0.5) 
            end
        end

        if d.hasChildren then
            r.expBtn:Show()
            if d.isExpanded then
                r.expBtn:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
            else
                r.expBtn:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
            end
            local nt = r.expBtn:GetNormalTexture()
            if nt then nt:SetDesaturated(true) end
        else
            r.expBtn:Hide()
        end
    end

    content:SetHeight(#data * ROW_HEIGHT)
end

function GR:BuildChatPane(parent)
    -- ADDED BUTTONS TO CHAT PANE HEADER
    local refBtn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    refBtn:SetSize(60, 20)
    refBtn:SetPoint("TOPRIGHT", -5, -5)
    refBtn:SetText("Refresh")
    refBtn:SetScript("OnClick", function() GR:PerformSync() end)

    local togBtn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    togBtn:SetSize(180, 20)
    togBtn:SetPoint("RIGHT", refBtn, "LEFT", -5, 0)
    togBtn:SetText("Toggle Offline Members")
    
    local function Skin(b)
        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)
        b:GetFontString():SetTextColor(1, 1, 1)
    end
    Skin(refBtn)
    Skin(togBtn)

    togBtn:SetScript("OnClick", function(s)
        GR.ShowOffline = not GR.ShowOffline
        local fs = s:GetFontString()
        if GR.ShowOffline then fs:SetTextColor(1, 1, 1) else fs:SetTextColor(1, 0.5, 0) end
        if #GR.RosterData > 0 then GR:BuildViewList() else GR:FetchGuildData() end
    end)

    local title = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", 10, -5)
    title:SetText("Guild Chat")

    local chatScroll = CreateFrame("ScrollingMessageFrame", nil, parent)
    chatScroll:SetPoint("TOPLEFT", 10, -35) -- Moved down to clear buttons
    chatScroll:SetPoint("BOTTOMRIGHT", -10, 35)
    chatScroll:SetFontObject(ChatFontNormal)
    chatScroll:SetJustifyH("LEFT")
    chatScroll:SetFading(false)
    chatScroll:SetMaxLines(500)
    GR.GuildChatFrame = chatScroll

    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)

    if not GR.ChatListener then
        GR.ChatListener = CreateFrame("Frame")
        GR.ChatListener:RegisterEvent("CHAT_MSG_GUILD")
        GR.ChatListener:RegisterEvent("CHAT_MSG_OFFICER")
        GR.ChatListener:RegisterEvent("CHAT_MSG_GUILD_ACHIEVEMENT")
		GR.ChatListener:SetScript("OnEvent", function(self, event, msg, sender)
			if not GR.GuildChatFrame then return end
			sender = Ambiguate(sender or "", "none")
			local classColor = "|cffffffff"
			if IsInGuild() and sender ~= "" then
				for i=1, GetNumGuildMembers() do
					local name, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
					if name and Ambiguate(name, "none") == sender then
						local c = C_ClassColor.GetClassColor(classFileName)
						if c then classColor = c:GenerateHexColorMarkup() end
						break
					end
				end
			end
			local msgColor = "|cff00ff00"
			local timestamp = date("%H:%M")
			if event == "CHAT_MSG_GUILD_ACHIEVEMENT" then
				local coloredName = classColor .. sender .. "|r"
				local fixedMsg = (msg or ""):gsub("%%s", coloredName)
				GR.GuildChatFrame:AddMessage("|cff999999["..timestamp.."]|r " .. msgColor .. fixedMsg .. "|r")
				return
			end
			GR.GuildChatFrame:AddMessage("|cff999999["..timestamp.."]|r " .. classColor .. "["..sender.."]:|r " .. msgColor .. (msg or "") .. "|r")
		end)
    end
end

-- =========================================================================
-- 6. INSPECTION POPUP WINDOW
-- =========================================================================
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)

    -- Close Button
    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -5, -5)

    -- Header Info
    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)

    -- Stats Block
    f.stats = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    f.stats:SetPoint("CENTER", 0, -50)
    f.stats:SetJustifyH("CENTER")

    -- Gear Icons
    f.slots = {}
    
    local leftSlots = {1, 2, 3, 15, 5, 4, 19, 9} -- Head, Neck, Shoulder, Back, Chest, Shirt, Tabard, Wrist
    local rightSlots = {10, 6, 7, 8, 11, 12, 13, 14} -- Hands, Waist, Legs, Feet, Ring1, Ring2, Trink1, Trink2
    local bottomSlots = {16, 17} -- MainHand, OffHand

    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\n" ..
            "Crit: %.1f%%  Haste: %.1f%%  Mast: %.1f%%  Vers: %.1f%%",
            s.str, s.agi, s.int, s.sta, s.crit, s.haste, s.mast, s.vers
        ))
    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

-- =========================================================================
-- 7. NOTIFICATIONS
-- =========================================================================
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

-- =========================================================================
-- 8. SMART IDENTITY & SYNC (V21: CANONICAL NAMES)
-- =========================================================================
function GR:SetIdentity(canonicalName, type, parent)
    -- canonicalName is passed in as Name-Realm.
    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

    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)
    -- Name/Parent MUST be canonical here.
    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
    
    -- 1. Identity Sync (Protocol V21)
    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
    
    -- 2. Inspect Broadcast
    GR:BroadcastInspectData()
    
    -- 3. Refresh View
    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

-- =========================================================================
-- 9. EVENT HANDLERS
-- =========================================================================

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()

    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, channel, sender = ...
        
        -- INSPECTION PROTOCOL
        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
                
                -- Reassembly
                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
        
        -- IDENTITY PROTOCOL V21
        elseif prefix == "GRS_ID" then
            if msg == "REQ" then
                -- 1. Send Self (Immediate - High Priority)
                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

                -- 2. Repository Dump
                local queue = {}
                
                -- Note: keys in pairs(alts) are now Canonical Name-Realm strings
                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
                -- INGESTION
                -- name here MUST be Canonical (Name-Realm)
                local name, role, parent, remoteTS, remoteVer = strsplit(":", msg)
                remoteTS = tonumber(remoteTS) or 0
                
                local rVer = tonumber(remoteVer)
                if not rVer or (rVer < PROTOCOL_VERSION) then
                    return 
                end

                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

                    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
        
        C_Timer.After(4, function()
            -- 1. Scan Self
            GR:ScanSelf()
            
            -- 2. Check for Auto-Link
            local name, realm = UnitFullName("player")
            local myCanonical = name .. "-" .. (realm or "")
            
            local knownMain = GRS_IdentityDB.accountMain

            if knownMain then
                if IsInGuild() and knownMain ~= "" and knownMain ~= myCanonical then
                    local currentParent = GRS_IdentityDB.alts[myCanonical]
                    
                    if currentParent ~= knownMain then
                        print("|cff00ff00GRS:|r Auto-detect: Linking " .. Ambiguate(myCanonical, "none") .. " as Alt of " .. Ambiguate(knownMain, "none"))
                        GR:SetIdentity(myCanonical, "ALT", knownMain)
                    end
                end
            end
            
            -- 3. Initial Broadcast
            GR:PerformSync(true)
            
            -- 4. Start Timer
            GR:StartHourlyTimer()
        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)