-- [ PROFILE MANAGER ]--------------------------------------------------------------------------------
local _, addonTable = ...
local Orbit = addonTable
local L = Orbit.L

---@class OrbitProfileManager
Orbit.Profile = Orbit.Profile or {}

-- [ CONSTANTS ]--------------------------------------------------------------------------------------
local CLEAN_PROFILE_SOURCE = ":CLEAN:"
local DEBOUNCE_KEY = "ProfileManager_SpecCheck"
local DEBOUNCE_DELAY = 0.1
local DEFAULT_PROFILE = "Global"
local DELAYED_REFRESH = 0.1
local PLAYER_CLASS = select(2, UnitClass("player"))
local CHAR_KEY = UnitName("player") .. "-" .. GetRealmName()



-- [ STATE ]------------------------------------------------------------------------------------------
local isActivatingProfile = false

-- [ CLASS-SCOPED SPEC PROFILES ]---------------------------------------------------------------------
function Orbit.Profile:IsSpecProfilesEnabled()
    local tbl = Orbit.db and Orbit.db.classSpecProfiles
    return tbl and tbl[PLAYER_CLASS] or false
end

function Orbit.Profile:SetSpecProfilesEnabled(value)
    if not Orbit.db then return end
    if not Orbit.db.classSpecProfiles then Orbit.db.classSpecProfiles = {} end
    Orbit.db.classSpecProfiles[PLAYER_CLASS] = value and true or nil
end

-- [ UTILITY ]----------------------------------------------------------------------------------------
local function CopyTable(src, dest)
    if type(dest) ~= "table" then dest = {} end
    for k, v in pairs(src) do
        dest[k] = type(v) == "table" and CopyTable(v, {}) or v
    end
    return dest
end

-- Idempotent: fold the legacy "Default" layout into the live "Orbit" layout (Orbit wins per-key), then drop it. Lets GetSetting read a single layout without a back-compat fallback walk.
local function MigrateDefaultLayout(layouts)
    if not layouts or not layouts.Default then return end
    layouts.Orbit = layouts.Orbit or {}
    for system, sysData in pairs(layouts.Default) do
        local target = layouts.Orbit[system]
        if target == nil then
            layouts.Orbit[system] = sysData
        else
            for sysIdx, idxData in pairs(sysData) do
                if target[sysIdx] == nil then
                    target[sysIdx] = idxData
                else
                    for k, v in pairs(idxData) do
                        if target[sysIdx][k] == nil then target[sysIdx][k] = v end
                    end
                end
            end
        end
    end
    layouts.Default = nil
end
Orbit.Profile.MigrateDefaultLayout = MigrateDefaultLayout

-- Per-profile visibility store: each profile owns { frames = <VisibilityEngine per-frame settings>, fade = <FadeProfiles> }. Resolved fresh on each access (like the Orbit.runtime.Layouts pointer) so a profile switch needs no clone-in/out and there is no account-wide layer — VisibilityEngine and FadeProfiles both read the active profile's copy.
local function NewFadeStore() return { profiles = {}, revealAll = false, nextId = 1 } end
function Orbit.Profile:GetActiveVisibility()
    local profile = Orbit.db and Orbit.db.profiles and Orbit.db.profiles[Orbit.db.activeProfile]
    if not profile then return nil end
    local vis = profile.Visibility
    if not vis then vis = {}; profile.Visibility = vis end
    if not vis.frames then vis.frames = {} end
    if not vis.fade then vis.fade = NewFadeStore() end
    return vis
end

-- One-time: visibility moved from the account-wide roots (Orbit.db.VisibilityEngine / .FadeProfiles) into the per-profile blob so each profile is independent, and the stale per-plugin opacity/fade keys the removed VE:Migrate left behind are stripped from every layout. Frames start clean (they have no editing UI and were the source of the old "locked" values); the shared fade profiles are preserved by copying them into every profile.
local LEGACY_VIS_KEYS = { "Opacity", "OutOfCombatFade", "ShowOnMouseover" }
local function MigrateVisibilityToProfiles()
    if not Orbit.db then return end
    local acct = Orbit.db.AccountSettings
    if not acct then acct = {}; Orbit.db.AccountSettings = acct end
    if acct.VisibilityPerProfileV1 then return end
    local rootFade = Orbit.db.FadeProfiles
    local profiles = Orbit.db.profiles
    if type(profiles) == "table" then
        for _, profile in pairs(profiles) do
            if type(profile) == "table" then
                local layouts = profile.Layouts
                if type(layouts) == "table" then
                    for _, sysMap in pairs(layouts) do
                        if type(sysMap) == "table" then
                            for _, idxMap in pairs(sysMap) do
                                if type(idxMap) == "table" then
                                    for _, node in pairs(idxMap) do
                                        if type(node) == "table" then
                                            for _, k in ipairs(LEGACY_VIS_KEYS) do node[k] = nil end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
                profile.Visibility = profile.Visibility or {}
                profile.Visibility.frames = {}
                profile.Visibility.fade = type(rootFade) == "table" and CopyTable(rootFade, {}) or NewFadeStore()
            end
        end
    end
    Orbit.db.VisibilityEngine = nil
    Orbit.db.FadeProfiles = nil
    acct.VisibilityPerProfileV1 = true
end

local function NormalizeBorderStyle(gs) Orbit.Constants.BorderStyle.Migrate(gs) end

local function SafeApplyPlugin(plugin)
    local success, err = pcall(function() plugin:ApplySettings(nil) end)
    if not success then
        Orbit:Print(L.MSG_PLUGIN_REFRESH_ERROR_F:format(plugin.name or "?", tostring(err)))
    end
end

-- [ INITIALIZATION ]---------------------------------------------------------------------------------
function Orbit.Profile:Initialize()
    if not Orbit.db then Orbit.db = {} end
    if not Orbit.db.profiles then Orbit.db.profiles = {} end
    if not Orbit.db.classSpecProfiles then Orbit.db.classSpecProfiles = {} end
    if not Orbit.db.charActiveProfiles then Orbit.db.charActiveProfiles = {} end



    if not Orbit.db.GlobalSettings then Orbit.db.GlobalSettings = {} end
    local gs = Orbit.db.GlobalSettings
    local globalDefaults = self.defaults and self.defaults.GlobalSettings
    if globalDefaults then
        for key, default in pairs(globalDefaults) do
            if gs[key] == nil then
                gs[key] = type(default) == "table" and CopyTable(default, {}) or default
            end
        end
    end
    NormalizeBorderStyle(gs)

    if not Orbit.db.profiles[DEFAULT_PROFILE] then
        Orbit.db.profiles[DEFAULT_PROFILE] = CopyTable(self.defaults, {})
    end

    local charProfile = Orbit.db.charActiveProfiles[CHAR_KEY]
    if charProfile and Orbit.db.profiles[charProfile] then
        Orbit.db.activeProfile = charProfile
    else
        Orbit.db.activeProfile = DEFAULT_PROFILE
        Orbit.db.charActiveProfiles[CHAR_KEY] = DEFAULT_PROFILE
    end

    if not Orbit.db.specMappings then
        Orbit.db.specMappings = {}
    end

    Orbit.runtime = Orbit.runtime or {}
    if not Orbit.runtime.Layouts then
        local activeProfile = Orbit.db.profiles[Orbit.db.activeProfile]
        if activeProfile then
            if not activeProfile.Layouts then activeProfile.Layouts = {} end
            Orbit.runtime.Layouts = activeProfile.Layouts
        end
    end

    self:CheckSpecProfile()

    for _, profileData in pairs(Orbit.db.profiles) do
        if not profileData.GlobalSettings then
            profileData.GlobalSettings = CopyTable(Orbit.db.GlobalSettings, {})
        end
        NormalizeBorderStyle(profileData.GlobalSettings)
        MigrateDefaultLayout(profileData.Layouts)
    end

    MigrateVisibilityToProfiles()

    self:InitializeSpecSwitching()
end

-- [ PROFILE GETTERS ]--------------------------------------------------------------------------------
function Orbit.Profile:GetActiveProfileName() return Orbit.db.activeProfile or DEFAULT_PROFILE end

function Orbit.Profile:GetProfiles()
    local names = {}
    for name, _ in pairs(Orbit.db.profiles) do table.insert(names, name) end
    table.sort(names)
    return names
end

-- [ SPEC MAPPING ] ----------------------------------------------------------------------------------
function Orbit.Profile:GetProfileForSpec(specID)
    if not Orbit.db.specMappings then return nil end
    return Orbit.db.specMappings[specID]
end

function Orbit.Profile:SetProfileForSpec(specID, profileName)
    if not Orbit.db.specMappings then Orbit.db.specMappings = {} end
    if profileName and profileName ~= DEFAULT_PROFILE then
        Orbit.db.specMappings[specID] = profileName
    else
        Orbit.db.specMappings[specID] = nil
    end
end



-- [ PROFILE CRUD ]-----------------------------------------------------------------------------------
function Orbit.Profile:CreateProfile(name, copyFrom)
    if Orbit.db.profiles[name] then return false end

    local sourceName = copyFrom or self:GetActiveProfileName()

    if sourceName == self:GetActiveProfileName() then
        if Orbit.Engine and Orbit.Engine.PositionManager then
            Orbit.Engine.PositionManager:FlushToStorage()
        end
        self:FlushGlobalSettings()
    end

    local sourceData
    if sourceName == CLEAN_PROFILE_SOURCE then
        sourceData = self.defaults
    else
        if not Orbit.db.profiles[sourceName] then sourceName = DEFAULT_PROFILE end
        sourceData = Orbit.db.profiles[sourceName]
    end

    Orbit.db.profiles[name] = CopyTable(sourceData, {})
    return true
end

function Orbit.Profile:DeleteProfile(name)
    if name == DEFAULT_PROFILE then return false end
    if name == self:GetActiveProfileName() then return false end
    Orbit.db.profiles[name] = nil
    if Orbit.db.specMappings then
        for specID, mapped in pairs(Orbit.db.specMappings) do
            if mapped == name then Orbit.db.specMappings[specID] = nil end
        end
    end
    return true
end

function Orbit.Profile:RenameProfile(oldName, newName)
    if oldName == DEFAULT_PROFILE then return false, "Cannot rename Global profile" end
    if not Orbit.db.profiles[oldName] then return false, "Profile does not exist" end
    if Orbit.db.profiles[newName] then return false, "A profile with that name already exists" end
    if not newName or newName == "" then return false, "Invalid name" end
    Orbit.db.profiles[newName] = Orbit.db.profiles[oldName]
    Orbit.db.profiles[oldName] = nil
    if Orbit.db.activeProfile == oldName then Orbit.db.activeProfile = newName end
    if Orbit.db.specMappings then
        for specID, mapped in pairs(Orbit.db.specMappings) do
            if mapped == oldName then Orbit.db.specMappings[specID] = newName end
        end
    end
    return true
end

StaticPopupDialogs["ORBIT_PROFILE_RELOAD"] = {
    text = L.MSG_PROFILE_RELOAD_REQUIRED,
    button1 = L.CMN_RELOAD,
    button2 = L.CMN_LATER,
    OnAccept = function() ReloadUI() end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
}

-- Live-toggle plugins whose enabled/hidden state switched cleanly; returns true if any change requires a /reload.
local function DiffPluginStates(self, oldDisabled, oldHidden)
    local needsReload = false
    local defaults = self.defaults and self.defaults.DisabledPlugins
    for _, plugin in ipairs(Orbit.Engine.systems) do
        local wasDisabled = oldDisabled[plugin.name]
        if wasDisabled == nil then wasDisabled = defaults and defaults[plugin.name] or false end
        local nowDisabled = Orbit.db.DisabledPlugins[plugin.name]
        if nowDisabled == nil then nowDisabled = defaults and defaults[plugin.name] or false end
        local wasHidden = oldHidden[plugin.name] or false
        local nowHidden = Orbit.db.HideBlizzardFrames[plugin.name] or false
        if (wasDisabled ~= nowDisabled) or (wasHidden ~= nowHidden) then
            if plugin.liveToggle and not nowHidden and not wasHidden then
                Orbit:LiveTogglePlugin(plugin.name, not nowDisabled)
            else
                needsReload = true
            end
        end
    end
    return needsReload
end

-- Priority-first apply, then the rest, then a deferred full sweep + coalesced anchor reconcile next frame.
local function RefreshAllPlugins()
    for _, plugin in ipairs(Orbit.Engine.systems) do
        if plugin.ApplySettings and plugin.refreshPriority then SafeApplyPlugin(plugin) end
    end
    for _, plugin in ipairs(Orbit.Engine.systems) do
        if plugin.ApplySettings and not plugin.refreshPriority then SafeApplyPlugin(plugin) end
    end
    C_Timer.After(DELAYED_REFRESH, function()
        for _, plugin in ipairs(Orbit.Engine.systems) do
            if plugin.ApplySettings then SafeApplyPlugin(plugin) end
        end
        if Orbit.Engine.FrameAnchor then Orbit.Engine.FrameAnchor:ScheduleReconcileAll() end
    end)
end

function Orbit.Profile:SetActiveProfile(name)
    if not Orbit.db.profiles[name] then return false end
    if isActivatingProfile then return false end
    if InCombatLockdown() then
        if Orbit.CombatManager then Orbit.CombatManager:QueueUpdate(function() self:SetActiveProfile(name) end) end
        return false
    end
    local profile = Orbit.db.profiles[name]
    if not profile.Layouts then profile.Layouts = {} end
    MigrateDefaultLayout(profile.Layouts)

    if Orbit.db.activeProfile == name then
        Orbit.runtime = Orbit.runtime or {}
        Orbit.runtime.Layouts = profile.Layouts
        if profile.GlobalSettings then Orbit.db.GlobalSettings = CopyTable(profile.GlobalSettings, {}) end
        return true
    end

    isActivatingProfile = true

    if Orbit.Engine and Orbit.Engine.PositionManager then
        Orbit.Engine.PositionManager:FlushToStorage()
        Orbit.Engine.PositionManager:DiscardChanges()
    end

    local oldProfile = Orbit.db.profiles[Orbit.db.activeProfile]
    if oldProfile then
        oldProfile.GlobalSettings = CopyTable(Orbit.db.GlobalSettings, {})
        oldProfile.DisabledPlugins = CopyTable(Orbit.db.DisabledPlugins or {}, {})
        oldProfile.HideBlizzardFrames = CopyTable(Orbit.db.HideBlizzardFrames or {}, {})
    end

    Orbit.db.activeProfile = name
    Orbit.db.charActiveProfiles[CHAR_KEY] = name
    Orbit.runtime = Orbit.runtime or {}
    Orbit.runtime.Layouts = profile.Layouts

    if profile.GlobalSettings then Orbit.db.GlobalSettings = CopyTable(profile.GlobalSettings, {}) end
    local oldDisabled = CopyTable(Orbit.db.DisabledPlugins or {}, {})
    local oldHidden = CopyTable(Orbit.db.HideBlizzardFrames or {}, {})
    Orbit.db.DisabledPlugins = CopyTable(profile.DisabledPlugins or {}, {})
    Orbit.db.HideBlizzardFrames = CopyTable(profile.HideBlizzardFrames or {}, {})
    Orbit:Print(L.MSG_PROFILE_LOADED_F:format(name))

    local needsReload = false
    if Orbit.Engine and Orbit.Engine.systems then
        needsReload = DiffPluginStates(self, oldDisabled, oldHidden)
        RefreshAllPlugins()
    end

    if Orbit.OptionsPanel and Orbit.OptionsPanel.Refresh then Orbit.OptionsPanel:Refresh() end
    isActivatingProfile = false

    if needsReload then StaticPopup_Show("ORBIT_PROFILE_RELOAD") end

    Orbit.EventBus:Fire("ORBIT_PROFILE_CHANGED", name)
    return true
end

-- [ SPEC MANAGEMENT ]--------------------------------------------------------------------------------
function Orbit.Profile:CheckSpecProfile()
    if not Orbit.db or not Orbit.db.profiles then return end
    if not self:IsSpecProfilesEnabled() then return end
    if not Orbit.db.specMappings then return end
    local specIndex = GetSpecialization and GetSpecialization()
    if not specIndex then return end
    local specID = GetSpecializationInfo(specIndex)
    if not specID then return end
    local mapped = Orbit.db.specMappings[specID]
    if mapped and Orbit.db.profiles[mapped] then
        self:SetActiveProfile(mapped)
    else
        self:SetActiveProfile(DEFAULT_PROFILE)
    end
end

function Orbit.Profile:InitializeSpecSwitching()
    if self._specSwitchFrame then return end
    local frame = CreateFrame("Frame")
    self._specSwitchFrame = frame
    frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
    frame:SetScript("OnEvent", function(_, event)
        Orbit.Async:Debounce(DEBOUNCE_KEY, function() self:CheckSpecProfile() end, DEBOUNCE_DELAY)
    end)
end

function Orbit.Profile:FlushGlobalSettings()
    local active = Orbit.db.profiles[Orbit.db.activeProfile]
    if active then
        active.GlobalSettings = CopyTable(Orbit.db.GlobalSettings, {})
        active.DisabledPlugins = CopyTable(Orbit.db.DisabledPlugins or {}, {})
        active.HideBlizzardFrames = CopyTable(Orbit.db.HideBlizzardFrames or {}, {})
    end
end

-- [ IMPORT / EXPORT ]--------------------------------------------------------------------------------
local LibSerialize = LibStub("LibSerialize")
local LibDeflate = LibStub("LibDeflate")

function Orbit.Profile:ExportProfile()
    self:FlushGlobalSettings()
    local exportData = {
        meta = { addon = "Orbit", version = Orbit.version, date = date(), type = "Collection", name = "All Profiles Backup" },
        data = Orbit.db.profiles,
        specMappings = Orbit.db.specMappings,
    }
    return "--OrbitProfile--" .. LibDeflate:EncodeForPrint(LibDeflate:CompressDeflate(LibSerialize:Serialize(exportData)))
end

function Orbit.Profile:ExportSingleProfile(profileName)
    self:FlushGlobalSettings()
    local profileData = Orbit.db.profiles[profileName]
    if not profileData then return nil end
    local exportData = {
        meta = { addon = "Orbit", version = Orbit.version, date = date(), type = "Single", name = profileName },
        data = profileData,
    }
    return "--OrbitProfile--" .. LibDeflate:EncodeForPrint(LibDeflate:CompressDeflate(LibSerialize:Serialize(exportData)))
end

function Orbit.Profile:ImportProfile(str, name, confirmed)
    str = str:gsub("^%-%-OrbitProfile%-%-", "")
    local decoded = LibDeflate:DecodeForPrint(str)
    if not decoded then return false, "Decoding Failed" end
    local decompressed = LibDeflate:DecompressDeflate(decoded)
    if not decompressed then return false, "Decompression Failed" end
    local success, t = LibSerialize:Deserialize(decompressed)
    if not success then return false, "Deserialization Failed: " .. tostring(t) end
    if type(t) ~= "table" or not t.meta or t.meta.addon ~= "Orbit" then return false, "Not an Orbit Profile" end

    if t.meta.type == "Collection" then
        if not confirmed then
            return false, "COLLECTION_CONFIRM"
        end
        Orbit.db.profiles = {}
        local count = 0
        for profileName, profileData in pairs(t.data) do
            Orbit.db.profiles[profileName] = profileData
            count = count + 1
        end
        if t.specMappings then
            Orbit.db.specMappings = CopyTable(t.specMappings, {})
        end
        if not Orbit.db.profiles[Orbit.db.activeProfile] then
            Orbit.db.activeProfile = DEFAULT_PROFILE
        end
        if not Orbit.db.profiles[DEFAULT_PROFILE] then
            Orbit.db.profiles[DEFAULT_PROFILE] = CopyTable(self.defaults, {})
        end
        Orbit:Print(L.MSG_PROFILE_COLLECTION_IMPORTED_F:format(count))
        self:SetActiveProfile(Orbit.db.activeProfile)
        return true
    end

    if not name or name == "" then name = t.meta.name or ("Imported " .. date("%Y%m%d")) end
    Orbit:Print(L.MSG_PROFILE_IMPORTING_F:format(name, t.meta.date or "Unknown"))
    Orbit.db.profiles[name] = t.data
    if name == self:GetActiveProfileName() then self:SetActiveProfile(name) end
    return true
end
