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

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

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
    end

    self:InitializeAccountProfiles()
    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

    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

-- [ ACCOUNT PROFILES ]-------------------------------------------------------------------------------
-- Named snapshots of { AccountSettings, SpecData } mirroring layout-profile semantics: one is always active, live edits belong to it (flushed on switch/export/logout), switching clones the incoming snapshot live and prompts a reload (account data is read at login). Never touches profiles, specMappings, or classSpecProfiles.
local DEFAULT_ACCOUNT_PROFILE = "Default"
Orbit.Profile.DEFAULT_ACCOUNT_PROFILE = DEFAULT_ACCOUNT_PROFILE

local function SnapshotLiveAccountData()
    return {
        AccountSettings = CopyTable(Orbit.db.AccountSettings or {}, {}),
        SpecData = CopyTable(Orbit.db.SpecData or {}, {}),
    }
end

function Orbit.Profile:GetActiveAccountProfileName()
    return Orbit.db.activeAccountProfile or DEFAULT_ACCOUNT_PROFILE
end

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

function Orbit.Profile:FlushAccountProfile()
    local store = Orbit.db.AccountProfiles
    if store then store[self:GetActiveAccountProfileName()] = SnapshotLiveAccountData() end
end

function Orbit.Profile:InitializeAccountProfiles()
    if not Orbit.db.AccountProfiles then Orbit.db.AccountProfiles = {} end
    local store = Orbit.db.AccountProfiles
    if not store[DEFAULT_ACCOUNT_PROFILE] then store[DEFAULT_ACCOUNT_PROFILE] = SnapshotLiveAccountData() end
    if not Orbit.db.activeAccountProfile or not store[Orbit.db.activeAccountProfile] then
        Orbit.db.activeAccountProfile = DEFAULT_ACCOUNT_PROFILE
    end
    if not self._accountFlushFrame then
        self._accountFlushFrame = CreateFrame("Frame")
        self._accountFlushFrame:RegisterEvent("PLAYER_LOGOUT")
        self._accountFlushFrame:SetScript("OnEvent", function() self:FlushAccountProfile() end)
    end
end

function Orbit.Profile:SetActiveAccountProfile(name)
    local store = Orbit.db.AccountProfiles
    if not store or not store[name] then return false end
    if name == self:GetActiveAccountProfileName() then return true end
    self:FlushAccountProfile()
    local snap = store[name]
    Orbit.db.AccountSettings = CopyTable(snap.AccountSettings or {}, {})
    Orbit.db.SpecData = Orbit.Migrations.FoldSpecDataByChar(CopyTable(snap.SpecData or {}, {}), CHAR_KEY)
    Orbit.db.activeAccountProfile = name
    Orbit:Print(L.MSG_ACCOUNT_PROFILE_LOADED_F:format(name))
    StaticPopup_Show("ORBIT_PROFILE_RELOAD")
    Orbit.EventBus:Fire("ORBIT_ACCOUNT_PROFILE_CHANGED", name)
    return true
end

function Orbit.Profile:CreateAccountProfile(name, copyFrom)
    local store = Orbit.db.AccountProfiles
    if not store or store[name] then return false end
    if not copyFrom or copyFrom == self:GetActiveAccountProfileName() then
        self:FlushAccountProfile()
        copyFrom = self:GetActiveAccountProfileName()
    end
    local src = store[copyFrom]
    store[name] = src and CopyTable(src, {}) or SnapshotLiveAccountData()
    return true
end

function Orbit.Profile:DeleteAccountProfile(name)
    if name == DEFAULT_ACCOUNT_PROFILE then return false end
    if name == self:GetActiveAccountProfileName() then return false end
    if Orbit.db.AccountProfiles then Orbit.db.AccountProfiles[name] = nil end
    return true
end

-- Factory reset: wiped tables re-seed their defaults (and re-run migrations) on the reload.
function Orbit.Profile:ResetAccountProfile(name)
    if name ~= DEFAULT_ACCOUNT_PROFILE then return false end
    Orbit.db.AccountProfiles[name] = { AccountSettings = {}, SpecData = {} }
    if name == self:GetActiveAccountProfileName() then
        Orbit.db.AccountSettings = {}
        Orbit.db.SpecData = {}
        StaticPopup_Show("ORBIT_PROFILE_RELOAD")
    end
    return true
end

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

local function Encode(exportData)
    return "--OrbitProfile--" .. LibDeflate:EncodeForPrint(LibDeflate:CompressDeflate(LibSerialize:Serialize(exportData)))
end

local function Decode(str)
    str = str:gsub("^%-%-OrbitProfile%-%-", "")
    local decoded = LibDeflate:DecodeForPrint(str)
    if not decoded then return nil, "Decoding Failed" end
    local decompressed = LibDeflate:DecompressDeflate(decoded)
    if not decompressed then return nil, "Decompression Failed" end
    local success, t = LibSerialize:Deserialize(decompressed)
    if not success then return nil, "Deserialization Failed: " .. tostring(t) end
    if type(t) ~= "table" or not t.meta or t.meta.addon ~= "Orbit" then return nil, "Not an Orbit Profile" end
    return t
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 Encode(exportData)
end

-- Account-scoped transport: exports a named account-profile snapshot (AccountSettings + SpecData); profiles, specMappings, and classSpecProfiles deliberately do not travel (spec profiles stay untouched by design).
function Orbit.Profile:ExportAccountData(name)
    if not name or name == "" then return nil end
    if name == self:GetActiveAccountProfileName() then self:FlushAccountProfile() end
    local snap = Orbit.db.AccountProfiles and Orbit.db.AccountProfiles[name]
    if not snap then return nil end
    local exportData = {
        meta = { addon = "Orbit", version = Orbit.version, date = date(), type = "Account", name = name },
        data = snap,
    }
    return Encode(exportData)
end

-- Cheap envelope read so the import UI can gate by payload type before committing anything.
function Orbit.Profile:PeekImport(str)
    local t = Decode(str)
    return t and t.meta or nil
end

function Orbit.Profile:ImportProfile(str, name, confirmed)
    local t, err = Decode(str)
    if not t then return false, err end

    -- Account import is non-destructive: it saves a named snapshot; the user activates it from the Account Data dropdown when ready.
    if t.meta.type == "Account" then
        if type(t.data) ~= "table" then return false, "Invalid Account Payload" end
        if not name or name == "" then name = t.meta.name or ("Imported " .. date("%Y%m%d")) end
        if not Orbit.db.AccountProfiles then Orbit.db.AccountProfiles = {} end
        if Orbit.db.AccountProfiles[name] then return false, "Account Profile Exists" end
        Orbit.db.AccountProfiles[name] = {
            AccountSettings = CopyTable(t.data.AccountSettings or {}, {}),
            SpecData = Orbit.Migrations.FoldSpecDataByChar(CopyTable(t.data.SpecData or {}, {}), CHAR_KEY),
        }
        Orbit:Print(L.MSG_ACCOUNT_PROFILE_SAVED_F:format(name))
        return true
    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.Migrations:RunProfile(profileData)
            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.Migrations:RunProfile(t.data)
    Orbit.db.profiles[name] = t.data
    if name == self:GetActiveProfileName() then self:SetActiveProfile(name) end
    return true
end
