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

local GLOBAL_DEFAULTS = {
    Font = "Barlow Condensed Bold",
    Texture = "Solid",
    BorderSize = 2,
    FontOutline = "OUTLINE",
    BackdropColour = { r = 0.145, g = 0.145, b = 0.145, a = 0.7 },
}

-- [ 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)
    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(system) return OrbitEngine:GetSystem(system) 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)
                        self.frame:Hide()
                    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 = "228"
Orbit.title = "Orbit"

-- Dev Fallback for version token
if Orbit.version == "@" .. "project-version" .. "@" then
    Orbit.version = "1.0.1"
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.db.SpecData = self.db.SpecData or {}

    -- Migrate TourComplete from profile-synced GlobalSettings to account-wide AccountSettings
    if self.db.GlobalSettings.TourComplete ~= nil and self.db.AccountSettings.TourComplete == nil then
        self.db.AccountSettings.TourComplete = self.db.GlobalSettings.TourComplete
        self.db.GlobalSettings.TourComplete = nil
    end

    for k, v in pairs(GLOBAL_DEFAULTS) do
        if self.db.GlobalSettings[k] == nil then
            self.db.GlobalSettings[k] = type(v) == "table" and Orbit.Profile and Orbit.Profile.CopyTable and Orbit.Profile.CopyTable(v, {}) or v
        end
    end

    if self.Profile then self.Profile:Initialize() 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("loaded. Type /orbit for config")
end

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

function Orbit:IsPluginEnabled(name)
    if self:IsPluginSpecLocked(name) then return false end
    if not self.db or not self.db.DisabledPlugins then return true end
    return not self.db.DisabledPlugins[name]
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) or nil
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.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
        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 event == "PLAYER_SPECIALIZATION_CHANGED" and Orbit.EventBus then
            Orbit.EventBus:Fire("PLAYER_SPECIALIZATION_CHANGED")
        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 not (self.db.DisabledPlugins and self.db.DisabledPlugins[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
