local _, private = ...

-- Lua Globals --
-- luacheck: globals next type _G

-- RealUI --
local RealUI = private.RealUI
local debug = RealUI.GetDebug("InstallWizard")

-- Installation Wizard Framework
-- Handles first-time setup detection, progressive installation stages, and resumption

local InstallWizard = {}
RealUI.InstallWizard = InstallWizard

-- StaticPopup dialog for reload after setup
_G.StaticPopupDialogs["REALUI_SETUP_RELOAD"] = {
    text = "|cff00ff00RealUI setup is complete!|r\n\nA UI reload is required to apply all settings properly.\n\n|cffff8000Please reload your UI now.|r",
    button1 = "Reload UI",
    button2 = "Later",
    OnAccept = function()
        _G.ReloadUI()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = false,
    preferredIndex = 3,
}

-- Installation stage constants
InstallWizard.STAGE_COMPLETE = -1
InstallWizard.STAGE_WELCOME = 0
InstallWizard.STAGE_LAYOUT = 1
InstallWizard.STAGE_DISPLAY = 2
InstallWizard.STAGE_CHAT = 3
InstallWizard.STAGE_QOL = 4
InstallWizard.STAGE_FINISH = 5
-- B04: appended numerically (saved `installStage` values must keep their
-- meaning), but ordered between LAYOUT and DISPLAY in the STAGES table below —
-- navigation iterates that table, never the numbers.
InstallWizard.STAGE_ACTIONBARS = 6

-- Ordered STAGES table is the single authority for flow order.
-- Navigation (Advance/Retreat) must iterate this table, not compare
-- numeric values directly, so future insertions only require adding
-- an entry here.
InstallWizard.STAGES = {
    InstallWizard.STAGE_WELCOME,
    InstallWizard.STAGE_LAYOUT,
    InstallWizard.STAGE_ACTIONBARS,
    InstallWizard.STAGE_DISPLAY,
    InstallWizard.STAGE_CHAT,
    InstallWizard.STAGE_QOL,
    InstallWizard.STAGE_FINISH,
}

-- Installation state
local installState = {
    currentStage = InstallWizard.STAGE_WELCOME,
    isFirstTime = false,
    needsResume = false,
    stageData = {}
}

local function ProfileExists(aceDB, profileName)
    if not aceDB or not profileName then
        return false
    end

    local profiles = aceDB:GetProfiles()
    if not profiles then
        return false
    end

    for _, existingName in ipairs(profiles) do
        if existingName == profileName then
            return true
        end
    end

    return false
end

local function ApplyStartupDisplayOptimization()
    if RealUI.IsUsingHighResDisplay and RealUI:IsUsingHighResDisplay() then
        local skinsOptions = RealUI.GetOptions and RealUI.GetOptions("Skins")
        local skinsDB = skinsOptions and skinsOptions.profile

        if skinsDB and not skinsDB.isHighRes and RealUI.UpdateUIScale then
            skinsDB.isHighRes = true
            RealUI.UpdateUIScale(skinsDB.customScale)
            debug("Applied startup high-res scaling for install wizard")
        end
    end
end

-- Check if this is first-time setup
function InstallWizard:IsFirstTime()
    if not RealUI.db or not RealUI.db.char then
        return true
    end

    local charInit = RealUI.db.char.init
    if not charInit then
        return true
    end

    return not charInit.initialized
end

-- Check if this is an upgrade from previous version
function InstallWizard:IsUpgrade()
    if not RealUI.db or not RealUI.db.char then
        return false
    end

    local charInit = RealUI.db.char.init
    if charInit and charInit.hadPreviousVersion then
        return true, charInit.previousVersion
    end

    return false, nil
end

-- Get current installation stage
function InstallWizard:GetCurrentStage()
    if not RealUI.db or not RealUI.db.char then
        return InstallWizard.STAGE_WELCOME
    end

    local stage = RealUI.db.char.init.installStage or InstallWizard.STAGE_WELCOME
    return stage
end

-- Set installation stage
function InstallWizard:SetStage(stage)
    if not RealUI.db or not RealUI.db.char then
        return
    end

    RealUI.db.char.init.installStage = stage
    installState.currentStage = stage

    debug("Stage set to:", stage)
end

-- Find the index of a stage in the STAGES table
local function StageIndex(stage)
    for i, s in ipairs(InstallWizard.STAGES) do
        if s == stage then
            return i
        end
    end
    return nil
end

-- Check if installation needs to be resumed
function InstallWizard:NeedsResume()
    local stage = self:GetCurrentStage()
    if stage == InstallWizard.STAGE_COMPLETE then
        return false
    end
    local idx = StageIndex(stage)
    -- Resume if the stage is past the first entry in the flow
    return idx ~= nil and idx > 1
end

-- Initialize installation system
function InstallWizard:Initialize()
    installState.isFirstTime = self:IsFirstTime()
    installState.currentStage = self:GetCurrentStage()
    installState.needsResume = self:NeedsResume()

    -- Check if this is an upgrade
    local isUpgrade, oldVersion = self:IsUpgrade()
    installState.isUpgrade = isUpgrade
    installState.oldVersion = oldVersion

    debug("Initialized - FirstTime:", installState.isFirstTime,
          "Stage:", installState.currentStage, "NeedsResume:", installState.needsResume,
          "IsUpgrade:", installState.isUpgrade, "OldVersion:", tostring(installState.oldVersion))

    return installState
end

-- Start installation wizard
function InstallWizard:Start(forceShow)
    -- Allow forcing the wizard to show (for manual /realui setup command)
    if forceShow then
        debug("Forcing installation wizard to show")
        self:SetStage(InstallWizard.STAGE_WELCOME)

        ApplyStartupDisplayOptimization()

        -- Show installation wizard UI
        if RealUI.InstallUI then
            RealUI.InstallUI:Show()
        end

        return true
    end

    if not installState.isFirstTime and not installState.needsResume then
        debug("Installation not needed")
        return false
    end

    if installState.needsResume then
        debug("Resuming installation from stage:", installState.currentStage)
    else
        debug("Starting new installation")
        self:SetStage(InstallWizard.STAGE_WELCOME)
    end

    ApplyStartupDisplayOptimization()

    -- Show installation wizard UI
    if RealUI.InstallUI then
        RealUI.InstallUI:Show()
    end

    return true
end

-- Propagate unified profiles across linked scopes after initial profile setup.
-- Creates Core profiles ("RealUI" / "RealUI-Healing") based on character specs,
-- assigns them via DualSpecSystem, and ensures matching profiles exist in the
-- action bars and Skins scopes when their scope links are enabled.
-- Does NOT overwrite existing custom profile assignments in db.char.specProfiles.
local function PropagateUnifiedProfiles()
    debug("PropagateUnifiedProfiles: starting")

    local specs = RealUI.charInfo and RealUI.charInfo.specs
    if not specs or #specs == 0 then
        debug("PropagateUnifiedProfiles: no spec info available, skipping")
        return
    end

    local DualSpec = RealUI.DualSpecSystem
    local PC = RealUI.ProfileCoordinator

    -- Determine if character has at least one healer spec
    local hasHealer = false
    local hasNonHealer = false
    for i = 1, #specs do
        if specs[i].role == "HEALER" then
            hasHealer = true
        else
            hasNonHealer = true
        end
    end

    local builtInProfiles  -- list of profile names to ensure
    if hasHealer and hasNonHealer then
        builtInProfiles = { "RealUI", "RealUI-Healing" }
    else
        builtInProfiles = { "RealUI" }
    end

    local scopeResults = {}  -- { scope = true/false }
    local scopeErrors = {}   -- { scope = errorMsg }

    ----------------------------------------------------------------
    -- 1. Core scope: create profiles and assign specs
    ----------------------------------------------------------------
    local coreOk, coreErr = pcall(function()
        if not RealUI.db then return end

        -- Ensure both built-in profiles exist in Core scope
        local currentProfile = RealUI.db:GetCurrentProfile()
        for _, profileName in ipairs(builtInProfiles) do
            -- SetProfile creates the profile if it doesn't exist
            RealUI.db:SetProfile(profileName)
        end
        -- Switch back to original profile
        RealUI.db:SetProfile(currentProfile)

        -- Assign specs via DualSpecSystem, respecting existing custom assignments
        if DualSpec then
            local dbc = RealUI.db.char
            for i = 1, #specs do
                -- Skip if user already has a custom assignment
                if dbc and dbc.specProfiles and dbc.specProfiles[i] then
                    debug("PropagateUnifiedProfiles: spec", i, "already has custom assignment:", dbc.specProfiles[i])
                else
                    local profileName
                    if hasHealer and hasNonHealer then
                        profileName = (specs[i].role == "HEALER") and "RealUI-Healing" or "RealUI"
                    else
                        profileName = "RealUI"
                    end
                    DualSpec:SetSpecProfile(i, profileName)
                    debug("PropagateUnifiedProfiles: assigned spec", i, "->", profileName)
                end
            end
        end
    end)
    scopeResults["core"] = coreOk
    if not coreOk then
        scopeErrors["core"] = coreErr
        debug("PropagateUnifiedProfiles: Core scope error:", coreErr)
    end

    ----------------------------------------------------------------
    -- 2. Action bars scope. RealUI_ActionBars seeds its built-in profiles
    -- through AceDB defaults, and ProfileCoordinator's SwitchActionBarsScope
    -- copy-on-creates any missing profile at first switch — nothing to
    -- pre-create here since Bartender4 support was removed (2026-08-22).
    -- The scopeResults key stays "bt4" (wire value; see ProfileCoordinator).
    ----------------------------------------------------------------
    if PC and PC:IsScopeLinked(PC.SCOPE_ACTIONBARS) then
        scopeResults["bt4"] = true
        debug("PropagateUnifiedProfiles: action bars scope seeds itself (AceDB defaults)")
    else
        debug("PropagateUnifiedProfiles: action bars scope not linked, skipping")
    end

    ----------------------------------------------------------------
    -- 3. Skins scope: create matching profiles
    ----------------------------------------------------------------
    if PC and PC:IsScopeLinked(PC.SCOPE_SKINS) then
        local skinsOk, skinsErr = pcall(function()
            local skinsModule = RealUI:GetModule("Skins", true)
            if not skinsModule or not skinsModule.db then
                debug("PropagateUnifiedProfiles: Skins module/db not available, skipping")
                return
            end
            local skinsDB = skinsModule.db
            local currentSkins = skinsDB:GetCurrentProfile()
            for _, profileName in ipairs(builtInProfiles) do
                local profileExists = ProfileExists(skinsDB, profileName)

                -- SetProfile creates the profile if it doesn't exist.
                -- When we create a built-in Skins profile during setup, seed it
                -- from the current profile so display scale choices detected for
                -- this character carry over instead of reverting to raw defaults.
                skinsDB:SetProfile(profileName)

                if not profileExists and currentSkins and currentSkins ~= profileName then
                    skinsDB:CopyProfile(currentSkins, true)
                    debug("PropagateUnifiedProfiles: seeded new Skins profile", profileName, "from", currentSkins)
                end
            end
            -- Switch back to original profile
            skinsDB:SetProfile(currentSkins)
            debug("PropagateUnifiedProfiles: Skins profiles ensured")
        end)
        scopeResults["skins"] = skinsOk
        if not skinsOk then
            scopeErrors["skins"] = skinsErr
            debug("PropagateUnifiedProfiles: Skins scope error:", skinsErr)
        end
    else
        debug("PropagateUnifiedProfiles: Skins scope not linked, skipping")
    end

    ----------------------------------------------------------------
    -- Summary
    ----------------------------------------------------------------
    local succeeded, failed = {}, {}
    for scope, ok in pairs(scopeResults) do
        if ok then
            succeeded[#succeeded + 1] = scope
        else
            failed[#failed + 1] = scope
        end
    end

    if #failed > 0 then
        debug("PropagateUnifiedProfiles: completed with errors. Succeeded:", table.concat(succeeded, ", "), "Failed:", table.concat(failed, ", "))
        for scope, err in pairs(scopeErrors) do
            debug("  ", scope, ":", tostring(err))
        end
    else
        debug("PropagateUnifiedProfiles: completed successfully. Scopes:", table.concat(succeeded, ", "))
    end
end

-- Complete installation
function InstallWizard:Complete()
    self:SetStage(InstallWizard.STAGE_COMPLETE)

    if RealUI.db and RealUI.db.char then
        RealUI.db.char.init.initialized = true
    end

    -- Disable old tutorial system
    local dbg = RealUI.db and RealUI.db.global
    if dbg and dbg.tutorial then
        dbg.tutorial.stage = -1
        debug("Disabled old tutorial system")
    end

    -- Capture first-time state before it gets cleared by the CVar block below
    local isFirstTimeAccount = dbg and dbg.tags and dbg.tags.firsttime

    -- Apply CVars
    if isFirstTimeAccount then
        debug("Applying first-time account CVars")
        self:ApplyAccountCVars()
        dbg.tags.firsttime = false
        dbg.tutorial = dbg.tutorial or {}
        dbg.tutorial.stage = -1
    end

    -- Character CVars apply for every new character, not just the first account
    debug("Applying character CVars")
    self:ApplyCharacterCVars()

    -- Chat frame setup
    for i = 1, 10 do
        local cf = _G["ChatFrame"..i]
        if cf then _G.FCF_SetLocked(cf, 1) end
    end

    -- Class-color player names in all chat channels
    for _, v in next, _G.CHAT_CONFIG_CHAT_LEFT do
        _G.ToggleChatColorNamesByClassGroup(true, v.type)
    end
    for iCh = 1, 15 do
        _G.ToggleChatColorNamesByClassGroup(true, "CHANNEL"..iCh)
    end

    -- Chat window transparency
    _G.SetChatWindowAlpha(1, 0)
    _G.SetChatWindowAlpha(2, 0)
    _G.DEFAULT_CHATFRAME_ALPHA = 0

    -- Update version info for minipatch system
    if dbg then
        dbg.verinfo = {}
        for k, v in next, RealUI.verinfo do
            dbg.verinfo[k] = v
        end
        debug("Updated verinfo")
    end

    -- Account-wide addon profile creation: only run for truly first-time accounts.
    -- Profile data already exists after the first character completes setup.
    if isFirstTimeAccount then
        if RealUI.AddRealUIProfiles then
            debug("Adding RealUI profiles to addons")
            RealUI:AddRealUIProfiles()
        end
    else
        debug("Skipping account-wide profile creation (firsttime already false)")
    end

    -- Character-specific profile assignment: always run so each new character
    -- (or resetchar'd character) gets linked to the correct addon profiles.
    if RealUI.SetProfilesToRealUI then
        debug("Setting addon profiles to RealUI for this character")
        RealUI:SetProfilesToRealUI()
    end

    -- Profile keys map this character to the correct profile in each addon's
    -- saved variables.  This is per-character data, so run for every setup.
    if RealUI.SetProfileKeys then
        debug("Setting profile keys for this character")
        RealUI:SetProfileKeys()
    end

    -- Propagate unified profiles across linked scopes (Core, Action Bars, Skins)
    -- Runs for all characters: creates built-in profiles, assigns specs,
    -- and ensures matching profiles in linked scopes.
    PropagateUnifiedProfiles()

    -- Perform character initialization
    -- For truly new characters, run full setup; for already-initialized characters,
    -- only run character-specific steps (layout, spec mapping, chat positioning)
    if RealUI.CharacterInit then
        if not RealUI.CharacterInit:IsInitialized() then
            -- B126: force the chat placement. Finishing the wizard is exactly
            -- the moment chat should land on the current default, and the
            -- `needchatmoved` gate reads false for every upgrading character.
            RealUI.CharacterInit:Setup(true)
        else
            -- Character already initialized — only apply role defaults and chat positioning
            debug("Character already initialized, running character-specific steps only")
            RealUI.CharacterInit:ApplyRoleDefaults()
            RealUI.CharacterInit:SetupChatFrames(true)
            RealUI.CharacterInit:RegisterCharacter()
        end
    end

    -- Complete setup in SetupSystem
    if RealUI.SetupSystem then
        RealUI.SetupSystem:CompleteSetup()
    end

    -- B126: land the defaults no other install-path code reliably applies, so
    -- the new-defaults nudge eight seconds after the next login has nothing to
    -- offer a character that just finished the wizard. Measured on a 3.4.0
    -- profile: the bag frame sat at CENTER 139,-68 after wizard + reload,
    -- despite Bags.lua:406 writing the correct anchor at OnInitialize.
    if RealUI.NewDefaults then
        local applied = RealUI.NewDefaults:ApplyInstallDefaults()
        debug("Applied install-owned defaults:", applied)
    end

    debug("Installation completed")

    -- Apply wizard choices: Naga bar
    local enableNaga = installState.stageData.enableNagaBar
    if enableNaga ~= nil then
        local ActionBarsModule = RealUI:GetModule("ActionBars", true)
        if ActionBarsModule and ActionBarsModule.db then
            ActionBarsModule.db.profile.enableNagaBar = enableNaga
            if ActionBarsModule:IsEnabled() then
                ActionBarsModule:UpdateNagaBarState()
            end
        end
    end

    -- Apply wizard choices: Repair mount
    local repairMountID = installState.stageData.repairMountID
    if repairMountID and repairMountID > 0 then
        if not RealUI.db.global.qol then
            RealUI.db.global.qol = {}
        end
        RealUI.db.global.qol.repairMountID = repairMountID
    end

    -- Apply the bar layout now that installStage is -1. RealUI_ActionBars
    -- hooks ApplyABSettings and rebuilds its own bars from it; no per-button
    -- forcing is needed (that was Bartender4 machinery, removed 2026-08-22).
    local ActionBars = RealUI:GetModule("ActionBars", true)
    if ActionBars and ActionBars:IsEnabled() then
        ActionBars:ApplyABSettings()
    end

    -- Apply EditMode layout for the current role and display preset
    if RealUI.EditModeManager and RealUI.EditModeManager:IsInitialized() then
        local role = (RealUI.cLayout == 2) and "healing" or "dpstank"
        local display = RealUI.db.global.display
        local presetId = display and display.presetId or "standard"
        RealUI.EditModeManager:ApplyLayout(role, presetId)
    end

    -- Seed the stored objective-tracker position into the freshly applied
    -- layout. This is the only sanctioned moment for that write: it is
    -- user-initiated and the wizard ends with a reload, which clears the
    -- EditMode taint any layout write introduces. RealUI_Tracker must never do
    -- this from an event handler — see the note in RealUI_Tracker/Container.lua.
    local Tracker = _G.LibStub and _G.LibStub("AceAddon-3.0", true)
    Tracker = Tracker and Tracker:GetAddon("RealUI_Tracker", true)
    if Tracker and Tracker.SeedPositionIntoEditMode then
        Tracker:SeedPositionIntoEditMode()
    end

    -- B126: correct the chat anchor in the layout we just applied. Must come
    -- AFTER ApplyLayout — EditMode owns system 8, so placing the chat frame
    -- before the layout is applied achieves nothing; the layout puts it back.
    -- That ordering is exactly what made SetupChatFrames look broken when it was
    -- running correctly all along. Targeted single-entry write, so the other 49
    -- entries (and any the user has moved) are untouched.
    if RealUI.EditModeManager and RealUI.EditModeManager.SetChatAnchor then
        RealUI.EditModeManager:BeginUserWrite()
        local wrote = RealUI.EditModeManager:SetChatAnchor()
        RealUI.EditModeManager:EndUserWrite()
        debug("Chat anchor corrected in EditMode layout:", wrote)
    end

    -- Hide installation wizard UI
    if RealUI.InstallUI then
        RealUI.InstallUI:Hide()
    end

    -- Show completion message
    print("|cff00ff00RealUI " .. RealUI.verinfo.string .. " setup completed successfully!|r")

    -- Show reload UI dialog
    _G.StaticPopup_Show("REALUI_SETUP_RELOAD")
end

-- Apply account-wide CVars (first-time setup only)
function InstallWizard:ApplyAccountCVars()
    local accountCVars = {
        -- Sound
        ["Sound_EnableErrorSpeech"] = 0,

        -- Screenshots
        ["screenshotQuality"] = "10",              -- Highest quality

        -- Help
        ["showTutorials"] = 0,                     -- Turn off Tutorials
        ["UberTooltips"] = 1,                      -- Turn on Enhanced Tooltips
        ["scriptErrors"] = 1,                      -- Turn on Display Lua Errors

        -- Controls
        ["deselectOnClick"] = 1,                   -- Turn off Sticky Targeting (inverted)

        -- Combat
        ["spellActivationOverlayOpacity"] = 0.75,  -- Spell Alert Opacity

        -- Social
        ["chatBubbles"] = 0,                       -- Turn off Chat Bubbles
        ["chatBubblesParty"] = 0,                  -- Turn off Party Chat Bubbles
        ["chatStyle"] = "classic",                 -- Chat Style = "Classic"
        ["whisperMode"] = "inline",                -- Whisper Mode = "In-line"

        -- ActionBars
        ["countdownForCooldowns"] = 0,             -- Disable Blizz cooldown count

        -- Quests
        ["autoQuestWatch"] = 1,                    -- Auto Track Quests

        -- Map
        -- Replaces RealUI's hand-rolled coordinate overlay, retired 2026-08-16.
        -- Blizzard's WorldMapCoordsPanel renders the same two values from
        -- inside secure code; the addon overlay had to dodge the pin-creation
        -- taint path by hand (aurora-121-surfaces task 4.2).
        ["worldMapShowPlayerCoords"] = 1,          -- Show player map coordinates
        ["worldMapShowCursorCoords"] = 1,          -- Show cursor map coordinates

        -- Names
        ["UnitNameNPC"] = 1,                       -- Turn on NPC Names
        ["UnitNamePlayerPVPTitle"] = 0,            -- Turn off PvP Player Titles
        ["UnitNameEnemyGuardianName"] = 1,         -- Turn on Enemy Pet Names
        ["UnitNameEnemyTotemName"] = 1,            -- Turn on Enemy Totem Names

        -- Camera
        ["cameraSmoothStyle"] = 0,                 -- Never adjust the camera

        -- Quality of Life
        ["guildShowOffline"] = 0,                  -- Hide Offline Guild Members
        ["profanityFilter"] = 0,                   -- Turn off Profanity Filter
    }

    -- Check if RealUI_CombatText is loaded
    if _G.C_AddOns and _G.C_AddOns.IsAddOnLoaded("RealUI_CombatText") and not RealUI.isMidnight then
        accountCVars["enableFloatingCombatText"] = 0   -- Turn off Combat Text
    end

    for cvar, value in next, accountCVars do
        _G.SetCVar(cvar, value)
    end

    debug("Account CVars applied")
end

-- Apply character-specific CVars (first-time setup only)
function InstallWizard:ApplyCharacterCVars()
    local characterCVars = {
        -- Nameplates
        ["nameplateMotion"] = 1,                       -- Stacking Nameplates
        ["nameplateShowAll"] = 1,                      -- Always show nameplates
        ["nameplateShowSelf"] = 0,                     -- Hide Personal Resource Display

        -- Combat
        ["displaySpellActivationOverlays"] = 1,        -- Turn on Spell Alerts

        -- Raid/Party
        ["useCompactPartyFrames"] = 1,                 -- Raid-style party frames

        -- Quality of Life
        ["autoLootDefault"] = 1,                       -- Turn on Auto Loot
    }

    for cvar, value in next, characterCVars do
        _G.SetCVar(cvar, value)
    end

    debug("Character CVars applied")
end


-- Advance to next stage
function InstallWizard:NextStage()
    local currentStage = self:GetCurrentStage()

    if currentStage == InstallWizard.STAGE_COMPLETE then
        return
    end

    local idx = StageIndex(currentStage)
    if not idx then
        return
    end

    if idx >= #InstallWizard.STAGES then
        self:Complete()
    else
        local nextStage = InstallWizard.STAGES[idx + 1]
        self:SetStage(nextStage)

        -- Update UI if available
        if RealUI.InstallUI then
            RealUI.InstallUI:UpdateStage(nextStage)
        end
    end
end

-- Go to previous stage
function InstallWizard:PreviousStage()
    local currentStage = self:GetCurrentStage()

    local idx = StageIndex(currentStage)
    if not idx or idx <= 1 then
        return
    end

    local prevStage = InstallWizard.STAGES[idx - 1]
    self:SetStage(prevStage)

    -- Update UI if available
    if RealUI.InstallUI then
        RealUI.InstallUI:UpdateStage(prevStage)
    end
end

-- Reset installation
function InstallWizard:Reset()
    self:SetStage(InstallWizard.STAGE_WELCOME)

    if RealUI.db and RealUI.db.char then
        RealUI.db.char.init.initialized = false
    end

    installState.stageData = {}

    debug("Installation reset")
end

-- Store data for current stage
function InstallWizard:SetStageData(key, value)
    local stage = self:GetCurrentStage()

    if not installState.stageData[stage] then
        installState.stageData[stage] = {}
    end

    installState.stageData[stage][key] = value
end

-- Get data for current stage
function InstallWizard:GetStageData(key)
    local stage = self:GetCurrentStage()

    if not installState.stageData[stage] then
        return nil
    end

    return installState.stageData[stage][key]
end

-- Get installation state
function InstallWizard:GetState()
    return installState
end
