-- =========================================================================
-- FILE: Guild Roster Members.lua
-- =========================================================================
local addonName, NS = ...
local GR = _G["GRS_MainFrame"]

-- =========================================================================
-- VERSION CONTROL & DEPENDENCIES
-- =========================================================================
local PROTOCOL_VERSION = 32 -- BUMPED for Appearance Data
print("|cff00ff00A.X.I.O.M.:|r Roster Members v32.0 (Appearance Update) Loaded.")

C_AddOns.LoadAddOn("Blizzard_Communities")
C_AddOns.LoadAddOn("Blizzard_GuildUI")

-- =========================================================================
-- 1. CONFIGURATION (SHARED)
-- =========================================================================
GR.ROW_HEIGHT = 24
GR.HEADER_HEIGHT = 20
GR.SIDE_PANEL_WIDTH = 500
GR.TOP_PANE_HEIGHT = 180
GR.MAX_HISTORY_LINES = 10000

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

-- =========================================================================
-- 2. STATE & DB
-- =========================================================================
-- [ISOLATION UPDATE] These DBs are now containers. Actual data is inside [GuildKey].
if not GRS_IdentityDB then GRS_IdentityDB = {} end

-- Legacy Wipe: If we detect old structure (flat tables), wipe it to initialize Isolation Mode
if GRS_IdentityDB.mains then 
    GRS_IdentityDB = {} 
    print("|cff00ff00A.X.I.O.M.:|r Old Identity DB detected. Wiping for Isolation update.")
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

if not GRS_SocialDB then GRS_SocialDB = {} end 
if not GRS_SocialDB.pending then GRS_SocialDB.pending = {} end

if not GRS_GchatHist then GRS_GchatHist = {} end

if not GR.AssignmentFilterRanks then GR.AssignmentFilterRanks = {} end
GRS_PendingAssignments = {} 

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 

GR.GUIDRegistry = {}

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

function GR:GetGuildKey()
    if not IsInGuild() then return "Guildless" end
    
    -- Corrected Logic: Use Name-Realm as the Unique Key
    local guildName, _, _, guildRealm = GetGuildInfo("player")
    
    if not guildName then return "Guildless" end
    if not guildRealm then guildRealm = GetRealmName() end -- Fallback to current realm if nil
    
    return guildName .. "-" .. guildRealm
end

function GR:GetDB()
    local key = GR:GetGuildKey()
    
    -- Ensure Identity Structure
    if not GRS_IdentityDB[key] then 
        GRS_IdentityDB[key] = {
            mains = {},
            alts = {},
            times = {},
            ghostData = {},
            guids = {},
            factions = {},
            accountMain = nil
        } 
    end
    
    -- Ensure Chat History Structure
    if not GRS_GchatHist[key] then
        GRS_GchatHist[key] = {}
    end
    
    return GRS_IdentityDB[key], GRS_GchatHist[key]
end

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

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

function GR:CreateRobustVerticalScrollbar(parent, targetFrame)
    local container = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    container:SetWidth(18)
    container:SetBackdrop({ 
        bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", 
        tile = false, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } 
    })
    container:SetBackdropColor(0, 0, 0, 0.8); container:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
    container:SetFrameLevel(parent:GetFrameLevel() + 10)

    local upBtn = CreateFrame("Button", nil, container); upBtn:SetSize(16, 16); upBtn:SetPoint("TOP", 0, -2)
    upBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
    upBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down")
    upBtn:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
    upBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")

    local downBtn = CreateFrame("Button", nil, container); downBtn:SetSize(16, 16); downBtn:SetPoint("BOTTOM", 0, 2)
    downBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
    downBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down")
    downBtn:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Disabled")
    downBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")

    local slider = CreateFrame("Slider", nil, container); slider:SetOrientation("VERTICAL")
    slider:SetPoint("TOP", upBtn, "BOTTOM", 0, 0); slider:SetPoint("BOTTOM", downBtn, "TOP", 0, 0); slider:SetWidth(18)

    local track = slider:CreateTexture(nil, "BACKGROUND"); track:SetAllPoints(); track:SetColorTexture(0.1, 0.1, 0.1, 1)
    local thumb = slider:CreateTexture(nil, "ARTWORK"); thumb:SetSize(14, 24); thumb:SetColorTexture(0.5, 0.5, 0.5, 1); slider:SetThumbTexture(thumb)

    slider:SetScript("OnValueChanged", function(self, value, userInput)
        if userInput then
            local _, max = self:GetMinMaxValues()
            if targetFrame.SetScrollOffset then targetFrame:SetScrollOffset(math.floor((max - value) + 0.5))
            elseif targetFrame.SetVerticalScroll then targetFrame:SetVerticalScroll(value) end
        end
        thumb:SetColorTexture(0.8, 0.6, 0, 1)
    end)
    slider:SetScript("OnMouseUp", function() thumb:SetColorTexture(0.5, 0.5, 0.5, 1) end)
    upBtn:SetScript("OnClick", function() if targetFrame.ScrollUp then targetFrame:ScrollUp() end; container:Sync() end)
    downBtn:SetScript("OnClick", function() if targetFrame.ScrollDown then targetFrame:ScrollDown() end; container:Sync() end)

    container.Sync = function(self)
        if targetFrame.GetNumMessages then
            local numMessages = targetFrame:GetNumMessages(); local maxScroll = math.max(0, numMessages - 1)
            slider:SetMinMaxValues(0, maxScroll); slider:SetValue(maxScroll - targetFrame:GetScrollOffset())
            if maxScroll <= 0 then upBtn:Disable(); downBtn:Disable(); thumb:Hide() else upBtn:Enable(); downBtn:Enable(); thumb:Show() end
        elseif targetFrame.GetVerticalScrollRange then
            local maxScroll = targetFrame:GetVerticalScrollRange()
            slider:SetMinMaxValues(0, maxScroll); slider:SetValue(targetFrame:GetVerticalScroll())
            if maxScroll <= 0 then upBtn:Disable(); downBtn:Disable(); thumb:Hide() else upBtn:Enable(); downBtn:Enable(); thumb:Show() end
        end
    end
    container:SetScript("OnShow", function() container:Sync() end); container.slider = slider
    return container
end

function GR: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

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

function GR: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

function GR:GetIdentity(fullName)
    if not fullName then return true, nil end
    local db = GR:GetDB() -- ISOLATION
    if db.mains[fullName] then return true, fullName end
    if db.alts[fullName] then return false, db.alts[fullName] end
    return true, fullName
end

function GR: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

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

function GR:GetSecondsSinceReset()
    local region = GR:GetRegionName(); local t = date("*t"); local resetDay = 3 
    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

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

function GR:ScanSelf()
    local _, avgEquip = GetAverageItemLevel(); 
    local name, realm = UnitFullName("player")
    if not realm or realm == "" then realm = GetRealmName() end
    local myName = name .. "-" .. realm
    
    local _, classFileName = UnitClass("player"); local specID = GetSpecialization(); local specName = specID and select(2, GetSpecializationInfo(specID)) or "Unknown"
    
    local _, playerRace = UnitRace("player") 
    local playerFaction = UnitFactionGroup("player") or "Neutral"
    
    local db = GR:GetDB() -- ISOLATION
    
    if not db.factions then db.factions = {} end
    db.factions[myName] = playerFaction
    
    if GRS_SocialDB then
        if not GRS_SocialDB[myName] then GRS_SocialDB[myName] = {} end
        if not GRS_SocialDB[myName].raceSex or GRS_SocialDB[myName].raceSex == "" then
             GRS_SocialDB[myName].raceSex = playerRace
        end
    end

    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(GR.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
    
    -- [[ NEW: APPEARANCE SCRAPING ]]
    local appearanceData = {}
    -- Use C_BarberShop if available (usually requires interaction), but GetCustomizationOptions works passively
    local customization = C_BarberShop.GetAvailableCustomizations()
    if not customization then
        -- Fallback: Use Current Character Data
        -- NOTE: C_BarberShop.GetCustomizationOptions() only works AT the barbershop.
        -- We must use C_PlayerInfo.GetDisplayID() or similar, but for full construction we need the specific option IDs.
        -- In 11.x/12.x, the best way to get CURRENT appearance without Barbershop is difficult. 
        -- However, we can use the "Current" return of GetAvailableCustomizations() if it returns data, 
        -- OR we rely on GetCharacterAppearanceInfo() if available (deprecated).
        
        -- WORKAROUND: Since passive scraping of ALL options is restricted, we will attempt to grab what we can.
        -- Ideally, this data is best captured when the player VISITS the barbershop.
        -- BUT, for now, we will send an empty block if we can't scrape, which the Inspector handles.
    else
        -- If we could get it:
        -- for _, category in ipairs(customization) do ... end
    end
    
    -- ALTERNATIVE: Send current Race/Sex ID explicitly to ensure base model is correct at least.
    local raceID = select(3, UnitRace("player"))
    local sexID = UnitSex("player")
    local appearanceStr = string.format("%d:%d", raceID, sexID) 

    local data = { ts = GetServerTime(), ilvl = tonumber(string.format("%.2f", avgEquip)), class = classFileName, spec = specName, stats = stats, gear = gear, appearance = appearanceStr }
    GRS_InspectionDB[myName] = data

    -- Lockout Scan: check for mythic raid lockouts (difficultyId 16 = Mythic Raid)
    if not GRS_LockoutDB then GRS_LockoutDB = {} end
    local lockoutBosses = {}
    local numInstances = GetNumSavedInstances()
    for i = 1, numInstances do
        local instanceName, _, _, difficulty, locked, _, _, isRaid = GetSavedInstanceInfo(i)
        if isRaid and locked and difficulty == 16 then
            table.insert(lockoutBosses, instanceName)
        end
    end
    if #lockoutBosses > 0 then
        GRS_LockoutDB[myName] = { status = "Saved", bosses = lockoutBosses, ts = GetServerTime() }
    else
        GRS_LockoutDB[myName] = { status = "Unsaved", bosses = {}, ts = GetServerTime() }
    end

    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, "|")
    
    -- [A.X.I.O.M. FIX] Handle Appearance as String OR Table to prevent bad argument #8 crash
    local appStr = "0:0"
    if type(d.appearance) == "table" then
        appStr = string.format("%d:%d", d.appearance.raceID or 0, d.appearance.sexID or 0)
    elseif d.appearance then
        appStr = d.appearance
    end
    
    return string.format("%d^%.2f^%s^%s^%s^%s^%s", d.ts, d.ilvl, d.class, d.spec, statStr, gearStr, appStr)
end

local function DeserializeInspectData(str)
    local ts, ilvl, cls, spec, statBlock, gearBlock, appBlock = 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
    
    -- Parse Appearance (Simple Race:Sex for now)
    local raceID, sexID = strsplit(":", appBlock or "0:0")
    local appearance = { raceID = tonumber(raceID), sexID = tonumber(sexID) }
    
    return { ts = tonumber(ts), ilvl = tonumber(ilvl), class = cls, spec = spec, stats = stats, gear = gear, appearance = appearance }
end

function GR:BroadcastInspectData()
    if not IsInGuild() then return end
    
    local name, realm = UnitFullName("player")
    if not realm or realm == "" then realm = GetRealmName() end
    local myName = name .. "-" .. realm
    
    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)
        C_ChatInfo.SendAddonMessage("GRS_INS", string.format("%s:%d:%d:%s", msgID, i, numChunks, chunk), "GUILD")
    end
end

function GR:BroadcastLockoutData()
    if not IsInGuild() then return end
    local name, realm = UnitFullName("player")
    if not realm or realm == "" then realm = GetRealmName() end
    local myName = name .. "-" .. realm
    if not GRS_LockoutDB then GRS_LockoutDB = {} end
    local lockout = GRS_LockoutDB[myName]
    if not lockout then return end
    local msg = lockout.status .. ":" .. (lockout.ts or 0)
    C_ChatInfo.SendAddonMessage("GRS_LOCKOUT", msg, "GUILD")
end

function GR:FetchGuildData()
    if not IsInGuild() then return end
    if GR.CheckPermissions then GR.hasPermission = GR:CheckPermissions() end
    SetGuildRosterShowOffline(true); C_GuildInfo.GuildRoster()
    local numMembers = GetNumGuildMembers(); if numMembers == 0 then return end
    local tempRoster = {}; GR.GUIDRegistry = {}; local secondsSinceReset = GR:GetSecondsSinceReset()

    local db = GR:GetDB() -- ISOLATION
    if not db.guids then db.guids = {} end
    if not db.factions then db.factions = {} end
    if not GRS_SocialDB then GRS_SocialDB = {} end
    if not GRS_SocialDB.pending then GRS_SocialDB.pending = {} end

    for i = 1, numMembers do
        local fullName, rank, rankIndex, level, class, zone, note, officernote, online, status, classFileName, achievementPoints, achievementRank, isMobile, canSoR, repStanding, guid = GetGuildRosterInfo(i)
        if fullName then
            local dbKey = GR:GetCanonicalName(fullName); local shortName = Ambiguate(fullName, "none")
            
            local autoDetectedRace = nil

            if dbKey then
                if guid then
                    db.guids[dbKey] = guid
                    local _, _, _, englishRace = GetPlayerInfoByGUID(guid)
                    if englishRace then 
                        autoDetectedRace = englishRace
                        -- Use shared DetermineFaction logic if available (Defined in DetailsPanel)
                        if GR.DetermineFaction then
                            local fact = GR:DetermineFaction(englishRace)
                            if fact and fact ~= "Neutral" then
                                db.factions[dbKey] = fact
                            end
                        end
                    end
                else
                    guid = db.guids[dbKey]
                end
            end
            
            if dbKey and GRS_SocialDB.pending then
                local keysToRemove = {}
                for idx, orphan in ipairs(GRS_SocialDB.pending) do
                    local match = false
                    if guid and orphan.guid and orphan.guid == guid then match = true
                    elseif orphan.name and orphan.name == shortName then match = true end
                    if match then
                        if not GRS_SocialDB[dbKey] then GRS_SocialDB[dbKey] = {} end
                        for k, v in pairs(orphan.data) do GRS_SocialDB[dbKey][k] = v end
                        if orphan.guid and not db.guids[dbKey] then db.guids[dbKey] = orphan.guid end
                        table.insert(keysToRemove, idx)
                    end
                end
                for j = #keysToRemove, 1, -1 do table.remove(GRS_SocialDB.pending, keysToRemove[j]) end
            end

            local timeText = "|cff00ff00Online|r"; local sortVal = 0; local activeThisWeek = false
            if online then activeThisWeek = true
            else
                local y, m, d, h = GetGuildRosterLastOnline(i)
                timeText = GR: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, sortVal, achievementPoints, activeThisWeek, rank, rankIndex, i, note, officernote, guid, autoDetectedRace)
        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, guid, autoRace)
    local dbKey = GR:GetCanonicalName(fullName)
    if guid and dbKey then GR.GUIDRegistry[guid] = dbKey end
    
    local isMain, parentName = GR:GetIdentity(dbKey)
    local shortName = Ambiguate(fullName, "none")
    local rioScore, warbandScore, raidN, raidH, raidM, maxKey, fastestKey = 0, 0, "-", "-", "-", "-", "-"
    
    local identityStr = ""
    if isMain then identityStr = "|cff00ff00" .. (NS.L["MAIN"] or "Main") .. "|r"
    else
        if parentName then identityStr = (NS.L["ALT_OF"] or "Alt of") .. " " .. Ambiguate(parentName, "none") else identityStr = "-" end
    end

    local n, r = strsplit("-", fullName); if not r or r == "" then r = GetRealmName() end
    local displayName = n .. " - " .. r; local cleanName = n 
    local ilvl = "-"; local iData = GRS_InspectionDB[dbKey]; if iData and iData.ilvl then ilvl = iData.ilvl end
    
    local faction = "Neutral"
    local db = GR:GetDB() -- ISOLATION
    if db.factions and db.factions[dbKey] then faction = db.factions[dbKey] end

    if _G.RaiderIO and _G.RaiderIO.GetProfile then
        local name, realm = strsplit("-", fullName); if not realm or realm == "" then realm = GetRealmName() end
        local factionID = GR:GetFactionID()
        local p = _G.RaiderIO.GetProfile(name, realm, factionID)
        if not p then p = _G.RaiderIO.GetProfile(name, realm, (factionID == 1 and 2 or 1)) end
        if not p then p = _G.RaiderIO.GetProfile(name, realm, GR:GetRegionName()) end
        if p then
            if p.mythicKeystoneProfile then
                rioScore = p.mythicKeystoneProfile.currentScore or 0
                if p.mythicKeystoneProfile.mplusWarbandCurrent then
                    warbandScore = p.mythicKeystoneProfile.mplusWarbandCurrent.score or 0
                end
            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 = prog.raid and prog.raid.bossCount or 8
                        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
                    local dName = top.dungeon and top.dungeon.shortName or top.name or nil
                    if dName then fastestKey = dName .. " +" .. top.level else fastestKey = "+" .. top.level end
                end
            end
        end
    end
    
    -- Lockout + Spec from synced AXIOM data
    local mythicLockout = "-"
    if GRS_LockoutDB and GRS_LockoutDB[dbKey] then mythicLockout = GRS_LockoutDB[dbKey].status or "-" end
    local spec = "-"
    if GRS_InspectionDB and GRS_InspectionDB[dbKey] and GRS_InspectionDB[dbKey].spec then spec = GRS_InspectionDB[dbKey].spec end

    -- PVP data from ArenaMaster/Drustvar/Cache
    local pvpData = GR:GetPvpData(n, r) or {}

    table.insert(rosterTable, {
        name = displayName, shortName = cleanName, fullName = fullName, dbKey = dbKey, level = level, class = class,
        zone = zone, online = online, offline = sortOffline, timeText = timeText, ach = ach,
        ilvl = ilvl, rio = rioScore, warbandScore = warbandScore, raidN = raidN, raidH = raidH, raidM = raidM, maxKey = maxKey,
        fastestKey = fastestKey, mythicLockout = mythicLockout, spec = spec,
        pvp2v2 = pvpData.pvp2v2 or 0, pvp3v3 = pvpData.pvp3v3 or 0,
        pvpSolo = pvpData.pvpSolo or 0, pvpRbg = pvpData.pvpRbg or 0,
        pvpBest = pvpData.pvpBest or 0, pvpExp = pvpData.pvpExp or 0,
        pvpSpec = pvpData.pvpSpec or "-",
        identity = identityStr, isMain = isMain, parent = parentName, rank = rank,
        rankIndex = rankIndex, rosterIndex = rosterIndex, publicNote = pubNote, officerNote = offNote,
        guid = guid, faction = faction, race = autoRace
    })
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.UpdateRosterList 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; local valA, valB = dA[k], dB[k]
        if k == "offline" then valA = dA.online and -1 or (dA.offline or 0); valB = dB.online and -1 or (dB.offline or 0) end
        local emptyA = (valA == nil or valA == "" or valA == "-"); local emptyB = (valB == nil or valB == "" or valB == "-")
        if emptyA and emptyB then return dA.name < dB.name end
        if emptyA then return false end
        if emptyB then return true end
        if type(valA) == "number" and type(valB) == "number" then if valA == valB then return dA.name < dB.name end; if GR.SortAsc then return valA < valB else return valA > valB end end
        valA = tostring(valA):lower(); valB = tostring(valB):lower(); 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
    if GR.UpdateRosterList then GR:UpdateRosterList() end
end

function GR:SetIdentity(canonicalName, type, parent)
    local db = GR:GetDB() -- ISOLATION
    db.ghostFinished = true; db.ghostData = nil; local timestamp = GetServerTime()
    if type == "MAIN" then db.mains[canonicalName] = true; db.alts[canonicalName] = nil; db.times[canonicalName] = timestamp; db.accountMain = canonicalName; GR:ShowToast(string.format("|cff00ff00%s|r set as MAIN.", Ambiguate(canonicalName, "none")))
    elseif type == "ALT" and parent then db.mains[canonicalName] = nil; db.alts[canonicalName] = parent; db.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 db = GR:GetDB() -- ISOLATION
    local ts = manualTS; if not ts and db.times then ts = db.times[name] end; if not ts then ts = 1 end
    
    local guidToSend = ""; local factionToSend = ""
    local myName, myRealm = UnitFullName("player"); local myCanonical = myName .. "-" .. (realm or "")
    
    if name == myCanonical then
        guidToSend = UnitGUID("player")
        factionToSend = UnitFactionGroup("player") or "Neutral"
    else
        if db.guids and db.guids[name] then guidToSend = db.guids[name] end
        if db.factions and db.factions[name] then factionToSend = db.factions[name] end
    end
    
    local msg = string.format("%s:%s:%s:%d:%d:%s:%s", name, role, parent or "NIL", ts, PROTOCOL_VERSION, guidToSend or "", factionToSend or "Neutral")
    if IsInGuild() then C_ChatInfo.SendAddonMessage("GRS_ID", msg, "GUILD") end
end

function GR:PerformSync(isAuto)
    if not IsInGuild() then return end
    
    -- [[ HARD WIPE PROTOCOL ]]
    if not isAuto then
        GR.RosterData = {}
        local db = GR:GetDB()
        if db then
            -- FORCE WIPE OF FACTIONS AND GUIDS ON MANUAL REFRESH
            db.factions = {} 
            db.guids = {}
        end
        print("|cff00ff00A.X.I.O.M.:|r Cache Cleared (HARD RESET). Re-scanning...")
    end

    C_ChatInfo.SendAddonMessage("GRS_ID", "REQ", "GUILD")
    
    local name, realm = UnitFullName("player")
    if not realm or realm == "" then realm = GetRealmName() end
    local myCanonical = name .. "-" .. realm
    
    local db = GR:GetDB() -- ISOLATION
    local myTS = db.times and db.times[myCanonical] or 1
    if db.mains[myCanonical] then GR:BroadcastIdentity(myCanonical, "MAIN", nil, myTS) elseif db.alts[myCanonical] then GR:BroadcastIdentity(myCanonical, "ALT", db.alts[myCanonical], myTS) end
    GR:BroadcastInspectData(); GR:BroadcastLockoutData(); GR:FetchGuildData(); if not isAuto then print("|cff00ff00A.X.I.O.M.:|r Manual Sync Executed (Identity, Inspection & Social).") 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 = GR:GetDB() -- ISOLATION
    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")
    if not realm or realm == "" then realm = GetRealmName() end
    local canonical = name .. "-" .. realm

    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("|cff00ff00A.X.I.O.M.:|r Auto-detect (24h) complete. Main: " .. bestMain)
            db.mains[bestMain] = true; db.alts[bestMain] = nil; db.times[bestMain] = GetServerTime(); db.accountMain = bestMain
            for charName, _ in pairs(db.ghostData) do if charName ~= bestMain then db.mains[charName] = nil; db.alts[charName] = bestMain; db.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:BroadcastLockoutData(); GR:CheckGhostProtocol() 
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, msg, channel, sender = ...
        if prefix == "GRS_INS" then
            sender = GR: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
            local db = GR:GetDB() -- ISOLATION
            if msg == "REQ" then
                local name, realm = UnitFullName("player")
                if not realm or realm == "" then realm = GetRealmName() end
                local myCanonical = name .. "-" .. realm
                
                local myTS = db.times and db.times[myCanonical] or 1
                if db.mains[myCanonical] then GR:BroadcastIdentity(myCanonical, "MAIN", nil, myTS) elseif db.alts[myCanonical] then GR:BroadcastIdentity(myCanonical, "ALT", db.alts[myCanonical], myTS) end
                local queue = {}; if db.alts then for altName, parentName in pairs(db.alts) do if altName ~= myCanonical then table.insert(queue, {name = altName, role = "ALT", parent = parentName}) end end end
                if db.mains then for mainName, _ in pairs(db.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 = db.times and db.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, remoteGUID, remoteFaction = strsplit(":", msg); remoteTS = tonumber(remoteTS) or 0
                
                if name then
                    if remoteGUID and remoteGUID ~= "" then
                        if not db.guids then db.guids = {} end
                        db.guids[name] = remoteGUID
                    end
                    if remoteFaction and remoteFaction ~= "" and remoteFaction ~= "Neutral" then
                        if not db.factions then db.factions = {} end
                        db.factions[name] = remoteFaction
                    end
                end

                if name and role then
                    db.ghostFinished = true; db.ghostData = nil; local localTS = db.times[name] or 0
                    if remoteTS > localTS or localTS == 0 then if role == "MAIN" then db.mains[name] = true; db.alts[name] = nil elseif role == "ALT" and parent then db.mains[name] = nil; db.alts[name] = parent end; db.times[name] = remoteTS
                    elseif localTS > remoteTS then local myselfName, myselfRealm = UnitFullName("player"); local myselfCanonical = myselfName .. "-" .. (myselfRealm or ""); if GR:GetCanonicalName(sender) ~= myselfCanonical then local correctParent = db.alts[name]; if db.mains[name] then GR:BroadcastIdentity(name, "MAIN", nil, localTS) elseif correctParent then GR:BroadcastIdentity(name, "ALT", correctParent, localTS) end end end
                end
            end
        elseif prefix == "GRS_ASGN" then
            if GR.HandleAssignmentPacket then
                GR:HandleAssignmentPacket(prefix, msg, GR:GetCanonicalName(sender))
            end
        elseif prefix == "GRS_LOCKOUT" then
            local senderKey = GR:GetCanonicalName(sender)
            local status, ts = strsplit(":", msg, 2)
            if not GRS_LockoutDB then GRS_LockoutDB = {} end
            GRS_LockoutDB[senderKey] = { status = status or "-", bosses = {}, ts = tonumber(ts) or 0 }
            if GR.rosterListContainer and GR.rosterListContainer:IsVisible() then GR:FetchGuildData() end
        end
    elseif event == "PLAYER_LOGIN" then
        C_ChatInfo.RegisterAddonMessagePrefix("GRS_ID"); C_ChatInfo.RegisterAddonMessagePrefix("GRS_INS"); C_ChatInfo.RegisterAddonMessagePrefix("GRS_ASGN"); C_ChatInfo.RegisterAddonMessagePrefix("GRS_SOC"); C_ChatInfo.RegisterAddonMessagePrefix("GRS_LOCKOUT")
        
        -- Safe Init for Global Containers
        if not GRS_IdentityDB then GRS_IdentityDB = {} end 
        if not GRS_GchatHist then GRS_GchatHist = {} end 
        
        if not GRS_InspectionDB then GRS_InspectionDB = {} end
        if not GRS_SocialDB then GRS_SocialDB = {} end
        if not GRS_LockoutDB then GRS_LockoutDB = {} end
        
        C_Timer.After(4, function() 
            GR:ScanSelf(); 
            local name, realm = UnitFullName("player")
            if not realm or realm == "" then realm = GetRealmName() end
            local myCanonical = name .. "-" .. realm
            
            local db = GR:GetDB() -- This initializes the specific Guild Bucket if missing
            
            local currentGUID = UnitGUID("player")
            local currentFaction = UnitFactionGroup("player")
            
            if currentGUID then db.guids[myCanonical] = currentGUID end
            if currentFaction then db.factions[myCanonical] = currentFaction end
            
            local knownMain = db.accountMain; 
            if knownMain and IsInGuild() and knownMain ~= "" and knownMain ~= myCanonical then 
                local currentParent = db.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 = GR.rosterListContainer or _G["GRS_RosterListFrame"]; if box then GR.rosterListContainer = box; if IsInGuild() then GR:FetchGuildData() end end end)