-------------------------------------------------------------------------------
-- HelwenLFG
-- Premade Group Finder filter panel inspired by ElvUI WindTools LFG,
-- redesigned for EllesmereUI.
-- Mythic+/Raid filters, presets, key level, match alert, leader blacklist,
-- List Key, Quick Access, and settings.
-------------------------------------------------------------------------------
local ADDON = ...

local issecretvalue = issecretvalue or function() return false end

local function IsRestricted()
    return InCombatLockdown() and true or false
end

local BG     = { 0.06, 0.06, 0.06 }
local BG_A   = 0.85
local BORDER = { 0, 0, 0, 1 }
local ACCENT = { 0.09, 0.52, 0.82 }
local PANEL_W = 248

local PANEL_EXTRA_BOTTOM = 48
local DUNGEONS_CATEGORY = GROUP_FINDER_CATEGORY_ID_DUNGEONS or 2

local LUST_CLASSES = { MAGE = true, SHAMAN = true, HUNTER = true, EVOKER = true }
local function PlayerHasLust()
    local _, cls = UnitClass("player")
    return cls and LUST_CLASSES[cls] or false
end

local function FontPath()
    return (EllesmereUI and EllesmereUI.GetFontPath and EllesmereUI.GetFontPath("blizzardSkin"))
        or STANDARD_TEXT_FONT
end

local DB
local function InitDB()

    if not HelwenLFGDB and BoringLFGFilterDB then
        HelwenLFGDB = BoringLFGFilterDB
        BoringLFGFilterDB = nil
    end
    HelwenLFGDB = HelwenLFGDB or {}
    DB = HelwenLFGDB
    DB.filters = DB.filters or {}
    local f = DB.filters
    f.dungeons     = f.dungeons or {}
    f.leaderScore  = f.leaderScore  or 0
    f.dungeonScore = f.dungeonScore or 0
    f.keyLevelMin  = f.keyLevelMin  or 0
    f.keyLevelMax  = f.keyLevelMax  or 0
    f.hasTank      = f.hasTank      or false
    f.hasHealer    = f.hasHealer    or false
    f.roleAvailable= f.roleAvailable or false
    f.bloodlust    = f.bloodlust    or false
    f.noMyClass    = f.noMyClass    or false
    f.raids        = f.raids        or {}
    f.raidDiffs    = f.raidDiffs    or {}
    DB.presets = DB.presets or {}
    DB.blacklist = DB.blacklist or {}

    DB.features = DB.features or {}
    local ft = DB.features
    if ft.leaderIO   == nil then ft.leaderIO   = true end
    if ft.specIcons  == nil then ft.specIcons  = true end
    if ft.matchAlert == nil then ft.matchAlert = false end
    ft.region = nil
    return DB
end

local function FeatureGet(key)
    local ft = (DB or HelwenLFGDB or {}).features
    return not (ft and ft[key] == false)
end

local function Filters() return (DB and DB.filters) or InitDB().filters end

local function AnyRaidFilterActive()
    local f = Filters()
    return next(f.raids) ~= nil or next(f.raidDiffs) ~= nil
end

local function AnyFilterActive()
    local f = Filters()
    if next(f.dungeons) then return true end
    if (f.leaderScore or 0) > 0 then return true end
    if (f.dungeonScore or 0) > 0 then return true end
    if (f.keyLevelMin or 0) > 0 or (f.keyLevelMax or 0) > 0 then return true end
    return f.hasTank or f.hasHealer or f.roleAvailable or f.bloodlust or f.noMyClass
end

local function ResetFilters()
    local f = Filters()
    wipe(f.dungeons)
    f.leaderScore, f.dungeonScore = 0, 0
    f.keyLevelMin, f.keyLevelMax = 0, 0
    f.hasTank, f.hasHealer, f.roleAvailable, f.bloodlust, f.noMyClass = false, false, false, false, false
    wipe(f.raids); wipe(f.raidDiffs)
end

local function CopyDungeons(src)
    local t = {}
    if src then for k, v in pairs(src) do if v then t[k] = true end end end
    return t
end

local function SnapshotFilters()
    local f = Filters()
    return {
        dungeons      = CopyDungeons(f.dungeons),
        leaderScore   = f.leaderScore or 0,
        dungeonScore  = f.dungeonScore or 0,
        keyLevelMin   = f.keyLevelMin or 0,
        keyLevelMax   = f.keyLevelMax or 0,
        hasTank       = f.hasTank and true or false,
        hasHealer     = f.hasHealer and true or false,
        roleAvailable = f.roleAvailable and true or false,
        bloodlust     = f.bloodlust and true or false,
        noMyClass     = f.noMyClass and true or false,
    }
end

local function ApplySnapshot(s)
    if type(s) ~= "table" then return end
    local f = Filters()
    wipe(f.dungeons)
    for k, v in pairs(CopyDungeons(s.dungeons)) do f.dungeons[k] = v end
    f.leaderScore   = s.leaderScore or 0
    f.dungeonScore  = s.dungeonScore or 0
    f.keyLevelMin   = s.keyLevelMin or 0
    f.keyLevelMax   = s.keyLevelMax or 0
    f.hasTank       = s.hasTank and true or false
    f.hasHealer     = s.hasHealer and true or false
    f.roleAvailable = s.roleAvailable and true or false
    f.bloodlust     = s.bloodlust and true or false
    f.noMyClass     = s.noMyClass and true or false
end

local function SavePreset(slot)
    InitDB()
    DB.presets[slot] = SnapshotFilters()
    print(("|cff1784d1HelwenLFG|r preset %d saved."):format(slot))
end

local function LoadPreset(slot)
    InitDB()
    local s = DB.presets[slot]
    if not s then
        print(("|cff1784d1HelwenLFG|r preset %d is empty (right-click to save)."):format(slot))
        return false
    end
    ApplySnapshot(s)
    print(("|cff1784d1HelwenLFG|r preset %d loaded."):format(slot))
    return true
end

local function BlacklistKey(name)
    if not name or issecretvalue(name) then return nil end
    return name:lower()
end

local function IsLeaderBlacklisted(name)
    local key = BlacklistKey(name)
    if not key then return false end
    local bl = (DB or HelwenLFGDB or {}).blacklist
    return bl and bl[key] and true or false
end

local function BlacklistAdd(name)
    InitDB()
    local key = BlacklistKey(name)
    if not key then return false end
    DB.blacklist[key] = true
    return true
end

local function BlacklistClear()
    InitDB()
    wipe(DB.blacklist)
end

local function BlacklistCount()
    local bl = (DB or HelwenLFGDB or {}).blacklist
    if not bl then return 0 end
    local n = 0
    for _ in pairs(bl) do n = n + 1 end
    return n
end

local BACKDROP_INFO = {
    edgeFile = "Interface\\Buttons\\WHITE8X8",
    edgeSize = 1,
    insets = { left = 0, right = 0, top = 0, bottom = 0 },
}
local function Backdrop(f, alpha)
    local bg = f:CreateTexture(nil, "BACKGROUND", nil, -7)
    bg:SetColorTexture(BG[1], BG[2], BG[3], alpha or BG_A)
    bg:SetPoint("TOPLEFT", 1, -1)
    bg:SetPoint("BOTTOMRIGHT", -1, 1)
    f._bg = bg
    if BackdropTemplateMixin and not f.SetBackdrop then
        Mixin(f, BackdropTemplateMixin)
        if f.OnBackdropLoaded then f:OnBackdropLoaded() end
    end
    if f.SetBackdrop then
        f:SetBackdrop(BACKDROP_INFO)
        f:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], BORDER[4] or 1)
    else
        local function line()
            local t = f:CreateTexture(nil, "BORDER")
            t:SetColorTexture(BORDER[1], BORDER[2], BORDER[3], BORDER[4])
            return t
        end
        local t, b, l, r = line(), line(), line(), line()
        t:SetPoint("TOPLEFT");    t:SetPoint("TOPRIGHT");    t:SetHeight(1)
        b:SetPoint("BOTTOMLEFT");  b:SetPoint("BOTTOMRIGHT");  b:SetHeight(1)
        l:SetPoint("TOPLEFT");     l:SetPoint("BOTTOMLEFT");   l:SetWidth(1)
        r:SetPoint("TOPRIGHT");    r:SetPoint("BOTTOMRIGHT");  r:SetWidth(1)
    end
    return bg
end

local function MakeButton(parent, text, w, h, toggle, template)
    local b = CreateFrame("Button", nil, parent, template)
    b:SetSize(w, h)
    Backdrop(b, BG_A)
    local hl = b:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints(); hl:SetColorTexture(ACCENT[1], ACCENT[2], ACCENT[3], 0.20)
    local fs = b:CreateFontString(nil, "OVERLAY")
    fs:SetFont(FontPath(), 12, ""); fs:SetPoint("CENTER", 0, 0)
    fs:SetText(text); fs:SetTextColor(0.86, 0.86, 0.86)
    b.text = fs
    b._toggle = toggle
    function b:SetSelected(on)
        self._sel = on and true or false
        if on then
            self._bg:SetColorTexture(ACCENT[1], ACCENT[2], ACCENT[3], 0.45)
            self.text:SetTextColor(1, 1, 1)
        else
            self._bg:SetColorTexture(BG[1], BG[2], BG[3], BG_A)
            self.text:SetTextColor(0.86, 0.86, 0.86)
        end
    end
    function b:IsSelected() return self._sel end
    function b:SetLabel(s) self.text:SetText(s) end
    function b:SetEnabledLook(on)
        self:SetEnabled(on)
        self.text:SetTextColor(on and 0.86 or 0.4, on and 0.86 or 0.4, on and 0.86 or 0.4)
    end
    return b
end

local function MakeNumberBox(parent, w, h, onChange)
    local eb = CreateFrame("EditBox", nil, parent)
    eb:SetSize(w, h)
    eb:SetAutoFocus(false); eb:SetNumeric(true); eb:SetMaxLetters(5)
    eb:SetFont(FontPath(), 12, ""); eb:SetJustifyH("CENTER")
    eb:SetTextColor(1, 1, 1); eb:SetTextInsets(4, 4, 0, 0)
    Backdrop(eb, BG_A)
    eb:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnEditFocusLost", function(self)
        if self:GetText() == "" then self:SetText("0") end
    end)
    eb:SetScript("OnTextChanged", function(self, user)
        if user then onChange(tonumber(self:GetText()) or 0) end
    end)
    return eb
end

local function MakeLabel(parent, text, size, accent)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont(FontPath(), size or 12, "")
    fs:SetText(text)
    if accent then fs:SetTextColor(ACCENT[1], ACCENT[2], ACCENT[3])
    else fs:SetTextColor(0.86, 0.86, 0.86) end
    return fs
end

local function MakeCheckRow(parent, labelText, getFn, setFn, tooltip)
    local row = CreateFrame("Button", nil, parent)
    row:SetSize(PANEL_W - 24, 28)

    Backdrop(row, BG_A)
    local hl = row:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints(); hl:SetColorTexture(ACCENT[1], ACCENT[2], ACCENT[3], 0.10)

    local lbl = row:CreateFontString(nil, "OVERLAY")
    lbl:SetFont(FontPath(), 12, "")
    lbl:SetPoint("LEFT", 10, 0)
    lbl:SetText(labelText)

    local box = CreateFrame("Frame", nil, row)
    box:SetSize(16, 16)
    box:SetPoint("RIGHT", -10, 0)
    Backdrop(box, BG_A)
    local fill = box:CreateTexture(nil, "ARTWORK")
    fill:SetPoint("TOPLEFT", 3, -3); fill:SetPoint("BOTTOMRIGHT", -3, 3)
    fill:SetColorTexture(ACCENT[1], ACCENT[2], ACCENT[3], 1)

    local function refresh()
        local on = getFn() and true or false
        fill:SetShown(on)
        local c = on and 0.92 or 0.5
        lbl:SetTextColor(c, c, c)
    end
    row:SetScript("OnClick", function() setFn(not getFn()); refresh() end)
    if tooltip then
        row:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(labelText, 1, 1, 1)
            GameTooltip:AddLine(tooltip, 0.8, 0.8, 0.8, true)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", GameTooltip_Hide)
    end
    refresh()
    return row, refresh
end

local DUNGEON_ABBR_BY_NAME = {
    ["algethar academy"] = "AA",
    ["skyreach"] = "SR",
    ["seat of the triumvirate"] = "SEAT",
    ["magisters terrace"] = "MT",
    ["windrunner spire"] = "WS",
    ["nexus-point xenas"] = "NPX",
    ["nexuspoint xenas"] = "NPX",
    ["pit of saron"] = "POS",
}
local DUNGEON_ABBR_TOKENS = {
    { "algeth", "AA" },
    { "skyreach", "SR" },
    { "triumvirate", "SEAT" },
    { "magister", "MT" },
    { "windrunner", "WS" },
    { "xenas", "NPX" },
    { "saron", "POS" },
}

local ABBR_STOP = {
    of = true, the = true, a = true, an = true, ["and"] = true,
    de = true, da = true, ["do"] = true, dos = true, das = true, e = true,
}

local function NormalizeDungeonKey(name)
    return (name:lower()
        :gsub("[\39\96\180]", "")
        :gsub("\226\128\153", "")
        :gsub("\226\128\152", "")
        :gsub("^the%s+", "")
        :gsub("^operation:%s*", "")
        :gsub("%s+", " ")
        :gsub("^%s+", ""):gsub("%s+$", ""))
end

local function Abbrev(name)
    if not name then return "?" end
    local key = NormalizeDungeonKey(name)
    if DUNGEON_ABBR_BY_NAME[key] then return DUNGEON_ABBR_BY_NAME[key] end
    for i = 1, #DUNGEON_ABBR_TOKENS do
        local tok = DUNGEON_ABBR_TOKENS[i]
        if key:find(tok[1], 1, true) then return tok[2] end
    end


    local n = name:gsub("[\39\96\180]", ""):gsub("\226\128\153", ""):gsub("\226\128\152", "")
    n = n:gsub("^[Tt]he ", ""):gsub("^Operation:%s*", "")
    local letters = {}
    for word in n:gmatch("%a+") do
        if not ABBR_STOP[word:lower()] then
            letters[#letters + 1] = word:sub(1, 1):upper()
        end
    end
    local abbr = table.concat(letters)

    if #abbr < 2 then
        local compact = n:upper():gsub("[%s%p]", "")
        abbr = compact:sub(1, 2)
    end
    return abbr:sub(1, 4)
end

local function GetSeasonDungeons()
    local out = {}
    if not (C_LFGList and C_LFGList.GetAvailableActivityGroups and C_LFGList.GetActivityGroupInfo) then
        return out
    end
    local filter = 0
    if Enum and Enum.LFGListFilter then
        filter = bit.bor(Enum.LFGListFilter.CurrentSeason or 0, Enum.LFGListFilter.PvE or 0)
    end
    local ok, groups = pcall(C_LFGList.GetAvailableActivityGroups, DUNGEONS_CATEGORY, filter)
    if not ok or type(groups) ~= "table" or #groups == 0 then
        ok, groups = pcall(C_LFGList.GetAvailableActivityGroups, DUNGEONS_CATEGORY, 0)
    end
    if ok and type(groups) == "table" then
        for _, gid in ipairs(groups) do
            local name = C_LFGList.GetActivityGroupInfo(gid)
            if name then
                out[#out + 1] = { groupID = gid, name = name, abbr = Abbrev(name) }
            end
        end
    end
    table.sort(out, function(a, b) return (a.name or "") < (b.name or "") end)
    return out
end

local _dungeonTex
local function DungeonTexture(name)
    if not _dungeonTex then
        _dungeonTex = {}
        if C_ChallengeMode and C_ChallengeMode.GetMapTable and C_ChallengeMode.GetMapUIInfo then
            local ok, maps = pcall(C_ChallengeMode.GetMapTable)
            if ok and type(maps) == "table" then
                for _, mapID in ipairs(maps) do
                    local mname, _, _, tex = C_ChallengeMode.GetMapUIInfo(mapID)
                    if mname and tex then _dungeonTex[mname] = tex end
                end
            end
        end
    end
    return name and _dungeonTex[name]
end

local function GetAffixes()
    if not (C_MythicPlus and C_MythicPlus.GetCurrentAffixes) then return nil end
    local ok, affixes = pcall(C_MythicPlus.GetCurrentAffixes)
    if ok and type(affixes) == "table" then return affixes end
    return nil
end

local function GetOwnedKey()
    if not C_MythicPlus then return nil end
    local mapID = C_MythicPlus.GetOwnedKeystoneChallengeMapID and C_MythicPlus.GetOwnedKeystoneChallengeMapID()
    local level = C_MythicPlus.GetOwnedKeystoneLevel and C_MythicPlus.GetOwnedKeystoneLevel()
    if mapID and level and level > 0 then return mapID, level end
    return nil
end

local function MyRole()
    local idx = GetSpecialization and GetSpecialization()
    if idx and GetSpecializationRole then
        local role = GetSpecializationRole(idx)
        if role then return role end
    end
    return "DAMAGER"
end

local RAIDS_CATEGORY = 3

local function ResolveRaidsCategory()
    if not (C_LFGList and C_LFGList.GetAvailableCategories) then return end
    local getInfo = C_LFGList.GetLfgCategoryInfo or C_LFGList.GetCategoryInfo
    if not getInfo then return end
    for _, cid in ipairs(C_LFGList.GetAvailableCategories()) do
        local info = getInfo(cid)
        local name = (type(info) == "table" and info.name) or (type(info) == "string" and info)
        if type(name) == "string" and name:lower():find("raid") then
            RAIDS_CATEGORY = cid
            return
        end
    end
end

local function GetCurrentRaids()
    local out, seen = {}, {}
    if not (C_LFGList and C_LFGList.GetAvailableActivityGroups and C_LFGList.GetActivityGroupInfo) then
        return out
    end
    local filterSets = {}
    if Enum and Enum.LFGListFilter then
        filterSets[#filterSets + 1] = bit.bor(Enum.LFGListFilter.CurrentExpansion or 0, Enum.LFGListFilter.PvE or 0)
        filterSets[#filterSets + 1] = bit.bor(Enum.LFGListFilter.Recommended or 0, Enum.LFGListFilter.PvE or 0)
    end
    filterSets[#filterSets + 1] = 0
    for _, filter in ipairs(filterSets) do
        local ok, groups = pcall(C_LFGList.GetAvailableActivityGroups, RAIDS_CATEGORY, filter)
        if ok and type(groups) == "table" then
            for _, gid in ipairs(groups) do
                if not seen[gid] then
                    local name = C_LFGList.GetActivityGroupInfo(gid)
                    if name then
                        seen[gid] = true
                        out[#out + 1] = { groupID = gid, name = name }
                    end
                end
            end
        end
        if #out > 0 then break end
    end
    return out
end

local RAID_DIFF_BY_ID = { [17] = "LFR", [14] = "Normal", [15] = "Heroic", [16] = "Mythic" }
local function RaidDifficulty(a)
    local byID = a.difficultyID and RAID_DIFF_BY_ID[a.difficultyID]
    if byID then return byID end
    if a.isMythicActivity then return "Mythic"
    elseif a.isHeroicActivity then return "Heroic"
    elseif a.isNormalActivity then return "Normal"
    else return "LFR" end
end

local function ResultRaid(info)
    local acts = info and info.activityIDs
    if issecretvalue(acts) or type(acts) ~= "table" then return nil end
    for _, aid in ipairs(acts) do
        local a = C_LFGList.GetActivityInfoTable(aid)
        if a then return a end
    end
    return nil
end

local function ResultMythicPlus(info)
    local acts = info and info.activityIDs
    if issecretvalue(acts) or type(acts) ~= "table" then return nil end
    for _, aid in ipairs(acts) do
        local a = C_LFGList.GetActivityInfoTable(aid)
        if a and a.isMythicPlusActivity then return a, a.mapID end
    end
    return nil
end

local function ResultKeyLevel(info)
    local acts = info and info.activityIDs
    if issecretvalue(acts) or type(acts) ~= "table" then return nil end
    for i = 1, #acts do
        local aid = acts[i]
        if C_LFGList.GetKeystoneForActivity then
            local ok, level = pcall(C_LFGList.GetKeystoneForActivity, aid)
            if ok and type(level) == "number" and level > 0 then return level end
        end
        if C_LFGList.GetActivityFullName then
            local ok, full = pcall(C_LFGList.GetActivityFullName, aid)
            if ok and type(full) == "string" then
                local n = full:match("%+(%d+)")
                if n then return tonumber(n) end
            end
        end
    end
    return nil
end

local panel, quickView, mplusView, raidView, settingsView, affixHosts
local EnsurePremadeShown
local currentSearchCategory

local function FilterContext()
    if not (panel and panel:IsShown()) then return nil end
    if mplusView and mplusView:IsShown() and currentSearchCategory == DUNGEONS_CATEGORY then
        return "mplus"
    end
    if raidView and raidView:IsShown() and currentSearchCategory == RAIDS_CATEGORY then
        return "raid"
    end
    return nil
end

local function PassesMythicPlus(resultID)
    local info = C_LFGList.GetSearchResultInfo(resultID)
    if not info then return true end
    local act = ResultMythicPlus(info)
    if not act then return false end

    local f = Filters()

    if not issecretvalue(info.leaderName) and IsLeaderBlacklisted(info.leaderName) then
        return false
    end


    if (f.leaderScore or 0) > 0 then
        if (info.leaderOverallDungeonScore or 0) < f.leaderScore then return false end
    end
    if (f.dungeonScore or 0) > 0 then
        local ds = info.leaderDungeonScoreInfo and info.leaderDungeonScoreInfo.mapScore or 0
        if ds < f.dungeonScore then return false end
    end
    local keyMin, keyMax = f.keyLevelMin or 0, f.keyLevelMax or 0
    if keyMin > 0 or keyMax > 0 then
        local level = ResultKeyLevel(info)
        if not level then return false end
        if keyMin > 0 and level < keyMin then return false end
        if keyMax > 0 and level > keyMax then return false end
    end
    if f.hasTank or f.hasHealer or f.roleAvailable then
        local mc = C_LFGList.GetSearchResultMemberCounts(resultID)
        if mc then
            if f.hasTank   and (mc.TANK   or 0) < 1 then return false end
            if f.hasHealer and (mc.HEALER or 0) < 1 then return false end
            if f.roleAvailable then
                local role = MyRole()
                local open = (role == "TANK"   and (mc.TANK   or 0) < 1)
                          or (role == "HEALER" and (mc.HEALER or 0) < 1)
                          or (role == "DAMAGER" and (mc.DAMAGER or 0) < 3)
                if not open then return false end
            end
        end
    end




    if f.bloodlust and not PlayerHasLust() then
        local n = info.numMembers
        if not issecretvalue(n) and n == 4 and C_LFGList.GetSearchResultPlayerInfo then
            local anySecret, hasLust = false, false
            for i = 1, 4 do
                local p = C_LFGList.GetSearchResultPlayerInfo(resultID, i)
                local cls = p and p.classFilename
                if cls == nil or issecretvalue(cls) then anySecret = true; break end
                if LUST_CLASSES[cls] then hasLust = true; break end
            end
            if not anySecret and not hasLust then return false end
        end
    end


    if f.noMyClass and C_LFGList.GetSearchResultPlayerInfo then
        local myCls = select(2, UnitClass("player"))
        local n = info.numMembers
        if myCls and not issecretvalue(n) then
            for i = 1, n do
                local p = C_LFGList.GetSearchResultPlayerInfo(resultID, i)
                local cls = p and p.classFilename
                if cls and not issecretvalue(cls) and cls == myCls then return false end
            end
        end
    end
    return true
end

local function PassesRaid(resultID)
    local info = C_LFGList.GetSearchResultInfo(resultID)
    if not info then return true end
    local a = ResultRaid(info)
    if not a then return false end
    local f = Filters()
    if next(f.raidDiffs) then
        if not f.raidDiffs[RaidDifficulty(a)] then return false end
    end
    if next(f.raids) then
        if not (a.groupFinderActivityGroupID and f.raids[a.groupFinderActivityGroupID]) then
            return false
        end
    end
    return true
end

local _filterHooked = false
local _kept = {}
local _seenMatches, _alertSkipNext = {}, true

local function MaybeAlertNewMatches(kept)
    InitDB()
    if DB.features.matchAlert ~= true then return end
    if _alertSkipNext then
        wipe(_seenMatches)
        for i = 1, #kept do _seenMatches[kept[i]] = true end
        _alertSkipNext = false
        return
    end
    local fresh = false
    for i = 1, #kept do
        local rid = kept[i]
        if not _seenMatches[rid] then
            _seenMatches[rid] = true
            fresh = true
        end
    end
    if fresh then
        local sound = (SOUNDKIT and SOUNDKIT.UI_AUTO_QUEST_COMPLETE) or 5274
        pcall(PlaySound, sound, "Master")
    end
end

local function InstallFilterHook()
    if _filterHooked or type(LFGListUtil_SortSearchResults) ~= "function" then return end
    _filterHooked = true
    hooksecurefunc("LFGListUtil_SortSearchResults", function(self)
        if IsRestricted() then return end
        if not (self and self.results) then return end
        local ctx = FilterContext()
        if not ctx then return end
        local passes = (ctx == "mplus") and PassesMythicPlus or PassesRaid
        local kept = _kept
        wipe(kept)
        for i = 1, #self.results do
            local rid = self.results[i]
            if passes(rid) then kept[#kept + 1] = rid end
        end
        if #kept ~= #self.results then
            wipe(self.results)
            for i = 1, #kept do self.results[i] = kept[i] end
            self.totalResults = #self.results
        end
        if ctx == "mplus" then MaybeAlertNewMatches(kept) end
    end)
end

local function RefreshSearch()
    if IsRestricted() then return end
    local sp = LFGListFrame and LFGListFrame.SearchPanel
    if sp and sp:IsVisible() and type(LFGListSearchPanel_UpdateResultList) == "function" then
        LFGListSearchPanel_UpdateResultList(sp)
    end
end

local function ApplyMplusAdvancedFilter()
    if not (C_LFGList and C_LFGList.GetAdvancedFilter and C_LFGList.SaveAdvancedFilter) then return end
    local ok, af = pcall(C_LFGList.GetAdvancedFilter)
    if not ok or type(af) ~= "table" then return end
    local f = Filters()
    local acts = {}
    for gid in pairs(f.dungeons) do acts[#acts + 1] = gid end
    af.activities = acts
    af.difficultyNormal  = false
    af.difficultyHeroic  = false
    af.difficultyMythic  = false
    af.difficultyMythicPlus = true
    af.generalPlaystyle1, af.generalPlaystyle2 = true, true
    af.generalPlaystyle3, af.generalPlaystyle4 = true, true
    af.hasTank, af.hasHealer = false, false
    af.needsTank, af.needsHealer, af.needsDamage, af.needsMyClass = false, false, false, false
    af.minimumRating = 0
    pcall(C_LFGList.SaveAdvancedFilter, af)
end

local function MplusReSearch()
    ApplyMplusAdvancedFilter()
    local sp = LFGListFrame and LFGListFrame.SearchPanel
    if sp and type(LFGListSearchPanel_DoSearch) == "function" then
        pcall(LFGListSearchPanel_DoSearch, sp)
    end
end

local function ListMyKey()
    EnsurePremadeShown()
    local lf = LFGListFrame
    if not (lf and lf.CategorySelection) then return end
    local cs = lf.CategorySelection









    if type(LFGListCategorySelection_SelectCategory) == "function" then
        pcall(LFGListCategorySelection_SelectCategory, cs, DUNGEONS_CATEGORY, 0)
    end
    if type(LFGListFrame_SetActivePanel) == "function" then
        pcall(LFGListFrame_SetActivePanel, lf, cs)
    end
end

panel, quickView, mplusView, raidView, settingsView, affixHosts = nil, nil, nil, nil, nil, {}

local function RefreshAffixRow(host)
    host.icons = host.icons or {}
    local affixes = GetAffixes()
    if not affixes then
        for _, ic in ipairs(host.icons) do ic:Hide() end
        host._affixIDs = nil
        return
    end

    local prev = host._affixIDs
    if prev and #prev == #affixes then
        local same = true
        for i = 1, #affixes do
            if prev[i] ~= affixes[i].id then same = false; break end
        end
        if same then return end
    end
    local ids = host._affixIDs or {}
    wipe(ids)
    for i = 1, #affixes do ids[i] = affixes[i].id end
    host._affixIDs = ids
    for _, ic in ipairs(host.icons) do ic:Hide() end
    local size, gap = 30, 5
    local total = #affixes
    local rowW = total * size + (total - 1) * gap
    for i, a in ipairs(affixes) do
        local ic = host.icons[i]
        if not ic then
            ic = CreateFrame("Button", nil, host)
            ic:SetSize(size, size)
            ic.tex = ic:CreateTexture(nil, "ARTWORK")
            ic.tex:SetAllPoints(); ic.tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            Backdrop(ic, 0)
            ic:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                local n, d = C_ChallengeMode.GetAffixInfo(self.affixID)
                GameTooltip:SetText(n or "")
                if d then GameTooltip:AddLine(d, 1, 1, 1, true) end
                GameTooltip:Show()
            end)
            ic:SetScript("OnLeave", function() GameTooltip:Hide() end)
            host.icons[i] = ic
        end
        local _, _, fileID = C_ChallengeMode.GetAffixInfo(a.id)
        ic.tex:SetTexture(fileID)
        ic.affixID = a.id
        ic:ClearAllPoints()
        ic:SetPoint("TOP", host, "TOP", -rowW / 2 + (i - 1) * (size + gap) + size / 2, -6)
        ic:Show()
    end
end

local keyButtons = {}
local function RefreshKeyButtons()
    local mapID, level = GetOwnedKey()
    local dungeon
    if mapID and C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
        dungeon = C_ChallengeMode.GetMapUIInfo(mapID)
    end
    for _, b in ipairs(keyButtons) do
        if mapID then
            if dungeon and dungeon ~= "" then
                b:SetLabel(("%s  +%d"):format(dungeon, level))
            else
                b:SetLabel(("List Key:  +%d"):format(level))
            end
            b:SetEnabledLook(true)
        else
            b:SetLabel("No Keystone")
            b:SetEnabledLook(false)
        end
    end
end

local dungeonButtons = {}
local mplusGrid, dungeonGridBuilt, RebuildDungeonGrid
local function RefreshDungeonButtons()
    local f = Filters()
    for _, b in ipairs(dungeonButtons) do
        b:SetSelected(f.dungeons[b.groupID] == true)
    end
end

local function MakeListKeyButton(parent)
    local key = MakeButton(parent, "List Key", PANEL_W - 24, 30, false, "SecureActionButtonTemplate")
    key:RegisterForClicks("AnyUp", "AnyDown")
    key:SetAttribute("type", "click")
    key:SetScript("PreClick", function(self)
        if InCombatLockdown() then return end
        EnsurePremadeShown()
        local lf = LFGListFrame
        local cs = lf and lf.CategorySelection
        if not cs then return end
        if cs.selectedCategory ~= DUNGEONS_CATEGORY then
            if type(LFGListCategorySelection_SelectCategory) == "function" then
                pcall(LFGListCategorySelection_SelectCategory, cs, DUNGEONS_CATEGORY, 0)
            end
            if type(LFGListFrame_SetActivePanel) == "function" then
                pcall(LFGListFrame_SetActivePanel, lf, cs)
            end
        end
        if cs.StartGroupButton then
            self:SetAttribute("clickbutton", cs.StartGroupButton)
        end
    end)
    keyButtons[#keyButtons + 1] = key
    return key
end

local roleWidgets = {}
local function RefreshRoleWidgets()
    local f = Filters()
    if roleWidgets.roleAvailable then roleWidgets.roleAvailable:SetSelected(f.roleAvailable) end
    if roleWidgets.hasTank then roleWidgets.hasTank:SetSelected(f.hasTank) end
    if roleWidgets.hasHealer then roleWidgets.hasHealer:SetSelected(f.hasHealer) end
    if roleWidgets.bloodlust then roleWidgets.bloodlust:SetSelected(f.bloodlust) end
    if roleWidgets.noMyClass then roleWidgets.noMyClass:SetSelected(f.noMyClass) end
    if roleWidgets.leaderBox then roleWidgets.leaderBox:SetText(tostring(f.leaderScore or 0)) end
    if roleWidgets.dungeonBox then roleWidgets.dungeonBox:SetText(tostring(f.dungeonScore or 0)) end
    if roleWidgets.keyMinBox then roleWidgets.keyMinBox:SetText(tostring(f.keyLevelMin or 0)) end
    if roleWidgets.keyMaxBox then roleWidgets.keyMaxBox:SetText(tostring(f.keyLevelMax or 0)) end
end

local raidButtons, raidDiffButtons = {}, {}
local function RefreshRaidButtons()
    local f = Filters()
    for _, b in ipairs(raidButtons)     do b:SetSelected(f.raids[b.groupID] == true) end
    for _, b in ipairs(raidDiffButtons) do b:SetSelected(f.raidDiffs[b.diff] == true) end
end

local ShowView, SelectLFGCategoryByName, GoToCategory, GoToRaids

EnsurePremadeShown = function()
    if InCombatLockdown() then return end
    if type(PVEFrame_ShowFrame) == "function" then
        pcall(PVEFrame_ShowFrame, "GroupFinderFrame")
    end
    local stub = _G.LFGListPVEStub
    if stub and type(GroupFinderFrame_ShowGroupFrame) == "function" then
        pcall(GroupFinderFrame_ShowGroupFrame, stub)
    end
end

local function BuildQuickView()
    local v = CreateFrame("Frame", nil, panel)
    v:SetAllPoints(panel)

    local affix = CreateFrame("Frame", nil, v)
    affix:SetPoint("TOPLEFT", 6, -6); affix:SetPoint("TOPRIGHT", -6, -6); affix:SetHeight(40)
    affixHosts[#affixHosts + 1] = affix

    local title = MakeLabel(v, "Quick Access", 16, true)
    title:SetPoint("TOP", affix, "BOTTOM", 0, -10)

    local cats = {
        { "Mythic+", function() ShowView("mplus"); ApplyMplusAdvancedFilter(); GoToCategory(DUNGEONS_CATEGORY, 0) end },
        { "Raids",   function() GoToRaids() end },
        { "Delves",  function() SelectLFGCategoryByName("delves") end },
        { "Quest",   function() SelectLFGCategoryByName("quest") end },
        { "Custom",  function() SelectLFGCategoryByName("custom") end },
    }
    local y = -78
    for _, c in ipairs(cats) do
        local b = MakeButton(v, c[1], PANEL_W - 24, 30)
        b:SetPoint("TOP", 0, y)
        b:SetScript("OnClick", c[2])
        y = y - 36
    end

    local keyBtn = MakeListKeyButton(v)
    keyBtn:SetPoint("BOTTOM", 0, 8)

    local credit = MakeLabel(v, "by devbygabriel", 9)
    credit:SetTextColor(0.42, 0.42, 0.42)
    credit:SetPoint("BOTTOM", keyBtn, "TOP", 0, 6)



    local settingsBtn = MakeButton(v, "Settings", PANEL_W - 24, 24)
    settingsBtn:SetPoint("BOTTOM", credit, "TOP", 0, 8)
    settingsBtn.text:SetTextColor(0.62, 0.62, 0.62)
    settingsBtn:HookScript("OnEnter", function(self) self.text:SetTextColor(0.95, 0.95, 0.95) end)
    settingsBtn:HookScript("OnLeave", function(self) self.text:SetTextColor(0.62, 0.62, 0.62) end)
    settingsBtn:SetScript("OnClick", function() ShowView("settings") end)

    local sep = v:CreateTexture(nil, "ARTWORK")
    sep:SetColorTexture(1, 1, 1, 0.07)
    sep:SetHeight(1)
    sep:SetPoint("BOTTOMLEFT", settingsBtn, "TOPLEFT", 2, 9)
    sep:SetPoint("BOTTOMRIGHT", settingsBtn, "TOPRIGHT", -2, 9)

    v:Hide()
    return v
end

local function BuildMplusView()
    local v = CreateFrame("Frame", nil, panel)
    v:SetAllPoints(panel)

    local affix = CreateFrame("Frame", nil, v)
    affix:SetPoint("TOPLEFT", 6, -6); affix:SetPoint("TOPRIGHT", -6, -6); affix:SetHeight(40)
    affixHosts[#affixHosts + 1] = affix

    local back = MakeButton(v, "<", 22, 18)
    back:SetPoint("TOPLEFT", 12, -50)
    back:SetScript("OnClick", function() ShowView("quick") end)








    local colW = (PANEL_W - 24 - 8) / 2
    local grid = CreateFrame("Frame", nil, v)
    grid:SetPoint("TOPLEFT", 12, -70)
    grid:SetWidth(PANEL_W - 24)
    grid:SetHeight(112)
    mplusGrid = grid

    RebuildDungeonGrid = function()
        if dungeonGridBuilt or not mplusGrid then return end
        local dungeons = GetSeasonDungeons()
        if #dungeons == 0 then return end
        for i, d in ipairs(dungeons) do
            local b = MakeButton(mplusGrid, d.abbr, colW, 24, true)
            local col, row = (i - 1) % 2, math.floor((i - 1) / 2)
            b:SetPoint("TOPLEFT", col * (colW + 8), -row * 28)
            b.groupID, b.dungeonName = d.groupID, d.name

            local tex = DungeonTexture(d.name)
            if tex and b.text then
                local ic = b:CreateTexture(nil, "ARTWORK")
                ic:SetSize(18, 18)
                ic:SetPoint("LEFT", 4, 0)
                ic:SetTexture(tex)
                ic:SetTexCoord(0.10, 0.90, 0.10, 0.90)
                b.preview = ic
                b.text:ClearAllPoints()
                b.text:SetPoint("LEFT", ic, "RIGHT", 2, 0)
                b.text:SetPoint("RIGHT", b, "RIGHT", -4, 0)
                b.text:SetJustifyH("CENTER")
            else
                b.text:ClearAllPoints()
                b.text:SetPoint("CENTER", 0, 0)
                b.text:SetJustifyH("CENTER")
            end
            b:SetScript("OnClick", function(self)
                local f = Filters()
                f.dungeons[self.groupID] = (not f.dungeons[self.groupID]) or nil
                self:SetSelected(f.dungeons[self.groupID] == true)
                MplusReSearch()
            end)
            b:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetText(self.dungeonName or self.text:GetText())
                GameTooltip:Show()
            end)
            b:SetScript("OnLeave", function() GameTooltip:Hide() end)
            dungeonButtons[#dungeonButtons + 1] = b
        end
        local rows = math.max(math.ceil(#dungeons / 2), 1)
        mplusGrid:SetHeight(rows * 28)
        dungeonGridBuilt = true
        RefreshDungeonButtons()
    end


    local ROW_GAP, ROW_H = 6, 24
    local sideMargin, midGap = 12, 8
    local halfW = (PANEL_W - sideMargin * 2 - midGap) / 2

    local function scoreRow(label, key, anchor)
        local row = CreateFrame("Frame", nil, v)
        row:SetSize(PANEL_W - 24, ROW_H)
        row:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -ROW_GAP)
        Backdrop(row, BG_A)
        local lbl = MakeLabel(row, label, 11)
        lbl:SetPoint("LEFT", 8, 0)
        local box = MakeNumberBox(row, 48, 18, function(val)
            Filters()[key] = val; RefreshSearch()
        end)
        box:SetPoint("RIGHT", -5, 0)
        return row, box
    end

    local leaderRow, leaderBox = scoreRow("Leader Score Over", "leaderScore", grid)
    local dungeonRow, dungeonBox = scoreRow("Dungeon Score Over", "dungeonScore", leaderRow)
    roleWidgets.leaderBox, roleWidgets.dungeonBox = leaderBox, dungeonBox

    local function keyBox(label, key)
        local row = CreateFrame("Frame", nil, v)
        row:SetSize(halfW, ROW_H)
        Backdrop(row, BG_A)
        local lbl = MakeLabel(row, label, 10)
        lbl:SetPoint("LEFT", 6, 0)
        local box = MakeNumberBox(row, 40, 18, function(val)
            Filters()[key] = val; RefreshSearch()
        end)
        box:SetPoint("RIGHT", -4, 0)
        box:SetMaxLetters(2)
        return row, box
    end
    local keyMinRow, keyMinBox = keyBox("Key Min", "keyLevelMin")
    keyMinRow:SetPoint("TOPLEFT", dungeonRow, "BOTTOMLEFT", 0, -ROW_GAP)
    local keyMaxRow, keyMaxBox = keyBox("Key Max", "keyLevelMax")
    keyMaxRow:SetPoint("TOPRIGHT", dungeonRow, "BOTTOMRIGHT", 0, -ROW_GAP)
    roleWidgets.keyMinBox, roleWidgets.keyMaxBox = keyMinBox, keyMaxBox

    local ra = MakeButton(v, "Role Available", PANEL_W - 24, ROW_H, true)
    ra:SetPoint("TOPLEFT", keyMinRow, "BOTTOMLEFT", 0, -ROW_GAP)
    ra:SetScript("OnClick", function(self)
        local f = Filters(); f.roleAvailable = not f.roleAvailable
        self:SetSelected(f.roleAvailable); RefreshSearch()
    end)
    roleWidgets.roleAvailable = ra

    local ht = MakeButton(v, "Has Tank", halfW, ROW_H, true)
    ht:SetPoint("TOPLEFT", ra, "BOTTOMLEFT", 0, -ROW_GAP)
    ht:SetScript("OnClick", function(self)
        local f = Filters(); f.hasTank = not f.hasTank
        self:SetSelected(f.hasTank); RefreshSearch()
    end)
    roleWidgets.hasTank = ht
    local hh = MakeButton(v, "Has Healer", halfW, ROW_H, true)
    hh:SetPoint("TOPRIGHT", ra, "BOTTOMRIGHT", 0, -ROW_GAP)
    hh:SetScript("OnClick", function(self)
        local f = Filters(); f.hasHealer = not f.hasHealer
        self:SetSelected(f.hasHealer); RefreshSearch()
    end)
    roleWidgets.hasHealer = hh

    local bl = MakeButton(v, "Bloodlust", halfW, ROW_H, true)
    bl:SetPoint("TOPLEFT", ht, "BOTTOMLEFT", 0, -ROW_GAP)
    bl:SetScript("OnClick", function(self)
        local f = Filters(); f.bloodlust = not f.bloodlust
        self:SetSelected(f.bloodlust); MplusReSearch()
    end)
    roleWidgets.bloodlust = bl

    local nmc = MakeButton(v, "Hide Class", halfW, ROW_H, true)
    nmc:SetPoint("TOPRIGHT", hh, "BOTTOMRIGHT", 0, -ROW_GAP)
    nmc:SetScript("OnClick", function(self)
        local f = Filters(); f.noMyClass = not f.noMyClass
        self:SetSelected(f.noMyClass); MplusReSearch()
    end)
    roleWidgets.noMyClass = nmc


    local footerGap, presetGap, defGap = 8, 10, 10
    local presetW = 34
    local presetsBlock = 3 * presetW + 2 * presetGap

    local presetRow = CreateFrame("Frame", nil, v)
    presetRow:SetSize(PANEL_W - 24, 24)
    presetRow:SetPoint("TOPLEFT", bl, "BOTTOMLEFT", 0, -footerGap)

    for slot = 1, 3 do
        local pb = MakeButton(presetRow, tostring(slot), presetW, 24)
        pb:SetPoint("LEFT", (slot - 1) * (presetW + presetGap), 0)
        pb:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        pb:SetScript("OnClick", function(_, button)
            if button == "RightButton" then
                SavePreset(slot)
            elseif LoadPreset(slot) then
                RefreshDungeonButtons(); RefreshRoleWidgets(); MplusReSearch()
            end
        end)
        pb:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText("Preset " .. slot, 1, 1, 1)
            GameTooltip:AddLine("Left-click: load", 0.8, 0.8, 0.8)
            GameTooltip:AddLine("Right-click: save current filters", 0.8, 0.8, 0.8)
            GameTooltip:Show()
        end)
        pb:SetScript("OnLeave", GameTooltip_Hide)
    end

    local def = MakeButton(presetRow, "Default", PANEL_W - 24 - presetsBlock - defGap, 24)
    def:SetPoint("RIGHT", 0, 0)
    def:SetScript("OnClick", function()
        ResetFilters(); RefreshDungeonButtons(); RefreshRoleWidgets(); MplusReSearch()
    end)

    local keyBtn = MakeListKeyButton(v)
    keyBtn:SetSize(PANEL_W - 24, 26)
    keyBtn:SetPoint("TOPLEFT", presetRow, "BOTTOMLEFT", 0, -footerGap)


    local bottomPad = CreateFrame("Frame", nil, v)
    bottomPad:SetSize(1, 8)
    bottomPad:SetPoint("TOP", keyBtn, "BOTTOM", 0, -2)

    RebuildDungeonGrid()

    v:Hide()
    return v
end

local function BuildRaidView()
    local v = CreateFrame("Frame", nil, panel)
    v:SetAllPoints(panel)

    local back = MakeButton(v, "<", 22, 18)
    back:SetPoint("TOPLEFT", 12, -10)
    back:SetScript("OnClick", function() ShowView("quick") end)

    local title = MakeLabel(v, "Raids", 16, true)
    title:SetPoint("TOP", 0, -12)


    local diffLabel = MakeLabel(v, "Difficulty", 11)
    diffLabel:SetPoint("TOPLEFT", 12, -42)
    local diffs = { "LFR", "Normal", "Heroic", "Mythic" }
    local sideMargin, midGap = 12, 8
    local halfW = (PANEL_W - sideMargin * 2 - midGap) / 2
    for i, d in ipairs(diffs) do
        local b = MakeButton(v, d, halfW, 24, true)
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        b:SetPoint("TOPLEFT", sideMargin + col * (halfW + midGap), -58 - row * 28)
        b.diff = d
        b:SetScript("OnClick", function(self)
            local f = Filters()
            f.raidDiffs[self.diff] = (not f.raidDiffs[self.diff]) or nil
            self:SetSelected(f.raidDiffs[self.diff] == true)
            RefreshSearch()
        end)
        raidDiffButtons[#raidDiffButtons + 1] = b
    end


    local raidLabel = MakeLabel(v, "Raids", 11)
    raidLabel:SetPoint("TOPLEFT", 12, -120)
    local raids = GetCurrentRaids()
    local ry = -136
    for _, r in ipairs(raids) do
        local b = MakeButton(v, r.name, PANEL_W - 24, 24, true)
        b:SetPoint("TOP", 0, ry)
        b.groupID = r.groupID
        b:SetScript("OnClick", function(self)
            local f = Filters()
            f.raids[self.groupID] = (not f.raids[self.groupID]) or nil
            self:SetSelected(f.raids[self.groupID] == true)
            RefreshSearch()
        end)
        raidButtons[#raidButtons + 1] = b
        ry = ry - 28
    end
    if #raids == 0 then
        local none = MakeLabel(v, "Open the raid list once to populate.", 10)
        none:SetPoint("TOP", 0, ry); none:SetTextColor(0.6, 0.6, 0.6)
    end


    local def = MakeButton(v, "Default", PANEL_W - 24, 24)
    def:SetPoint("BOTTOM", 0, 12)
    def:SetScript("OnClick", function()
        ResetFilters(); RefreshRaidButtons(); RefreshSearch()
    end)

    v:Hide()
    return v
end

local function FeatureSet(key, v)
    InitDB()
    DB.features[key] = v and true or false
    if key == "matchAlert" and v then _alertSkipNext = true end
    RefreshSearch()
end

local settingsRefreshers = {}
local function BuildSettingsView()
    local v = CreateFrame("Frame", nil, panel)
    v:SetAllPoints(panel)


    local back = MakeButton(v, "<", 22, 18)
    back:SetPoint("TOPLEFT", 12, -14)
    back:SetScript("OnClick", function() ShowView("quick") end)

    local title = MakeLabel(v, "Settings", 15, true)
    title:SetPoint("TOP", 0, -16)

    local underline = v:CreateTexture(nil, "ARTWORK")
    underline:SetColorTexture(ACCENT[1], ACCENT[2], ACCENT[3], 0.55)
    underline:SetHeight(1)
    underline:SetPoint("TOPLEFT", 12, -40)
    underline:SetPoint("TOPRIGHT", -12, -40)


    local sec = MakeLabel(v, "GROUP FINDER DISPLAY", 10, false)
    sec:SetTextColor(0.48, 0.48, 0.48)
    sec:SetPoint("TOPLEFT", 12, -54)

    local rows = {
        { "Leader Score",   "leaderIO",  "Show the group leader's Mythic+ score on dungeon listings." },
        { "Spec Icons",     "specIcons", "Show group members' spec icons on listings, and applicants' spec icons in the viewer." },
    }
    local y = -70
    for _, r in ipairs(rows) do
        local key = r[2]
        local row, refresh = MakeCheckRow(v, r[1],
            function() return FeatureGet(key) end,
            function(val) FeatureSet(key, val) end,
            r[3])
        row:SetPoint("TOP", 0, y)
        settingsRefreshers[#settingsRefreshers + 1] = refresh
        y = y - 32
    end

    local sec2 = MakeLabel(v, "ALERTS & BLACKLIST", 10, false)
    sec2:SetTextColor(0.48, 0.48, 0.48)
    sec2:SetPoint("TOPLEFT", 12, y - 8)
    y = y - 24

    local alertRow, alertRefresh = MakeCheckRow(v, "Match Alert",
        function() InitDB(); return DB.features.matchAlert == true end,
        function(val) FeatureSet("matchAlert", val) end,
        "Play a sound when a new group appears that passes your Mythic+ filters.")
    alertRow:SetPoint("TOP", 0, y)
    settingsRefreshers[#settingsRefreshers + 1] = alertRefresh
    y = y - 36

    local blHint = MakeLabel(v, "Shift+Right-click a listing\nto blacklist its leader.", 10)
    blHint:SetJustifyH("CENTER")
    blHint:SetPoint("TOP", 0, y)
    blHint:SetTextColor(0.62, 0.62, 0.62)
    y = y - 36

    local blCount = MakeLabel(v, "Blacklisted: 0", 11)
    blCount:SetPoint("TOP", 0, y)
    settingsRefreshers[#settingsRefreshers + 1] = function()
        blCount:SetText("Blacklisted: " .. BlacklistCount())
    end
    y = y - 28

    local clearBl = MakeButton(v, "Clear Blacklist", PANEL_W - 24, 24)
    clearBl:SetPoint("TOP", 0, y)
    clearBl:SetScript("OnClick", function()
        BlacklistClear()
        blCount:SetText("Blacklisted: 0")
        RefreshSearch()
        print("|cff1784d1HelwenLFG|r blacklist cleared.")
    end)

    local credit = MakeLabel(v, "Author: devbygabriel", 10)
    credit:SetTextColor(0.45, 0.45, 0.45)
    credit:SetPoint("BOTTOM", 0, 14)

    v:Hide()
    return v
end

ShowView = function(which)
    if not panel then return end
    quickView:Hide(); mplusView:Hide()
    if raidView then raidView:Hide() end
    if settingsView then settingsView:Hide() end
    if which == "settings" then
        settingsView:Show()
        for _, refresh in ipairs(settingsRefreshers) do refresh() end
        return
    end
    if which == "mplus" then
        mplusView:Show()
        if RebuildDungeonGrid then RebuildDungeonGrid() end
        RefreshDungeonButtons(); RefreshRoleWidgets()
    elseif which == "raid" then
        if raidView then raidView:Show(); RefreshRaidButtons() end
    else
        quickView:Show()
    end
    for _, host in ipairs(affixHosts) do RefreshAffixRow(host) end
    RefreshKeyButtons()
end

GoToCategory = function(categoryID, filters)
    EnsurePremadeShown()
    local lf = LFGListFrame
    if not (lf and lf.CategorySelection) then return end
    local cs = lf.CategorySelection
    if type(LFGListCategorySelection_SelectCategory) == "function" then
        pcall(LFGListCategorySelection_SelectCategory, cs, categoryID, filters or 0)
    end
    if type(LFGListCategorySelection_StartFindGroup) == "function" then
        pcall(LFGListCategorySelection_StartFindGroup, cs)
    end
end

GoToRaids = function()
    ShowView("raid")
    local recommended = (Enum and Enum.LFGListFilter and Enum.LFGListFilter.Recommended) or 0
    GoToCategory(RAIDS_CATEGORY, recommended)
end

SelectLFGCategoryByName = function(want)
    if not (C_LFGList and C_LFGList.GetAvailableCategories) then return end
    local getInfo = C_LFGList.GetLfgCategoryInfo or C_LFGList.GetCategoryInfo
    if not getInfo then return end
    for _, cid in ipairs(C_LFGList.GetAvailableCategories()) do
        local info = getInfo(cid)
        local name = (type(info) == "table" and info.name) or (type(info) == "string" and info)
        if type(name) == "string" then
            local low = name:lower()
            if (want == "delves" and low:find("delve"))
            or (want == "quest"  and low:find("quest"))
            or (want == "custom" and low:find("custom")) then
                ShowView("quick")
                GoToCategory(cid, 0)
                return
            end
        end
    end
end

local HookRaiderIO

local function LayoutPanel()
    if not panel or not PVEFrame then return end
    panel:ClearAllPoints()
    panel:SetWidth(PANEL_W)
    panel:SetPoint("TOPLEFT", PVEFrame, "TOPRIGHT", 8, 0)
    panel:SetPoint("BOTTOMLEFT", PVEFrame, "BOTTOMRIGHT", 8, -PANEL_EXTRA_BOTTOM)
end

local function BuildPanel()
    if panel then return end



    if _G.__HelwenLFGBuilt or _G.__BoringLFGFilterBuilt then return end
    _G.__HelwenLFGBuilt = true
    InitDB()




    local name = (_G.PremadeGroupsFilterDialog == nil) and "PremadeGroupsFilterDialog" or "HelwenLFGPanel"




    panel = CreateFrame("Frame", name, UIParent)
    if PVEFrame then
        LayoutPanel()
    else
        panel:SetWidth(PANEL_W)
        panel:SetPoint("CENTER")
        panel:SetHeight(480)
    end
    panel:SetFrameStrata(PVEFrame and PVEFrame:GetFrameStrata() or "HIGH")
    Backdrop(panel, BG_A)

    quickView    = BuildQuickView()
    mplusView    = BuildMplusView()
    raidView     = BuildRaidView()
    settingsView = BuildSettingsView()
    ShowView("quick")
    panel:Hide()

    HookRaiderIO()
end

local RAIDERIO_GAP = 16
HookRaiderIO = function()
    local anchor = _G.RaiderIO_ProfileTooltipAnchor
    local tip    = _G.RaiderIO_ProfileTooltip
    if not tip then return end
    local function apply()
        if not (panel and panel:IsShown() and tip:IsShown()) then return end
        tip:ClearAllPoints()
        tip:SetPoint("TOPLEFT", panel, "TOPRIGHT", RAIDERIO_GAP, 0)
        if anchor then
            anchor:ClearAllPoints()
            anchor:SetPoint("TOPLEFT", panel, "TOPRIGHT", RAIDERIO_GAP, 0)
        end
    end
    local function pin()
        apply()
        if C_Timer and C_Timer.After then C_Timer.After(0, apply) end
    end
    tip:HookScript("OnShow", pin)
    if panel then
        panel:HookScript("OnShow", function()
            LayoutPanel()
            if tip:IsShown() then pin() end
        end)
    end
end

local function ShouldShow()
    if not (PVEFrame and PVEFrame:IsShown()) then return false end
    if PVPUIFrame and PVPUIFrame:IsShown() then return false end
    return true
end

local function UpdatePanelVisibility()
    if not panel then return end
    if ShouldShow() then
        LayoutPanel()
        panel:Show()
        for _, host in ipairs(affixHosts) do RefreshAffixRow(host) end
        RefreshKeyButtons(); RefreshDungeonButtons(); RefreshRoleWidgets(); RefreshRaidButtons()
    else
        panel:Hide()
    end
end

local function SyncViewToCategory(categoryID)
    currentSearchCategory = categoryID
    if not panel then return end
    if categoryID == DUNGEONS_CATEGORY then
        ShowView("mplus")
    elseif categoryID == RAIDS_CATEGORY then
        ShowView("raid")
    else
        ShowView("quick")
    end
end

local function PGFLoaded()
    local f = (C_AddOns and C_AddOns.IsAddOnLoaded) or IsAddOnLoaded
    return (f and f("PremadeGroupsFilter")) and true or false
end

local function DisableAddonAndReload(addon)
    local disable = (C_AddOns and C_AddOns.DisableAddOn) or _G.DisableAddOn
    if disable then pcall(disable, addon) end
    ReloadUI()
end

local _pgfPopup
local function ShowPGFConflictPopup()
    if _pgfPopup then _pgfPopup:Show(); return end
    local p = CreateFrame("Frame", "HelwenLFGConflictPopup", UIParent)
    p:SetSize(404, 188)
    p:SetPoint("CENTER", 0, 120)
    p:SetFrameStrata("FULLSCREEN_DIALOG")
    if p.SetToplevel then p:SetToplevel(true) end
    Backdrop(p, 0.97)

    local title = p:CreateFontString(nil, "OVERLAY")
    title:SetFont(FontPath(), 15, "OUTLINE")
    title:SetPoint("TOP", 0, -16)
    title:SetText("Addon Conflict")
    title:SetTextColor(0.30, 0.68, 1.00)

    local msg = p:CreateFontString(nil, "OVERLAY")
    msg:SetFont(FontPath(), 12, "")
    msg:SetPoint("TOP", 0, -48)
    msg:SetWidth(364)
    msg:SetJustifyH("CENTER")
    msg:SetText("|cffffffffHelwenLFG|r and |cffffffffPremade Groups Filter|r both filter the Group Finder and will conflict. "
        .. "Keep one; the other is disabled and your UI reloads.")
    msg:SetTextColor(0.82, 0.82, 0.82)


    local keepMine = MakeButton(p, "Disable Premade Filter", 184, 26)
    keepMine:SetPoint("BOTTOMLEFT", 16, 16)
    keepMine:SetScript("OnClick", function() DisableAddonAndReload("PremadeGroupsFilter") end)

    local keepPGF = MakeButton(p, "Disable HelwenLFG", 172, 26)
    keepPGF:SetPoint("BOTTOMRIGHT", -16, 16)
    keepPGF:SetScript("OnClick", function() DisableAddonAndReload("HelwenLFG") end)


    local close = MakeButton(p, "X", 24, 20)
    close:SetPoint("TOPRIGHT", -8, -8)
    close:SetScript("OnClick", function() p:Hide() end)

    _pgfPopup = p
end

local _pgfChecked
local function CheckPGFConflict()
    if _pgfChecked then return end
    if not PGFLoaded() then return end
    _pgfChecked = true
    ShowPGFConflictPopup()
end

local _lfgHooked = false
local function HookLFG()
    if _lfgHooked or not LFGListFrame then return end
    _lfgHooked = true
    ResolveRaidsCategory()
    BuildPanel()
    InstallFilterHook()





    local liveFilter = CreateFrame("Frame")
    liveFilter:RegisterEvent("LFG_LIST_SEARCH_RESULT_UPDATED")
    liveFilter:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
    local pending = false
    local function LiveFilterRun()
        pending = false
        RefreshSearch()
    end
    liveFilter:SetScript("OnEvent", function(_, event)
        if event == "LFG_LIST_SEARCH_RESULTS_RECEIVED" then
            _alertSkipNext = true
        end
        if pending or IsRestricted() then return end
        local f = Filters()
        local bl = (DB or HelwenLFGDB or {}).blacklist
        if not (f.roleAvailable or f.hasTank or f.hasHealer or f.bloodlust or f.noMyClass
            or (f.keyLevelMin or 0) > 0 or (f.keyLevelMax or 0) > 0
            or (bl and next(bl))) then
            return
        end
        pending = true
        if C_Timer and C_Timer.After then C_Timer.After(0.25, LiveFilterRun) else LiveFilterRun() end
    end)


    if _G.LFGListSearchEntry_Update then
        hooksecurefunc("LFGListSearchEntry_Update", function(entry)
            if not entry or entry._hlfgBlHooked then return end
            entry._hlfgBlHooked = true
            entry:HookScript("OnMouseUp", function(self, button)
                if button ~= "RightButton" or not IsShiftKeyDown() then return end
                if IsRestricted() or not self.resultID then return end
                local info = C_LFGList.GetSearchResultInfo(self.resultID)
                local name = info and info.leaderName
                if not name or issecretvalue(name) then return end
                if BlacklistAdd(name) then
                    print("|cff1784d1HelwenLFG|r blacklisted: " .. name)
                    RefreshSearch()
                end
            end)
        end)
    end



    local regen = CreateFrame("Frame")
    regen:RegisterEvent("PLAYER_REGEN_ENABLED")
    regen:SetScript("OnEvent", function()
        if C_Timer and C_Timer.After then C_Timer.After(0.1, RefreshSearch) else RefreshSearch() end
    end)



    if PVEFrame then
        PVEFrame:HookScript("OnShow", UpdatePanelVisibility)
        PVEFrame:HookScript("OnHide", UpdatePanelVisibility)
    end
    if type(PVEFrame_ShowFrame) == "function" then
        hooksecurefunc("PVEFrame_ShowFrame", UpdatePanelVisibility)
    end
    if type(GroupFinderFrame_ShowGroupFrame) == "function" then
        hooksecurefunc("GroupFinderFrame_ShowGroupFrame", UpdatePanelVisibility)
    end
    if type(LFGListFrame_SetActivePanel) == "function" then
        hooksecurefunc("LFGListFrame_SetActivePanel", UpdatePanelVisibility)
    end
    if type(LFGListSearchPanel_SetCategory) == "function" then
        hooksecurefunc("LFGListSearchPanel_SetCategory", function(_, categoryID)
            SyncViewToCategory(categoryID)
        end)
    end

    UpdatePanelVisibility()
end

SLASH_HELWENLFG1 = "/helwenlfg"
SLASH_HELWENLFG2 = "/hlfg"
SLASH_HELWENLFG3 = "/blfg"
SLASH_HELWENLFG4 = "/lfgfilter"
SlashCmdList["HELWENLFG"] = function(msg)
    msg = (msg or ""):lower():gsub("%s+", "")
    if msg == "reset" then
        ResetFilters(); RefreshDungeonButtons(); RefreshRoleWidgets(); RefreshRaidButtons(); RefreshSearch()
        print("|cff1784d1HelwenLFG|r filters reset.")
    elseif msg == "blclear" or msg == "clearbl" then
        BlacklistClear(); RefreshSearch()
        print("|cff1784d1HelwenLFG|r blacklist cleared.")
    elseif msg:match("^save[123]$") then
        SavePreset(tonumber(msg:sub(5)))
    elseif msg:match("^load[123]$") then
        if LoadPreset(tonumber(msg:sub(5))) then
            RefreshDungeonButtons(); RefreshRoleWidgets(); RefreshSearch()
        end
    elseif panel then
        panel:SetShown(not panel:IsShown())
    else
        print("|cff1784d1HelwenLFG|r open the Premade Group Finder first.")
        print("  /hlfg reset | blclear | save1-3 | load1-3")
    end
end

local boot = CreateFrame("Frame")
boot:RegisterEvent("PLAYER_LOGIN")
boot:RegisterEvent("ADDON_LOADED")
boot:SetScript("OnEvent", function(_, event, name)
    if event == "ADDON_LOADED" and name == "Blizzard_GroupFinder" then
        HookLFG()
    elseif event == "PLAYER_LOGIN" then
        InitDB()
        if C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("Blizzard_GroupFinder") then
            HookLFG()
        end
        if C_Timer and C_Timer.After then C_Timer.After(4, CheckPGFConflict) else CheckPGFConflict() end
    end
end)
