-- [ ORBIT INIT ]-------------------------------------------------------------------------------------
local addonName, addonTable = ...

---@class Orbit
---@field Engine OrbitEngine
---@field Constants OrbitConstants
---@field Profile OrbitProfileManager
---@field CombatManager OrbitCombatManager
---@field PluginMixin OrbitPluginMixin
---@field EventBus OrbitEventBus
---@field Async OrbitAsync
---@field API OrbitAPI
---@field AuraMixin OrbitAuraMixin
---@field CastBarMixin OrbitCastBarMixin
---@field UnitFrameMixin OrbitUnitFrameMixin
---@field ResourceBarMixin OrbitResourceBarMixin
---@field Skin OrbitSkin
---@field Frame OrbitFrameManager
---@field Config OrbitConfig
---@field Layout OrbitLayout
Orbit = addonTable
Orbit.Engine = Orbit.Engine or {}
local OrbitEngine = Orbit.Engine

-- [ EDIT MODE QUERY ]--------------------------------------------------------------------------------
function Orbit:IsEditMode() return EditModeManagerFrame and EditModeManagerFrame:IsEditModeActive() or false end

-- [ COMBAT-SAFE HELPERS ]----------------------------------------------------------------------------
function Orbit:SafeAction(callback)
    if InCombatLockdown() then
        if self.CombatManager then
            self.CombatManager:QueueUpdate(callback)
        end
        return false
    end
    callback()
    return true
end

-- [ PLUGIN REGISTRATION ]----------------------------------------------------------------------------
function Orbit:RegisterPlugin(name, system, mixin)
    local combinedMixin = Mixin({}, Orbit.PluginMixin, mixin)
    local plugin = OrbitEngine:RegisterSystem(name, system, combinedMixin)
    plugin.displayName = mixin.displayName or name
    plugin.liveToggle = mixin.liveToggle or false
    plugin.disabledSpecs = mixin.disabledSpecs

    if not self._pluginsByName then
        self._pluginsByName = {}
    end
    self._pluginsByName[name] = plugin

    return plugin
end

function Orbit:GetPlugin(identifier)
    return (self._pluginsByName and self._pluginsByName[identifier]) or OrbitEngine:GetSystem(identifier)
end

-- Wrap ApplySettings here, not in RegisterPlugin (plugins define methods after registration).
function Orbit:InitializePlugins()
    local systems = OrbitEngine.systems
    for _, plugin in ipairs(systems) do
        if plugin.ApplySettings and not plugin._applyWrapped then
            local original = plugin.ApplySettings
            plugin.ApplySettings = function(self, ...)
                if not Orbit:IsPluginEnabled(self.name) then
                    if self.frame then
                        OrbitEngine.FrameAnchor:SetFrameDisabled(self.frame, true)
                        if InCombatLockdown() then
                            if Orbit.CombatManager then
                                local f = self.frame
                                Orbit.CombatManager:QueueUpdate(function() f:Hide() end)
                            end
                        else
                            self.frame:Hide()
                        end
                    end
                    return
                end
                if self.frame and self.frame.orbitDisabled then
                    OrbitEngine.FrameAnchor:SetFrameDisabled(self.frame, false)
                end
                return original(self, ...)
            end
            plugin._applyWrapped = true
        end
        if self:IsPluginEnabled(plugin.name) and plugin.OnLoad then
            self.ErrorHandler:Wrap(function() plugin:OnLoad() end, plugin.name .. ".OnLoad")()
            plugin._initialized = true
        end
        -- Auto-subscribe canvasMode plugins to live preview (skip preview-based plugins)
        if plugin.canvasMode and not plugin.SchedulePreviewUpdate and plugin.WatchCanvasChanges then
            plugin:WatchCanvasChanges()
        end
    end
    local i = 0
    local function ApplyNext()
        i = i + 1
        if i > #systems then
            return
        end
        local plugin = systems[i]
        -- Schedule the next tick first so a throw in one plugin's first ApplySettings can't strand the rest of the chain.
        C_Timer.After(0, ApplyNext)
        if self:IsPluginEnabled(plugin.name) and plugin.ApplySettings then
            self.ErrorHandler:Wrap(function() plugin:ApplySettings() end, plugin.name .. ".ApplySettings")()
        end
    end
    ApplyNext()
end

-- [ ADDON INITIALIZATION ]---------------------------------------------------------------------------
Orbit.version = "1.7"
Orbit.title = "Orbit"

-- Dev Fallback for version token (when not built by BigWigsMods/packager)
if Orbit.version == "@" .. "project-version" .. "@" then
    Orbit.version = "0.0-dev"
end
_G["Orbit"] = Orbit
OrbitDB = OrbitDB or {}

function Orbit:OnLoad()
    self.db = OrbitDB
    self.db.GlobalSettings = self.db.GlobalSettings or {}
    self.db.AccountSettings = self.db.AccountSettings or {}
    self.CHAR_KEY = UnitName("player") .. "-" .. GetRealmName()

    -- One-time migration: ErrorLog moved out of OrbitDB into its own SavedVariable.
    if self.db.ErrorLog then
        OrbitErrorLogDB = OrbitErrorLogDB or {}
        OrbitErrorLogDB.entries = self.db.ErrorLog
        OrbitErrorLogDB.index = self.db.ErrorLogIndex or 0
        self.db.ErrorLog = nil
        self.db.ErrorLogIndex = nil
    end

    -- SpecData is character-scoped: { [charKey] = { [specID] = { [sysIdx] = { key = value } } } }
    self.db.SpecData = self.db.SpecData or {}
    self.db.SpecData[self.CHAR_KEY] = self.db.SpecData[self.CHAR_KEY] or {}

    -- Wipe SpecData on class change (services / PTR copy / realm merge) — SpecIDs are class-bound, surviving data would be stale.
    local _, playerClass = UnitClass("player")
    self.db.SpecDataMeta = self.db.SpecDataMeta or {}
    local meta = self.db.SpecDataMeta[self.CHAR_KEY]
    if meta and meta.class and meta.class ~= playerClass then
        self.db.SpecData[self.CHAR_KEY] = {}
    end
    self.db.SpecDataMeta[self.CHAR_KEY] = { class = playerClass }



    local globalDefaults = self.Profile and self.Profile.defaults and self.Profile.defaults.GlobalSettings
    if globalDefaults then
        for k, v in pairs(globalDefaults) do
            if self.db.GlobalSettings[k] == nil then
                self.db.GlobalSettings[k] = type(v) == "table" and CopyTable(v) or v
            end
        end
    end

    if self.Profile then self.Profile:Initialize() end
    if self.StrataEngine and self.StrataEngine.PopulateDefaults then self.StrataEngine:PopulateDefaults() end
    self:InitializePlugins()

    if NSAPI and NSAPI.RegisterCallback and self.EventBus then
        NSAPI:RegisterCallback("NSRT_NICKNAME_UPDATED", function() self.EventBus:Fire("ORBIT_NICKNAME_UPDATED") end, self)
    end

    if self.EventBus then
        self.EventBus:On("ORBIT_DISPLAY_SIZE_CHANGED", function()
            if self.Engine and self.Engine.systems then
                for _, plugin in ipairs(self.Engine.systems) do
                    if plugin.ApplyAll then
                        plugin:ApplyAll()
                    elseif plugin.ApplySettings then
                        plugin:ApplySettings()
                    end
                end
            end
        end)
    end
end

function Orbit:Print(...) print("|cFF00FFFF" .. self.title .. ":|r", ...) end

function Orbit:IsPluginEnabled(name)
    if self:IsPluginSpecLocked(name) then
        return false
    end
    local isDisabled = self.db and self.db.DisabledPlugins and self.db.DisabledPlugins[name]
    if isDisabled == nil then
        local defaults = self.Profile and self.Profile.defaults and self.Profile.defaults.DisabledPlugins
        isDisabled = defaults and defaults[name] or false
    end
    return not isDisabled
end

function Orbit:IsPluginSpecLocked(name)
    local plugin = self._pluginsByName and self._pluginsByName[name]
    if not plugin or not plugin.disabledSpecs then
        return false
    end
    local spec = GetSpecialization and GetSpecialization()
    local specID = spec and GetSpecializationInfo(spec)
    return specID and plugin.disabledSpecs[specID] or false
end

function Orbit:SetPluginEnabled(name, enabled)
    if not self.db then return end
    if not self.db.DisabledPlugins then
        self.db.DisabledPlugins = {}
    end
    self.db.DisabledPlugins[name] = not enabled
end

function Orbit:IsBlizzardHidden(name)
    if not self.db or not self.db.HideBlizzardFrames then
        return false
    end
    return self.db.HideBlizzardFrames[name] == true
end

function Orbit:SetBlizzardHidden(name, hidden)
    if not self.db then return end
    if not self.db.HideBlizzardFrames then
        self.db.HideBlizzardFrames = {}
    end
    self.db.HideBlizzardFrames[name] = hidden or nil
end

function Orbit:IsLiveToggle(name)
    local plugin = self._pluginsByName and self._pluginsByName[name]
    return plugin and plugin.liveToggle or false
end

-- [ PLUGIN LIFECYCLE ]-------------------------------------------------------------------------------
-- Single teardown/setup pair shared by live-toggle and spec-lock so OnDisable invocation, combat deferral, and canvas re-watch stay consistent by construction.
local function SetupPlugin(plugin)
    if plugin.OnLoad and not plugin._initialized then
        Orbit.ErrorHandler:Wrap(function() plugin:OnLoad() end, plugin.name .. ".OnLoad")()
        plugin._initialized = true
    end
    if plugin.frame then OrbitEngine.FrameAnchor:SetFrameDisabled(plugin.frame, false) end
    if plugin.canvasMode and not plugin.SchedulePreviewUpdate and plugin.WatchCanvasChanges then plugin:WatchCanvasChanges() end
    if plugin.ApplySettings then plugin:ApplySettings() end
end

local function TeardownPlugin(plugin)
    if plugin.OnDisable then
        Orbit.ErrorHandler:Wrap(function() plugin:OnDisable() end, plugin.name .. ".OnDisable")()
    end
    if plugin.frame then
        OrbitEngine.FrameAnchor:SetFrameDisabled(plugin.frame, true)
        plugin.frame:UnregisterAllEvents()
        if Orbit.OOCFadeMixin then Orbit.OOCFadeMixin:RemoveOOCFade(plugin.frame) end
        -- SetScript+Hide are combat-locked for secure frames — defer the whole tear-down so the frame ends fully torn down.
        local f = plugin.frame
        local function TearDownFrame()
            f:SetScript("OnEvent", nil)
            f:SetScript("OnUpdate", nil)
            f:Hide()
        end
        if InCombatLockdown() then
            if Orbit.CombatManager then Orbit.CombatManager:QueueUpdate(TearDownFrame) end
        else
            TearDownFrame()
        end
    end
    if plugin.timer then
        plugin.timer:Cancel()
        plugin.timer = nil
    end
    if plugin.UnwatchCanvasChanges then plugin:UnwatchCanvasChanges() end
    Orbit.EventBus:OffContext(plugin)
    if OrbitEngine.EditMode then OrbitEngine.EditMode:UnregisterCallbacks(plugin) end
    plugin._initialized = false
end

function Orbit:LiveTogglePlugin(name, enabled)
    self:SetPluginEnabled(name, enabled)
    local plugin = self._pluginsByName and self._pluginsByName[name]
    if not plugin then return end
    if enabled then
        SetupPlugin(plugin)
    else
        TeardownPlugin(plugin)
    end
end

-- [ BLIZZARD FRAME HIDERS ]--------------------------------------------------------------------------
Orbit._blizzardHiders = {}

function Orbit:RegisterBlizzardHider(pluginName, hiderFunc) self._blizzardHiders[pluginName] = hiderFunc end

-- [ EVENT HANDLERS ]---------------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_LOGOUT")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
eventFrame:SetScript("OnEvent", function(self, event)
    if event == "PLAYER_LOGIN" then
        Orbit:OnLoad()
        for name, hider in pairs(Orbit._blizzardHiders) do
            if not Orbit:IsPluginEnabled(name) and Orbit:IsBlizzardHidden(name) then
                hider()
            end
        end
    elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_SPECIALIZATION_CHANGED" then
        Orbit:RefreshSpecLockedPlugins()
        if Orbit.EventBus then
            if event == "PLAYER_ENTERING_WORLD" then
                Orbit.EventBus:Fire("ORBIT_PLAYER_ENTERING_WORLD")
            elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
                Orbit.EventBus:Fire("ORBIT_PLAYER_SPECIALIZATION_CHANGED")
            end
        end
    elseif event == "PLAYER_LOGOUT" then
        if Orbit.Engine and Orbit.Engine.PositionManager then Orbit.Engine.PositionManager:FlushToStorage() end
        if Orbit.Profile then Orbit.Profile:FlushGlobalSettings() end
    end
end)

function Orbit:RefreshSpecLockedPlugins()
    if not self._pluginsByName then return end
    for name, plugin in pairs(self._pluginsByName) do
        if plugin.disabledSpecs then
            local locked = self:IsPluginSpecLocked(name)
            if locked and plugin._initialized then
                TeardownPlugin(plugin)
            elseif not locked and not plugin._initialized and self:IsPluginEnabled(name) then
                SetupPlugin(plugin)
            end
        end
    end
end
