local addonName, NS = ...
local GR = NS.GR -- Reference the Core Frame

-- =========================================================================
-- ENGINE: HELPERS
-- =========================================================================

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

NS.Engine = {}
NS.Engine.GetTableSize = GetTableSize 

-- Initialize Engine Properties on GR
GR.foundPlayers = {}    
GR.currentTemplate = "M+ Focused"
GR.rows = {}            
GR.chatFrames = {}      
GR.activeChats = {} 
GR.currentPage = 1 
GR.classIndex = 1
GR.selectedFilterData = nil 
GR.isScanning = false 
GR.lastScanTime = 0 
GR.sortKey = "name"
GR.sortOrder = "ASC" 

-- Whitelist for "Replies Only" filtering (Session based)
GR.sessionWhitelist = {}

-- Scan State Tracking for Pruning
GR.lastScanClassFile = nil

-- =========================================================================
-- ENGINE: STRICT BYTE-RATE LIMITER (12 BPS)
-- =========================================================================

NS.MsgQueue = {}
NS.ByteBudget = 255 -- Start with enough for 1 message immediately
NS.MaxBudget = 512  -- Cap stored budget (don't let it grow infinitely)
NS.RefillRate = 12  -- Bytes per second

-- The Heartbeat: Processes the queue based on Byte Budget
local function ProcessMsgQueue()
    -- 1. Refill Budget
    if NS.ByteBudget < NS.MaxBudget then
        NS.ByteBudget = NS.ByteBudget + NS.RefillRate
        if NS.ByteBudget > NS.MaxBudget then NS.ByteBudget = NS.MaxBudget end
    end

    -- 2. Process Queue
    if #NS.MsgQueue == 0 then return end
    
    local task = NS.MsgQueue[1] -- Peek at first item
    
    -- Calculate Cost (Prefix + Text Length)
    local cost = string.len(task.prefix or "") + string.len(task.text or "") + 1
    
    -- Check Affordability
    if NS.ByteBudget >= cost then
        -- We can afford it. Send it.
        table.remove(NS.MsgQueue, 1) -- Pop
        C_ChatInfo.SendAddonMessage(task.prefix, task.text, task.channel)
        
        -- Deduct cost
        NS.ByteBudget = NS.ByteBudget - cost
        
        -- Optional Debug
        if GR.db and GR.db.debugMode then
            print(string.format("GRS SafeSend: Sent %d bytes. Remaining Budget: %d", cost, NS.ByteBudget))
        end
    else
        -- Too expensive. Wait for next tick.
        -- Do nothing.
    end
end

-- Ticker runs every 1.0 second to add 12 bytes
if not NS.QueueTicker then
    NS.QueueTicker = C_Timer.NewTicker(1.0, ProcessMsgQueue)
end

function GR:SafeSendAddonMessage(prefix, text, channel)
    if not prefix or not text or not channel then return end
    
    table.insert(NS.MsgQueue, {
        prefix = prefix,
        text = text,
        channel = channel
    })
    
    -- Trigger an immediate check in case we have budget now (don't wait 1s if we don't have to)
    ProcessMsgQueue()
end

-- =========================================================================
-- ENGINE: SEARCH & DATA PROCESSING
-- =========================================================================

-- InjectGlobalMenus removed: Menu.ModifyMenu does not exist in Cataclysm Classic

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

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

    if self.SearchBtn then
        self.SearchBtn:Disable()
        local cd = 5
        self.SearchBtn:SetText("|cffff0000" .. (NS.L["COOLDOWN_FOR_SEARCH"] or "Cooldown for search") .. " ("..cd..")|r")
        
        C_Timer.NewTicker(1, function(timer)
            cd = cd - 1
            if cd > 0 then
                if self.SearchBtn then self.SearchBtn:SetText("|cffff0000" .. (NS.L["COOLDOWN_FOR_SEARCH"] or "Cooldown for search") .. " ("..cd..")|r") end
            else
                timer:Cancel()
                if self.SearchBtn then self.SearchBtn:Enable(); self.SearchBtn:SetText(NS.L["SEARCH_PLAYERS"] or "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(NS.L["SCAN_TIMED_OUT"] or "Scan Timed Out - Ready") end end
    end)

    local query = ""
    local filterName = "Custom"
    local modeLabel = ""
    if self.db.remixMode == "OFF" then modeLabel = NS.L["WORLD"] or "World"
    elseif self.db.remixMode == "ALLOW" then modeLabel = NS.L["ALL_ZONES"] or "All Zones"
    -- [[ REMIX OVER ]]
    -- elseif self.db.remixMode == "ONLY" then modeLabel = "Remix Only" 
    end

    local filterType = self.filterType or "Default"
    local activeClass = nil
    local activeRace = nil
    
    local CLASS_LIST = NS.Constants.CLASS_LIST

    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

    -- Store the Class File for Pruning Logic (Match standard API return like "MAGE" or "DEATHKNIGHT")
    if activeClass then
        self.lastScanClassFile = string.upper(activeClass):gsub(" ", "")
    else
        self.lastScanClassFile = nil
    end

    local parts = {}
    if activeClass then 
        -- Get WoW client's locale and look up localized class name for /who query
        local clientLocale = GetLocale()
        local localeTable = NS.Constants.LOCALIZED_CLASSES[clientLocale] or NS.Constants.LOCALIZED_CLASSES["enUS"]
        local localizedClass = localeTable and localeTable[activeClass] or activeClass
        table.insert(parts, 'c-"'..localizedClass..'"') 
    end
    if activeRace then table.insert(parts, 'r-"'..activeRace..'"') end
    local lMin = self.db.levelMin or 1
    local lMax = self.db.levelMax or 999
    table.insert(parts, lMin .. "-" .. lMax)
    query = table.concat(parts, " ")

    if self.ResultsHeader then
        self.ResultsHeader:SetText(string.format((NS.L["UNGUILDED_PLAYERS_FILTER"] or "Unguilded Players - Current Filter: %s + %s"), (filterName == "Custom" and activeClass or filterName), modeLabel))
    end
    if self.StatusText then
        self.StatusText:SetText((NS.L["SCANNING_QUERY"] or "Scanning: %s..."):format(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
    local myRealm = GetNormalizedRealmName()
    
    -- Track names found in this batch for pruning
    local batchNames = {}

    -- Determine which filter data to use (Current selection or nil/default)
    local activeFilter = nil
    if self.filterType == "Custom" and self.selectedFilterData then
        activeFilter = self.selectedFilterData
    end

    for i = 1, numWhos do
        local info = C_FriendList.GetWhoInfo(i)
        if info and (not info.fullGuildName or info.fullGuildName == "") then
            -- Mark as seen in this batch
            batchNames[info.fullName] = true
            
            local zone = info.area or "Unknown"
            local isBlocklisted = NS.Data and NS.Data.BLOCKLISTED_ZONES and NS.Data.BLOCKLISTED_ZONES[zone]
            
            -- [[ SCAN SCOPE LOGIC ]]
            -- "OFF" = World Mode: Exclude players in dungeon/raid instances
            -- "ALLOW" = All Mode: Include everyone regardless of zone
            local mode = self.db.remixMode
            local keep = false
            
            if mode == "ALLOW" then
                keep = true -- "All" mode: ignore zone blocklist entirely
            elseif mode == "OFF" then
                if not isBlocklisted then keep = true end -- "World" mode: enforce blocklist
            end
            
            if keep then
                local normName = info.fullName
                if not string.find(normName, "-") then
                     normName = normName .. "-" .. myRealm
                end
                
                -- Check BOTH Blacklists
                local isBlacklisted = self.db.blacklist[info.fullName] or self.db.permaBlacklist[info.fullName]
                if not isBlacklisted then
                    if self.db.blacklist[normName] or self.db.permaBlacklist[normName] then isBlacklisted = true end
                end
                
                if not isBlacklisted then
                    if not self:IsDuplicate(info.fullName) then
                        -- Pass activeFilter to AddPlayerToTable to validate requirements
                        if self:AddPlayerToTable(info, zone, isBlocklisted, activeFilter) then
                            newCount = newCount + 1
                        end
                    end
                end
            end
        end
    end

    -- PRUNING LOGIC: Remove players who match the scanned criteria but are missing from result
    -- Only prune if result count < 50 (Result is not truncated)
    if self.lastScanClassFile and numWhos < 50 then
        local removed = 0
        for i = #self.foundPlayers, 1, -1 do
            local p = self.foundPlayers[i]
            -- Only check players of the class we just scanned
            if p.classFile == self.lastScanClassFile then
                 -- If they are not in the current batch, they are offline or filtered out by Blizz
                 if not batchNames[p.name] then
                     table.remove(self.foundPlayers, i)
                     removed = removed + 1
                 end
            end
        end
        if removed > 0 and self.db.debugMode then
            print("GRS Pruner: Removed " .. removed .. " missing " .. self.lastScanClassFile .. "s.")
        end
    end

    if self.StatusText then
        self.StatusText:SetText((NS.L["SCAN_COMPLETE_ADDED"] or "Scan Complete. Added %d new players."):format(newCount))
    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

function GR:AddPlayerToTable(info, zone, isBlocklisted, filterData)
    -- Cataclysm Classic: No RaiderIO, no WCL, no M+, no Warband
    local raidN, raidH = "-", "-"

    -- === 1. Apply Custom Filters (Requirements Check) ===
    if filterData then
        local function CheckRaid(playerStr, reqStr)
            if not reqStr or reqStr == "n/a" then return true end
            local pVal = 0
            if playerStr ~= "-" then pVal = tonumber(playerStr:match("^(%d+)")) or 0 end
            local rVal = tonumber(reqStr:match("^(%d+)")) or 0
            if pVal < rVal then return false end
            return true
        end

        if not CheckRaid(raidN, filterData.raidN) then return false end
        if not CheckRaid(raidH, filterData.raidH) then return false end
    end

    -- === 2. Add to Table ===
    local localizedClassName = (LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[info.filename]) or info.filename
    
    table.insert(self.foundPlayers, {
        name = info.fullName,
        race = info.raceStr or "Unknown", 
        fullClass = localizedClassName,
        classFile = info.filename,
        level = info.level,
        zone = zone,
        status = "Online",
        version = isBlocklisted and "Instance" or "World",
        raidN = raidN, 
        heroic = raidH,
        selected = false 
    })
    
    self:SortPlayers(self.sortKey, true)
    return true -- Successfully added
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 == "raidN" then
            valA = tonumber((valA:match("^(%d+)") or 0))
            valB = tonumber((valB:match("^(%d+)") or 0))
        elseif key == "level" then
            valA = tonumber(valA) or 0
            valB = tonumber(valB) 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] or self.db.permaBlacklist[name] then
        print("Debug Info: " .. name .. " is in blacklist. Skipping table add.")
        return
    end

    local score = 0
    local heroic = "-" 
    local mythic = "-" 
    local maxKey = "+15"
    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

-- [[ NEW: IMPLEMENTATION OF PERMA-BLACKLIST LOGIC ]]
function GR:AddToPermaBlacklist(name)
    if not name then return end
    -- Normalize
    if not string.find(name, "-") then
        name = name .. "-" .. GetNormalizedRealmName()
    end
    
    if not self.db.permaBlacklist then self.db.permaBlacklist = {} end
    self.db.permaBlacklist[name] = true
    
    -- Also add to session blacklist to be safe
    if not self.db.blacklist then self.db.blacklist = {} end
    self.db.blacklist[name] = true
    
    print("|cffff0000GRS:|r " .. name .. " added to PERMANENT Blacklist.")
    
    -- Update views if possible
    if self.UpdateScrollFrame then self:UpdateScrollFrame() end
end

function GR:UpdateRecentList() 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

-- =========================================================================
-- ENGINE: CHAT / MESSAGING LOGIC
-- =========================================================================



function GR:SendMessageSequence()
    local sendMode = self.db.sendMode or "MESSAGE"
    local rawMsg = self.InputBody:GetText()
    
    -- Only require message if we're sending messages
    if (sendMode == "MESSAGE" or sendMode == "BOTH") and rawMsg == "" 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
    
    local modeLabel = sendMode == "MESSAGE" and "messages" or (sendMode == "INVITE" and "invites" or "messages + invites")
    self.StatusText:SetText("Initializing Batch Protocol (" .. modeLabel .. ")...")
    
    -- =========================================================================
    -- PHASE 1: SEND ALL GUILD INVITES IMMEDIATELY (in button click context)
    -- =========================================================================
    local inviteCount = 0
    if sendMode == "INVITE" or sendMode == "BOTH" then
        for _, p in ipairs(queue) do
            local checkName = p.name
            if not string.find(checkName, "-") then checkName = checkName .. "-" .. GetNormalizedRealmName() end
            
            local bl = GR.db.blacklist[checkName] or GR.db.blacklist[p.name]
            local pbl = GR.db.permaBlacklist[checkName] or GR.db.permaBlacklist[p.name]
            
            if not bl and not pbl then
                if self.db.debugMode then
                    print("|cff00ff00[DEBUG]:|r Would guild invite: " .. Ambiguate(p.name, "none"))
                else
                    GuildInvite(p.name)
                end
                inviteCount = inviteCount + 1
            end
        end
        
        if inviteCount > 0 and not self.db.debugMode then
            print("|cff00ccff[A.X.I.O.M.]|r Sent " .. inviteCount .. " guild invite(s).")
        end
    end
    
    -- =========================================================================
    -- PHASE 2: QUEUE WHISPER MESSAGES WITH TIMER DELAYS
    -- =========================================================================
    if sendMode == "MESSAGE" or sendMode == "BOTH" then
        local batchSize = 5; local batchCooldown = 5; local currentIndex = 1
        
        local function ProcessNext()
            if currentIndex > #queue then 
                local doneLabel = sendMode == "MESSAGE" and "All messages sent." or ("All " .. inviteCount .. " invite(s) + messages sent.")
                GR.StatusText:SetText(doneLabel)
                return 
            end
            local p = queue[currentIndex]
            
            local msg = rawMsg
            local safeName = Ambiguate(p.name, "none")
            local myName = UnitName("player")
            local gName = GetGuildInfo("player") or "Guild"
            local pClass = p.fullClass or "Hero"
            
            msg = msg:gsub("%b[]", function(captured)
                local inner = captured:sub(2, -2)
                local clean = inner:lower():gsub("%s+", "")
                if clean == "player" then return safeName end
                if clean == "ownname" then return myName end
                if clean == "guild" then return gName end
                if clean == "playerclass" or clean == "class" then return pClass end
                return captured
            end)
            
            local isBatchPause = (currentIndex > 1) and ((currentIndex - 1) % batchSize == 0)
            local delay = isBatchPause and batchCooldown or GR.db.scanDelay
            
            C_Timer.After(delay, function()
                local checkName = p.name
                if not string.find(checkName, "-") then checkName = checkName .. "-" .. GetNormalizedRealmName() end
                
                local bl = GR.db.blacklist[checkName] or GR.db.blacklist[p.name]
                local pbl = GR.db.permaBlacklist[checkName] or GR.db.permaBlacklist[p.name]

                if not bl and not pbl then
                    if GR.db.debugMode then
                        print("|cff00ff00[DEBUG]:|r Would whisper: " .. p.name .. " -> " .. msg)
                        if p.name == UnitName("player") then SendChatMessage(msg, "WHISPER", nil, p.name) end
                        GR:AddToRecent(p.name, "Sent (Debug)")
                    else
                        SendChatMessage(msg, "WHISPER", nil, p.name)
                        local statusLabel = sendMode == "MESSAGE" and "Sent" or "Sent + Invited"
                        GR:AddToRecent(p.name, statusLabel)
                    end
                    
                    GR:AddToBlacklist(p.name)
                    GR.sessionWhitelist[p.name] = true
                    GR.sessionWhitelist[checkName] = true
                end
                currentIndex = currentIndex + 1
                ProcessNext() 
            end)
        end
        ProcessNext()
    else
        -- INVITE ONLY mode: just blacklist and finish
        for _, p in ipairs(queue) do
            local checkName = p.name
            if not string.find(checkName, "-") then checkName = checkName .. "-" .. GetNormalizedRealmName() end
            
            local bl = GR.db.blacklist[checkName] or GR.db.blacklist[p.name]
            local pbl = GR.db.permaBlacklist[checkName] or GR.db.permaBlacklist[p.name]
            
            if not bl and not pbl then
                GR:AddToBlacklist(p.name)
                GR:AddToRecent(p.name, "Invited")
                GR.sessionWhitelist[p.name] = true
                GR.sessionWhitelist[checkName] = true
            end
        end
        GR.StatusText:SetText(inviteCount .. " invite(s) sent.")
    end
end

function GR:HandleChatEvent(event, msg, sender, ...)
    if not sender then return end
    -- Capture GUID (Index 12 in 11.0+)
    local senderGUID = select(10, ...) -- The 12th arg is at index 10 when '...' starts after sender
    
    sender = Ambiguate(sender, "none") 
    if not self:IsShown() then return end
    
    -- [[ REPLIES ONLY FILTERING ]]
    if self.RepliesOnlyCheck and self.RepliesOnlyCheck:GetChecked() then
        -- 1. Handling Outgoing (INFORM)
        -- STRICT RULE: If "Replies Only" is checked, NEVER create a new chat window for an outgoing message.
        -- We only allow the update if the chat session ALREADY exists.
        if event == "CHAT_MSG_WHISPER_INFORM" then
             local isActive = false
             for _, s in ipairs(self.activeChats) do 
                 if s.name == sender then isActive = true; break end
                 local sName = strsplit("-", s.name)
                 if sName == sender then isActive = true; break end
             end
             -- If we are not already talking to them, suppress the visual clutter.
             if not isActive then return end
        end
        
        -- 2. Handling Incoming (WHISPER)
        if event == "CHAT_MSG_WHISPER" then
             -- Check if we are already tracking this user in activeChats (Conversation active)
             local isTracked = false
             
             -- Check Active Chats
             for _, s in ipairs(self.activeChats) do 
                 if s.name == sender then isTracked = true; break end
                 local sName = strsplit("-", s.name)
                 if sName == sender then isActive = true; break end
             end
             
             -- Check Whitelist (Is this a reply to our batch?)
             if not isTracked then
                 local isTarget = GR.sessionWhitelist[sender]
                 if not isTarget then
                     local sName = strsplit("-", sender)
                     if GR.sessionWhitelist[sName] then isTarget = true end
                 end
                 if isTarget then isTracked = true end
             end
             
             -- If neither active nor whitelisted, ignore
             if not isTracked then return end
        end
    end
    
    self:UpdateWhisperGrid(sender, msg, event == "CHAT_MSG_WHISPER_INFORM", senderGUID)
end

function GR:UpdateWhisperGrid(name, msg, isOutgoing, senderGUID)
    local session = nil
    -- [[ IMPROVED MATCHING: CHECK SHORT AND FULL NAMES ]]
    for _, s in ipairs(self.activeChats) do 
        if s.name == name then 
            session = s; break 
        end
        local sName, sRealm = strsplit("-", s.name)
        local tName, tRealm = strsplit("-", name)
        if sName == tName then
            session = s; break
        end
    end
    
    local pageSize = self.chatPageSize or 6 -- MATCHED TO UI.LUA (6)
    
    if not session then
        if #self.activeChats >= 100 then table.remove(self.activeChats, 1) end
        
        -- [[ FIND CLASS FOR COLOR ]]
        local classFile = "PRIEST"
        
        -- 1. Check Existing Search Results (Fastest)
        if GR.foundPlayers then
            for _, p in ipairs(GR.foundPlayers) do
                if p.name == name then
                    classFile = p.classFile
                    break
                end
                -- Check Shortname match
                local short = strsplit("-", p.name)
                if short == name then
                    classFile = p.classFile
                    break
                end
            end
        end
        
        -- 2. If not found, use GUID (Best for incoming random whispers)
        if classFile == "PRIEST" and senderGUID then
            local _, cFile = GetPlayerInfoByGUID(senderGUID)
            if cFile then
                classFile = cFile
            end
        end
        
        session = {name = name, classFile = classFile, log = {}}
        table.insert(self.activeChats, session)
        self.currentPage = math.ceil(#self.activeChats / pageSize)
    end
    
    -- Late bind class if previously unknown (update existing session)
    if session.classFile == "PRIEST" and senderGUID then
         local _, cFile = GetPlayerInfoByGUID(senderGUID)
         if cFile then session.classFile = cFile end
    end

    session.lastActivity = GetTime()
    
    -- [[ COLOR LOGIC FOR LOG ]]
    local cObj = RAID_CLASS_COLORS[session.classFile or "PRIEST"]
    local cHex = cObj and cObj:GenerateHexColor() or "ffffffff"
    
    local senderColor = isOutgoing and "ff00ccff" or cHex -- Cyan for self, Class for target
    local senderName = isOutgoing and "You" or name
    
    -- Format: [Name]: Message
    local logLine = string.format("|c%s[%s]:|r %s", senderColor, senderName, msg)
    
    table.insert(session.log, logLine)
    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()
    local pageSize = self.chatPageSize or 6 -- MATCHED TO UI.LUA (6)
    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
    
    -- [[ FIXED: NON-DESTRUCTIVE UPDATE ]]
    -- We now check if the frame is already visible before calling Show() to prevent focus stealing.
    
    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
                if not frame:IsShown() then frame:Show() end -- Guard against focus loss
                frame.headerBtn:Show()
                frame.dataIndex = dataIndex 
                
                -- [[ COLOR HEADER NAME ]]
                local cObj = RAID_CLASS_COLORS[session.classFile or "PRIEST"]
                local cHex = cObj and cObj:GenerateHexColor() or "ffffffff"
                frame.headerBtn.text:SetText("|c"..cHex..session.name.."|r")
                frame.headerBtn.playerName = session.name 
                
                local fullText = ""
                for _, line in ipairs(session.log) do fullText = fullText .. line .. "\n" end
                
                -- Check if scroll is near bottom before update to maintain stickiness
                local oldScroll = frame.scroll:GetVerticalScroll()
                local isAtBottom = (oldScroll >= frame.scroll:GetVerticalScrollRange() - 10)
                
                frame.text:SetText(fullText)
                
                if isAtBottom then
                    frame.scroll:SetVerticalScroll(frame.scroll:GetVerticalScrollRange())
                end
            else
                if frame:IsShown() then frame:Hide() end -- Hide ONLY if necessary
            end
        end
    end
end

-- =========================================================================
-- ENGINE: FILTERS & LOGIC
-- =========================================================================

function GR:BuildFilterUI()
    local f = CreateFrame("Frame", "GRS_FilterFrame", UIParent)
    f:SetSize(340, 460); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:Hide()
    NS.CreateBg(f, 0, 0, 0, 0.95); NS.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

    -- Reusable: Styled EditBox (dark container with border)
    local function CreateStyledEdit(parent, width, height, numericOnly, maxLetters)
        local box = CreateFrame("Frame", nil, parent)
        box:SetSize(width, height)
        NS.CreateBg(box, 0.1, 0.1, 0.1, 0.8); NS.CreateBorder(box, 0.3, 0.3, 0.3, 1)
        local eb = CreateFrame("EditBox", nil, box)
        eb:SetPoint("TOPLEFT", 6, -2); eb:SetPoint("BOTTOMRIGHT", -6, 2)
        eb:SetFontObject("GameFontHighlightSmall"); eb:SetAutoFocus(false)
        if maxLetters then eb:SetMaxLetters(maxLetters) end
        eb:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
        eb:SetScript("OnEnterPressed", function(s) s:ClearFocus() end)
        if numericOnly then
            eb:SetScript("OnTextChanged", function(s)
                local t = s:GetText(); local clean = t:gsub("[^0-9]", "")
                if t ~= clean then s:SetText(clean) end
            end)
        end
        box:EnableMouse(true)
        box:SetScript("OnMouseDown", function() eb:SetFocus() end)
        return box, eb
    end

    -- Reusable: Styled Dropdown Button (dark bg, border, ▾ arrow)
    local function CreateStyledDropdown(parent, width)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetSize(width, 24)
        NS.CreateBg(btn, 0.15, 0.15, 0.15, 1); NS.CreateBorder(btn, 0.3, 0.3, 0.3, 1)
        local txt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        txt:SetPoint("LEFT", 8, 0); txt:SetText("Any")
        local arrow = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        arrow:SetPoint("RIGHT", -8, 0); arrow:SetText("|cff888888\226\150\190|r")
        btn:SetScript("OnEnter", function() arrow:SetText("|cffFFFFFF\226\150\190|r") end)
        btn:SetScript("OnLeave", function() arrow:SetText("|cff888888\226\150\190|r") end)
        return btn, txt
    end

    local LABEL_X = 20
    local CONTROL_X = 150
    local ROW_H = 35
    local y = -50

    -- =============================================
    -- ROW 1: FILTER NAME (at top)
    -- =============================================
    local lblN = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lblN:SetPoint("TOPLEFT", LABEL_X, y); lblN:SetText("Filter Name:")
    local boxN, editN = CreateStyledEdit(f, 160, 24, false, nil)
    boxN:SetPoint("TOPLEFT", CONTROL_X, y)
    y = y - ROW_H

    -- =============================================
    -- ROW 2: CLASS DROPDOWN
    -- =============================================
    local lblC = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lblC:SetPoint("TOPLEFT", LABEL_X, y); lblC:SetText("Class:")
    local btnC, txtC = CreateStyledDropdown(f, 160)
    btnC:SetPoint("TOPLEFT", CONTROL_X, y)
    y = y - ROW_H

    -- =============================================
    -- ROW 3: RACE DROPDOWN
    -- =============================================
    local lblR = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lblR:SetPoint("TOPLEFT", LABEL_X, y); lblR:SetText("Race:")
    local btnR, txtR = CreateStyledDropdown(f, 160)
    btnR:SetPoint("TOPLEFT", CONTROL_X, y)
    y = y - ROW_H

    -- =============================================
    -- ROW 4: M+ SCORE MIN
    -- =============================================
    local lblM = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lblM:SetPoint("TOPLEFT", LABEL_X, y); lblM:SetText("M+ Score min.:")
    local boxM, editM = CreateStyledEdit(f, 80, 24, true, 4)
    boxM:SetPoint("TOPLEFT", CONTROL_X, y)
    y = y - ROW_H

    -- =============================================
    -- ROW 5: WARBAND SCORE MIN (NEW)
    -- =============================================
    local lblW = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lblW:SetPoint("TOPLEFT", LABEL_X, y); lblW:SetText("Warband Score min.:")
    local boxW, editW = CreateStyledEdit(f, 80, 24, true, 4)
    boxW:SetPoint("TOPLEFT", CONTROL_X + 30, y)
    y = y - ROW_H

    -- =============================================
    -- ROW 6-8: RAID PROGRESS DROPDOWNS
    -- =============================================
    local function CreateRaidDropdown(label, yPos, parentKey)
        local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        lbl:SetPoint("TOPLEFT", LABEL_X, yPos); lbl:SetText(label)
        local btn, txt = CreateStyledDropdown(f, 100)
        btn:SetPoint("TOPLEFT", CONTROL_X, yPos); txt:SetText("n/a")
        
        local list = CreateFrame("Frame", nil, f)
        NS.CreateBg(list, 0.1, 0.1, 0.1, 1); NS.CreateBorder(list, 0.4, 0.4, 0.4, 1)
        list:SetSize(100, 200); list:SetPoint("TOPLEFT", btn, "BOTTOMLEFT", 0, -2)
        list:SetFrameStrata("TOOLTIP"); list:Hide()
        
        local opts = {"n/a", "1/8", "2/8", "3/8", "4/8", "5/8", "6/8", "7/8", "8/8"}
        local ly = -5
        for _, opt in ipairs(opts) do
            local b = CreateFrame("Button", nil, list); b:SetSize(90, 20); b:SetPoint("TOPLEFT", 5, ly)
            local glow = b:CreateTexture(nil, "BACKGROUND"); glow:SetAllPoints(); glow:SetColorTexture(0.2, 0.8, 0.3, 0.15); glow:Hide()
            local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(opt)
            b:SetScript("OnClick", function() 
                f[parentKey] = opt; txt:SetText(opt); list:Hide()
            end)
            b:SetScript("OnEnter", function() glow:Show(); t:SetTextColor(1, 1, 1) end)
            b:SetScript("OnLeave", function() glow:Hide(); t:SetTextColor(1, 0.82, 0) end)
            ly = ly - 20
        end
        list:SetHeight(math.abs(ly) + 5)
        
        -- Override the dropdown button click to toggle this list (and close others)
        f[parentKey.."List"] = list
        f[parentKey.."Btn"] = btn
        return btn, txt, list
    end

    local btnRaidN, txtRaidN, listRaidN = CreateRaidDropdown("Normal Raid:", y, "selRaidN")
    y = y - ROW_H
    local btnRaidH, txtRaidH, listRaidH = CreateRaidDropdown("Heroic Raid:", y, "selRaidH")
    y = y - ROW_H
    local btnRaidM, txtRaidM, listRaidM = CreateRaidDropdown("Mythic Raid:", y, "selRaidM")

    -- Wire raid dropdown clicks (close siblings)
    btnRaidN:SetScript("OnClick", function() listRaidN:SetShown(not listRaidN:IsShown()); listRaidH:Hide(); listRaidM:Hide() end)
    btnRaidH:SetScript("OnClick", function() listRaidH:SetShown(not listRaidH:IsShown()); listRaidN:Hide(); listRaidM:Hide() end)
    btnRaidM:SetScript("OnClick", function() listRaidM:SetShown(not listRaidM:IsShown()); listRaidN:Hide(); listRaidH:Hide() end)

    -- =============================================
    -- BOTTOM: SAVE / CANCEL
    -- =============================================
    local bSave = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    bSave:SetSize(100, 24); bSave:SetPoint("BOTTOMLEFT", 20, 20); bSave:SetText("Save")
    NS.SkinButton(bSave)

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

    -- =============================================
    -- CLASS / RACE DROPDOWN LISTS
    -- =============================================
    local listC = CreateFrame("Frame", nil, f)
    NS.CreateBg(listC, 0.1, 0.1, 0.1, 1); NS.CreateBorder(listC, 0.4, 0.4, 0.4, 1)
    listC:SetSize(160, 300); listC:SetPoint("TOPLEFT", btnC, "BOTTOMLEFT", 0, -2)
    listC:SetFrameStrata("TOOLTIP"); listC:Hide()

    local listR = CreateFrame("Frame", nil, f)
    NS.CreateBg(listR, 0.1, 0.1, 0.1, 1); NS.CreateBorder(listR, 0.4, 0.4, 0.4, 1)
    listR:SetSize(160, 300); listR:SetPoint("TOPLEFT", btnR, "BOTTOMLEFT", 0, -2)
    listR:SetFrameStrata("TOOLTIP"); listR:Hide()
    
    local function MkDropBtn(parent, text, func, yOff)
        local b = CreateFrame("Button", nil, parent); b:SetSize(148, 20); b:SetPoint("TOPLEFT", 5, yOff)
        local glow = b:CreateTexture(nil, "BACKGROUND"); glow:SetAllPoints(); glow:SetColorTexture(0.2, 0.8, 0.3, 0.15); glow:Hide()
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(text)
        b:SetScript("OnClick", function() func(text); parent:Hide() end)
        b:SetScript("OnEnter", function() glow:Show(); t:SetTextColor(1, 1, 1) end)
        b:SetScript("OnLeave", function() glow:Hide(); t:SetTextColor(1, 0.82, 0) end)
    end
    
    MkDropBtn(listC, "Any", function(v) f.selClass=v; txtC:SetText(v) end, -5)
    local lyC = -25
    for _, c in ipairs(NS.Constants.CLASS_LIST) do
        MkDropBtn(listC, c, function(v) f.selClass=v; txtC:SetText(v) end, lyC)
        lyC = lyC - 20
    end
    listC:SetHeight(math.abs(lyC) + 5)
    
    MkDropBtn(listR, "Any", function(v) f.selRace=v; txtR:SetText(v) end, -5)
    local lyR = -25
    for _, r in ipairs(NS.Constants.RACES[UnitFactionGroup("player")] or {}) do
        MkDropBtn(listR, r, function(v) f.selRace=v; txtR:SetText(v) end, lyR)
        lyR = lyR - 20
    end
    listR:SetHeight(math.abs(lyR) + 5)
    
    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)
    
    -- =============================================
    -- SAVE LOGIC
    -- =============================================
    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,
            minScore = editM:GetText(),
            minWarbandScore = editW:GetText(),
            raidN = f.selRaidN or "n/a",
            raidH = f.selRaidH or "n/a",
            raidM = f.selRaidM or "n/a"
        }
        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)
    
    -- =============================================
    -- OPEN CREATE / OPEN EDIT
    -- =============================================
    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("")
        editM:SetText("")
        editW:SetText("")
        f.selRaidN = "n/a"; txtRaidN:SetText("n/a")
        f.selRaidH = "n/a"; txtRaidH:SetText("n/a")
        f.selRaidM = "n/a"; txtRaidM:SetText("n/a")
        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)
        editM:SetText(data.minScore or "")
        editW:SetText(data.minWarbandScore or "")
        f.selRaidN = data.raidN or "n/a"; txtRaidN:SetText(f.selRaidN)
        f.selRaidH = data.raidH or "n/a"; txtRaidH:SetText(f.selRaidH)
        f.selRaidM = data.raidM or "n/a"; txtRaidM:SetText(f.selRaidM)
        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:DeleteCurrentFilter()
    if not self.selectedFilterData then
        print("|cffff0000GRS: No custom filter selected. Select a custom filter first.|r")
        return
    end
    
    local filterName = self.selectedFilterData.name
    
    StaticPopupDialogs["GRS_DELETE_FILTER"] = {
        text = "Delete filter \"%s\"?",
        button1 = "Delete",
        button2 = "Cancel",
        OnAccept = function()
            GR.db.customFilters[filterName] = nil
            GR.filterType = "Default"
            GR.selectedFilterData = nil
            GR.dFiltTxt:SetText("Filter: Default (All)")
            GR:RefreshMainFilterList()
            print("|cff00ccffGRS:|r Filter deleted: " .. filterName)
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = 3,
    }
    StaticPopup_Show("GRS_DELETE_FILTER", filterName)
end

function GR:DeleteCurrentTemplate(dTempTxt)
    local templateName = self.InputName:GetText()
    if not templateName or templateName == "" then
        print("|cffff0000GRS: No template name entered. Type or select a template name first.|r")
        return
    end
    if not self.db.templates[templateName] then
        print("|cffff0000GRS: Template '" .. templateName .. "' does not exist.|r")
        return
    end
    
    StaticPopupDialogs["GRS_DELETE_TEMPLATE"] = {
        text = "Delete template \"%s\"?",
        button1 = "Delete",
        button2 = "Cancel",
        OnAccept = function()
            GR.db.templates[templateName] = nil
            
            -- If the deleted template was the active one, switch to first remaining or "Default"
            if GR.db.messageTemplate == templateName then
                local firstKey = nil
                for k, _ in pairs(GR.db.templates) do
                    firstKey = k
                    break
                end
                if firstKey then
                    GR.db.messageTemplate = firstKey
                    if dTempTxt then dTempTxt:SetText(firstKey) end
                    GR.InputName:SetText(firstKey)
                    GR.InputBody:SetText(GR.db.templates[firstKey] or "")
                else
                    -- No templates left, create a fresh Default
                    GR.db.templates["Default"] = "Hi there!"
                    GR.db.messageTemplate = "Default"
                    if dTempTxt then dTempTxt:SetText("Default") end
                    GR.InputName:SetText("Default")
                    GR.InputBody:SetText("Hi there!")
                end
            end
            
            -- Clear the inputs
            GR.InputName:SetText("")
            GR.InputBody:SetText("")
            
            print("|cff00ccffGRS:|r Template deleted: " .. templateName)
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = 3,
    }
    StaticPopup_Show("GRS_DELETE_TEMPLATE", templateName)
end

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

    local isOfficer = false
    if IsInGuild() or GR.devOfficerMode then
        isOfficer = CanEditOfficerNote() or GR.devOfficerMode
    end

    -- Cata Classic: No RaiderIO, no WCL — just display greeting or guild status
    if not isOfficer then
        local name = UnitName("player")
        self.ioStatus:SetText((NS.L["GREETINGS"] or "Greetings") .. " " .. name .. "   |cff808080[Classic Era]|r")
        self.ioStatus:SetTextColor(1, 1, 1)
    else
        self.ioStatus:SetText("|cff00ff00A.X.I.O.M. Classic Era|r")
        self.ioStatus:SetTextColor(0, 1, 0)
    end
end

function GR:UpdateOfficerStatus()
    if not self.officerStatus then return end

    local hasAccess = false
    local hasGuild = IsInGuild()
    
    if hasGuild or GR.devOfficerMode then
        local canEditNote = CanEditOfficerNote() or GR.devOfficerMode
        local canInvite = (CanGuildInvite and CanGuildInvite()) or GR.devOfficerMode
        if canEditNote or canInvite then
            hasAccess = true
        end
    end

    if not hasGuild and not GR.devOfficerMode then
        self.officerStatus:SetText(NS.L["NO_GUILD_DETECTED"] or "No Guild detected.")
        self.officerStatus:SetTextColor(0.5, 0.5, 0.5) 
    elseif hasAccess then
        self.officerStatus:SetText(NS.L["RECRUITER_MODE_ACTIVE"] or "Recruiter mode Active ")
        self.officerStatus:SetTextColor(0, 1, 0) 
    else
        self.officerStatus:SetText("")
        self.officerStatus:SetTextColor(1, 0, 0) 
    end
    
    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 hasAccess then
                    tab:Show()
                else
                    tab:Hide()
                end
            end
            if tab:IsShown() then
                tab:ClearAllPoints()
                tab:SetPoint("TOP", self.SideBar, "TOP", 0, currentY)
                currentY = currentY - 40
            end
        end
    end
    
    if not hasAccess and restrictedTabs[self.activeTab] then
        print("|cffff0000GRS: Access restricted. Switching to Guild Roster.|r")
        self:SwitchView(5)
    end

    -- [FIX] Rebuild empty module views when access is granted
    -- BuildRequirementsView may have returned early at init (GetDB() was nil).
    -- When access is later granted (e.g. devOfficerMode toggled), rebuild it now.
    if hasAccess then
        if self.BuildRequirementsView and self.views and self.views[3] then
            if not self.views[3].container then
                self:BuildRequirementsView(self.views[3])
            elseif self.RefreshReqUI then
                self:RefreshReqUI()
            end
        end
    end
end

-- =========================================================================
-- ENGINE: AUTO WELCOME LISTENER
-- =========================================================================
local autoWelcomeFrame = CreateFrame("Frame")
autoWelcomeFrame:RegisterEvent("CHAT_MSG_SYSTEM")
autoWelcomeFrame:SetScript("OnEvent", function(_, _, msg)
    if not GR.db or not GR.db.automateWelcome then return end
    
    -- Parse "X has joined the guild." using Global String
    -- ERR_GUILD_JOIN_S is typically "%s has joined the guild."
    local pattern = ERR_GUILD_JOIN_S:gsub("%%s", "(.*)")
    local joinedPlayer = msg:match(pattern)
    
    if joinedPlayer then
        -- Check Whitelist (Did we recruit them?)
        local short = strsplit("-", joinedPlayer)
        -- Normalizing check
        local isRecruit = GR.sessionWhitelist[joinedPlayer] or GR.sessionWhitelist[short]
        
        if isRecruit then
            local msgs = GR.db.welcomeMessages
            local candidates = {}
            if msgs then
                for _, m in pairs(msgs) do if m ~= "" then table.insert(candidates, m) end end
            end
            
            if #candidates > 0 then
                local text = candidates[math.random(1, #candidates)]
                
                -- [[ DATA GATHERING ]]
                local myName = UnitName("player")
                local gName = GetGuildInfo("player") or "Guild"
                local pClass = "Hero" -- Default
                
                -- Attempt class lookup from recent scans
                for _, p in ipairs(GR.foundPlayers) do
                    -- Check exact match or shortname match
                    if p.name == joinedPlayer or (strsplit("-", p.name) == short) then
                        pClass = p.fullClass or p.classFile or "Hero"
                        break
                    end
                end
                
                -- [[ SMART REPLACEMENT ]]
                text = text:gsub("%b[]", function(captured)
                    local inner = captured:sub(2, -2) -- Strip brackets
                    local clean = inner:lower():gsub("%s+", "") -- Lowercase, remove spaces
                    
                    if clean == "player" then return joinedPlayer end
                    if clean == "ownname" then return myName end
                    if clean == "guild" then return gName end
                    if clean == "playerclass" or clean == "class" then return pClass end
                    return captured -- Return original if no match
                end)
                
                -- Send to Guild Chat (Standard for welcomes) with delay
                local function SendWelcome() SendChatMessage(text, "GUILD") end
                if C_Timer and C_Timer.After then C_Timer.After(8.0, SendWelcome) else
                    local wf = CreateFrame("Frame"); local we = 0
                    wf:SetScript("OnUpdate", function(s, dt) we = we + dt; if we >= 8.0 then s:SetScript("OnUpdate", nil); SendWelcome() end end)
                end
            end
        end
    end
end)