local addonName, NS = ...

-- =========================================================================
-- 1. HELPERS & UTILITIES
-- =========================================================================

local function CreateBg(f, r, g, b, a)
    local t = f:CreateTexture(nil, "BACKGROUND")
    t:SetAllPoints()
    t:SetColorTexture(r, g, b, a)
    f.bg = t
end

local function CreateBorder(f, r, g, b, a)
    -- Remove existing border if any
    if f.border then f.border:Hide() end
    
    local border = f:CreateTexture(nil, "BORDER")
    border:SetPoint("TOPLEFT", -1, 1)
    border:SetPoint("BOTTOMRIGHT", 1, -1)
    border:SetColorTexture(r, g, b, a)
    f.border = border
end

-- [[ UNIFORM BUTTON STYLER ]]
local function SkinButton(btn)
    if not btn then return end
    
    -- Strip standard textures
    btn:SetNormalTexture("")
    btn:SetHighlightTexture("")
    btn:SetPushedTexture("")
    btn:SetDisabledTexture("")
    
    -- Hide Template Textures (Left/Middle/Right) if they exist
    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
    
    -- Apply Custom Style
    CreateBg(btn, 0.2, 0.2, 0.2, 1) -- Dark Grey
    CreateBorder(btn, 0, 0, 0, 1)   -- Black Border
    
    -- Hover Effects
    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)
    
    -- Ensure Text is Visible/White
    local fs = btn:GetFontString()
    if fs then fs:SetTextColor(1, 1, 1) end
end

-- Dynamic Region Detection for RIO
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
    elseif factionGroup == "Horde" then return 2
    else return 2 end 
end

local function GetTableSize(t)
    local count = 0
    if t then for _ in pairs(t) do count = count + 1 end end
    return count
end

local CLASS_LIST = {
    "Death Knight", "Demon Hunter", "Druid", "Evoker", "Hunter", 
    "Mage", "Monk", "Paladin", "Priest", "Rogue", 
    "Shaman", "Warlock", "Warrior"
}

local RACES = {
    ["Alliance"] = {"Human", "Dwarf", "Night Elf", "Gnome", "Draenei", "Worgen", "Pandaren", "Void Elf", "Lightforged Draenei", "Dark Iron Dwarf", "Kul Tiran", "Mechagnome", "Dracthyr", "Earthen"},
    ["Horde"] = {"Orc", "Undead", "Tauren", "Troll", "Blood Elf", "Goblin", "Pandaren", "Nightborne", "Highmountain Tauren", "Mag'har Orc", "Zandalari Troll", "Vulpera", "Dracthyr", "Earthen"}
}

-- [[ TAB DEFINITIONS ]]
local TABS = {
    { id = 1, title = "Guild Recruiter System", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Guild Recruitment System.tga" },
    { id = 2, title = "Recruitment Blacklist", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Recruitment Blacklist.tga" },
    { id = 3, title = "Recruitment requirements", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Recruitment requirements.tga" },
    { id = 4, title = "Recruitment History", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Recruitment History.tga" },
    { id = 5, title = "Guild Roster", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Guild Roster.tga" },
    { id = 6, title = "Raid Roster", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Raid Roster.tga" },
    { id = 7, title = "Settings", icon = "Interface\\AddOns\\GuildRecruiterManagement\\Icons\\Settings.tga" }
}

-- =========================================================================
-- 2. MAIN FRAME & SLASH COMMANDS
-- =========================================================================

local GR = CreateFrame("Frame", "GRS_MainFrame", UIParent)
GR:Hide() 

GR:SetFrameStrata("DIALOG") 
GR:SetToplevel(true)
GR:EnableMouse(true)
GR:SetMovable(true)
GR:SetResizable(true)
GR:SetClampedToScreen(true)

-- Initial Default Size (Will be overwritten by DB)
GR:SetSize(1280, 720)
GR:SetPoint("CENTER")

CreateBg(GR, 0, 0, 0, 0.85)
CreateBorder(GR, 0.3, 0.3, 0.3, 1)

-- State Flags
GR.isInitialized = false
GR.foundPlayers = {}    
GR.currentTemplate = "M+ Focused"
GR.rows = {}            -- Pool for Result Rows
GR.chatFrames = {}      -- Pool for Chat Frames
GR.activeChats = {} 
GR.currentPage = 1 
GR.classIndex = 1
GR.selectedFilterData = nil 
GR.isScanning = false 
GR.lastScanTime = 0 
-- Sorting State
GR.sortKey = "name"
GR.sortOrder = "ASC"    
-- View State
GR.activeTab = 1
GR.views = {}

-- DEFAULTS
local DEFAULTS = {
    debugMode = false,
    remixMode = "OFF", 
    scanDelay = 0.8,
    minLevel = 80, -- UPDATED: TWW / Season 3 Max Level
    maxLevel = 80,
    templates = NS.Data and NS.Data.DefaultTemplates or {["Default"] = "Hello!"}, 
    blacklist = {},
    recentHistory = {},
    messageTemplate = "M+ Focused",
    customFilters = {},
    -- UI PERSISTENCE DEFAULTS
    scale = 1.0,
    alpha = 1.0, 
    windowWidth = 1280,
    windowHeight = 720
    -- Removed: rightPaneCollapsed
}

-- SLASH COMMANDS
SLASH_GUILDRECRUITERSYSTEM1 = "/grs"
SlashCmdList["GUILDRECRUITERSYSTEM"] = function(msg) 
    if not GR.isInitialized then
        GR:Initialize()
    end
    
    if msg == "rio" then
        print("|cff00ffff--- GRS RIO DIAGNOSTIC ---|r")
        return
    end

    if GR:IsShown() then GR:Hide() else GR:Show() end 
end

-- =========================================================================
-- 3. INITIALIZATION & EVENTS
-- =========================================================================

GR:RegisterEvent("ADDON_LOADED")
GR:RegisterEvent("WHO_LIST_UPDATE")
GR:RegisterEvent("CHAT_MSG_WHISPER")
GR:RegisterEvent("CHAT_MSG_WHISPER_INFORM")
GR:RegisterEvent("PLAYER_ENTERING_WORLD") 
GR:RegisterEvent("CHAT_MSG_ADDON")
GR:RegisterEvent("PLAYER_GUILD_UPDATE") -- Added for Officer Status check

GR:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" and ... == addonName then
        if not self.isInitialized then self:Initialize() end
        
        -- [[ RESTORE SAVED UI STATE ]]
        if self.db.scale then 
            self:SetScale(self.db.scale) 
            self:UpdateResizeBounds() 
        end
        
        -- [[ RESTORE SAVED ALPHA ]]
        if self.db.alpha then
            self:SetAlpha(self.db.alpha)
        end
        
        if self.db.windowWidth and self.db.windowHeight then
            self:SetSize(self.db.windowWidth, self.db.windowHeight)
        end
        
        if self.UpdateRecentList then self:UpdateRecentList() end
        
        -- Register Sync Prefix
        C_ChatInfo.RegisterAddonMessagePrefix("GRS_SYNC")
        
        local blSize = self.db and self.db.blacklist and GetTableSize(self.db.blacklist) or 0
        print("|cff00ff00GRS V1.2.1 (Season 3 Ready) Loaded.|r")
        print("|cff00ffff[System]: Blacklist integrity check: " .. blSize .. " entries found.|r")
        
    elseif event == "PLAYER_ENTERING_WORLD" then
        if self.isInitialized then 
            self:UpdateRaiderIOStatus() 
            self:UpdateOfficerStatus()
        end
    elseif event == "WHO_LIST_UPDATE" then
        self:ProcessWhoResults()
    elseif event == "CHAT_MSG_WHISPER" or event == "CHAT_MSG_WHISPER_INFORM" then
        self:HandleChatEvent(event, ...)
    elseif event == "CHAT_MSG_ADDON" then
        if self.HandleComm then self:HandleComm(...) end
    elseif event == "PLAYER_GUILD_UPDATE" then
        if self.isInitialized then self:UpdateOfficerStatus() end
    end
end)

function GR:Initialize()
    self:InitDB()
    self.db.debugMode = false -- FORCE DEBUG OFF to override SavedVariables
    
    self:BuildStructuralUI() -- Build Sidebar and Container
    self:BuildHomeView()     -- View 1: Original UI
    -- Blacklist module (moved to Blacklist.lua)
    if NS and NS.Blacklist and NS.Blacklist.Attach then
        NS.Blacklist.Attach(self)
    end
    if self.BuildBlacklistView then
        self:BuildBlacklistView() -- View 2: Blacklist Management
    end
    
    -- [[ MODULAR LOADING ]]
    -- Instead of Placeholders, we create empty containers so other modules
    -- can attach their content.
    self:BuildModuleContainers() 
    self:BuildSettingsView() 
    
    self:InjectGlobalMenus()
    self:UpdateRecentList()
    
    -- Set Initial View
    self:SwitchView(1)
    
    self.isInitialized = true
    self:UpdateOfficerStatus()
end

function GR:InitDB()
    if not _G.GRS_DB then _G.GRS_DB = {} end
    if not _G.GRS_DB.blacklist then _G.GRS_DB.blacklist = {} end
    if not _G.GRS_DB.recentHistory then _G.GRS_DB.recentHistory = {} end
    if not _G.GRS_DB.templates then _G.GRS_DB.templates = {["Default"] = "Hi there!"} end
    if not _G.GRS_DB.customFilters then _G.GRS_DB.customFilters = {} end

    for k, v in pairs(DEFAULTS) do
        if _G.GRS_DB[k] == nil then _G.GRS_DB[k] = v end
    end
    self.db = _G.GRS_DB
    
    -- Migration: Ensure blacklist entries are tables
    for k, v in pairs(self.db.blacklist) do
        if type(v) == "boolean" then
            self.db.blacklist[k] = {
                name = k,
                rio = 0,
                heroic = "-",
                mythic = "-",
                race = "Unknown",
                fullClass = "Unknown",
                level = 0,
                zone = "Unknown"
            }
        end
    end
end

-- =========================================================================
-- 4. UI STRUCTURE: TABS & VIEWS
-- =========================================================================

function GR:UpdateResizeBounds()
    local currentScale = self:GetScale() or 1
    local maxWidth = 1750 / currentScale
    local maxHeight = 745 / currentScale
    
    -- Updated Min Width to 1235 and Min Height to 630 as requested
    self:SetResizeBounds(1750, 745, maxWidth, maxHeight)
end

function GR:BuildStructuralUI()
    self:RegisterForDrag("LeftButton")
    self:SetScript("OnDragStart", self.StartMoving)
    self:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
    end)
    
    self:UpdateResizeBounds()

    local resize = CreateFrame("Button", nil, self)
    resize:SetSize(16, 16); resize:SetPoint("BOTTOMRIGHT")
    resize:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    resize:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
    resize:SetScript("OnMouseDown", function() self:StartSizing("BOTTOMRIGHT") end)
    resize:SetScript("OnMouseUp", function() 
        self:StopMovingOrSizing() 
        if self.db then
            self.db.windowWidth = self:GetWidth()
            self.db.windowHeight = self:GetHeight()
        end
    end)
    
    table.insert(UISpecialFrames, self:GetName()) 
    
    -- 1. Sidebar (The Left Navigation Pane)
    local sideBar = CreateFrame("Frame", "GRS_SideBar", self)
    sideBar:SetWidth(40)
    sideBar:SetPoint("TOPLEFT", 0, 0)
    sideBar:SetPoint("BOTTOMLEFT", 0, 0)
    CreateBg(sideBar, 0.15, 0.15, 0.15, 1)
    CreateBorder(sideBar, 0, 0, 0, 1)
    self.SideBar = sideBar
    
    -- 2. View Container (The content area)
    local container = CreateFrame("Frame", "GRS_ViewContainer", self)
    container:SetPoint("TOPLEFT", sideBar, "TOPRIGHT", 0, 0)
    container:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
    self.ViewContainer = container

    -- 3. Top Title Bar (Inside Container)
    local top = CreateFrame("Frame", nil, container)
    CreateBg(top, 0.1, 0.1, 0.1, 1)
    top:SetHeight(30); top:SetPoint("TOPLEFT"); top:SetPoint("TOPRIGHT")
    self.TitleBar = top
    
    local title = top:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("LEFT", 10, 0); title:SetText("Guild Recruiter System")
    self.MainTitle = title
    
    local ioStatus = top:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    ioStatus:SetPoint("LEFT", title, "RIGHT", 10, 0)
    self.ioStatus = ioStatus; self:UpdateRaiderIOStatus()
    
    -- [[ OFFICER STATUS DISPLAY ]]
    local offStatus = top:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    offStatus:SetPoint("LEFT", ioStatus, "RIGHT", 10, 0)
    self.officerStatus = offStatus
    
    local close = CreateFrame("Button", nil, top)
    close:SetPoint("TOPRIGHT", -5, -2) 
    close:SetSize(24, 24)
    close:SetNormalFontObject("GameFontNormal")
    close:SetText("X")
    close:SetScript("OnClick", function() self:Hide() end)
    SkinButton(close)

    -- 4. Generate Tabs
    self.Tabs = {}
    for i, data in ipairs(TABS) do
        local tab = CreateFrame("Button", nil, sideBar)
        tab:SetSize(32, 32)
        tab:SetPoint("TOP", sideBar, "TOP", 0, -10 - ((i-1) * 40))
        
        -- Icon
        local tex = tab:CreateTexture(nil, "ARTWORK")
        tex:SetAllPoints()
        tex:SetTexture(data.icon)
        tab.icon = tex
        
        -- Hover Logic (Floating Tooltip 5s)
        tab:SetScript("OnEnter", function(s)
            GameTooltip:SetOwner(s, "ANCHOR_CURSOR")
            GameTooltip:SetText(data.title)
            GameTooltip:Show()
            
            -- Timer to hide after 5 seconds even if still hovering
            if s.timer then s.timer:Cancel() end
            s.timer = C_Timer.NewTimer(5, function()
                GameTooltip:Hide()
            end)
            
            if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end
        end)
        
        tab:SetScript("OnLeave", function(s)
            GameTooltip:Hide()
            if s.timer then s.timer:Cancel() end
            if s.bg then 
                 -- Reset alpha based on selection
                 if GR.activeTab == i then
                     s.bg:SetColorTexture(1, 1, 1, 0.2)
                 else
                     s.bg:SetColorTexture(0, 0, 0, 0)
                 end
            end
        end)
        
        tab:SetScript("OnClick", function()
            if data.id == 7 and GR.OpenSettings then
                GR:OpenSettings()
            else
                GR:SwitchView(data.id)
            end
        end)
        -- Selection Background
        local selBg = tab:CreateTexture(nil, "BACKGROUND")
        selBg:SetAllPoints()
        selBg:SetColorTexture(1, 1, 1, 0.2)
        selBg:Hide()
        tab.selBg = selBg
        
        self.Tabs[i] = tab
    end
end

function GR:SwitchView(id)
    self.activeTab = id
    
    -- Update Tabs
    for i, tab in ipairs(self.Tabs) do
        if i == id then
            tab:SetAlpha(1.0)
            tab.selBg:Show()
        else
            tab:SetAlpha(0.6)
            tab.selBg:Hide()
        end
    end
    
    -- Toggle Views
    for viewId, frame in pairs(self.views) do
        if viewId == id then
            frame:Show()
        else
            frame:Hide()
        end
    end
    
    -- Update Title
    self.MainTitle:SetText(TABS[id].title)
end

local function CreateCustomScrollbar(parent, orientation, targetScrollFrame)
    local slider = CreateFrame("Slider", nil, parent)
    slider:SetOrientation(orientation)
    local isVert = (orientation == "VERTICAL")
    
    local track = slider:CreateTexture(nil, "BACKGROUND")
    track:SetAllPoints()
    track:SetColorTexture(0, 0, 0, 0.8)
    
    local thumb = slider:CreateTexture(nil, "OVERLAY")
    thumb:SetTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
    thumb:SetSize(24, 24)
    slider:SetThumbTexture(thumb)
    
    if isVert then
        slider:SetWidth(16)
        slider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -2, -18)
        slider:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -2, 18)
    else
        slider:SetHeight(16)
        slider:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 18, -5)
        slider:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", -18, -5)
    end
    
    slider:SetScript("OnValueChanged", function(s, val)
        if isVert then targetScrollFrame:SetVerticalScroll(val)
        else targetScrollFrame:SetHorizontalScroll(val) end
    end)

    return slider
end

-- =========================================================================
-- 5. VIEW 1: HOME (ORIGINAL GRS UI)
-- =========================================================================

function GR:BuildHomeView()
    local parent = CreateFrame("Frame", "GRS_HomeView", self.ViewContainer)
    parent:SetPoint("TOPLEFT", 0, -30) -- Below TitleBar
    parent:SetPoint("BOTTOMRIGHT", 0, 0)
    self.views[1] = parent

    -- OUTER SCROLL
    local outerScroll = CreateFrame("ScrollFrame", "GRS_OuterScroll", parent, "UIPanelScrollFrameTemplate")
    -- Anchor offsets define margins for scrollbars:
    -- X: -28 (Right margin)
    -- Y: 25 (Bottom margin)
    outerScroll:SetPoint("TOPLEFT", 0, 0)
    outerScroll:SetPoint("BOTTOMRIGHT", -28, 25) 
    
    if outerScroll.ScrollBar then outerScroll.ScrollBar:Hide(); outerScroll.ScrollBar:SetWidth(0) end
    
    local content = CreateFrame("Frame", "GRS_ScrollChild", outerScroll)
    -- No hardcoded size here; strictly handled in OnSizeChanged
    outerScroll:SetScrollChild(content)
    
    -- PANES
    local left = CreateFrame("Frame", nil, content)
    left:SetWidth(320); left:SetPoint("TOPLEFT"); left:SetPoint("BOTTOMLEFT")
    CreateBg(left, 0.1, 0.1, 0.1, 0.5); CreateBorder(left, 0.2, 0.2, 0.2, 1)
    self:BuildLeftPane(left)

    -- Right Pane (Dynamic Width)
    local rightPane = CreateFrame("Frame", "GRS_RightPane", content)
    -- [[ FIXED: Always Expanded, Width 500 ]]
    rightPane:SetWidth(500)
    rightPane:SetPoint("TOPRIGHT"); rightPane:SetPoint("BOTTOMRIGHT")
    -- Create Background with Dark color
    CreateBg(rightPane, 0.05, 0.05, 0.05, 0.8)
    CreateBorder(rightPane, 0.2, 0.2, 0.2, 1)
    self:BuildWhisperPane(rightPane)

    local center = CreateFrame("Frame", "GRS_CenterPane", content)
    center:SetPoint("TOPLEFT", left, "TOPRIGHT")
    center:SetPoint("BOTTOMRIGHT", rightPane, "BOTTOMLEFT")
    self:BuildCenterPane(center)
    self:BuildFilterUI()
    
    -- [[ FIXED LAYOUT ENGINE: STATIC SIZING ]]
    parent:SetScript("OnSizeChanged", function(self, w, h)
        -- Margin adjustments for scrollbars
        local scrollW = w - 52
        local scrollH = h - 45

        -- Define FIXED minimum widths
        local leftPaneW = 320
        -- Right pane is ALWAYS 500 now
        local rightPaneW = 500 
        -- Center pane needs at least 760px to show buttons and grid without cutting off
        local minCenterW = 760 

        -- Calculate the Required Minimum Width for the entire content
        local requiredMinW = leftPaneW + minCenterW + rightPaneW

        -- Force content width to be the greater of:
        -- 1. The visible window width (fitting normally)
        -- 2. The required minimum width (forcing scrollbar)
        local finalContentW = math.max(scrollW, requiredMinW)
        
        -- Strictly use available height, no minimum that forces scrollbars unnecessarily
        local finalH = math.max(scrollH, 1)
        
        content:SetWidth(finalContentW)
        content:SetHeight(finalH)
        
        local listAvailable = finalH - 115
        if listAvailable < 20 then listAvailable = 20 end

        local whisperAvailable = finalH - 50
        if whisperAvailable < 20 then whisperAvailable = 20 end

        if GR.UpdateResultsLayout then GR:UpdateResultsLayout(listAvailable) end
        if GR.UpdateWhisperLayout then GR:UpdateWhisperLayout(whisperAvailable) end
    end)
    
    -- Helper to force update layout manually (called when toggling panes)
    GR.UpdateHomeLayout = function()
        local w, h = parent:GetSize()
        parent:GetScript("OnSizeChanged")(parent, w, h)
    end
end

-- =========================================================================
-- 6. MODULAR LOADING SYSTEM (Tabs 3-6)
-- =========================================================================

function GR:BuildModuleContainers()
    -- Instead of creating "Placeholder" views with text, we create clean
    -- empty frames. The individual Lua modules (RecruitmentRequirements, etc.)
    -- will hook into GR:Initialize() and populate these frames if they exist.
    
    for i = 3, 6 do
        local f = CreateFrame("Frame", nil, self.ViewContainer)
        f:SetAllPoints()
        f:Hide()
        self.views[i] = f
    end
end

-- =========================================================================
-- 7. SETTINGS (TAB 7)
-- =========================================================================

function GR:BuildSettingsView()
    local parent = CreateFrame("Frame", "GRS_SettingsView", self.ViewContainer)
    parent:SetPoint("TOPLEFT", 0, -30) -- Below TitleBar
    parent:SetPoint("BOTTOMRIGHT", 0, 0)
    parent:Hide()
    self.views[7] = parent

    -- Dim overlay (popup feel)
    local dim = CreateFrame("Frame", nil, parent)
    dim:SetAllPoints()
    CreateBg(dim, 0, 0, 0, 0.45)

    -- Settings panel
    local panel = CreateFrame("Frame", nil, parent)
    panel:SetSize(560, 300)
    panel:SetPoint("CENTER", 0, 0)
    CreateBg(panel, 0.08, 0.08, 0.08, 0.95)
    CreateBorder(panel, 0.3, 0.3, 0.3, 1)
    self.SettingsPanel = panel

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 15, -12)
    title:SetText("Settings")

    local close = CreateFrame("Button", nil, panel)
    close:SetSize(24, 24)
    close:SetPoint("TOPRIGHT", -8, -8)
    close:SetNormalFontObject("GameFontNormal")
    close:SetText("X")
    close:SetScript("OnClick", function()
        GR:CloseSettings()
    end)
    SkinButton(close)

    local info = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    info:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    info:SetWidth(530)
    info:SetJustifyH("LEFT")
    info:SetText("Configure global preferences for the Guild Recruiter System. More options will be added in a future update.")

    -- Section: UI Scale
    local hdr = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    hdr:SetPoint("TOPLEFT", 15, -70)
    hdr:SetText("UI Scale")

    local help = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    help:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", 0, -6)
    help:SetWidth(530)
    help:SetJustifyH("LEFT")
    help:SetText("Adjusts the overall scale of the Guild Recruiter System window.")

    local val = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    val:SetPoint("TOPRIGHT", -15, -112)

    local function Round2(v)
        return math.floor(v * 100 + 0.5) / 100
    end

    local function Format2(v)
        return string.format("%.2f", v)
    end

    -- Native WoW slider (clean, consistent visuals)
    local slider = CreateFrame("Slider", "GRS_SettingsScaleSlider", panel, "OptionsSliderTemplate")
    slider:SetPoint("TOPLEFT", 15, -120)
    slider:SetWidth(360)
    slider:SetMinMaxValues(0.6, 1.4)
    slider:SetValueStep(0.05)
    slider:SetObeyStepOnDrag(true)

    local sName = slider:GetName()
    local sText = _G[sName .. "Text"]
    local sLow  = _G[sName .. "Low"]
    local sHigh = _G[sName .. "High"]
    if sText then sText:SetText("") end
    if sLow  then sLow:SetText("0.60") end
    if sHigh then sHigh:SetText("1.40") end

    slider:SetValue(self.db and self.db.scale or 1.0)


    local function UpdateScaleVisual(v)
        v = Round2(v)
        val:SetText("Current: " .. Format2(v))
    end

    local function ApplyScale(v)
        v = Round2(v)
        if not GR.db then return end

        GR.db.scale = v
        GR:SetScale(v)
        GR:UpdateResizeBounds()
        UpdateScaleVisual(v)
        print("GRS: UI Scale set to " .. Format2(v))
    end

    slider:SetScript("OnValueChanged", function(_, v)
        UpdateScaleVisual(v)
    end)

    slider:SetScript("OnMouseUp", function(s)
        ApplyScale(s:GetValue())
    end)

    UpdateScaleVisual(slider:GetValue())

    local note = panel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    note:SetPoint("BOTTOMLEFT", 15, 14)
    note:SetWidth(530)
    note:SetJustifyH("LEFT")
    note:SetText("Note: More settings options are coming in a future update.")
end

function GR:OpenSettings()
    self._settingsPrevTab = self.activeTab or 1
    self:SwitchView(7)
end

function GR:CloseSettings()
    local back = self._settingsPrevTab or 1
    if back == 7 then back = 1 end
    self:SwitchView(back)
end

-- =========================================================================
-- 8. CORE SEARCH & CHAT LOGIC
-- =========================================================================

function GR:InjectGlobalMenus()
    if Menu and Menu.ModifyMenu then
        local function AddGuildInvite(rootDescription, contextData)
            local name = contextData.name
            if not name and contextData.unit then name = UnitName(contextData.unit) end
            if name then
                name = Ambiguate(name, "none")
                rootDescription:CreateDivider()
                rootDescription:CreateTitle("GRS Actions")
                rootDescription:CreateButton("Invite to Guild", function() GuildInvite(name) end)
            end
        end
        Menu.ModifyMenu("MENU_UNIT_PLAYER", function(owner, root, context) AddGuildInvite(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_FRIEND", function(owner, root, context) AddGuildInvite(root, context) end)
        Menu.ModifyMenu("MENU_CHAT_PLAYER", function(owner, root, context) AddGuildInvite(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_PARTY", function(owner, root, context) AddGuildInvite(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_RAID", function(owner, root, context) AddGuildInvite(root, context) end)
    end
end

function GR:StartSearch()
    local now = GetTime()
    if (now - self.lastScanTime) < 5 then
        return
    end

    self:UpdateRaiderIOStatus() 
    self.lastScanTime = now
    self.isScanning = true 

    -- VISUAL COOLDOWN ON BUTTON
    if self.SearchBtn then
        self.SearchBtn:Disable()
        local cd = 5
        self.SearchBtn:SetText("|cffff0000Cooldown for search ("..cd..")|r")
        
        C_Timer.NewTicker(1, function(timer)
            cd = cd - 1
            if cd > 0 then
                if self.SearchBtn then self.SearchBtn:SetText("|cffff0000Cooldown for search ("..cd..")|r") end
            else
                timer:Cancel()
                if self.SearchBtn then 
                    self.SearchBtn:Enable()
                    self.SearchBtn:SetText("Search Players")
                end
            end
        end, 5)
    end

    C_Timer.After(60, function()
        if GR.isScanning then
            GR.isScanning = false
            if GR.StatusText then GR.StatusText:SetText("Scan Timed Out - Ready") end
        end
    end)

    local query = ""
    local filterName = "Custom"
    local modeLabel = ""
    if self.db.remixMode == "OFF" then modeLabel = "Retail"
    elseif self.db.remixMode == "ALLOW" then modeLabel = "All Zones"
    elseif self.db.remixMode == "ONLY" then modeLabel = "Remix Only" end

    local filterType = self.filterType or "Default"
    local activeClass = nil
    local activeRace = nil

    if filterType == "Default" then
        activeClass = CLASS_LIST[self.classIndex]
        self.classIndex = self.classIndex + 1
        if self.classIndex > #CLASS_LIST then self.classIndex = 1 end
    else
        if self.selectedFilterData then
            if self.selectedFilterData.class == "Any" then
                activeClass = CLASS_LIST[self.classIndex]
                self.classIndex = self.classIndex + 1
                if self.classIndex > #CLASS_LIST then self.classIndex = 1 end
            else
                activeClass = self.selectedFilterData.class
            end
            if self.selectedFilterData.race ~= "Any" then activeRace = self.selectedFilterData.race end
            filterName = self.selectedFilterData.name
        end
    end

    local parts = {}
    if activeClass then table.insert(parts, 'c-"'..activeClass..'"') end
    if activeRace then table.insert(parts, 'r-"'..activeRace..'"') end
    table.insert(parts, string.format("%d-%d", self.db.minLevel, self.db.maxLevel))
    query = table.concat(parts, " ")

    if self.ResultsHeader then
        self.ResultsHeader:SetText(string.format("Unguilded Players - Current Filter: %s + %s", (filterName == "Custom" and activeClass or filterName), modeLabel))
    end
    if self.StatusText then
        self.StatusText:SetText("Scanning: " .. query .. "...")
    end

    if self.db.debugMode then
        self:GenerateDebugData()
        self.isScanning = false 
        return
    end

    if FriendsFrame then FriendsFrame:UnregisterEvent("WHO_LIST_UPDATE") end
    C_FriendList.SetWhoToUi(1) 
    C_FriendList.SendWho(query)
end

function GR:ProcessWhoResults()
    if not self.isScanning then return end
    if FriendsFrame and FriendsFrame:IsShown() then FriendsFrame:Hide() end
    if self.db.debugMode then self.isScanning = false; return end
    
    local numWhos = C_FriendList.GetNumWhoResults()
    local newCount = 0
    
    for i = 1, numWhos do
        local info = C_FriendList.GetWhoInfo(i)
        if info and (not info.fullGuildName or info.fullGuildName == "") then
            local zone = info.area or "Unknown"
            local isBlocklisted = NS.Data and NS.Data.BLOCKLISTED_ZONES and NS.Data.BLOCKLISTED_ZONES[zone]
            
            if not isBlocklisted then
                local isRemixZone = NS.Data and NS.Data.REMIX_ZONES and NS.Data.REMIX_ZONES[zone]
                local mode = self.db.remixMode
                local keep = false
                if mode == "OFF" then if not isRemixZone then keep = true end
                elseif mode == "ALLOW" then keep = true 
                elseif mode == "ONLY" then if isRemixZone then keep = true end end
                
                if keep and not self.db.blacklist[info.fullName] then
                    if not self:IsDuplicate(info.fullName) then
                        self:AddPlayerToTable(info, zone, isRemixZone)
                        newCount = newCount + 1
                    end
                end
            end
        end
    end
    if self.StatusText then
        self.StatusText:SetText("Scan Complete. Added " .. newCount .. " new players.")
    end
    self:UpdateScrollFrame()
    self.isScanning = false
end

function GR:IsDuplicate(name)
    for _, p in ipairs(self.foundPlayers) do
        if p.name == name then return true end
    end
    return false
end

-- [[ CORE UPDATE: LOGIC PORTED FROM GUILD ROSTER MEMBERS.LUA ]]
function GR:AddPlayerToTable(info, zone, isRemix)
    local score = 0
    local raidN, raidH, raidM = "-", "-", "-"
    local maxKey = "-"
    
    if _G.RaiderIO and _G.RaiderIO.GetProfile then
        local name, realm = strsplit("-", info.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
            -- 1. Mythic+ Score Retrieval
            if p.mythicKeystoneProfile then 
                score = p.mythicKeystoneProfile.currentScore or 0 
            end
            
            -- 2. Max Key Retrieval (NEW)
            if p.mythicKeystoneProfile and p.mythicKeystoneProfile.sortedDungeons then
                local top = p.mythicKeystoneProfile.sortedDungeons[1]
                if top then maxKey = "+"..top.level end
            end

            -- 3. Raid Progress Retrieval (IMPROVED)
            if p.raidProfile and p.raidProfile.progress then
                for _, prog in ipairs(p.raidProfile.progress) do
                    local rName = prog.raid and prog.raid.name or ""
                    -- [[ TWW SEASON 3 FIX: Use 'undermine' (Liberation of Undermine) or 'nerub' (Palace) ]]
                    -- Removing specific filter to allow the API to return whatever is current is often safer, 
                    -- but to follow the 'Structure' of the source file while fixing the 'Manaforge' bug:
                    if string.find(string.lower(rName), "undermine") or string.find(string.lower(rName), "nerub") then
                        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 -- Normal
                        if prog.difficulty == 2 then raidH = progressStr end -- Heroic
                        if prog.difficulty == 3 then raidM = progressStr end -- Mythic
                    end
                end
            end
        end
    end

    table.insert(self.foundPlayers, {
        name = info.fullName,
        race = info.raceStr or "Unknown", 
        fullClass = info.filename,
        classFile = info.filename,
        level = info.level,
        zone = zone,
        status = "Online",
        version = isRemix and "Remix" or "Retail",
        rio = score,
        raidN = raidN, -- Stored but not currently displayed in main view columns
        heroic = raidH,
        mythic = raidM,
        maxKey = maxKey, -- Added to Table
        selected = false 
    })
    
    self:SortPlayers(self.sortKey, true)
end

function GR:SortPlayers(key, keepOrder)
    if not keepOrder then
        if self.sortKey == key then
            self.sortOrder = (self.sortOrder == "ASC") and "DESC" or "ASC"
        else
            self.sortKey = key
            self.sortOrder = "ASC"
        end
    end

    table.sort(self.foundPlayers, function(a, b)
        local valA, valB = a[key], b[key]
        
        if key == "heroic" or key == "mythic" or key == "raidN" then
            valA = tonumber((valA:match("^(%d+)") or 0))
            valB = tonumber((valB:match("^(%d+)") or 0))
        elseif key == "level" or key == "rio" then
            valA = tonumber(valA) or 0
            valB = tonumber(valB) or 0
        elseif key == "maxKey" then
            -- Sort "+15" as number 15
            valA = tonumber((valA:match("^(%d+)") or 0)) or tonumber((valA:match("%+(%d+)") or 0))
            valB = tonumber((valB:match("^(%d+)") or 0)) or tonumber((valB:match("%+(%d+)") or 0))
        else
            valA = tostring(valA):lower()
            valB = tostring(valB):lower()
        end

        if self.sortOrder == "ASC" then
            return valA < valB
        else
            return valA > valB
        end
    end)
    
    self:UpdateScrollFrame()
end

function GR:GenerateDebugData()
    local name = UnitName("player")
    local _, classFile = UnitClass("player")
    local className = UnitClass("player") 
    local level = UnitLevel("player")
    local zone = GetZoneText()
    local race = UnitRace("player") 
    
    if self.db.blacklist[name] then
        print("Debug Info: " .. name .. " is in blacklist. Skipping table add.")
        return
    end

    local score = 0
    local heroic = "-" 
    local mythic = "-" 
    local maxKey = "+15"
    
    -- Debug RIO Mock
    score = 2450 

    local p = {
        name = name,
        race = race, 
        fullClass = className,
        classFile = classFile,
        level = level,
        zone = zone,
        status = "Online",
        version = "Retail",
        rio = score,
        raidN = "8/8",
        heroic = heroic,
        mythic = mythic,
        maxKey = maxKey,
        selected = true
    }
    
    if not self:IsDuplicate(p.name) then
        table.insert(self.foundPlayers, p)
        self:SortPlayers(self.sortKey, true)
        self:UpdateScrollFrame()
        self.StatusText:SetText("Added SELF (Debug).")
    else
        print("Debug: You are already in the list.")
    end
end

function GR:RemovePlayer(name)
    if not name then return end
    for i = #self.foundPlayers, 1, -1 do
        if self.foundPlayers[i].name == name then table.remove(self.foundPlayers, i) end
    end
    self:UpdateScrollFrame()
end

function GR:AddToRecent(name, status)
    local hist = self.db.recentHistory
    for i, v in ipairs(hist) do if v.name == name then table.remove(hist, i); break end end
    table.insert(hist, 1, {name=name, status=status})
    if #hist > 10 then table.remove(hist) end 
    self:UpdateRecentList()
end

-- =========================================================================
-- 9. PANE BUILDING HELPERS (LEFT/CENTER/RIGHT)
-- =========================================================================

function GR:BuildLeftPane(parent)
    local y = -15
    local function AddText(txt, font, dy)
        local t = parent:CreateFontString(nil, "OVERLAY", font)
        t:SetPoint("TOPLEFT", 15, y + (dy or 0)); t:SetText(txt)
    end
    
    AddText("SEARCH FILTERS", "GameFontNormal", 0); y = y - 30
    
    local dFilt = CreateFrame("Button", nil, parent); CreateBg(dFilt, 0.2, 0.2, 0.2, 1)
    dFilt:SetSize(180, 24); dFilt:SetPoint("TOPLEFT", 15, y)
    local dFiltTxt = dFilt:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); dFiltTxt:SetPoint("LEFT", 5, 0); dFiltTxt:SetText("Filter: Default (All)")
    self.dFiltTxt = dFiltTxt
    
    local dFiltArrow = dFilt:CreateTexture(nil, "ARTWORK")
    dFiltArrow:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
    dFiltArrow:SetSize(20, 20)
    dFiltArrow:SetPoint("RIGHT", -2, 0)
    
    local dFiltList = CreateFrame("Frame", nil, dFilt); CreateBg(dFiltList, 0.1, 0.1, 0.1, 1); CreateBorder(dFiltList, 0.5, 0.5, 0.5, 1)
    dFiltList:SetSize(180, 50); dFiltList:SetPoint("TOPLEFT", dFilt, "BOTTOMLEFT", 0, -2); dFiltList:SetFrameStrata("FULLSCREEN_DIALOG"); dFiltList:Hide()
    self.dFiltList = dFiltList; GR:RefreshMainFilterList()
    
    dFilt:SetScript("OnClick", function() GR:RefreshMainFilterList(); dFiltList:SetShown(not dFiltList:IsShown()) end)
    
    local cIcon = CreateFrame("Button", nil, parent); cIcon:SetSize(24, 24); cIcon:SetPoint("LEFT", dFilt, "RIGHT", 5, 0)
    cIcon:SetNormalTexture("Interface\\Buttons\\UI-OptionsButton")
    cIcon:SetScript("OnClick", function() if GR.FilterFrame then GR.FilterFrame.OpenCreate() end end)

    local eIcon = CreateFrame("Button", nil, parent); eIcon:SetSize(24, 24); eIcon:SetPoint("LEFT", cIcon, "RIGHT", 2, 0)
    eIcon:SetNormalTexture("Interface\\Buttons\\UI-GuildButton-PublicNote-Up") 
    eIcon:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")
    eIcon:SetScript("OnClick", function() 
        if GR.selectedFilterData then 
            GR.FilterFrame.OpenEdit(GR.selectedFilterData)
        else
            print("|cffff0000GRS: Cannot edit Default filter. Select a custom filter first.|r")
        end
    end)
    
    y = y - 40; AddText("Level Range", "GameFontHighlightSmall", 0)
    local minB = CreateFrame("EditBox", nil, parent, "InputBoxTemplate"); minB:SetSize(40, 20); minB:SetPoint("TOPLEFT", 15, y - 20); minB:SetNumeric(true); minB:SetText(self.db.minLevel)
    minB:SetAutoFocus(false)
    minB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    minB:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    minB:SetScript("OnTextChanged", function(e) self.db.minLevel = tonumber(e:GetText()) or 70 end)
    
    local maxB = CreateFrame("EditBox", nil, parent, "InputBoxTemplate"); maxB:SetSize(40, 20); maxB:SetPoint("LEFT", minB, "RIGHT", 15, 0); maxB:SetNumeric(true); maxB:SetText(self.db.maxLevel)
    maxB:SetAutoFocus(false)
    maxB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    maxB:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    maxB:SetScript("OnTextChanged", function(e) self.db.maxLevel = tonumber(e:GetText()) or 80 end)
    
    -- [[ FIXED: SCAN SCOPE LOGIC FROM OLD VERSION ]]
    y = y - 50; AddText("Scan Scope", "GameFontHighlightSmall", 0); y = y - 20
    local function MkRemBtn(text, mode, x)
        local b = CreateFrame("Button", nil, parent); CreateBg(b, 0.2, 0.2, 0.2, 1); b:SetSize(80, 20); b:SetPoint("TOPLEFT", 15 + x, y)
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("CENTER"); t:SetText(text)
        b.bg = b.bg
        b:SetScript("OnClick", function() self.db.remixMode = mode; self:UpdateRemixVisuals() end)
        return b
    end
    self.btnRetail = MkRemBtn("Retail", "OFF", 0); self.btnAllow = MkRemBtn("All", "ALLOW", 85); self.btnRemix = MkRemBtn("Remix", "ONLY", 170)
    
    function GR:UpdateRemixVisuals()
        self.btnRetail.bg:SetColorTexture(0.2,0.2,0.2,1); self.btnAllow.bg:SetColorTexture(0.2,0.2,0.2,1); self.btnRemix.bg:SetColorTexture(0.2,0.2,0.2,1)
        if self.db.remixMode=="OFF" then self.btnRetail.bg:SetColorTexture(0,0.6,0,1) end
        if self.db.remixMode=="ALLOW" then self.btnAllow.bg:SetColorTexture(0,0.6,0,1) end
        if self.db.remixMode=="ONLY" then self.btnRemix.bg:SetColorTexture(0,0.6,0,1) end
    end
    self:UpdateRemixVisuals()
    -- [[ END SCAN SCOPE FIX ]]
    
    y = y - 40; AddText("MESSAGE TEMPLATES", "GameFontNormal", 0); y = y - 30
    local dTemp = CreateFrame("Button", nil, parent); CreateBg(dTemp, 0.2, 0.2, 0.2, 1); dTemp:SetSize(200, 24); dTemp:SetPoint("TOPLEFT", 15, y)
    local dTempTxt = dTemp:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); dTempTxt:SetPoint("LEFT", 5, 0); dTempTxt:SetText(self.db.messageTemplate)
    
    local dTempArrow = dTemp:CreateTexture(nil, "ARTWORK")
    dTempArrow:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
    dTempArrow:SetSize(20, 20)
    dTempArrow:SetPoint("RIGHT", -2, 0)
    
    local dTempList = CreateFrame("Frame", nil, dTemp); CreateBg(dTempList, 0.1, 0.1, 0.1, 1); CreateBorder(dTempList, 0.5, 0.5, 0.5, 1)
    dTempList:SetSize(200, 100); dTempList:SetPoint("TOPLEFT", dTemp, "BOTTOMLEFT", 0, -2); dTempList:SetFrameStrata("FULLSCREEN_DIALOG"); dTempList:Hide()
    
    dTemp:SetScript("OnClick", function() 
        local kids = {dTempList:GetChildren()}; for _, k in ipairs(kids) do k:Hide() end
        local ty = -5
        for k, v in pairs(GR.db.templates) do
            local b = CreateFrame("Button", nil, dTempList); b:SetSize(190, 20); b:SetPoint("TOPLEFT", 5, ty)
            local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(k)
            b:SetScript("OnClick", function() 
                GR.db.messageTemplate = k; dTempTxt:SetText(k); GR.InputBody:SetText(v); GR.InputName:SetText(k); dTempList:Hide() 
                -- Manually trigger update if SetText doesn't fire OnTextChanged
                if GR.InputBody:GetScript("OnTextChanged") then
                     GR.InputBody:GetScript("OnTextChanged")(GR.InputBody)
                end
                -- [[ FIX: Trigger Name Input placeholder check ]]
                if GR.InputName:GetScript("OnTextChanged") then
                     GR.InputName:GetScript("OnTextChanged")(GR.InputName)
                end
            end)
            ty = ty - 20
        end
        dTempList:SetHeight(math.abs(ty) + 5)
        dTempList:SetShown(not dTempList:IsShown()) 
    end)
    
    local btnNew = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btnNew:SetSize(50, 22); btnNew:SetPoint("LEFT", dTemp, "RIGHT", 5, 0); btnNew:SetText("New")
    btnNew:SetScript("OnClick", function() GR.InputName:SetText(""); GR.InputBody:SetText(""); GR.InputName:SetFocus() end)
    SkinButton(btnNew)

    y = y - 30
    local nameIn = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    nameIn:SetSize(250, 20); nameIn:SetPoint("TOPLEFT", 15, y); nameIn:SetAutoFocus(false)
    nameIn:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    self.InputName = nameIn
    local ph = nameIn:CreateFontString(nil, "ARTWORK", "GameFontDisable"); ph:SetPoint("LEFT", 5, 0); ph:SetText("Template Name...")
    
    -- [[ FIX: Ensure placeholder hides on text update ]]
    nameIn:SetScript("OnTextChanged", function(self) 
        if self:GetText() ~= "" then ph:Hide() else ph:Show() end 
    end)
    
    nameIn:SetScript("OnEditFocusGained", function() ph:Hide() end)
    nameIn:SetScript("OnEditFocusLost", function(s) if s:GetText()=="" then ph:Show() end end)
    
    y = y - 30
    local bodyF = CreateFrame("Frame", nil, parent); CreateBg(bodyF, 0.1, 0.1, 0.1, 0.5); bodyF:SetSize(290, 100); bodyF:SetPoint("TOPLEFT", 15, y)
    local scrollM = CreateFrame("ScrollFrame", nil, bodyF, "UIPanelScrollFrameTemplate"); scrollM:SetPoint("TOPLEFT", 5, -5); scrollM:SetPoint("BOTTOMRIGHT", -25, 5)
    local editM = CreateFrame("EditBox", nil, scrollM); editM:SetSize(265, 100); editM:SetMultiLine(true); editM:SetFontObject(ChatFontNormal); 
    editM:SetMaxLetters(255); -- CHANGED to 255
    editM:SetAutoFocus(false) 
    editM:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editM:SetText(self.db.templates[self.db.messageTemplate] or ""); scrollM:SetScrollChild(editM); self.InputBody = editM

    -- [[ NEW: LETTER COUNT ]]
    local charCount = bodyF:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    charCount:SetPoint("BOTTOMRIGHT", -30, 2)
    local function UpdateCharCount()
        local len = string.len(editM:GetText() or "")
        charCount:SetText(len .. " / 255")
        if len >= 255 then charCount:SetTextColor(1, 0.2, 0.2) else charCount:SetTextColor(0.5, 0.5, 0.5) end
    end
    editM:SetScript("OnTextChanged", function(self)
        UpdateCharCount()
    end)
    editM:SetScript("OnEditFocusGained", function(self) UpdateCharCount() end) -- Ensure update on focus just in case
    UpdateCharCount() -- Initial call
    
    y = y - 110
    local btnSave = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate"); btnSave:SetSize(80, 22); btnSave:SetPoint("TOPLEFT", 15, y); btnSave:SetText("Save")
    btnSave:SetScript("OnClick", function()
        local n, b = self.InputName:GetText(), self.InputBody:GetText()
        if n ~= "" and b ~= "" then
            self.db.templates[n] = b; self.db.messageTemplate = n; dTempTxt:SetText(n); print("Saved Template: " .. n)
        end
    end)
    SkinButton(btnSave)

    -- Display settings moved to Settings tab.
end

function GR:UpdateRecentList()
    -- Feature disabled (UI components removed)
end

function GR:UpdateRaiderIOStatus()
    if not self.ioStatus then return end

    -- Determine Officer Status safely
    local isOfficer = false
    if IsInGuild() then
        isOfficer = C_GuildInfo.CanEditOfficerNote()
    end

    if _G.RaiderIO and _G.RaiderIO.GetProfile then
        self.ioStatus:SetText("Raider.IO detected. Data active.")
        self.ioStatus:SetTextColor(0, 1, 0)
    else
        -- Logic: If RIO is missing, check if they are an officer before showing error
        if not isOfficer then
            local name = UnitName("player")
            self.ioStatus:SetText("Greetings " .. name)
            self.ioStatus:SetTextColor(1, 1, 1) -- White Text for Greeting
        else
            self.ioStatus:SetText("Raider.IO missing. Data empty.")
            self.ioStatus:SetTextColor(1, 0, 0) -- Red Text for Error
        end
    end
end

-- [[ UPDATED: OFFICER STATUS & UI RESTRICTIONS ]]
function GR:UpdateOfficerStatus()
    if not self.officerStatus then return end

    local isOfficer = false
    local hasGuild = IsInGuild()
    
    if hasGuild then
        isOfficer = C_GuildInfo.CanEditOfficerNote()
    end

    -- 1. Update Status Label
    if not hasGuild then
        self.officerStatus:SetText("No Guild")
        self.officerStatus:SetTextColor(0.5, 0.5, 0.5) -- Grey
    elseif isOfficer then
        self.officerStatus:SetText("Officer: Yes")
        self.officerStatus:SetTextColor(0, 1, 0) -- Green
    else
        self.officerStatus:SetText("Officer: No")
        self.officerStatus:SetTextColor(1, 0, 0) -- Red
    end
    
    -- 2. Restrict Access to Tabs 1, 2, 3
    local restrictedTabs = {[1] = true, [2] = true, [3] = true}
    
    if self.Tabs then
        local currentY = -10
        for i, tab in ipairs(self.Tabs) do
            if restrictedTabs[i] then
                if isOfficer then
                    tab:Show()
                else
                    tab:Hide()
                end
            end
            
            -- Re-anchor visible tabs to prevent gaps
            if tab:IsShown() then
                tab:ClearAllPoints()
                tab:SetPoint("TOP", self.SideBar, "TOP", 0, currentY)
                currentY = currentY - 40
            end
        end
    end
    
    -- 3. Force Switch if currently on restricted view
    -- If user is NOT an officer and is currently on tab 1, 2, or 3 -> Switch to Tab 5
    if not isOfficer and restrictedTabs[self.activeTab] then
        print("|cffff0000GRS: Access restricted. Switching to Guild Roster.|r")
        self:SwitchView(5)
    end
end

function GR:BuildCenterPane(parent)
    self.ResultsHeader = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    self.ResultsHeader:SetPoint("TOPLEFT", 15, -15); self.ResultsHeader:SetText("Unguilded Players - Current Filter: Default (All)")
    
    local sBtn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    sBtn:SetSize(140, 30); sBtn:SetPoint("TOPRIGHT", -15, -10); sBtn:SetText("Search Players")
    self.SearchBtn = sBtn 
    sBtn:SetScript("OnClick", function() self:StartSearch() end)
    SkinButton(sBtn) 
    
    -- [[ NEW: RESET LIST BUTTON ]]
    local rListBtn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    rListBtn:SetSize(100, 30)
    rListBtn:SetPoint("RIGHT", sBtn, "LEFT", -5, 0)
    rListBtn:SetText("Reset List")
    rListBtn:SetScript("OnClick", function()
        GR.foundPlayers = {}
        GR:UpdateScrollFrame()
        if GR.StatusText then GR.StatusText:SetText("List Discarded.") end
    end)
    SkinButton(rListBtn)

    local headF = CreateFrame("Frame", nil, parent); headF:SetSize(750, 20); headF:SetPoint("TOPLEFT", 21, -45) 

    -- [[ NEW: SELECT ALL CHECKBOX ]]
    local selectAll = CreateFrame("CheckButton", nil, headF, "UICheckButtonTemplate")
    selectAll:SetSize(20, 20)
    selectAll:SetPoint("LEFT", 0, 0)
    selectAll:SetScript("OnClick", function(self)
        local state = self:GetChecked()
        for _, p in ipairs(GR.foundPlayers) do
            p.selected = state
        end
        GR:UpdateScrollFrame()
    end)
    
    -- [[ UI UPDATE: RE-ADDED LEVEL COLUMN ]]
    local headers = {
        {text="Name", key="name", width=110}, 
        {text="Race", key="race", width=90},  
        {text="Class", key="fullClass", width=100},
        {text="Level", key="level", width=40}, -- Restored Level
        {text="Max Key", key="maxKey", width=50}, -- Renamed
        {text="Zone", key="zone", width=110}, 
        {text="Score", key="rio", width=70},  
        {text="Heroic", key="heroic", width=70}, 
        {text="Mythic", key="mythic", width=70}  
    }
    
    local lastH = nil
    for i, data in ipairs(headers) do
        local btn = CreateFrame("Button", nil, headF)
        btn:SetSize(data.width, 20)
        
        if i==1 then 
            btn:SetPoint("LEFT", 30, 0) 
        else 
            btn:SetPoint("LEFT", lastH, "RIGHT", 0, 0) 
        end
        
        local txt = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        txt:SetPoint("LEFT", 0, 0)
        txt:SetText(data.text)
        btn:SetFontString(txt)
        
        btn:SetScript("OnEnter", function(s) 
            s:GetFontString():SetTextColor(1, 1, 1) 
        end)
        btn:SetScript("OnLeave", function(s) 
            s:GetFontString():SetTextColor(1, 0.82, 0) 
        end)
        
        btn:SetScript("OnClick", function() 
            GR:SortPlayers(data.key) 
        end)
        
        lastH = btn
    end
    
    local bot = CreateFrame("Frame", nil, parent); bot:SetHeight(40)
    bot:SetPoint("BOTTOMLEFT", 15, 5)
    -- Initial anchor, will be updated by resize script below
    bot:SetPoint("BOTTOMRIGHT", -15, 5)
    
    local slL = bot:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); slL:SetPoint("LEFT", 0, 0); slL:SetText("Msg Delay: 0.8s")
    local sl = CreateFrame("Slider", nil, bot, "OptionsSliderTemplate")
    sl:SetPoint("LEFT", slL, "RIGHT", 10, 0); sl:SetWidth(150); sl:SetMinMaxValues(0.5, 1.0); sl:SetValue(0.8); sl:SetValueStep(0.1); sl:SetObeyStepOnDrag(true)
    sl:SetScript("OnValueChanged", function(s,v) v=math.floor(v*10)/10; self.db.scanDelay=v; slL:SetText("Msg Delay: "..v.."s") end)
    
    local snd = CreateFrame("Button", nil, bot, "UIPanelButtonTemplate"); snd:SetSize(120, 30); snd:SetPoint("RIGHT", -10, 0); snd:SetText("Send Batch")
    snd:SetScript("OnClick", function() self:SendMessageSequence() end)
    SkinButton(snd) 

    -- [[ NEW: BLACKLIST SELECTION BUTTON ]]
    local btnBL = CreateFrame("Button", nil, bot, "UIPanelButtonTemplate")
    btnBL:SetSize(130, 30)
    btnBL:SetPoint("RIGHT", snd, "LEFT", -10, 0)
    btnBL:SetText("Blacklist Selected")
    btnBL:SetScript("OnClick", function() self:ManualBlacklist() end)
    SkinButton(btnBL)

    self.StatusText = bot:CreateFontString(nil,"OVERLAY","GameFontNormal"); 
    -- Re-anchored to left of the new Blacklist button
    self.StatusText:SetPoint("RIGHT", btnBL, "LEFT", -20, 0); 
    self.StatusText:SetText("Ready.")

    self.scroll = CreateFrame("ScrollFrame", "GRS_S", parent, "FauxScrollFrameTemplate")
    -- Explicitly attaching to parent to allow self.scrollFrame access if needed, but primarily using local
    parent.scrollFrame = self.scroll
    parent.scroll = self.scroll -- Double assignment for legacy safety
    local scrollFrame = self.scroll 
    
    scrollFrame:SetPoint("TOPLEFT", headF, "BOTTOMLEFT", 0, -5)
    -- Initial anchor, will be updated by resize script below
    scrollFrame:SetPoint("BOTTOMRIGHT", bot, "TOPRIGHT", -5, 5)
    
    -- [[ FIXED: Permanent Background for List ]]
    CreateBg(scrollFrame, 0, 0, 0, 0.8) 
    
    -- [[ FIXED: Permanent Background for Scrollbar Track ]]
    -- We specifically find the ScrollBar created by the template and skin it
    local scrollBar = _G["GRS_SScrollBar"]
    if scrollBar then
        CreateBg(scrollBar, 0.1, 0.1, 0.1, 0.8) -- Visible Dark Track
    end

    scrollFrame:SetScript("OnVerticalScroll", function(s, offset)
        FauxScrollFrame_OnVerticalScroll(s, offset, 20, function() self:UpdateScrollFrame() end)
    end)
    
    scrollFrame:EnableMouseWheel(true)
    
    -- [[ CONSTRAINT: MAX WIDTH 760px (Hardcoded to match Headers 750 + Padding 10) ]]
    parent:SetScript("OnSizeChanged", function(frame, w, h)
        -- CRITICAL FIX: Ensure scrollFrame exists before operating on it
        local sFrame = scrollFrame or frame.scrollFrame or frame.scroll
        if not sFrame then return end

        -- w here is scrollW from the parent, so it's already safe
        local availableW = w - 30 -- minus margins
        local MAX_WIDTH = 760 -- CHANGED: WAS 885, NOW 760 TO PREVENT WIDENING ON PANE COLLAPSE
        
        if availableW > MAX_WIDTH then
            -- Cap the frames width
            bot:ClearAllPoints()
            bot:SetPoint("BOTTOMLEFT", 15, 5)
            bot:SetWidth(MAX_WIDTH)
            
            sFrame:ClearAllPoints()
            sFrame:SetPoint("TOPLEFT", headF, "BOTTOMLEFT", 0, -5)
            sFrame:SetPoint("BOTTOMLEFT", bot, "TOPLEFT", 0, 5)
            sFrame:SetWidth(MAX_WIDTH)
        else
            -- Use full width
            bot:ClearAllPoints()
            bot:SetPoint("BOTTOMLEFT", 15, 5)
            bot:SetPoint("BOTTOMRIGHT", -15, 5)
            
            sFrame:ClearAllPoints()
            sFrame:SetPoint("TOPLEFT", headF, "BOTTOMLEFT", 0, -5)
            sFrame:SetPoint("BOTTOMRIGHT", bot, "TOPRIGHT", -5, 5)
        end
    end)
    
    self.rows = {}
    
    function GR:UpdateResultsLayout(height)
        if not height then height = self.scroll:GetHeight() end
        local numRows = math.floor(height / 20)
        self.visibleRows = numRows 
        
        for i = 1, numRows do
            if not self.rows[i] then
                local r = CreateFrame("Button", nil, parent)
                r:SetHeight(20); 
                -- [[ ALIGNMENT FIX: 55 -> 41 ]]
                r:SetPoint("LEFT", 41, 0); 
                r:SetPoint("RIGHT", -25, 0)
                if i==1 then r:SetPoint("TOP", self.scroll, "TOP") else r:SetPoint("TOP", self.rows[i-1], "BOTTOM") end
                
                r.c = CreateFrame("CheckButton", nil, r, "UICheckButtonTemplate"); r.c:SetSize(20,20); r.c:SetPoint("LEFT", -20, 0)
                r.c:SetScript("OnClick", function(b) if r.d then r.d.selected=b:GetChecked() end end)
                
                -- [[ UI UPDATE: ADJUSTED WIDTHS FOR MAX KEY & LEVEL ]]
                r.n = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.n:SetPoint("LEFT",10,0); r.n:SetWidth(110); r.n:SetJustifyH("LEFT")
                r.ra = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.ra:SetPoint("LEFT",r.n,"RIGHT"); r.ra:SetWidth(90); r.ra:SetJustifyH("LEFT")
                r.cl = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.cl:SetPoint("LEFT",r.ra,"RIGHT"); r.cl:SetWidth(100); r.cl:SetJustifyH("LEFT")
                
                -- Level and Max Key
                r.lvl = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.lvl:SetPoint("LEFT",r.cl,"RIGHT"); r.lvl:SetWidth(40); r.lvl:SetJustifyH("LEFT")
                r.mk = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.mk:SetPoint("LEFT",r.lvl,"RIGHT"); r.mk:SetWidth(50); r.mk:SetJustifyH("LEFT") 

                r.z = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.z:SetPoint("LEFT",r.mk,"RIGHT"); r.z:SetWidth(110); r.z:SetJustifyH("LEFT")
                r.s = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.s:SetPoint("LEFT",r.z,"RIGHT"); r.s:SetWidth(70); r.s:SetJustifyH("LEFT")
                r.h = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.h:SetPoint("LEFT",r.s,"RIGHT"); r.h:SetWidth(70); r.h:SetJustifyH("LEFT")
                r.m = r:CreateFontString(nil,"OVERLAY","GameFontHighlightSmall"); r.m:SetPoint("LEFT",r.h,"RIGHT"); r.m:SetWidth(70); r.m:SetJustifyH("LEFT")
                self.rows[i] = r
            end
        end
        self:UpdateScrollFrame() 
    end
end

function GR:BuildWhisperPane(parent)
    -- Parent is GRS_RightPane
    
    -- [[ REMOVED COLLAPSING LOGIC AND BUTTONS ]]
    -- We now only build the Expanded View directly into the parent.
    
    local expandedF = CreateFrame("Frame", nil, parent)
    expandedF:SetAllPoints()
    self.WhisperExpandedFrame = expandedF
    
    -- Header
    local gridH = expandedF:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    gridH:SetPoint("TOPLEFT", 15, -15)
    gridH:SetText("ACTIVE WHISPERS")
    
    -- [[ REMOVED COLLAPSE BUTTON ]]
    
    local pText = expandedF:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    pText:SetPoint("LEFT", gridH, "RIGHT", 30, 0) -- Re-anchored to Header
    pText:SetText("Page 1 / 1")
    self.pageText = pText
    
    local prevBtn = CreateFrame("Button", nil, expandedF)
    prevBtn:SetSize(20, 20)
    prevBtn:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
    prevBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")
    prevBtn:SetPoint("RIGHT", pText, "LEFT", -5, 0)
    prevBtn:SetScript("OnClick", function() 
        if GR.currentPage > 1 then 
            GR.currentPage = GR.currentPage - 1
            GR:RenderWhisperGrid() 
        end 
    end)
    self.prevBtn = prevBtn
    
    local nextBtn = CreateFrame("Button", nil, expandedF)
    nextBtn:SetSize(20, 20)
    nextBtn:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
    nextBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight")
    nextBtn:SetPoint("LEFT", pText, "RIGHT", 5, 0)
    nextBtn:SetScript("OnClick", function() 
        local maxP = math.ceil(#GR.activeChats/(GR.chatPageSize or 4))
        if maxP < 1 then maxP=1 end
        if GR.currentPage < maxP then 
            GR.currentPage = GR.currentPage + 1
            GR:RenderWhisperGrid() 
        end 
    end)
    self.nextBtn = nextBtn

    local pruneBtn = CreateFrame("Button", nil, expandedF, "UIPanelButtonTemplate")
    pruneBtn:SetSize(100, 20)
    pruneBtn:SetPoint("LEFT", nextBtn, "RIGHT", 20, 0)
    pruneBtn:SetText("Prune Inactive")
    pruneBtn:SetScript("OnClick", function() self:PruneInactiveChats() end)
    SkinButton(pruneBtn)
    
    self.chatFrames = {}
    local gridBase = CreateFrame("Frame", nil, expandedF)
    gridBase:SetPoint("TOPLEFT", gridH, "BOTTOMLEFT", 0, -10)
    gridBase:SetPoint("BOTTOMRIGHT", -15, 10)
    self.WhisperGridBase = gridBase
    
    function GR:UpdateWhisperLayout(totalHeight)
        -- FIXED: Hardcoded to 4 items per page as requested by user.
        -- Removed dynamic height calculation to prevent "1 item per page" bug.
        self.chatPageSize = 4 
        local chatH = 80 
        local gap = 5
        
        for i = 1, self.chatPageSize do
            if not self.chatFrames[i] then
                local cf = CreateFrame("Frame", nil, self.WhisperGridBase)
                CreateBg(cf, 0, 0, 0, 0.6); CreateBorder(cf, 0.4, 0.4, 0.4, 1)
                cf:SetHeight(chatH); cf:SetPoint("LEFT"); cf:SetPoint("RIGHT")
                -- Anchor logic is handled below
                
                -- FORCE HIDE ON CREATION to prevent ghosting
                cf:Hide() 
                
                cf.headerBtn = CreateFrame("Button", nil, cf); cf.headerBtn:SetPoint("TOPLEFT", 5, -5); cf.headerBtn:SetSize(200, 15)
                cf.headerBtn.text = cf.headerBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); cf.headerBtn.text:SetPoint("LEFT"); cf.headerBtn.text:SetText("Inactive")
                cf.headerBtn:RegisterForClicks("AnyUp") 
                cf.headerBtn:SetScript("OnClick", function(s, button)
                    if not s.playerName then return end
                    if button == "LeftButton" then ChatFrame_SendTell(s.playerName)
                    elseif button == "RightButton" and MenuUtil then
                        MenuUtil.CreateContextMenu(s, function(owner, root)
                            root:CreateTitle(s.playerName); root:CreateButton("Invite", function() C_PartyInfo.InviteUnit(s.playerName) end)
                        end)
                    end
                end)
                
                local closeChat = CreateFrame("Button", nil, cf, "UIPanelCloseButton"); closeChat:SetSize(20, 20); closeChat:SetPoint("TOPRIGHT", -2, -2)
                closeChat:SetScript("OnClick", function()
                    local idx = (GR.currentPage - 1) * self.chatPageSize + i
                    if GR.activeChats[idx] then table.remove(GR.activeChats, idx); GR:RenderWhisperGrid() end
                end)
                
                cf.scroll = CreateFrame("ScrollFrame", nil, cf, "UIPanelScrollFrameTemplate"); cf.scroll:SetPoint("TOPLEFT", 5, -20); cf.scroll:SetPoint("BOTTOMRIGHT", -25, 5)
                cf.text = CreateFrame("EditBox", nil, cf.scroll); cf.text:SetMultiLine(true); cf.text:SetFontObject(ChatFontSmall); cf.text:SetWidth(430); cf.text:SetAutoFocus(false); cf.text:EnableMouse(false)
                cf.scroll:SetScrollChild(cf.text)
                self.chatFrames[i] = cf
            end
            
            -- Ensure anchors are correct
            if i==1 then 
                self.chatFrames[i]:SetPoint("TOP") 
            else 
                self.chatFrames[i]:SetPoint("TOP", self.chatFrames[i-1], "BOTTOM", 0, -gap) 
            end
        end
        
        -- Cleanup any extra frames if they exist from a previous state
        for i = self.chatPageSize + 1, #self.chatFrames do
            if self.chatFrames[i] then self.chatFrames[i]:Hide() end
        end
        
        self:RenderWhisperGrid()
    end
end

function GR:UpdateScrollFrame()
    local d = self.foundPlayers
    local rowCount = self.visibleRows or 15
    if rowCount > #self.rows then rowCount = #self.rows end
    
    FauxScrollFrame_Update(self.scroll, #d, rowCount, 20)
    
    -- [[ FIXED: Force Scrollbar Visible ]]
    -- Ensure the track stays visible even if the list is short
    local scrollBar = _G["GRS_SScrollBar"]
    if scrollBar then
        scrollBar:Show() 
        
        local _, maxVal = scrollBar:GetMinMaxValues()
        local thumb = scrollBar:GetThumbTexture()
        
        -- If no scroll is actually needed, hide the thumb (knob) but keep the track
        if not maxVal or maxVal == 0 then
             if thumb then thumb:Hide() end
             
             local up = _G["GRS_SScrollBarScrollUpButton"]
             local down = _G["GRS_SScrollBarScrollDownButton"]
             if up then up:Disable() end
             if down then down:Disable() end
        else
             if thumb then thumb:Show() end
             
             local up = _G["GRS_SScrollBarScrollUpButton"]
             local down = _G["GRS_SScrollBarScrollDownButton"]
             if up then up:Enable() end
             if down then down:Enable() end
        end
    end
    
    local offset = FauxScrollFrame_GetOffset(self.scroll)
    
    for i=1, #self.rows do
        local r = self.rows[i]
        if i > rowCount then
            r:Hide()
        else
            local idx = offset + i
            if idx <= #d then
                local p = d[idx]
                r.d = p; r:Show()
                r.n:SetText(p.name); 
                -- [[ UI UPDATE: Level & Max Key Display ]]
                r.lvl:SetText(p.level);
                r.mk:SetText(p.maxKey); 
                
                r.z:SetText(p.zone); r.ra:SetText(p.race or "Unknown")
                local cc = C_ClassColor.GetClassColor(p.classFile or "PRIEST")
                r.cl:SetText(cc and cc:WrapTextInColorCode(p.fullClass) or p.fullClass)
                
                -- Score Coloring
                local sc = p.rio or 0; local col = "|cffffffff"
                if sc>=2500 then col="|cffff8000" -- Legendary
                elseif sc>=2000 then col="|cffa335ee" -- Epic
                elseif sc>=1000 then col="|cff0070dd" -- Rare (Blue)
                end
                r.s:SetText(col..sc.."|r")
                
                -- Raid Progress Coloring
                local function Col(str)
                    if not str or str == "-" then return "|cff808080"..str.."|r" end
                    local k = tonumber(str:match("^(%d+)")) or 0
                    if k >= 8 then return "|cffff8000"..str.."|r"
                    elseif k >= 4 then return "|cffa335ee"..str.."|r"
                    else return "|cff00ff00"..str.."|r" end
                end
                
                r.h:SetText(Col(p.heroic)); r.m:SetText(Col(p.mythic))
                r.c:SetChecked(p.selected)
            else 
                r:Hide() 
            end
        end
    end
end

-- =========================================================================
-- 10. CHAT HANDLING (Send/Receive)
-- =========================================================================

function GR:SendMessageSequence()
    local msg = self.InputBody:GetText()
    if msg == "" then print("Message empty."); return end
    local queue = {}
    for _, p in ipairs(self.foundPlayers) do if p.selected then table.insert(queue, p) end end
    if #queue == 0 then print("No players selected."); return end
    
    self.StatusText:SetText("Initializing Batch Protocol...")
    local batchSize = 5; local batchCooldown = 5; local currentIndex = 1
    
    local function ProcessNext()
        if currentIndex > #queue then GR.StatusText:SetText("All messages sent."); return end
        local p = queue[currentIndex]
        local isBatchPause = (currentIndex > 1) and ((currentIndex - 1) % batchSize == 0)
        local delay = isBatchPause and batchCooldown or GR.db.scanDelay
        
        C_Timer.After(delay, function()
            if not GR.db.blacklist[p.name] then
                if GR.db.debugMode then
                    print("|cff00ff00[DEBUG]:|r Would whisper: " .. p.name)
                    GR:AddToRecent(p.name, "Sent (Debug)")
                    if p.name == UnitName("player") then SendChatMessage(msg, "WHISPER", nil, p.name) end
                    GR:AddToBlacklist(p.name) 
                else
                    SendChatMessage(msg, "WHISPER", nil, p.name)
                    GR:AddToBlacklist(p.name) 
                    GR:AddToRecent(p.name, "Sent")
                end
            end
            currentIndex = currentIndex + 1
            ProcessNext() 
        end)
    end
    ProcessNext() 
end

function GR:HandleChatEvent(event, msg, sender, ...)
    if not sender then return end
    sender = Ambiguate(sender, "none") 
    if not self:IsShown() then return end
    self:UpdateWhisperGrid(sender, msg, event == "CHAT_MSG_WHISPER_INFORM")
end

function GR:UpdateWhisperGrid(name, msg, isOutgoing)
    local session = nil
    for _, s in ipairs(self.activeChats) do if s.name == name then session = s; break end end
    
    -- FIXED: Use hardcoded default of 4 for calculation safety
    local pageSize = self.chatPageSize or 4
    
    if not session then
        if #self.activeChats >= 100 then table.remove(self.activeChats, 1) end
        session = {name = name, log = {}}
        table.insert(self.activeChats, session)
        self.currentPage = math.ceil(#self.activeChats / pageSize)
    end
    session.lastActivity = GetTime()
    local color = isOutgoing and "|cff00ffff[You]: |r" or "|cffff8000["..name.."]: |r"
    table.insert(session.log, color .. msg)
    if #session.log > 10 then table.remove(session.log, 1) end 
    self:RenderWhisperGrid()
end

function GR:PruneInactiveChats()
    local limit = 120; local now = GetTime(); local count = 0
    for i = #self.activeChats, 1, -1 do
        local s = self.activeChats[i]
        if s.lastActivity and (now - s.lastActivity > limit) then
            table.remove(self.activeChats, i); count = count + 1
        end
    end
    self:RenderWhisperGrid()
    print("GRS: Pruned " .. count .. " inactive chats.")
end

function GR:RenderWhisperGrid()
    -- FIXED: Default to 4 to match layout update
    local pageSize = self.chatPageSize or 4
    local totalChats = #self.activeChats
    local totalPages = math.ceil(totalChats / pageSize)
    if totalPages == 0 then totalPages = 1 end
    
    if self.currentPage > totalPages then self.currentPage = totalPages end
    if self.currentPage < 1 then self.currentPage = 1 end
    
    if self.pageText then
        self.pageText:SetText(string.format("Page %d / %d", self.currentPage, totalPages))
        if self.currentPage == 1 then self.prevBtn:Disable() else self.prevBtn:Enable() end
        if self.currentPage == totalPages then self.nextBtn:Disable() else self.nextBtn:Enable() end
    end
    
    local startIndex = (self.currentPage - 1) * pageSize + 1
    
    for _, f in pairs(self.chatFrames) do f:Hide() end

    for i = 1, pageSize do
        local frame = self.chatFrames[i]
        if frame then
            local dataIndex = startIndex + (i - 1)
            local session = self.activeChats[dataIndex]
            
            if session then
                frame:Show()
                frame.headerBtn:Show()
                frame.headerBtn.text:SetText(session.name)
                frame.headerBtn.playerName = session.name 
                frame.dataIndex = dataIndex 
                local fullText = ""
                for _, line in ipairs(session.log) do fullText = fullText .. line .. "\n" end
                frame.text:SetText(fullText)
                frame.scroll:SetVerticalScroll(frame.scroll:GetVerticalScrollRange())
            end
        end
    end
end

-- =========================================================================
-- 11. FILTER UI (Retained)
-- =========================================================================

function GR:BuildFilterUI()
    local f = CreateFrame("Frame", "GRS_FilterFrame", UIParent)
    f:SetSize(300, 250); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:Hide()
    CreateBg(f, 0, 0, 0, 0.95); CreateBorder(f, 0.5, 0.5, 0.5, 1)
    GR.FilterFrame = f
    
    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -15); title:SetText("Create Custom Filter")
    f.Title = title
    
    local lblC = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblC:SetPoint("TOPLEFT", 20, -50); lblC:SetText("Class:")
    local btnC = CreateFrame("Button", nil, f); CreateBg(btnC, 0.2, 0.2, 0.2, 1); btnC:SetSize(150, 24); btnC:SetPoint("LEFT", lblC, "RIGHT", 10, 0)
    local txtC = btnC:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); txtC:SetPoint("LEFT", 5, 0); txtC:SetText("Any")
    
    local lblR = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblR:SetPoint("TOPLEFT", 20, -85); lblR:SetText("Race:")
    local btnR = CreateFrame("Button", nil, f); CreateBg(btnR, 0.2, 0.2, 0.2, 1); btnR:SetSize(150, 24); btnR:SetPoint("LEFT", lblR, "RIGHT", 10, 0)
    local txtR = btnR:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); txtR:SetPoint("LEFT", 5, 0); txtR:SetText("Any")
    
    local lblN = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblN:SetPoint("TOPLEFT", 20, -120); lblN:SetText("Name:")
    local editN = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); editN:SetSize(160, 24); editN:SetPoint("LEFT", lblN, "RIGHT", 10, 0)
    editN:SetAutoFocus(false)
    editN:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editN:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    
    local bSave = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); bSave:SetSize(100, 24); bSave:SetPoint("BOTTOMLEFT", 20, 20); bSave:SetText("Save")
    SkinButton(bSave) -- Skin Save

    local bCancel = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); bCancel:SetSize(100, 24); bCancel:SetPoint("BOTTOMRIGHT", -20, 20); bCancel:SetText("Cancel")
    SkinButton(bCancel) -- Skin Cancel

    local listC = CreateFrame("Frame", nil, f); CreateBg(listC, 0.1, 0.1, 0.1, 1); listC:SetSize(150, 300); listC:SetPoint("TOPLEFT", btnC, "BOTTOMLEFT", 0, -2); listC:SetFrameStrata("Tooltip"); listC:Hide()
    local listR = CreateFrame("Frame", nil, f); CreateBg(listR, 0.1, 0.1, 0.1, 1); listR:SetSize(150, 300); listR:SetPoint("TOPLEFT", btnR, "BOTTOMLEFT", 0, -2); listR:SetFrameStrata("Tooltip"); listR:Hide()
    
    local function MkBtn(parent, text, func, y)
        local b = CreateFrame("Button", nil, parent); b:SetSize(140, 20); b:SetPoint("TOPLEFT", 5, y)
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(text)
        b:SetScript("OnClick", function() func(text); parent:Hide() end)
    end
    
    MkBtn(listC, "Any", function(v) f.selClass=v; txtC:SetText(v) end, -5)
    for i, c in ipairs(CLASS_LIST) do MkBtn(listC, c, function(v) f.selClass=v; txtC:SetText(v) end, -5 - (i*20)) end
    
    MkBtn(listR, "Any", function(v) f.selRace=v; txtR:SetText(v) end, -5)
    for i, r in ipairs(RACES[UnitFactionGroup("player")] or {}) do MkBtn(listR, r, function(v) f.selRace=v; txtR:SetText(v) end, -5 - (i*20)) end
    
    btnC:SetScript("OnClick", function() listC:SetShown(not listC:IsShown()); listR:Hide() end)
    btnR:SetScript("OnClick", function() listR:SetShown(not listR:IsShown()); listC:Hide() end)
    bCancel:SetScript("OnClick", function() f:Hide() end)
    
    bSave:SetScript("OnClick", function()
        local newName = editN:GetText()
        if not newName or newName == "" then return end
        
        if f.mode == "EDIT" and f.origName and f.origName ~= newName then
            GR.db.customFilters[f.origName] = nil
        end
        
        GR.db.customFilters[newName] = { class = f.selClass, race = f.selRace, name = newName }
        GR:RefreshMainFilterList()
        
        if GR.filterType == "Custom" and GR.selectedFilterData and (GR.selectedFilterData.name == f.origName or GR.selectedFilterData.name == newName) then
             GR.selectedFilterData = GR.db.customFilters[newName]
             GR.dFiltTxt:SetText("Filter: " .. newName)
        end
        
        f:Hide()
        print("Filter Saved: " .. newName)
    end)
    
    f.OpenCreate = function()
        f.mode = "CREATE"; f.origName = nil
        f.Title:SetText("Create Custom Filter")
        f.selClass = "Any"; txtC:SetText("Any")
        f.selRace = "Any"; txtR:SetText("Any")
        editN:SetText("")
        f:Show()
    end
    
    f.OpenEdit = function(data)
        f.mode = "EDIT"; f.origName = data.name
        f.Title:SetText("Edit Filter: " .. data.name)
        f.selClass = data.class or "Any"; txtC:SetText(f.selClass)
        f.selRace = data.race or "Any"; txtR:SetText(f.selRace)
        editN:SetText(data.name)
        f:Show()
    end
end

function GR:RefreshMainFilterList()
    local list = self.dFiltList
    local kids = {list:GetChildren()}; for _, k in ipairs(kids) do k:Hide() end
    
    local y = -5
    local function Mk(text, func)
        local b = CreateFrame("Button", nil, list); b:SetSize(170, 20); b:SetPoint("TOPLEFT", 5, y)
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(text)
        b:SetScript("OnClick", func); y = y - 20
    end
    
    Mk("Default (All)", function()
        self.filterType = "Default"; self.dFiltTxt:SetText("Filter: Default (All)")
        self.selectedFilterData = nil; list:Hide()
    end)
    
    for name, data in pairs(self.db.customFilters) do
        Mk(name, function()
            self.filterType = "Custom"; self.dFiltTxt:SetText("Filter: " .. name)
            self.selectedFilterData = data; list:Hide()
        end)
    end
    list:SetHeight(math.abs(y) + 5)
end

function GR:UpdateRaiderIOStatus()
    if not self.ioStatus then return end

    -- Determine Officer Status safely
    local isOfficer = false
    if IsInGuild() then
        isOfficer = C_GuildInfo.CanEditOfficerNote()
    end

    if _G.RaiderIO and _G.RaiderIO.GetProfile then
        self.ioStatus:SetText("Raider.IO detected. Data active.")
        self.ioStatus:SetTextColor(0, 1, 0)
    else
        -- Logic: If RIO is missing, check if they are an officer before showing error
        if not isOfficer then
            local name = UnitName("player")
            self.ioStatus:SetText("Greetings " .. name)
            self.ioStatus:SetTextColor(1, 1, 1) -- White Text for Greeting
        else
            self.ioStatus:SetText("Raider.IO missing. Data empty.")
            self.ioStatus:SetTextColor(1, 0, 0) -- Red Text for Error
        end
    end
end