local addonName, NS = ...

-- =========================================================================
-- Blacklist Module (Split View: Normal & Permanent)
-- =========================================================================

NS.Blacklist = NS.Blacklist or {}
local M = NS.Blacklist

-- =========================================================================
-- Local Helpers (Surgically aligned with UI_Shared.lua)
-- =========================================================================

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

local function CreateBorder(f, r, g, b, a)
    if f.border and not f.border.SetBackdrop then
        f.border:Hide()
        f.border = nil
    end

    if not f.border then
        local border = CreateFrame("Frame", nil, f, "BackdropTemplate")
        border:SetAllPoints()
        border:SetBackdrop({
            bgFile = nil,
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        f.border = border
    end
    f.border:SetBackdropBorderColor(r, g, b, a)
end

local function SkinButton(btn)
    if not btn then return end
    btn:SetNormalTexture("")
    btn:SetHighlightTexture("")
    btn:SetPushedTexture("")
    btn:SetDisabledTexture("")
    if btn.Left then btn.Left:SetAlpha(0) end
    if btn.Middle then btn.Middle:SetAlpha(0) end
    if btn.Right then btn.Right:SetAlpha(0) end
    if btn.Top then btn.Top:SetAlpha(0) end
    if btn.Bottom then btn.Bottom:SetAlpha(0) end
    CreateBg(btn, 0.2, 0.2, 0.2, 1)
    CreateBorder(btn, 0, 0, 0, 1)
    btn:HookScript("OnEnter", function(s) if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end end)
    btn:HookScript("OnLeave", function(s) if s.bg then s.bg:SetColorTexture(0.2, 0.2, 0.2, 1) end end)
    btn:HookScript("OnMouseDown", function(s) if s.bg then s.bg:SetColorTexture(0.1, 0.1, 0.1, 1) end end)
    btn:HookScript("OnMouseUp", function(s) if s.bg then s.bg:SetColorTexture(0.3, 0.3, 0.3, 1) end end)
    local fs = btn:GetFontString()
    if fs then fs:SetTextColor(1, 1, 1) end
end

-- =========================================================================
-- Attach API
-- =========================================================================

function M.Attach(GR)
    if not GR or GR.__BlacklistAttached then return end
    GR.__BlacklistAttached = true

    function GR:BuildBlacklistView()
        local GR = self 
        local parent = CreateFrame("Frame", "GRS_BlacklistView", self.ViewContainer)
        parent:SetPoint("TOPLEFT", 0, -30)
        parent:SetPoint("BOTTOMRIGHT", 0, 0)
        parent:Hide()
        parent:SetClipsChildren(true)
        self.views[2] = parent

        local controls = CreateFrame("Frame", nil, parent)
        controls:SetWidth(220); controls:SetPoint("TOPLEFT"); controls:SetPoint("BOTTOMLEFT")
        CreateBg(controls, 0.1, 0.1, 0.1, 0.5); CreateBorder(controls, 0.2, 0.2, 0.2, 1)

        local normalFrame = CreateFrame("Frame", nil, parent)
        normalFrame:SetWidth(475); normalFrame:SetPoint("TOPLEFT", controls, "TOPRIGHT", 0, 0); normalFrame:SetPoint("BOTTOMLEFT", controls, "BOTTOMRIGHT", 0, 0)
        CreateBg(normalFrame, 0, 0, 0, 0.5); CreateBorder(normalFrame, 0, 0, 0, 0.5)

        local permaFrame = CreateFrame("Frame", nil, parent)
        permaFrame:SetWidth(475); permaFrame:SetPoint("TOPLEFT", normalFrame, "TOPRIGHT", 50, 0); permaFrame:SetPoint("BOTTOMLEFT", normalFrame, "BOTTOMRIGHT", 50, 0)
        CreateBg(permaFrame, 0, 0, 0, 0.5); CreateBorder(permaFrame, 0, 0, 0, 0.5)

        local y = -20
        local function AddLbl(txt, container, yVal, font)
            local t = container:CreateFontString(nil, "OVERLAY", font or "GameFontNormal")
            t:SetPoint("TOPLEFT", 15, yVal); t:SetText(txt); return t
        end
        AddLbl(NS.L["MANAGE_BLACKLIST"] or "MANAGE BLACKLIST", controls, y)

        local btnReset = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnReset:SetSize(190, 24); btnReset:SetPoint("TOPLEFT", 15, y - 25); btnReset:SetText(NS.L["RESET_ENTIRE_NORMAL"] or "Reset Entire Normal List")
        SkinButton(btnReset); btnReset:SetScript("OnClick", function() GR:ResetBlacklist() end)

        local btnDelSel = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnDelSel:SetSize(190, 24); btnDelSel:SetPoint("TOPLEFT", btnReset, "BOTTOMLEFT", 0, -5); btnDelSel:SetText(NS.L["RESET_SELECTION_BOTH"] or "Reset Selection (Both)")
        SkinButton(btnDelSel); btnDelSel:SetScript("OnClick", function() GR:RemoveSelectedFromBlacklist() end)

        local btnRefresh = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnRefresh:SetSize(190, 24); btnRefresh:SetPoint("TOPLEFT", btnDelSel, "BOTTOMLEFT", 0, -5); btnRefresh:SetText("Refresh / Reset Sync History")
        SkinButton(btnRefresh); btnRefresh:SetScript("OnClick", function()
             local c = 0
             for k, v in pairs(GR.db.blacklist) do v.isSynced = nil; c = c + 1 end
             for k, v in pairs(GR.db.permaBlacklist) do v.isSynced = nil; c = c + 1 end
             print("GRS: Sync History Reset. " .. c .. " entries marked for re-sync.")
             if GR.SanitizeDatabase then GR:SanitizeDatabase() end
        end)

        y = y - 115 
        AddLbl(NS.L["MANUAL_ADD"] or "MANUAL ADD", controls, y)
        local inputAdd = CreateFrame("EditBox", nil, controls, "InputBoxTemplate")
        inputAdd:SetSize(190, 24); inputAdd:SetPoint("TOPLEFT", 15, y - 25); inputAdd:SetAutoFocus(false)
        inputAdd:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
        local btnAdd = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnAdd:SetSize(190, 24); btnAdd:SetPoint("TOPLEFT", inputAdd, "BOTTOMLEFT", 0, -5); btnAdd:SetText(NS.L["ADD_TO_NORMAL"] or "Add to Normal")
        SkinButton(btnAdd); btnAdd:SetScript("OnClick", function()
            local n = inputAdd:GetText()
            if n and n ~= "" then GR:AddToBlacklist(n); inputAdd:SetText(""); print("Added " .. n .. " to Blacklist.") end
        end)

        y = y - 85
        AddLbl(NS.L["AUTOMATE_RESET"] or "AUTOMATE RESET", controls, y)
        local autoCheck = CreateFrame("CheckButton", nil, controls, "UICheckButtonTemplate")
        autoCheck:SetSize(24, 24); autoCheck:SetPoint("TOPLEFT", 15, y - 25); autoCheck:SetChecked(GR.db.automateReset)
        autoCheck.text = autoCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        autoCheck.text:SetPoint("LEFT", autoCheck, "RIGHT", 2, 0); autoCheck.text:SetText("Automate reset:")
        autoCheck:SetScript("OnClick", function(self) GR.db.automateReset = self:GetChecked(); if GR.db.automateReset then GR:PruneExpiredBlacklist() end end)

        local function CreateDurationDropdown(parent, width)
            local btn = CreateFrame("Button", nil, parent); CreateBg(btn, 0.2, 0.2, 0.2, 1); btn:SetSize(width, 24)
            local txt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); txt:SetPoint("LEFT", 5, 0)
            local function GetLabel(sec) if sec == 86400 then return "1 Day" elseif sec == 604800 then return "1 Week" elseif sec == 1209600 then return "2 Weeks" elseif sec == 2592000 then return "1 Month" elseif sec == 5184000 then return "2 Months" else return "1 Week" end end
            txt:SetText(GetLabel(GR.db.resetDuration or 604800))
            local list = CreateFrame("Frame", nil, parent); CreateBg(list, 0.1, 0.1, 0.1, 1); CreateBorder(list, 0.5, 0.5, 0.5, 1)
            list:SetSize(width, 110); list:SetPoint("TOPLEFT", btn, "BOTTOMLEFT", 0, -2); list:SetFrameStrata("FULLSCREEN_DIALOG"); list:Hide()
            local opts = {{l="1 Day", v=86400},{l="1 Week", v=604800},{l="2 Weeks", v=1209600},{l="1 Month", v=2592000},{l="2 Months", v=5184000}}
            local dy = -5
            for _, opt in ipairs(opts) do
                local b = CreateFrame("Button", nil, list); b:SetSize(width - 10, 20); b:SetPoint("TOPLEFT", 5, dy)
                local t = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); t:SetPoint("LEFT"); t:SetText(opt.l)
                b:SetScript("OnClick", function() GR.db.resetDuration = opt.v; txt:SetText(opt.l); list:Hide(); if GR.db.automateReset then GR:PruneExpiredBlacklist() end end)
                dy = dy - 20
            end
            btn:SetScript("OnClick", function() list:SetShown(not list:IsShown()) end)
            return btn
        end
        local ddReset = CreateDurationDropdown(controls, 190); ddReset:SetPoint("TOPLEFT", autoCheck, "BOTTOMLEFT", 0, -5)

        y = y - 100
        AddLbl(NS.L["GUILD_SYNC"] or "GUILD SYNC", controls, y)
        local btnSync = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnSync:SetSize(190, 30); btnSync:SetPoint("TOPLEFT", 15, y - 25); btnSync:SetText(NS.L["SYNC_WITH_GUILD"] or "Sync with Guild")
        SkinButton(btnSync); btnSync:SetScript("OnClick", function() GR:StartSyncProcess() end)
        
        local syncStats = CreateFrame("Frame", nil, controls)
        syncStats:SetSize(190, 80); syncStats:SetPoint("TOPLEFT", btnSync, "BOTTOMLEFT", 0, -5)
        local function CreateStatBlock(parent, yOffset, titleText)
            local title = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); title:SetPoint("TOPLEFT", 0, yOffset); title:SetText(titleText); title:SetTextColor(0.7, 0.7, 0.7)
            local h1 = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); h1:SetPoint("TOPLEFT", 0, yOffset - 12); h1:SetText("Status"); h1:SetWidth(60); h1:SetJustifyH("LEFT"); h1:SetTextColor(0.5, 0.5, 0.5)
            local h2 = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); h2:SetPoint("LEFT", h1, "RIGHT", 0, 0); h2:SetText("Total"); h2:SetWidth(40); h2:SetJustifyH("RIGHT"); h2:SetTextColor(0.5, 0.5, 0.5)
            local h3 = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); h3:SetPoint("LEFT", h2, "RIGHT", 0, 0); h3:SetText("New"); h3:SetWidth(40); h3:SetJustifyH("RIGHT"); h3:SetTextColor(0.5, 0.5, 0.5)
            local vStatus = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); vStatus:SetPoint("TOPLEFT", 0, yOffset - 24); vStatus:SetWidth(60); vStatus:SetJustifyH("LEFT"); vStatus:SetText("Idle")
            local vAmt = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); vAmt:SetPoint("LEFT", vStatus, "RIGHT", 0, 0); vAmt:SetWidth(40); vAmt:SetJustifyH("RIGHT"); vAmt:SetText("-")
            local vNew = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); vNew:SetPoint("LEFT", vAmt, "RIGHT", 0, 0); vNew:SetWidth(40); vNew:SetJustifyH("RIGHT"); vNew:SetText("-"); vNew:SetTextColor(0, 1, 0)
            return vStatus, vAmt, vNew
        end
        GR.lblSyncN_Stat, GR.lblSyncN_Amt, GR.lblSyncN_New = CreateStatBlock(syncStats, 0, "Temp. Blacklist")
        GR.lblSyncP_Stat, GR.lblSyncP_Amt, GR.lblSyncP_New = CreateStatBlock(syncStats, -45, "Perm. Blacklist")

        local btnExIm = CreateFrame("Button", nil, controls, "UIPanelButtonTemplate")
        btnExIm:SetSize(190, 30); btnExIm:SetPoint("TOPLEFT", syncStats, "BOTTOMLEFT", 0, -10); btnExIm:SetText("EXPORT / IMPORT")
        SkinButton(btnExIm); btnExIm:SetScript("OnClick", function() GR:ShowExportImportFrame() end)

        local titleNormal = normalFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        titleNormal:SetPoint("TOP", 0, -10); titleNormal:SetText(NS.L["TEMPORARILY_BLACKLISTED"] or "Temporarily Blacklisted"); titleNormal:SetTextColor(1, 0.82, 0) 
        local titlePerma = permaFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        titlePerma:SetPoint("TOP", 0, -10); titlePerma:SetText(NS.L["PERMANENT_BLACKLIST"] or "PERMANENT Blacklist"); titlePerma:SetTextColor(1, 0.2, 0.2) 

        local headers = {{text="Name", width=120},{text="Race", width=80},{text="Class", width=80},{text="Score", width=55},{text="Heroic", width=50},{text="Mythic", width=50}}
        local function BuildList(frame, prefix)
            local headF = CreateFrame("Frame", nil, frame); headF:SetSize(20, 20); headF:SetPoint("TOPLEFT", 0, -35); headF:SetPoint("TOPRIGHT", 0, -35)
            -- Select All Checkbox
            local selectAll = CreateFrame("CheckButton", nil, headF, "UICheckButtonTemplate")
            selectAll:SetSize(20, 20); selectAll:SetPoint("LEFT", 5, 0)
            local lh = nil
            for i, h in ipairs(headers) do
                local b = CreateFrame("Button", nil, headF); b:SetSize(h.width, 20)
                if i == 1 then b:SetPoint("LEFT", 25, 0) else b:SetPoint("LEFT", lh, "RIGHT", 0, 0) end
                local t = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); t:SetPoint("LEFT"); t:SetText(h.text); lh = b
            end
            local scroll = CreateFrame("ScrollFrame", prefix.."Scroll", frame, "FauxScrollFrameTemplate")
            scroll:SetPoint("TOPLEFT", headF, "BOTTOMLEFT", 0, -5); scroll:SetPoint("BOTTOMRIGHT", -25, 5); CreateBg(scroll, 0, 0, 0, 0.2) 
            if NS.StyleScrollBar then NS.StyleScrollBar(scroll) end
            local rows = {}
            for i = 1, 25 do
                local r = CreateFrame("Button", nil, frame); r:SetHeight(20); r:SetPoint("LEFT", 5, 0); r:SetPoint("RIGHT", -5, 0)
                if i == 1 then r:SetPoint("TOP", scroll, "TOP") else r:SetPoint("TOP", rows[i-1], "BOTTOM") end
                r.c = CreateFrame("CheckButton", nil, r, "UICheckButtonTemplate"); r.c:SetSize(20, 20); r.c:SetPoint("LEFT", 0, 0)
                r.c:SetScript("OnClick", function(b) if r.data then r.data.selected = b:GetChecked() end end)
                r.n = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.n:SetPoint("LEFT", 20, 0); r.n:SetWidth(120); r.n:SetJustifyH("LEFT")
                r.ra = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.ra:SetPoint("LEFT", r.n, "RIGHT"); r.ra:SetWidth(80); r.ra:SetJustifyH("LEFT")
                r.cl = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.cl:SetPoint("LEFT", r.ra, "RIGHT"); r.cl:SetWidth(80); r.cl:SetJustifyH("LEFT")
                r.s = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.s:SetPoint("LEFT", r.cl, "RIGHT"); r.s:SetWidth(55); r.s:SetJustifyH("LEFT")
                r.h = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.h:SetPoint("LEFT", r.s, "RIGHT"); r.h:SetWidth(50); r.h:SetJustifyH("LEFT")
                r.m = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall"); r.m:SetPoint("LEFT", r.h, "RIGHT"); r.m:SetWidth(50); r.m:SetJustifyH("LEFT")
                rows[i] = r
            end
            return scroll, rows, selectAll
        end
        self.blScroll, self.blRows, self.blSelectAll = BuildList(normalFrame, "GRS_NormalBL")
        self.pblScroll, self.pblRows, self.pblSelectAll = BuildList(permaFrame, "GRS_PermaBL")
        self.blSelectAll:SetScript("OnClick", function(cb)
            local state = cb:GetChecked()
            for k, v in pairs(GR.db.blacklist) do if type(v) == "table" then v.selected = state end end
            self:UpdateBlacklistScroll()
        end)
        self.pblSelectAll:SetScript("OnClick", function(cb)
            local state = cb:GetChecked()
            for k, v in pairs(GR.db.permaBlacklist) do if type(v) == "table" then v.selected = state end end
            self:UpdateBlacklistScroll()
        end)
        self.blScroll:SetScript("OnVerticalScroll", function(s, offset) FauxScrollFrame_OnVerticalScroll(s, offset, 20, function() self:UpdateBlacklistScroll() end) end)
        self.pblScroll:SetScript("OnVerticalScroll", function(s, offset) FauxScrollFrame_OnVerticalScroll(s, offset, 20, function() self:UpdateBlacklistScroll() end) end)
        parent:SetScript("OnShow", function() if GR.PruneExpiredBlacklist then GR:PruneExpiredBlacklist() end; self:UpdateBlacklistScroll() end)
    end

    function GR:GetBlacklistAsArray(dbTable)
        local arr = {}
        for k, v in pairs(dbTable or {}) do if type(v) == "table" then table.insert(arr, v) else table.insert(arr, {name = k, rio = 0, heroic = "-", mythic = "-", fullClass = "Unknown", race = "Unknown"}) end end
        table.sort(arr, function(a, b) return a.name < b.name end)
        return arr
    end

    function GR:UpdateBlacklistScroll()
        local list = self:GetBlacklistAsArray(self.db.blacklist)
        FauxScrollFrame_Update(self.blScroll, #list, #self.blRows, 20)
        local offset = FauxScrollFrame_GetOffset(self.blScroll)
        for i, r in ipairs(self.blRows) do
            local idx = offset + i
            if idx <= #list then
                local d = list[idx]; r.data = d; r:Show()
                r.n:SetText(d.name); r.ra:SetText(d.race or "-"); r.cl:SetText(d.fullClass or "-")
                r.s:SetText(d.rio or 0); r.h:SetText(d.heroic or "-"); r.m:SetText(d.mythic or "-")
                r.c:SetChecked(d.selected or false)
            else r:Hide() end
        end
        local pList = self:GetBlacklistAsArray(self.db.permaBlacklist)
        FauxScrollFrame_Update(self.pblScroll, #pList, #self.pblRows, 20)
        local pOffset = FauxScrollFrame_GetOffset(self.pblScroll)
        for i, r in ipairs(self.pblRows) do
            local idx = pOffset + i
            if idx <= #pList then
                local d = pList[idx]; r.data = d; r:Show()
                r.n:SetText(d.name); r.ra:SetText(d.race or "-"); r.cl:SetText(d.fullClass or "-")
                r.s:SetText(d.rio or 0); r.h:SetText(d.heroic or "-"); r.m:SetText(d.mythic or "-")
                r.c:SetChecked(d.selected or false)
            else r:Hide() end
        end
    end

    function GR:PruneExpiredBlacklist()
        if not self.db.automateReset then return end
        local limit, now, pruned = self.db.resetDuration or 604800, time(), 0
        for k, v in pairs(self.db.blacklist) do if v.addedAt and (now - v.addedAt > limit) then self.db.blacklist[k] = nil; pruned = pruned + 1 end end
        if pruned > 0 then print("GRS: Auto-Pruned " .. pruned .. " expired blacklist entries."); if self:IsViewVisible(2) then self:UpdateBlacklistScroll() end end
    end

    function GR:NormalizeName(name) if not string.find(name, "-") then return name .. "-" .. GetNormalizedRealmName() end; return name end
    function GR:AddToBlacklist(name, isAuto) if not name then return end; name = self:NormalizeName(name); if self.db.permaBlacklist[name] then print("GRS: " .. name .. " is already PERMANENTLY blacklisted."); return end; local data = self:FindPlayerData(name) or self:CreateBlankData(name); data.addedAt = time(); data.isAutoInvite = isAuto; self.db.blacklist[name] = data; self:CleanUpScanResults(name); if self.AddToRecent then self:AddToRecent(name, "Blacklisted") end; if self:IsViewVisible(2) then self:UpdateBlacklistScroll() end end
    function GR:AddToPermaBlacklist(name) if not name then return end; name = self:NormalizeName(name); local data = self.db.blacklist[name] or self:FindPlayerData(name) or self:CreateBlankData(name); self.db.permaBlacklist[name] = data; self.db.blacklist[name] = nil; self:CleanUpScanResults(name); print("GRS: " .. name .. " added to PERMANENT Blacklist."); if self.AddToRecent then self:AddToRecent(name, "PERMA-BANNED") end; if self:IsViewVisible(2) then self:UpdateBlacklistScroll() end end
    function GR:FindPlayerData(name) for _, p in ipairs(self.foundPlayers or {}) do local pName = p.name; if not string.find(pName, "-") then pName = pName .. "-" .. GetNormalizedRealmName() end; if pName == name then return p end end; return nil end
    function GR:CreateBlankData(name) return {name = name, race = "Unknown", fullClass = "Unknown", rio = 0, heroic = "-", mythic = "-", level = 0, zone = "Unknown", addedAt = time()} end
    function GR:CleanUpScanResults(name) if self.RemovePlayer then self:RemovePlayer(name) end; local shortName = string.match(name, "^(.-)-"); if shortName and self.RemovePlayer then self:RemovePlayer(shortName) end end
    function GR:IsViewVisible(id) return self.views and self.views[id] and self.views[id].IsShown and self.views[id]:IsShown() end
    function GR:RemoveSelectedFromBlacklist()
        for k, v in pairs(self.db.blacklist) do if type(v) == "table" and v.selected then self.db.blacklist[k] = nil end end
        for k, v in pairs(self.db.permaBlacklist) do if type(v) == "table" and v.selected then self.db.permaBlacklist[k] = nil end end
        self:UpdateBlacklistScroll()
    end
    function GR:ScanGuildForBlacklistMatches()
        if not IsInGuild() then return end
        -- Force the native "Show Offline" flag only on a cooldown, so we don't thrash the
        -- player's native guild-frame checkbox on every GUILD_ROSTER_UPDATE. Offline members
        -- only need an occasional pass to catch blacklisted players who joined the guild.
        if GetGuildRosterShowOffline and not GetGuildRosterShowOffline() then
            local now = GetTime()
            if not GR._blOfflineScanAt or (now - GR._blOfflineScanAt) > 300 then
                GR._blOfflineScanAt = now
                SetGuildRosterShowOffline(true)
                if C_GuildInfo and C_GuildInfo.GuildRoster then C_GuildInfo.GuildRoster() elseif GuildRoster then GuildRoster() end
                -- the resulting GUILD_ROSTER_UPDATE re-enters this scan with offline rows loaded
                GR._blRestoreOffline = true
                return
            end
            -- within cooldown: scan only the currently-loaded (mostly online) members this pass
        end
        local totalMembers, moves = GetNumGuildMembers(), 0
        for i = 1, totalMembers do local fullName = GetGuildRosterInfo(i); if fullName then fullName = self:NormalizeName(fullName); if self.db.blacklist[fullName] then self:AddToPermaBlacklist(fullName); moves = moves + 1 end end end
        if moves > 0 then print("GRS: Moved " .. moves .. " guild members from Normal to Permanent blacklist.") end
        if GR._blRestoreOffline then
            GR._blRestoreOffline = nil
            C_Timer.After(1, function()
                local _lf = _G["GRS_RosterListFrame"]
                if not (_lf and _lf:IsVisible()) and SetGuildRosterShowOffline then SetGuildRosterShowOffline(false) end
            end)
        end
    end
    function GR:ResetBlacklist() self.db.blacklist = {}; if self:IsViewVisible(2) then self:UpdateBlacklistScroll() end; print("GRS: Normal Blacklist wiped.") end

    function GR:SanitizeDatabase()
        local removedLog, count = {}, 0
        local function CleanTable(dbTable, listName)
            local groups = {}
            for key, data in pairs(dbTable) do local baseName = key:lower(); if not groups[baseName] then groups[baseName] = {} end; table.insert(groups[baseName], { key = key, data = data }) end
            for baseName, entries in pairs(groups) do
                if #entries > 1 then
                    table.sort(entries, function(a, b) local raceA = a.data.race and a.data.race ~= "-" and a.data.race ~= "Unknown"; local raceB = b.data.race and b.data.race ~= "-" and b.data.race ~= "Unknown"; if raceA and not raceB then return true elseif not raceA and raceB then return false end; return (a.data.addedAt or 0) > (b.data.addedAt or 0) end)
                    for i = 2, #entries do local loser = entries[i]; dbTable[loser.key] = nil; table.insert(removedLog, string.format("[%s] DESTROYED: %s", listName, loser.key)); count = count + 1 end
                end
            end
        end
        CleanTable(self.db.blacklist, "Temp"); CleanTable(self.db.permaBlacklist, "Perma")
        if count > 0 then if self:IsViewVisible(2) then self:UpdateBlacklistScroll() end end
    end
    C_Timer.After(1.0, function() GR:SanitizeDatabase() end)

    -- [[ SYNC PROTOCOL v15.0 ]]
    function GR:StartSyncProcess()
        if not IsInGuild() and not GR.devOfficerMode then print("GRS: Guild required for sync."); return end
        if self.SyncTicker and not self.SyncTicker:IsCancelled() then print("|cffff0000GRS:|r Sync already in progress."); return end
        self:SanitizeDatabase()
        print("|cff00ffffGRS Sync:|r Initializing Protocol v15...")
        C_ChatInfo.SendAddonMessage("GRS_SYNC", "PING", "GUILD")
        if self.lblSyncN_Stat then self.lblSyncN_Stat:SetText("Init..."); self.lblSyncN_Amt:SetText("0"); self.lblSyncN_New:SetText("-") end
        if self.lblSyncP_Stat then self.lblSyncP_Stat:SetText("Init..."); self.lblSyncP_Amt:SetText("0"); self.lblSyncP_New:SetText("-") end
        C_Timer.After(1.0, function() self:BeginDataUpload() end)
    end

    function GR:BeginDataUpload()
        local nQ, pQ = {}, {}
        for k, v in pairs(self.db.blacklist) do if not v.isSynced then table.insert(nQ, v) end end
        for k, v in pairs(self.db.permaBlacklist) do if not v.isSynced then table.insert(pQ, v) end end

        local nTotal = #nQ
        local pTotal = #pQ
        local combinedTotal = nTotal + pTotal

        if combinedTotal == 0 then
            print("|cff00ff00GRS Sync:|r Already synced.")
            if self.lblSyncN_Stat then self.lblSyncN_Stat:SetText("Synced") end
            if self.lblSyncP_Stat then self.lblSyncP_Stat:SetText("Synced") end
            return
        end

        print(string.format("|cff00ffffGRS Sync:|r Starting Upload. New Temp: %d | New Perma: %d", nTotal, pTotal))
        
        -- MANIFEST PHASE (Restored)
        C_ChatInfo.SendAddonMessage("GRS_SYNC", "HEAD:"..nTotal..":"..pTotal, "GUILD")

        self.masterQueue = {}
        for _, d in ipairs(nQ) do table.insert(self.masterQueue, {type="DATN", data=d}) end
        for _, d in ipairs(pQ) do table.insert(self.masterQueue, {type="DATP", data=d}) end

        self.currentIndex = 1
        self.maxIndex = #self.masterQueue
        self.batchSize = 50
        self.waitingForAck = false
        
        self:ProcessSyncBatch()
    end

    function GR:ProcessSyncBatch()
        if self.waitingForAck then return end
        local batchCount = 0

        -- Due to blizzard throttling message, increased to 1.1. Was 1.0
        self.SyncTicker = C_Timer.NewTicker(1.1, function()
            if self.currentIndex > self.maxIndex then
                self.SyncTicker:Cancel()
                C_ChatInfo.SendAddonMessage("GRS_SYNC", "DONE", "GUILD")
                if GR.lblSyncN_Stat then GR.lblSyncN_Stat:SetText("Done") end
                if GR.lblSyncP_Stat then GR.lblSyncP_Stat:SetText("Done") end
                return
            end

            if batchCount >= self.batchSize then
                self.SyncTicker:Cancel()
                GR.waitingForAck = true
                local exp = self.currentIndex - 1
                C_ChatInfo.SendAddonMessage("GRS_SYNC", "STEP_WAIT:"..exp, "GUILD")
                GR.PingTicker = C_Timer.NewTicker(3.0, function()
                    if GR.waitingForAck then
                        C_ChatInfo.SendAddonMessage("GRS_SYNC", "STEP_WAIT:"..exp, "GUILD")
                    else
                        GR.PingTicker:Cancel()
                    end
                end)
                return
            end

            local item = self.masterQueue[self.currentIndex]
            local d = item.data
            local safeN = d.name or "Unknown"
            
            -- Payload format: TYPE:NAME:RIO:HEROIC:MYTHIC:CLASS:RACE:SYNCEDBY
            local payload = string.format("%s:%s:%s:%s:%s:%s:%s:%s", 
                item.type, 
                safeN, 
                d.rio or 0, 
                d.heroic or "-", 
                d.mythic or "-", 
                d.fullClass or "Unknown", 
                d.race or "Unknown", 
                UnitName("player")
            )
            
            C_ChatInfo.SendAddonMessage("GRS_SYNC", payload, "GUILD")
            d.isSynced = true 
            
            if item.type == "DATN" and GR.lblSyncN_Stat then
                GR.lblSyncN_Stat:SetText("Sending")
                GR.lblSyncN_Amt:SetText(self.currentIndex)
            elseif item.type == "DATP" and GR.lblSyncP_Stat then
                GR.lblSyncP_Stat:SetText("Sending")
                GR.lblSyncP_Amt:SetText(self.currentIndex)
            end
            
            self.currentIndex = self.currentIndex + 1
            batchCount = batchCount + 1
        end)
    end

    function GR:HandleSyncComm(msg, sender)
        if sender == UnitName("player") then return end; local cmd, rest = strsplit(":", msg, 2)
        if cmd == "PING" then if (C_GuildInfo.CanEditOfficerNote()) or GR.devOfficerMode then C_ChatInfo.SendAddonMessage("GRS_SYNC", "PONG", "GUILD") end
        elseif cmd == "PONG" then print("|cff00ff00GRS:|r Officer Online: " .. sender)
        elseif cmd == "HEAD" then local nT, pT = strsplit(":", rest); self.syncSession = {sender = sender, totalN = tonumber(nT) or 0, totalP = tonumber(pT) or 0, gotN = 0, newN = 0, gotP = 0, newP = 0}
            if self.lblSyncN_Stat then self.lblSyncN_Stat:SetText("Receiving"); self.lblSyncN_Amt:SetText("0"); self.lblSyncN_New:SetText("0") end; if self.lblSyncP_Stat then self.lblSyncP_Stat:SetText("Receiving"); self.lblSyncP_Amt:SetText("0"); self.lblSyncP_New:SetText("0") end
        elseif cmd == "DATN" or cmd == "DATP" then if not self.syncSession or self.syncSession.sender ~= sender then return end; local n, r, h, m, c, rc, off = strsplit(":", rest); if not string.find(n, "-") then n = n .. "-" .. GetNormalizedRealmName() end; if off == UnitName("player") then return end; local data = {name = n, rio = tonumber(r) or 0, heroic = h, mythic = m, fullClass = c, race = rc or "Unknown", addedAt = time(), isSynced = true, syncedBy = off}
            local wasNew = false; if cmd == "DATN" then if not self.db.blacklist[n] then self.db.blacklist[n] = data; wasNew = true else self.db.blacklist[n] = data end; self.syncSession.gotN = self.syncSession.gotN + 1; if wasNew then self.syncSession.newN = self.syncSession.newN + 1 end; if self.lblSyncN_Stat then self.lblSyncN_Amt:SetText(self.syncSession.gotN); self.lblSyncN_New:SetText("+"..self.syncSession.newN) end
            else if not self.db.permaBlacklist[n] then self.db.permaBlacklist[n] = data; wasNew = true else self.db.permaBlacklist[n] = data end; self.syncSession.gotP = self.syncSession.gotP + 1; if wasNew then self.syncSession.newP = self.syncSession.newP + 1 end; if self.lblSyncP_Stat then self.lblSyncP_Amt:SetText(self.syncSession.gotP); self.lblSyncP_New:SetText("+"..self.syncSession.newP) end end
        elseif cmd == "STEP_WAIT" then if self.syncSession then local exp = tonumber(rest) or 0; if (self.syncSession.gotN + self.syncSession.gotP) >= exp then C_ChatInfo.SendAddonMessage("GRS_SYNC", "STEP_ACK", "GUILD") else C_ChatInfo.SendAddonMessage("GRS_SYNC", "STEP_REQ:"..(self.syncSession.gotN + self.syncSession.gotP), "GUILD") end end
        elseif cmd == "STEP_ACK" then if self.waitingForAck then if self.PingTicker then self.PingTicker:Cancel() end; self.waitingForAck = false; C_Timer.After(5.0, function() self:ProcessSyncBatch() end) end
        elseif cmd == "STEP_REQ" then if self.waitingForAck then if self.PingTicker then self.PingTicker:Cancel() end; self.waitingForAck = false; self.currentIndex = (tonumber(rest) or 0) + 1; self:ProcessSyncBatch() end
        elseif cmd == "DONE" then if self.syncSession then self:SanitizeDatabase(); self:UpdateBlacklistScroll(); self.syncSession = nil; C_Timer.After(5.0, function() if GR.lblSyncN_Stat then GR.lblSyncN_Stat:SetText("Idle") end; if GR.lblSyncP_Stat then GR.lblSyncP_Stat:SetText("Idle") end end) end end
    end

    -- [[ EXPORT / IMPORT LOGIC v16.2 ]]
    function GR:ShowExportImportFrame()
        if not self.ExImFrame then
            local f = CreateFrame("Frame", "GRS_ExImFrame", UIParent)
            f:SetSize(500, 450); f:SetPoint("CENTER"); f:SetFrameStrata("FULLSCREEN_DIALOG")
            CreateBg(f, 0.1, 0.1, 0.1, 0.95); CreateBorder(f, 0.5, 0.5, 0.5, 1)

            local btnEx = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
            btnEx:SetSize(235, 30); btnEx:SetPoint("TOPLEFT", 10, -10); btnEx:SetText("EXPORT"); SkinButton(btnEx)

            local btnIm = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
            btnIm:SetSize(235, 30); btnIm:SetPoint("TOPRIGHT", -10, -10); btnIm:SetText("IMPORT"); SkinButton(btnIm)

            local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
            scroll:SetPoint("TOPLEFT", 10, -50); scroll:SetPoint("BOTTOMRIGHT", -30, 50)
            if NS.StyleScrollBar then NS.StyleScrollBar(scroll) end
            
            -- [[ ACCESSIBILITY TRIGGER: Click anywhere to focus ]]
            local clickArea = CreateFrame("Button", nil, scroll)
            clickArea:SetAllPoints(scroll)
            clickArea:SetScript("OnClick", function() if f.box then f.box:SetFocus() end end)

            local box = CreateFrame("EditBox", nil, scroll)
            box:SetMultiLine(true); box:SetMaxLetters(99999); box:SetWidth(440); box:SetFontObject("GameFontHighlightSmall")
            box:SetAutoFocus(false); box:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
            scroll:SetScrollChild(box); f.box = box

            local btnClose = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
            btnClose:SetSize(480, 30); btnClose:SetPoint("BOTTOM", 0, 10); btnClose:SetText("Close")
            SkinButton(btnClose); btnClose:SetScript("OnClick", function() f:Hide() end)

            btnEx:SetScript("OnClick", function()
                local data = ""
                local function Append(db, t) for k, v in pairs(db) do data = data .. string.format("%s:%s:%s:%s:%s:%s:%s:%s\n", t, k, v.rio or 0, v.heroic or "-", v.mythic or "-", v.fullClass or "Unknown", v.race or "Unknown", v.syncedBy or UnitName("player")) end end
                Append(self.db.blacklist, "DATN"); Append(self.db.permaBlacklist, "DATP")
                f.box:SetText(data); f.box:HighlightText(); f.box:SetFocus()
            end)

            btnIm:SetScript("OnClick", function()
                local raw = f.box:GetText(); if not raw or raw == "" then return end
                local lines = {strsplit("\n", raw)}
                local currentRealm = GetNormalizedRealmName()
                local player = UnitName("player")
                
                -- Build set of known realms (current + connected)
                local knownRealms = {}
                knownRealms[currentRealm] = true
                if GetAutoCompleteRealms then
                    local connected = GetAutoCompleteRealms()
                    if connected then for _, r in ipairs(connected) do knownRealms[r] = true end end
                end
                
                local imported, localCount, crossCount, skipped = 0, 0, 0, 0
                for _, line in ipairs(lines) do
                    line = string.gsub(line, "\r", "")
                    if line ~= "" then
                        local t, n, r, h, m, c, rc, off = strsplit(":", line)
                        if not n or n == "" then skipped = skipped + 1
                        else
                            local entryRealm = string.match(n, "-(.*)")
                            local normalizedEntry = entryRealm and entryRealm:gsub("[%s%-%.]+", "") or currentRealm
                            local isLocal = knownRealms[normalizedEntry]
                            
                            local data = {name = n, rio = tonumber(r) or 0, heroic = h, mythic = m, fullClass = c, race = rc or "Unknown", addedAt = time(), isSynced = true, syncedBy = off}
                            if t == "DATN" then self.db.blacklist[n] = data else self.db.permaBlacklist[n] = data end
                            imported = imported + 1
                            if isLocal then localCount = localCount + 1 else crossCount = crossCount + 1 end
                        end
                    end
                end
                
                self:UpdateBlacklistScroll(); f:Hide()
                local msg = string.format("|cff00ccff[A.X.I.O.M.]|r Imported %d entries (%d local, %d cross-realm)", imported, localCount, crossCount)
                if skipped > 0 then msg = msg .. string.format(". %d skipped.", skipped) end
                print(msg)
            end)
            self.ExImFrame = f
        end
        self.ExImFrame:Show(); self.ExImFrame.box:SetText(""); self.ExImFrame.box:SetFocus()
    end
    StaticPopupDialogs["GRS_REALM_MISMATCH"] = { text = "This import contains data from a different realm and cannot be processed.", button1 = "OK", timeout = 0, whileDead = true, hideOnEscape = true }

    -- =========================================================================
    -- REALM NAME MIGRATION CHECK
    -- =========================================================================
    function GR:CheckRealmMigration()
        -- Scan both blacklists for entries with spaced realm names
        local faultyEntries = {}

        local function ScanTable(tbl, label)
            for key, val in pairs(tbl) do
                local name, realm = strsplit("-", key)
                if realm and realm:find(" ") then
                    local newRealm = realm:gsub("[%s%-%.]+", "")
                    local newKey = name .. "-" .. newRealm
                    table.insert(faultyEntries, {
                        oldKey = key,
                        newKey = newKey,
                        source = label,
                        data = val,
                    })
                end
            end
        end

        ScanTable(self.db.blacklist, "Temp")
        ScanTable(self.db.permaBlacklist, "Perma")

        if #faultyEntries == 0 then return end

        -- Build Migration Popup
        if self.RealmMigrationFrame then self.RealmMigrationFrame:Show(); return end

        local f = CreateFrame("Frame", "GRS_RealmMigrationFrame", UIParent)
        f:SetSize(500, 400)
        f:SetPoint("CENTER")
        f:SetFrameStrata("FULLSCREEN_DIALOG")
        f:EnableMouse(true)
        f:SetMovable(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)
        CreateBg(f, 0.1, 0.1, 0.1, 0.95)
        CreateBorder(f, 0.5, 0.5, 0.5, 1)

        -- Title
        local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOPLEFT", 15, -15)
        title:SetText("|cffFFD100Blacklist Realm Name Update|r")

        -- Description
        local desc = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        desc:SetPoint("TOPLEFT", 15, -40)
        desc:SetPoint("TOPRIGHT", -15, -40)
        desc:SetJustifyH("LEFT")
        desc:SetWordWrap(true)
        desc:SetText("Current blacklist contains faulty realm-names. Do you wish to convert these to the correct format?\n\nPlayers affected:")

        -- Scrollable List
        local scroll = CreateFrame("ScrollFrame", "GRS_MigrationScroll", f, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 15, -105)
        scroll:SetPoint("BOTTOMRIGHT", -30, 55)
        if NS.StyleScrollBar then NS.StyleScrollBar(scroll) end

        local content = CreateFrame("Frame", nil, scroll)
        content:SetSize(440, #faultyEntries * 40)
        scroll:SetScrollChild(content)

        local y = 0
        for i, entry in ipairs(faultyEntries) do
            local row = CreateFrame("Frame", nil, content)
            row:SetSize(440, 35)
            row:SetPoint("TOPLEFT", 0, y)

            -- Source label (Temp / Perma)
            local srcLabel = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
            srcLabel:SetPoint("TOPLEFT", 0, 0)
            if entry.source == "Perma" then
                srcLabel:SetText("|cffFF3333[PERMANENT]|r")
            else
                srcLabel:SetText("|cffFFFF00[TEMP]|r")
            end

            -- Old key
            local oldText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            oldText:SetPoint("TOPLEFT", srcLabel, "TOPRIGHT", 5, 0)
            oldText:SetText("|cffFF6666" .. entry.oldKey .. "|r")

            -- Arrow
            local arrow = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            arrow:SetPoint("TOPLEFT", 0, -14)
            arrow:SetText("  ->")

            -- New key
            local newText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            newText:SetPoint("TOPLEFT", arrow, "TOPRIGHT", 5, 0)
            newText:SetText("|cff00FF00" .. entry.newKey .. "|r")

            y = y - 40
        end

        -- Convert Button
        local btnConvert = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        btnConvert:SetSize(150, 30)
        btnConvert:SetPoint("BOTTOMLEFT", 40, 15)
        btnConvert:SetText("Convert")
        SkinButton(btnConvert)

        btnConvert:SetScript("OnClick", function()
            local converted = 0
            for _, entry in ipairs(faultyEntries) do
                if entry.source == "Temp" then
                    if not GR.db.blacklist[entry.newKey] then
                        GR.db.blacklist[entry.newKey] = entry.data
                        if type(entry.data) == "table" then
                            entry.data.name = entry.newKey
                        end
                    end
                    GR.db.blacklist[entry.oldKey] = nil
                elseif entry.source == "Perma" then
                    if not GR.db.permaBlacklist[entry.newKey] then
                        GR.db.permaBlacklist[entry.newKey] = entry.data
                        if type(entry.data) == "table" then
                            entry.data.name = entry.newKey
                        end
                    end
                    GR.db.permaBlacklist[entry.oldKey] = nil
                end
                converted = converted + 1
            end
            print("|cff00ff00GRS:|r Converted " .. converted .. " blacklist entries to normalized realm names.")
            if GR.UpdateBlacklistScroll then GR:UpdateBlacklistScroll() end
            f:Hide()
        end)

        -- Cancel Button
        local btnCancel = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        btnCancel:SetSize(150, 30)
        btnCancel:SetPoint("BOTTOMRIGHT", -40, 15)
        btnCancel:SetText("Cancel")
        SkinButton(btnCancel)
        btnCancel:SetScript("OnClick", function() f:Hide() end)

        -- Count label
        local countLabel = f:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
        countLabel:SetPoint("BOTTOM", 0, 50)
        countLabel:SetText(#faultyEntries .. " entries found")

        self.RealmMigrationFrame = f
    end
end