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

-- [ CONSTANTS ]-------------------------------------------------------------------------------------

local MAX_ERRORS = 50
local INIT_TIMER_DELAY = 0.05

-- [ ERROR HANDLER ]---------------------------------------------------------------------------------

Orbit.ErrorHandler = {}
local ErrorHandler = Orbit.ErrorHandler

function ErrorHandler:Wrap(func, context)
    return function(...)
        local success, result = pcall(func, ...)
        if not success then
            local contextStr = context or "Unknown"
            Orbit:Print("|cFFFF0000ERROR:|r", contextStr, "-", tostring(result))
            self:LogError(contextStr, "wrapped_call", result)
        end
        return result
    end
end

function ErrorHandler:LogError(source, method, err)
    if not Orbit.db then return end
    if not Orbit.db.ErrorLog then
        Orbit.db.ErrorLog = {}
        Orbit.db.ErrorLogIndex = 0
    end

    local index = (Orbit.db.ErrorLogIndex % MAX_ERRORS) + 1
    Orbit.db.ErrorLogIndex = index
    Orbit.db.ErrorLog[index] = {
        time = time(),
        date = date("%Y-%m-%d %H:%M:%S"),
        source = tostring(source),
        method = tostring(method),
        error = tostring(err),
    }
end

-- [ VISIBILITY MANAGER ]----------------------------------------------------------------------------

Orbit.Visibility = {}

function Orbit.Visibility:ApplyState(frame, visibilityMode)
    -- RegisterStateDriver / UnregisterStateDriver / frame:Show are all
    -- combat-locked. Defer the whole call until after combat so the driver
    -- cache never drifts out of sync with the actual secure state.
    if InCombatLockdown() then
        if Orbit.CombatManager then
            Orbit.CombatManager:QueueUpdate(function()
                Orbit.Visibility:ApplyState(frame, visibilityMode)
            end)
        end
        return
    end

    if frame.isOrbitUpdating then return end
    frame.isOrbitUpdating = true

    local driver
    if Orbit:IsEditMode() then
        driver = "show"
        frame:SetAlpha(1)
        frame.orbitLastVisibilityDriver = nil
    else
        local vis = visibilityMode or 0
        if vis == 3 then
            driver = "hide"
        elseif vis == 1 then
            driver = "[combat] show; hide"
        elseif vis == 2 then
            driver = "[combat] hide; show"
        else
            driver = "show"
        end
    end

    if frame.orbitLastVisibilityDriver == driver then
        frame.isOrbitUpdating = false
        return
    end
    frame.orbitLastVisibilityDriver = driver

    if driver then
        RegisterStateDriver(frame, "visibility", driver)
    else
        UnregisterStateDriver(frame, "visibility")
        frame:Show()
    end

    frame.isOrbitUpdating = false
end

-- [ 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.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]
        if self:IsPluginEnabled(plugin.name) and plugin.ApplySettings then
            plugin:ApplySettings()
        end
        C_Timer.After(0, ApplyNext)
    end
    ApplyNext()
end

-- [ ADDON INITIALIZATION ]--------------------------------------------------------------------------

Orbit.addonName = addonName
Orbit.version = "0.259.0"
Orbit.title = "Orbit"

-- Dev Fallback for version token (when not built by BigWigsMods/packager)
if Orbit.version == "@" .. "project-version" .. "@" then
    Orbit.version = "0.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()

    -- SpecData is character-scoped: { [charKey] = { [specID] = { [sysIdx] = { key = value } } } }
    -- Migrate legacy account-wide layout ({ [specID] = ... }) under the current character key.
    self.db.SpecData = self.db.SpecData or {}
    local legacySpecData = false
    for k in pairs(self.db.SpecData) do
        if type(k) == "number" then legacySpecData = true; break end
    end
    if legacySpecData then
        local legacy = self.db.SpecData
        self.db.SpecData = { [self.CHAR_KEY] = legacy }
    end
    self.db.SpecData[self.CHAR_KEY] = self.db.SpecData[self.CHAR_KEY] or {}

    -- Detect class change (character services, PTR copies, realm merges that rename
    -- characters onto an existing key). SpecIDs are class-bound, so any surviving
    -- data would be stale — wipe and reseed rather than leave orphan frames.
    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

    self:Print(self.L.MSG_LOADED)
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

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
        if plugin.OnLoad and not plugin._initialized then
            self.ErrorHandler:Wrap(function() plugin:OnLoad() end, name .. ".OnLoad")()
            plugin._initialized = true
        end
        if plugin.frame then OrbitEngine.FrameAnchor:SetFrameDisabled(plugin.frame, false) end
        if plugin.ApplySettings then plugin:ApplySettings() end
    else
        if plugin.OnDisable then
            self.ErrorHandler:Wrap(function() plugin:OnDisable() end, 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 and Hide are combat-locked for secure frames — defer the
            -- whole tear-down block until combat ends so the frame ends in a
            -- consistent (fully torn down) state.
            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
        Orbit.EventBus:OffContext(plugin)
        if OrbitEngine.EditMode then OrbitEngine.EditMode:UnregisterCallbacks(plugin) end
        plugin._initialized = false
    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("PLAYER_ENTERING_WORLD")
            elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
                Orbit.EventBus:Fire("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
                if plugin.frame then
                    OrbitEngine.FrameAnchor:SetFrameDisabled(plugin.frame, true)
                    plugin.frame:SetScript("OnEvent", nil)
                    plugin.frame:SetScript("OnUpdate", nil)
                    plugin.frame:UnregisterAllEvents()
                    if Orbit.OOCFadeMixin then Orbit.OOCFadeMixin:RemoveOOCFade(plugin.frame) end
                    plugin.frame:Hide()
                end
                if plugin.timer then
                    plugin.timer:Cancel()
                    plugin.timer = nil
                end
                self.EventBus:OffContext(plugin)
                if OrbitEngine.EditMode then OrbitEngine.EditMode:UnregisterCallbacks(plugin) end
                plugin._initialized = false
            elseif not locked and not plugin._initialized and self:IsPluginEnabled(name) then
                if plugin.frame then
                    OrbitEngine.FrameAnchor:SetFrameDisabled(plugin.frame, false)
                end
                if plugin.OnLoad then
                    self.ErrorHandler:Wrap(function() plugin:OnLoad() end, name .. ".OnLoad")()
                    plugin._initialized = true
                end
                if plugin.ApplySettings then
                    plugin:ApplySettings()
                end
            end
        end
    end
end
