src/UI/05-Init.ps1

# 05-Init.ps1 — lazy UI bootstrap: dependency, console, UI state.

# Ensure PwshSpectreConsole (ships the Spectre.Console assembly) is loaded,
# offering to install it on first use. NEVER runs at profile import.
function script:Initialize-PSMMUI {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'First-use install prompt is interactive host output by design (mirrors the original block).')]
    [CmdletBinding()] param()
    if (-not ('Spectre.Console.AnsiConsole' -as [type])) {
        if (Get-Module -ListAvailable -Name PwshSpectreConsole) {
            Import-Module PwshSpectreConsole -ErrorAction Stop -Global
        } else {
            Write-Host 'PwshSpectreConsole is required for the interactive manager.' -ForegroundColor Yellow
            if ((Read-Host 'Install it now from the PowerShell Gallery? (y/N)') -notmatch '^(y|yes)$') { return $false }
            try {
                Install-PSMMModule -Name PwshSpectreConsole
                Import-Module PwshSpectreConsole -ErrorAction Stop -Global
            } catch {
                Write-Warning "Could not install PwshSpectreConsole: $($_.Exception.Message)"
                return $false
            }
        }
    }
    $null = Get-PSMMConsole
    $true
}

# First manager run with no config file anywhere: create the main config,
# seeded with psmm's own UI dependency as a managed entry (2026-07-05
# feedback). Mode InstallOnly keeps profile startup import-free - psmm loads
# Spectre lazily itself. Only the TUI does this, never Invoke-PSMMStartup:
# profile import must not write files.
function script:Initialize-PSMMMainConfig {
    [CmdletBinding()] param()
    $null = Get-PSMMEntry   # (re)build file metadata before inspecting it
    if (@((Get-PSMMFileMeta).Values | Where-Object { $_.Kind -ne 'inline' }).Count) { return }
    $main = Get-PSMMMainConfigPath
    if (Test-Path -LiteralPath $main) { return }
    try {
        $dir = Split-Path -Parent $main
        if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
        ([ordered]@{
            Includes = @()
            Modules  = @([ordered]@{
                Name        = 'PwshSpectreConsole'
                Description = 'TUI engine used by psmm itself'
                Install     = 'IfMissing'
                Mode        = 'InstallOnly'
            })
        } | ConvertTo-Json -Depth 10) | Set-Content -LiteralPath $main -Encoding utf8
        $script:PSMM_UI.Status = "[green3]created $(ConvertTo-PSMMSafe $main) - psmm's UI dependency is managed there[/]"
    } catch { }   # best-effort: the add flows still offer creation
}

# Fresh UI state for a manager session.
function script:Initialize-PSMMUIState {
    [CmdletBinding()] param()
    $script:PSMM_UI = @{
        Entries       = [System.Collections.Generic.List[object]]::new()
        Cursor        = 0
        Top           = 0
        Sel           = [System.Collections.Generic.HashSet[int]]::new()
        Filter        = ''
        FilterMode    = $false
        View          = @()
        Status        = ''
        Dirty         = $false
        HardQuit      = $false
        Unmanaged     = $null      # results of the background scan (#26)
        ShowUnmanaged = $false     # 'm' toggles unmanaged rows in the grid
        Elevated      = Test-PSMMElevated
        Engine        = Get-PSMMInstallEngine
    }
    Initialize-PSMMMainConfig
    Sync-PSMMUIEntries -FullScan
}

# (Re)build the entry list from config + disk. -FullScan does the one
# expensive Get-Module -ListAvailable sweep (open + explicit reload only).
function script:Sync-PSMMUIEntries {
    [CmdletBinding()] param([switch]$FullScan)
    $active = Get-PSMMEntry
    $list = [System.Collections.Generic.List[object]]::new()
    foreach ($e in $active) { $list.Add($e) }
    # unmanaged rows appended when the toggle is on and the scan has results
    if ($script:PSMM_UI.ShowUnmanaged -and $script:PSMM_UI.Unmanaged) {
        $known = [System.Collections.Generic.HashSet[string]]::new(
            [string[]]@($active.Name), [System.StringComparer]::OrdinalIgnoreCase)
        foreach ($u in $script:PSMM_UI.Unmanaged) {
            if ($known.Contains($u.Name)) { continue }
            $x = Resolve-PSMMEntry -Raw ([pscustomobject]@{ Name = $u.Name; Description = $u.Description }) -Source '<unmanaged>' -Writable $false
            $x | Add-Member -NotePropertyName FromMain -NotePropertyValue $false -Force
            $x | Add-Member -NotePropertyName FileEnabled -NotePropertyValue $true -Force
            $x | Add-Member -NotePropertyName Unmanaged -NotePropertyValue $true -Force
            $x.Installed = $true
            $x.InstalledVersion = $u.Version
            $x.InstallScope = $u.Scope
            $x.Mode = '-'
            $x.Install = '-'
            $list.Add($x)
        }
    }
    $script:PSMM_UI.Entries = $list
    $all = Get-PSMMAllEntries
    if ($FullScan) { Update-PSMMAvailable -Entries ($all + @($list | Where-Object { $_.PSObject.Properties['Unmanaged'] })) }
    Update-PSMMLoaded -Entries $list
    Update-PSMMLoaded -Entries $all
    $script:PSMM_UI.Sel.Clear()
    $script:PSMM_UI.Dirty = $false
}

# Kick off the background unmanaged-module scan (#26). Results surface as an
# unobtrusive overlay line; 'm' shows them in the grid.
function script:Start-PSMMUnmanagedScan {
    [CmdletBinding()] param()
    $managed = @((Get-PSMMAllEntries).Name | Where-Object { $_ })
    $null = Start-PSMMTask -Label 'scan: unmanaged modules' -Kind 'unmanagedscan' -ArgumentList (, $managed) -ScriptBlock {
        param($managedNames)
        $managedSet = [System.Collections.Generic.HashSet[string]]::new(
            [string[]]$managedNames, [System.StringComparer]::OrdinalIgnoreCase)
        Get-Module -ListAvailable -ErrorAction SilentlyContinue |
            Group-Object Name |
            Where-Object { -not $managedSet.Contains($_.Name) } |
            ForEach-Object {
                $newest = @($_.Group | Sort-Object Version -Descending)[0]
                [pscustomobject]@{
                    Name        = $_.Name
                    Version     = $newest.Version
                    Scope       = if ($newest.ModuleBase.StartsWith($HOME, [System.StringComparison]::OrdinalIgnoreCase)) { 'CurrentUser' } else { 'AllUsers' }
                    Description = $newest.Description
                }
            }
    }
}