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.recruitedPlayers = {} -- Registry for strict recruitment tracking
GR.currentPage = 1
GR.classIndex = 1
GR.selectedFilterData = nil
GR.isScanning = false
GR.lastScanTime = 0
GR.sortKey = "name"
GR.sortOrder = "ASC"

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

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

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

    -- [[ BUTTON COOLDOWN MANAGEMENT ]]
    -- Apply cooldown visual to BOTH Standard and Compact buttons
    local buttonsToUpdate = { self.SearchBtn, self.CompactSearchBtn }

    for _, btn in pairs(buttonsToUpdate) do
        if btn then
            btn:Disable()
            -- Set initial text
            if btn == self.SearchBtn then
                btn:SetText("|cffff0000Cooldown for search (5)|r") -- Standard Text
            else
                btn:SetText("|cffff0000Cooldown (5)|r")            -- Compact Text (Shorter)
            end
        end
    end

    -- Timer for countdown
    local cd = 5
    C_Timer.NewTicker(1, function(timer)
        cd = cd - 1
        if cd > 0 then
            for _, btn in pairs(buttonsToUpdate) do
                if btn then
                    if btn == self.SearchBtn then
                        btn:SetText("|cffff0000Cooldown for search (" .. cd .. ")|r")
                    else
                        btn:SetText("|cffff0000Cooldown (" .. cd .. ")|r")
                    end
                end
            end
        else
            timer:Cancel()
            for _, btn in pairs(buttonsToUpdate) do
                if btn then
                    btn:Enable()
                    btn:SetText("Search Players")
                end
            end
        end
    end, 5)

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

    local query = ""
    local filterName = "Custom"
    local modeLabel = ""
    if self.db.remixMode == "OFF" then
        modeLabel = "World Only"
    elseif self.db.remixMode == "ALLOW" then
        modeLabel = "All Zones"
    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

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

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

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

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

function GR:ProcessWhoResults()
    if not self.isScanning then return end
    if FriendsFrame and FriendsFrame:IsShown() then FriendsFrame:Hide() end
    if self.db.debugMode then
        self.isScanning = false; return
    end

    local numWhos = C_FriendList.GetNumWhoResults()
    local newCount = 0
    local myRealm = GetRealmName()

    for i = 1, numWhos do
        local info = C_FriendList.GetWhoInfo(i)
        if info and (not info.fullGuildName or info.fullGuildName == "") then
            local zone = info.area or "Unknown"

            -- [[ LOGIC: World Only vs All ]]
            local isBlocklisted = NS.Data and NS.Data.BLOCKLISTED_ZONES and NS.Data.BLOCKLISTED_ZONES[zone]
            local mode = self.db.remixMode -- "OFF" = World Only, "ALLOW" = All

            local keep = false
            if mode == "ALLOW" then
                keep = true -- "All" mode: Ignore Blocklist (Risky)
            elseif mode == "OFF" then
                -- "World Only" mode: Strict Blocklist enforcement
                if not isBlocklisted then keep = true end
            end

            if keep then
                local isBlacklisted = self.db.blacklist[info.fullName]
                    or self.db.permaBlacklist[info.fullName]
                if not isBlacklisted then
                    local normName = info.fullName
                    if not string.find(normName, "-") then
                        normName = normName .. "-" .. myRealm
                    end
                    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
                        self:AddPlayerToTable(info, zone, isBlocklisted)
                        newCount = newCount + 1
                    end
                end
            end
        end
    end
    if self.StatusText then
        self.StatusText:SetText("Scan Complete. Added " .. newCount .. " new players.")
    end
    self:UpdateScrollFrame()
    self.isScanning = false
end

-- [[ FIXED DUPLICATE CHECK ]]
function GR:IsDuplicate(name)
    local target = Ambiguate(name, "none") -- Strip realm for comparison
    for _, p in ipairs(self.foundPlayers) do
        if Ambiguate(p.name, "none") == target then return true end
    end
    return false
end

function GR:AddPlayerToTable(info, zone, isRiskyZone)
    -- CLEANED FOR TBC: No RIO, No Raid Progress Logic
    table.insert(self.foundPlayers, {
        name = info.fullName,
        race = info.raceStr or "Unknown",
        fullClass = info.filename,
        classFile = info.filename,
        level = info.level,
        zone = zone,
        status = "Online",
        version = isRiskyZone and "Instance" or "World",
        selected = false
    })

    self:SortPlayers(self.sortKey, true)
end

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

    table.sort(self.foundPlayers, function(a, b)
        local valA, valB = a[key], b[key]

        if key == "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] then
        print("Debug Info: " .. name .. " is in blacklist. Skipping table add.")
        return
    end

    local p = {
        name = name,
        race = race,
        fullClass = className,
        classFile = classFile,
        level = level,
        zone = zone,
        status = "Online",
        version = "Retail",
        selected = true
    }

    if not self:IsDuplicate(p.name) then
        table.insert(self.foundPlayers, p)
        self:SortPlayers(self.sortKey, true)
        self:UpdateScrollFrame()
        self.StatusText:SetText("Added SELF (Debug).")
    else
        print("Debug: You are already in the list.")
    end
end

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

function GR: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 .. ")...")

    -- =========================================================================
    -- GuildInvite() is PROTECTED in TBC Anniversary.
    -- INVITE/BOTH modes are handled by each button's OnClick (hardware event).
    -- SendMessageSequence only handles MESSAGE mode whisper batching.
    -- =========================================================================

    -- MESSAGE ONLY: Whisper logic via C_Timer (not protected)
    local batchSize = 5; local batchCooldown = 5; local currentIndex = 1

    local function ProcessNext()
        if currentIndex > #queue then
            GR.StatusText:SetText("All messages sent.")
            return
        end
        local p = queue[currentIndex]

        local 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 .. "-" .. GetRealmName() end

            if not GR.db.blacklist[checkName] and not GR.db.blacklist[p.name]
                and not GR.db.permaBlacklist[checkName] and not GR.db.permaBlacklist[p.name] 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)
                    GR:AddToRecent(p.name, "Sent")
                end

                GR:AddToBlacklist(p.name)
                GR.recruitedPlayers[Ambiguate(p.name, "none")] = true
            end
            currentIndex = currentIndex + 1
            ProcessNext()
        end)
    end
    ProcessNext()
end

function GR:HandleChatEvent(event, msg, sender, ...)
    if not sender then return end
    -- Capture GUID for class color lookup
    local senderGUID = select(10, ...) -- Index 10 after msg/sender

    sender = Ambiguate(sender, "none")
    if not self:IsShown() then return end

    -- [[ REPLIES ONLY FILTERING - 1:1 RETAIL PARITY ]]
    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
                    isTracked = true; break
                end
            end

            -- Check Whitelist (Is this a reply to our batch?)
            if not isTracked then
                local isTarget = GR.recruitedPlayers[sender]
                if not isTarget then
                    local sName = strsplit("-", sender)
                    if GR.recruitedPlayers[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

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

    -- [[ ENSURE CHAT FRAMES EXIST ]]
    if not self.chatFrames or not self.chatFrames[1] then
        if self.UpdateWhisperLayout then
            self:UpdateWhisperLayout(500)
        end
        if not self.chatFrames or not self.chatFrames[1] then return end
    end

    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

    -- [[ NON-DESTRUCTIVE UPDATE - RETAIL PARITY ]]
    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
                frame.headerBtn:Show()
                frame.dataIndex = dataIndex

                -- [[ COLOR HEADER NAME ]]
                local cObj = C_ClassColor and 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
            end
        end
    end
end

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

function GR:BuildFilterUI()
    local f = CreateFrame("Frame", "GRS_FilterFrame", UIParent)
    f:SetSize(300, 250); 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

    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")

    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")

    local lblN = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); lblN:SetPoint("TOPLEFT", 20, -120); lblN
        :SetText("Name:")
    local editN = CreateFrame("EditBox", nil, f, "InputBoxTemplate"); editN:SetSize(160, 24); editN:SetPoint("LEFT", lblN,
        "RIGHT", 10, 0)
    editN:SetAutoFocus(false)
    editN:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editN:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)

    local bSave = CreateFrame("Button", nil, f, "UIPanelButtonTemplate"); bSave:SetSize(100, 24); bSave:SetPoint(
    "BOTTOMLEFT", 20, 20); bSave:SetText("Save")
    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)

    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 }
        GR:RefreshMainFilterList()

        if GR.filterType == "Custom" and GR.selectedFilterData and (GR.selectedFilterData.name == f.origName or GR.selectedFilterData.name == newName) then
            GR.selectedFilterData = GR.db.customFilters[newName]
            GR.dFiltTxt:SetText("Filter: " .. newName)
        end

        f:Hide()
        print("Filter Saved: " .. newName)
    end)

    f.OpenCreate = function()
        f.mode = "CREATE"; f.origName = nil
        f.Title:SetText("Create Custom Filter")
        f.selClass = "Any"; txtC:SetText("Any")
        f.selRace = "Any"; txtR:SetText("Any")
        editN:SetText("")
        f:Show()
    end

    f.OpenEdit = function(data)
        f.mode = "EDIT"; f.origName = data.name
        f.Title:SetText("Edit Filter: " .. data.name)
        f.selClass = data.class or "Any"; txtC:SetText(f.selClass)
        f.selRace = data.race or "Any"; txtR:SetText(f.selRace)
        editN:SetText(data.name)
        f:Show()
    end
end

function GR:RefreshMainFilterList()
    local list = self.dFiltList
    local kids = { list:GetChildren() }; for _, k in ipairs(kids) do k:Hide() end

    local y = -5
    local function Mk(text, func)
        local b = CreateFrame("Button", nil, list); b:SetSize(170, 20); b:SetPoint("TOPLEFT", 5, y)
        local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(text)
        b:SetScript("OnClick", func); y = y - 20
    end

    Mk("Default (All)", function()
        self.filterType = "Default"; self.dFiltTxt:SetText("Filter: Default (All)")
        self.selectedFilterData = nil; list:Hide()
    end)

    for name, data in pairs(self.db.customFilters) do
        Mk(name, function()
            self.filterType = "Custom"; self.dFiltTxt:SetText("Filter: " .. name)
            self.selectedFilterData = data; list:Hide()
        end)
    end
    list:SetHeight(math.abs(y) + 5)
end

function GR: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()
    -- TBC: Raider.IO is not available. Just show status text.
    if not self.ioStatus then return end

    local name = UnitName("player")
    self.ioStatus:SetText("Greetings " .. name)
    self.ioStatus:SetTextColor(1, 1, 1)
end

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

    local hasAccess = false
    local hasGuild = IsInGuild()

    if hasGuild then
        -- TBC: Use CanGuildInvite instead of C_GuildInfo.CanEditOfficerNote
        local canInvite = CanGuildInvite and CanGuildInvite() or false
        if canInvite then
            hasAccess = true
        end
    end

    if not hasGuild then
        self.officerStatus:SetText("No Guild detected.")
        self.officerStatus:SetTextColor(0.5, 0.5, 0.5)
    elseif hasAccess then
        self.officerStatus:SetText("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
