local addonName, SinsToolkit = ...

SinsToolkit.UI = SinsToolkit.UI or {}
SinsToolkit.UI.Pages = SinsToolkit.UI.Pages or {}

local PageBase = SinsToolkit.UI.Pages.Base or {}
local Helpers = SinsToolkit.UI.Pages.Helpers or {}
local Page = {}
setmetatable(Page, { __index = PageBase })
SinsToolkit.UI.Pages.Profiles = Page
Page.density = Helpers.GetDefaultDensity and Helpers.GetDefaultDensity() or "compact"

local UNKNOWN_CLASS = "Unknown Class"
local UNKNOWN_SPEC = "Unknown Spec"
local SECTION_OPTIONS = {
    { key = "actions", label = "Action Bars" },
    { key = "macros", label = "Macros" },
    { key = "bindings", label = "Keybindings" },
    { key = "pets", label = "Pet Actions" },
    { key = "editMode", label = "Edit Mode Layout" },
}

StaticPopupDialogs["SINS_TOOLKIT_PROFILE_EXPORT"] = {
    text = "%s",
    button1 = CLOSE,
    hasEditBox = true,
    editBoxWidth = 360,
    OnShow = function(self, data)
        local editBox = self.EditBox or self.editBox
        if editBox then
            editBox:SetMaxLetters(0)
            editBox:SetText((type(data) == "table" and data.text) or "")
            editBox:HighlightText()
            editBox:SetFocus()
        end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

StaticPopupDialogs["SINS_TOOLKIT_PROFILE_IMPORT_DUPLICATE"] = {
    text = "A Profile Vault backup named \"%s\" already exists for this character, class, and specialization.\n\nChoose how to save this import.",
    button1 = "Overwrite Existing",
    button2 = "Import as Copy",
    button3 = CANCEL,
    OnAccept = function(_, data)
        if data and data.finish then data.finish("overwrite") end
    end,
    OnCancel = function(_, data, reason)
        if data and data.finish and reason == "clicked" then
            data.finish("copy")
        elseif data and data.cancel then
            data.cancel()
        end
    end,
    OnAlt = function(_, data)
        if data and data.cancel then data.cancel() end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

local function EscapeKey(value)
    return tostring(value or ""):gsub("[^%w_%-]", "_")
end

local function GetClassColor(classToken)
    local color = classToken and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken]
    if color then
        return color.r or 1, color.g or 1, color.b or 1
    end
    return 0.72, 0.72, 0.76
end

local function WrapClassColor(text, classToken)
    local r, g, b = GetClassColor(classToken)
    return ("|cff%02x%02x%02x%s|r"):format(math.floor(r * 255), math.floor(g * 255), math.floor(b * 255), text or UNKNOWN_CLASS)
end

local function NormalizeBackup(backup)
    local character = backup.character or "Unknown Character"
    local realm = backup.realm or ""
    local className = backup.className or UNKNOWN_CLASS
    local classToken = backup.classToken
    local specName = backup.specName or UNKNOWN_SPEC
    return {
        id = backup.id,
        name = backup.name or "Backup",
        character = character,
        realm = realm,
        characterKey = backup.characterKey or (realm ~= "" and (character .. "-" .. realm) or character),
        className = className,
        classToken = classToken,
        classKey = classToken or className,
        specName = specName,
        specKey = specName,
        createdAt = backup.createdAt or "Unknown date",
        createdAtEpoch = backup.createdAtEpoch or 0,
    }
end

local function BuildBackupGroups(backups)
    local classOrder, classMap = {}, {}
    local byID = {}
    for _, backup in ipairs(backups or {}) do
        local meta = NormalizeBackup(backup)
        byID[meta.id] = meta
        local classGroup = classMap[meta.classKey]
        if not classGroup then
            classGroup = {
                key = meta.classKey,
                name = meta.className,
                token = meta.classToken,
                specs = {},
                specOrder = {},
                count = 0,
            }
            classMap[meta.classKey] = classGroup
            classOrder[#classOrder + 1] = classGroup
        end

        local specGroup = classGroup.specs[meta.specKey]
        if not specGroup then
            specGroup = { key = meta.specKey, name = meta.specName, backups = {}, count = 0 }
            classGroup.specs[meta.specKey] = specGroup
            classGroup.specOrder[#classGroup.specOrder + 1] = specGroup
        end

        specGroup.backups[#specGroup.backups + 1] = meta
        specGroup.count = specGroup.count + 1
        classGroup.count = classGroup.count + 1
    end

    table.sort(classOrder, function(a, b)
        if a.name == UNKNOWN_CLASS then return false end
        if b.name == UNKNOWN_CLASS then return true end
        return a.name < b.name
    end)
    for _, classGroup in ipairs(classOrder) do
        table.sort(classGroup.specOrder, function(a, b)
            if a.name == UNKNOWN_SPEC then return false end
            if b.name == UNKNOWN_SPEC then return true end
            return a.name < b.name
        end)
    end
    return classOrder, byID
end

local function IsPetActionsSupported()
    return GetPetActionInfo and NUM_PET_ACTION_SLOTS
end

local function IsEditModeLayoutSupported()
    return C_EditMode and C_EditMode.GetLayouts and C_EditMode.ConvertLayoutInfoToString and C_EditMode.ConvertStringToLayoutInfo
end

local function ReadSectionState(state)
    local options = {}
    for _, section in ipairs(SECTION_OPTIONS) do
        options[section.key] = state[section.key] == true
    end
    return options
end

local function GetAvailableSections(contents, allowCapture)
    local available = {}
    for _, section in ipairs(SECTION_OPTIONS) do
        local ok = allowCapture or (contents and contents[section.key] == true)
        if section.key == "pets" then ok = ok and IsPetActionsSupported() end
        if section.key == "editMode" then ok = ok and IsEditModeLayoutSupported() end
        available[section.key] = ok and true or false
    end
    return available
end

local function SummarizeSections(state, available)
    local total, selected, names = 0, 0, {}
    for _, section in ipairs(SECTION_OPTIONS) do
        if not available or available[section.key] then
            total = total + 1
            if state[section.key] then
                selected = selected + 1
                names[#names + 1] = section.label
            end
        end
    end
    if selected == 0 then return "None selected" end
    if selected == total then return "All sections" end
    if selected == 1 then return names[1] .. " only" end
    if selected > 2 then return ("%d sections selected"):format(selected) end
    return table.concat(names, " + ")
end

local function SyncStateToAvailability(state, available, defaultSelected)
    for _, section in ipairs(SECTION_OPTIONS) do
        if available[section.key] then
            if state[section.key] == nil then state[section.key] = defaultSelected ~= false end
        else
            state[section.key] = false
        end
    end
end

local activeSectionFlyout

local function HideActiveSectionFlyout()
    if activeSectionFlyout then
        activeSectionFlyout:Hide()
        activeSectionFlyout = nil
    end
end

local function StyleFlyout(frame)
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    frame:SetBackdropColor(0.07, 0.06, 0.09, 0.98)
    frame:SetBackdropBorderColor(0.55, 0.32, 0.75, 0.95)
end

local function PositionFlyout(flyout, anchor)
    local parent = anchor and anchor:GetParent()
    local flyoutHeight = flyout:GetHeight() or 0
    local parentBottom = parent and parent.GetBottom and parent:GetBottom()
    local anchorBottom = anchor and anchor.GetBottom and anchor:GetBottom()
    flyout:ClearAllPoints()
    if parentBottom and anchorBottom and (anchorBottom - flyoutHeight - 180) < parentBottom then
        flyout:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, 4)
    else
        flyout:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -4)
    end
end

local function CreateSectionFlyout(anchor, state, available, onRefresh)
    HideActiveSectionFlyout()

    local flyout = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    flyout:SetFrameStrata("FULLSCREEN_DIALOG")
    flyout:SetFrameLevel((anchor:GetFrameLevel() or 1) + 50)
    flyout:SetClampedToScreen(true)
    flyout:SetSize(236, 18 + (#SECTION_OPTIONS * 24))
    StyleFlyout(flyout)
    PositionFlyout(flyout, anchor)
    flyout.checks = {}
    activeSectionFlyout = flyout

    for index, section in ipairs(SECTION_OPTIONS) do
        local check = CreateFrame("CheckButton", nil, flyout, "UICheckButtonTemplate")
        check:SetSize(22, 22)
        check:SetPoint("TOPLEFT", flyout, "TOPLEFT", 10, -8 - ((index - 1) * 24))
        check.text = check:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        check.text:SetPoint("LEFT", check, "RIGHT", 2, 1)
        check.text:SetWidth(180)
        check.text:SetJustifyH("LEFT")
        check.text:SetText(section.label)
        check.key = section.key
        check:SetChecked(state[section.key] == true)
        check:SetEnabled(available[section.key] == true)
        if available[section.key] then
            check.text:SetTextColor(0.92, 0.9, 0.96)
        else
            check.text:SetTextColor(0.48, 0.46, 0.52)
        end
        check:SetScript("OnClick", function(self)
            if available[self.key] then
                state[self.key] = self:GetChecked() == true
                if onRefresh then onRefresh() end
            end
        end)
        flyout.checks[index] = check
    end

    flyout:SetScript("OnUpdate", function(self)
        local leftDown = IsMouseButtonDown and IsMouseButtonDown("LeftButton")
        if leftDown and not self.wasLeftDown and not self:IsMouseOver() and not anchor:IsMouseOver() then
            self:Hide()
        end
        self.wasLeftDown = leftDown
    end)
    flyout:SetScript("OnHide", function(self)
        if activeSectionFlyout == self then activeSectionFlyout = nil end
    end)
    flyout:Show()
    return flyout
end

local function CreateSectionSelector(Window, parent, label, x, y, width, state, getAvailable, defaultSelected)
    local caption = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    caption:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    caption:SetText(label)

    local button
    local function Refresh()
        local available = getAvailable and getAvailable() or GetAvailableSections(nil, true)
        SyncStateToAvailability(state, available, defaultSelected)
        button:SetText(SummarizeSections(state, available))
    end

    button = Window:CreateButton(parent, "All sections", width or 220, 26, function(self)
        local available = getAvailable and getAvailable() or GetAvailableSections(nil, true)
        SyncStateToAvailability(state, available, defaultSelected)
        if activeSectionFlyout and activeSectionFlyout.owner == self then
            HideActiveSectionFlyout()
            return
        end
        local flyout = CreateSectionFlyout(self, state, available, Refresh)
        flyout.owner = self
    end, "Choose which Profile Vault sections this action uses.")
    button:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 18)
    button.Refresh = Refresh
    Refresh()
    return button
end

function Page:Create(CreateScrollablePage, Window, CreateWrappedText)
    local Theme = Helpers.GetTheme and Helpers.GetTheme()
    local profilesPage, content = CreateScrollablePage("Profiles")
    Helpers.CreatePageIntro(content, Window, "Profile Vault", "Save and restore character setup backups. Backups include action bars, account macros, character macros, keybinds, character name, realm, class, spec, backup date, and your custom backup name. Restore and delete actions require confirmation.", CreateWrappedText, 720)

    local createOptions = { actions = true, macros = true, bindings = true, pets = false, editMode = false }
    local exportOptions = { actions = true, macros = true, bindings = true, pets = true, editMode = true }
    local importOptions = { actions = true, macros = true, bindings = true, pets = true, editMode = true }

    local createCard = Helpers.CreatePageCard(content, "Create Backup", 704, 126)
    createCard:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -92)
    Helpers.SetCardDescription(createCard, "Name this backup and choose which parts of this character's setup to save.")

    local pBackupNameInput = CreateFrame("EditBox", nil, createCard, "InputBoxTemplate")
    pBackupNameInput:SetSize(220, 24)
    pBackupNameInput:SetPoint("TOPLEFT", createCard, "TOPLEFT", 18, -64)
    pBackupNameInput:SetAutoFocus(false)
    Window:SetTooltip(pBackupNameInput, "Backup Name", "Name this backup before saving your current action bars, macros, and keybinds.")

    local inputLabel = createCard:CreateFontString(nil, "OVERLAY", Theme and Theme.GetFont and Theme.GetFont("label") or "GameFontNormalSmall")
    inputLabel:SetPoint("BOTTOMLEFT", pBackupNameInput, "TOPLEFT", 0, 4)
    inputLabel:SetText("Backup Name")
    if Theme and Theme.ApplyLabelFont then Theme.ApplyLabelFont(inputLabel) end

    local pBtnCreate = Window:CreateButton(createCard, "Create Backup", 160, 30, function()
        if not SinsToolkit.ProfileVault then return end
        local name = pBackupNameInput:GetText()
        local backup, message = SinsToolkit.ProfileVault:CreateBackup(name, ReadSectionState(createOptions))
        if not backup then
            print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. (message or "Profile backup could not be saved."))
            return
        end
        Window.selectedProfileVaultBackup = backup.id
        print("|cff9b59b6[Sin's Toolkit: QOL]|r Profile backup saved: " .. backup.name)
        if Window.RefreshProfilesPage then Window:RefreshProfilesPage() end
    end, "Saves the selected parts of this character's setup into Profile Vault.")
    pBtnCreate:SetPoint("LEFT", pBackupNameInput, "RIGHT", 14, 0)
    local createSelector = CreateSectionSelector(Window, createCard, "Backup Contents", 430, -62, 238, createOptions, function()
        return GetAvailableSections(nil, true)
    end, true)

    local listCard = Helpers.ClearCardBody(Helpers.CreatePageCard(content, "Saved Backups", 386, 430))
    listCard:SetPoint("TOPLEFT", createCard, "BOTTOMLEFT", 0, -14)

    local pBackupList = CreateFrame("Frame", nil, listCard, "BackdropTemplate")
    pBackupList:SetSize(350, 366)
    pBackupList:SetPoint("TOPLEFT", listCard, "TOPLEFT", 18, -46)
    Helpers.ApplyListBackdrop(pBackupList, 0.1, 0.1, 0.1, 0.8, 0.4, 0.4, 0.4, 1)

    local pScrollFrame = CreateFrame("ScrollFrame", nil, pBackupList, "UIPanelScrollFrameTemplate")
    pScrollFrame:SetPoint("TOPLEFT", 4, -4)
    pScrollFrame:SetPoint("BOTTOMRIGHT", -26, 4)
    local pScrollChild = CreateFrame("Frame", nil, pScrollFrame)
    pScrollChild:SetSize(318, 1)
    pScrollFrame:SetScrollChild(pScrollChild)

    local detailsCard = Helpers.ClearCardBody(Helpers.CreatePageCard(content, "Backup Details", 304, 430))
    detailsCard:SetPoint("TOPLEFT", listCard, "TOPRIGHT", 14, 0)

    local detailsTextBox = CreateFrame("Frame", nil, detailsCard, "BackdropTemplate")
    detailsTextBox:SetSize(268, 224)
    detailsTextBox:SetPoint("TOPLEFT", detailsCard, "TOPLEFT", 18, -46)
    Helpers.ApplyListBackdrop(detailsTextBox, 0.06, 0.06, 0.07, 0.55, 0.18, 0.18, 0.22, 0.45)

    local detailsScroll = CreateFrame("ScrollFrame", nil, detailsTextBox, "UIPanelScrollFrameTemplate")
    detailsScroll:SetPoint("TOPLEFT", 4, -4)
    detailsScroll:SetPoint("BOTTOMRIGHT", -24, 4)
    local detailsScrollChild = CreateFrame("Frame", nil, detailsScroll)
    detailsScrollChild:SetSize(236, 1)
    detailsScroll:SetScrollChild(detailsScrollChild)

    local pSummaryDetails = detailsScrollChild:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pSummaryDetails:SetPoint("TOPLEFT", detailsScrollChild, "TOPLEFT", 0, 0)
    pSummaryDetails:SetWidth(236)
    pSummaryDetails:SetJustifyH("LEFT")
    pSummaryDetails:SetJustifyV("TOP")
    if Theme and Theme.ApplyStatusText then Theme.ApplyStatusText(pSummaryDetails) end

    local function SetDetailsText(text)
        pSummaryDetails:SetText(text or "")
        detailsScrollChild:SetHeight(math.max(1, (pSummaryDetails:GetStringHeight() or 1) + 8))
    end
    SetDetailsText("No saved backups yet. Create a backup to store this character's current setup.")

    local pBtnRestore = Window:CreateButton(detailsCard, "Restore to This Character", 268, 28, function()
        if not SinsToolkit.ProfileVault or not Window.selectedProfileVaultBackup then return end
        SinsToolkit.ProfileVault:ConfirmRestoreBackup(Window.selectedProfileVaultBackup, function(ok, message)
            print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. (message or (ok and "Profile restored." or "Profile restore failed.")))
        end)
    end, "Applies the selected backup to the current character after confirmation. This overwrites action bars, updates or creates macros, and replaces keybindings.")
    pBtnRestore:SetPoint("TOPLEFT", detailsCard, "TOPLEFT", 18, -384)

    local pBtnDelete = Window:CreateButton(detailsCard, "Delete Profile", 128, 28, function()
        if not SinsToolkit.ProfileVault or not Window.selectedProfileVaultBackup then return end
        SinsToolkit.ProfileVault:ConfirmDeleteBackup(Window.selectedProfileVaultBackup, function(ok, message)
            if ok then
                Window.selectedProfileVaultBackup = nil
                if Window.RefreshProfilesPage then Window:RefreshProfilesPage() end
            end
            print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. (message or (ok and "Profile backup deleted." or "Profile backup delete failed.")))
        end)
    end, "Deletes the selected backup from Sin's Toolkit saved variables after confirmation.")
    pBtnDelete:SetPoint("TOPLEFT", detailsCard, "TOPLEFT", 158, -342)

    local pBtnExport = Window:CreateButton(detailsCard, "Export Backup", 128, 28, function()
        if not SinsToolkit.ProfileVault or not Window.selectedProfileVaultBackup then return end
        local text, message = SinsToolkit.ProfileVault:ExportBackup(Window.selectedProfileVaultBackup, ReadSectionState(exportOptions))
        if text and StaticPopup_Show then
            StaticPopup_Show("SINS_TOOLKIT_PROFILE_EXPORT", "Copy Profile Vault Export", nil, { text = text })
        else
            print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. (message or "Profile backup export failed."))
        end
    end, "Opens a copyable text export for the selected backup.")
    pBtnExport:SetPoint("TOPLEFT", detailsCard, "TOPLEFT", 18, -342)

    local currentContents = nil
    local exportSelector = CreateSectionSelector(Window, detailsCard, "Export Contents", 18, -284, 268, exportOptions, function()
        return GetAvailableSections(currentContents, false)
    end, true)

    local importCard = Helpers.ClearCardBody(Helpers.CreatePageCard(content, "Import Backup", 704, 170))
    importCard:SetPoint("TOPLEFT", listCard, "BOTTOMLEFT", 0, -14)
    Helpers.SetCardDescription(importCard, "Paste a Sin's Toolkit Profile Vault export. Imported profiles are saved as normal backups before restore.")

    local importBox = CreateFrame("EditBox", nil, importCard, "InputBoxTemplate")
    importBox:SetSize(668, 24)
    importBox:SetPoint("TOPLEFT", importCard, "TOPLEFT", 18, -70)
    importBox:SetAutoFocus(false)
    importBox:SetMaxLetters(0)
    Window:SetTooltip(importBox, "Import Text", "Paste exported Profile Vault text here. Imports are validated before being saved.")

    local function FinishImport(duplicateMode)
        if not SinsToolkit.ProfileVault then return end
        local backup, message, extra = SinsToolkit.ProfileVault:ImportBackup(importBox:GetText(), nil, ReadSectionState(importOptions), duplicateMode)
        if backup then
            Window.selectedProfileVaultBackup = backup.id
            importBox:SetText("")
            print("|cff9b59b6[Sin's Toolkit: QOL]|r Imported profile backup saved, not restored yet: " .. (backup.name or "Imported Backup"))
            if Window.RefreshProfilesPage then Window:RefreshProfilesPage() end
        elseif message == "DUPLICATE_BACKUP" and extra and StaticPopup_Show then
            StaticPopup_Show("SINS_TOOLKIT_PROFILE_IMPORT_DUPLICATE", extra.name or "Imported Backup", nil, {
                finish = function(mode) FinishImport(mode) end,
                cancel = function()
                    print("|cff9b59b6[Sin's Toolkit: QOL]|r Import canceled.")
                end,
            })
        else
            print("|cff9b59b6[Sin's Toolkit: QOL]|r Import rejected: " .. (message or "Profile backup import failed."))
        end
    end

    local importButton = Window:CreateButton(importCard, "Import Backup", 150, 28, function()
        FinishImport(nil)
    end, "Validates pasted text and saves it as a normal Profile Vault backup.")
    importButton:SetPoint("TOPLEFT", importCard, "TOPLEFT", 320, -112)
    local importSelector = CreateSectionSelector(Window, importCard, "Import Contents", 18, -94, 268, importOptions, function()
        return GetAvailableSections(nil, true)
    end, true)

    local profileButtons = {}
    function Window:RefreshProfilesPage()
        for _, b in ipairs(profileButtons) do b:Hide() end
        if not SinsToolkit.ProfileVault then return end
        local backups = SinsToolkit.ProfileVault:GetBackups()
        local groups, backupByID = BuildBackupGroups(backups)
        if listCard.title then
            listCard.title:SetText("Saved Backups (" .. #backups .. ")")
        end
        Window.profileVaultExpandedClasses = Window.profileVaultExpandedClasses or {}
        Window.profileVaultExpandedSpecs = Window.profileVaultExpandedSpecs or {}
        local y = -2
        local hasSelection = false
        local rowIndex = 0

        local function AddRow(rowType, key, label, indent, height, backupID, onClick)
            rowIndex = rowIndex + 1
            local b = profileButtons[rowIndex]
            if not b then
                b = CreateFrame("Button", nil, pScrollChild, "BackdropTemplate")
                b.text = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                b.text:SetJustifyH("LEFT")
                if Theme and Theme.ApplySmallBodyFont then Theme.ApplySmallBodyFont(b.text) end
                b:SetScript("OnClick", function(self)
                    if self.onClick then self.onClick(self) end
                end)
                profileButtons[rowIndex] = b
            end
            b:SetSize(318, height)
            b.backupID = backupID
            b.onClick = onClick
            b.text:ClearAllPoints()
            b.text:SetPoint("LEFT", 6 + (indent or 0), 0)
            b.text:SetPoint("RIGHT", -6, 0)
            b:SetPoint("TOPLEFT", 0, y)
            b.text:SetText(label)
            if rowType == "backup" and Window.selectedProfileVaultBackup == backupID then
                Helpers.ApplySelectionBackdrop(b)
                hasSelection = true
            elseif rowType == "class" then
                Helpers.ApplyListBackdrop(b, 0.16, 0.12, 0.22, 0.86, 0.34, 0.22, 0.44, 0.75)
            elseif rowType == "spec" then
                Helpers.ApplyListBackdrop(b, 0.12, 0.12, 0.16, 0.76, 0.24, 0.24, 0.30, 0.65)
            else
                b:SetBackdrop(nil)
            end
            b:Show()
            y = y - height - 3
            return b
        end

        if #backups > 0 then
            for _, classGroup in ipairs(groups) do
                local classKey = EscapeKey(classGroup.key)
                if Window.profileVaultExpandedClasses[classKey] == nil then
                    Window.profileVaultExpandedClasses[classKey] = true
                end
                local classOpen = Window.profileVaultExpandedClasses[classKey]
                AddRow("class", classKey, ("%s %s (%d)"):format(classOpen and "-" or "+", WrapClassColor(classGroup.name, classGroup.token), classGroup.count), 0, 24, nil, function()
                    Window.profileVaultExpandedClasses[classKey] = not Window.profileVaultExpandedClasses[classKey]
                    Window:RefreshProfilesPage()
                end)

                if classOpen then
                    for _, specGroup in ipairs(classGroup.specOrder) do
                        local specKey = classKey .. "::" .. EscapeKey(specGroup.key)
                        if Window.profileVaultExpandedSpecs[specKey] == nil then
                            Window.profileVaultExpandedSpecs[specKey] = true
                        end
                        local specOpen = Window.profileVaultExpandedSpecs[specKey]
                        AddRow("spec", specKey, ("%s %s (%d)"):format(specOpen and "-" or "+", specGroup.name, specGroup.count), 14, 23, nil, function()
                            Window.profileVaultExpandedSpecs[specKey] = not Window.profileVaultExpandedSpecs[specKey]
                            Window:RefreshProfilesPage()
                        end)

                        if specOpen then
                            for _, backup in ipairs(specGroup.backups) do
                                AddRow("backup", backup.id, ("%s - %s - %s"):format(backup.name, backup.characterKey, backup.createdAt), 28, 27, backup.id, function(self)
                                    Window.selectedProfileVaultBackup = self.backupID
                                    Window:RefreshProfilesPage()
                                end)
                            end
                        end
                    end
                end
            end
        end
        for i = rowIndex + 1, #profileButtons do
            profileButtons[i]:Hide()
            profileButtons[i].onClick = nil
            profileButtons[i].backupID = nil
        end
        if Window.selectedProfileVaultBackup and not backupByID[Window.selectedProfileVaultBackup] then
            Window.selectedProfileVaultBackup = nil
        end
        pScrollChild:SetHeight(math.max(1, -y))

        if Window.selectedProfileVaultBackup then
            SetDetailsText(SinsToolkit.ProfileVault:GetRestoreSummary(Window.selectedProfileVaultBackup) or "No details available.")
            currentContents = SinsToolkit.ProfileVault:GetBackupContents(Window.selectedProfileVaultBackup)
            if exportSelector.Refresh then exportSelector:Refresh() end
            pBtnRestore:Enable()
            pBtnDelete:Enable()
            pBtnExport:Enable()
        elseif #backups == 0 then
            SetDetailsText("No saved backups yet. Create a backup to store this character's current action bars, macros, and keybinds.")
            currentContents = nil
            if exportSelector.Refresh then exportSelector:Refresh() end
            pBtnRestore:Disable()
            pBtnDelete:Disable()
            pBtnExport:Disable()
        else
            SetDetailsText("Select a saved backup to view its details.")
            currentContents = nil
            if exportSelector.Refresh then exportSelector:Refresh() end
            pBtnRestore:Disable()
            pBtnDelete:Disable()
            pBtnExport:Disable()
        end
    end
    profilesPage.OnShowCallback = function() Window:RefreshProfilesPage() end

    content:SetHeight(900)
    self.frame = profilesPage
    return profilesPage
end

Page:Register("Profiles")

return Page
