local addonName, RF = ...
local T

-------------------------------------------------------------------------------
-- Management Tab: Bulk actions, Rank viewer, Guild Recruitment
-- Since WoW 7.3+, most guild management actions require hardware events.
-- We generate macro text for users to copy/paste and execute.
-------------------------------------------------------------------------------

local selectedMembers = {}  -- name -> true
local filterResult = {}     -- filtered roster entries for bulk ops
local activeMode = "bulk"   -- "bulk", "recruit", "ranks", "bans", "audit", "alts"
local lastApplicantSnapshot = {}  -- name -> applicant data from last refresh
local selectedAltMain = nil       -- currently selected main in alt management

local function RecordApplicantHistory(data, outcome)
    local gdb = RF:GetGuildDB()
    if not gdb then return end
    tinsert(gdb.applicantHistory, 1, {
        name = data.name,
        classFile = data.classFile,
        className = data.className,
        specName = data.specName,
        level = data.level,
        ilvl = data.ilvl,
        message = data.message,
        appliedAt = data.appliedAt or time(),
        resolvedAt = time(),
        outcome = outcome,
    })
    while #gdb.applicantHistory > RF.MAX_APPLICANT_HISTORY do
        tremove(gdb.applicantHistory)
    end
end

function RF:InitManagementTab(parent)
    T = RF.Theme

    ---------------------------------------------------------------------------
    -- Header
    ---------------------------------------------------------------------------
    local header = RF:CreateLabel(parent, "Guild Management", RF.FONT_SIZES.medium, "textPrimary")
    header:SetPoint("TOPLEFT", 12, -8)

    ---------------------------------------------------------------------------
    -- Mode Toggle Pills
    ---------------------------------------------------------------------------
    local modeBar = CreateFrame("Frame", nil, parent)
    modeBar:SetHeight(24)
    modeBar:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -4)
    modeBar:SetPoint("RIGHT", parent, "RIGHT", -12, 0)

    local pillBulk = RF:CreatePillButton(modeBar, "Bulk Actions")
    pillBulk:SetPoint("LEFT", 0, 0)
    pillBulk:SetActive(true)

    local pillRecruit = RF:CreatePillButton(modeBar, "Recruitment")
    pillRecruit:SetPoint("LEFT", pillBulk, "RIGHT", 4, 0)

    local pillRanks = RF:CreatePillButton(modeBar, "Ranks")
    pillRanks:SetPoint("LEFT", pillRecruit, "RIGHT", 4, 0)

    local pillBans = RF:CreatePillButton(modeBar, "Ban List")
    pillBans:SetPoint("LEFT", pillRanks, "RIGHT", 4, 0)

    local pillAudit = RF:CreatePillButton(modeBar, "Audit Log")
    pillAudit:SetPoint("LEFT", pillBans, "RIGHT", 4, 0)

    local pillAlts = RF:CreatePillButton(modeBar, "Alt Mgmt")
    pillAlts:SetPoint("LEFT", pillAudit, "RIGHT", 4, 0)

    local pillNotes = RF:CreatePillButton(modeBar, "Notes")
    pillNotes:SetPoint("LEFT", pillAlts, "RIGHT", 4, 0)

    ---------------------------------------------------------------------------
    -- Container Frames (switched by mode pills)
    ---------------------------------------------------------------------------
    local bulkContainer = CreateFrame("Frame", nil, parent)
    bulkContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    bulkContainer:SetPoint("BOTTOMRIGHT", -12, 8)

    local recruitContainer = CreateFrame("Frame", nil, parent)
    recruitContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    recruitContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    recruitContainer:Hide()

    local ranksContainer = CreateFrame("Frame", nil, parent)
    ranksContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    ranksContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    ranksContainer:Hide()

    local bansContainer = CreateFrame("Frame", nil, parent)
    bansContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    bansContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    bansContainer:Hide()

    local auditContainer = CreateFrame("Frame", nil, parent)
    auditContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    auditContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    auditContainer:Hide()

    local altsContainer = CreateFrame("Frame", nil, parent)
    altsContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    altsContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    altsContainer:Hide()

    local notesContainer = CreateFrame("Frame", nil, parent)
    notesContainer:SetPoint("TOPLEFT", modeBar, "BOTTOMLEFT", 0, -6)
    notesContainer:SetPoint("BOTTOMRIGHT", -12, 8)
    notesContainer:Hide()

    parent.bulkContainer = bulkContainer
    parent.recruitContainer = recruitContainer
    parent.ranksContainer = ranksContainer
    parent.bansContainer = bansContainer
    parent.auditContainer = auditContainer
    parent.altsContainer = altsContainer
    parent.notesContainer = notesContainer

    -- Sync button (officer only, right-aligned on mode bar)
    local syncBtn = RF:CreateButton(modeBar, "Send Data", 90, 24, "danger")
    syncBtn:SetPoint("RIGHT", modeBar, "RIGHT", 0, 0)
    syncBtn:SetScript("OnClick", function()
        RF:ShowConfirmDialog("Send Data",
            "Push all RosterForge data to the guild?\nThis will sync alts, tags, notes, bans, and teams.",
            function()
                RF:SendFullSync()
            end)
    end)
    syncBtn:Hide()

    local allPills = { pillBulk, pillRecruit, pillRanks, pillBans, pillAudit, pillAlts, pillNotes }
    local allContainers = {
        bulk = bulkContainer, recruit = recruitContainer, ranks = ranksContainer,
        bans = bansContainer, audit = auditContainer, alts = altsContainer,
        notes = notesContainer,
    }

    -- Officer-only modes
    local OFFICER_MODES = { bulk = true, recruit = true, ranks = true, bans = true }
    local OFFICER_PILLS = { pillBulk, pillRecruit, pillRanks, pillBans }

    local function IsOfficer()
        if IsGuildLeader and IsGuildLeader() then return true end
        if C_GuildInfo and C_GuildInfo.CanEditOfficerNote and C_GuildInfo.CanEditOfficerNote() then return true end
        return false
    end

    local function UpdateOfficerVisibility()
        local isOff = IsOfficer()
        for _, pill in ipairs(OFFICER_PILLS) do
            if isOff then
                pill:SetAlpha(1)
                pill:EnableMouse(true)
            else
                pill:SetAlpha(0.35)
                pill:EnableMouse(false)
            end
        end
        syncBtn:SetShown(isOff)
    end

    local function SwitchMode(mode)
        if OFFICER_MODES[mode] and not IsOfficer() then return end
        activeMode = mode
        for key, c in pairs(allContainers) do
            c:SetShown(key == mode)
        end
        pillBulk:SetActive(mode == "bulk")
        pillRecruit:SetActive(mode == "recruit")
        pillRanks:SetActive(mode == "ranks")
        pillBans:SetActive(mode == "bans")
        pillAudit:SetActive(mode == "audit")
        pillAlts:SetActive(mode == "alts")
        pillNotes:SetActive(mode == "notes")
    end

    pillBulk:SetScript("OnClick", function() SwitchMode("bulk") end)
    pillRecruit:SetScript("OnClick", function() SwitchMode("recruit") end)
    pillRanks:SetScript("OnClick", function() SwitchMode("ranks") end)
    pillBans:SetScript("OnClick", function() SwitchMode("bans") end)
    pillAudit:SetScript("OnClick", function() SwitchMode("audit") end)
    pillAlts:SetScript("OnClick", function() SwitchMode("alts") end)
    pillNotes:SetScript("OnClick", function() SwitchMode("notes") end)

    -- Re-check officer status and refresh pill enable state + active mode.
    -- Called at init and whenever guild rank/permission state could have
    -- changed mid-session (a promoted officer could otherwise not click into
    -- gated panels, and a demoted one would keep access, until /reload).
    local function RefreshOfficerState()
        UpdateOfficerVisibility()
        if OFFICER_MODES[activeMode] and not IsOfficer() then
            SwitchMode("alts")
        end
    end

    RefreshOfficerState()

    RF:RegisterCallback("GUILD_RANKS_UPDATE", RefreshOfficerState)
    RF:RegisterCallback("PLAYER_GUILD_UPDATE", RefreshOfficerState)

    ---------------------------------------------------------------------------
    -- BULK ACTIONS MODE
    ---------------------------------------------------------------------------
    local subtext = RF:CreateLabel(bulkContainer,
        "Bulk actions generate macros for you to copy and execute (WoW restriction).",
        RF.FONT_SIZES.small, "textMuted")
    subtext:SetPoint("TOPLEFT", 0, 0)

    -- Filter Controls
    local filterFrame = CreateFrame("Frame", nil, bulkContainer, "BackdropTemplate")
    filterFrame:SetPoint("TOPLEFT", subtext, "BOTTOMLEFT", 0, -4)
    filterFrame:SetPoint("RIGHT", bulkContainer, "RIGHT", 0, 0)
    filterFrame:SetHeight(80)
    filterFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    filterFrame:SetBackdropColor(unpack(T.bgTertiary))
    filterFrame:SetBackdropBorderColor(unpack(T.border))

    -- Rank filter
    local rankLabel = RF:CreateLabel(filterFrame, "Rank:", RF.FONT_SIZES.small, "textSecondary")
    rankLabel:SetPoint("TOPLEFT", 8, -8)

    local rankItems = { { label = "All Ranks", value = nil } }
    local rankDropdown = RF:CreateDropdown(filterFrame, {
        width = 140,
        defaultText = "All Ranks",
        items = rankItems,
        onChanged = function(value)
            RF:UpdateBulkFilter(parent)
        end,
    })
    rankDropdown:SetPoint("LEFT", rankLabel, "RIGHT", 4, 0)
    parent.rankDropdown = rankDropdown

    -- Offline threshold
    local offlineLabel = RF:CreateLabel(filterFrame, "Offline >", RF.FONT_SIZES.small, "textSecondary")
    offlineLabel:SetPoint("LEFT", rankDropdown, "RIGHT", 12, 0)

    local offlineEB = CreateFrame("EditBox", nil, filterFrame, "BackdropTemplate")
    offlineEB:SetSize(40, 22)
    offlineEB:SetPoint("LEFT", offlineLabel, "RIGHT", 4, 0)
    offlineEB:SetFontObject("ChatFontNormal")
    offlineEB:SetFont(offlineEB:GetFont(), RF.FONT_SIZES.small, "")
    offlineEB:SetAutoFocus(false)
    offlineEB:SetMaxLetters(4)
    offlineEB:SetNumeric(true)
    offlineEB:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    offlineEB:SetBackdropColor(0.1, 0.1, 0.12, 1)
    offlineEB:SetBackdropBorderColor(unpack(T.border))
    offlineEB:SetTextColor(unpack(T.textPrimary))
    parent.offlineEB = offlineEB

    local daysLabel = RF:CreateLabel(filterFrame, "days", RF.FONT_SIZES.small, "textSecondary")
    daysLabel:SetPoint("LEFT", offlineEB, "RIGHT", 4, 0)

    local applyBtn = RF:CreateButton(filterFrame, "Apply Filter", 90, 24, "accent")
    applyBtn:SetPoint("TOPRIGHT", -8, -8)
    applyBtn:SetScript("OnClick", function()
        RF:UpdateBulkFilter(parent)
    end)

    local selectAllBtn = RF:CreateButton(filterFrame, "Select All", 75, 22)
    selectAllBtn:SetPoint("BOTTOMLEFT", 8, 8)
    selectAllBtn:SetScript("OnClick", function()
        for _, entry in ipairs(filterResult) do
            selectedMembers[entry.name] = true
        end
        RF:RefreshBulkList(parent)
    end)
    parent.selectAllBtn = selectAllBtn

    local selectNoneBtn = RF:CreateButton(filterFrame, "Select None", 80, 22)
    selectNoneBtn:SetPoint("LEFT", selectAllBtn, "RIGHT", 4, 0)
    selectNoneBtn:SetScript("OnClick", function()
        wipe(selectedMembers)
        RF:RefreshBulkList(parent)
    end)

    local countLabel = RF:CreateLabel(filterFrame, "0 selected", RF.FONT_SIZES.small, "textSecondary")
    countLabel:SetPoint("BOTTOMRIGHT", -8, 12)
    parent.countLabel = countLabel

    -- Member List
    local listFrame = RF:CreateScrollList(bulkContainer, {
        rowHeight = 22,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(22)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.check = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.check:SetFont(row.check:GetFont(), RF.FONT_SIZES.normal, "")
            row.check:SetPoint("LEFT", 6, 0)
            row.check:SetWidth(16)

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(row.nameText:GetFont(), RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", 26, 0)
            row.nameText:SetWidth(150)
            row.nameText:SetJustifyH("LEFT")

            row.rankText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.rankText:SetFont(row.rankText:GetFont(), RF.FONT_SIZES.small, "")
            row.rankText:SetPoint("LEFT", 180, 0)
            row.rankText:SetWidth(90)
            row.rankText:SetJustifyH("LEFT")
            row.rankText:SetTextColor(unpack(T.textSecondary))

            row.offlineText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.offlineText:SetFont(row.offlineText:GetFont(), RF.FONT_SIZES.small, "")
            row.offlineText:SetPoint("LEFT", 280, 0)
            row.offlineText:SetWidth(80)
            row.offlineText:SetJustifyH("RIGHT")
            row.offlineText:SetTextColor(unpack(T.textMuted))

            return row
        end,
        updateRow = function(row, data, index, isSelected)
            local checked = selectedMembers[data.name]
            row.check:SetText(checked and "|TInterface\\RAIDFRAME\\ReadyCheck-Ready:14|t" or "|cFF666666-|r")
            row.nameText:SetText(RF:GetClassColoredName(data.name, data.classFile))
            row.rankText:SetText(data.rankName)
            row.offlineText:SetText(data.lastOnlineText)
        end,
        onRowClick = function(row, data, button)
            if data then
                selectedMembers[data.name] = not selectedMembers[data.name] or nil
                RF:RefreshBulkList(parent)
            end
        end,
    })
    listFrame:SetPoint("TOPLEFT", filterFrame, "BOTTOMLEFT", 0, -4)
    listFrame:SetPoint("RIGHT", bulkContainer, "CENTER", -4, 0)
    listFrame:SetPoint("BOTTOM", bulkContainer, "BOTTOM", 0, 0)
    parent.bulkList = listFrame

    -- Action Panel (right side of bulk)
    local actionPanel = CreateFrame("Frame", nil, bulkContainer, "BackdropTemplate")
    actionPanel:SetPoint("TOPLEFT", listFrame, "TOPRIGHT", 4, 0)
    actionPanel:SetPoint("BOTTOMRIGHT", 0, 0)
    actionPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    actionPanel:SetBackdropColor(unpack(T.bgTertiary))
    actionPanel:SetBackdropBorderColor(unpack(T.border))

    local actionsHeader = RF:CreateLabel(actionPanel, "Bulk Actions", RF.FONT_SIZES.medium, "textPrimary")
    actionsHeader:SetPoint("TOPLEFT", 8, -8)

    local promoteBtn = RF:CreateButton(actionPanel, "Generate Promote Macro", 180, 26, "accent")
    promoteBtn:SetPoint("TOPLEFT", actionsHeader, "BOTTOMLEFT", 0, -8)
    promoteBtn:SetScript("OnClick", function() RF:GenerateBulkMacro("promote") end)

    local demoteBtn = RF:CreateButton(actionPanel, "Generate Demote Macro", 180, 26, "warning")
    demoteBtn:SetPoint("TOPLEFT", promoteBtn, "BOTTOMLEFT", 0, -4)
    demoteBtn:SetScript("OnClick", function()
        RF:ShowConfirmDialog("Generate Demote Macro",
            "Generate demote macro for all selected members?",
            function() RF:GenerateBulkMacro("demote") end)
    end)

    local kickBtn = RF:CreateButton(actionPanel, "Generate Kick Macro", 180, 26, "danger")
    kickBtn:SetPoint("TOPLEFT", demoteBtn, "BOTTOMLEFT", 0, -4)
    kickBtn:SetScript("OnClick", function()
        RF:ShowConfirmDialog("Generate Kick Macro",
            "Generate kick macro for all selected members?",
            function() RF:GenerateBulkMacro("kick") end)
    end)

    local exportBtn = RF:CreateButton(actionPanel, "Export Selected (CSV)", 180, 26, "accent")
    exportBtn:SetPoint("TOPLEFT", kickBtn, "BOTTOMLEFT", 0, -12)
    exportBtn:SetScript("OnClick", function() RF:ExportSelectedMembers("csv") end)

    local macroLabel = RF:CreateLabel(actionPanel, "Macro Output:", RF.FONT_SIZES.small, "textMuted")
    macroLabel:SetPoint("TOPLEFT", exportBtn, "BOTTOMLEFT", 0, -16)
    parent.macroLabel = macroLabel

    local macroScroll = CreateFrame("ScrollFrame", nil, actionPanel, "UIPanelScrollFrameTemplate")
    macroScroll:SetPoint("TOPLEFT", macroLabel, "BOTTOMLEFT", 0, -4)
    macroScroll:SetPoint("BOTTOMRIGHT", -24, 8)

    local macroEB = CreateFrame("EditBox", nil, macroScroll)
    macroEB:SetWidth(380)
    macroEB:SetFontObject("ChatFontNormal")
    macroEB:SetFont(macroEB:GetFont(), RF.FONT_SIZES.small, "")
    macroEB:SetTextColor(unpack(T.textPrimary))
    macroEB:SetMultiLine(true)
    macroEB:SetAutoFocus(false)
    macroEB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    macroScroll:SetScrollChild(macroEB)
    parent.macroEB = macroEB

    ---------------------------------------------------------------------------
    -- RANKS VIEWER MODE
    ---------------------------------------------------------------------------
    RF:InitRanksViewer(ranksContainer)

    ---------------------------------------------------------------------------
    -- RECRUITMENT MODE
    ---------------------------------------------------------------------------
    RF:InitRecruitmentPanel(recruitContainer)

    ---------------------------------------------------------------------------
    -- BAN LIST MODE
    ---------------------------------------------------------------------------
    RF:InitBanListPanel(bansContainer)

    ---------------------------------------------------------------------------
    -- AUDIT LOG MODE
    ---------------------------------------------------------------------------
    RF:InitAuditLogPanel(auditContainer)

    ---------------------------------------------------------------------------
    -- ALT MANAGEMENT MODE
    ---------------------------------------------------------------------------
    RF:InitAltManagementPanel(altsContainer)

    ---------------------------------------------------------------------------
    -- NOTES MANAGEMENT MODE
    ---------------------------------------------------------------------------
    RF:InitNotesManagementPanel(notesContainer)

    ---------------------------------------------------------------------------
    -- Hook tab switch
    ---------------------------------------------------------------------------
    RF:RegisterCallback("SWITCH_TAB", function(_, key)
        if key == "Management" then
            RefreshOfficerState()
            RF:PopulateRankDropdown(parent)
            RF:UpdateBulkFilter(parent)
            -- Request recruitment data
            local clubId = C_Club and C_Club.GetGuildClubId and C_Club.GetGuildClubId()
            if clubId and C_ClubFinder then
                if C_ClubFinder.RequestPostingInformationFromClubId then
                    C_ClubFinder.RequestPostingInformationFromClubId(clubId)
                end
                if C_ClubFinder.RequestApplicantList and Enum and Enum.ClubFinderRequestType then
                    C_ClubFinder.RequestApplicantList(Enum.ClubFinderRequestType.Guild)
                end
            end
            -- Refresh active mode's container
            local active = allContainers[activeMode]
            if active and active:IsShown() and active.Refresh then
                active.Refresh()
            end
        end
    end)

    RF:PopulateRankDropdown(parent)
    RF:UpdateBulkFilter(parent)
end

-------------------------------------------------------------------------------
-- Ranks Viewer (Feature 5)
-------------------------------------------------------------------------------
function RF:InitRanksViewer(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    local header = RF:CreateLabel(container, "Guild Rank Permissions (Read-Only)", RF.FONT_SIZES.medium, "textPrimary")
    header:SetPoint("TOPLEFT", 0, 0)

    -- Left: rank list
    local rankListBg = CreateFrame("Frame", nil, container, "BackdropTemplate")
    rankListBg:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -4)
    rankListBg:SetPoint("BOTTOMLEFT", 0, 0)
    rankListBg:SetWidth(130)
    rankListBg:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    rankListBg:SetBackdropColor(unpack(T.bgTertiary))
    rankListBg:SetBackdropBorderColor(unpack(T.border))

    -- Right: permission list
    local permBg = CreateFrame("Frame", nil, container, "BackdropTemplate")
    permBg:SetPoint("TOPLEFT", rankListBg, "TOPRIGHT", 4, 0)
    permBg:SetPoint("BOTTOMRIGHT", 0, 0)
    permBg:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    permBg:SetBackdropColor(unpack(T.bgTertiary))
    permBg:SetBackdropBorderColor(unpack(T.border))

    -- Permission labels (pre-create 21 rows)
    local permLabels = {}
    local permIndicators = {}
    local py = -8
    for i, flagName in ipairs(RF.PERMISSION_FLAGS) do
        local indicator = permBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        indicator:SetFont(fontPath, RF.FONT_SIZES.normal, "")
        indicator:SetPoint("TOPLEFT", 8, py)
        indicator:SetWidth(16)
        indicator:SetText("-")
        indicator:SetTextColor(unpack(T.textMuted))
        permIndicators[i] = indicator

        local label = permBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetFont(fontPath, RF.FONT_SIZES.small, "")
        label:SetPoint("LEFT", indicator, "RIGHT", 4, 0)
        label:SetTextColor(unpack(T.textSecondary))
        label:SetText(flagName)
        permLabels[i] = label

        py = py - 18
    end

    -- Selected rank title
    local selectedRankLabel = RF:CreateLabel(permBg, "Select a rank", RF.FONT_SIZES.medium, "textMuted")
    selectedRankLabel:SetPoint("TOP", 0, -8)
    selectedRankLabel:Hide()

    -- Build rank buttons
    container.rankButtons = {}
    local selectedRankBtn = nil

    local function ShowRankPermissions(rankIndex)
        local flags = {}
        if C_GuildInfo and C_GuildInfo.GuildControlGetRankFlags then
            flags = C_GuildInfo.GuildControlGetRankFlags(rankIndex)
        end

        for i = 1, #RF.PERMISSION_FLAGS do
            local enabled = flags[i]
            if enabled then
                permIndicators[i]:SetText("|cFF4ADE80+|r")
            else
                permIndicators[i]:SetText("|cFFFF6B6B-|r")
            end
        end
    end

    local function BuildRankList()
        -- Clear old buttons
        for _, btn in ipairs(container.rankButtons) do
            btn:Hide()
        end
        container.rankButtons = {}

        if not GuildControlGetNumRanks then return end
        local numRanks = GuildControlGetNumRanks()
        local ry = -8

        for i = 1, numRanks do
            local rankName = GuildControlGetRankName(i) or ("Rank " .. i)
            local btn = CreateFrame("Button", nil, rankListBg)
            btn:SetSize(118, 22)
            btn:SetPoint("TOPLEFT", 6, ry)

            local btnBg = btn:CreateTexture(nil, "BACKGROUND")
            btnBg:SetAllPoints()
            btnBg:SetColorTexture(0, 0, 0, 0)
            btn.bg = btnBg

            local btnText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            btnText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            btnText:SetAllPoints()
            btnText:SetJustifyH("LEFT")
            btnText:SetText(rankName)

            local rankColor = RF.RANK_COLORS[i - 1]
            if rankColor then
                btnText:SetTextColor(rankColor.r, rankColor.g, rankColor.b)
            else
                btnText:SetTextColor(unpack(T.textSecondary))
            end

            btn:SetScript("OnClick", function()
                -- Highlight selected
                if selectedRankBtn then
                    selectedRankBtn.bg:SetColorTexture(0, 0, 0, 0)
                end
                btnBg:SetColorTexture(T.accent[1], T.accent[2], T.accent[3], 0.2)
                selectedRankBtn = btn

                ShowRankPermissions(i)
            end)

            btn:SetScript("OnEnter", function()
                if btn ~= selectedRankBtn then
                    btnBg:SetColorTexture(0.2, 0.2, 0.25, 0.5)
                end
            end)
            btn:SetScript("OnLeave", function()
                if btn ~= selectedRankBtn then
                    btnBg:SetColorTexture(0, 0, 0, 0)
                end
            end)

            tinsert(container.rankButtons, btn)
            ry = ry - 24
        end
    end

    -- Build on first show
    container:SetScript("OnShow", function()
        BuildRankList()
    end)
end

-------------------------------------------------------------------------------
-- Recruitment Panel (Feature 7)
-------------------------------------------------------------------------------
function RF:InitRecruitmentPanel(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    local header = RF:CreateLabel(container, "Guild Recruitment", RF.FONT_SIZES.medium, "textPrimary")
    header:SetPoint("TOPLEFT", 0, 0)

    -- Current posting section
    local postingHeader = RF:CreateLabel(container, "Current Posting", RF.FONT_SIZES.normal, "textSecondary")
    postingHeader:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -8)

    local postingBg = CreateFrame("Frame", nil, container, "BackdropTemplate")
    postingBg:SetPoint("TOPLEFT", postingHeader, "BOTTOMLEFT", 0, -4)
    postingBg:SetPoint("RIGHT", container, "RIGHT", 0, 0)
    postingBg:SetHeight(80)
    postingBg:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    postingBg:SetBackdropColor(unpack(T.bgTertiary))
    postingBg:SetBackdropBorderColor(unpack(T.border))

    local postingText = postingBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    postingText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    postingText:SetPoint("TOPLEFT", 8, -8)
    postingText:SetPoint("BOTTOMRIGHT", -8, 8)
    postingText:SetJustifyH("LEFT")
    postingText:SetJustifyV("TOP")
    postingText:SetTextColor(unpack(T.textSecondary))
    postingText:SetWordWrap(true)
    postingText:SetText("No recruitment posting found.")
    container.postingText = postingText

    -- Applicants section
    local applicantHeader = RF:CreateLabel(container, "Applicants", RF.FONT_SIZES.normal, "textSecondary")
    applicantHeader:SetPoint("TOPLEFT", postingBg, "BOTTOMLEFT", 0, -8)

    -- Column header for applicant list
    local colHeader = CreateFrame("Frame", nil, container, "BackdropTemplate")
    colHeader:SetHeight(20)
    colHeader:SetPoint("TOPLEFT", applicantHeader, "BOTTOMLEFT", 0, -4)
    colHeader:SetPoint("RIGHT", container, "RIGHT", 0, 0)
    colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    colHeader:SetBackdropColor(unpack(T.bgSecondary))

    local colLabels = { { "Name", 8, 130 }, { "Lvl", 142, 28 }, { "Class", 174, 70 }, { "Spec", 248, 80 }, { "iLvl", 332, 40 }, { "Note", 376, 200 } }
    for _, col in ipairs(colLabels) do
        local lbl = colHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetFont(lbl:GetFont(), RF.FONT_SIZES.small, "")
        lbl:SetPoint("LEFT", col[2], 0)
        lbl:SetWidth(col[3])
        lbl:SetText(col[1])
        lbl:SetTextColor(unpack(T.textSecondary))
        lbl:SetJustifyH("LEFT")
    end

    local applicantList = RF:CreateScrollList(container, {
        rowHeight = 28,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(28)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(row.nameText:GetFont(), RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", 8, 0)
            row.nameText:SetWidth(130)
            row.nameText:SetJustifyH("LEFT")

            row.levelText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.levelText:SetFont(row.levelText:GetFont(), RF.FONT_SIZES.small, "")
            row.levelText:SetPoint("LEFT", 142, 0)
            row.levelText:SetWidth(28)
            row.levelText:SetTextColor(unpack(T.textSecondary))
            row.levelText:SetJustifyH("CENTER")

            row.classText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.classText:SetFont(row.classText:GetFont(), RF.FONT_SIZES.small, "")
            row.classText:SetPoint("LEFT", 174, 0)
            row.classText:SetWidth(70)
            row.classText:SetJustifyH("LEFT")

            row.specText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.specText:SetFont(row.specText:GetFont(), RF.FONT_SIZES.small, "")
            row.specText:SetPoint("LEFT", 248, 0)
            row.specText:SetWidth(80)
            row.specText:SetJustifyH("LEFT")
            row.specText:SetTextColor(unpack(T.textSecondary))

            row.ilvlText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.ilvlText:SetFont(row.ilvlText:GetFont(), RF.FONT_SIZES.small, "")
            row.ilvlText:SetPoint("LEFT", 332, 0)
            row.ilvlText:SetWidth(40)
            row.ilvlText:SetJustifyH("CENTER")
            row.ilvlText:SetTextColor(unpack(T.textSecondary))

            row.noteText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.noteText:SetFont(row.noteText:GetFont(), RF.FONT_SIZES.small, "")
            row.noteText:SetPoint("LEFT", 376, 0)
            row.noteText:SetPoint("RIGHT", -120, 0)
            row.noteText:SetJustifyH("LEFT")
            row.noteText:SetTextColor(unpack(T.textMuted))

            row.acceptBtn = RF:CreateButton(row, "Accept", 50, 20, "accent")
            row.acceptBtn:SetPoint("RIGHT", -56, 0)

            row.declineBtn = RF:CreateButton(row, "Decline", 50, 20, "danger")
            row.declineBtn:SetPoint("RIGHT", -2, 0)

            return row
        end,
        updateRow = function(row, data, index, isSelected)
            -- Class-colored name
            if data.classFile then
                row.nameText:SetText(RF:GetClassColoredName(data.name, data.classFile))
            else
                row.nameText:SetText(data.name or "Unknown")
            end

            row.levelText:SetText(data.level or "")

            -- Class name with color
            if data.classFile then
                local cc = RF:GetClassColor(data.classFile)
                row.classText:SetText(data.className or "")
                row.classText:SetTextColor(cc.r, cc.g, cc.b)
            else
                row.classText:SetText("-")
                row.classText:SetTextColor(unpack(T.textMuted))
            end

            -- Spec
            row.specText:SetText(data.specName or "-")

            -- Item level
            if data.ilvl and data.ilvl > 0 then
                row.ilvlText:SetText(tostring(data.ilvl))
            else
                row.ilvlText:SetText("-")
            end

            -- Note (truncated in row, full in tooltip)
            row.noteText:SetText(data.message or "")

            row.acceptBtn:SetScript("OnClick", function()
                if C_ClubFinder and C_ClubFinder.RespondToApplicant and data.clubFinderGUID and data.playerGUID then
                    RecordApplicantHistory(data, "accepted")
                    lastApplicantSnapshot[data.name] = nil
                    C_ClubFinder.RespondToApplicant(data.clubFinderGUID, data.playerGUID, true, Enum.ClubFinderRequestType.Guild, data.name or "", false)
                    RF:Print("Accepted applicant: " .. (data.name or "Unknown"))
                    if container.RefreshHistory then container.RefreshHistory() end
                end
            end)

            row.declineBtn:SetScript("OnClick", function()
                if C_ClubFinder and C_ClubFinder.RespondToApplicant and data.clubFinderGUID and data.playerGUID then
                    RecordApplicantHistory(data, "declined")
                    lastApplicantSnapshot[data.name] = nil
                    C_ClubFinder.RespondToApplicant(data.clubFinderGUID, data.playerGUID, false, Enum.ClubFinderRequestType.Guild, data.name or "", false)
                    RF:Print("Declined applicant: " .. (data.name or "Unknown"))
                    if container.RefreshHistory then container.RefreshHistory() end
                end
            end)
        end,
        onRowEnter = function(row, data)
            if not data then return end
            GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
            GameTooltip:AddLine(data.name or "Unknown", 1, 1, 1)
            if data.className then
                GameTooltip:AddLine(format("Level %s %s", data.level or "?", data.className), 0.7, 0.7, 0.7)
            end
            if data.specName then
                GameTooltip:AddLine("Spec: " .. data.specName, 0.7, 0.7, 0.7)
            end
            if data.ilvl and data.ilvl > 0 then
                GameTooltip:AddLine("Item Level: " .. data.ilvl, 0.7, 0.7, 0.7)
            end
            if data.message and data.message ~= "" then
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Note: " .. data.message, 0.6, 0.8, 0.6, true)
            end
            GameTooltip:Show()
        end,
        onRowLeave = function()
            GameTooltip:Hide()
        end,
    })
    applicantList:SetPoint("TOPLEFT", colHeader, "BOTTOMLEFT", 0, -1)
    applicantList:SetPoint("RIGHT", container, "RIGHT", 0, 0)
    applicantList:SetHeight(150)
    container.applicantList = applicantList

    ---------------------------------------------------------------------------
    -- Applicant History Section
    ---------------------------------------------------------------------------
    local historyHeader = RF:CreateLabel(container, "Applicant History", RF.FONT_SIZES.normal, "textSecondary")
    historyHeader:SetPoint("TOPLEFT", applicantList, "BOTTOMLEFT", 0, -8)

    local clearHistBtn = RF:CreateButton(container, "Clear History", 90, 20, "danger")
    clearHistBtn:SetPoint("RIGHT", container, "RIGHT", 0, 0)
    clearHistBtn:SetPoint("TOP", historyHeader, "TOP", 0, 2)
    clearHistBtn:SetScript("OnClick", function()
        local gdb = RF:GetGuildDB()
        if gdb then
            wipe(gdb.applicantHistory)
        end
        if container.RefreshHistory then container.RefreshHistory() end
    end)

    -- History column headers
    local histColHeader = CreateFrame("Frame", nil, container, "BackdropTemplate")
    histColHeader:SetHeight(20)
    histColHeader:SetPoint("TOPLEFT", historyHeader, "BOTTOMLEFT", 0, -4)
    histColHeader:SetPoint("RIGHT", container, "RIGHT", 0, 0)
    histColHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    histColHeader:SetBackdropColor(unpack(T.bgSecondary))

    local histColDefs = {
        { "Date", 8, 70 }, { "Name", 82, 120 }, { "Class", 206, 70 },
        { "Spec", 280, 80 }, { "iLvl", 364, 40 }, { "Outcome", 408, 70 },
    }
    for _, col in ipairs(histColDefs) do
        local lbl = histColHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetFont(lbl:GetFont(), RF.FONT_SIZES.small, "")
        lbl:SetPoint("LEFT", col[2], 0)
        lbl:SetWidth(col[3])
        lbl:SetText(col[1])
        lbl:SetTextColor(unpack(T.textSecondary))
        lbl:SetJustifyH("LEFT")
    end

    local OUTCOME_COLORS = {
        accepted = { 0.30, 0.90, 0.30 },
        declined = { 0.90, 0.30, 0.30 },
        expired  = { 0.50, 0.50, 0.50 },
    }

    local historyList = RF:CreateScrollList(container, {
        rowHeight = 22,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(22)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.dateText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.dateText:SetFont(row.dateText:GetFont(), RF.FONT_SIZES.small, "")
            row.dateText:SetPoint("LEFT", 8, 0)
            row.dateText:SetWidth(70)
            row.dateText:SetJustifyH("LEFT")
            row.dateText:SetTextColor(unpack(T.textMuted))

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(row.nameText:GetFont(), RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", 82, 0)
            row.nameText:SetWidth(120)
            row.nameText:SetJustifyH("LEFT")

            row.classText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.classText:SetFont(row.classText:GetFont(), RF.FONT_SIZES.small, "")
            row.classText:SetPoint("LEFT", 206, 0)
            row.classText:SetWidth(70)
            row.classText:SetJustifyH("LEFT")

            row.specText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.specText:SetFont(row.specText:GetFont(), RF.FONT_SIZES.small, "")
            row.specText:SetPoint("LEFT", 280, 0)
            row.specText:SetWidth(80)
            row.specText:SetJustifyH("LEFT")
            row.specText:SetTextColor(unpack(T.textSecondary))

            row.ilvlText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.ilvlText:SetFont(row.ilvlText:GetFont(), RF.FONT_SIZES.small, "")
            row.ilvlText:SetPoint("LEFT", 364, 0)
            row.ilvlText:SetWidth(40)
            row.ilvlText:SetJustifyH("CENTER")
            row.ilvlText:SetTextColor(unpack(T.textSecondary))

            row.outcomeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.outcomeText:SetFont(row.outcomeText:GetFont(), RF.FONT_SIZES.small, "")
            row.outcomeText:SetPoint("LEFT", 408, 0)
            row.outcomeText:SetWidth(70)
            row.outcomeText:SetJustifyH("LEFT")

            return row
        end,
        updateRow = function(row, data, index, isSelected)
            row.dateText:SetText(RF:FormatTimestamp(data.resolvedAt))

            if data.classFile then
                row.nameText:SetText(RF:GetClassColoredName(data.name, data.classFile))
                local cc = RF:GetClassColor(data.classFile)
                row.classText:SetText(data.className or "")
                row.classText:SetTextColor(cc.r, cc.g, cc.b)
            else
                row.nameText:SetText(data.name or "Unknown")
                row.classText:SetText("-")
                row.classText:SetTextColor(unpack(T.textMuted))
            end

            row.specText:SetText(data.specName or "-")

            if data.ilvl and data.ilvl > 0 then
                row.ilvlText:SetText(tostring(data.ilvl))
            else
                row.ilvlText:SetText("-")
            end

            local outcomeLabel = (data.outcome == "accepted" and "Accepted")
                or (data.outcome == "declined" and "Declined")
                or "Expired"
            local outcomeColor = OUTCOME_COLORS[data.outcome] or OUTCOME_COLORS.expired
            row.outcomeText:SetText(outcomeLabel)
            row.outcomeText:SetTextColor(unpack(outcomeColor))
        end,
        onRowEnter = function(row, data)
            if not data then return end
            GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
            GameTooltip:AddLine(data.name or "Unknown", 1, 1, 1)
            if data.className then
                GameTooltip:AddLine(format("Level %s %s", data.level or "?", data.className), 0.7, 0.7, 0.7)
            end
            if data.specName then
                GameTooltip:AddLine("Spec: " .. data.specName, 0.7, 0.7, 0.7)
            end
            if data.ilvl and data.ilvl > 0 then
                GameTooltip:AddLine("Item Level: " .. data.ilvl, 0.7, 0.7, 0.7)
            end
            if data.message and data.message ~= "" then
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Note: " .. data.message, 0.6, 0.8, 0.6, true)
            end
            if data.resolvedAt then
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Resolved: " .. RF:FormatTimestamp(data.resolvedAt), 0.5, 0.5, 0.5)
            end
            GameTooltip:Show()
        end,
        onRowLeave = function()
            GameTooltip:Hide()
        end,
    })
    historyList:SetPoint("TOPLEFT", histColHeader, "BOTTOMLEFT", 0, -1)
    historyList:SetPoint("BOTTOMRIGHT", 0, 0)
    container.historyList = historyList

    function container.RefreshHistory()
        local gdb = RF:GetGuildDB()
        local sorted = {}
        if gdb and gdb.applicantHistory then
            for _, entry in ipairs(gdb.applicantHistory) do
                tinsert(sorted, entry)
            end
        end
        -- Already stored newest-first from tinsert(_, 1, ...), no sort needed
        historyList:SetData(sorted)
    end

    -- Refresh posting info
    local function RefreshPostingInfo()
        local clubId = C_Club and C_Club.GetGuildClubId and C_Club.GetGuildClubId()
        if not clubId or not C_ClubFinder then
            container.postingText:SetText("Guild recruitment unavailable.")
            return
        end

        local info
        if C_ClubFinder.GetRecruitingClubInfoFromClubID then
            info = C_ClubFinder.GetRecruitingClubInfoFromClubID(clubId)
        end

        if info then
            local lines = {}
            if info.name and info.name ~= "" then
                tinsert(lines, "|cFF3B82F6" .. info.name .. "|r")
            end
            if info.comment and info.comment ~= "" then
                tinsert(lines, info.comment)
            end
            local details = {}
            if info.numActiveMembers then
                tinsert(details, "Members: " .. info.numActiveMembers)
            end
            if info.minILvl and info.minILvl > 0 then
                tinsert(details, "Min iLvl: " .. info.minILvl)
            end
            if #details > 0 then
                tinsert(lines, table.concat(details, "  |  "))
            end
            container.postingText:SetText(#lines > 0 and table.concat(lines, "\n") or "No posting details available.")
        else
            container.postingText:SetText("No recruitment posting found.\nCreate one via the default Guild & Communities UI.")
        end
    end

    -- Refresh applicant list
    local function RefreshApplicants()
        local applicants = {}
        local clubId = C_Club and C_Club.GetGuildClubId and C_Club.GetGuildClubId()
        if clubId and C_ClubFinder and C_ClubFinder.ReturnClubApplicantList then
            local list = C_ClubFinder.ReturnClubApplicantList(clubId)
            if list then
                for _, info in ipairs(list) do
                    -- Resolve class from classID
                    local className, classFile
                    if info.classID and GetClassInfo then
                        className, classFile = GetClassInfo(info.classID)
                    end

                    -- Resolve primary spec from specIds
                    local specName
                    if info.specIds and #info.specIds > 0 and GetSpecializationInfoByID then
                        local _, name = GetSpecializationInfoByID(info.specIds[1])
                        specName = name
                    end

                    local appName = info.name or "Unknown"
                    local appliedAt = lastApplicantSnapshot[appName]
                        and lastApplicantSnapshot[appName].appliedAt or time()

                    tinsert(applicants, {
                        name = appName,
                        level = info.level,
                        className = className,
                        classFile = classFile,
                        specName = specName,
                        ilvl = info.ilvl,
                        message = info.message or "",
                        clubFinderGUID = info.clubFinderGUID,
                        playerGUID = info.playerGUID,
                        appliedAt = appliedAt,
                    })
                end
            end
        end

        -- Auto-detect outcomes for applicants that disappeared
        local currentNames = {}
        for _, app in ipairs(applicants) do
            currentNames[app.name] = true
        end
        for name, snapData in pairs(lastApplicantSnapshot) do
            if not currentNames[name] then
                local outcome = "expired"
                if RF.rosterByName and RF.rosterByName[name] then
                    outcome = "accepted"
                end
                RecordApplicantHistory(snapData, outcome)
            end
        end

        -- Update snapshot with current applicants
        wipe(lastApplicantSnapshot)
        for _, app in ipairs(applicants) do
            lastApplicantSnapshot[app.name] = app
        end

        container.applicantList:SetData(applicants)
        if container.RefreshHistory then container.RefreshHistory() end
    end

    -- Hook callbacks — always refresh data so it's ready when panel is shown
    RF:RegisterCallback("CLUB_FINDER_RECRUIT_LIST_CHANGED", function()
        RefreshPostingInfo()
    end)

    RF:RegisterCallback("CLUB_FINDER_RECRUITS_UPDATED", function()
        RefreshApplicants()
    end)

    RF:RegisterCallback("CLUB_FINDER_APPLICATIONS_UPDATED", function()
        RefreshApplicants()
    end)

    container:SetScript("OnShow", function()
        -- Request fresh data each time the panel is shown
        local clubId = C_Club and C_Club.GetGuildClubId and C_Club.GetGuildClubId()
        if clubId and C_ClubFinder then
            if C_ClubFinder.RequestPostingInformationFromClubId then
                C_ClubFinder.RequestPostingInformationFromClubId(clubId)
            end
            if C_ClubFinder.RequestApplicantList and Enum and Enum.ClubFinderRequestType then
                C_ClubFinder.RequestApplicantList(Enum.ClubFinderRequestType.Guild)
            end
        end
        RefreshPostingInfo()
        RefreshApplicants()
        if container.RefreshHistory then container.RefreshHistory() end
    end)
end

-------------------------------------------------------------------------------
-- Ban List Panel
-------------------------------------------------------------------------------
function RF:InitBanListPanel(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    -- Toolbar
    local toolbar = CreateFrame("Frame", nil, container)
    toolbar:SetHeight(26)
    toolbar:SetPoint("TOPLEFT", 0, 0)
    toolbar:SetPoint("RIGHT", 0, 0)

    local searchBar = RF:CreateSearchBar(toolbar, {
        width = 180,
        placeholder = "Search bans...",
        onTextChanged = function() if container.Refresh then container.Refresh() end end,
    })
    searchBar:SetPoint("LEFT", 0, 0)

    local banCountLabel = RF:CreateLabel(toolbar, "0 bans", RF.FONT_SIZES.small, "textMuted")
    banCountLabel:SetPoint("LEFT", searchBar, "RIGHT", 8, 0)

    local addBanBtn = RF:CreateButton(toolbar, "Add Ban", 80, 24, "accent")
    addBanBtn:SetPoint("RIGHT", 0, 0)

    -- Column headers
    local colHeader = CreateFrame("Frame", nil, container, "BackdropTemplate")
    colHeader:SetHeight(20)
    colHeader:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -4)
    colHeader:SetPoint("RIGHT", 0, 0)
    colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    colHeader:SetBackdropColor(unpack(T.bgSecondary))

    local banColDefs = {
        { "Name", 8, 140 }, { "Reason", 152, 200 }, { "Added By", 356, 100 }, { "Date", 460, 70 },
    }
    for _, col in ipairs(banColDefs) do
        local lbl = colHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetFont(fontPath, RF.FONT_SIZES.small, "")
        lbl:SetPoint("LEFT", col[2], 0)
        lbl:SetWidth(col[3])
        lbl:SetText(col[1])
        lbl:SetTextColor(unpack(T.textSecondary))
        lbl:SetJustifyH("LEFT")
    end

    -- Scroll list
    local banList = RF:CreateScrollList(container, {
        rowHeight = 24,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(24)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", 8, 0)
            row.nameText:SetWidth(140)
            row.nameText:SetJustifyH("LEFT")

            row.reasonText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.reasonText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.reasonText:SetPoint("LEFT", 152, 0)
            row.reasonText:SetWidth(200)
            row.reasonText:SetJustifyH("LEFT")
            row.reasonText:SetTextColor(unpack(T.textSecondary))

            row.addedByText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.addedByText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.addedByText:SetPoint("LEFT", 356, 0)
            row.addedByText:SetWidth(100)
            row.addedByText:SetJustifyH("LEFT")
            row.addedByText:SetTextColor(unpack(T.textMuted))

            row.dateText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.dateText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.dateText:SetPoint("LEFT", 460, 0)
            row.dateText:SetWidth(70)
            row.dateText:SetJustifyH("LEFT")
            row.dateText:SetTextColor(unpack(T.textMuted))

            row.unbanBtn = RF:CreateButton(row, "Unban", 50, 20, "danger")
            row.unbanBtn:SetPoint("RIGHT", -4, 0)

            return row
        end,
        updateRow = function(row, data, index, isSelected)
            local member = RF:GetMemberByName(data.name)
            if member then
                row.nameText:SetText(RF:GetClassColoredName(data.name, member.classFile))
            else
                row.nameText:SetText(data.name)
                row.nameText:SetTextColor(unpack(T.textPrimary))
            end
            row.reasonText:SetText(data.reason or "")
            row.addedByText:SetText(data.addedBy or "")
            row.dateText:SetText(RF:FormatTimestamp(data.time))

            row.unbanBtn:SetScript("OnClick", function()
                RF:ShowConfirmDialog("Remove Ban",
                    "Remove " .. data.name .. " from the ban list?",
                    function()
                        RF:RemoveBan(data.name)
                        container.Refresh()
                    end)
            end)
        end,
        onRowClick = function(row, data, button)
            if button == "RightButton" and data then
                RF:ShowContextMenu(row, {
                    { text = "Copy Name", func = function() RF:ShowCopyPopup(data.name) end },
                    { text = "Remove Ban", func = function()
                        RF:ShowConfirmDialog("Remove Ban",
                            "Remove " .. data.name .. " from the ban list?",
                            function()
                                RF:RemoveBan(data.name)
                                container.Refresh()
                            end)
                    end },
                })
            end
        end,
        onRowEnter = function(row, data)
            if not data then return end
            GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
            GameTooltip:AddLine(data.name, 1, 1, 1)
            if data.reason and data.reason ~= "" then
                GameTooltip:AddLine("Reason: " .. data.reason, 0.7, 0.7, 0.7, true)
            end
            if data.addedBy then
                GameTooltip:AddLine("Added by: " .. data.addedBy, 0.5, 0.5, 0.5)
            end
            if data.time then
                GameTooltip:AddLine("Date: " .. date("%Y-%m-%d %H:%M", data.time), 0.5, 0.5, 0.5)
            end
            GameTooltip:Show()
        end,
        onRowLeave = function() GameTooltip:Hide() end,
    })
    banList:SetPoint("TOPLEFT", colHeader, "BOTTOMLEFT", 0, -1)
    banList:SetPoint("RIGHT", 0, 0)
    banList:SetPoint("BOTTOM", 0, 130)
    container.banList = banList

    -- Rejoin Alerts Section
    local rejoinHeader = RF:CreateLabel(container, "Rejoin Alerts", RF.FONT_SIZES.normal, "textSecondary")
    rejoinHeader:SetPoint("TOPLEFT", banList, "BOTTOMLEFT", 0, -8)

    local rejoinList = RF:CreateScrollList(container, {
        rowHeight = 22,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(22)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.msgText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.msgText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.msgText:SetPoint("LEFT", 8, 0)
            row.msgText:SetPoint("RIGHT", -80, 0)
            row.msgText:SetJustifyH("LEFT")
            row.msgText:SetTextColor(unpack(T.danger))

            row.dateText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.dateText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.dateText:SetPoint("RIGHT", -4, 0)
            row.dateText:SetWidth(70)
            row.dateText:SetJustifyH("RIGHT")
            row.dateText:SetTextColor(unpack(T.textMuted))

            return row
        end,
        updateRow = function(row, data)
            row.msgText:SetText(data.msg or "")
            row.dateText:SetText(RF:FormatTimestamp(data.time))
        end,
    })
    rejoinList:SetPoint("TOPLEFT", rejoinHeader, "BOTTOMLEFT", 0, -4)
    rejoinList:SetPoint("BOTTOMRIGHT", 0, 0)
    container.rejoinList = rejoinList

    -- Add Ban popup
    addBanBtn:SetScript("OnClick", function()
        if not container._addBanDialog then
            local f = RF:CreateStyledFrame("RosterForgeBanDialog", UIParent, 320, 160, {
                title = "Add Ban",
                closable = true,
                strata = "DIALOG",
            })
            f:SetPoint("CENTER")
            f:Hide()

            local nameLabel = RF:CreateLabel(f, "Character Name:", RF.FONT_SIZES.small, "textSecondary")
            nameLabel:SetPoint("TOPLEFT", f.titleBar, "BOTTOMLEFT", 12, -10)

            local nameEB = CreateFrame("EditBox", nil, f, "BackdropTemplate")
            nameEB:SetSize(290, 22)
            nameEB:SetPoint("TOPLEFT", nameLabel, "BOTTOMLEFT", 0, -2)
            nameEB:SetFontObject("ChatFontNormal")
            nameEB:SetFont(nameEB:GetFont(), RF.FONT_SIZES.normal, "")
            nameEB:SetAutoFocus(false)
            nameEB:SetMaxLetters(50)
            nameEB:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
            })
            nameEB:SetBackdropColor(0.1, 0.1, 0.12, 1)
            nameEB:SetBackdropBorderColor(unpack(T.border))
            nameEB:SetTextColor(unpack(T.textPrimary))
            nameEB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
            f.nameEB = nameEB

            local reasonLabel = RF:CreateLabel(f, "Reason:", RF.FONT_SIZES.small, "textSecondary")
            reasonLabel:SetPoint("TOPLEFT", nameEB, "BOTTOMLEFT", 0, -6)

            local reasonEB = CreateFrame("EditBox", nil, f, "BackdropTemplate")
            reasonEB:SetSize(290, 22)
            reasonEB:SetPoint("TOPLEFT", reasonLabel, "BOTTOMLEFT", 0, -2)
            reasonEB:SetFontObject("ChatFontNormal")
            reasonEB:SetFont(reasonEB:GetFont(), RF.FONT_SIZES.normal, "")
            reasonEB:SetAutoFocus(false)
            reasonEB:SetMaxLetters(100)
            reasonEB:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
            })
            reasonEB:SetBackdropColor(0.1, 0.1, 0.12, 1)
            reasonEB:SetBackdropBorderColor(unpack(T.border))
            reasonEB:SetTextColor(unpack(T.textPrimary))
            reasonEB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
            f.reasonEB = reasonEB

            local confirmBtn = RF:CreateButton(f, "Add Ban", 90, 26, "accent")
            confirmBtn:SetPoint("BOTTOMRIGHT", -12, 12)
            confirmBtn:SetScript("OnClick", function()
                local name = strtrim(nameEB:GetText())
                if name == "" then return end
                local reason = strtrim(reasonEB:GetText())
                RF:AddBan(name, reason ~= "" and reason or nil)
                nameEB:SetText("")
                reasonEB:SetText("")
                f:Hide()
                container.Refresh()
            end)

            local cancelBtn = RF:CreateButton(f, "Cancel", 90, 26)
            cancelBtn:SetPoint("RIGHT", confirmBtn, "LEFT", -4, 0)
            cancelBtn:SetScript("OnClick", function() f:Hide() end)

            container._addBanDialog = f
        end

        container._addBanDialog.nameEB:SetText("")
        container._addBanDialog.reasonEB:SetText("")
        container._addBanDialog:Show()
        container._addBanDialog.nameEB:SetFocus()
    end)

    -- Refresh function
    function container.Refresh()
        local banData = RF:GetBanList()
        local search = searchBar:GetText():lower()
        local sorted = {}
        for name, info in pairs(banData) do
            local entry = { name = name, reason = info.reason, addedBy = info.addedBy, time = info.time }
            if search == "" or name:lower():find(search, 1, true) or (info.reason and info.reason:lower():find(search, 1, true)) then
                tinsert(sorted, entry)
            end
        end
        table.sort(sorted, function(a, b) return (a.time or 0) > (b.time or 0) end)
        banList:SetData(sorted)
        banCountLabel:SetText(#sorted .. " ban" .. (#sorted ~= 1 and "s" or ""))

        -- Rejoin alerts
        local guildDB = RF:GetGuildDB()
        local alerts = {}
        if guildDB and guildDB.tracking and guildDB.tracking.log then
            for _, entry in ipairs(guildDB.tracking.log) do
                if entry.type == "BAN_REJOIN" then
                    tinsert(alerts, entry)
                end
            end
        end
        -- Reverse for newest first
        local reversed = {}
        for i = #alerts, 1, -1 do
            tinsert(reversed, alerts[i])
        end
        rejoinList:SetData(reversed)
    end

    container:SetScript("OnShow", function() container.Refresh() end)
end

-------------------------------------------------------------------------------
-- Audit Log Panel
-------------------------------------------------------------------------------
function RF:InitAuditLogPanel(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    local LOG_TYPE_COLORS = {
        JOIN       = T.success,
        LEAVE      = T.warning,
        RANK       = T.accent,
        BAN        = T.danger,
        BAN_REJOIN = T.danger,
        UNBAN      = T.success,
    }
    local LOG_TYPES = { "All Types", "JOIN", "LEAVE", "RANK", "BAN", "UNBAN", "BAN_REJOIN" }

    -- Toolbar
    local toolbar = CreateFrame("Frame", nil, container)
    toolbar:SetHeight(26)
    toolbar:SetPoint("TOPLEFT", 0, 0)
    toolbar:SetPoint("RIGHT", 0, 0)

    local searchBar = RF:CreateSearchBar(toolbar, {
        width = 180,
        placeholder = "Search log...",
        onTextChanged = function() if container.Refresh then container.Refresh() end end,
    })
    searchBar:SetPoint("LEFT", 0, 0)

    local typeItems = {}
    for _, t in ipairs(LOG_TYPES) do
        tinsert(typeItems, { label = t, value = t == "All Types" and "ALL" or t })
    end
    local typeDropdown = RF:CreateDropdown(toolbar, {
        width = 120,
        defaultText = "All Types",
        defaultValue = "ALL",
        items = typeItems,
        onChanged = function()
            C_Timer.After(0, function()
                if container.Refresh then container.Refresh() end
            end)
        end,
    })
    typeDropdown:SetPoint("LEFT", searchBar, "RIGHT", 8, 0)
    container.typeDropdown = typeDropdown

    local logCountLabel = RF:CreateLabel(toolbar, "0 entries", RF.FONT_SIZES.small, "textMuted")
    logCountLabel:SetPoint("LEFT", typeDropdown, "RIGHT", 8, 0)

    local exportBtn = RF:CreateButton(toolbar, "Export", 60, 24, "accent")
    exportBtn:SetPoint("RIGHT", 0, 0)

    -- Column headers
    local colHeader = CreateFrame("Frame", nil, container, "BackdropTemplate")
    colHeader:SetHeight(20)
    colHeader:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -4)
    colHeader:SetPoint("RIGHT", 0, 0)
    colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    colHeader:SetBackdropColor(unpack(T.bgSecondary))

    local logColDefs = {
        { "Date/Time", 8, 120 }, { "Type", 132, 80 }, { "Message", 220, 400 },
    }
    for _, col in ipairs(logColDefs) do
        local lbl = colHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetFont(fontPath, RF.FONT_SIZES.small, "")
        lbl:SetPoint("LEFT", col[2], 0)
        lbl:SetWidth(col[3])
        lbl:SetText(col[1])
        lbl:SetTextColor(unpack(T.textSecondary))
        lbl:SetJustifyH("LEFT")
    end

    -- Scroll list
    local auditList = RF:CreateScrollList(container, {
        rowHeight = 22,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(22)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.dateText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.dateText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.dateText:SetPoint("LEFT", 8, 0)
            row.dateText:SetWidth(120)
            row.dateText:SetJustifyH("LEFT")
            row.dateText:SetTextColor(unpack(T.textMuted))

            row.typeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.typeText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.typeText:SetPoint("LEFT", 132, 0)
            row.typeText:SetWidth(80)
            row.typeText:SetJustifyH("LEFT")

            row.msgText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.msgText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.msgText:SetPoint("LEFT", 220, 0)
            row.msgText:SetPoint("RIGHT", -4, 0)
            row.msgText:SetJustifyH("LEFT")
            row.msgText:SetTextColor(unpack(T.textPrimary))

            return row
        end,
        updateRow = function(row, data)
            row.dateText:SetText(data.time and date("%Y-%m-%d %H:%M", data.time) or "")
            row.typeText:SetText(data.type or "")
            local typeColor = LOG_TYPE_COLORS[data.type] or T.textSecondary
            row.typeText:SetTextColor(unpack(typeColor))
            row.msgText:SetText(data.msg or "")
        end,
        onRowEnter = function(row, data)
            if not data then return end
            GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
            GameTooltip:AddLine(data.type or "Event", 1, 1, 1)
            if data.time then
                GameTooltip:AddLine(date("%Y-%m-%d %H:%M:%S", data.time), 0.5, 0.5, 0.5)
            end
            if data.msg then
                GameTooltip:AddLine(data.msg, 0.7, 0.7, 0.7, true)
            end
            GameTooltip:Show()
        end,
        onRowLeave = function() GameTooltip:Hide() end,
    })
    auditList:SetPoint("TOPLEFT", colHeader, "BOTTOMLEFT", 0, -1)
    auditList:SetPoint("BOTTOMRIGHT", 0, 0)
    container.auditList = auditList

    -- Export
    exportBtn:SetScript("OnClick", function()
        local log = RF:GetGuildLog()
        if #log == 0 then
            RF:Print("No log entries to export.")
            return
        end
        local lines = { "RosterForge Audit Log - " .. date("%Y-%m-%d %H:%M:%S") }
        tinsert(lines, "---")
        for i = #log, 1, -1 do
            local e = log[i]
            tinsert(lines, format("[%s] [%s] %s",
                e.time and date("%Y-%m-%d %H:%M:%S", e.time) or "?",
                e.type or "?",
                e.msg or ""))
        end
        RF:ShowCopyPopup(table.concat(lines, "\n"))
    end)

    -- Strip repeated realm suffixes from a log message (e.g. "Name-Realm-Realm" → "Name-Realm")
    -- Handles truncated last segment (e.g. "Name-Mal'Ganis-Mal'Ganis-Mal'Ga" → "Name-Mal'Ganis")
    local function CleanLogMsg(msg)
        if not msg then return msg end
        local word = msg:match("^(%S+)")
        if not word then return msg end
        local parts = {}
        for part in word:gmatch("[^%-]+") do
            tinsert(parts, part)
        end
        if #parts <= 2 then return msg end
        -- Check if parts[2] repeats (exact or truncated prefix) starting from parts[3]
        local realm = parts[2]
        local dupeCount = 0
        for i = 3, #parts do
            if parts[i] == realm or realm:sub(1, #parts[i]) == parts[i] then
                dupeCount = dupeCount + 1
            else
                break
            end
        end
        if dupeCount > 0 then
            local cleanName = parts[1] .. "-" .. realm
            local escaped = word:gsub("([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1")
            msg = msg:gsub(escaped, cleanName)
        end
        return msg
    end

    -- Refresh function
    function container.Refresh()
        local log = RF:GetGuildLog()
        local search = searchBar:GetText():lower()
        local typeFilter = typeDropdown:GetValue()
        if typeFilter == "ALL" then typeFilter = nil end
        local filtered = {}
        for i = #log, 1, -1 do
            local e = log[i]
            -- Clean repeated realm names in-place
            if e.msg then e.msg = CleanLogMsg(e.msg) end
            local passType = not typeFilter or e.type == typeFilter
            local passSearch = search == "" or (e.msg and e.msg:lower():find(search, 1, true))
            if passType and passSearch then
                tinsert(filtered, e)
            end
        end
        auditList:SetData(filtered)
        logCountLabel:SetText(#filtered .. " entr" .. (#filtered ~= 1 and "ies" or "y"))
    end

    container:SetScript("OnShow", function() container.Refresh() end)
end

-------------------------------------------------------------------------------
-- Alt Management Panel
-------------------------------------------------------------------------------
function RF:InitAltManagementPanel(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    -- Toolbar
    local toolbar = CreateFrame("Frame", nil, container)
    toolbar:SetHeight(26)
    toolbar:SetPoint("TOPLEFT", 0, 0)
    toolbar:SetPoint("RIGHT", 0, 0)

    local createGroupBtn = RF:CreateButton(toolbar, "Create Group", 110, 24, "accent")
    createGroupBtn:SetPoint("LEFT", 0, 0)

    local autoLinkBtn = RF:CreateButton(toolbar, "Auto-Link by Alt Rank", 160, 24)
    autoLinkBtn:SetPoint("LEFT", createGroupBtn, "RIGHT", 6, 0)

    local searchBar = RF:CreateSearchBar(toolbar, {
        width = 160,
        placeholder = "Search alts...",
        onTextChanged = function() if container.Refresh then container.Refresh() end end,
    })
    searchBar:SetPoint("LEFT", autoLinkBtn, "RIGHT", 8, 0)

    local statsLabel = RF:CreateLabel(toolbar, "Groups: 0, Alts: 0", RF.FONT_SIZES.small, "textMuted")
    statsLabel:SetPoint("RIGHT", 0, 0)

    -- Left panel (55% width): groups list + unlinked
    local leftPanel = CreateFrame("Frame", nil, container, "BackdropTemplate")
    leftPanel:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -4)
    leftPanel:SetPoint("BOTTOMLEFT", 0, 0)
    leftPanel:SetWidth(1) -- will be resized in OnSizeChanged
    leftPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    leftPanel:SetBackdropColor(unpack(T.bgTertiary))
    leftPanel:SetBackdropBorderColor(unpack(T.border))
    container.leftPanel = leftPanel

    -- Right panel (45% width): detail panel
    local rightPanel = CreateFrame("Frame", nil, container, "BackdropTemplate")
    rightPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", 4, 0)
    rightPanel:SetPoint("BOTTOMRIGHT", 0, 0)
    rightPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    rightPanel:SetBackdropColor(unpack(T.bgTertiary))
    rightPanel:SetBackdropBorderColor(unpack(T.border))
    container.rightPanel = rightPanel

    -- Resize left panel to 55% on parent size change
    local function ResizeLeftPanel()
        local w = container:GetWidth()
        if w and w > 10 then
            leftPanel:SetWidth(math.floor((w - 4) * 0.55))
        end
    end
    container:SetScript("OnSizeChanged", function(self, w, h)
        ResizeLeftPanel()
    end)

    -- Left panel: unlinked section at bottom (120px)
    local unlinkedBg = CreateFrame("Frame", nil, leftPanel, "BackdropTemplate")
    unlinkedBg:SetHeight(120)
    unlinkedBg:SetPoint("BOTTOMLEFT", 0, 0)
    unlinkedBg:SetPoint("BOTTOMRIGHT", 0, 0)
    unlinkedBg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    unlinkedBg:SetBackdropColor(unpack(T.bgSecondary))

    local unlinkedHeader = RF:CreateLabel(unlinkedBg, "Unlinked at Alt Rank", RF.FONT_SIZES.small, "textSecondary")
    unlinkedHeader:SetPoint("TOPLEFT", 6, -4)

    local unlinkedList = RF:CreateScrollList(unlinkedBg, {
        rowHeight = 20,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(20)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.nameText:SetPoint("LEFT", 8, 0)
            row.nameText:SetPoint("RIGHT", -4, 0)
            row.nameText:SetJustifyH("LEFT")
            row.nameText:SetTextColor(unpack(T.textPrimary))

            return row
        end,
        updateRow = function(row, data)
            local member = RF:GetMemberByName(data)
            if member then
                row.nameText:SetText(RF:GetClassColoredName(data, member.classFile))
            else
                row.nameText:SetText(data)
            end
        end,
        onRowClick = function(row, data, button)
            if not data then return end
            if button == "RightButton" then
                RF:ShowContextMenu(row, {
                    { text = "Set as Main", func = function()
                        RF:SetMain(data)
                        selectedAltMain = data
                        container.Refresh()
                    end },
                    { text = "Link to Main...", func = function()
                        -- Open detail panel create flow with this name ready to link
                        detailEmpty:Hide()
                        container.detailContent:Hide()
                        container.createPanel:Show()
                        mainPickEB:SetText("")
                        mainPickEB:SetFocus()
                        container._pendingAltLink = data
                    end },
                })
            end
        end,
    })
    unlinkedList:SetPoint("TOPLEFT", unlinkedHeader, "BOTTOMLEFT", 0, -2)
    unlinkedList:SetPoint("BOTTOMRIGHT", -2, 2)
    container.unlinkedList = unlinkedList

    -- Collapse state for alt groups
    local collapsedGroups = {}

    -- Left panel: groups list (above unlinked)
    local groupList = RF:CreateScrollList(leftPanel, {
        rowHeight = 22,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(22)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.toggle = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.toggle:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.toggle:SetPoint("LEFT", 4, 0)
            row.toggle:SetWidth(14)
            row.toggle:SetJustifyH("CENTER")
            row.toggle:SetTextColor(unpack(T.textMuted))

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", row.toggle, "RIGHT", 2, 0)
            row.nameText:SetPoint("RIGHT", -4, 0)
            row.nameText:SetJustifyH("LEFT")

            return row
        end,
        updateRow = function(row, data, index, isSelected)
            row.nameText:ClearAllPoints()
            row.nameText:SetPoint("RIGHT", -4, 0)
            if data.isMain then
                local member = RF:GetMemberByName(data.name)
                local display = data.name
                if member then
                    display = RF:GetClassColoredName(data.name, member.classFile)
                end
                local arrow = collapsedGroups[data.name] and "+" or "-"
                row.toggle:SetText(arrow)
                row.toggle:Show()
                row.nameText:SetPoint("LEFT", row.toggle, "RIGHT", 2, 0)
                row.nameText:SetText(display .. "  |cFF888888(" .. data.altCount .. " alts)|r")
                row.nameText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
            else
                row.toggle:SetText("")
                row.toggle:Hide()
                local member = RF:GetMemberByName(data.name)
                local display = data.name
                if member then
                    display = RF:GetClassColoredName(data.name, member.classFile)
                end
                row.nameText:SetPoint("LEFT", 22, 0)
                row.nameText:SetText("> " .. display)
                row.nameText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            end

            if isSelected then
                row.bg:SetColorTexture(T.accent[1], T.accent[2], T.accent[3], 0.2)
            end
        end,
        onRowClick = function(row, data, button)
            if not data then return end
            if button == "RightButton" then
                local items = {
                    { text = "Copy Name", func = function() RF:ShowCopyPopup(data.name) end },
                }
                if data.isMain then
                    local isCollapsed = collapsedGroups[data.name]
                    tinsert(items, { text = isCollapsed and "Expand" or "Collapse", func = function()
                        collapsedGroups[data.name] = not isCollapsed or nil
                        container.Refresh()
                    end })
                else
                    tinsert(items, { text = "Set as Main", func = function()
                        RF:SetMain(data.name)
                        container.Refresh()
                    end })
                    tinsert(items, { text = "Unlink", func = function()
                        RF:UnlinkAlt(data.name)
                        container.Refresh()
                    end })
                end
                RF:ShowContextMenu(row, items)
            else
                if data.isMain then
                    -- Toggle collapse on left-click
                    collapsedGroups[data.name] = not collapsedGroups[data.name] or nil
                    container.Refresh()
                end
                -- Select the main for detail panel
                local mainName = data.isMain and data.name or RF:GetMainFor(data.name) or data.name
                selectedAltMain = mainName
                container.RefreshDetail()
            end
        end,
    })
    groupList:SetPoint("TOPLEFT", 2, -2)
    groupList:SetPoint("RIGHT", leftPanel, "RIGHT", -2, 0)
    groupList:SetPoint("BOTTOM", unlinkedBg, "TOP", 0, 2)
    container.groupList = groupList

    -- Right panel: detail view
    local detailEmpty = RF:CreateLabel(rightPanel, "Select a group or click Create Group", RF.FONT_SIZES.normal, "textMuted")
    detailEmpty:SetPoint("CENTER", 0, 0)
    container.detailEmpty = detailEmpty

    -- Create Group panel
    local createPanel = CreateFrame("Frame", nil, rightPanel)
    createPanel:SetAllPoints()
    createPanel:Hide()
    container.createPanel = createPanel

    local createTitle = RF:CreateLabel(createPanel, "Create Alt Group", RF.FONT_SIZES.large, "textPrimary")
    createTitle:SetPoint("TOPLEFT", 10, -10)

    local createDesc = RF:CreateLabel(createPanel, "Select a main character to start a new alt group.", RF.FONT_SIZES.small, "textMuted")
    createDesc:SetPoint("TOPLEFT", createTitle, "BOTTOMLEFT", 0, -6)

    local mainPickLabel = RF:CreateLabel(createPanel, "Main Character:", RF.FONT_SIZES.small, "textSecondary")
    mainPickLabel:SetPoint("TOPLEFT", createDesc, "BOTTOMLEFT", 0, -14)

    local mainPickEB = CreateFrame("EditBox", nil, createPanel, "BackdropTemplate")
    mainPickEB:SetSize(180, 24)
    mainPickEB:SetPoint("TOPLEFT", mainPickLabel, "BOTTOMLEFT", 0, -4)
    mainPickEB:SetFontObject("ChatFontNormal")
    mainPickEB:SetFont(mainPickEB:GetFont(), RF.FONT_SIZES.normal, "")
    mainPickEB:SetAutoFocus(false)
    mainPickEB:SetMaxLetters(50)
    mainPickEB:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    mainPickEB:SetBackdropColor(0.1, 0.1, 0.12, 1)
    mainPickEB:SetBackdropBorderColor(unpack(T.border))
    mainPickEB:SetTextColor(unpack(T.textPrimary))
    mainPickEB:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

    -- Autocomplete for main picker
    local CREATE_MAX_SUGGEST = 6
    local CREATE_ROW_H = 20
    local createSuggestFrame = CreateFrame("Frame", nil, createPanel, "BackdropTemplate")
    createSuggestFrame:SetPoint("TOPLEFT", mainPickEB, "BOTTOMLEFT", 0, -1)
    createSuggestFrame:SetWidth(180)
    createSuggestFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    createSuggestFrame:SetBackdropColor(unpack(T.bgSecondary))
    createSuggestFrame:SetBackdropBorderColor(unpack(T.border))
    createSuggestFrame:SetFrameStrata("DIALOG")
    createSuggestFrame:Hide()

    local createSuggestRows = {}
    for i = 1, CREATE_MAX_SUGGEST do
        local row = CreateFrame("Button", nil, createSuggestFrame)
        row:SetHeight(CREATE_ROW_H)
        row:SetPoint("TOPLEFT", 1, -((i - 1) * CREATE_ROW_H) - 1)
        row:SetPoint("RIGHT", createSuggestFrame, "RIGHT", -1, 0)

        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints()
        row.bg:SetColorTexture(0, 0, 0, 0)

        row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.text:SetFont(fontPath, RF.FONT_SIZES.small, "")
        row.text:SetPoint("LEFT", 6, 0)
        row.text:SetPoint("RIGHT", -4, 0)
        row.text:SetJustifyH("LEFT")

        row:SetScript("OnEnter", function(self)
            self.bg:SetColorTexture(T.accent[1], T.accent[2], T.accent[3], 0.25)
        end)
        row:SetScript("OnLeave", function(self)
            self.bg:SetColorTexture(0, 0, 0, 0)
        end)
        row:SetScript("OnMouseDown", function(self)
            mainPickEB:SetText(self.rawName)
            createSuggestFrame:Hide()
            mainPickEB:SetCursorPosition(#self.rawName)
        end)

        createSuggestRows[i] = row
    end

    local function UpdateCreateSuggestions(text)
        if not text or text == "" then
            createSuggestFrame:Hide()
            return
        end
        local query = text:lower()
        local matches = {}
        if RF.rosterData then
            for _, entry in ipairs(RF.rosterData) do
                if entry.name:lower():find(query, 1, true) then
                    tinsert(matches, entry)
                    if #matches >= CREATE_MAX_SUGGEST then break end
                end
            end
        end
        if #matches == 0 then
            createSuggestFrame:Hide()
            return
        end
        for i = 1, CREATE_MAX_SUGGEST do
            local row = createSuggestRows[i]
            if i <= #matches then
                local m = matches[i]
                row.rawName = m.name
                row.text:SetText(RF:GetClassColoredName(m.name, m.classFile))
                row:Show()
            else
                row:Hide()
            end
        end
        createSuggestFrame:SetHeight(#matches * CREATE_ROW_H + 2)
        createSuggestFrame:Show()
    end

    mainPickEB:SetScript("OnTextChanged", function(self, userInput)
        if userInput then UpdateCreateSuggestions(self:GetText()) end
    end)
    mainPickEB:SetScript("OnEditFocusLost", function()
        C_Timer.After(0.1, function() createSuggestFrame:Hide() end)
    end)
    mainPickEB:SetScript("OnEditFocusGained", function(self)
        local text = self:GetText()
        if text and text ~= "" then UpdateCreateSuggestions(text) end
    end)

    local createConfirmBtn = RF:CreateButton(createPanel, "Set as Main & Create", 150, 26, "accent")
    createConfirmBtn:SetPoint("TOPLEFT", mainPickEB, "BOTTOMLEFT", 0, -10)

    local function DoCreateGroup()
        createSuggestFrame:Hide()
        local mainName = strtrim(mainPickEB:GetText())
        if mainName == "" then return end
        local member = RF:GetMemberByName(mainName)
        if not member then
            RF:Print(mainName .. " not found in roster.")
            return
        end
        RF:SetMain(mainName)
        -- If we came from "Link to Main..." on an unlinked member, link them now
        if container._pendingAltLink then
            RF:LinkAlt(container._pendingAltLink, mainName)
            container._pendingAltLink = nil
        end
        mainPickEB:SetText("")
        selectedAltMain = mainName
        createPanel:Hide()
        container.Refresh()
    end

    createConfirmBtn:SetScript("OnClick", DoCreateGroup)
    mainPickEB:SetScript("OnEnterPressed", DoCreateGroup)

    local createCancelBtn = RF:CreateButton(createPanel, "Cancel", 80, 26)
    createCancelBtn:SetPoint("LEFT", createConfirmBtn, "RIGHT", 6, 0)
    createCancelBtn:SetScript("OnClick", function()
        createSuggestFrame:Hide()
        mainPickEB:SetText("")
        container._pendingAltLink = nil
        createPanel:Hide()
        detailEmpty:Show()
    end)

    -- Create Group button handler
    createGroupBtn:SetScript("OnClick", function()
        selectedAltMain = nil
        detailEmpty:Hide()
        container.detailContent:Hide()
        createPanel:Show()
        mainPickEB:SetText("")
        mainPickEB:SetFocus()
    end)

    local detailContent = CreateFrame("Frame", nil, rightPanel)
    detailContent:SetAllPoints()
    detailContent:Hide()
    container.detailContent = detailContent

    local mainNameLabel = detailContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    mainNameLabel:SetFont(fontPath, RF.FONT_SIZES.medium, "")
    mainNameLabel:SetPoint("TOPLEFT", 10, -10)
    mainNameLabel:SetTextColor(unpack(T.textPrimary))
    container.mainNameLabel = mainNameLabel

    local mainInfoLabel = detailContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    mainInfoLabel:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    mainInfoLabel:SetPoint("TOPLEFT", mainNameLabel, "BOTTOMLEFT", 0, -2)
    mainInfoLabel:SetTextColor(unpack(T.textPrimary))
    container.mainInfoLabel = mainInfoLabel

    local mainZoneLabel = detailContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    mainZoneLabel:SetFont(fontPath, RF.FONT_SIZES.small, "")
    mainZoneLabel:SetPoint("TOPLEFT", mainInfoLabel, "BOTTOMLEFT", 0, -1)
    mainZoneLabel:SetTextColor(unpack(T.textSecondary))
    container.mainZoneLabel = mainZoneLabel

    local altDetailHeader = RF:CreateLabel(detailContent, "Alts:", RF.FONT_SIZES.small, "textSecondary")
    altDetailHeader:SetPoint("TOPLEFT", mainZoneLabel, "BOTTOMLEFT", 0, -8)

    -- Alt detail list
    local altDetailList = RF:CreateScrollList(detailContent, {
        rowHeight = 40,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(40)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("TOPLEFT", 8, -4)
            row.nameText:SetWidth(140)
            row.nameText:SetJustifyH("LEFT")

            row.detailText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.detailText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.detailText:SetPoint("TOPLEFT", row.nameText, "BOTTOMLEFT", 0, -1)
            row.detailText:SetPoint("RIGHT", -60, 0)
            row.detailText:SetJustifyH("LEFT")
            row.detailText:SetTextColor(unpack(T.textSecondary))

            row.unlinkBtn = RF:CreateButton(row, "Unlink", 50, 20, "danger")
            row.unlinkBtn:SetPoint("RIGHT", -4, 0)

            return row
        end,
        updateRow = function(row, data)
            local member = RF:GetMemberByName(data)
            if member then
                row.nameText:SetText(RF:GetClassColoredName(data, member.classFile))
                local online = member.isOnline and "|cFF4ADE80Online|r" or "|cFFAAAAAAOffline|r"
                row.detailText:SetText(format("Lv%d %s - %s", member.level or 0, member.classDisplayName or "", online))
            else
                row.nameText:SetText(data)
                row.nameText:SetTextColor(unpack(T.textPrimary))
                row.detailText:SetText("|cFFAAAAAANot in guild|r")
            end
            row.unlinkBtn:SetScript("OnClick", function()
                RF:UnlinkAlt(data)
                container.Refresh()
            end)
        end,
    })
    altDetailList:SetPoint("TOPLEFT", altDetailHeader, "BOTTOMLEFT", 0, -4)
    altDetailList:SetPoint("RIGHT", detailContent, "RIGHT", -10, 0)
    altDetailList:SetHeight(120)
    container.altDetailList = altDetailList

    -- Link New Alt
    local linkLabel = RF:CreateLabel(detailContent, "Link New Alt:", RF.FONT_SIZES.small, "textSecondary")
    linkLabel:SetPoint("TOPLEFT", altDetailList, "BOTTOMLEFT", 0, -10)

    local linkEB = CreateFrame("EditBox", nil, detailContent, "BackdropTemplate")
    linkEB:SetSize(140, 22)
    linkEB:SetPoint("TOPLEFT", linkLabel, "BOTTOMLEFT", 0, -2)
    linkEB:SetFontObject("ChatFontNormal")
    linkEB:SetFont(linkEB:GetFont(), RF.FONT_SIZES.normal, "")
    linkEB:SetAutoFocus(false)
    linkEB:SetMaxLetters(50)
    linkEB:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    linkEB:SetBackdropColor(0.1, 0.1, 0.12, 1)
    linkEB:SetBackdropBorderColor(unpack(T.border))
    linkEB:SetTextColor(unpack(T.textPrimary))
    linkEB:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    container.linkEB = linkEB

    -- Autocomplete dropdown
    local MAX_SUGGESTIONS = 6
    local ROW_H = 20
    local suggestFrame = CreateFrame("Frame", nil, detailContent, "BackdropTemplate")
    suggestFrame:SetPoint("TOPLEFT", linkEB, "BOTTOMLEFT", 0, -1)
    suggestFrame:SetWidth(140)
    suggestFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    suggestFrame:SetBackdropColor(unpack(T.bgSecondary))
    suggestFrame:SetBackdropBorderColor(unpack(T.border))
    suggestFrame:SetFrameStrata("DIALOG")
    suggestFrame:Hide()

    local suggestRows = {}
    for i = 1, MAX_SUGGESTIONS do
        local row = CreateFrame("Button", nil, suggestFrame)
        row:SetHeight(ROW_H)
        row:SetPoint("TOPLEFT", 1, -((i - 1) * ROW_H) - 1)
        row:SetPoint("RIGHT", suggestFrame, "RIGHT", -1, 0)

        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints()
        row.bg:SetColorTexture(0, 0, 0, 0)

        row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.text:SetFont(fontPath, RF.FONT_SIZES.small, "")
        row.text:SetPoint("LEFT", 6, 0)
        row.text:SetPoint("RIGHT", -4, 0)
        row.text:SetJustifyH("LEFT")

        row:SetScript("OnEnter", function(self)
            self.bg:SetColorTexture(T.accent[1], T.accent[2], T.accent[3], 0.25)
        end)
        row:SetScript("OnLeave", function(self)
            self.bg:SetColorTexture(0, 0, 0, 0)
        end)
        row:SetScript("OnMouseDown", function(self)
            linkEB:SetText(self.rawName)
            suggestFrame:Hide()
            linkEB:SetCursorPosition(#self.rawName)
        end)

        suggestRows[i] = row
    end

    local function UpdateSuggestions(text)
        if not text or text == "" then
            suggestFrame:Hide()
            return
        end
        local query = text:lower()
        local matches = {}
        if RF.rosterData then
            for _, entry in ipairs(RF.rosterData) do
                if entry.name:lower():find(query, 1, true) and entry.name ~= selectedAltMain then
                    tinsert(matches, entry)
                    if #matches >= MAX_SUGGESTIONS then break end
                end
            end
        end
        if #matches == 0 then
            suggestFrame:Hide()
            return
        end
        for i = 1, MAX_SUGGESTIONS do
            local row = suggestRows[i]
            if i <= #matches then
                local m = matches[i]
                row.rawName = m.name
                row.text:SetText(RF:GetClassColoredName(m.name, m.classFile))
                row:Show()
            else
                row:Hide()
            end
        end
        suggestFrame:SetHeight(#matches * ROW_H + 2)
        suggestFrame:Show()
    end

    linkEB:SetScript("OnTextChanged", function(self, userInput)
        if userInput then
            UpdateSuggestions(self:GetText())
        end
    end)

    linkEB:SetScript("OnEditFocusLost", function(self)
        C_Timer.After(0.1, function()
            suggestFrame:Hide()
        end)
    end)

    linkEB:SetScript("OnEditFocusGained", function(self)
        local text = self:GetText()
        if text and text ~= "" then
            UpdateSuggestions(text)
        end
    end)

    local linkBtn = RF:CreateButton(detailContent, "Link", 50, 22, "accent")
    linkBtn:SetPoint("LEFT", linkEB, "RIGHT", 4, 0)
    linkBtn:SetScript("OnClick", function()
        suggestFrame:Hide()
        local altName = strtrim(linkEB:GetText())
        if altName == "" or not selectedAltMain then return end
        local member = RF:GetMemberByName(altName)
        if not member then
            RF:Print(altName .. " not found in roster.")
            return
        end
        RF:LinkAlt(altName, selectedAltMain)
        linkEB:SetText("")
        container.Refresh()
    end)

    linkEB:SetScript("OnEnterPressed", function()
        linkBtn:Click()
    end)

    -- Unlink All button
    local unlinkAllBtn = RF:CreateButton(detailContent, "Unlink All", 80, 24, "danger")
    unlinkAllBtn:SetPoint("BOTTOMLEFT", 10, 10)
    unlinkAllBtn:SetScript("OnClick", function()
        if not selectedAltMain then return end
        RF:ShowConfirmDialog("Unlink All",
            "Unlink all alts from " .. selectedAltMain .. "?",
            function()
                local alts = RF:GetAltsForMain(selectedAltMain)
                for i = #alts, 1, -1 do
                    RF:UnlinkAlt(alts[i])
                end
                selectedAltMain = nil
                container.Refresh()
            end)
    end)

    -- Auto-Link
    autoLinkBtn:SetScript("OnClick", function()
        local linked = RF:AutoLinkByAltRank()
        RF:Print("Auto-linked " .. linked .. " alt(s) by alt rank.")
        container.Refresh()
    end)

    -- Refresh detail panel
    function container.RefreshDetail()
        if not selectedAltMain then
            detailContent:Hide()
            createPanel:Hide()
            detailEmpty:Show()
            return
        end
        detailEmpty:Hide()
        createPanel:Hide()
        detailContent:Show()

        local member = RF:GetMemberByName(selectedAltMain)
        if member then
            mainNameLabel:SetText(RF:GetClassColoredName(selectedAltMain, member.classFile))
            local online = member.isOnline and "|cFF4ADE80Online|r" or "|cFFAAAAAAOffline|r"
            mainInfoLabel:SetText(format("Level %d %s - %s", member.level or 0, member.classDisplayName or "", online))
            mainZoneLabel:SetText(member.zone or "")
        else
            mainNameLabel:SetText(selectedAltMain)
            mainInfoLabel:SetText("|cFFAAAAAANot in guild|r")
            mainZoneLabel:SetText("")
        end

        local alts = RF:GetAltsForMain(selectedAltMain)
        altDetailList:SetData(alts)
    end

    -- Main refresh function
    function container.Refresh()
        local groups = RF:GetAltGroups()
        local search = searchBar:GetText():lower()
        local flatList = {}
        local groupCount = 0
        local altCount = 0

        -- Sort mains alphabetically
        local mainNames = {}
        for mainName in pairs(groups) do
            tinsert(mainNames, mainName)
        end
        table.sort(mainNames, function(a, b) return a:lower() < b:lower() end)

        for _, mainName in ipairs(mainNames) do
            local group = groups[mainName]
            local alts = group.alts or {}
            local matchesSearch = search == "" or mainName:lower():find(search, 1, true)

            -- Check if any alt matches search
            if not matchesSearch then
                for _, altName in ipairs(alts) do
                    if altName:lower():find(search, 1, true) then
                        matchesSearch = true
                        break
                    end
                end
            end

            if matchesSearch then
                groupCount = groupCount + 1
                altCount = altCount + #alts
                tinsert(flatList, { name = mainName, isMain = true, altCount = #alts })
                if not collapsedGroups[mainName] then
                    for _, altName in ipairs(alts) do
                        tinsert(flatList, { name = altName, isMain = false })
                    end
                end
            end
        end

        groupList:SetData(flatList)
        statsLabel:SetText(format("Groups: %d, Alts: %d", groupCount, altCount))

        -- Unlinked members
        local altRankIdx = RF.charDB and RF.charDB.altAutoRankIndex
        if altRankIdx then
            local unlinked = RF:GetUnlinkedAltRankMembers()
            unlinkedList:SetData(unlinked)
            unlinkedHeader:SetText("Unlinked at Alt Rank (" .. #unlinked .. ")")
        else
            unlinkedList:SetData({})
            unlinkedHeader:SetText("No alt rank configured")
        end

        container.RefreshDetail()
    end

    container:SetScript("OnShow", function()
        ResizeLeftPanel()
        container.Refresh()
    end)
end

-------------------------------------------------------------------------------
-- Notes Management Panel
-------------------------------------------------------------------------------
function RF:InitNotesManagementPanel(container)
    T = RF.Theme
    local fontPath = ChatFontNormal:GetFont()

    -- Toolbar
    local toolbar = CreateFrame("Frame", nil, container)
    toolbar:SetHeight(26)
    toolbar:SetPoint("TOPLEFT", 0, 0)
    toolbar:SetPoint("RIGHT", 0, 0)

    local searchBar = RF:CreateSearchBar(toolbar, {
        width = 220,
        placeholder = "Search notes...",
        onTextChanged = function() if container.Refresh then container.Refresh() end end,
    })
    searchBar:SetPoint("LEFT", 0, 0)

    local countLabel = RF:CreateLabel(toolbar, "0 notes", RF.FONT_SIZES.small, "textMuted")
    countLabel:SetPoint("LEFT", searchBar, "RIGHT", 8, 0)

    local addBtn = RF:CreateButton(toolbar, "Add Note", 80, 24, "accent")
    addBtn:SetPoint("RIGHT", 0, 0)
    addBtn:SetScript("OnClick", function()
        -- Open the note editor with a name prompt
        if RF.ShowRosterNoteEditor then
            -- Show a simple name input first
            RF:ShowRosterNoteNamePicker()
        end
    end)

    -- Column headers
    local colHeader = CreateFrame("Frame", nil, container, "BackdropTemplate")
    colHeader:SetHeight(20)
    colHeader:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -4)
    colHeader:SetPoint("RIGHT", 0, 0)
    colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" })
    colHeader:SetBackdropColor(unpack(T.bgSecondary))

    local colDefs = {
        { "Player", 8, 140 }, { "Note", 156, 300 }, { "Updated", 464, 100 },
    }
    for _, col in ipairs(colDefs) do
        local lbl = colHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetFont(fontPath, RF.FONT_SIZES.small, "")
        lbl:SetPoint("LEFT", col[2], 0)
        lbl:SetWidth(col[3])
        lbl:SetText(col[1])
        lbl:SetTextColor(unpack(T.textSecondary))
        lbl:SetJustifyH("LEFT")
    end

    -- Scroll list
    local notesList = RF:CreateScrollList(container, {
        rowHeight = 28,
        createRow = function(p, i)
            local row = CreateFrame("Button", nil, p)
            row:SetHeight(28)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()

            row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.nameText:SetFont(fontPath, RF.FONT_SIZES.normal, "")
            row.nameText:SetPoint("LEFT", 8, 0)
            row.nameText:SetWidth(140)
            row.nameText:SetJustifyH("LEFT")

            row.noteText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.noteText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.noteText:SetPoint("LEFT", 156, 0)
            row.noteText:SetWidth(300)
            row.noteText:SetJustifyH("LEFT")
            row.noteText:SetTextColor(unpack(T.textSecondary))
            row.noteText:SetWordWrap(false)

            row.timeText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.timeText:SetFont(fontPath, RF.FONT_SIZES.small, "")
            row.timeText:SetPoint("LEFT", 464, 0)
            row.timeText:SetWidth(100)
            row.timeText:SetJustifyH("LEFT")
            row.timeText:SetTextColor(unpack(T.textMuted))

            return row
        end,
        updateRow = function(row, data)
            local member = RF:GetMemberByName(data.name)
            if member then
                row.nameText:SetText(RF:GetClassColoredName(data.name, member.classFile))
            else
                row.nameText:SetText(data.name)
                row.nameText:SetTextColor(unpack(T.textPrimary))
            end
            -- Truncate note preview
            local preview = (data.note or ""):gsub("\n", " ")
            if #preview > 60 then preview = preview:sub(1, 57) .. "..." end
            row.noteText:SetText(preview)

            if data.timestamp then
                row.timeText:SetText(date("%m/%d %H:%M", data.timestamp))
            else
                row.timeText:SetText("")
            end
        end,
        onRowClick = function(row, data, button)
            if not data then return end
            if button == "RightButton" then
                RF:ShowContextMenu(row, {
                    { text = "Edit Note", func = function()
                        RF:ShowRosterNoteEditor({ name = data.name })
                    end },
                    { text = "Copy Note", func = function()
                        RF:ShowCopyPopup(data.note)
                    end },
                    { separator = true },
                    { text = "Delete Note", color = { r = 0.9, g = 0.25, b = 0.25 }, func = function()
                        RF:ShowConfirmDialog("Delete RosterNote",
                            "Delete RosterNote for " .. data.name .. "?",
                            function()
                                RF:DeleteExtendedNote(data.name)
                                container.Refresh()
                            end)
                    end },
                })
            else
                RF:ShowRosterNoteEditor({ name = data.name })
            end
        end,
        onRowEnter = function(row, data)
            if not data or not data.note then return end
            GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
            GameTooltip:AddLine(data.name, 1, 1, 1)
            GameTooltip:AddLine(data.note, 0.7, 0.7, 0.7, true)
            GameTooltip:Show()
        end,
        onRowLeave = function() GameTooltip:Hide() end,
    })
    notesList:SetPoint("TOPLEFT", colHeader, "BOTTOMLEFT", 0, -1)
    notesList:SetPoint("BOTTOMRIGHT", 0, 0)
    container.notesList = notesList

    -- Refresh
    function container.Refresh()
        local query = searchBar:GetText():lower()
        local notes = {}
        local allNotes = RF:GetAllExtendedNotes()
        local guildDB = RF:GetGuildDB()
        local timestamps = guildDB and guildDB.noteTimestamps or {}

        for name, text in pairs(allNotes) do
            local pass = query == "" or name:lower():find(query, 1, true) or text:lower():find(query, 1, true)
            if pass then
                tinsert(notes, { name = name, note = text, timestamp = timestamps[name] })
            end
        end

        table.sort(notes, function(a, b) return a.name:lower() < b.name:lower() end)
        countLabel:SetText(#notes .. " note" .. (#notes ~= 1 and "s" or ""))
        notesList:SetData(notes)
    end

    container:SetScript("OnShow", function() container.Refresh() end)

    -- Auto-refresh when notes change
    RF:RegisterCallback("NOTE_UPDATED", function()
        if container:IsShown() then container.Refresh() end
    end)
end

-------------------------------------------------------------------------------
-- RosterNote Name Picker (for adding new notes from Management tab)
-------------------------------------------------------------------------------
function RF:ShowRosterNoteNamePicker()
    if not RF.noteNamePickerFrame then
        local T = RF.Theme
        local fontPath = ChatFontNormal:GetFont()
        local f = RF:CreateStyledFrame("RosterForgeNotePicker", UIParent, 300, 100, {
            title = "Add RosterNote",
            closable = true,
            strata = "DIALOG",
        })
        f:SetPoint("CENTER")
        f:Hide()

        local lbl = RF:CreateLabel(f, "Player name:", RF.FONT_SIZES.small, "textSecondary")
        lbl:SetPoint("TOPLEFT", f.titleBar, "BOTTOMLEFT", 12, -8)

        local eb = CreateFrame("EditBox", nil, f, "BackdropTemplate")
        eb:SetSize(200, 24)
        eb:SetPoint("TOPLEFT", lbl, "BOTTOMLEFT", 0, -4)
        eb:SetFontObject("ChatFontNormal")
        eb:SetFont(fontPath, RF.FONT_SIZES.normal, "")
        eb:SetAutoFocus(false)
        eb:SetMaxLetters(50)
        eb:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        eb:SetBackdropColor(0.1, 0.1, 0.12, 1)
        eb:SetBackdropBorderColor(unpack(T.border))
        eb:SetTextColor(unpack(T.textPrimary))
        eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

        -- Autocomplete dropdown
        local sugFrame = CreateFrame("Frame", nil, f, "BackdropTemplate")
        sugFrame:SetPoint("TOPLEFT", eb, "BOTTOMLEFT", 0, -2)
        sugFrame:SetPoint("TOPRIGHT", eb, "BOTTOMRIGHT", 0, -2)
        sugFrame:SetHeight(1)
        sugFrame:SetFrameStrata("TOOLTIP")
        sugFrame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        sugFrame:SetBackdropColor(0.10, 0.10, 0.13, 0.98)
        sugFrame:SetBackdropBorderColor(unpack(T.border))
        sugFrame:Hide()

        local sugButtons = {}
        local MAX_SUGGESTIONS = 8

        for i = 1, MAX_SUGGESTIONS do
            local row = CreateFrame("Button", nil, sugFrame)
            row:SetHeight(20)
            row:SetPoint("TOPLEFT", 2, -((i - 1) * 20) - 2)
            row:SetPoint("TOPRIGHT", -2, -((i - 1) * 20) - 2)

            local rowBg = row:CreateTexture(nil, "BACKGROUND")
            rowBg:SetAllPoints()
            rowBg:SetColorTexture(0, 0, 0, 0)

            local rowText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            rowText:SetFont(rowText:GetFont(), RF.FONT_SIZES.normal, "")
            rowText:SetPoint("LEFT", 4, 0)
            rowText:SetTextColor(unpack(T.textPrimary))
            row.label = rowText

            row:SetScript("OnEnter", function() rowBg:SetColorTexture(1, 1, 1, 0.06) end)
            row:SetScript("OnLeave", function() rowBg:SetColorTexture(0, 0, 0, 0) end)
            row:SetScript("OnMouseDown", function()
                eb:SetText(rowText:GetText():gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""))
                sugFrame:Hide()
                eb:SetFocus()
            end)

            sugButtons[i] = row
        end

        local function UpdateSuggestions()
            local text = eb:GetText():lower():trim()
            if text == "" or not RF.rosterData then
                sugFrame:Hide()
                return
            end

            local matches = {}
            for _, member in ipairs(RF.rosterData) do
                if member.name:lower():find(text, 1, true) then
                    tinsert(matches, member)
                    if #matches >= MAX_SUGGESTIONS then break end
                end
            end

            if #matches == 0 then
                sugFrame:Hide()
                return
            end

            for i = 1, MAX_SUGGESTIONS do
                if i <= #matches then
                    local m = matches[i]
                    sugButtons[i].label:SetText(RF:GetClassColoredName(m.name, m.classFile))
                    sugButtons[i]:Show()
                else
                    sugButtons[i]:Hide()
                end
            end

            sugFrame:SetHeight(#matches * 20 + 4)
            sugFrame:Show()
        end

        eb:SetScript("OnTextChanged", function(self, userInput)
            if userInput then UpdateSuggestions() end
        end)
        eb:SetScript("OnEditFocusLost", function()
            C_Timer.After(0.1, function()
                sugFrame:Hide()
            end)
        end)

        local goBtn = RF:CreateButton(f, "Next", 60, 24, "accent")
        goBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0)

        local function DoOpen()
            local name = strtrim(eb:GetText())
            if name == "" then return end
            sugFrame:Hide()
            f:Hide()
            RF:ShowRosterNoteEditor({ name = name })
        end

        goBtn:SetScript("OnClick", DoOpen)
        eb:SetScript("OnEnterPressed", DoOpen)
        eb:SetScript("OnEscapePressed", function(self)
            sugFrame:Hide()
            self:ClearFocus()
        end)

        f.editBox = eb
        RF.noteNamePickerFrame = f
    end

    RF.noteNamePickerFrame.editBox:SetText("")
    RF.noteNamePickerFrame:Show()
    RF.noteNamePickerFrame.editBox:SetFocus()
end

-------------------------------------------------------------------------------
-- Populate rank dropdown from current guild
-------------------------------------------------------------------------------
function RF:PopulateRankDropdown(parent)
    if not parent.rankDropdown then return end
    local items = { { label = "All Ranks", value = nil } }
    if GuildControlGetNumRanks then
        local numRanks = GuildControlGetNumRanks()
        for i = 1, numRanks do
            local rankName = GuildControlGetRankName(i)
            if rankName and rankName ~= "" then
                tinsert(items, { label = rankName, value = i - 1 })
            end
        end
    end
    parent.rankDropdown:SetItems(items)
end

-------------------------------------------------------------------------------
-- Update Bulk Filter
-------------------------------------------------------------------------------
function RF:UpdateBulkFilter(parent)
    filterResult = {}
    local bulkRankFilter = parent.rankDropdown and parent.rankDropdown:GetValue()
    local offlineDays = parent.offlineEB and tonumber(parent.offlineEB:GetText()) or nil
    local offlineThreshold = offlineDays and (offlineDays * 86400) or nil

    for _, entry in ipairs(RF.rosterData) do
        local pass = true

        if bulkRankFilter and entry.rankIndex ~= bulkRankFilter then
            pass = false
        end

        if offlineThreshold and entry.lastOnlineSeconds < offlineThreshold then
            pass = false
        end

        if pass then
            tinsert(filterResult, entry)
        end
    end

    wipe(selectedMembers)

    -- Disable Select All when filter result is empty
    if parent.selectAllBtn then
        local hasResults = #filterResult > 0
        parent.selectAllBtn:SetEnabled(hasResults)
        if parent.selectAllBtn.label then
            parent.selectAllBtn.label:SetTextColor(unpack(
                hasResults and RF.Theme.textPrimary or RF.Theme.textMuted
            ))
        end
    end

    RF:RefreshBulkList(parent)
end

-------------------------------------------------------------------------------
-- Refresh Bulk List
-------------------------------------------------------------------------------
function RF:RefreshBulkList(parent)
    if parent.bulkList then
        parent.bulkList:SetData(filterResult)
    end

    local count = 0
    for _ in pairs(selectedMembers) do count = count + 1 end
    if parent.countLabel then
        parent.countLabel:SetText(count .. " selected")
    end
end

-------------------------------------------------------------------------------
-- Generate Bulk Macro
-------------------------------------------------------------------------------
function RF:GenerateBulkMacro(action)
    local names = {}
    for name in pairs(selectedMembers) do
        tinsert(names, name)
    end

    if #names == 0 then
        RF:Print("No members selected.")
        return
    end

    local slashCmd
    if action == "promote" then
        slashCmd = "/gpromote"
    elseif action == "demote" then
        slashCmd = "/gdemote"
    elseif action == "kick" then
        slashCmd = "/gkick"
    else
        return
    end

    local lines = {}
    for _, name in ipairs(names) do
        local member = RF:GetMemberByName(name)
        local fullName = member and member.fullName or name
        tinsert(lines, slashCmd .. " " .. fullName)
    end

    -- Real macros are capped at 255 characters total. Chunk lines into
    -- macro-sized groups instead of dumping everything into one blob --
    -- previously anything past 255 chars was silently truncated by
    -- Blizzard's macro editor on paste, quietly dropping selected members.
    local MACRO_LIMIT = 255
    local chunks = {}
    local current = {}
    local currentLen = 0
    for _, line in ipairs(lines) do
        local addLen = #line + (currentLen > 0 and 1 or 0) -- +1 for the newline joiner
        if currentLen + addLen > MACRO_LIMIT and #current > 0 then
            tinsert(chunks, table.concat(current, "\n"))
            current = {}
            currentLen = 0
            addLen = #line
        end
        tinsert(current, line)
        currentLen = currentLen + addLen
    end
    if #current > 0 then
        tinsert(chunks, table.concat(current, "\n"))
    end

    local macroText
    if #chunks <= 1 then
        macroText = chunks[1] or ""
    else
        local parts = {}
        for i, chunk in ipairs(chunks) do
            tinsert(parts, format("-- Macro %d of %d (paste this block into its own macro slot) --\n%s", i, #chunks, chunk))
        end
        macroText = table.concat(parts, "\n\n")
    end

    if RF.mainFrame then
        local mgmtTab = RF.mainFrame.tabFrames["Management"]
        if mgmtTab and mgmtTab.macroEB then
            mgmtTab.macroEB:SetText(macroText)
        end
        if mgmtTab and mgmtTab.macroLabel then
            if #chunks > 1 then
                mgmtTab.macroLabel:SetText(format("Macro Output (%d members, needs %d separate macros):", #names, #chunks))
            else
                mgmtTab.macroLabel:SetText(format("Macro Output (%d members):", #names))
            end
        end
    end

    if #chunks > 1 then
        RF:Print(format("Generated %s macro for %d members -- exceeds the 255-char macro limit, split into %d macros. Copy each labeled block into its own macro slot.", action, #names, #chunks))
    else
        RF:Print(format("Generated %s macro for %d members. Copy from the output box.", action, #names))
    end
end

-------------------------------------------------------------------------------
-- Export Selected Members
-------------------------------------------------------------------------------
function RF:ExportSelectedMembers(fmt)
    local names = {}
    for name in pairs(selectedMembers) do
        tinsert(names, name)
    end
    if #names == 0 then
        RF:Print("No members selected for export.")
        return
    end

    local entries = {}
    for _, name in ipairs(names) do
        local member = RF:GetMemberByName(name)
        if member then
            tinsert(entries, member)
        end
    end

    if RF.ExportData then
        RF:ExportData(entries, fmt)
    end
end
