modules/WinGet-Merge.psm1

Set-StrictMode -Version 3
Import-Module "$PSScriptRoot\WinGet-Utils.psm1"

[string]$CheckpointFilePath = "$PSScriptRoot\winget.{HOSTNAME}.checkpoint"
[string]$PackageDatabase = "$PSScriptRoot\winget.packages.json"
[string]$DefaultSource = 'winget'

<#
.DESCRIPTION
    Merge host-installed package IDs into "winget.packages.json".
#>

function Merge-WinGetRestore
{
    param(
        # When set, the package ignore-file is not applied.
        [switch]$NoIgnore,

        # Skip prompts for confirmation to merge missing package IDs.
        # Each package will be prompted for tagging unless -NoTags is also set.
        [switch]$MergeAll,

        # Skip prompts for tagging new package IDs.
        [switch]$NoTags,

        # Skips performing a checkpoint.
        [switch]$NoCheckpoint,

        # When set, a CLI based UI will be presented to allow for more refined
        # selection of packages to merge into 'winget.packages.json'. This may
        # be paired with -MergeAll to skip the additional final prompt to merge
        # a package.
        [switch]$UseUI
    )

    if ($NoCheckpoint) {
        # Skip performing a checkpoint.
    } else {
        Checkpoint-WinGetSoftware
        Start-Sleep -Milliseconds 500
    }

    $checkpointFile = $CheckpointFilePath.Replace('{HOSTNAME}', $(hostname).ToLower())
    if (-not(Test-Path $checkpointFile)) {
        Write-Error "No checkpoint file found."
        return
    }

    $installedPackages = Get-Content $checkpointFile | ConvertFrom-Json
    $installedPackages = @($installedPackages.Sources | Where-Object { $_.SourceDetails.Name -eq $DefaultSource }).Packages

    if (-not(Test-Path $PackageDatabase)) {
        Write-Error "A 'winget.packages.json' is required to use this cmdlet. Please see Initialize-WinGetRestore."
        return
    }

    $packages = @(Get-Content $PackageDatabase | ConvertFrom-Json)
    if ($packages.Count -eq 0) {
        $newPackages = $installedPackages
    } else {
        $newPackages = $installedPackages | Where-Object { $packages.PackageIdentifier -notcontains $_.PackageIdentifier }
    }

    if ($NoIgnore) {
        # Skip ignore package filtering.
    } else {
        $ignorePackages = Get-WinGetSoftwareIgnores
        $newPackages = $newPackages | Where-Object { $ignorePackages -notcontains $_.PackageIdentifier }
    }

    if ($null -eq $newPackages) {
        Write-Output "Nothing to merge to: '$PackageDatabase'"
        return
    }

    $newPackages = $newPackages | Sort-Object -Unique -Property PackageIdentifier

    if ($UseUI) {
        $selections = [bool[]]@()

        $showPackageDetailsScriptBlock = {
            param($currentSelections, $selectedIndex)
            $commandArgs = @('show', $newPackages[$selectedIndex].PackageIdentifier)
            if (-not([string]::IsNullOrWhiteSpace($DefaultSource))) {
                $commandArgs += @('--source', $DefaultSource)
            }
            $consoleEncoding = [console]::OutputEncoding
            [console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
            $details = winget $commandArgs --no-vt
            [console]::OutputEncoding = $consoleEncoding
            $firstLine = $details | Select-String -Pattern 'Found\s+(.*\[.*\])'
            if ($firstLine.Matches.Count -eq 1) {
                $details = $details[$firstLine.LineNumber..($details.Length - 1)]
                Show-Paginated -TextData $details -Title $firstLine.Matches[0].Groups[1].Value
                Hide-TerminalCursor
            }
        }

        $TableUIArgs = @{
            Table = $newPackages
            Title = 'Select Software to Merge'
            EnterKeyDescription = "Press ENTER to show selection details. "
            EnterKeyScript = $showPackageDetailsScriptBlock
            DefaultMemberToShow = "PackageIdentifier"
            SelectedItemMembersToShow = @("PackageIdentifier")
            Selections = ([ref]$selections)
        }

        Enter-AltScreenBuffer
        Hide-TerminalCursor
        Show-TableUI @TableUIArgs
        Exit-AltScreenBuffer

        if ($null -eq $selections) {
            $newPackages = @();
        } else {
            $newPackages = $newPackages | Where-Object { $selections[$newPackages.indexOf($_)] }
        }
    }

    $jsonModified = $false
    $newPackages | ForEach-Object {
        $id = $_.PackageIdentifier
        $merge = $true
        if (-not($MergeAll)) {
            $question = "Merge package '$id' into 'winget.packages.json'?"
            $choices = @(
                [System.Management.Automation.Host.ChoiceDescription]::new("&Yes", "Do merge")
                [System.Management.Automation.Host.ChoiceDescription]::new("&No", "Do not merge")
            )

            $decision = $Host.UI.PromptForChoice($null, $question, $choices, 1)
            $merge = $decision -eq 0
        }

        if ($merge) {
            $newEntry = [PSCustomObject]@{
                PackageIdentifier = [string]$id
                Tags = @()
            }

            if ($NoTags) {
                # Skip prompting user for tagging each new package.
            } else {
                Write-Output "Enter tags for package '$id'. An empty entry concludes data entry."
                while ($true) {
                    $tagEntry = Read-Host "Enter a tag name"

                    if ([string]::IsNullOrWhiteSpace($tagEntry)) {
                        break
                    }

                    $newEntry.Tags += $tagEntry
                }
            }

            $packages += $newEntry
            $jsonModified = $true
            Write-Output "Added package '$id'"
        } else {
            Write-Output "Skipped package: '$id'"
        }
    }

    if ($jsonModified) {
        $packages | Sort-Object -Property PackageIdentifier | ConvertTo-Json -Depth 3 | Out-File $PackageDatabase
        if ($?) {
            Write-Output "Saved configuration: '$PackageDatabase'"
        }
    } else {
        Write-Output "Nothing to merge to: '$PackageDatabase'"
    }
}