local _, SinsToolkit = ...

local Vault = {}
SinsToolkit.ProfileVault = Vault

local MAX_ACTION_SLOTS = 180
local CHARACTER_MACRO_START = (MAX_ACCOUNT_MACROS or 120) + 1
local HasActionCompat = (C_ActionBar and C_ActionBar.HasAction) or HasAction
local EXPORT_PREFIX = "SINSTOOLKIT_PROFILE_VAULT_V1:"
local EXPORT_FORMAT_VERSION = 1
local MAX_IMPORT_LENGTH = 1024 * 1024

local GetCharacterKey
local GetClassInfo
local GetSpecInfo
local DeepCopy

local SECTION_DEFS = {
    { key = "actions", label = "Action Bars" },
    { key = "macros", label = "Macros" },
    { key = "bindings", label = "Keybindings" },
    { key = "pets", label = "Pet Actions" },
    { key = "editMode", label = "Edit Mode Layout" },
}

local function EnsureDB()
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
end

local function GetBackup(id)
    EnsureDB()
    return SinsToolkitDB.profileVaultBackups[id]
end

local function GetCurrentRestoreContext()
    local characterKey, characterName, realmName = GetCharacterKey()
    local classToken, className = GetClassInfo()
    local specIndex, specID, specName = GetSpecInfo()
    return {
        characterKey = characterKey,
        character = characterName,
        realm = realmName,
        classToken = classToken,
        className = className,
        specIndex = specIndex,
        specID = specID,
        specName = specName,
    }
end

local function AddWarning(warnings, text)
    warnings[#warnings + 1] = "|cffffd166Warning:|r " .. text
end

local function HasMacros(macros)
    return type(macros) == "table" and (#(macros.account or {}) > 0 or #(macros.character or {}) > 0)
end

local function GetBackupContents(backup)
    backup = backup or {}
    local contents = backup.contents
    return {
        actions = contents and contents.actions ~= nil and contents.actions or (type(backup.actions) == "table" and #backup.actions > 0),
        macros = contents and contents.macros ~= nil and contents.macros or HasMacros(backup.macros),
        bindings = contents and contents.bindings ~= nil and contents.bindings or (type(backup.bindings) == "table" and #backup.bindings > 0),
        pets = contents and contents.pets ~= nil and contents.pets or (type(backup.pets) == "table" and #backup.pets > 0),
        editMode = contents and contents.editMode ~= nil and contents.editMode or (type(backup.editMode) == "table" and type(backup.editMode.layoutString) == "string" and backup.editMode.layoutString ~= ""),
    }
end

local function HasAnySection(options)
    for _, section in ipairs(SECTION_DEFS) do
        if options and options[section.key] then return true end
    end
    return false
end

local function NormalizeSectionOptions(options, backup, defaultAll)
    local contents = GetBackupContents(backup)
    local normalized = {}
    for _, section in ipairs(SECTION_DEFS) do
        local key = section.key
        if options and options[key] ~= nil then
            normalized[key] = options[key] and contents[key] and true or false
        else
            normalized[key] = defaultAll and contents[key] and true or false
        end
    end
    return normalized, contents
end

local function BuildSectionList(options, fallback)
    local values = {}
    for _, section in ipairs(SECTION_DEFS) do
        if options and options[section.key] then
            values[#values + 1] = section.label
        end
    end
    return #values > 0 and table.concat(values, ", ") or fallback
end

local function SummarizeSectionOptions(options, contents)
    local total, selected, names = 0, 0, {}
    for _, section in ipairs(SECTION_DEFS) do
        if not contents or contents[section.key] then
            total = total + 1
            if options and options[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 EmptyMacros()
    return { account = {}, character = {} }
end

local function IsPetActionsSupported()
    return GetPetActionInfo and PickupPetAction 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 FilterBackupSections(backup, options)
    local filtered = DeepCopy(backup)
    local normalized, contents = NormalizeSectionOptions(options, backup, true)
    filtered.actions = normalized.actions and (filtered.actions or {}) or {}
    filtered.macros = normalized.macros and (filtered.macros or EmptyMacros()) or EmptyMacros()
    filtered.bindings = normalized.bindings and (filtered.bindings or {}) or {}
    filtered.pets = normalized.pets and (filtered.pets or {}) or {}
    filtered.editMode = normalized.editMode and filtered.editMode or nil
    filtered.contents = {
        actions = normalized.actions and contents.actions and true or false,
        macros = normalized.macros and contents.macros and true or false,
        bindings = normalized.bindings and contents.bindings and true or false,
        pets = normalized.pets and contents.pets and true or false,
        editMode = normalized.editMode and contents.editMode and true or false,
    }
    return filtered, normalized
end

local function GetBackupSummary(backup, restoreOptions)
    if not backup then return "Backup not found." end
    local actionCount = backup.actions and #backup.actions or 0
    local accountMacroCount = backup.macros and backup.macros.account and #backup.macros.account or 0
    local characterMacroCount = backup.macros and backup.macros.character and #backup.macros.character or 0
    local bindingCount = backup.bindings and #backup.bindings or 0
    local petActionCount = backup.pets and #backup.pets or 0
    local editModeText = backup.editMode and backup.editMode.layoutName or "None"
    local restore, contents = NormalizeSectionOptions(restoreOptions, backup, true)
    local backupCharacter = backup.character or "Unknown Character"
    local backupRealm = backup.realm or ""
    local character = backup.characterKey or ((backup.realm ~= "" and (backupCharacter .. "-" .. backupRealm)) or backupCharacter)
    local className = backup.className or "Unknown Class"
    local specName = backup.specName or "Unknown Spec"
    local createdAt = backup.createdAt or "Unknown date"
    local current = GetCurrentRestoreContext()
    local warnings = {}

    if character ~= current.characterKey then
        AddWarning(warnings, "This backup was created for a different character or realm.")
    end
    if className == "Unknown Class" or specName == "Unknown Spec" then
        AddWarning(warnings, "This backup has unknown class/spec metadata.")
    else
        if backup.classToken and current.classToken and backup.classToken ~= current.classToken then
            AddWarning(warnings, "This backup class differs from your current character.")
        elseif className ~= current.className then
            AddWarning(warnings, "This backup class may differ from your current character.")
        end
        if backup.specID and current.specID and backup.specID ~= current.specID then
            AddWarning(warnings, "This backup spec differs from your current specialization.")
        elseif specName ~= current.specName then
            AddWarning(warnings, "This backup spec may differ from your current specialization.")
        end
    end

    local importedLine = backup.importedAt and ("\nImported: " .. backup.importedAt) or ""
    local warningText = (#warnings > 0 and ("\n\nWarnings:\n- " .. table.concat(warnings, "\n- ")) or "")

    local impact = {}
    if restore.actions then impact[#impact + 1] = "Overwrites this character's action bars." end
    if restore.macros then impact[#impact + 1] = "Updates or creates macros from the backup." end
    if restore.bindings then impact[#impact + 1] = "Replaces this character's keybindings." end
    if restore.pets then impact[#impact + 1] = "Restores supported pet actions." end
    if restore.editMode then impact[#impact + 1] = "Creates and activates a Blizzard Edit Mode layout from this backup." end
    impact[#impact + 1] = "May skip unavailable spells, items, mounts, or pet actions."
    impact[#impact + 1] = "Cannot be automatically undone."

    local notRestore = {}
    for _, section in ipairs(SECTION_DEFS) do
        if contents[section.key] and not restore[section.key] then
            notRestore[#notRestore + 1] = section.label
        end
    end

    return ("%s\n\nBackup Source: %s\nCurrent Character: %s\nClass / Spec: %s / %s\nCreated: %s%s%s\n\nWill restore: %s\nWill not restore: %s\n\nRestore Impact:\n- %s\n\nContains:\nActions: %d\nPet actions: %d\nAccount macros: %d\nCharacter macros: %d\nKeybindings: %d\nEdit Mode Layout: %s"):format(
        backup.name or "Selected backup",
        character,
        current.characterKey,
        className,
        specName,
        createdAt,
        importedLine,
        warningText,
        BuildSectionList(restore, "Nothing selected"),
        #notRestore > 0 and table.concat(notRestore, ", ") or "None",
        table.concat(impact, "\n- "),
        actionCount,
        petActionCount,
        accountMacroCount,
        characterMacroCount,
        bindingCount,
        editModeText
    )
end

local function ReadRestorePopupOptions(dialog)
    if dialog.profileVaultRestoreOptions then
        return DeepCopy(dialog.profileVaultRestoreOptions)
    end
    local options = {}
    for _, section in ipairs(SECTION_DEFS) do
        local check = dialog.profileVaultChecks and dialog.profileVaultChecks[section.key]
        options[section.key] = check and check:GetChecked() and true or false
    end
    return options
end

local activeRestoreFlyout

local function HideRestoreFlyout()
    if activeRestoreFlyout then
        activeRestoreFlyout:Hide()
        activeRestoreFlyout = nil
    end
end

local function StyleRestoreFlyout(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 UpdateRestorePopupText(dialog)
    local data = dialog and dialog.data
    if not data or not data.backup then return end
    local options = ReadRestorePopupOptions(dialog)
    if dialog.text then
        dialog.text:SetText("Restore Profile Vault backup?\n\n" .. GetBackupSummary(data.backup, options))
    end
    if dialog.profileVaultSelectorButton then
        local _, contents = NormalizeSectionOptions(options, data.backup, true)
        dialog.profileVaultSelectorButton:SetText("Restore Contents: " .. SummarizeSectionOptions(options, contents))
    end
end

local function ToggleRestoreSectionFlyout(dialog)
    if activeRestoreFlyout and activeRestoreFlyout.owner == dialog then
        HideRestoreFlyout()
        return
    end
    HideRestoreFlyout()
    if not dialog or not dialog.data or not dialog.data.backup or not dialog.profileVaultSelectorButton then return end

    local options, contents = NormalizeSectionOptions(dialog.profileVaultRestoreOptions, dialog.data.backup, true)
    dialog.profileVaultRestoreOptions = options

    local flyout = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    flyout.owner = dialog
    flyout:SetFrameStrata("FULLSCREEN_DIALOG")
    flyout:SetFrameLevel((dialog:GetFrameLevel() or 1) + 60)
    flyout:SetClampedToScreen(true)
    flyout:SetSize(250, 18 + (#SECTION_DEFS * 24))
    StyleRestoreFlyout(flyout)
    flyout:SetPoint("TOPLEFT", dialog.profileVaultSelectorButton, "BOTTOMLEFT", 0, -4)
    activeRestoreFlyout = flyout

    for index, section in ipairs(SECTION_DEFS) do
        local key = section.key
        local available = contents[key]
            and (key ~= "pets" or IsPetActionsSupported())
            and (key ~= "editMode" or IsEditModeLayoutSupported())
            and true or false
        local check = CreateFrame("CheckButton", nil, flyout, "UICheckButtonTemplate")
        check:SetPoint("TOPLEFT", flyout, "TOPLEFT", 10, -8 - ((index - 1) * 24))
        check:SetSize(22, 22)
        check.text = check:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        check.text:SetPoint("LEFT", check, "RIGHT", 2, 1)
        check.text:SetText(section.label)
        check.text:SetTextColor(available and 0.92 or 0.48, available and 0.9 or 0.46, available and 0.96 or 0.52)
        check.key = key
        check:SetChecked(options[key] and available)
        check:SetEnabled(available)
        check:SetScript("OnClick", function(self)
            if not contents[self.key] then return end
            dialog.profileVaultRestoreOptions[self.key] = self:GetChecked() == true
            UpdateRestorePopupText(dialog)
        end)
    end

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

StaticPopupDialogs["SINS_TOOLKIT_PROFILE_RESTORE_CONFIRM"] = {
    text = "Restore Profile Vault backup?\n\n%s",
    button1 = YES,
    button2 = CANCEL,
    OnAccept = function(_, data)
        if not data then return end
        local options = data.options
        if data.dialog then
            options = ReadRestorePopupOptions(data.dialog)
        end
        local ok, message = Vault:RestoreBackup(data.id, true, options)
        if data.callback then data.callback(ok, message) end
    end,
    OnShow = function(self, data)
        self:SetFrameStrata("FULLSCREEN_DIALOG")
        self:SetFrameLevel(1000)
        if not data or not data.backup then return end
        self.data = data
        data.dialog = self
        local options = NormalizeSectionOptions(data.options, data.backup, true)
        self.profileVaultRestoreOptions = options
        self.profileVaultSelectorButton = self.profileVaultSelectorButton or CreateFrame("Button", nil, self, "UIPanelButtonTemplate")
        self.profileVaultSelectorButton:SetSize(250, 24)
        self.profileVaultSelectorButton:ClearAllPoints()
        self.profileVaultSelectorButton:SetPoint("TOPLEFT", self.text or self, "BOTTOMLEFT", 0, -10)
        self.profileVaultSelectorButton:SetScript("OnClick", function()
            ToggleRestoreSectionFlyout(self)
        end)
        self.profileVaultSelectorButton:Show()
        if self.SetHeight then
            self:SetHeight(math.max(self:GetHeight() or 0, 440))
        end
        UpdateRestorePopupText(self)
    end,
    OnHide = function(self)
        HideRestoreFlyout()
        if self.profileVaultSelectorButton then self.profileVaultSelectorButton:Hide() end
        if self.profileVaultChecks then
            for _, check in pairs(self.profileVaultChecks) do
                check:Hide()
            end
        end
        self.profileVaultRestoreOptions = nil
        self.data = nil
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

StaticPopupDialogs["SINS_TOOLKIT_PROFILE_DELETE_CONFIRM"] = {
    text = "Delete Profile Vault backup?\n\n%s",
    button1 = DELETE or "Delete",
    button2 = CANCEL,
    OnAccept = function(_, data)
        if not data then return end
        local ok, message = Vault:DeleteBackup(data.id, true)
        if data.callback then data.callback(ok, message) end
    end,
    OnShow = function(self)
        self:SetFrameStrata("FULLSCREEN_DIALOG")
        self:SetFrameLevel(1000)
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

DeepCopy = function(value)
    if type(value) ~= "table" then return value end
    local copy = {}
    for k, v in pairs(value) do
        copy[k] = DeepCopy(v)
    end
    return copy
end

local function GetSerializer()
    return LibStub and LibStub("AceSerializer-3.0", true)
end

local function CalculateChecksum(text)
    local checksum = 0
    for i = 1, #(text or "") do
        checksum = (checksum + (string.byte(text, i) or 0) * i) % 1000000007
    end
    return tostring(checksum)
end

GetCharacterKey = function()
    local name = UnitName("player") or UNKNOWN
    local realm = GetRealmName() or ""
    return name .. "-" .. realm, name, realm
end

GetClassInfo = function()
    local localizedClass, classToken = UnitClass("player")
    return classToken or "UNKNOWN", localizedClass or "Unknown"
end

GetSpecInfo = function()
    if GetSpecialization and GetSpecializationInfo then
        local specIndex = GetSpecialization()
        if specIndex then
            local specID, specName = GetSpecializationInfo(specIndex)
            return specIndex, specID, specName or "Unknown Spec"
        end
    end
    return nil, nil, "Unknown Spec"
end

local function MakeBackupID()
    local characterKey = GetCharacterKey()
    return characterKey .. "-" .. time()
end

local function MakeImportedBackupID()
    local characterKey = GetCharacterKey()
    return characterKey .. "-import-" .. time() .. "-" .. math.random(1000, 9999)
end

local function GetDuplicateGroupValue(backup, primary, fallback)
    local value = backup and backup[primary]
    if value == nil or value == "" then value = backup and backup[fallback] end
    return tostring(value or "")
end

local function BackupsShareImportGroup(a, b)
    if not a or not b then return false end
    return GetDuplicateGroupValue(a, "characterKey", "character") == GetDuplicateGroupValue(b, "characterKey", "character")
        and GetDuplicateGroupValue(a, "classToken", "className") == GetDuplicateGroupValue(b, "classToken", "className")
        and GetDuplicateGroupValue(a, "specID", "specName") == GetDuplicateGroupValue(b, "specID", "specName")
end

local function FindDuplicateBackup(imported)
    EnsureDB()
    local name = imported and imported.name
    if not name or name == "" then return nil end
    for id, backup in pairs(SinsToolkitDB.profileVaultBackups or {}) do
        if backup.name == name and BackupsShareImportGroup(backup, imported) then
            return id, backup
        end
    end
    return nil
end

local function NameExistsInImportGroup(name, imported)
    for _, backup in pairs(SinsToolkitDB.profileVaultBackups or {}) do
        if backup.name == name and BackupsShareImportGroup(backup, imported) then
            return true
        end
    end
    return false
end

local function MakeImportedCopyName(imported)
    local base = imported.name or "Imported Backup"
    local candidate = base .. " (Imported)"
    if not NameExistsInImportGroup(candidate, imported) then return candidate end
    local index = 2
    repeat
        candidate = ("%s (Imported %d)"):format(base, index)
        index = index + 1
    until not NameExistsInImportGroup(candidate, imported)
    return candidate
end

local function CaptureAction(slot)
    local actionType, id, subType = GetActionInfo(slot)
    if not actionType or not id then return nil end

    local action = { slot = slot, type = actionType, id = id, subType = subType }
    if actionType == "macro" then
        local name, icon, body = GetMacroInfo(id)
        action.name = name
        action.icon = icon
        action.body = body
        action.perCharacter = id >= CHARACTER_MACRO_START
    elseif actionType == "equipmentset" and C_EquipmentSet then
        action.name = C_EquipmentSet.GetEquipmentSetInfo(id)
    end
    return action
end

local function CaptureActions()
    local actions = {}
    for slot = 1, MAX_ACTION_SLOTS do
        local action = CaptureAction(slot)
        if action then
            actions[#actions + 1] = action
        end
    end
    return actions
end

local function EmptyActions()
    return {}
end

local function CaptureMacros()
    local macros = { account = {}, character = {} }
    local accountCount, characterCount = GetNumMacros()

    for index = 1, accountCount do
        local name, icon, body = GetMacroInfo(index)
        if name then
            macros.account[#macros.account + 1] = { id = index, name = name, icon = icon, body = body }
        end
    end

    for offset = 1, characterCount do
        local index = CHARACTER_MACRO_START + offset - 1
        local name, icon, body = GetMacroInfo(index)
        if name then
            macros.character[#macros.character + 1] = { id = index, name = name, icon = icon, body = body }
        end
    end

    return macros
end

local function EmptyBindings()
    return {}
end

local function CaptureBindings()
    local bindings = {}
    for index = 1, GetNumBindings() do
        local command, _, key1, key2 = GetBinding(index)
        if command and (key1 or key2) then
            bindings[#bindings + 1] = { command = command, key1 = key1, key2 = key2 }
        end
    end
    return bindings
end

local function EmptyPetActions()
    return {}
end

local function CapturePetActions()
    local pets = {}
    if not GetPetActionInfo or not NUM_PET_ACTION_SLOTS then return pets end
    for slot = 1, NUM_PET_ACTION_SLOTS do
        local name, texture, isToken, isActive, autoCastAllowed, autoCastEnabled, spellID = GetPetActionInfo(slot)
        if name or spellID then
            pets[#pets + 1] = {
                slot = slot,
                name = name,
                isToken = isToken and true or false,
                spellID = spellID,
                autoCastEnabled = autoCastEnabled and true or false,
            }
        end
    end
    return pets
end

local function CaptureEditModeLayout()
    if not IsEditModeLayoutSupported() then return nil end
    local okLayouts, layouts = pcall(C_EditMode.GetLayouts)
    if not okLayouts or type(layouts) ~= "table" or type(layouts.layouts) ~= "table" then return nil end
    local activeIndex = layouts.activeLayout
    local activeLayout = activeIndex and layouts.layouts[activeIndex]
    if type(activeLayout) ~= "table" then return nil end
    local okString, layoutString = pcall(C_EditMode.ConvertLayoutInfoToString, activeLayout)
    if not okString or type(layoutString) ~= "string" or layoutString == "" then return nil end
    return {
        layoutName = activeLayout.layoutName or "Edit Mode Layout",
        layoutType = activeLayout.layoutType,
        layoutString = layoutString,
        capturedAt = date("%Y-%m-%d %H:%M:%S"),
        capturedAtEpoch = time(),
    }
end

function Vault:CreateBackup(name, options)
    EnsureDB()

    name = (name or ""):match("^%s*(.-)%s*$")
    if name == "" then
        name = "Backup"
    end

    local characterKey, characterName, realmName = GetCharacterKey()
    local classToken, className = GetClassInfo()
    local specIndex, specID, specName = GetSpecInfo()
    local selected = {
        actions = not options or options.actions ~= false,
        macros = not options or options.macros ~= false,
        bindings = not options or options.bindings ~= false,
        pets = not options or options.pets ~= false,
        editMode = options and options.editMode == true and IsEditModeLayoutSupported() or false,
    }
    if not HasAnySection(selected) then
        return nil, "Select at least one Profile Vault section to save."
    end
    local backup = {
        id = MakeBackupID(),
        name = name,
        character = characterName,
        realm = realmName,
        characterKey = characterKey,
        classToken = classToken,
        className = className,
        specIndex = specIndex,
        specID = specID,
        specName = specName,
        createdAt = date("%Y-%m-%d %H:%M:%S"),
        createdAtEpoch = time(),
        actions = selected.actions and CaptureActions() or EmptyActions(),
        macros = selected.macros and CaptureMacros() or EmptyMacros(),
        bindings = selected.bindings and CaptureBindings() or EmptyBindings(),
        pets = selected.pets and CapturePetActions() or EmptyPetActions(),
        editMode = selected.editMode and CaptureEditModeLayout() or nil,
    }
    backup.contents = {
        actions = selected.actions and true or false,
        macros = selected.macros and true or false,
        bindings = selected.bindings and true or false,
        pets = selected.pets and true or false,
        editMode = selected.editMode and backup.editMode ~= nil or false,
    }
    if not HasAnySection(backup.contents) then
        return nil, "The selected Profile Vault sections did not capture any restorable data."
    end

    SinsToolkitDB.profileVaultBackups[backup.id] = backup
    return backup
end

local function ClearActionSlot(slot)
    if HasActionCompat and HasActionCompat(slot) then
        PickupAction(slot)
        ClearCursor()
    end
end

local function FindMacroIndex(name, perCharacter)
    local accountCount, characterCount = GetNumMacros()
    local startIndex = perCharacter and CHARACTER_MACRO_START or 1
    local endIndex = perCharacter and (CHARACTER_MACRO_START + characterCount - 1) or accountCount

    for index = startIndex, endIndex do
        local macroName = GetMacroInfo(index)
        if macroName == name then
            return index
        end
    end
end

local function BuildMacroIndex()
    local byNameBody, byBody, byName = {}, {}, {}
    local accountCount, characterCount = GetNumMacros()

    local function AddMacro(index)
        local name, icon, body = GetMacroInfo(index)
        if not name then return end
        body = body or ""
        byNameBody[name .. "\031" .. body] = index
        byBody[body] = byBody[body] or index
        byName[name] = byName[name] or index
    end

    for index = 1, accountCount do AddMacro(index) end
    for offset = 1, characterCount do AddMacro(CHARACTER_MACRO_START + offset - 1) end

    return byNameBody, byBody, byName
end

local function FindMacroByContent(macro, byNameBody, byBody, byName)
    if not macro then return nil end
    local name = macro.name
    local body = macro.body or ""
    if name and byNameBody[name .. "\031" .. body] then return byNameBody[name .. "\031" .. body] end
    if body ~= "" and byBody[body] then return byBody[body] end
    if name and byName[name] then return byName[name] end
end

local function UpsertMacro(macro, perCharacter)
    if not macro or not macro.name then return end

    local index = FindMacroIndex(macro.name, perCharacter)
    if index then
        EditMacro(index, macro.name, macro.icon or "INV_Misc_QuestionMark", macro.body or "")
        return index
    end

    local created = CreateMacro(macro.name, macro.icon or "INV_Misc_QuestionMark", macro.body or "", perCharacter)
    return created or FindMacroIndex(macro.name, perCharacter)
end

local function RestoreMacros(macros, actions, allowCreate)
    local macroIdMap = {}

    if allowCreate then
        for _, macro in ipairs((macros and macros.account) or {}) do
            local index = UpsertMacro(macro, false)
            if index and macro.id then macroIdMap[macro.id] = index end
        end
        for _, macro in ipairs((macros and macros.character) or {}) do
            local index = UpsertMacro(macro, true)
            if index and macro.id then macroIdMap[macro.id] = index end
        end
    end

    local byNameBody, byBody, byName = BuildMacroIndex()
    for _, action in ipairs(actions or {}) do
        if action.type == "macro" then
            local index = macroIdMap[action.id] or FindMacroByContent(action, byNameBody, byBody, byName)
            if index and action.id then macroIdMap[action.id] = index end
        end
    end

    return macroIdMap
end

local function FindMountIndexByID(mountID)
    if not (C_MountJournal and C_MountJournal.GetNumDisplayedMounts and C_MountJournal.GetDisplayedMountInfo) then return nil end
    for index = 1, C_MountJournal.GetNumDisplayedMounts() do
        local _, _, _, _, _, _, _, _, _, _, isCollected, candidateID = C_MountJournal.GetDisplayedMountInfo(index)
        if isCollected and candidateID == mountID then
            return index
        end
    end
end

local function PickupStoredAction(action, macroIdMap)
    if not action then return false end

    if action.type == "spell" then
        if C_Spell and C_Spell.PickupSpell then
            C_Spell.PickupSpell(action.id)
        elseif PickupSpell then
            PickupSpell(action.id)
        end
    elseif action.type == "item" then
        if C_Item and C_Item.PickupItem then
            C_Item.PickupItem(action.id)
        elseif PickupItem then
            PickupItem(action.id)
        end
    elseif action.type == "macro" then
        local macroID = macroIdMap and macroIdMap[action.id]
        PickupMacro(macroID or action.name or action.id)
    elseif action.type == "flyout" and PickupSpellBookItem then
        PickupSpellBookItem(action.id, BOOKTYPE_SPELL)
    elseif action.type == "equipmentset" and C_EquipmentSet and action.name then
        C_EquipmentSet.PickupEquipmentSetByName(action.name)
    elseif action.type == "summonmount" and C_MountJournal and C_MountJournal.Pickup then
        C_MountJournal.Pickup(FindMountIndexByID(action.id) or action.id)
    elseif action.type == "companion" and PickupCompanion then
        PickupCompanion(action.subType or "CRITTER", action.id)
    end

    return GetCursorInfo() ~= nil
end

local function RestorePetActions(pets)
    if not pets or #pets == 0 then return 0, 0 end
    if not (GetPetActionInfo and PickupPetAction and NUM_PET_ACTION_SLOTS) then return 0, #pets end
    local restored, skipped = 0, 0
    local tokenSlots = {}
    for slot = 1, NUM_PET_ACTION_SLOTS do
        local name, _, isToken = GetPetActionInfo(slot)
        if name and isToken then tokenSlots[name] = slot end
    end
    for _, petAction in ipairs(pets) do
        ClearCursor()
        local slot = tonumber(petAction.slot)
        if slot and slot >= 1 and slot <= NUM_PET_ACTION_SLOTS then
            if petAction.isToken and petAction.name and tokenSlots[petAction.name] then
                PickupPetAction(tokenSlots[petAction.name])
            elseif petAction.spellID and PickupPetSpell then
                PickupPetSpell(petAction.spellID)
            end
            if GetCursorInfo() then
                PickupPetAction(slot)
                restored = restored + 1
            else
                skipped = skipped + 1
            end
        else
            skipped = skipped + 1
        end
        ClearCursor()
    end
    return restored, skipped
end

local function RestoreActions(actions, macros, allowMacroCreate)
    local restored = 0
    local skipped = 0
    local slotBucket = {}
    local macroIdMap = RestoreMacros(macros, actions, allowMacroCreate)

    for _, action in ipairs(actions or {}) do
        ClearCursor()
        local slot = tonumber(action.slot)
        if slot and slot >= 1 and slot <= MAX_ACTION_SLOTS then
            slotBucket[slot] = true
        end
        if slot and slot >= 1 and slot <= MAX_ACTION_SLOTS and PickupStoredAction(action, macroIdMap) then
            PlaceAction(slot)
            restored = restored + 1
        else
            skipped = skipped + 1
        end
        ClearCursor()
    end

    for slot = 1, MAX_ACTION_SLOTS do
        if not slotBucket[slot] then
            ClearActionSlot(slot)
        end
    end

    return restored, skipped
end

local function RestoreBindings(bindings)
    for index = 1, GetNumBindings() do
        local _, _, key1, key2 = GetBinding(index)
        if key1 then SetBinding(key1, nil) end
        if key2 then SetBinding(key2, nil) end
    end

    for _, binding in ipairs(bindings or {}) do
        local key1, key2 = GetBindingKey(binding.command)
        if key1 then SetBinding(key1, nil) end
        if key2 then SetBinding(key2, nil) end
    end

    for _, binding in ipairs(bindings or {}) do
        if binding.key1 then SetBinding(binding.key1, binding.command) end
        if binding.key2 then SetBinding(binding.key2, binding.command) end
    end
    SaveBindings(GetCurrentBindingSet())
end

local function RestoreEditModeLayout(editMode, backupName)
    if not editMode or type(editMode.layoutString) ~= "string" or editMode.layoutString == "" then
        return false, "No Edit Mode layout was stored in this backup."
    end
    if not IsEditModeLayoutSupported() then
        return false, "Blizzard Edit Mode layout APIs are not available."
    end
    if InCombatLockdown and InCombatLockdown() then
        return false, "Edit Mode layout restore is blocked while in combat."
    end
    if C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_EditMode")
    elseif LoadAddOn then
        pcall(LoadAddOn, "Blizzard_EditMode")
    end
    if not (EditModeManagerFrame and EditModeManagerFrame.ImportLayout) then
        return false, "Blizzard Edit Mode manager is not available."
    end
    local okLayout, layoutInfo = pcall(C_EditMode.ConvertStringToLayoutInfo, editMode.layoutString)
    if not okLayout or type(layoutInfo) ~= "table" then
        return false, "Stored Edit Mode layout could not be decoded."
    end
    local layoutType = editMode.layoutType or layoutInfo.layoutType
    local layoutName = ("Sin's Toolkit - %s"):format(backupName or editMode.layoutName or "Profile Vault")
    if C_EditMode.IsValidLayoutName and not C_EditMode.IsValidLayoutName(layoutName) then
        layoutName = "Sin's Toolkit Profile Vault"
    end
    local okImport, err = pcall(EditModeManagerFrame.ImportLayout, EditModeManagerFrame, layoutInfo, layoutType, layoutName)
    if not okImport then
        return false, "Blizzard Edit Mode layout import failed: " .. tostring(err)
    end
    return true, layoutName
end

local function ValidateAction(action)
    if type(action) ~= "table" then return false, "Action entry is not a table." end
    local slot = tonumber(action.slot)
    if not slot or slot < 1 or slot > MAX_ACTION_SLOTS then return false, "Action slot is out of range." end
    if type(action.type) ~= "string" or action.type == "" then return false, "Action type is missing." end
    if action.id == nil then return false, "Action id is missing." end
    if action.type == "macro" then
        if action.name ~= nil and type(action.name) ~= "string" then return false, "Macro action name is invalid." end
        if action.body ~= nil and type(action.body) ~= "string" then return false, "Macro action body is invalid." end
    end
    return true
end

local function ValidateMacro(macro)
    if type(macro) ~= "table" then return false, "Macro entry is not a table." end
    if type(macro.name) ~= "string" or macro.name == "" then return false, "Macro name is missing." end
    if macro.body ~= nil and type(macro.body) ~= "string" then return false, "Macro body is invalid." end
    return true
end

local function ValidateBinding(binding)
    if type(binding) ~= "table" then return false, "Binding entry is not a table." end
    if type(binding.command) ~= "string" or binding.command == "" then return false, "Binding command is missing." end
    if binding.key1 ~= nil and type(binding.key1) ~= "string" then return false, "Binding key1 is invalid." end
    if binding.key2 ~= nil and type(binding.key2) ~= "string" then return false, "Binding key2 is invalid." end
    return true
end

local function ValidatePetAction(petAction)
    if type(petAction) ~= "table" then return false, "Pet action entry is not a table." end
    local slot = tonumber(petAction.slot)
    local maxPetSlots = NUM_PET_ACTION_SLOTS or 10
    if not slot or slot < 1 or slot > maxPetSlots then return false, "Pet action slot is out of range." end
    if petAction.name ~= nil and type(petAction.name) ~= "string" then return false, "Pet action name is invalid." end
    if petAction.spellID ~= nil and type(petAction.spellID) ~= "number" then return false, "Pet action spell id is invalid." end
    return true
end

local function ValidateEditMode(editMode)
    if editMode == nil then return true end
    if type(editMode) ~= "table" then return false, "Edit Mode layout entry is invalid." end
    if editMode.layoutString ~= nil and type(editMode.layoutString) ~= "string" then return false, "Edit Mode layout string is invalid." end
    if editMode.layoutName ~= nil and type(editMode.layoutName) ~= "string" then return false, "Edit Mode layout name is invalid." end
    return true
end

function Vault:ValidateBackupPayload(payload)
    if type(payload) ~= "table" then return false, "Imported payload is not a table." end
    if payload.addon ~= "SinsToolkit" or payload.feature ~= "ProfileVault" then return false, "Imported text is not a Profile Vault export." end
    if payload.formatVersion ~= EXPORT_FORMAT_VERSION then return false, "Unsupported Profile Vault export version." end
    local backup = payload.backup
    if type(backup) ~= "table" then return false, "Imported backup is missing." end
    backup.actions = backup.actions or {}
    backup.macros = backup.macros or EmptyMacros()
    backup.bindings = backup.bindings or {}
    backup.pets = backup.pets or {}
    if type(backup.actions) ~= "table" then return false, "Imported backup actions are missing or invalid." end
    if type(backup.macros) ~= "table" then return false, "Imported backup macros are missing or invalid." end
    if type(backup.bindings) ~= "table" then return false, "Imported backup bindings are missing or invalid." end
    if backup.macros.account ~= nil and type(backup.macros.account) ~= "table" then return false, "Imported account macros are invalid." end
    if backup.macros.character ~= nil and type(backup.macros.character) ~= "table" then return false, "Imported character macros are invalid." end
    if type(backup.pets) ~= "table" then return false, "Imported backup pet actions are invalid." end
    local editModeValid, editModeMessage = ValidateEditMode(backup.editMode)
    if not editModeValid then return false, editModeMessage end
    if #backup.actions > MAX_ACTION_SLOTS then return false, "Imported backup has too many action entries." end
    if backup.pets and #backup.pets > (NUM_PET_ACTION_SLOTS or 10) then return false, "Imported backup has too many pet action entries." end
    if #(backup.macros.account or {}) > (MAX_ACCOUNT_MACROS or 120) then return false, "Imported backup has too many account macros." end
    if #(backup.macros.character or {}) > (MAX_CHARACTER_MACROS or 18) then return false, "Imported backup has too many character macros." end

    for _, action in ipairs(backup.actions) do
        local ok, message = ValidateAction(action)
        if not ok then return false, message end
    end
    for _, macro in ipairs(backup.macros.account or {}) do
        local ok, message = ValidateMacro(macro)
        if not ok then return false, message end
    end
    for _, macro in ipairs(backup.macros.character or {}) do
        local ok, message = ValidateMacro(macro)
        if not ok then return false, message end
    end
    for _, binding in ipairs(backup.bindings) do
        local ok, message = ValidateBinding(binding)
        if not ok then return false, message end
    end
    for _, petAction in ipairs(backup.pets or {}) do
        local ok, message = ValidatePetAction(petAction)
        if not ok then return false, message end
    end

    backup.contents = GetBackupContents(backup)
    if not HasAnySection(backup.contents) then return false, "Imported backup does not contain any restorable sections." end
    return true, backup
end

function Vault:ExportBackup(id, options)
    local backup = GetBackup(id)
    if not backup then return nil, "Backup not found." end
    local serializer = GetSerializer()
    if not serializer then return nil, "AceSerializer-3.0 is not available." end
    local exportBackup, normalized = FilterBackupSections(backup, options)
    if not HasAnySection(normalized) then
        return nil, "Select at least one Profile Vault section to export."
    end
    exportBackup.id = nil
    local payload = {
        addon = "SinsToolkit",
        feature = "ProfileVault",
        formatVersion = EXPORT_FORMAT_VERSION,
        exportedAt = time(),
        backup = exportBackup,
    }
    local serialized = serializer:Serialize(payload)
    return EXPORT_PREFIX .. CalculateChecksum(serialized) .. ":" .. serialized
end

function Vault:ParseImportedBackup(text)
    if type(text) ~= "string" or text == "" then return nil, "Paste exported Profile Vault text first." end
    if #text > MAX_IMPORT_LENGTH then return nil, "Imported Profile Vault text is too large." end
    text = text:gsub("%s+", "")
    if text:sub(1, #EXPORT_PREFIX) ~= EXPORT_PREFIX then
        return nil, "Imported text does not use the Profile Vault export format."
    end
    local serializer = GetSerializer()
    if not serializer then return nil, "AceSerializer-3.0 is not available." end
    local encoded = text:sub(#EXPORT_PREFIX + 1)
    local expectedChecksum, serialized = encoded:match("^(%d+):(.+)$")
    if expectedChecksum then
        if CalculateChecksum(serialized) ~= expectedChecksum then
            return nil, "Imported Profile Vault text failed integrity check."
        end
    else
        serialized = encoded
    end
    local ok, payload = serializer:Deserialize(serialized)
    if not ok then return nil, "Unable to decode Profile Vault import text." end
    local valid, result = self:ValidateBackupPayload(payload)
    if not valid then return nil, result end
    return result
end

function Vault:ImportBackup(text, nameOverride, options, duplicateMode)
    EnsureDB()
    local backup, message = self:ParseImportedBackup(text)
    if not backup then return nil, message end
    local imported, normalized = FilterBackupSections(backup, options)
    if not HasAnySection(normalized) then
        return nil, "Select at least one Profile Vault section to import."
    end
    imported.id = MakeImportedBackupID()
    imported.name = ((nameOverride and nameOverride ~= "") and nameOverride) or imported.name or "Imported Backup"
    imported.character = imported.character or "Unknown Character"
    imported.realm = imported.realm or ""
    imported.characterKey = imported.characterKey or ((imported.realm ~= "" and (imported.character .. "-" .. imported.realm)) or imported.character)
    imported.className = imported.className or "Unknown Class"
    imported.specName = imported.specName or "Unknown Spec"
    imported.createdAt = imported.createdAt or date("%Y-%m-%d %H:%M:%S")
    imported.createdAtEpoch = imported.createdAtEpoch or time()
    imported.importedAt = date("%Y-%m-%d %H:%M:%S")
    imported.importedAtEpoch = time()
    imported.filteredImport = true

    local duplicateID, duplicateBackup = FindDuplicateBackup(imported)
    if duplicateID and duplicateMode == "overwrite" then
        imported.id = duplicateID
    elseif duplicateID and duplicateMode == "copy" then
        imported.name = MakeImportedCopyName(imported)
    elseif duplicateID then
        return nil, "DUPLICATE_BACKUP", {
            id = duplicateID,
            name = imported.name,
            backup = duplicateBackup,
        }
    end

    SinsToolkitDB.profileVaultBackups[imported.id] = imported
    return imported
end

function Vault:GetRestoreSummary(id, options)
    return GetBackupSummary(GetBackup(id), options)
end

function Vault:GetBackupContents(id)
    local backup = GetBackup(id)
    if not backup then return nil end
    return GetBackupContents(backup)
end

function Vault:ConfirmRestoreBackup(id, callback, options)
    local backup = GetBackup(id)
    if not backup then
        if callback then callback(false, "Backup not found.") end
        return
    end
    local normalized = NormalizeSectionOptions(options, backup, true)
    StaticPopup_Show("SINS_TOOLKIT_PROFILE_RESTORE_CONFIRM", GetBackupSummary(backup, normalized), nil, { id = id, backup = backup, options = normalized, callback = callback })
end

function Vault:ConfirmDeleteBackup(id, callback)
    local backup = GetBackup(id)
    if not backup then
        if callback then callback(false, "Backup not found.") end
        return
    end
    StaticPopup_Show("SINS_TOOLKIT_PROFILE_DELETE_CONFIRM", backup.name or "Selected backup", nil, { id = id, callback = callback })
end

function Vault:RestoreBackup(id, confirmed, options)
    EnsureDB()
    if InCombatLockdown and InCombatLockdown() then
        return false, "Profile Vault restore is blocked while in combat."
    end
    if not confirmed then
        return false, "Profile Vault restore requires confirmation."
    end

    local backup = GetBackup(id)
    if not backup then
        return false, "Backup not found."
    end

    local restore = NormalizeSectionOptions(options, backup, true)
    if not HasAnySection(restore) then
        return false, "Select at least one Profile Vault section to restore."
    end

    local restoredActions, skippedActions = 0, 0
    local restoredPetActions, skippedPetActions = 0, 0
    local bindingCount = 0
    local editModeMessage = nil
    if restore.actions then
        restoredActions, skippedActions = RestoreActions(backup.actions, backup.macros, restore.macros)
    elseif restore.macros then
        RestoreMacros(backup.macros, nil, true)
    end
    if restore.pets then
        restoredPetActions, skippedPetActions = RestorePetActions(backup.pets)
    end
    if restore.bindings then
        RestoreBindings(backup.bindings)
        bindingCount = backup.bindings and #backup.bindings or 0
    end
    if restore.editMode then
        local okEditMode, editResult = RestoreEditModeLayout(backup.editMode, backup.name)
        editModeMessage = okEditMode and ("Edit Mode layout imported as " .. tostring(editResult) .. ".") or ("Edit Mode layout skipped: " .. tostring(editResult))
    end
    return true, ("Restored %s. Sections: %s. Actions: %d restored, %d skipped. Pet actions: %d restored, %d skipped. Bindings: %d applied.%s"):format(
        backup.name or "backup",
        BuildSectionList(restore, "none"),
        restoredActions or 0,
        skippedActions or 0,
        restoredPetActions or 0,
        skippedPetActions or 0,
        bindingCount,
        editModeMessage and (" " .. editModeMessage) or ""
    )
end

function Vault:DeleteBackup(id, confirmed)
    EnsureDB()
    if not confirmed then
        return false, "Profile Vault delete requires confirmation."
    end
    SinsToolkitDB.profileVaultBackups[id] = nil
    return true, "Profile backup deleted."
end

function Vault:GetBackups()
    EnsureDB()
    local backups = {}
    for id, backup in pairs(SinsToolkitDB.profileVaultBackups) do
        local copy = DeepCopy(backup)
        copy.id = id
        backups[#backups + 1] = copy
    end
    table.sort(backups, function(a, b)
        return (a.createdAtEpoch or 0) > (b.createdAtEpoch or 0)
    end)
    return backups
end
