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 function EnsureDB()
    if SinsToolkit.EnsureDB then SinsToolkit.EnsureDB() end
end

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

local function GetBackupSummary(backup)
    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 character = backup.characterKey or ((backup.character or "Unknown Character") .. "-" .. (backup.realm or ""))
    local className = backup.className or "Unknown Class"
    local specName = backup.specName or "Unknown Spec"
    local createdAt = backup.createdAt or "Unknown date"
    return ("%s\n\nCharacter: %s\nClass: %s\nSpec: %s\nCreated: %s\n\nThis will replace action bars, update/create macros, and replace keybindings on this character.\n\nActions: %d\nPet actions: %d\nAccount macros: %d\nCharacter macros: %d\nKeybindings: %d"):format(
        backup.name or "Selected backup",
        character,
        className,
        specName,
        createdAt,
        actionCount,
        petActionCount,
        accountMacroCount,
        characterMacroCount,
        bindingCount
    )
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 ok, message = Vault:RestoreBackup(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,
}

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,
}

local function DeepCopy(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

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

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

local function GetSpecInfo()
    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 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 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 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 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

function Vault:CreateBackup(name)
    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 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 = CaptureActions(),
        macros = CaptureMacros(),
        bindings = CaptureBindings(),
        pets = CapturePetActions(),
    }

    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)
    local macroIdMap = {}

    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

    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)
    local restored = 0
    local skipped = 0
    local slotBucket = {}
    local macroIdMap = RestoreMacros(macros, actions)

    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 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

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
    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 backup.pets ~= nil and type(backup.pets) ~= "table" then return false, "Imported backup pet actions are invalid." 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

    return true, backup
end

function Vault:ExportBackup(id)
    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 = DeepCopy(backup)
    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)
    EnsureDB()
    local backup, message = self:ParseImportedBackup(text)
    if not backup then return nil, message end
    local imported = DeepCopy(backup)
    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()
    SinsToolkitDB.profileVaultBackups[imported.id] = imported
    return imported
end

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

function Vault:ConfirmRestoreBackup(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_RESTORE_CONFIRM", GetBackupSummary(backup), nil, { id = id, 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)
    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 restoredActions, skippedActions = RestoreActions(backup.actions, backup.macros)
    local restoredPetActions, skippedPetActions = RestorePetActions(backup.pets)
    RestoreBindings(backup.bindings)
    local bindingCount = backup.bindings and #backup.bindings or 0
    return true, ("Restored %s. Actions: %d restored, %d skipped. Pet actions: %d restored, %d skipped. Bindings: %d applied."):format(
        backup.name or "backup",
        restoredActions or 0,
        skippedActions or 0,
        restoredPetActions or 0,
        skippedPetActions or 0,
        bindingCount
    )
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
