src/UI/50-Files.ps1

# 50-Files.ps1 — config-file manager: list files, toggle Enabled, apply to
# session, create (from scenario templates, #29), move, Includes wiring.

function script:Build-PSMMFilesView {
    param(
        [Parameter(Mandatory)] $State,
        [Parameter(Mandatory)] $Metas,
        [string]$StatusMarkup
    )
    $n = $Metas.Count
    $win = Get-PSMMWinSize
    $vp = Get-PSMMViewport -State $State -Count $n -Rows ($win.Height - 12)
    $T = [Spectre.Console.Table]::new()
    $T.Border = [Spectre.Console.TableBorder]::Rounded
    foreach ($h in ' ', 'File', 'Kind', 'On', 'RW', 'Mods', '!') { [void][Spectre.Console.TableExtensions]::AddColumn($T, $h) }
    for ($i = $vp.First; $i -le $vp.Last; $i++) {
        $m = $Metas[$i]
        $leaf = if ($m.Kind -eq 'inline') { 'profile inline' } else { Split-Path $m.Path -Leaf }
        $nm = ConvertTo-PSMMSafe $leaf
        if ($i -eq $State.Cursor) { $nm = "[$script:PSMM_ColAccent]$nm[/]" }
        $on = if ($m.Enabled) { '[green3]on[/]' } else { '[indianred1]off[/]' }
        $rw = if ($m.Writable) { 'rw' } else { 'ro' }
        $fl = if ($m.IncludesIgnored) { '[orange1]![/]' } else { ' ' }
        [void][Spectre.Console.TableExtensions]::AddRow($T, [string[]]@(
                $(if ($i -eq $State.Cursor) { "[$script:PSMM_ColAccent]>[/]" } else { ' ' }), $nm, $m.Kind, $on, $rw, "$($m.ModuleCount)", $fl))
    }
    $pos = Get-PSMMPositionMarkup -State $State -Count $n -Viewport $vp
    $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new()
    $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]Config files[/]$pos [$script:PSMM_ColMute]- full path of current row below[/]"))
    $items.Add($T)
    if ($n) {
        $cur = $Metas[$State.Cursor]
        $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColMute]$(ConvertTo-PSMMSafe $cur.Path)[/]"))
        if ($cur.IncludesIgnored) { $items.Add([Spectre.Console.Markup]::new('[orange1]! this file has an Includes section that is being ignored (main config only)[/]')) }
    }
    $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('up/dn=move', 'space=enable/disable+save', 'a=apply to session', '?=help', 'esc=back', 'Ctrl+Q=quit'))))
    $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('n=new config (templates)', 'm=move file', 'c=conflicts'))))
    foreach ($w in @(Get-PSMMWarning | Select-Object -First 4)) { $items.Add([Spectre.Console.Markup]::new("[orange1]$(ConvertTo-PSMMSafe $w)[/]")) }
    if ($StatusMarkup) { $items.Add([Spectre.Console.Markup]::new($StatusMarkup)) }
    [Spectre.Console.Rows]::new($items)
}

# Config file manager loop.
function script:Show-PSMMFiles {
    $ui = $script:PSMM_UI
    $st = New-PSMMListState
    $st.Status = ''
    while ($true) {
        if ($ui.HardQuit) { return }
        $cmd = @{ Name = $null }
        Clear-PSMMScreen
        Invoke-PSMMLive -Body {
            param($ctx)
            while ($true) {
                if ($script:PSMM_UI.HardQuit) { return }
                $metas = @((Get-PSMMFileMeta).Values)
                $ctx.UpdateTarget((Build-PSMMFilesView -State $st -Metas $metas -StatusMarkup $st.Status))
                $ctx.Refresh()
                $k = Read-PSMMKeyResize
                if ($null -eq $k) { continue }
                if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return }
                $st.Status = ''
                if (Invoke-PSMMListNav -State $st -KeyInfo $k -Count $metas.Count) { continue }
                switch ($k.Key) {
                    ([ConsoleKey]::Spacebar) {
                        $m = $metas[$st.Cursor]
                        if ($m.Kind -eq 'inline' -or -not $m.Writable) { $st.Status = '[indianred1]read-only - cannot toggle[/]' }
                        else {
                            $m.Enabled = -not $m.Enabled
                            Save-PSMMFile -Path $m.Path -Entries (Get-PSMMAllEntries)
                            $script:PSMM_UI.Dirty = $true
                            $st.Status = "[green3]saved ($(if ($m.Enabled) { 'enabled' } else { 'disabled' })) - 'a' applies load/unload changes[/]"
                        }
                        continue
                    }
                    ([ConsoleKey]::A) { $cmd.Name = 'apply'; return }
                    ([ConsoleKey]::N) { $cmd.Name = 'new'; return }
                    ([ConsoleKey]::M) { $cmd.Name = 'move'; return }
                    ([ConsoleKey]::C) { $cmd.Name = 'conflicts'; return }
                    ([ConsoleKey]::Escape) { return }
                    default { if ($k.KeyChar -eq '?') { $cmd.Name = 'help'; return } }
                }
            }
        }
        if ($ui.HardQuit) { return }

        # full-screen follow-ups (prompts cannot run inside the live display)
        if ($ui.Dirty) { Sync-PSMMUIEntries -FullScan }
        switch ($cmd.Name) {
            'apply'     { Invoke-PSMMApply }
            'new'       { New-PSMMConfigFile }
            'move'      { $metas = @((Get-PSMMFileMeta).Values); if ($metas.Count) { Move-PSMMConfigFile -Meta $metas[$st.Cursor] } }
            'conflicts' { Show-PSMMConflicts }
            'help'      { Show-PSMMHelpScreen -Topic 'files' }
            default     { return }
        }
        if ($ui.Dirty) { Sync-PSMMUIEntries -FullScan }
    }
}

# Apply config changes to the live session: import newly-active Load-mode
# modules; unload loaded modules that are managed but no longer active.
function script:Invoke-PSMMApply {
    $ui = $script:PSMM_UI
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]Apply config changes[/]"
    $activeEntries = @(Get-PSMMEntry)
    $active = @{}; foreach ($e in $activeEntries) { if ($e.Name) { $active[$e.Name] = $e } }
    $managed = @{}; foreach ($e in (Get-PSMMAllEntries)) { if ($e.Name) { $managed[$e.Name] = $true } }
    $did = 0
    foreach ($e in $activeEntries) {
        if ($e.Mode -eq 'Load' -and -not (Get-Module -Name $e.Name)) {
            Write-PSMMLine "[$script:PSMM_ColAccent]loading $(ConvertTo-PSMMSafe $e.Name)...[/]"
            try { Import-PSMMModuleTimed -Entry $e; $did++ }
            catch { Write-PSMMLine "[indianred1] $(ConvertTo-PSMMSafe $_.Exception.Message)[/]" }
        }
    }
    foreach ($m in @(Get-Module)) {
        if ($managed.ContainsKey($m.Name) -and -not $active.ContainsKey($m.Name)) {
            Write-PSMMLine "[$script:PSMM_ColAccent]unloading $(ConvertTo-PSMMSafe $m.Name) (no longer active)...[/]"
            try { Remove-Module -Name $m.Name -Force -ErrorAction Stop; $did++ }
            catch { Write-PSMMLine "[indianred1] $(ConvertTo-PSMMSafe $_.Exception.Message)[/]" }
        }
    }
    $ui.Dirty = $true
    Write-PSMMLine "[green3]$did change(s) applied.[/]"
    $null = Wait-PSMMKey
}

# Scenario templates shipped with the module (#29). Key = menu label.
function script:Get-PSMMConfigTemplate {
    [CmdletBinding()] param()
    $dir = Join-Path $script:PSMMRoot 'Configs'
    $templates = [ordered]@{ 'blank (example entry only)' = $null }
    foreach ($f in @(Get-ChildItem -LiteralPath $dir -Filter 'scenario-*.json' -ErrorAction SilentlyContinue | Sort-Object Name)) {
        $label = ($f.BaseName -replace '^scenario-', '') -replace '-', ' '
        $templates[$label] = $f.FullName
    }
    $templates
}

function script:New-PSMMConfigFile {
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]Create config file[/]"
    $main = Get-PSMMMainConfigPath
    $prof = Get-PSMMProfileConfigPath
    $opts = @()
    if (-not (Test-Path -LiteralPath $main)) { $opts += "main: $main" }
    if ($prof -and -not (Test-Path -LiteralPath $prof)) { $opts += "profile dir: $prof" }
    $opts += 'custom path...'
    $pick = Read-SpectreSelection -Message 'Where?' -Choices $opts -Color $script:PSMM_ColAccent
    $path = switch -Wildcard ($pick) {
        'main:*'        { $main }
        'profile dir:*' { $prof }
        default         { Read-SpectreText -Message 'Full path for the new .json' }
    }
    if ([string]::IsNullOrWhiteSpace($path)) { return }
    if (Test-Path -LiteralPath $path) { Write-PSMMLine '[indianred1]File already exists - not overwriting.[/]'; $null = Wait-PSMMKey; return }

    # scenario templates (#29)
    $templates = Get-PSMMConfigTemplate
    $tpl = Read-SpectreSelection -Message 'Start from' -Choices @($templates.Keys) -Color $script:PSMM_ColAccent
    $tplPath = $templates[$tpl]

    $dir = Split-Path -Parent $path
    if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
    $isMain = ($path -eq $main)
    if ($tplPath) {
        Copy-Item -LiteralPath $tplPath -Destination $path
        if ($isMain) {
            # main config carries an Includes array; add it if the template lacks one
            try {
                $obj = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
                if (-not $obj.PSObject.Properties['Includes']) {
                    $obj | Add-Member -NotePropertyName Includes -NotePropertyValue @()
                    ($obj | ConvertTo-Json -Depth 10) | Set-Content -LiteralPath $path -Encoding utf8
                }
            } catch { }
        }
        Write-PSMMLine "[green3]Created $(ConvertTo-PSMMSafe $path) from the '$tpl' template.[/]"
        Write-PSMMLine "[grey66]All template modules are set to Mode=Ignore - flip the ones you want to Load/InstallOnly.[/]"
    } else {
        Get-PSMMExampleConfigJson -IsMain $isMain | Set-Content -LiteralPath $path -Encoding utf8
        Write-PSMMLine "[green3]Created $(ConvertTo-PSMMSafe $path) (example content, sample module set to Ignore).[/]"
    }
    if (-not $isMain -and $path -ne $prof) {
        Write-PSMMLine "[orange1]Note: this path is not auto-discovered. Add it to the main config's Includes (or `$PSMM_JsonPath).[/]"
        $mainMeta = (Get-PSMMFileMeta)[$main]
        if ($mainMeta -and $mainMeta.Writable) {
            if (Read-SpectreConfirm -Message 'Add it to the main config Includes now?' -DefaultAnswer 'y') {
                $mainMeta.Includes = @($mainMeta.Includes) + $path
                Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries)
                Write-PSMMLine '[green3]Added to Includes.[/]'
            }
        }
    }
    $script:PSMM_UI.Dirty = $true
    $null = Wait-PSMMKey
}

function script:Move-PSMMConfigFile {
    param([Parameter(Mandatory)] $Meta)
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]Move $(ConvertTo-PSMMSafe (Split-Path $Meta.Path -Leaf))[/]"
    if ($Meta.Kind -eq 'inline') { Write-PSMMLine '[indianred1]The inline profile block cannot be moved.[/]'; $null = Wait-PSMMKey; return }
    $dir = Read-SpectreText -Message 'Target folder'
    if ([string]::IsNullOrWhiteSpace($dir)) { return }
    if (-not (Test-Path -LiteralPath $dir -PathType Container)) { Write-PSMMLine '[indianred1]Folder not found.[/]'; $null = Wait-PSMMKey; return }
    $new = Join-Path $dir (Split-Path $Meta.Path -Leaf)
    if (Test-Path -LiteralPath $new) { Write-PSMMLine '[indianred1]A file with that name already exists there.[/]'; $null = Wait-PSMMKey; return }
    try { Move-Item -LiteralPath $Meta.Path -Destination $new -ErrorAction Stop }
    catch { Write-PSMMLine "[indianred1]$(ConvertTo-PSMMSafe $_.Exception.Message)[/]"; $null = Wait-PSMMKey; return }
    Write-PSMMLine "[green3]Moved to $(ConvertTo-PSMMSafe $new).[/]"
    # discovery follow-up: keep the file reachable
    $main = Get-PSMMMainConfigPath
    $mainMeta = (Get-PSMMFileMeta)[$main]
    $wasIncluded = $mainMeta -and (@($mainMeta.Includes) -contains $Meta.Path)
    if ($wasIncluded) {
        $mainMeta.Includes = @(@($mainMeta.Includes | Where-Object { $_ -ne $Meta.Path }) + $new)
        Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries)
        Write-PSMMLine '[green3]Main config Includes updated to the new path.[/]'
    } elseif ($Meta.Kind -in 'legacy', 'profile') {
        Write-PSMMLine '[orange1]The new location may not be auto-discovered.[/]'
        if ($mainMeta -and $mainMeta.Writable) {
            if (Read-SpectreConfirm -Message 'Add the new path to the main config Includes?' -DefaultAnswer 'y') {
                $mainMeta.Includes = @($mainMeta.Includes) + $new
                Save-PSMMFile -Path $main -Entries (Get-PSMMAllEntries)
                Write-PSMMLine '[green3]Added to Includes.[/]'
            }
        }
    }
    $script:PSMM_UI.Dirty = $true
    $null = Wait-PSMMKey
}