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

function GR:InjectGlobalMenus()
    if Menu and Menu.ModifyMenu then
        local function AddGRSActions(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)
                -- [[ NEW: Perma-Blacklist Option ]]
                rootDescription:CreateButton("Perma-Blacklist", function() 
                    if GR.AddToPermaBlacklist then GR:AddToPermaBlacklist(name) end
                end)
            end
        end
        Menu.ModifyMenu("MENU_UNIT_PLAYER", function(owner, root, context) AddGRSActions(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_FRIEND", function(owner, root, context) AddGRSActions(root, context) end)
        Menu.ModifyMenu("MENU_CHAT_PLAYER", function(owner, root, context) AddGRSActions(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_PARTY", function(owner, root, context) AddGRSActions(root, context) end)
        Menu.ModifyMenu("MENU_UNIT_RAID", function(owner, root, context) AddGRSActions(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 

    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 80
    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)
    local score = 0
    local raidN, raidH, raidM = "-", "-", "-"
    local maxKey = "-"
    local warbandScore = 0
    
    -- === 1. Fetch Raider.IO Data ===
    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
            if p.mythicKeystoneProfile then 
                score = p.mythicKeystoneProfile.currentScore or 0 
                if p.mythicKeystoneProfile.mplusWarbandCurrent then
                    warbandScore = p.mythicKeystoneProfile.mplusWarbandCurrent.score or 0
                end
            end
            if p.mythicKeystoneProfile and p.mythicKeystoneProfile.sortedDungeons then
                local top = p.mythicKeystoneProfile.sortedDungeons[1]
                if top then maxKey = "+"..top.level end
            end
            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 ""
                    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 
                        if prog.difficulty == 2 then raidH = progressStr end 
                        if prog.difficulty == 3 then raidM = progressStr end 
                    end
                end
            end
        end
    end

    -- === 2. Apply Custom Filters (Requirements Check) ===
    if filterData then
        -- M+ Score Check
        if filterData.minScore and filterData.minScore ~= "" then
            local req = tonumber(filterData.minScore) or 0
            if score < req then return false end
        end

        local function CheckRaid(playerStr, reqStr)
            if not reqStr or reqStr == "n/a" then return true end
            -- Convert "X/8" or "n/a" to number
            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
        if not CheckRaid(raidM, filterData.raidM) then return false end
    end

    -- === 3. Add to Table ===
    -- Get localized class name for display from WoW's built-in 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",
        rio = score,
        raidN = raidN, 
        heroic = raidH,
        mythic = raidM,
        maxKey = maxKey,
        warbandScore = warbandScore,
        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 == "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" or key == "warbandScore" then
            valA = tonumber(valA) or 0
            valB = tonumber(valB) or 0
        elseif key == "maxKey" then
            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] 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
                    C_GuildInfo.Invite(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 = C_ClassColor.GetClassColor(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 = C_ClassColor.GetClassColor(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(300, 480); 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
    
    -- CLASS
    local lblC = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblC:SetPoint("TOPLEFT", 20, -50); lblC:SetText("Class:")
    local btnC = CreateFrame("Button", nil, f); NS.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")
    
    -- RACE
    local lblR = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblR:SetPoint("TOPLEFT", 20, -85); lblR:SetText("Race:")
    local btnR = CreateFrame("Button", nil, f); NS.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")
    
    -- NAME
    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)
    
    -- M+ SCORE MIN
    local lblM = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblM:SetPoint("TOPLEFT", 20, -155); lblM:SetText("M+ Score min.:")
    local editM = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); editM:SetSize(80, 24); editM:SetPoint("LEFT", lblM, "RIGHT", 10, 0)
    editM:SetAutoFocus(false); editM:SetMaxLetters(4)
    editM:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editM:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    editM:SetScript("OnTextChanged", function(self)
        local t = self:GetText()
        local clean = t:gsub("[^0-9]", "")
        if t ~= clean then self:SetText(clean) end
    end)
    
    -- RAID PROGRESS DROPDOWNS
    local function CreateRaidDropdown(label, y, parentKey)
        local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lbl:SetPoint("TOPLEFT", 20, y); lbl:SetText(label)
        local btn = CreateFrame("Button", nil, f); NS.CreateBg(btn, 0.2, 0.2, 0.2, 1); btn:SetSize(100, 24); btn:SetPoint("LEFT", lbl, "RIGHT", 10, 0)
        local txt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); txt:SetPoint("LEFT", 5, 0); txt:SetText("n/a")
        
        local list = CreateFrame("Frame", nil, f); NS.CreateBg(list, 0.1, 0.1, 0.1, 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 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)
            ly = ly - 20
        end
        list:SetHeight(math.abs(ly) + 5)
        btn:SetScript("OnClick", function() list:SetShown(not list:IsShown()) end)
        
        f[parentKey.."List"] = list
        return btn, txt
    end

    local btnRaidN, txtRaidN = CreateRaidDropdown("Normal Raid:", -190, "selRaidN")
    local btnRaidH, txtRaidH = CreateRaidDropdown("Heroic Raid:", -225, "selRaidH")
    local btnRaidM, txtRaidM = CreateRaidDropdown("Mythic Raid:", -260, "selRaidM")

    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) 

    -- Dropdowns for Class/Race
    local listC = CreateFrame("Frame", nil, f); NS.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); NS.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(NS.Constants.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(NS.Constants.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,
            minScore = editM: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)
    
    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("")
        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 "")
        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:UpdateRaiderIOStatus()
    if not self.ioStatus then return end

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

    if _G.RaiderIO and _G.RaiderIO.GetProfile then
        self.ioStatus:SetText(NS.L["RAIDERIO_ACTIVE"] or "Raider.IO addon active. Data retrieved")
        self.ioStatus:SetTextColor(0, 1, 0)
    else
        if not isOfficer then
            local name = UnitName("player")
            self.ioStatus:SetText((NS.L["GREETINGS"] or "Greetings") .. " " .. name)
            self.ioStatus:SetTextColor(1, 1, 1) 
        else
            self.ioStatus:SetText(NS.L["RAIDERIO_MISSING"] or "Raider.IO missing or not loaded. Check addons load list.")
            self.ioStatus:SetTextColor(1, 0, 0) 
        end
    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 = C_GuildInfo.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
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
                C_Timer.After(8.0, function()
                    SendChatMessage(text, "GUILD")
                end)
            end
        end
    end
end)