bin/uninstall.ps1

# WinMole - Smart App Uninstaller
# Lists installed apps with sizes, lets you select which to remove,
# then runs their uninstallers and cleans up leftover AppData/registry entries.

param(
    [switch]$DryRun
)

$ErrorActionPreference = 'SilentlyContinue'
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\ui.ps1"

function Get-AppPropertyValue {
    param(
        [object]$App,
        [string]$Name,
        $Default = $null
    )
    return Get-OptionalPropertyValue -InputObject $App -Name $Name -Default $Default
}

Write-Header -Title 'WinMole Uninstall' -Sub $(if ($DryRun) { '(dry run)' } else { '' })
Write-SpectreHost " [grey]Scanning installed applications... (this may take a moment)[/]"

# ── Collect installed apps from registry ──────────────────────────────────────
$regPaths = @(
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
    'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)

$apps = foreach ($rp in $regPaths) {
    Get-ItemProperty $rp -ErrorAction SilentlyContinue |
    Where-Object {
        (Get-AppPropertyValue -App $_ -Name 'DisplayName') -and
        (Get-AppPropertyValue -App $_ -Name 'UninstallString') -and
        -not (Get-AppPropertyValue -App $_ -Name 'SystemComponent' -Default $false) -and
        -not (Get-AppPropertyValue -App $_ -Name 'ParentKeyName')
    }
}

# Deduplicate by DisplayName, prefer entries with more info
$seen = @{}
$deduped = foreach ($app in $apps) {
    $name = ([string](Get-AppPropertyValue -App $app -Name 'DisplayName' -Default '')).Trim()
    if (-not $name) { continue }
    $size = [long](Get-AppPropertyValue -App $app -Name 'EstimatedSize' -Default 0)
    if (-not $seen.ContainsKey($name)) {
        $seen[$name] = $app
    } elseif ($size -gt [long](Get-AppPropertyValue -App $seen[$name] -Name 'EstimatedSize' -Default 0)) {
        $seen[$name] = $app
    }
}
$deduped = $seen.Values

# ── Determine last-used heuristic ─────────────────────────────────────────────
$cutoff = (Get-Date).AddDays(-30)

$menuItems = foreach ($app in $deduped | Sort-Object DisplayName) {
    $displayName = ([string](Get-AppPropertyValue -App $app -Name 'DisplayName' -Default '')).Trim()
    if (-not $displayName) { continue }
    $size = [long](Get-AppPropertyValue -App $app -Name 'EstimatedSize' -Default 0) * 1KB   # registry stores in KB

    # Heuristic: check install date
    $tag = ''
    $installDateValue = [string](Get-AppPropertyValue -App $app -Name 'InstallDate' -Default '')
    if ($installDateValue -and $installDateValue -match '^\d{8}$') {
        try {
            $installDate = [datetime]::ParseExact($installDateValue, 'yyyyMMdd', $null)
            if ($installDate -lt $cutoff.AddDays(-150)) { $tag = 'Old' }
            elseif ($installDate -ge $cutoff) { $tag = 'Recent' }
        } catch {}
    }

    [pscustomobject]@{
        Label        = $displayName
        Size         = $size
        Tag          = $tag
        Selected     = ($tag -eq 'Old')
        Disabled     = $false
        UninstallStr = [string](Get-AppPropertyValue -App $app -Name 'UninstallString' -Default '')
        QuietStr     = [string](Get-AppPropertyValue -App $app -Name 'QuietUninstallString' -Default '')
        InstallLoc   = [string](Get-AppPropertyValue -App $app -Name 'InstallLocation' -Default '')
        Publisher    = [string](Get-AppPropertyValue -App $app -Name 'Publisher' -Default '')
    }
}

$menuItems = @($menuItems)

if ($menuItems.Count -eq 0) {
    Write-Warn "No removable applications found."
    return
}

Write-Host ""

# ── Interactive selection ─────────────────────────────────────────────────────
$chosen = Show-ChecklistMenu -Title 'Select Apps to Remove' -Items $menuItems
if (-not $chosen) {
    Write-SpectreHost " [grey]Cancelled.[/]"
    Write-Host ""
    return
}

$toRemove = @($chosen | Where-Object { $_.Selected })
if ($toRemove.Count -eq 0) {
    Write-Info "No apps selected."
    return
}

Write-Host ""
Write-SpectreHost " [bold]Apps to remove ($($toRemove.Count)):[/]"
foreach ($app in $toRemove) {
    Write-SpectreHost " [gold1]•[/] $(Esc $app.Label) [grey]$(Format-Bytes $app.Size)[/]"
}
Write-Host ""

if ($DryRun) {
    Write-Info "Dry run — no changes made."
    return
}

if (-not (Confirm-Action -Message 'Proceed with uninstall?')) {
    Write-SpectreHost " [grey]Cancelled.[/]"
    Write-Host ""
    return
}

# ── Leftover path patterns ────────────────────────────────────────────────────
function Get-AppLeftoverPaths {
    param([string]$AppName, [string]$Publisher, [string]$InstallLocation)
    $slug = $AppName -replace '[^a-zA-Z0-9 ]','' -replace '\s+', ' '
    $words = ($slug -split ' ' | Where-Object { $_.Length -gt 3 })
    $short = if ($words) { $words[0] } else { ($slug -split ' ')[0] }

    $paths = @(
        "$env:APPDATA\$AppName",
        "$env:APPDATA\$short",
        "$env:LOCALAPPDATA\$AppName",
        "$env:LOCALAPPDATA\$short",
        "$env:PROGRAMDATA\$AppName",
        "$env:PROGRAMDATA\$short"
    )
    if ($Publisher -and $Publisher.Trim()) {
        $pub = $Publisher.Trim()
        $paths += @(
            "$env:APPDATA\$pub",
            "$env:LOCALAPPDATA\$pub",
            "$env:PROGRAMDATA\$pub"
        )
    }
    if ($InstallLocation -and (Test-Path $InstallLocation)) {
        $paths += $InstallLocation
    }
    return @($paths | Select-Object -Unique | Where-Object { Test-Path $_ })
}

# ── Uninstall loop ────────────────────────────────────────────────────────────
$totalFreed = [long]0

foreach ($app in $toRemove) {
    Write-Host ""
    Write-SpectreHost " [bold]Uninstalling:[/] [deepskyblue1]$(Esc $app.Label)[/]"

    # Run uninstaller
    $uninstalled = $false
    $uninstStr = if ($app.QuietStr) { $app.QuietStr } else { $app.UninstallStr }

    if ($uninstStr) {
        try {
            $invocation = Get-UninstallInvocation -UninstallString $app.UninstallStr -QuietUninstallString $app.QuietStr
            if (-not $invocation) {
                throw "Invalid uninstall command for $($app.Label)"
            }

            $proc = Start-Process -FilePath $invocation.FilePath -ArgumentList $invocation.ArgumentList -Wait -PassThru `
                -ErrorAction SilentlyContinue
            $uninstalled = ($null -ne $proc -and $proc.ExitCode -eq 0)
            if ($uninstalled) {
                Write-Success -Message 'Removed application'
            } else {
                $exitCode = if ($null -ne $proc) { $proc.ExitCode } else { 'unknown' }
                Write-Warn "Uninstaller exited with code $exitCode"
            }
        } catch {
            Write-Warn "Could not run uninstaller: $_"
        }
    }

    # Clean up leftover paths
    $leftovers = Get-AppLeftoverPaths -AppName $app.Label `
        -Publisher $app.Publisher -InstallLocation $app.InstallLoc

    $leftoverFreed = [long]0
    $leftoverCount = 0

    foreach ($path in $leftovers) {
        $sz = Remove-ItemSafe -Path $path -Recurse
        if ($sz -gt 0 -or (Test-Path $path) -eq $false) {
            $leftoverFreed += $sz
            $leftoverCount++
            Write-OpLog "Removed leftover: $path ($(Format-Bytes $sz))"
        }
    }

    if ($leftoverCount -gt 0) {
        Write-Success -Message "Cleaned $leftoverCount leftover location(s)" `
            -Right (Format-Bytes $leftoverFreed)
    }

    $appFreed = $(if ($uninstalled) { $app.Size } else { 0 }) + $leftoverFreed
    $totalFreed += $appFreed
    Write-OpLog "Uninstalled '$($app.Label)': freed ~$(Format-Bytes $appFreed)"
}

Write-Host ""
Write-Summary -Label 'Space freed:' -Value (Format-Bytes -Bytes $totalFreed)