local ADDON_NAME, private = ...

-- Lua Globals --
-- luacheck: globals next type strsplit tonumber

-- RealUI Core System
-- This file contains the main RealUI core functionality including version management,
-- profile system, layout management, and module coordination

-- RealUI --
local RealUI = private.RealUI
local L = RealUI.L
local db, dbc, dbg
local debug = RealUI.GetDebug("Core")

-- NOTE: LibDualSpec-1.0 is loaded and used exclusively by DualSpecSystem

-- Enhanced Version Management System
local version = _G.C_AddOns.GetAddOnMetadata(ADDON_NAME, "Version")
RealUI.verinfo = {strsplit(".", version)}
for i = 1, 3 do
    RealUI.verinfo[i] = tonumber(RealUI.verinfo[i]) or 0
end
RealUI.verinfo.string = version
RealUI.verinfo.build = select(4, _G.GetBuildInfo())
RealUI.verinfo.gameVersion = _G.GetBuildInfo()

-- Version comparison utilities
function RealUI:CompareVersions(ver1, ver2)
    for i = 1, 3 do
        local v1 = ver1[i] or 0
        local v2 = ver2[i] or 0
        if v1 > v2 then
            return 1
        elseif v1 < v2 then
            return -1
        end
    end
    return 0
end

function RealUI:IsNewerVersion(newVer, oldVer)
    return self:CompareVersions(newVer, oldVer) > 0
end

-- Configuration Mode Management
RealUI.isConfigMode = false

-- Layout Position Defaults
--[[ B78: HuD modules whose absence is indistinguishable from a bug.
     Each of these draws something the user expects to see; switched off, it
     simply is not there — no error, no message. Cast bars sat disabled in a
     healing profile for months (a stale `false` written by the 3.x-era
     profile-cascade faults, which the old force-enable used to mask until
     that was correctly removed in beta 5) with nothing on screen to say so.
     Reported at login and by `/realui devcheck`. Disabling any of these is a
     legitimate choice — the point is to make it a VISIBLE one. ]]
RealUI.CORE_HUD_MODULES = {
    "CastBars", "UnitFrames", "ClassResource", "SpellAlerts", "Infobar",
}

--- Report core HuD modules switched off in the ACTIVE profile.
-- Returns a comma-joined name list, or nil when everything is on.
function RealUI:GetDisabledCoreModules()
    local profile = self.db and self.db.profile
    if not (profile and profile.modules) then return nil end

    local off
    for _, name in next, RealUI.CORE_HUD_MODULES do
        if profile.modules[name] == false then
            off = off and (off .. ", " .. name) or name
        end
    end
    return off
end

RealUI.defaultPositions = {
    [1] = {
        -- DPS/Tank Layout
        ["HuDX"] = 0,
        ["HuDY"] = -38,
        ["UFHorizontal"] = 200,
        ["ActionBarsY"] = -161.5,
        ["ActionBarsBotY"] = 16,
        ["CastBarPlayerX"] = 0,
        ["CastBarPlayerY"] = 0,
        ["CastBarTargetX"] = 0,
        ["CastBarTargetY"] = 0,
        -- (B13: SpellAlertWidth retired — spell alerts are statically sized
        -- by Modules/SpellAlerts.lua; stale saved values are ignored.)
        ["BossX"] = -32, -- Boss anchored to RIGHT
        ["BossY"] = 314
    },
    [2] = {
        -- Healing Layout
        ["HuDX"] = 0,
        ["HuDY"] = -38,
        ["UFHorizontal"] = 200,
        ["ActionBarsY"] = -115.5,
        ["ActionBarsBotY"] = 16,
        ["CastBarPlayerX"] = 0,
        ["CastBarPlayerY"] = -20,
        ["CastBarTargetX"] = 0,
        ["CastBarTargetY"] = -20,
        ["BossX"] = -32, -- Boss anchored to RIGHT
        ["BossY"] = 314
    }
}

-- Profile to Layout Mapping
private.profileToLayout = {
    ["RealUI"] = 1,
    ["RealUI-Healing"] = 2
}
private.layoutToProfile = {
    "RealUI",
    "RealUI-Healing"
}

-- HuD Size Offset Configuration
RealUI.hudSizeOffsets = {
    [1] = {
        -- Small HuD
        ["UFHorizontal"] = 0,
        ["ActionBarsY"] = 0,
        ["CastBarPlayerY"] = 0,
        ["CastBarTargetY"] = 0
    },
    [2] = {
        -- Large HuD
        ["UFHorizontal"] = 100,
        ["ActionBarsY"] = -20,
        ["CastBarPlayerY"] = -20,
        ["CastBarTargetY"] = -20
    }
}

-- Default Options
local defaults, charInit
do
    charInit = {
        installStage = 0,
        initialized = false,
        needchatmoved = true
    }
    local spec = {}
    for specIndex = 1, #RealUI.charInfo.specs do
        local role = RealUI.charInfo.specs[specIndex].role
        debug("Spec info", specIndex, role)
        spec[specIndex] = role == "HEALER" and 2 or 1
    end
    defaults = {
        global = {
            tutorial = {
                stage = -1
            },
            tags = {
                firsttime = true,
                lowResOptimized = false,
                slashRealUITyped = false -- To disable "Type /realui" message
            },
            messages = {},
            verinfo = {},
            patchedTOC = 0,
            currency = {},
            qol = {
                repairMountID = 0
            },
            unifiedProfilesMigrated = false, -- Migration marker for unified profile system
            display = {
                presetId     = false,   -- false = not yet configured
                customScale  = 1.0,
                isHighRes    = false,
                isPixelScale = true,
                fontScale    = 1.0,
                hdrEnabled   = false,   -- independent HDR toggle; colorMode derived at apply time
            },
            editmode = {
                migrationVersion = false, -- false = not yet migrated
            },
            --[[ "Link Layouts": do HuD position edits apply to BOTH layouts?
                 ACCOUNT-WIDE, because the data it governs is. `positions`
                 lives in profile scope, and the layout profiles (RealUI /
                 RealUI-Healing) are shared by every character using them — so
                 a per-character flag could not protect anything: one character
                 editing with link on rewrites both profiles for everyone.
                 It was profile scoped until 2026-08-23 (which let it read ON
                 in Healing and OFF in DPS/Tank at once), then briefly char
                 scoped, which had the same mismatch one level up.
                 Unlike scopeLinks, which IS per-character because it selects
                 which profile each scope follows and that varies by spec. ]]
            -- Default OFF (owner decision, 2026-08-24, verifying beta 10 p3).
            -- Linking is a deliberate choice: it makes every position write hit
            -- BOTH layouts, which is surprising for anyone who has not asked
            -- for it and is the mechanism behind the B86/B89/B104 confusion.
            -- Opt in from HuD > General.
            positionsLink = false,
        },
        char = {
            init = charInit,
            layout = {
                current = 1, -- 1 = DPS/Tank, 2 = Healing
                spec = spec -- Save layout for each spec
            },
            specProfiles = {}, -- Custom spec-to-profile mapping
            -- Scope link toggles for coordinated profile switching (per-character, not per-profile)
            scopeLinks = {
                skins = false,  -- Appearance shared across specs by default
                bt4 = true      -- Action bars change with spec by default
            },

            editmode = {
                perCharacter = false, -- Use account-wide EditMode layouts by default
            }
        },
        profile = {
            modules = {
                ["*"] = true
            },
            registeredChars = {},
            -- HuD positions
            -- NOTE: positionsLink is account-wide (db.global) since 2026-08-23.
            --[[ NO position defaults. This is deliberate — B80/B86, 2026-08-24.

                 `RealUI.defaultPositions` used to be handed to AceDB here, as
                 the same table object rather than a copy, because it doubles
                 as the shared runtime fallback: HuDPositioning writes its
                 resolution-scaled calculated values into it and deliberately
                 does NOT write them into `db.profile.positions` (that path is
                 fill-missing-only — writing there wiped user values on every
                 layout switch, B44).

                 That made AceDB the delivery mechanism for calculated values,
                 and therefore also the DELETION mechanism for saved ones:
                 `removeDefaults` strips any saved key equal to the current
                 default, and the "defaults" here are a live table that the
                 user's own values get promoted into. Caught in the act
                 2026-08-24 — a profile went from 11 saved position keys to 3
                 between two dumps, losing exactly the eight that matched the
                 defaults table. It is what deleted a linked HuDY write and
                 left the slider reading 0 (B86).

                 Deep-copying (attempted and reverted 2026-08-23) is NOT the
                 answer: it severs the channel, unsaved keys fall back to raw
                 shipped constants instead of scaled ones, and the unit frames
                 visibly collapse toward centre on HiDPI.

                 Instead the fallback is now explicit, in
                 `Positioners.GetKeyAdjust`: a missing key reads
                 `RealUI.defaultPositions[layout][key]` directly. Same value,
                 same aliasing, same behaviour — AceDB simply is not involved,
                 so it has nothing to strip. An empty table keeps
                 `db.profile.positions` a real table for the code that indexes
                 it without checking. ]]
            positions = {},
            -- Action Bar settings
            abSettingsLink = false,
            -- Dynamic UI settings
            settings = {
                hudSize = 2,
                reverseUnitFrameBars = false,
                performanceMonitorEnabled = false
            }
        }
    }
end

--------------------------------------------------------

-- Move HuD Up if using a Low Resolution display
function RealUI:SetLowResOptimizations(...)
    -- Use HuD positioning system if available
    if self.HuDPositioning then
        self.HuDPositioning:ApplyResolutionOptimizations()
        dbg.tags.lowResOptimized = true
        return
    end

    -- Fallback to legacy optimization
    local dbp, dp = db.positions, self.defaultPositions
    if (dbp[RealUI.cLayout]["HuDY"] == dp[RealUI.cLayout]["HuDY"]) then
        dbp[RealUI.cLayout]["HuDY"] = -5
    end
    if (dbp[RealUI.ncLayout]["HuDY"] == dp[RealUI.ncLayout]["HuDY"]) then
        dbp[RealUI.ncLayout]["HuDY"] = -5
    end
    db.settings.hudSize = 1

    RealUI:UpdateLayout()

    dbg.tags.lowResOptimized = true
end

function RealUI:IsUsingLowResDisplay()
    -- Use HuD positioning system if available
    if self.HuDPositioning then
        return self.HuDPositioning:IsLowResolution()
    end

    -- Fallback to legacy detection
    local _, pysHeight = _G.GetPhysicalScreenSize()
    return pysHeight < 1080
end

function RealUI:IsUsingHighResDisplay()
    -- Use HuD positioning system if available
    if self.HuDPositioning then
        return self.HuDPositioning:IsHighResolution()
    end

    -- Fallback to legacy detection
    local _, pysHeight = _G.GetPhysicalScreenSize()
    return pysHeight >= 1440
end

-- Layout Updates
function RealUI:UpdateLayout(layout)
    layout = layout or dbc.layout.current

    -- Only delegate to LayoutManager if it's not already mid-switch
    -- (prevents recursion if called from PerformLayoutSwitch or callbacks)
    if self.LayoutManager and self.LayoutManager:IsValidLayout(layout)
       and not self.LayoutManager:IsSwitchInProgress() then
        return self.LayoutManager:SwitchToLayout(layout)
    end

    -- Fallback to legacy layout handling (also used when LayoutManager
    -- is mid-switch to avoid re-entry)
    dbc.layout.current = layout

    -- Set Current and Not-Current layout variables
    self.cLayout = layout
    self.ncLayout = layout == 1 and 2 or 1

    -- Update HuD positioning for the new layout
    if self.HuDPositioning then
        self.HuDPositioning:CalculatePositions()
    end

    -- Update ActionBars for the new layout
    local ActionBars = self:GetModule("ActionBars", true)
    if ActionBars and ActionBars:IsEnabled() then
        ActionBars:ApplyABSettings()
    end

    -- Update Positioners
    self:UpdatePositioners()

    if self.isConfigMode and _G.Grid2Options then
        self:ScheduleTimer(
            function()
                self:ToggleGridTestMode(false)
                self:ToggleGridTestMode(true)
            end,
            0.5
        )
    end
end

-- Version info retrieval
function RealUI:GetVerString()
    return RealUI.verinfo.string
end
function RealUI:GetVersionChange(oldVer, curVer)
    if curVer[1] > oldVer[1] then
        return "major"
    elseif curVer[2] > oldVer[2] then
        return "minor"
    end
    -- Same major.minor — no version change (patch-level handled by minipatches)
    return nil
end

-- To help position UI elements
function _G.RealUI_TestRaidWarnings()
    -- 12.1: RaidNotice_AddMessage is a CVar-gated deprecation shim and
    -- RaidBossEmoteFrame is gone — boss emotes are a MessageType on the
    -- single rebuilt RaidWarningFrame, driven via RaidWarningUtil
    local MessageType = _G.RaidWarningUtil.MessageType
    RealUI:ScheduleRepeatingTimer(
        function()
            _G.RaidWarningUtil.AddMessage(_G.CHAT_MSG_RAID_WARNING, {r = 0, g = 1, b = 0}, nil, MessageType.RaidWarning)
            _G.RaidWarningUtil.AddMessage(_G.CHAT_MSG_RAID_BOSS_EMOTE, {r = 0, g = 1, b = 0}, nil, MessageType.BossEmote)
        end,
        5
    )
end

function RealUI:Taint_Logging_Toggle()
    local taintLog = _G.GetCVar("taintLog")
    _G.SetCVar("taintLog", (taintLog ~= "0") and "0" or "2")
    _G.ReloadUI()
end

function RealUI:ChatCommand_Config(input)
    -- Handle subcommands
    if input and input ~= "" then
        local command = input:lower():match("^(%S+)")

        if command == "setup" then
            -- Run setup wizard (force show)
            if self.SetupSystem then
                self.SetupSystem:StartSetup(true)
            else
                print("Setup system not available")
            end
            return
        elseif command == "reset" then
            -- Ask which scope. This used to wipe every setting for every
            -- character instantly, with no confirmation at all — the most
            -- destructive command in the suite was the only one that never
            -- asked. `/realui resetchar` and `/realui resetall` remain the
            -- unprompted quick paths.
            _G.StaticPopup_Show("REALUI_RESET_SCOPE")
            return
        elseif command == "resetchar" then
            -- Quick path, no prompt (`/realui reset` is the one that asks).
            -- Wipes EVERY setting stored against this character: RealUI's char
            -- scope and its namespaces, plus this character's entry in each
            -- sibling addon DB. Profile and account-wide data are untouched,
            -- so Link Layouts in particular survives — see ResetCharacter.
            self:ResetCharacter()
            return
        elseif command == "resetall" then
            -- Quick path, no prompt: full wipe, all characters.
            self:ResetEverything()
            return
        elseif command == "editmode" then
            -- B131: an upgrade that changes the shipped layout rebuilds every
            -- EditMode entry, discarding whatever the user had moved. The
            -- rebuild now copies the old layout aside first; this is how it
            -- comes back.
            local EMM = self.EditModeManager
            if not EMM then
                print("|cff0099ffRealUI|r: EditMode manager not available.")
                return
            end

            local sub = input:lower():match("^%S+%s+(%S+)")
            if sub == "restore" then
                local ok, msg = EMM:RestoreLayoutBackup()
                print(("|cff0099ffRealUI|r: %s"):format(msg))
                if ok then
                    print("|cff808080  Your own EditMode changes are back. Reload to see them.|r")
                end
            else
                local backups = EMM:GetLayoutBackups()
                if next(backups) then
                    print("|cff0099ffRealUI|r: EditMode layout backups available:")
                    for layoutName, fromVersion in next, backups do
                        print(("|cffff9900  %s|r |cff808080(your layout as it was at version %d, before it was rebuilt)|r")
                            :format(layoutName, fromVersion))
                    end
                    print("|cff808080  /realui editmode restore|r — put them back")
                else
                    print("|cff0099ffRealUI|r: No EditMode layout backup stored.")
                end
            end
            return
        elseif command == "display" then
            if self.DisplayStage then
                self.DisplayStage.Open()
            else
                print("|cff0099ffRealUI|r: Display stage not available.")
            end
            return
        elseif command == "grid2update" then
            if not _G.Grid2DB then
                print("|cff0099ffRealUI|r: Grid2 is not loaded.")
                return
            end
            local changed = private.Grid2ProfileMigration()
            if changed then
                local dbg2 = self.db and self.db.global
                if dbg2 then
                    dbg2.grid2MigrationState = "applied"
                end
                print("|cff0099ffRealUI|r: Grid2 profiles updated with new 3.x additions.")
            else
                print("|cff0099ffRealUI|r: Grid2 profiles are already up to date.")
            end
            return
        elseif command == "grid2setpos" then
            local GridLayoutMod = self:GetModule("GridLayout", true)
            if GridLayoutMod and GridLayoutMod:IsEnabled() then
                GridLayoutMod:ApplyPosition()
            else
                print("|cff0099ffRealUI|r: GridLayout module not available or Grid2 not loaded.")
            end
            return
        elseif command == "resetinventory" then
            local invMod = self:GetModule("Inventory", true)
            if invMod then
                local inv = invMod.main
                if inv then
                    inv:ClearAllPoints()
                    inv:SetPoint("CENTER", _G.UIParent, "CENTER", -200, 0)
                    inv:SetUserPlaced(false)
                end

                local bank = invMod.bank
                if bank then
                    bank:ClearAllPoints()
                    bank:SetPoint("CENTER", _G.UIParent, "CENTER", 200, 0)
                    bank:SetUserPlaced(false)
                end

                print("|cff0099ffRealUI|r: Inventory and Bank positions have been reset.")
            else
                print("|cff0099ffRealUI|r: Inventory module not loaded.")
            end
            return
        elseif command == "setupauras" then
            local AurasAddon = _G.RealUI_Auras
            if not AurasAddon or not AurasAddon.db then
                print("|cff0099ffRealUI|r: RealUI_Auras not loaded.")
                return
            end
            if not C_CooldownViewer.IsCooldownViewerAvailable() then
                print("|cff0099ffRealUI|r: CooldownViewer is not available.")
                return
            end
            local classID = select(3, UnitClass("player"))
            local specIndex = GetSpecialization()
            local presetStr = AurasAddon.Presets and AurasAddon.Presets[classID * 10 + specIndex]
            if not presetStr then
                print("|cff0099ffRealUI|r: No cooldown preset found for your specialization.")
                return
            end
            local added, _, err = AurasAddon.CooldownViewer.MergePreset(presetStr)
            AurasAddon.db.char.cooldownPresetOffered = true
            if err then
                print("|cff0099ffRealUI|r: Preset apply failed:", err)
            elseif added and added > 0 then
                StaticPopup_Show("REALUI_AURAS_RELOAD")
            else
                print("|cff0099ffRealUI|r: Cooldown presets are already applied for your specialization.")
            end
            return
        elseif command == "resetauras" then
            -- Disable all RealUI_Auras groups and re-enable oUF aura elements.
            -- For users who ran the preview and want to revert to native auras.
            local AurasAddon = _G.RealUI_Auras
            if AurasAddon and AurasAddon.db then
                local groups = AurasAddon.db.profile.groups
                if groups then
                    for name, group in pairs(groups) do
                        group.disabled = true
                    end
                end
                -- Re-enable oUF aura elements
                if AurasAddon.EnableOufAuraElements then
                    AurasAddon:EnableOufAuraElements()
                end
                print("|cff0099ffRealUI|r: Aura groups disabled. Reload UI to apply fully.")
                self:ReloadUIDialog()
            else
                print("|cff0099ffRealUI|r: RealUI_Auras not loaded.")
            end
            return
        elseif command == "newdefaults" then
            -- B47: the "apply new defaults" picker (auto-shows once per
            -- character; this reopens it on demand, listing everything)
            if RealUI.NewDefaults then
                RealUI.NewDefaults:Show(false)
            end
            return
        elseif command == "devcheck" then
            -- Reports anything left in a non-shipping state: dev A/B toggles,
            -- taint logging, disabled skins, leftover diagnostic keys. Three
            -- separate debugging detours in Aug 2026 turned out to be a stale
            -- toggle from an earlier session (objectiveTracker, chat,
            -- devRestoreInsertFrame), each of which looked exactly like a bug.
            local found = 0
            local function report(label, value, expected)
                found = found + 1
                print(("|cffff9900  %s|r = %s |cff808080(expected %s)|r")
                    :format(label, tostring(value), tostring(expected)))
            end

            print("|cff0099ffRealUI|r: dev/diagnostic state check")

            local taintLog = _G.GetCVar("taintLog")
            if taintLog and taintLog ~= "0" then
                report("CVar taintLog", taintLog, 0)
            end

            local ac = _G.AuroraConfig
            if ac then
                -- Dev A/B toggles: anything true here is a test flag.
                if ac.devRestoreInsertFrame then
                    report("AuroraConfig.devRestoreInsertFrame", true, false)
                end

                -- Skin toggles that default ON. A disabled skin is a valid user
                -- choice, but it also silently changes what is being tested.
                -- Only keys whose shipping default is true; mainmenubar
                -- deliberately defaults false and is not listed.
                for _, key in next, {
                    "chat", "fonts", "objectiveTracker", "bags", "banks",
                    "tooltips",
                } do
                    if ac[key] == false then
                        report("AuroraConfig." .. key, false, true)
                    end
                end

                -- Leftover keys from removed diagnostic harnesses.
                for _, key in next, { "fontBisect", "chatBisect" } do
                    if ac[key] then
                        report("AuroraConfig." .. key .. " (harness removed)", ac[key], "nil")
                    end
                end
            end

            -- B78: a switched-off HuD module looks EXACTLY like a broken one —
            -- no error, no message, the element simply is not there. A stale
            -- `false` from the 3.x-era profile-cascade bugs left cast bars off
            -- in a healing profile for months without a single clue on screen.
            local offModules = RealUI:GetDisabledCoreModules()
            if offModules then
                report("Modules " .. offModules .. " (disabled in profile '"
                    .. tostring(RealUI.db:GetCurrentProfile()) .. "')", false, true)
            end

            if _G.C_AddOns.IsAddOnLoaded("RealUI_Dev") then
                print("|cff808080  RealUI_Dev is loaded (not shipped to users).|r")
            end

            if found == 0 then
                print("|cff00ff00  Nothing unusual — everything is at its shipping default.|r")
            else
                print(("|cffff9900  %d setting(s) differ from shipping defaults.|r"):format(found))
            end
            return
        end
    end

    -- Default behavior: open config
    RealUI.Debug("Config", "/real")
    dbg.tags.slashRealUITyped = true
    RealUI.LoadConfig("HuD")
end

local configLoaded = false
function RealUI.LoadConfig(app, section, ...)
    debug("RealUI.LoadConfig", app, section, ...)
    if _G.InCombatLockdown() then
        return RealUI:Notification(
            L["Alert_CombatLockdown"],
            true,
            L["Alert_CantOpenInCombat"],
            nil,
            [[Interface\AddOns\RealUI\Media\Notification_Alert]]
        )
    end
    debug("is loaded", configLoaded)
    if not configLoaded then
        local reason
        configLoaded, reason = _G.C_AddOns.LoadAddOn("RealUI_Config")
        debug("LoadAddOn", configLoaded, reason)
        if not configLoaded then
            _G.error(_G.ADDON_LOAD_FAILED:format("RealUI_Config", _G["ADDON_" .. reason]))
        end
    end
    debug("ToggleConfig", RealUI.ToggleConfig)
    if type(RealUI.ToggleConfig) ~= "function" then
        -- Config.lua errored during load before defining ToggleConfig.
        -- Reset so the next invocation retries LoadAddOn.
        configLoaded = false
        return
    end
    RealUI.ToggleConfig(app, section, ...)
end

-- One-shot event frame for deferring layout updates out of combat lockdown.
-- Uses a dedicated frame to avoid overwriting the PLAYER_REGEN_ENABLED handler
-- registered by SystemIntegration on the RealUI AceEvent object.
local profileUpdateDeferFrame = _G.CreateFrame("Frame")
profileUpdateDeferFrame:Hide()

function RealUI:OnProfileUpdate(event, database, profile)
    db = database.profile
    dbc = database.char
    dbg = database.global

    -- When a brand-new profile is created, AceDB gives us a mostly-empty
    -- table.  Merge any missing keys from ProfileSystem defaults so that
    -- modules never read nil where they expect a value.
    if event == "OnNewProfile" and self.ProfileSystem then
        local profileDefaults = self.ProfileSystem:GetDatabaseDefaults().profile
        if profileDefaults then
            for key, value in next, profileDefaults do
                if db[key] == nil then
                    if type(value) == "table" then
                        db[key] = self.ProfileSystem:DeepCopyTable(value)
                    else
                        db[key] = value
                    end
                end
            end
        end
    end

    -- Only push profiles to third-party addons if the install wizard has
    -- completed (installStage == -1).  During initial setup the wizard
    -- calls AddRealUIProfiles + SetProfilesToRealUI explicitly at the end.
    if dbc.init.installStage == -1 then
        -- Bug 5 fix: Snapshot module enabled states before profile forcing
        -- so we can restore any modules that get incorrectly disabled by the
        -- profile cascade.
        local moduleSnapshot = {}
        if db.modules then
            for modName, enabled in next, db.modules do
                moduleSnapshot[modName] = enabled
            end
        end

        -- Bug 5 fix (7.3): Respect userOverride per addon. After the wizard
        -- (installStage == -1), skip profile forcing for addons where the
        -- user has set userOverride = true in AddonControl.
        local acModule = self:GetModule("AddonControl", true)
        local acDB = acModule and acModule.db and acModule.db.profile
        for addonName in next, private.Profiles do
            local skip = false
            if acDB and acDB.addonControl and acDB.addonControl[addonName] then
                local addonCtrl = acDB.addonControl[addonName]
                if addonCtrl.profiles and addonCtrl.profiles.base
                        and addonCtrl.profiles.base.userOverride then
                    skip = true
                end
            end
            if not skip then
                self:SetAddOnProfileToRealUI(addonName)
            end
        end

        -- Restore any modules that were incorrectly disabled by the cascade.
        -- Only modules that were enabled *before* the cascade are restored, so
        -- a module the user deliberately turned off stays off. Do not add
        -- per-module special cases here: a blanket "always re-enable X" cannot
        -- tell a cascade-cleared flag from a user-cleared one, which is exactly
        -- how CastBars became impossible to disable (B54).
        if db.modules then
            for modName, wasEnabled in next, moduleSnapshot do
                if wasEnabled and not db.modules[modName] then
                    db.modules[modName] = true
                end
            end
        end
    end

    -- Update current/not-current layout BEFORE module callbacks run.
    -- Modules like ActionBars key off RealUI.cLayout when applying
    -- positions; if we leave the layout variables stale the bars are
    -- positioned for the old profile's layout and only snap into place
    -- after the user reloads.
    local targetLayout = private.profileToLayout[profile]
    if targetLayout and self.cLayout ~= targetLayout then
        self.cLayout = targetLayout
        self.ncLayout = (targetLayout == 1) and 2 or 1
        if dbc and dbc.layout then
            dbc.layout.current = targetLayout
        end
    end

    -- Module callbacks run immediately regardless of combat state — they
    -- only update non-protected data references (db.profile pointers, etc.)
    -- Some entries in self.modules are lightweight proxies (e.g. created
    -- by sub-addons that inject a `{ db = ... }` table so RealUI's path
    -- helpers can find them). Skip entries that don't implement the
    -- callback rather than crashing during profile switch.
    -- Each callback is isolated: an error in one module must not abort the
    -- cascade for the remaining modules (or the layout update below), which
    -- would leave the UI half-switched. Errors still reach BugGrabber.
    for _, module in self:IterateModules() do
        if type(module) == "table" and type(module.OnProfileUpdate) == "function" then
            local ok, err = _G.pcall(module.OnProfileUpdate, module, event, profile)
            if not ok then
                debug("OnProfileUpdate failed for module", module.moduleName, err)
                _G.geterrorhandler()(err)
            end
        end
    end

    -- Position/scale updates touch protected frames and must be deferred
    -- when the player is in combat lockdown.
    local function applyLayoutUpdates()
        if not (self.LayoutManager and self.LayoutManager:IsSwitchInProgress()) then
            RealUI:UpdateLayout(private.profileToLayout[profile])
        end
    end

    if _G.InCombatLockdown() then
        debug("OnProfileUpdate: deferring layout updates until combat ends")
        profileUpdateDeferFrame:SetScript("OnEvent", function(frame)
            frame:UnregisterEvent("PLAYER_REGEN_ENABLED")
            frame:SetScript("OnEvent", nil)
            applyLayoutUpdates()
        end)
        profileUpdateDeferFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    else
        applyLayoutUpdates()
    end

    if event == "OnProfileReset" then
        debug("OnProfileReset", RealUI.db.char.init, RealUI.db.char.init.installStage)
        RealUI.db.char.init = charInit
        debug("Char", RealUI.db.char.init, RealUI.db.char.init.installStage)
        -- Only show reload dialog if install wizard won't run;
        -- the wizard has its own reload prompt (REALUI_SETUP_RELOAD).
        if charInit.installStage == -1 then
            RealUI:ReloadUIDialog()
        end
    end
end

---------------------------------------------------------------------------
-- Reset scopes
--
-- What is actually per-character is narrow: AceDB keeps `char` (install
-- state, current layout, spec->profile mapping, scope links) inside the
-- account-wide RealUIDB. Everything *visual* — positions, HuD size, module
-- toggles, bar settings — lives in the PROFILE, and profiles are shared by
-- every character using them. So "reset this character" can only mean
-- "re-run setup for this character"; it cannot reset one character's look
-- in isolation. Profile-level resets belong on the Unified Profile Page,
-- where the user can see which characters share the profile first.
---------------------------------------------------------------------------

-- Every RealUI-owned SavedVariable that stores configuration.
-- Deliberately excludes RealUI_Bugs' RealUI_Storage/RealUI_Debug: those are
-- the captured error log and debug flags, not UI settings — wiping a
-- tester's error history as a side effect of a UI reset loses real data.
local RESET_ALL_SAVED_VARIABLES = {
    "RealUIDB", "RealUICharacter",
    "nibRealUIDB", "nibRealUICharacter",
    "RealUI_ActionBarsDB",
    "RealUI_AurasDB",
    "RealUI_ChatDB",
    "RealUI_CombatTextDB",
    "RealUI_InventoryDB",
    "RealUI_NameplatesDB",
    "RealUI_SkinsDB",
    "RealUI_TooltipsDB",
    "RealUI_TrackerDB",
}

--[[ Wipe every RealUI setting stored against THIS character.

     Rewritten 2026-08-25: it used to reset three keys of `db.char.init` and
     nothing else, so `layout`, `specProfiles`, `scopeLinks` and `editmode`
     survived — along with every namespace's character scope (Infobar's
     progress/currency state, MinimapAdv, AddonListAdv) and the per-character
     scope of the sibling addon DBs. "Reset character" that leaves most of the
     character's settings in place is a trap, and it is how a tester hit a
     ticked Link Layouts box on what they believed was a clean slate.

     Scope is deliberately CHARACTER only:

     - profile data is shared by whichever characters use that profile, so
       wiping it here would reach other characters;
     - `db.global` is account-wide by design — `positionsLink` in particular
       (see the note on it above), which is why `/realui resetchar` still will
       not clear Link Layouts. That is stated in the option's own tooltip
       rather than fixed here, because the layout profiles it writes through
       genuinely are shared.

     Use `/realui resetall` for everything. Third-party saved variables are
     never touched by either — see ResetEverything below. ]]
local RESET_CHAR_DATABASES = {
    "RealUI_ActionBarsDB",
    "RealUI_AurasDB",
    "RealUI_ChatDB",
    "RealUI_CombatTextDB",
    "RealUI_InventoryDB",
    "RealUI_NameplatesDB",
    "RealUI_SkinsDB",
    "RealUI_TooltipsDB",
    "RealUI_TrackerDB",
}

function RealUI:ResetCharacter()
    if not (self.db and self.db.char) then
        print("|cff0099ffRealUI|r: Database not available.")
        return false
    end

    local wiped = 0

    -- RealUI's own character scope, plus every namespace registered under it
    -- (AceDB keeps those in `db.children`). Wiping the stored table is enough:
    -- AceDB re-supplies defaults through the metatable on next access.
    _G.wipe(self.db.char)
    wiped = wiped + 1
    for _, namespace in next, (self.db.children or {}) do
        if namespace.char then
            _G.wipe(namespace.char)
            wiped = wiped + 1
        end
    end

    -- The sibling addon DBs. Reached through the saved variable rather than
    -- the live AceDB object so this works whether or not the addon is loaded;
    -- `char` is keyed by "Name - Realm" and only this character's entry goes.
    local charKey = _G.UnitName("player") .. " - " .. _G.GetRealmName()
    for _, svName in next, RESET_CHAR_DATABASES do
        local sv = _G[svName]
        if type(sv) == "table" and type(sv.char) == "table" and sv.char[charKey] then
            sv.char[charKey] = nil
            wiped = wiped + 1
        end
        -- Namespaced character data lives one level deeper.
        if type(sv) == "table" and type(sv.namespaces) == "table" then
            for _, ns in next, sv.namespaces do
                if type(ns) == "table" and type(ns.char) == "table" and ns.char[charKey] then
                    ns.char[charKey] = nil
                    wiped = wiped + 1
                end
            end
        end
    end

    -- SavedVariablesPerCharacter are this character's by definition.
    _G.RealUICharacter = nil
    _G.nibRealUICharacter = nil

    -- Re-arm the install wizard. Written last so it survives the wipe above.
    self.db.char.init = {
        installStage = 0,
        initialized = false,
        needchatmoved = true
    }

    print(("|cff0099ffRealUI|r: Character settings reset (%d stores). Account-wide settings and other characters are untouched. Reloading UI..."):format(wiped))
    _G.ReloadUI()
    return true
end

--- Wipe every RealUI setting for every character.
-- Third-party saved variables are never touched: Bartender4, Platynator and
-- friends belong to the user's own installs (BT4/Platynator support was
-- removed 2026-08-22), and Grid2/BadBoy are supported optional installs
-- whose data is not ours to destroy either.
function RealUI:ResetEverything()
    for _, name in next, RESET_ALL_SAVED_VARIABLES do
        _G[name] = nil
    end
    print("|cff0099ffRealUI|r: All settings reset for all characters. Reloading UI...")
    _G.ReloadUI()
end

_G.StaticPopupDialogs["REALUI_RESET_SCOPE"] = {
    text = "|cff0099ffRealUI|r Reset\n\n"
        .. "|cffffcc00This Character|r re-runs the setup wizard here. Your saved\n"
        .. "settings are kept.\n\n"
        .. "|cffff4444Everything|r wipes all RealUI settings for |cffff4444every|r character:\n"
        .. "layouts, positions, skins, bars, nameplates, auras, inventory.\n\n"
        .. "To reset just one profile's settings, use Advanced \226\134\146 Profiles.",
    button1 = "This Character",
    button2 = _G.CANCEL,
    button3 = "Everything...",
    OnAccept = function()
        RealUI:ResetCharacter()
    end,
    OnAlt = function()
        -- Second gate: the full wipe is the one action here nobody can undo.
        _G.StaticPopup_Show("REALUI_RESET_ALL_CONFIRM")
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    showAlert = true,
}

_G.StaticPopupDialogs["REALUI_RESET_ALL_CONFIRM"] = {
    text = "|cffff4444Wipe every RealUI setting?|r\n\n"
        .. "This affects |cffff4444all characters on this account|r and cannot be\n"
        .. "undone. The UI will reload and the setup wizard will run again.",
    button1 = "Wipe Everything",
    button2 = _G.CANCEL,
    OnAccept = function()
        RealUI:ResetEverything()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    showAlert = true,
}

_G.StaticPopupDialogs["PUDRUIRELOADUI"] = {
    text = L["DoReloadUI"],
    button1 = "Yes",
    button2 = "No",
    OnAccept = function()
        _G.ReloadUI()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    notClosableByLogout = false
}
--- True while the install wizard is on screen mid-flow.
--- STAGE_COMPLETE is -1; every real stage is >= 0.
function RealUI:IsInstallWizardActive()
    local IW = self.InstallWizard
    if not IW or not IW.GetCurrentStage then return false end

    local stage = IW:GetCurrentStage()
    if not stage or stage < 0 then return false end

    -- The wizard's X button only hides the frame — it does not Complete(), so
    -- the stage deliberately stays >= 0 for NeedsResume to pick up later.
    -- Requiring the frame to be visible stops a dismissed-but-unfinished
    -- wizard from suppressing every reload prompt for the rest of the session.
    local UI = self.InstallUI
    if UI and UI.IsShown then
        return UI:IsShown() and true or false
    end

    return true
end

function RealUI:ReloadUIDialog()
    -- The install wizard raises its own reload at the end of the flow
    -- (REALUI_SETUP_RELOAD, STAGE_FINISH). Any other reload prompt raised
    -- while it is running stacks a second dialog on top of the wizard — which
    -- is what testers hit on first login. Suppress rather than flag pending:
    -- ConsumeReloadPending is only read when the config panel closes, so
    -- flagging here would surface the prompt at an unrelated later moment.
    if self:IsInstallWizardActive() then
        return
    end

    _G.StaticPopup_Show("PUDRUIRELOADUI")
end

---------------------------------------------------------------------------
-- Reload-pending tracking
-- Some config settings can't take effect without a /reload (they touch
-- frames/state that's only set up once at login). Rather than prompting
-- immediately on every such change, config UIs flag the change here and
-- prompt once when the settings panel closes. See RealUI.NeedsReload below
-- for the AceConfig option wrapper that hooks into this automatically.
---------------------------------------------------------------------------
RealUI.reloadPending = false

--- Mark that a pending settings change requires /reload to take effect.
--- Call from a config "set" handler for options that can't apply live.
function RealUI:FlagReloadPending()
    self.reloadPending = true
end

--- Read and clear the pending-reload flag in one step.
--- @return boolean wasPending
function RealUI:ConsumeReloadPending()
    local wasPending = self.reloadPending
    self.reloadPending = false
    return wasPending
end

--- Wrap an AceConfig option table so that changing it flags a pending
--- reload and notes the requirement in its tooltip. The option's own
--- "set" handler still runs first; this only adds the reload flag on top.
--- Usage: args.someToggle = RealUI.NeedsReload({ type = "toggle", ... })
--- @param option table  AceConfig option definition (mutated and returned)
function RealUI.NeedsReload(option)
    local origSet = option.set
    option.set = function(info, ...)
        if origSet then origSet(info, ...) end
        RealUI:FlagReloadPending()
    end
    if type(option.desc) == "string" then
        option.desc = option.desc .. "\n\n|cffffcc00Requires UI reload to take effect.|r"
    elseif option.desc == nil then
        option.desc = "|cffffcc00Requires UI reload to take effect.|r"
    end
    return option
end

function RealUI:OnInitialize()
    debug("OnInitialize starting...")

    -- Initialize Module Framework first
    if self.ModuleFramework then
        self.ModuleFramework:Initialize()
    end

    -- Initialize Version Manager first
    if self.VersionManager then
        self.VersionManager:Initialize()
    end

    -- Initialize Profile System
    if self.ProfileSystem then
        self.ProfileSystem:Initialize()
    end

    -- Initialize Dual-Spec System
    if self.DualSpecSystem then
        self.DualSpecSystem:Initialize()
    end

    -- Initialize Configuration Persistence System
    if self.ConfigPersistence then
        self.ConfigPersistence:Initialize()
    end

    -- Initialize Layout Manager
    if self.LayoutManager then
        self.LayoutManager:Initialize()
    end

    -- Initialize HuD Positioning System
    if self.HuDPositioning then
        self.HuDPositioning:Initialize()
    end

    -- Initialize Frame Movement System
    if self.FrameMover then
        self.FrameMover:Initialize()
    end

    -- Initialize Configuration Mode System
    if self.ConfigMode then
        self.ConfigMode:Initialize()
    end

    -- Initialize Performance Monitor System
    if self.PerformanceMonitor then
        self.PerformanceMonitor:Initialize()
    end

    -- Initialize Feedback System
    if self.FeedbackSystem then
        self.FeedbackSystem:Initialize()
    end

    -- Initialize Diagnostic Tools
    if self.DiagnosticTools then
        self.DiagnosticTools:Initialize()
    end

    -- Initialize Error Recovery System
    if self.ErrorRecovery then
        self.ErrorRecovery:Initialize()
    end

    -- Initialize Resource Manager
    -- NOTE: ResourceManager requires the database to check
    -- settings.performanceMonitorEnabled, so actual initialization is
    -- deferred to after the database is created (see below).

    -- Initialize User Experience Polish
    if self.UXPolish then
        self.UXPolish:Initialize()
    end

    -- NOTE: FinalMigrations:Initialize() is called after the AceDB database
    -- is created and dbg.verinfo is updated (see below), because it needs
    -- RealUI.db to exist for migration checks.

    -- Initialize Setup System
    if self.SetupSystem then
        self.SetupSystem:Initialize()
    end

    -- Perform SavedVariables Migration (before database initialization)
    if self.SavedVariablesMigration then
        local success, reason = self.SavedVariablesMigration:PerformMigration()
        -- Store result for later notification
        self.migrationResult = {success = success, reason = reason}
    end

    -- Note: DeploymentValidator moved to after database initialization

    -- Initialize AceDB-3.0 database with enhanced defaults from ProfileSystem
    local profileDefaults = self.ProfileSystem and self.ProfileSystem:GetDatabaseDefaults() or defaults
    self.db = _G.LibStub("AceDB-3.0"):New("RealUIDB", profileDefaults, private.layoutToProfile[1])

    -- Create healing profile (must exist before DualSpecSystem sets up LDS)
    self.db:SetProfile(private.layoutToProfile[2]) -- create healing profile
    self.db:SetProfile(private.layoutToProfile[1]) -- switch back to default

    -- NOTE: LDS:EnhanceDatabase and SetDualSpecProfile calls are handled
    -- exclusively by DualSpecSystem:PostInitialize() -> SetupLibDualSpec()
    -- to avoid duplicate/conflicting profile mappings.

    -- Initialize ProfileSystem with the database
    if self.ProfileSystem then
        self.ProfileSystem:Initialize(self.db)
    end

    -- Post-initialize DualSpecSystem with database
    if self.DualSpecSystem then
        self.DualSpecSystem:PostInitialize()
    end

    debug("Database initialized", self.db.keys, self.db.char.init.installStage)

    -- Set up database references
    db = self.db.profile
    dbc = self.db.char
    dbg = self.db.global
    self.media = db.media

    -- Character identification
    self.key = ("%s - %s"):format(self.charInfo.name, self.charInfo.realm)
    self.cLayout = dbc.layout.current
    self.ncLayout = self.cLayout == 1 and 2 or 1

    -- Initialize Deployment Validator (after database is ready)
    if self.DeploymentValidator then
        self.DeploymentValidator:Initialize()
    end

    -- Note: Module enabling moved to PLAYER_LOGIN to ensure all modules are registered first
    -- Modules register themselves in OnInitialize which happens during addon loading

    -- Post-initialize LayoutManager with database
    if self.LayoutManager then
        self.LayoutManager:LoadLayoutState()
        self.LayoutManager:ValidateCurrentLayout()
    end

    -- Initialize Resource Manager (deferred to here because it needs the
    -- database to check settings.performanceMonitorEnabled)
    if self.ResourceManager then
        self.ResourceManager:Initialize()
    end

    -- Handle legacy character data migration
    if _G.nibRealUICharacter then
        debug("Migrating legacy character data", self.db.keys.char, _G.nibRealUIDB.profileKeys[self.db.keys.char])
        if db.registeredChars[self.key] then
            dbc.init.installStage = _G.nibRealUICharacter.installStage
            dbc.init.initialized = _G.nibRealUICharacter.initialized
            dbc.init.needchatmoved = _G.nibRealUICharacter.needchatmoved
        end
        _G.nibRealUICharacter = nil
    end

    -- Enhanced version tracking and migration detection
    local currentVersion = RealUI.verinfo
    local savedVersion = dbg.verinfo

    if savedVersion and savedVersion.string and self.VersionManager then
        local versionChange = self.VersionManager:GetVersionType(savedVersion, currentVersion)
        if versionChange ~= "none" then
            debug("Version change detected:", savedVersion.string, "->", currentVersion.string, "Type:", versionChange)
            dbg.versionChange = versionChange

            -- Run any necessary migrations using ConfigPersistence
            if self.ConfigPersistence then
                local configVersion = dbg.configVersion or 0
                local success, err = self.ConfigPersistence:RunMigrations(tostring(configVersion), tostring(1))
                if not success then
                    debug("Configuration migration failed:", err)
                end
            end

            -- Run version-specific migrations
            local success, err = self.VersionManager:RunMigrations(savedVersion, currentVersion)
            if not success then
                debug("Version migration failed:", err)
            end
        end
    end

    -- Update saved version info
    dbg.verinfo = {
        [1] = currentVersion[1],
        [2] = currentVersion[2],
        [3] = currentVersion[3],
        string = currentVersion.string,
        build = currentVersion.build,
        gameVersion = currentVersion.gameVersion
    }

    -- Check library integration
    local libsOk, missingLibs = self:CheckLibraryIntegration()
    if not libsOk then
        debug("Missing libraries detected:", table.concat(missingLibs, ", "))
    end

    -- Initialize Final Migrations (must run after database + verinfo are ready)
    if self.FinalMigrations then
        self.FinalMigrations:Initialize()
    end

    -- Register ProfileCoordinator's OnProfileChanged hook FIRST so linked
    -- scopes (Action Bars, Skins) switch to the new profile BEFORE RealUI's own
    -- cascade reads them. The bars layout recompute runs during the cascade
    -- and needs the bars DB to already be on the right profile.
    if self.ProfileCoordinator and self.ProfileCoordinator.RegisterProfileCallback then
        self.ProfileCoordinator:RegisterProfileCallback()
    end

    -- Register profile change callbacks
    debug("Registering profile callbacks")
    self.db.RegisterCallback(self, "OnNewProfile", "OnProfileUpdate")
    self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileUpdate")
    self.db.RegisterCallback(self, "OnProfileCopied", "OnProfileUpdate")
    self.db.RegisterCallback(self, "OnProfileReset", "OnProfileUpdate")

    -- Register game events for specialization tracking (handled by DualSpecSystem)
    -- Events are now registered in DualSpecSystem:Initialize()

    -- Register font recheck system
    self:RegisterEvent(
        "ADDON_LOADED",
        function()
            if RealUI.recheckFonts then
                local SkinsDB = RealUI.GetOptions("Skins").profile
                local LSM = _G.LibStub("LibSharedMedia-3.0")
                for fontType in next, RealUI.recheckFonts do
                    local font = SkinsDB.fonts[fontType]
                    if type(font) == "table" then
                        for name, path in next, LSM.MediaTable.font do
                            if font.name == name then
                                RealUI.recheckFonts[fontType] = nil
                                SkinsDB.fonts[fontType] = {
                                    name = name,
                                    path = path
                                }
                                break
                            elseif font.name == "" and font.path == path then
                                RealUI.recheckFonts[fontType] = nil
                                SkinsDB.fonts[fontType] = {
                                    name = name,
                                    path = path
                                }
                                break
                            end
                        end
                    end
                end
            end
        end
    )

    -- Register chat commands
    self:RegisterChatCommand("real", "ChatCommand_Config")
    self:RegisterChatCommand("realui", "ChatCommand_Config")
    self:RegisterChatCommand(
        "realadv",
        function()
            RealUI.Debug("Config", "/realadv")
            RealUI.LoadConfig("RealUI")
        end
    )
    self:RegisterChatCommand(
        "rl",
        function()
            _G.C_UI.Reload()
        end
    )
    self:RegisterChatCommand(
        "taintLogging",
        function(input)
            local level = input and input:match("^%s*([012])%s*$")
            if self.DiagnosticTools then
                self.DiagnosticTools:ToggleTaintLogging(level)
            else
                self:Taint_Logging_Toggle()
            end
        end
    )
    self:RegisterChatCommand(
        "taintScan",
        function(input)
            if self.DiagnosticTools then
                self.DiagnosticTools:ScanTaint(input)
            else
                _G.print("DiagnosticTools not available.")
            end
        end
    )
    self:RegisterChatCommand(
        "findSpell",
        function(input)
            -- /findSpell "Spell Name" (player)|target (buff)|debuff
            local spellName, unit, auraType = self:GetArgs(input, 3)
            _G.assert(type(spellName) == "string", "A spell name must be provided")
            unit = unit or "player"
            if auraType == nil then
                -- Default this to false for player, true for target.
                auraType = unit == "target" and "debuff" or "buff"
            end
            self.FindSpellID(spellName, unit, auraType)
        end
    )
    self:RegisterChatCommand(
        "rc",
        function()
            _G.C_PartyInfo.DoReadyCheck()
        end
    )

    -- LayoutManager test commands
    self:RegisterChatCommand(
        "layoutstatus",
        function()
            if self.LayoutManager then
                self.LayoutManager:PrintStatus()
            else
                print("LayoutManager not available")
            end
        end
    )
    self:RegisterChatCommand(
        "layoutswitch",
        function(input)
            if self.LayoutManager then
                local layoutId = tonumber(input)
                if layoutId then
                    local success = self.LayoutManager:SwitchToLayout(layoutId)
                    print("Layout switch to", layoutId, success and "succeeded" or "failed")
                else
                    print("Usage: /layoutswitch <1|2>")
                end
            else
                print("LayoutManager not available")
            end
        end
    )
    self:RegisterChatCommand(
        "layouttoggle",
        function()
            if self.LayoutManager then
                local success = self.LayoutManager:ToggleLayout()
                print("Layout toggle", success and "succeeded" or "failed")
            else
                print("LayoutManager not available")
            end
        end
    )

    -- Frame positioning and movement commands
    self:RegisterChatCommand(
        "framemover",
        function(input)
            if self.FrameMover then
                if input == "status" then
                    self.FrameMover:PrintStatus()
                elseif input == "config" then
                    self.FrameMover:ToggleConfigMode()
                elseif input == "reset" then
                    self.FrameMover:ResetAllPositions()
                    print("All frame positions reset")
                else
                    print("Usage: /framemover <status|config|reset>")
                end
            else
                print("FrameMover not available")
            end
        end
    )
    self:RegisterChatCommand(
        "configmode",
        function()
            if self.ConfigMode then
                self.ConfigMode:ToggleConfigMode()
                print("Config mode", self.ConfigMode:IsConfigModeActive() and "enabled" or "disabled")
            else
                print("ConfigMode not available")
            end
        end
    )

    -- ModuleFramework test commands
    self:RegisterChatCommand(
        "moduleinfo",
        function(input)
            if self.ModuleFramework then
                if input and input ~= "" then
                    local info = self.ModuleFramework:GetModuleInfo(input)
                    if info then
                        print("Module:", info.name, "Type:", info.type, "State:", info.state, "Enabled:", info.enabled)
                        if #info.dependencies > 0 then
                            print("Dependencies:", table.concat(info.dependencies, ", "))
                        end
                    else
                        print("Module not found:", input)
                    end
                else
                    local status = self.ModuleFramework:GetFrameworkStatus()
                    print("ModuleFramework Status:")
                    print("Initialized:", status.initialized)
                    print("Total Modules:", status.totalModules)
                    print("Enabled Modules:", status.enabledModules)
                end
            else
                print("ModuleFramework not available")
            end
        end
    )
    self:RegisterChatCommand(
        "modulelist",
        function()
            if self.ModuleFramework then
                local modules = self.ModuleFramework:GetRegisteredModules()
                print("Registered Modules:")
                for name, info in pairs(modules) do
                    print("-", name, "(" .. info.type .. ")", "State:", info.state)
                end
            else
                print("ModuleFramework not available")
            end
        end
    )
    self:RegisterChatCommand(
        "hudstatus",
        function()
            if self.HuDPositioning then
                self.HuDPositioning:PrintStatus()
            else
                print("HuDPositioning not available")
            end
        end
    )
    self:RegisterChatCommand(
        "hudsize",
        function(input)
            if self.HuDPositioning then
                local sizeId = tonumber(input)
                if sizeId then
                    local success = self.HuDPositioning:SetHuDSize(sizeId)
                    print("HuD size change to", sizeId, success and "succeeded" or "failed")
                else
                    print("Usage: /hudsize <1|2|3>")
                    print("1 = Small, 2 = Large, 3 = Extra Large")
                end
            else
                print("HuDPositioning not available")
            end
        end
    )
    self:RegisterChatCommand(
        "hudrecalc",
        function()
            if self.HuDPositioning then
                self.HuDPositioning:CalculatePositions()
                print("HuD positions recalculated")
            else
                print("HuDPositioning not available")
            end
        end
    )
    self:RegisterChatCommand(
        "hudoptimize",
        function()
            if self.HuDPositioning then
                local optimized = self.HuDPositioning:ApplyResolutionOptimizations()
                print("Resolution optimizations", optimized and "applied" or "not needed")
            else
                print("HuDPositioning not available")
            end
        end
    )

    -- Performance Monitor commands
    self:RegisterChatCommand(
        "perfmon",
        function(input)
            if self.PerformanceMonitor then
                if input == "start" then
                    local success = self.PerformanceMonitor:StartMonitoring()
                    print("Performance monitoring", success and "started" or "already active")
                elseif input == "stop" then
                    local success = self.PerformanceMonitor:StopMonitoring()
                    print("Performance monitoring", success and "stopped" or "not active")
                elseif input == "status" then
                    self.PerformanceMonitor:PrintStatus()
                elseif input == "gc" then
                    local freed, time = self.PerformanceMonitor:PerformGarbageCollection()
                    print(("Garbage collection: freed %s in %s"):format(
                        self.PerformanceMonitor:FormatBytes(freed),
                        self.PerformanceMonitor:FormatTime(time)
                    ))
                elseif input == "alerts on" then
                    self.PerformanceMonitor:SetAlertsEnabled(true)
                    print("Performance alerts enabled")
                elseif input == "alerts off" then
                    self.PerformanceMonitor:SetAlertsEnabled(false)
                    print("Performance alerts disabled")
                else
                    print("Usage: /perfmon <start||stop||status||gc||alerts on||alerts off>")
                end
            else
                print("PerformanceMonitor not available")
            end
        end
    )

    -- Installation Wizard commands
    self:RegisterChatCommand(
        "installwizard",
        function(input)
            if self.InstallWizard then
                if input == "start" then
                    local success = self.InstallWizard:Start()
                    print("Installation wizard", success and "started" or "not needed")
                elseif input == "reset" then
                    self.InstallWizard:Reset()
                    print("Installation wizard reset")
                elseif input == "complete" then
                    self.InstallWizard:Complete()
                    print("Installation marked as complete")
                else
                    print("Usage: /installwizard <start|reset|complete>")
                end
            else
                print("InstallWizard not available")
            end
        end
    )

    -- Tutorial System commands
    self:RegisterChatCommand(
        "tutorial",
        function(input)
            if self.TutorialSystem then
                if input == "start" then
                    self.TutorialSystem:Start()
                    print("Tutorial started")
                elseif input == "skip" then
                    self.TutorialSystem:Skip()
                    print("Tutorial skipped")
                elseif input == "reset" then
                    self.TutorialSystem:Reset()
                    print("Tutorial reset")
                elseif input == "help" then
                    self.TutorialSystem:ShowSetupInstructions()
                else
                    print("Usage: /tutorial <start|skip|reset|help>")
                end
            else
                print("TutorialSystem not available")
            end
        end
    )

    -- Character initialization commands
    self:RegisterChatCommand(
        "charinit",
        function(input)
            if self.CharacterInit then
                if input == "setup" then
                    self.CharacterInit:Setup()
                    print("Character setup completed")
                elseif input == "reset" then
                    self.CharacterInit:Reset()
                    print("Character initialization reset")
                elseif input == "info" then
                    local info = self.CharacterInit:GetCharacterInfo()
                    print("Character:", info.fullName, "Role:", info.role, "Level:", info.level)
                else
                    print("Usage: /charinit <setup / reset / info>")
                end
            else
                print("CharacterInit not available")
            end
        end
    )

    -- Setup System commands
    self:RegisterChatCommand(
        "realuisetup",
        function(input)
            if self.SetupSystem then
                if input == "run" or input == "" then
                    self.SetupSystem:StartSetup(true)
                elseif input == "check" then
                    local state = self.SetupSystem:GetState()
                    print("Setup needed:", state.needsSetup)
                    print("Is upgrade:", state.isUpgrade)
                    print("Old version:", tostring(state.oldVersion))
                    print("Detected old config:", state.detectedOldConfig)
                elseif input == "migrate" then
                    local success, errors = self.SetupSystem:MigrateOldSettings()
                    if success then
                        print("Migration completed successfully")
                    else
                        print("Migration completed with errors:", table.concat(errors or {}, ", "))
                    end
                else
                    print("Usage: /realuisetup <run|check|migrate>")
                end
            else
                print("SetupSystem not available")
            end
        end
    )

    -- Diagnostic Tools commands
    self:RegisterChatCommand(
        "diagnostic",
        function(input)
            if self.DiagnosticTools then
                self.DiagnosticTools:RunDiagnostic(input or "all")
            else
                print("DiagnosticTools not available")
            end
        end
    )
    self:RegisterChatCommand(
        "healthcheck",
        function()
            if self.DiagnosticTools then
                self.DiagnosticTools:PrintHealthCheck()
            else
                print("DiagnosticTools not available")
            end
        end
    )
    self:RegisterChatCommand(
        "systemstatus",
        function()
            if self.DiagnosticTools then
                self.DiagnosticTools:PrintSystemStatus()
            else
                print("DiagnosticTools not available")
            end
        end
    )
    self:RegisterChatCommand(
        "exportdiag",
        function()
            if self.DiagnosticTools then
                self.DiagnosticTools:ExportDiagnosticReport()
            else
                print("DiagnosticTools not available")
            end
        end
    )
    self:RegisterChatCommand(
        "troubleshoot",
        function(input)
            if self.FeedbackSystem then
                if input and input ~= "" then
                    self.FeedbackSystem:ShowTroubleshootingGuide(input)
                else
                    print("Usage: /troubleshoot <guide_key>")
                    print("Available guides: combat_lockdown, addon_conflict, profile_corruption, performance_issues")
                end
            else
                print("FeedbackSystem not available")
            end
        end
    )
    self:RegisterChatCommand(
        "feedbackhistory",
        function(input)
            if self.FeedbackSystem then
                local count = tonumber(input) or 10
                self.FeedbackSystem:PrintHistory(count)
            else
                print("FeedbackSystem not available")
            end
        end
    )

    -- Resolution Optimizer commands
    self:RegisterChatCommand(
        "resoptimizer",
        function(input)
            if self.ResolutionOptimizer then
                if input == "status" then
                    self.ResolutionOptimizer:PrintStatus()
                elseif input == "optimize" then
                    local success = self.ResolutionOptimizer:ApplyOptimizations(true)
                    print("Resolution optimization", success and "applied" or "failed")
                elseif input == "reset" then
                    local success = self.ResolutionOptimizer:ResetOptimizations()
                    print("Resolution optimizations", success and "reset" or "failed")
                else
                    print("Usage: /resoptimizer <status|optimize|reset>")
                end
            else
                print("ResolutionOptimizer not available")
            end
        end
    )

    -- Compatibility Manager commands
    self:RegisterChatCommand(
        "compat",
        function(input)
            if self.CompatibilityManager then
                if input == "status" then
                    self.CompatibilityManager:PrintStatus()
                elseif input == "check" then
                    self.CompatibilityManager:CheckCompatibility()
                    print("Compatibility check complete")
                elseif input == "safemode on" then
                    local success = self.CompatibilityManager:EnableSafeMode()
                    print("Safe mode", success and "enabled" or "already active")
                elseif input == "safemode off" then
                    local success = self.CompatibilityManager:DisableSafeMode()
                    print("Safe mode", success and "disabled" or "not active")
                else
                    print("Usage: /compat <status|check|safemode on|safemode off>")
                end
            else
                print("CompatibilityManager not available")
            end
        end
    )

    -- Profile Manager commands
    self:RegisterChatCommand(
        "profilemgr",
        function(input)
            if self.ProfileManager then
                if input == "status" then
                    self.ProfileManager:PrintStatus()
                elseif input == "backup" then
                    local success = self.ProfileManager:CreateBackup("manual")
                    print("Backup", success and "created" or "failed")
                elseif input == "restore" then
                    local success = self.ProfileManager:RestoreBackup(1)
                    print("Backup", success and "restored" or "failed")
                elseif input == "backups" then
                    local backups = self.ProfileManager:GetBackups()
                    print("Available backups:")
                    for _, backup in ipairs(backups) do
                        print(("%d. %s - %s"):format(backup.index, backup.label, backup.date))
                    end
                else
                    print("Usage: /profilemgr <status|backup|restore|backups>")
                end
            else
                print("ProfileManager not available")
            end
        end
    )

    -- Initialize chat frame positioning if needed
    if dbc.init.needchatmoved then
        _G.ChatFrame1:ClearAllPoints()
        _G.ChatFrame1:SetPoint("BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 6, RealUI.GetChatYOffset())
        _G.ChatFrame1:SetFrameLevel(15)
        _G.ChatFrame1:SetHeight(145)
        _G.ChatFrame1:SetWidth(400)
        _G.ChatFrame1:SetUserPlaced(true)
        _G.FCF_SavePositionAndDimensions(_G.ChatFrame1)
        dbc.init.needchatmoved = false
    end

    -- Configure user settings synchronization for first-time users
    -- Defer SetCVar calls to a new execution context so addon code does not
    -- contaminate the ADDON_LOADED processing chain with taint that propagates
    -- to C_ActionBar and causes ADDON_ACTION_BLOCKED on MultiBar:ShowBase().
    _G.C_Timer.After(0, function()
        if dbg.tags.firsttime then
            _G.SetCVar("synchronizeSettings", 1)
            _G.SetCVar("synchronizeConfig", 1)
            _G.SetCVar("synchronizeBindings", 1)
            _G.SetCVar("synchronizeMacros", 1)
        end

        -- Ensure quest text contrast is properly set
        if ((_G.GetCVarNumberOrDefault("questTextContrast")) ~= 4) then
            _G.SetCVar("questTextContrast", 4)
        end
    end)

    -- Mark framework as initialized
    RealUI.isInitialized = true

    -- Initialize System Integration (must be last)
    if self.SystemIntegration then
        self.SystemIntegration:Initialize()
    end

    -- Initialization complete message
    _G.print(("RealUI %s loaded."):format(RealUI:GetVerString(true)))
    if not dbg.tags.slashRealUITyped and dbc.init.installStage == -1 then
        _G.print(L["Slash_RealUI"]:format("|cFFFF8000/realui|r"))
    end

    -- Display account status information
    _G.print(("Limited mode is active: %s."):format(_G.tostring(_G.GameLimitedMode_IsActive())))

    debug("OnInitialize completed successfully")
end

local onLoadMessages = {
    --[[
    test = {
        text = "This is a test",
        func = function(...)
            _G.print("Test message clicked!!")
        end,
    }
    ]]
    reload = {
        text = "When changing the position of UI frames, please be sure to reload the UI with /rl"
    }
}
function RealUI:OnEnable()
    debug("OnEnable starting", dbc.init.installStage)

    -- Reset per-session flags
    dbg.tags.uiScaleWarningDismissed = false

    -- Enable modules from database (after all modules have registered)
    if self.ModuleFramework then
        self.ModuleFramework:EnableModulesFromDatabase()
    end

    -- Post-initialize HuD positioning system with database
    if self.HuDPositioning and not self.HuDPositioning:GetHuDState().initialized then
        self.HuDPositioning:Initialize()
    end

    -- Check if setup is needed for 3.0.0
    if self.SetupSystem then
        local setupState = self.SetupSystem:GetState()
        if setupState.needsSetup then
            debug("Setup needed, checking and running setup system")
            self.SetupSystem:CheckAndRun()

            -- Show upgrade notification if this is an upgrade
            if setupState.isUpgrade then
                self.SetupSystem:ShowUpgradeNotification()
            end
        end
    end

    -- Show migration notification if migration was performed
    if self.migrationResult and self.SavedVariablesMigration then
        self.SavedVariablesMigration:ShowMigrationNotification(
            self.migrationResult.success,
            self.migrationResult.reason
        )
    end

    -- Check if Installation/Patch is necessary
    self:InstallProcedure()

    if dbc.init.installStage == -1 then
        -- Apply low resolution optimizations if needed
        if self:IsUsingLowResDisplay() and not dbg.tags.lowResOptimized then
            self:SetLowResOptimizations()
        end

        -- Handle tutorial system
        if dbg.tutorial.stage > -1 then
            self:InitTutorial()
        else
            -- Display helpful messages for completed installations
            for name, messageInfo in next, onLoadMessages do
                if not dbg.messages[name] then
                    self:Notification(name, true, messageInfo.text, messageInfo.func, messageInfo.icon)
                    if name ~= "test" then
                        dbg.messages[name] = true
                    end
                end
            end

            -- Localization contribution message
            if not _G.LOCALE_enUS then
                _G.print(
                    "Want to contribute? You can help localize RealUI into your native language at bit.ly/RealUILocale"
                )
            end
        end
    end

    -- UI Scale CVar conflict detection
    -- RealUI_Skins.UpdateUIScale force-disables useUiScale at startup and
    -- hooks SetCVar to revert it if toggled mid-session. The notification
    -- is shown from the hook in RealUI_Skins when the revert happens.

    -- Performance debugging notifications
    if _G.GetCVar("taintLog") ~= "0" then
        _G.print(L["Slash_Taint"])
    end

    -- Initialize specialization tracking (handled by DualSpecSystem)
    if self.DualSpecSystem and self.DualSpecSystem:IsInitialized() then
        local currentSpec = self.DualSpecSystem:GetCurrentSpec()
        if currentSpec then
            debug("Current specialization:", currentSpec)
        end
    end

    -- Update frame styling
    self:UpdateFrameStyle()

    -- Grid2 profile migration check for existing users
    -- Deferred to avoid conflicting with minipatch or install popups
    if dbc.init.installStage == -1 and _G.Grid2DB then
        _G.C_Timer.After(3, function()
            private.CheckGrid2Migration()
        end)
    end

    -- Mark framework as enabled
    RealUI.isEnabled = true

    debug("OnEnable completed successfully")
end

-- Enhanced Module Management System (handled by ModuleFramework)
-- The module prototype is now set up in ModuleFramework:Initialize()

-- Enhanced Module State Management (using ModuleFramework)
function RealUI:GetModuleEnabled(module)
    if self.ModuleFramework and self.ModuleFramework:IsModuleRegistered(module) then
        return self.ModuleFramework:IsModuleEnabled(module)
    end

    -- Fallback to database check
    return self.db.profile.modules[module]
end

function RealUI:SetModuleEnabled(module, value)
    value = value and true or false

    -- Compare against the module's live state as well as the stored flag.
    -- The two can disagree, and testing only the flag turned this into a
    -- silent no-op in exactly the case where the user most needs it: the
    -- setting reads enabled while the module is not actually running, so
    -- ticking the box did nothing and there was no way out through the UI
    -- (the shape of B54). Bailing only when BOTH already match lets the
    -- toggle repair a desync instead of ignoring it.
    local moduleObj = self:GetModule(module, true)
    local flagMatches = (self:GetModuleEnabled(module) and true or false) == value
    local stateMatches = (moduleObj == nil)
        or ((moduleObj:IsEnabled() and true or false) == value)
    if flagMatches and stateMatches then
        return value
    end

    -- Update database
    if self.db.profile.modules[module] ~= value then
        self.db.profile.modules[module] = value
    end

    -- Use ModuleFramework if available
    if self.ModuleFramework and self.ModuleFramework:IsModuleRegistered(module) then
        if value then
            return self.ModuleFramework:EnableModule(module)
        else
            return self.ModuleFramework:DisableModule(module)
        end
    end

    -- Fallback to direct module control
    if value then
        self:EnableModule(module)
    else
        self:DisableModule(module)
    end

    return value
end

-- Enhanced Module Registration
function RealUI:RegisterRealUIModule(moduleName, moduleType, dependencies, options)
    if not self.ModuleFramework then
        debug("ModuleFramework not available for module registration:", moduleName)
        return false
    end

    return self.ModuleFramework:RegisterModule(moduleName, moduleType, dependencies, options)
end

-- Framework Status Functions
function RealUI:IsFrameworkReady()
    return self.isInitialized and self.isEnabled
end

function RealUI:GetFrameworkStatus()
    return {
        initialized = self.isInitialized,
        enabled = self.isEnabled,
        version = self.verinfo,
        layout = self.cLayout,
        installStage = dbc and dbc.init.installStage or 0
    }
end

-- Enhanced Library Integration Check
function RealUI:CheckLibraryIntegration()
    local libraries = {
        "AceAddon-3.0",
        "AceConsole-3.0",
        "AceEvent-3.0",
        "AceTimer-3.0",
        "LibDualSpec-1.0"
    }

    local missing = {}
    for _, lib in ipairs(libraries) do
        if not _G.LibStub:GetLibrary(lib, true) then
            table.insert(missing, lib)
        end
    end

    if #missing > 0 then
        debug("Missing libraries:", table.concat(missing, ", "))
        return false, missing
    end

    return true, {}
end

-- Namespace Management
function RealUI:RegisterNamespace(name, namespace)
    if not self.namespaces then
        self.namespaces = {}
    end
    self.namespaces[name] = namespace
    debug("Registered namespace:", name)
end

function RealUI:GetNamespace(name)
    return self.namespaces and self.namespaces[name]
end

--[[ B78: announce core HuD modules that are switched off.

     MUST be deferred, not printed from OnInitialize. LibDualSpec switches the
     profile after init — a healer logging in is still on "RealUI" at
     OnInitialize time and only lands on "RealUI-Healing" a moment later. The
     first attempt printed from OnInitialize and stayed silent for exactly the
     case it was written for: CastBars disabled in RealUI-Healing, checked
     against RealUI, reported nothing (verified in game 2026-08-23).

     Same PLAYER_ENTERING_WORLD + 8s pattern the new-defaults popup uses, for
     the same reason: it is the first point at which the profile cascade,
     spec detection and module states have all settled. ]]
local moduleNoticeFrame = _G.CreateFrame("Frame")
moduleNoticeFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
moduleNoticeFrame:SetScript("OnEvent", function(self)
    self:UnregisterAllEvents()
    _G.C_Timer.After(8, function()
        local off = RealUI:GetDisabledCoreModules()
        if not off then return end
        _G.print(("|cff0099ffRealUI|r: |cffffcc00%s|r switched off in profile '%s' — that is a setting, not a fault. Re-enable in |cff88ccff/realadv|r.")
            :format(off, RealUI.db:GetCurrentProfile()))
    end)
end)
