local addonName, RF = ...
local T

-------------------------------------------------------------------------------
-- Perks Tab: Guild reputation, perks, and rewards
-------------------------------------------------------------------------------

local STANDING_LABELS = {
    [0] = "Unknown", [1] = "Hated", [2] = "Hostile", [3] = "Unfriendly",
    [4] = "Neutral", [5] = "Friendly", [6] = "Honored", [7] = "Revered",
    [8] = "Exalted",
}

local STANDING_COLORS = {
    [0] = { 0.50, 0.50, 0.50 },
    [1] = { 0.80, 0.13, 0.13 },
    [2] = { 0.80, 0.13, 0.13 },
    [3] = { 0.75, 0.27, 0.00 },
    [4] = { 0.90, 0.70, 0.00 },
    [5] = { 0.00, 0.60, 0.00 },
    [6] = { 0.00, 0.60, 0.40 },
    [7] = { 0.00, 0.35, 0.75 },
    [8] = { 0.00, 0.60, 0.75 },
}

-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------
local function FormatGold(copper)
    if not copper or copper == 0 then return "" end
    local gold = math.floor(copper / 10000)
    local silver = math.floor((copper % 10000) / 100)
    if gold > 0 and silver > 0 then
        return format("%d|cFFFFD700g|r %d|cFFC0C0C0s|r", gold, silver)
    elseif gold > 0 then
        return format("%d|cFFFFD700g|r", gold)
    else
        return format("%d|cFFC0C0C0s|r", silver)
    end
end

local function SafeGetSpellDescription(spellID)
    if C_Spell and C_Spell.GetSpellDescription then
        return C_Spell.GetSpellDescription(spellID) or ""
    end
    if GetSpellDescription then
        return GetSpellDescription(spellID) or ""
    end
    return ""
end

-------------------------------------------------------------------------------
-- Section frame with accent title and divider
-------------------------------------------------------------------------------
local function CreateSection(parent, title)
    local section = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    section:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    section:SetBackdropColor(unpack(T.bgSecondary))
    section:SetBackdropBorderColor(unpack(T.border))

    local titleFS = section:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    local fontPath = titleFS:GetFont()
    titleFS:SetFont(fontPath, RF.FONT_SIZES.medium, "")
    titleFS:SetPoint("TOPLEFT", 12, -8)
    titleFS:SetTextColor(unpack(T.accent))
    titleFS:SetText(title)
    section.titleFS = titleFS

    local div = section:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetPoint("TOPLEFT", 12, -26)
    div:SetPoint("TOPRIGHT", -12, -26)
    div:SetColorTexture(T.border[1], T.border[2], T.border[3], 0.5)

    return section
end

-------------------------------------------------------------------------------
-- Init
-------------------------------------------------------------------------------
function RF:InitPerksTab(parent)
    T = RF.Theme

    local CONTENT_WIDTH = 1050
    local fontPath

    ---------------------------------------------------------------------------
    -- "Not in guild" overlay
    ---------------------------------------------------------------------------
    local noGuildFrame = CreateFrame("Frame", nil, parent)
    noGuildFrame:SetAllPoints()
    noGuildFrame:SetFrameLevel(parent:GetFrameLevel() + 10)
    local noGuildText = noGuildFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fontPath = noGuildText:GetFont()
    noGuildText:SetFont(fontPath, RF.FONT_SIZES.large, "")
    noGuildText:SetPoint("CENTER", 0, 0)
    noGuildText:SetTextColor(unpack(T.textMuted))
    noGuildText:SetText("You are not in a guild.")
    noGuildFrame:Hide()

    ---------------------------------------------------------------------------
    -- Main scroll frame
    ---------------------------------------------------------------------------
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 0, 0)
    scrollFrame:SetPoint("BOTTOMRIGHT", -24, 0)

    local content = CreateFrame("Frame", nil, scrollFrame)
    content:SetWidth(CONTENT_WIDTH)
    content:SetHeight(1)
    scrollFrame:SetScrollChild(content)

    -- Keep content width in sync with scroll frame so sections fill the space
    scrollFrame:SetScript("OnSizeChanged", function(self, w)
        content:SetWidth(w)
    end)

    ---------------------------------------------------------------------------
    -- 1. GUILD REPUTATION SECTION
    ---------------------------------------------------------------------------
    local repSection = CreateSection(content, "Guild Reputation")
    repSection:SetPoint("TOPLEFT", 8, -8)
    repSection:SetPoint("RIGHT", content, "RIGHT", -8, 0)
    repSection:SetHeight(82)

    -- Guild name label
    local guildNameFS = repSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fontPath = guildNameFS:GetFont()
    guildNameFS:SetFont(fontPath, RF.FONT_SIZES.small, "")
    guildNameFS:SetPoint("TOPLEFT", 12, -30)
    guildNameFS:SetTextColor(unpack(T.textSecondary))

    -- Rep bar background
    local repBarBg = CreateFrame("Frame", nil, repSection, "BackdropTemplate")
    repBarBg:SetHeight(20)
    repBarBg:SetPoint("TOPLEFT", 12, -46)
    repBarBg:SetPoint("RIGHT", repSection, "RIGHT", -12, 0)
    repBarBg:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    repBarBg:SetBackdropColor(0.05, 0.05, 0.07, 1)
    repBarBg:SetBackdropBorderColor(T.border[1], T.border[2], T.border[3], 0.5)

    -- Rep bar fill
    local repBarFill = repBarBg:CreateTexture(nil, "ARTWORK")
    repBarFill:SetPoint("TOPLEFT", 1, -1)
    repBarFill:SetPoint("BOTTOMLEFT", 1, 1)
    repBarFill:SetWidth(1)
    repBarFill:SetColorTexture(unpack(T.accent))

    -- Rep text overlay (centered on bar)
    local repBarText = repBarBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    repBarText:SetFont(fontPath, RF.FONT_SIZES.small, "")
    repBarText:SetPoint("CENTER", 0, 0)
    repBarText:SetTextColor(unpack(T.textHighlight))

    ---------------------------------------------------------------------------
    -- 2. GUILD PERKS SECTION
    ---------------------------------------------------------------------------
    local perksSection = CreateSection(content, "Guild Perks")
    perksSection:SetPoint("TOPLEFT", repSection, "BOTTOMLEFT", 0, -10)
    perksSection:SetPoint("RIGHT", content, "RIGHT", -8, 0)
    perksSection:SetHeight(60) -- default, updated in refresh

    -- "No perks" label
    local noPerksFS = perksSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    noPerksFS:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    noPerksFS:SetPoint("TOPLEFT", 12, -34)
    noPerksFS:SetTextColor(unpack(T.textMuted))
    noPerksFS:SetText("No guild perks data available.")
    noPerksFS:Hide()

    -- Perk card pool (max 8 for safety)
    local CARD_COLS = 2
    local CARD_HEIGHT = 72
    local CARD_GAP_X = 10
    local CARD_GAP_Y = 8
    local CARD_TOP = -32

    -- Cards are created lazily (see CreatePerkCard/RefreshPerksSection below)
    -- and the pool grows to fit however many perks the guild actually has --
    -- previously a fixed pool of 8 silently dropped any perk beyond that,
    -- while still reserving row space for it (phantom blank space).
    local perkCards = {}
    local function CreatePerkCard()
        local card = CreateFrame("Frame", nil, perksSection, "BackdropTemplate")
        card:SetHeight(CARD_HEIGHT)
        card:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        card:SetBackdropColor(unpack(T.bgTertiary))
        card:SetBackdropBorderColor(T.border[1], T.border[2], T.border[3], 0.5)

        -- Icon
        local icon = card:CreateTexture(nil, "ARTWORK")
        icon:SetSize(36, 36)
        icon:SetPoint("TOPLEFT", 10, -10)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        card.icon = icon

        -- Icon border
        local ib = CreateFrame("Frame", nil, card, "BackdropTemplate")
        ib:SetSize(38, 38)
        ib:SetPoint("CENTER", icon, "CENTER")
        ib:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
        ib:SetBackdropBorderColor(unpack(T.border))

        -- Name
        local nfs = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        nfs:SetFont(fontPath, RF.FONT_SIZES.normal, "")
        nfs:SetPoint("TOPLEFT", icon, "TOPRIGHT", 8, -2)
        nfs:SetPoint("RIGHT", card, "RIGHT", -8, 0)
        nfs:SetJustifyH("LEFT")
        nfs:SetTextColor(unpack(T.textHighlight))
        card.nameFS = nfs

        -- Description
        local dfs = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        dfs:SetFont(fontPath, RF.FONT_SIZES.small, "")
        dfs:SetPoint("TOPLEFT", nfs, "BOTTOMLEFT", 0, -3)
        dfs:SetPoint("RIGHT", card, "RIGHT", -8, 0)
        dfs:SetJustifyH("LEFT")
        dfs:SetWordWrap(true)
        dfs:SetMaxLines(2)
        dfs:SetTextColor(unpack(T.textSecondary))
        card.descFS = dfs

        -- Tooltip + right-click to link spell in chat
        card:EnableMouse(true)
        card.spellID = nil
        card:SetScript("OnEnter", function(self)
            self:SetBackdropBorderColor(unpack(T.accent))
            if self.spellID then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetSpellByID(self.spellID)
                GameTooltip:Show()
            end
        end)
        card:SetScript("OnLeave", function(self)
            self:SetBackdropBorderColor(T.border[1], T.border[2], T.border[3], 0.5)
            GameTooltip:Hide()
        end)
        card:SetScript("OnMouseUp", function(self, button)
            if button == "RightButton" and self.spellID then
                local link
                if C_Spell and C_Spell.GetSpellLink then
                    link = C_Spell.GetSpellLink(self.spellID)
                elseif GetSpellLink then
                    link = GetSpellLink(self.spellID)
                end
                if link and ChatEdit_InsertLink then
                    ChatEdit_InsertLink(link)
                end
            end
        end)

        card:Hide()
        return card
    end

    ---------------------------------------------------------------------------
    -- 3. GUILD REWARDS SECTION
    ---------------------------------------------------------------------------
    local rewardsSection = CreateSection(content, "Guild Rewards")
    rewardsSection:SetPoint("TOPLEFT", perksSection, "BOTTOMLEFT", 0, -10)
    rewardsSection:SetPoint("RIGHT", content, "RIGHT", -8, 0)
    rewardsSection:SetHeight(100) -- default, updated in refresh

    -- Reward count label (next to section title)
    local rewardCountFS = rewardsSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    rewardCountFS:SetFont(fontPath, RF.FONT_SIZES.small, "")
    rewardCountFS:SetPoint("LEFT", rewardsSection.titleFS, "RIGHT", 8, 0)
    rewardCountFS:SetTextColor(unpack(T.textMuted))

    -- Filter pills
    local filterFrame = CreateFrame("Frame", nil, rewardsSection)
    filterFrame:SetHeight(28)
    filterFrame:SetPoint("TOPLEFT", 12, -32)
    filterFrame:SetPoint("RIGHT", rewardsSection, "RIGHT", -12, 0)

    local FILTERS = { "All", "Friendly", "Honored", "Revered", "Exalted" }
    local FILTER_STANDING = { All = 0, Friendly = 5, Honored = 6, Revered = 7, Exalted = 8 }
    local filterButtons = {}
    local currentFilter = "All"

    local function UpdateFilterVisuals()
        for _, pill in ipairs(filterButtons) do
            if pill.filterName == currentFilter then
                pill.bg:SetColorTexture(T.accent[1], T.accent[2], T.accent[3], 0.8)
                pill.label:SetTextColor(unpack(T.textHighlight))
            else
                pill.bg:SetColorTexture(T.bgTertiary[1], T.bgTertiary[2], T.bgTertiary[3], 1)
                pill.label:SetTextColor(unpack(T.textSecondary))
            end
        end
    end

    local xOff = 0
    for _, filterName in ipairs(FILTERS) do
        local pill = CreateFrame("Button", nil, filterFrame)
        pill:SetHeight(24)

        local pillLabel = pill:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        pillLabel:SetFont(fontPath, RF.FONT_SIZES.small, "")
        pillLabel:SetPoint("CENTER", 0, 0)
        pillLabel:SetText(filterName)
        pill.label = pillLabel

        local textWidth = pillLabel:GetStringWidth()
        pill:SetWidth(math.max(textWidth + 20, 50))

        local pillBg = pill:CreateTexture(nil, "BACKGROUND")
        pillBg:SetAllPoints()
        pill.bg = pillBg

        pill:SetPoint("LEFT", filterFrame, "LEFT", xOff, 0)
        xOff = xOff + pill:GetWidth() + 6

        pill.filterName = filterName
        pill:SetScript("OnEnter", function(self)
            if self.filterName ~= currentFilter then
                self.bg:SetColorTexture(T.bgRowHover[1], T.bgRowHover[2], T.bgRowHover[3], 1)
            end
        end)
        pill:SetScript("OnLeave", function(self)
            if self.filterName ~= currentFilter then
                self.bg:SetColorTexture(T.bgTertiary[1], T.bgTertiary[2], T.bgTertiary[3], 1)
            end
        end)

        tinsert(filterButtons, pill)
    end
    UpdateFilterVisuals()

    -- Column headers
    local headerFrame = CreateFrame("Frame", nil, rewardsSection)
    headerFrame:SetHeight(20)
    headerFrame:SetPoint("TOPLEFT", filterFrame, "BOTTOMLEFT", 0, -8)
    headerFrame:SetPoint("RIGHT", rewardsSection, "RIGHT", -12, 0)

    local COL_DEFS = {
        { text = "Item",        x = 8,   w = 380, align = "LEFT" },
        { text = "Requires",    x = 400, w = 90,  align = "CENTER" },
        { text = "Cost",        x = 500, w = 100, align = "RIGHT" },
        { text = "Achievement", x = 610, w = 380, align = "LEFT" },
    }
    for _, col in ipairs(COL_DEFS) do
        local fs = headerFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        fs:SetFont(fontPath, RF.FONT_SIZES.small, "")
        fs:SetPoint("LEFT", headerFrame, "LEFT", col.x, 0)
        fs:SetWidth(col.w)
        fs:SetJustifyH(col.align)
        fs:SetTextColor(unpack(T.textMuted))
        fs:SetText(col.text)
    end

    -- Divider under headers
    local headerDiv = rewardsSection:CreateTexture(nil, "ARTWORK")
    headerDiv:SetHeight(1)
    headerDiv:SetPoint("TOPLEFT", headerFrame, "BOTTOMLEFT", 0, -2)
    headerDiv:SetPoint("RIGHT", rewardsSection, "RIGHT", -12, 0)
    headerDiv:SetColorTexture(T.border[1], T.border[2], T.border[3], 0.5)

    -- Reward rows container
    local rewardContainer = CreateFrame("Frame", nil, rewardsSection)
    rewardContainer:SetPoint("TOPLEFT", headerDiv, "BOTTOMLEFT", 0, -2)
    rewardContainer:SetPoint("RIGHT", rewardsSection, "RIGHT", -12, 0)
    rewardContainer:SetHeight(1)

    -- "No rewards" label
    local noRewardsFS = rewardContainer:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    noRewardsFS:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    noRewardsFS:SetPoint("TOPLEFT", 8, -12)
    noRewardsFS:SetTextColor(unpack(T.textMuted))
    noRewardsFS:SetText("No guild rewards found.")
    noRewardsFS:Hide()

    -- Row pool
    local ROW_HEIGHT = 36
    local rewardRows = {}

    local function GetOrCreateRow(idx)
        if rewardRows[idx] then return rewardRows[idx] end

        local row = CreateFrame("Frame", nil, rewardContainer)
        row:SetHeight(ROW_HEIGHT)

        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        row.bg = bg
        row.rowIndex = idx

        -- Icon
        local icon = row:CreateTexture(nil, "ARTWORK")
        icon:SetSize(28, 28)
        icon:SetPoint("LEFT", 8, 0)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row.icon = icon

        local ib = CreateFrame("Frame", nil, row, "BackdropTemplate")
        ib:SetSize(30, 30)
        ib:SetPoint("CENTER", icon, "CENTER")
        ib:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
        ib:SetBackdropBorderColor(unpack(T.border))
        row.iconBorder = ib

        -- Item name
        local nfs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        local fp = nfs:GetFont()
        nfs:SetFont(fp, RF.FONT_SIZES.normal, "")
        nfs:SetPoint("LEFT", icon, "RIGHT", 8, 0)
        nfs:SetWidth(350)
        nfs:SetJustifyH("LEFT")
        row.nameFS = nfs

        -- Standing requirement
        local rfs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        rfs:SetFont(fp, RF.FONT_SIZES.small, "")
        rfs:SetPoint("LEFT", row, "LEFT", 400, 0)
        rfs:SetWidth(90)
        rfs:SetJustifyH("CENTER")
        row.repFS = rfs

        -- Cost
        local cfs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        cfs:SetFont(fp, RF.FONT_SIZES.small, "")
        cfs:SetPoint("LEFT", row, "LEFT", 500, 0)
        cfs:SetWidth(100)
        cfs:SetJustifyH("RIGHT")
        cfs:SetTextColor(unpack(T.textSecondary))
        row.costFS = cfs

        -- Achievement
        local afs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        afs:SetFont(fp, RF.FONT_SIZES.small, "")
        afs:SetPoint("LEFT", row, "LEFT", 610, 0)
        afs:SetPoint("RIGHT", row, "RIGHT", -8, 0)
        afs:SetJustifyH("LEFT")
        afs:SetTextColor(unpack(T.textMuted))
        row.achFS = afs

        -- Hover: item tooltip + highlight, right-click to link in chat
        row:EnableMouse(true)
        row.itemID = nil
        row:SetScript("OnEnter", function(self)
            self.bg:SetColorTexture(unpack(T.bgRowHover))
            if self.itemID then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetItemByID(self.itemID)
                GameTooltip:Show()
            end
        end)
        row:SetScript("OnLeave", function(self)
            if self.rowIndex % 2 == 0 then
                self.bg:SetColorTexture(unpack(T.bgRowAlt))
            else
                self.bg:SetColorTexture(unpack(T.bgRow))
            end
            GameTooltip:Hide()
        end)
        row:SetScript("OnMouseUp", function(self, button)
            if button == "RightButton" and self.itemID then
                local _, link = GetItemInfo(self.itemID)
                if link and ChatEdit_InsertLink then
                    ChatEdit_InsertLink(link)
                end
            end
        end)

        rewardRows[idx] = row
        return row
    end

    ---------------------------------------------------------------------------
    -- Data
    ---------------------------------------------------------------------------
    local allPerkData = {}
    local allRewardData = {}
    local playerStanding = 0

    local function CollectData()
        allPerkData = {}
        allRewardData = {}

        -- Guild perks
        if GetNumGuildPerks and GetGuildPerkInfo then
            local numPerks = GetNumGuildPerks()
            for i = 1, numPerks do
                local name, spellID, iconTexture = GetGuildPerkInfo(i)
                if name then
                    tinsert(allPerkData, {
                        name = name,
                        spellID = spellID,
                        icon = iconTexture,
                        description = SafeGetSpellDescription(spellID),
                    })
                end
            end
        end

        -- Guild rewards
        if GetNumGuildRewards and GetGuildRewardInfo then
            local numRewards = GetNumGuildRewards()
            for i = 1, numRewards do
                local achievementID, itemID, itemName, iconTexture, repLevel, moneyCost = GetGuildRewardInfo(i)
                if itemID then
                    local achName = nil
                    if achievementID and achievementID > 0 and GetAchievementInfo then
                        achName = select(2, GetAchievementInfo(achievementID))
                    end
                    tinsert(allRewardData, {
                        achievementID = achievementID,
                        itemID = itemID,
                        itemName = itemName or ("Item #" .. itemID),
                        icon = iconTexture,
                        repLevel = repLevel or 0,
                        cost = moneyCost or 0,
                        achName = achName,
                    })
                end
            end
        end

        -- Sort: rep level asc, then name asc
        table.sort(allRewardData, function(a, b)
            if a.repLevel ~= b.repLevel then return a.repLevel < b.repLevel end
            return (a.itemName or "") < (b.itemName or "")
        end)

        -- Player's guild rep standing
        playerStanding = 0
        if C_Reputation and C_Reputation.GetGuildFactionData then
            local data = C_Reputation.GetGuildFactionData()
            if data then playerStanding = data.reaction or 0 end
        end
    end

    ---------------------------------------------------------------------------
    -- Refresh: Reputation
    ---------------------------------------------------------------------------
    local function RefreshRep()
        if not IsInGuild() then return end

        local guildName = GetGuildInfo("player") or "Unknown Guild"
        guildNameFS:SetText(guildName)

        local repData = nil
        if C_Reputation and C_Reputation.GetGuildFactionData then
            repData = C_Reputation.GetGuildFactionData()
        end

        if repData then
            local standing = repData.reaction or 0
            local label = STANDING_LABELS[standing] or "Unknown"
            local color = STANDING_COLORS[standing] or STANDING_COLORS[0]
            local current = repData.currentStanding or 0
            local minRep = repData.currentReactionThreshold or 0
            local maxRep = repData.nextReactionThreshold or 1
            local range = maxRep - minRep
            local progress = 0
            if range > 0 then progress = (current - minRep) / range end
            if standing == 8 then progress = 1 end -- Exalted = full bar

            local barWidth = repBarBg:GetWidth() - 2
            if barWidth < 1 then barWidth = CONTENT_WIDTH - 40 end
            repBarFill:SetWidth(math.max(barWidth * progress, 1))
            repBarFill:SetColorTexture(color[1], color[2], color[3], 0.8)

            if standing == 8 then
                repBarText:SetText(label)
            else
                repBarText:SetText(format("%s  -  %d / %d", label, current - minRep, range))
            end
            guildNameFS:SetTextColor(color[1], color[2], color[3], 1)
        else
            repBarFill:SetWidth(1)
            repBarText:SetText("Reputation data unavailable")
            guildNameFS:SetTextColor(unpack(T.textSecondary))
        end
    end

    ---------------------------------------------------------------------------
    -- Refresh: Perks
    ---------------------------------------------------------------------------
    local function RefreshPerksSection()
        for _, card in ipairs(perkCards) do card:Hide() end
        noPerksFS:Hide()

        if #allPerkData == 0 then
            noPerksFS:Show()
            perksSection:SetHeight(60)
            return
        end

        local sectionWidth = perksSection:GetWidth() - 24
        if sectionWidth < 100 then sectionWidth = CONTENT_WIDTH - 40 end
        local cardWidth = (sectionWidth - CARD_GAP_X * (CARD_COLS - 1)) / CARD_COLS
        local rows = math.ceil(#allPerkData / CARD_COLS)

        for i, data in ipairs(allPerkData) do
            local card = perkCards[i]
            if not card then
                card = CreatePerkCard()
                perkCards[i] = card
            end

            local col = (i - 1) % CARD_COLS
            local row = math.floor((i - 1) / CARD_COLS)

            card:ClearAllPoints()
            card:SetWidth(cardWidth)
            card:SetPoint("TOPLEFT", perksSection, "TOPLEFT",
                12 + col * (cardWidth + CARD_GAP_X),
                CARD_TOP - row * (CARD_HEIGHT + CARD_GAP_Y))

            card.icon:SetTexture(data.icon)
            card.nameFS:SetText(data.name or "Unknown Perk")
            card.descFS:SetText(data.description or "")
            card.spellID = data.spellID
            card:Show()
        end

        local totalH = -CARD_TOP + rows * (CARD_HEIGHT + CARD_GAP_Y) - CARD_GAP_Y + 12
        perksSection:SetHeight(totalH)
    end

    ---------------------------------------------------------------------------
    -- Refresh: Rewards
    ---------------------------------------------------------------------------
    local function RefreshRewards()
        -- Apply filter
        local filtered = {}
        local filterStanding = FILTER_STANDING[currentFilter] or 0
        for _, data in ipairs(allRewardData) do
            if currentFilter == "All" or data.repLevel == filterStanding then
                tinsert(filtered, data)
            end
        end

        -- Update count label
        rewardCountFS:SetText(format("(%d)", #filtered))

        -- Hide existing rows
        for _, row in pairs(rewardRows) do row:Hide() end
        noRewardsFS:Hide()

        if #filtered == 0 then
            noRewardsFS:Show()
            rewardContainer:SetHeight(40)
        else
            noRewardsFS:Hide()
            for i, data in ipairs(filtered) do
                local row = GetOrCreateRow(i)
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", 0, -(i - 1) * ROW_HEIGHT)
                row:SetPoint("RIGHT", rewardContainer, "RIGHT", 0, 0)

                -- Icon
                row.icon:SetTexture(data.icon)
                row.iconBorder:Show()

                -- Name - dim if player lacks rep
                row.nameFS:SetText(data.itemName)
                if playerStanding >= data.repLevel or data.repLevel == 0 then
                    row.nameFS:SetTextColor(unpack(T.textPrimary))
                else
                    row.nameFS:SetTextColor(T.textMuted[1], T.textMuted[2], T.textMuted[3], 0.7)
                end

                -- Standing requirement
                local standingLabel = STANDING_LABELS[data.repLevel] or ""
                local standingColor = STANDING_COLORS[data.repLevel] or STANDING_COLORS[0]
                row.repFS:SetText(standingLabel)
                if playerStanding >= data.repLevel then
                    row.repFS:SetTextColor(T.success[1], T.success[2], T.success[3], 1)
                else
                    row.repFS:SetTextColor(standingColor[1], standingColor[2], standingColor[3], 1)
                end

                -- Cost
                row.costFS:SetText(FormatGold(data.cost))

                -- Achievement
                row.achFS:SetText(data.achName or "")

                -- Row bg
                if i % 2 == 0 then
                    row.bg:SetColorTexture(unpack(T.bgRowAlt))
                else
                    row.bg:SetColorTexture(unpack(T.bgRow))
                end

                row.itemID = data.itemID
                row:Show()
            end
            rewardContainer:SetHeight(#filtered * ROW_HEIGHT)
        end

        -- Update rewards section height:
        -- title(26) + gap(6) + filterFrame(28) + gap(8) + headerFrame(20) + div(4) + rows + padding(12)
        local rowsH = math.max(#filtered, 1) * ROW_HEIGHT
        if #filtered == 0 then rowsH = 40 end
        rewardsSection:SetHeight(32 + 28 + 8 + 20 + 4 + rowsH + 12)

        -- Update total scroll content height
        local totalH = 8 + repSection:GetHeight() + 10
            + perksSection:GetHeight() + 10
            + rewardsSection:GetHeight() + 20
        content:SetHeight(totalH)
    end

    -- Wire up filter pill clicks
    for _, pill in ipairs(filterButtons) do
        pill:SetScript("OnClick", function(self)
            currentFilter = self.filterName
            UpdateFilterVisuals()
            RefreshRewards()
        end)
    end

    ---------------------------------------------------------------------------
    -- Master refresh
    ---------------------------------------------------------------------------
    local function RefreshAll()
        if not parent:IsShown() then return end

        if not IsInGuild() then
            noGuildFrame:Show()
            scrollFrame:Hide()
            return
        end
        noGuildFrame:Hide()
        scrollFrame:Show()

        -- Request guild rewards data from the server
        if RequestGuildRewards then RequestGuildRewards() end

        CollectData()
        RefreshRep()
        RefreshPerksSection()
        RefreshRewards()
    end

    ---------------------------------------------------------------------------
    -- Hooks
    ---------------------------------------------------------------------------
    RF:RegisterCallback("SWITCH_TAB", function(_, key)
        if key == "Perks" then
            RefreshAll()
        end
    end)

    RF:RegisterCallback("GUILD_ROSTER_UPDATE", function()
        if parent:IsShown() then
            RefreshAll()
        end
    end)

    RF:RegisterCallback("GUILD_REWARDS_LIST", function()
        if parent:IsShown() then
            CollectData()
            RefreshRewards()
        end
    end)

    RefreshAll()
end
