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

print("|cff00ff00A.X.I.O.M.:|r Axiom Version 4.2.9")

-- DEFINITION: restricted = true means "Only show if GR:CheckPermissions() is true"
-- NOTE: 'lkey' stores the localization key, looked up at runtime via GetColName()
GR.COL_DEF = {
    { lkey = nil, name = "", width = 20, key = "expand" },
    { lkey = "NAME", name = "Name", width = 210, key = "name", sort = "name" },
    { lkey = "RANK", name = "Rank", width = 80, key = "rank", sort = "rankIndex" },
    { lkey = "LAST_ONLINE", name = "Last Online", width = 80, key = "offline", sort = "offline" },
    { lkey = "LVL", name = "Lvl", width = 35, key = "level", sort = "level" },
    { lkey = "CLASS", name = "Class", width = 80, key = "class", sort = "class" },
    { lkey = "ZONE", name = "Zone", width = 110, key = "zone", sort = "zone" },
    { lkey = "ACHV", name = "Achv", width = 50, key = "ach", sort = "ach", customizable = true },
    { lkey = "ILVL", name = "ilvl", width = 45, key = "ilvl", sort = "ilvl", customizable = true },
    { lkey = nil, name = "N", width = 55, key = "raidN", sort = "raidN", advanced = true, customizable = true },
    { lkey = nil, name = "H", width = 55, key = "raidH", sort = "raidH", advanced = true, customizable = true },
    { lkey = nil, name = "Race", width = 80, key = "race", sort = "race", social = true },
    { lkey = nil, name = "Joined", width = 75, key = "joinDate", social = true },
    { lkey = nil, name = "Promoted", width = 75, key = "promDate", social = true },
    { lkey = nil, name = "Birthday", width = 75, key = "birthDate", social = true },
    { lkey = nil, name = "Note", width = 200, key = "customNote", social = true },
    { lkey = nil, name = "N", width = 55, key = "raidRaidN", sort = "raidN", raid = true },
    { lkey = nil, name = "H", width = 55, key = "raidRaidH", sort = "raidH", raid = true },
    { lkey = nil, name = "Spec", width = 80, key = "raidSpec", raid = true },
    { lkey = nil, name = "Lockout", width = 90, key = "mythicLockout", raid = true },
    { lkey = nil, name = "2v2", width = 55, key = "pvp2v2", sort = "pvp2v2", pvp = true, customizable = true },
    { lkey = nil, name = "3v3", width = 55, key = "pvp3v3", sort = "pvp3v3", pvp = true, customizable = true },
    { lkey = nil, name = "Solo", width = 55, key = "pvpSolo", sort = "pvpSolo", pvp = true, customizable = true },
    { lkey = nil, name = "RBG", width = 55, key = "pvpRbg", sort = "pvpRbg", pvp = true, customizable = true },
    { lkey = nil, name = "Best", width = 55, key = "pvpBest", sort = "pvpBest", pvp = true, customizable = true },
    { lkey = nil, name = "Exp", width = 55, key = "pvpExp", sort = "pvpExp", pvp = true, customizable = true },
    { lkey = nil, name = "Spec", width = 80, key = "pvpSpec", pvp = true },
    { lkey = "IDENTITY", name = "Identity", width = 130, key = "identity", sort = "parent", restricted = true },
}

-- Helper to get localized column name at runtime
local function GetColName(col)
    if col.lkey and NS.L and NS.L[col.lkey] then
        return NS.L[col.lkey]
    end
    return col.name
end

-- =========================================================================
-- 1. STYLING HELPERS & VISUALS
-- =========================================================================

local function SkinButton(btn)
    if not btn then return end
    btn:SetNormalTexture("")
    btn:SetHighlightTexture("")
    btn:SetPushedTexture("")
    btn:SetDisabledTexture("")
    if btn.Left then btn.Left:SetAlpha(0) end
    if btn.Middle then btn.Middle:SetAlpha(0) end
    if btn.Right then btn.Right:SetAlpha(0) end
    if btn.Top then btn.Top:SetAlpha(0) end
    if btn.Bottom then btn.Bottom:SetAlpha(0) end
    local t = btn:CreateTexture(nil, "BACKGROUND")
    t:SetAllPoints(); t:SetColorTexture(0.2, 0.2, 0.2, 1); btn.bg = t
    local b = btn:CreateTexture(nil, "BORDER")
    b:SetPoint("TOPLEFT", -1, 1); b:SetPoint("BOTTOMRIGHT", 1, -1); b:SetColorTexture(0, 0, 0, 1); btn.border = b
    btn:HookScript("OnEnter", function(s) if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end end)
    btn:HookScript("OnLeave", function(s) if s.bg then s.bg:SetColorTexture(0.2, 0.2, 0.2, 1) end end)
    btn:HookScript("OnMouseDown", function(s) if s.bg then s.bg:SetColorTexture(0.1, 0.1, 0.1, 1) end end)
    btn:HookScript("OnMouseUp", function(s) if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end end)
    local fs = btn:GetFontString(); if fs then fs:SetTextColor(1, 1, 1) end
end

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

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

local function GetIlvlColor(ilvl)
    if not ilvl or type(ilvl) ~= "number" then return "|cff808080" end
    if ilvl >= 635 then return "|cffff8000"
    elseif ilvl >= 620 then return "|cffa335ee"
    elseif ilvl >= 600 then return "|cff0070dd"
    elseif ilvl >= 580 then return "|cff1eff00"
    else return "|cffffffff" end
end

-- =========================================================================
-- 2. HELPER: OFFICER CHECK
-- =========================================================================
function GR:IsOfficer()
    if GR.devOfficerMode then return true end
    if not IsInGuild() then return false end
    return CanEditOfficerNote() 
end

function GR:IsGM()
    if GR.devOfficerMode then return true end
    if not IsInGuild() then return false end
    return IsGuildLeader()
end

function GR:CheckPermissions()
    if GR.devOfficerMode then return true end
    if not IsInGuild() then 
        return false 
    end
    return CanEditOfficerNote()
end

-- =========================================================================
-- 3. ROSTER LIST GENERATION
-- =========================================================================

function GR:CreateRosterHeader(parent)
    local hFrame = CreateFrame("Frame", nil, parent)
    hFrame:SetClipsChildren(true)
    hFrame:SetHeight(GR.HEADER_HEIGHT)
    hFrame:SetPoint("TOPLEFT", 0, 0); hFrame:SetPoint("TOPRIGHT", -18, 0)
    local innerHeader = CreateFrame("Frame", nil, hFrame)
    innerHeader:SetHeight(GR.HEADER_HEIGHT)
    innerHeader:SetPoint("TOPLEFT", 0, 0)
    GR.RosterHeaderInner = innerHeader
    local xOffset = 0
    for _, col in ipairs(GR.COL_DEF) do
        local hideAdvanced = GR.db and GR.db.disableAdvancedRoster and col.advanced
        local hideCustom
        if col.pvp and GR.db and GR.db.guildMode == "pvp" and GR.db.customColumns and GR.db.customColumns.pvpRoster then
            hideCustom = col.customizable and GR.db.customColumns.pvpRoster[col.key] == false
        else
            hideCustom = col.customizable and GR.db and GR.db.customColumns and GR.db.customColumns.roster and GR.db.customColumns.roster[col.key] == false
        end
        local hideSocial = col.social and (not GR.db or GR.db.guildMode ~= "social")
        local hideRaid = col.raid and (not GR.db or GR.db.guildMode ~= "raid")
        local hideMplus = col.mplus  -- Always hide M+ columns in classic
        local hidePvp = col.pvp and (not GR.db or GR.db.guildMode ~= "pvp")
        if (not col.restricted or GR.hasPermission) and not hideAdvanced and not hideCustom and not hideSocial and not hideRaid and not hideMplus and not hidePvp then
            local btn = CreateFrame("Button", nil, innerHeader)
            btn:SetHeight(GR.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(GetColName(col))
            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
    innerHeader:SetWidth(math.max(xOffset, 1))
end

function GR:CreateRosterRow(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(GR.ROW_HEIGHT)
    local totalWidth = 0
    for _, col in ipairs(GR.COL_DEF) do
        local hideAdvanced = GR.db and GR.db.disableAdvancedRoster and col.advanced
        local hideCustom
        if col.pvp and GR.db and GR.db.guildMode == "pvp" and GR.db.customColumns and GR.db.customColumns.pvpRoster then
            hideCustom = col.customizable and GR.db.customColumns.pvpRoster[col.key] == false
        else
            hideCustom = col.customizable and GR.db and GR.db.customColumns and GR.db.customColumns.roster and GR.db.customColumns.roster[col.key] == false
        end
        local hideSocial = col.social and (not GR.db or GR.db.guildMode ~= "social")
        local hideRaid = col.raid and (not GR.db or GR.db.guildMode ~= "raid")
        local hideMplus = col.mplus  -- Always hide M+ columns in classic
        local hidePvp = col.pvp and (not GR.db or GR.db.guildMode ~= "pvp")
        if (not col.restricted or GR.hasPermission) and not hideAdvanced and not hideCustom and not hideSocial and not hideRaid and not hideMplus and not hidePvp 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")
    
    -- HOOK: Interaction Scripts
    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)
    
    -- HOOK: Social Tooltip
    row:SetScript("OnEnter", function(self)
        if self.data and GR.ShowSocialTooltip and GR.db and GR.db.socialEnabled then
            GR:ShowSocialTooltip(self.data)
        end
    end)
    
    row:SetScript("OnLeave", function(self)
        if GR.SocialTooltip then GR.SocialTooltip:Hide() end
    end)

    row.cols = {}
    local xOffset = 0
    for _, def in ipairs(GR.COL_DEF) do
        local hideAdvanced = GR.db and GR.db.disableAdvancedRoster and def.advanced
        local hideCustom
        if def.pvp and GR.db and GR.db.guildMode == "pvp" and GR.db.customColumns and GR.db.customColumns.pvpRoster then
            hideCustom = def.customizable and GR.db.customColumns.pvpRoster[def.key] == false
        else
            hideCustom = def.customizable and GR.db and GR.db.customColumns and GR.db.customColumns.roster and GR.db.customColumns.roster[def.key] == false
        end
        local hideSocial = def.social and (not GR.db or GR.db.guildMode ~= "social")
        local hideRaid = def.raid and (not GR.db or GR.db.guildMode ~= "raid")
        local hideMplus = def.mplus  -- Always hide M+ columns in classic
        local hidePvp = def.pvp and (not GR.db or GR.db.guildMode ~= "pvp")
        if (not def.restricted or GR.hasPermission) and not hideAdvanced and not hideCustom and not hideSocial and not hideRaid and not hideMplus and not hidePvp then
            local f = CreateFrame("Frame", nil, row)
            f:SetHeight(GR.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(NS.L["CLICK_TO_INSPECT"] or "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 == "customNote" then
                 f:EnableMouse(true)
                 f:SetScript("OnEnter", function()
                     if row.data then
                         local s = GRS_SocialDB and GRS_SocialDB[row.data.dbKey]
                         local noteText = s and s.customNote
                         if noteText and noteText ~= "" then
                             GameTooltip:SetOwner(f, "ANCHOR_RIGHT")
                             GameTooltip:AddLine("Custom Note", 1, 0.82, 0)
                             GameTooltip:AddLine(noteText, 1, 1, 1, true)
                             GameTooltip:Show()
                         end
                     end
                 end)
                 f:SetScript("OnLeave", function() GameTooltip:Hide() 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

    -- [[ DYNAMIC WIDTH CALCULATION: STRICT MODE ]]
    local rosterWidth = 0
    for _, col in ipairs(GR.COL_DEF) do
        local hideAdvanced = GR.db and GR.db.disableAdvancedRoster and col.advanced
        local hideCustom
        if col.pvp and GR.db and GR.db.guildMode == "pvp" and GR.db.customColumns and GR.db.customColumns.pvpRoster then
            hideCustom = col.customizable and GR.db.customColumns.pvpRoster[col.key] == false
        else
            hideCustom = col.customizable and GR.db and GR.db.customColumns and GR.db.customColumns.roster and GR.db.customColumns.roster[col.key] == false
        end
        local hideSocial = col.social and (not GR.db or GR.db.guildMode ~= "social")
        local hideRaid = col.raid and (not GR.db or GR.db.guildMode ~= "raid")
        local hideMplus = col.mplus  -- Always hide M+ columns in classic
        local hidePvp = col.pvp and (not GR.db or GR.db.guildMode ~= "pvp")
        if (not col.restricted or GR.hasPermission) and not hideAdvanced and not hideCustom and not hideSocial and not hideRaid and not hideMplus and not hidePvp then
            rosterWidth = rosterWidth + col.width
        end
    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(rosterWidth) 

        GR.RightPaneTop = CreateFrame("Frame", nil, mainContainer, "BackdropTemplate")
        GR.RightPaneTop:SetPoint("TOPRIGHT", 0, 0)
        GR.RightPaneTop:SetPoint("TOPLEFT", GR.RosterSplitMain, "TOPRIGHT", 5, 0)
        GR.RightPaneTop:SetHeight(GR.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")
        GR.RightPaneBottom:SetPoint("TOPLEFT", GR.RightPaneTop, "BOTTOMLEFT", 0, 0); GR.RightPaneBottom:SetPoint("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)

        -- Vertical ScrollFrame (no template scrollbar — we add a Raid-style one)
        local sf = CreateFrame("ScrollFrame", "GRS_RosterScrollDirect", GR.RosterSplitMain)
        sf:SetPoint("TOPLEFT", GR.RosterSplitMain, "TOPLEFT", 0, -GR.HEADER_HEIGHT - 5)
        sf:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", -18, 20)
        local content = CreateFrame("Frame", nil, sf)
        content:SetHeight(500); content:SetWidth(rosterWidth); sf:SetScrollChild(content)
        sf:SetScript("OnSizeChanged", function(self, w, h) content:SetWidth(math.max(w, rosterWidth)) end)
        GR.RosterContent = content; GR.RosterScrollFrame = sf

        -- Raid-style vertical scrollbar (18px, dark track, grey thumb, gold-on-drag, arrows)
        local vSlider = CreateFrame("Slider", nil, GR.RosterSplitMain)
        vSlider:SetWidth(18)
        vSlider:SetPoint("TOPRIGHT", GR.RosterSplitMain, "TOPRIGHT", 0, -GR.HEADER_HEIGHT - 5 - 18)
        vSlider:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", 0, 20 + 18)
        vSlider:SetOrientation("VERTICAL")
        local vTrack = vSlider:CreateTexture(nil, "BACKGROUND")
        vTrack:SetAllPoints(); vTrack:SetColorTexture(0.1, 0.1, 0.1, 1)
        local vThumb = vSlider:CreateTexture(nil, "ARTWORK")
        vThumb:SetSize(14, 24); vThumb:SetColorTexture(0.5, 0.5, 0.5, 1)
        vSlider:SetThumbTexture(vThumb)
        vSlider:SetMinMaxValues(0, 1); vSlider:SetValue(0)
        vSlider:SetScript("OnValueChanged", function(s, v)
            sf:SetVerticalScroll(v)
            vThumb:SetColorTexture(0.8, 0.6, 0, 1)
        end)
        vSlider:SetScript("OnMouseUp", function()
            vThumb:SetColorTexture(0.5, 0.5, 0.5, 1)
        end)
        sf:SetScript("OnScrollRangeChanged", function(self, xRange, yRange)
            local maxV = yRange or 0
            vSlider:SetMinMaxValues(0, math.max(maxV, 0))
        end)
        sf:EnableMouseWheel(true)
        sf:SetScript("OnMouseWheel", function(self, delta)
            local cur = vSlider:GetValue()
            local _, maxV = vSlider:GetMinMaxValues()
            local step = GR.ROW_HEIGHT * 3
            local newVal = math.max(0, math.min(maxV, cur - (delta * step)))
            vSlider:SetValue(newVal)
        end)
        -- Up arrow button
        local vUpBtn = CreateFrame("Button", nil, GR.RosterSplitMain)
        vUpBtn:SetSize(18, 18)
        vUpBtn:SetPoint("TOPRIGHT", GR.RosterSplitMain, "TOPRIGHT", 0, -GR.HEADER_HEIGHT - 5)
        local vUpBg = vUpBtn:CreateTexture(nil, "BACKGROUND")
        vUpBg:SetAllPoints(); vUpBg:SetColorTexture(0.1, 0.1, 0.1, 1)
        local vUpArrow = vUpBtn:CreateTexture(nil, "ARTWORK")
        vUpArrow:SetSize(10, 10); vUpArrow:SetPoint("CENTER")
        vUpArrow:SetTexture("Interface\\Buttons\\Arrow-Up-Up")
        vUpArrow:SetDesaturated(true)
        vUpBtn:SetScript("OnClick", function()
            local cur = vSlider:GetValue()
            local step = GR.ROW_HEIGHT * 3
            vSlider:SetValue(math.max(0, cur - step))
        end)
        vUpBtn:SetScript("OnEnter", function() vUpBg:SetColorTexture(0.2, 0.2, 0.2, 1) end)
        vUpBtn:SetScript("OnLeave", function() vUpBg:SetColorTexture(0.1, 0.1, 0.1, 1) end)
        -- Down arrow button
        local vDownBtn = CreateFrame("Button", nil, GR.RosterSplitMain)
        vDownBtn:SetSize(18, 18)
        vDownBtn:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", 0, 20)
        local vDownBg = vDownBtn:CreateTexture(nil, "BACKGROUND")
        vDownBg:SetAllPoints(); vDownBg:SetColorTexture(0.1, 0.1, 0.1, 1)
        local vDownArrow = vDownBtn:CreateTexture(nil, "ARTWORK")
        vDownArrow:SetSize(10, 10); vDownArrow:SetPoint("CENTER")
        vDownArrow:SetTexture("Interface\\Buttons\\Arrow-Down-Up")
        vDownArrow:SetDesaturated(true)
        vDownBtn:SetScript("OnClick", function()
            local cur = vSlider:GetValue()
            local _, maxV = vSlider:GetMinMaxValues()
            local step = GR.ROW_HEIGHT * 3
            vSlider:SetValue(math.min(maxV, cur + step))
        end)
        vDownBtn:SetScript("OnEnter", function() vDownBg:SetColorTexture(0.2, 0.2, 0.2, 1) end)
        vDownBtn:SetScript("OnLeave", function() vDownBg:SetColorTexture(0.1, 0.1, 0.1, 1) end)
        GR.RosterVSlider = vSlider

        -- Horizontal scrollbar (18px, dark track, grey thumb, gold-on-drag, arrows)
        local hSlider = CreateFrame("Slider", nil, GR.RosterSplitMain)
        hSlider:SetHeight(18)
        hSlider:SetPoint("BOTTOMLEFT", GR.RosterSplitMain, "BOTTOMLEFT", 18, 0)
        hSlider:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", -18, 0)
        hSlider:SetOrientation("HORIZONTAL")
        local hTrack = hSlider:CreateTexture(nil, "BACKGROUND")
        hTrack:SetAllPoints(); hTrack:SetColorTexture(0.1, 0.1, 0.1, 1)
        local hThumb = hSlider:CreateTexture(nil, "ARTWORK")
        hThumb:SetSize(24, 14); hThumb:SetColorTexture(0.5, 0.5, 0.5, 1)
        hSlider:SetThumbTexture(hThumb)
        hSlider:SetMinMaxValues(0, 1); hSlider:SetValue(0)
        hSlider:SetScript("OnValueChanged", function(s, v)
            sf:SetHorizontalScroll(v)
            if GR.RosterHeaderInner then
                GR.RosterHeaderInner:SetPoint("TOPLEFT", -v, 0)
            end
            hThumb:SetColorTexture(0.8, 0.6, 0, 1)
        end)
        hSlider:SetScript("OnMouseUp", function()
            hThumb:SetColorTexture(0.5, 0.5, 0.5, 1)
        end)
        sf:HookScript("OnScrollRangeChanged", function(self, xRange, yRange)
            local maxH = xRange or 0
            hSlider:SetMinMaxValues(0, math.max(maxH, 0))
            if maxH <= 0 then hSlider:Hide() else hSlider:Show() end
        end)
        -- Left arrow button
        local hLeftBtn = CreateFrame("Button", nil, GR.RosterSplitMain)
        hLeftBtn:SetSize(18, 18)
        hLeftBtn:SetPoint("BOTTOMLEFT", GR.RosterSplitMain, "BOTTOMLEFT", 0, 0)
        local hLeftBg = hLeftBtn:CreateTexture(nil, "BACKGROUND")
        hLeftBg:SetAllPoints(); hLeftBg:SetColorTexture(0.1, 0.1, 0.1, 1)
        local hLeftArrow = hLeftBtn:CreateTexture(nil, "ARTWORK")
        hLeftArrow:SetSize(10, 10); hLeftArrow:SetPoint("CENTER")
        hLeftArrow:SetTexture("Interface\\Buttons\\Arrow-Up-Up")
        hLeftArrow:SetDesaturated(true)
        hLeftArrow:SetRotation(math.rad(90))
        hLeftBtn:SetScript("OnClick", function()
            local cur = hSlider:GetValue()
            local step = 50
            hSlider:SetValue(math.max(0, cur - step))
        end)
        hLeftBtn:SetScript("OnEnter", function() hLeftBg:SetColorTexture(0.2, 0.2, 0.2, 1) end)
        hLeftBtn:SetScript("OnLeave", function() hLeftBg:SetColorTexture(0.1, 0.1, 0.1, 1) end)
        -- Right arrow button
        local hRightBtn = CreateFrame("Button", nil, GR.RosterSplitMain)
        hRightBtn:SetSize(18, 18)
        hRightBtn:SetPoint("BOTTOMRIGHT", GR.RosterSplitMain, "BOTTOMRIGHT", 0, 0)
        local hRightBg = hRightBtn:CreateTexture(nil, "BACKGROUND")
        hRightBg:SetAllPoints(); hRightBg:SetColorTexture(0.1, 0.1, 0.1, 1)
        local hRightArrow = hRightBtn:CreateTexture(nil, "ARTWORK")
        hRightArrow:SetSize(10, 10); hRightArrow:SetPoint("CENTER")
        hRightArrow:SetTexture("Interface\\Buttons\\Arrow-Down-Up")
        hRightArrow:SetDesaturated(true)
        hRightArrow:SetRotation(math.rad(90))
        hRightBtn:SetScript("OnClick", function()
            local cur = hSlider:GetValue()
            local _, maxH = hSlider:GetMinMaxValues()
            local step = 50
            hSlider:SetValue(math.min(maxH, cur + step))
        end)
        hRightBtn:SetScript("OnEnter", function() hRightBg:SetColorTexture(0.2, 0.2, 0.2, 1) end)
        hRightBtn:SetScript("OnLeave", function() hRightBg:SetColorTexture(0.1, 0.1, 0.1, 1) end)
        GR.RosterHSlider = hSlider

        GR:BuildRightSide(GR.RightPaneTop, GR.RightPaneBottom)
        GR.RosterRows = {}
    end
    
    if GR.RosterSplitMain then GR.RosterSplitMain:SetWidth(rosterWidth) 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) * GR.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 = RAID_CLASS_COLORS[(d.class or "PRIEST"):upper()] or RAID_CLASS_COLORS["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 r.cols["ach"]:SetText(d.ach) end
        if r.cols["ilvl"] then local ilvlVal = tonumber(d.ilvl) or 0; r.cols["ilvl"]:SetText(ilvlVal > 0 and (GetIlvlColor(ilvlVal) .. d.ilvl .. "|r") or "-") end
        -- Advanced columns
        if r.cols["raidN"] then r.cols["raidN"]:SetText(RaidProgressColor(d.raidN) .. d.raidN .. "|r") end
        if r.cols["raidH"] then r.cols["raidH"]:SetText(RaidProgressColor(d.raidH) .. d.raidH .. "|r") end
        -- Raid mode columns
        if r.cols["raidRaidN"] then r.cols["raidRaidN"]:SetText(RaidProgressColor(d.raidN) .. d.raidN .. "|r") end
        if r.cols["raidRaidH"] then r.cols["raidRaidH"]:SetText(RaidProgressColor(d.raidH) .. d.raidH .. "|r") end
        -- PVP mode columns
        if r.cols["pvp2v2"] then local v = d.pvp2v2 or 0; r.cols["pvp2v2"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvp3v3"] then local v = d.pvp3v3 or 0; r.cols["pvp3v3"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvpSolo"] then local v = d.pvpSolo or 0; r.cols["pvpSolo"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvpRbg"] then local v = d.pvpRbg or 0; r.cols["pvpRbg"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvpBest"] then local v = d.pvpBest or 0; r.cols["pvpBest"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvpExp"] then local v = d.pvpExp or 0; r.cols["pvpExp"]:SetText(v > 0 and (GR:PvpRatingColor(v) .. v .. "|r") or "-") end
        if r.cols["pvpSpec"] then r.cols["pvpSpec"]:SetText(d.pvpSpec or "-") end
        -- Social columns: pull from GRS_SocialDB
        if r.cols["race"] then r.cols["race"]:SetText(d.race or "-") end
        if r.cols["joinDate"] or r.cols["promDate"] or r.cols["birthDate"] or r.cols["customNote"] then
            local soc = GRS_SocialDB and GRS_SocialDB[d.dbKey] or {}
            if r.cols["joinDate"] then
                local jm, jd, jy = soc.joinMonth, soc.joinDay, soc.joinYear
                local jYear = (jy and jy ~= "") and (" '" .. jy:sub(-2)) or ""
                r.cols["joinDate"]:SetText(jm and (jm:sub(1,3) .. " " .. (jd or "") .. jYear) or "-")
            end
            if r.cols["promDate"] then
                local pm, pd, py = soc.promMonth, soc.promDay, soc.promYear
                local pYear = (py and py ~= "") and (" '" .. py:sub(-2)) or ""
                r.cols["promDate"]:SetText(pm and (pm:sub(1,3) .. " " .. (pd or "") .. pYear) or "-")
            end
            if r.cols["birthDate"] then
                local bm, bd, by = soc.birthMonth, soc.birthDay, soc.birthYear
                local bYear = (by and by ~= "") and (" '" .. by:sub(-2)) or ""
                r.cols["birthDate"]:SetText(bm and (bm:sub(1,3) .. " " .. (bd or "") .. bYear) or "-")
            end
            if r.cols["customNote"] then
                r.cols["customNote"]:SetText(soc.customNote or "-")
            end
        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 * GR.ROW_HEIGHT)
end

-- =========================================================================
-- 4. MAIN BUILD FUNCTION
-- =========================================================================
function GR:BuildRosterView(parent)
    if not parent then return end
    if parent.isRosterBuilt then return end
    parent.isRosterBuilt = true

    for _, region in ipairs({parent:GetRegions()}) do
        if region:GetObjectType() == "FontString" then region:Hide() end
    end
    
    local gmotdLabel = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    gmotdLabel:SetPoint("TOPLEFT", 20, -50)
    gmotdLabel:SetPoint("TOPRIGHT", -20, -50)
    gmotdLabel:SetJustifyH("CENTER")
    gmotdLabel:SetTextColor(0, 1, 0, 1)
    
    gmotdLabel:SetText(GetGuildRosterMOTD() or NS.L["NO_MOTD"] or "No Message of the Day")
    parent.gmotdLabel = gmotdLabel
    
    local motdFrame = CreateFrame("Frame", nil, parent)
    motdFrame:RegisterEvent("GUILD_MOTD")
    motdFrame:RegisterEvent("GUILD_ROSTER_UPDATE")
    
    local function UpdateMOTD()
        local msg = GetGuildRosterMOTD()
        if msg and msg ~= "" then
            gmotdLabel:SetText(msg)
        else
            gmotdLabel:SetText(NS.L["NO_MOTD"] or "No Message of the Day")
        end
    end

    motdFrame:SetScript("OnEvent", function() 
        if parent:IsVisible() then UpdateMOTD() end 
    end)
    
    parent:HookScript("OnShow", function()
        if GuildRoster then GuildRoster() end
        UpdateMOTD()
    end)

    local listContainer = CreateFrame("Frame", "GRS_RosterListFrame", parent, "BackdropTemplate")
    listContainer:SetPoint("TOPLEFT", 10, -80)
    listContainer:SetPoint("BOTTOMRIGHT", -10, 50)
    listContainer: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 }
    })
    listContainer:SetBackdropColor(0, 0, 0, 0.8)
    GR.rosterListContainer = listContainer

    local btnHeight = 22
    local btnWidth = 120

    local btnInvite = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btnInvite:SetSize(btnWidth, btnHeight)
    btnInvite:SetPoint("BOTTOMLEFT", 10, 10)
    btnInvite:SetText(NS.L["INVITE_MEMBER"] or "Invite Member")
    SkinButton(btnInvite)
    if CanGuildInvite() then btnInvite:Enable() else btnInvite:Disable() end
    btnInvite:SetScript("OnClick", function() StaticPopup_Show("ADD_GUILDMEMBER") end)

    local btnSettings = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btnSettings:SetSize(btnWidth, btnHeight)
    btnSettings:SetPoint("LEFT", btnInvite, "RIGHT", 10, 0)
    btnSettings:SetText(NS.L["GUILD_SETTINGS"] or "Guild Settings")
    SkinButton(btnSettings)
    if GR:IsGM() then btnSettings:Show() else btnSettings:Hide() end
    btnSettings:SetScript("OnClick", function() 
        if not InCombatLockdown() then
            if not GuildControlUI then GuildControlUI_LoadUI() end
            if GuildControlUI then GuildControlUI:Show() end
        end
    end)

    local btnRecruit = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btnRecruit:SetSize(btnWidth, btnHeight)
    btnRecruit:SetPoint("LEFT", btnSettings, "RIGHT", 10, 0)
    btnRecruit:SetText(NS.L["RECRUITMENT"] or "Recruitment")
    SkinButton(btnRecruit)
    if GR:IsOfficer() or GR:IsGM() then btnRecruit:Show() else btnRecruit:Hide() end
    
    btnRecruit:SetScript("OnClick", function() 
        if not InCombatLockdown() then
            if LoadAddOn then LoadAddOn("Blizzard_Communities") end
            local dialog = CommunitiesFrame and CommunitiesFrame.RecruitmentDialog
            if dialog then
                dialog:SetParent(UIParent)
                dialog:SetFrameStrata("DIALOG")
                dialog:Show()
            else
                print("|cffff0000GRS:|r Recruitment Dialog not available in this client.")
            end
        end
    end)

    local chkReplace = CreateFrame("CheckButton", nil, parent, "ChatConfigCheckButtonTemplate")
    chkReplace:SetPoint("BOTTOMRIGHT", -200, 10)
    chkReplace.Text:SetText(NS.L["REPLACE_NATIVE_FRAME"] or "Replace Native Frame")
    if GRS_DB and GRS_DB.ReplaceGuildFrame then chkReplace:SetChecked(true) else chkReplace:SetChecked(false) end
    
    chkReplace:SetScript("OnClick", function(self)
        if not GRS_DB then GRS_DB = {} end
        GRS_DB.ReplaceGuildFrame = self:GetChecked()
        print("|cff00ff00GRS:|r Native Frame Replacement: " .. (GRS_DB.ReplaceGuildFrame and "ON" or "OFF"))
    end)
    
    if not GR.hookedToggle then
        hooksecurefunc("ToggleGuildFrame", function()
            if GRS_DB and GRS_DB.ReplaceGuildFrame then
                if CommunitiesFrame and CommunitiesFrame:IsShown() then HideUIPanel(CommunitiesFrame) end
                if GR:IsShown() then GR:Hide() else GR:Show() end
            end
        end)
        GR.hookedToggle = true
    end
    
    if GR.UpdateRosterList then GR:UpdateRosterList() end
    
    -- print("|cff00ff00GRS:|r Roster UI Built.")
end

local loader = CreateFrame("Frame")
loader:RegisterEvent("PLAYER_ENTERING_WORLD")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self, event, arg1)
    if GR and GR.views and GR.views[5] then
        GR:BuildRosterView(GR.views[5])
        self:UnregisterAllEvents() 
    end
end)