bin/clean.ps1

# WinMole - Deep System Cleanup
# Cleans temp files, browser caches, dev caches, app caches, Recycle Bin

param(
    [switch]$DryRun,
    [switch]$Debug,
    [switch]$Whitelist
)

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

# ── Whitelist management ──────────────────────────────────────────────────────
if ($Whitelist) {
    if (-not (Test-Path $script:WINMOLE_CONFIG)) {
        [void][System.IO.Directory]::CreateDirectory($script:WINMOLE_CONFIG)
    }
    if (-not (Test-Path $script:WHITELIST_FILE)) {
        "# WinMole Clean Whitelist - one glob pattern per line" | Set-Content $script:WHITELIST_FILE
    }
    Write-SpectreHost " [deepskyblue1]Opening whitelist:[/] $(Esc $script:WHITELIST_FILE)"
    Start-Process notepad $script:WHITELIST_FILE
    return
}

$wl = Get-Whitelist

# ── Scan category builder ─────────────────────────────────────────────────────
function New-Category {
    param([string]$Name, [string[]]$Paths, [switch]$RequiresAdmin)
    $exists = $Paths | Where-Object { Test-Path $_ }
    [pscustomobject]@{
        Name          = $Name
        Paths         = @($exists)
        RequiresAdmin = $RequiresAdmin.IsPresent
        Size          = [long]0
        FileCount     = 0
    }
}

$isAdmin = Test-IsAdmin

# ── Define all cleanup categories ─────────────────────────────────────────────
$categories = @(
    [pscustomobject]@{
        Name = 'User temp files'
        Paths = @(
            $env:TEMP,
            "$env:LOCALAPPDATA\Temp",
            "$env:WINDIR\Temp"
        )
        RequiresAdmin = $false
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Browser cache (Chrome, Edge, Firefox, Brave)'
        Paths = @(
            "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache",
            "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Code Cache",
            "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\GPUCache",
            "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache",
            "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Code Cache",
            "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\GPUCache",
            "$env:APPDATA\Mozilla\Firefox\Profiles",
            "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache",
            "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Code Cache"
        )
        RequiresAdmin = $false
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Developer tools (npm, pip, NuGet, Maven, Gradle, .NET)'
        Paths = @(
            "$env:APPDATA\npm-cache",
            "$env:LOCALAPPDATA\npm-cache",
            "$env:USERPROFILE\.npm\_cacache",
            "$env:USERPROFILE\.pip\cache",
            "$env:LOCALAPPDATA\pip\Cache",
            "$env:USERPROFILE\.nuget\packages\.cache",
            "$env:LOCALAPPDATA\NuGet\Cache",
            "$env:LOCALAPPDATA\NuGet\v3-cache",
            "$env:USERPROFILE\.m2\repository\.cache",
            "$env:USERPROFILE\.gradle\caches",
            "$env:USERPROFILE\.dotnet\toolResolverCache",
            "$env:LOCALAPPDATA\Microsoft\dotnet\toolResolverCache",
            "$env:USERPROFILE\.cargo\registry\src",
            "$env:USERPROFILE\.cache\yarn",
            "$env:LOCALAPPDATA\Yarn\Berry\cache",
            "$env:USERPROFILE\.pnpm-store",
            "$env:LOCALAPPDATA\pnpm\store"
        )
        RequiresAdmin = $false
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Windows Update cache'
        Paths = @(
            "$env:WINDIR\SoftwareDistribution\Download",
            "$env:WINDIR\SoftwareDistribution\DataStore\Logs"
        )
        RequiresAdmin = $true
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Windows logs and temp files'
        Paths = @(
            "$env:LOCALAPPDATA\CrashDumps",
            "$env:LOCALAPPDATA\Microsoft\Windows\WER",
            "$env:PROGRAMDATA\Microsoft\Windows\WER",
            "$env:WINDIR\Logs",
            "$env:WINDIR\Prefetch"
        )
        RequiresAdmin = $true
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'App-specific cache (Spotify, Slack, VS Code, Teams)'
        Paths = @(
            "$env:LOCALAPPDATA\Spotify\Storage",
            "$env:APPDATA\Spotify\Storage",
            "$env:APPDATA\Slack\Cache",
            "$env:APPDATA\Slack\Code Cache",
            "$env:APPDATA\Code\Cache",
            "$env:APPDATA\Code\Code Cache",
            "$env:APPDATA\Code\CachedData",
            "$env:APPDATA\Code\User\workspaceStorage",
            "$env:LOCALAPPDATA\Microsoft\Teams\Cache",
            "$env:LOCALAPPDATA\Microsoft\Teams\Code Cache",
            "$env:APPDATA\Discord\Cache",
            "$env:APPDATA\Discord\Code Cache"
        )
        RequiresAdmin = $false
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Thumbnail and icon cache'
        Paths = @(
            "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"
        )
        RequiresAdmin = $false
        IsRecycleBin  = $false
        Size          = [long]0
        FileCount     = 0
    }
    [pscustomobject]@{
        Name = 'Recycle Bin'
        Paths = @()  # handled specially
        RequiresAdmin = $false
        IsRecycleBin  = $true
        Size          = [long]0
        FileCount     = 0
    }
)

# ── Scan: calculate sizes in parallel ─────────────────────────────────────────
Write-Header -Title 'WinMole Clean' -Sub $(if ($DryRun) { '(dry run)' } else { '' })
Write-Host " Scanning cache directories...`n"

$allPaths = $categories |
    Where-Object { -not (Get-OptionalPropertyValue -InputObject $_ -Name 'IsRecycleBin' -Default $false) } |
    ForEach-Object { $_.Paths } |
    Where-Object { (Test-Path $_) -and (-not (Test-Whitelisted $_ $wl)) }

$sizes = Get-DirectorySizes -Paths @($allPaths)

foreach ($cat in $categories) {
    if (Get-OptionalPropertyValue -InputObject $cat -Name 'IsRecycleBin' -Default $false) {
        # Measure Recycle Bin via Shell
        try {
            $shell = New-Object -ComObject Shell.Application
            $bin   = $shell.Namespace(0x0A)
            $sz    = [long]0
            foreach ($item in $bin.Items()) { try { $sz += $item.Size } catch {} }
            $cat.Size = $sz
        } catch { $cat.Size = 0 }
        continue
    }
    $cat.Size = ($cat.Paths | ForEach-Object { if ($sizes.ContainsKey($_)) { $sizes[$_] } else { 0 } } |
        Measure-Object -Sum).Sum
}

# ── Display scan results ───────────────────────────────────────────────────────
$totalSize = [long]0
foreach ($cat in $categories) {
    $skip = $cat.RequiresAdmin -and -not $isAdmin
    $tag  = if ($skip) { ' (needs admin)' } else { '' }
    Write-Success -Message " $($cat.Name)$tag" -Right (Format-Bytes -Bytes $cat.Size)
    $totalSize += $cat.Size
}

Write-Host ""
Write-Summary -Label 'Estimated space to free:' -Value (Format-Bytes -Bytes $totalSize)

if ($DryRun) {
    if ($Debug) {
        Write-SpectreHost " [grey][[debug]] Paths that would be cleaned:[/]"
        foreach ($cat in $categories) {
            foreach ($p in $cat.Paths) {
                if (Test-Path $p) {
                    Write-SpectreHost " [grey]$(Esc $p)[/]"
                }
            }
        }
        Write-Host ""
    }
    Write-Info "Dry run complete. No files were deleted."
    return
}

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

# ── Execute cleanup ───────────────────────────────────────────────────────────
Write-Host ""
$freed = [long]0

foreach ($cat in $categories) {
    if ($cat.RequiresAdmin -and -not $isAdmin) {
        Write-Warn "$($cat.Name) — skipped (requires admin)"
        continue
    }

    if (Get-OptionalPropertyValue -InputObject $cat -Name 'IsRecycleBin' -Default $false) {
        try {
            Clear-RecycleBin -Force -ErrorAction SilentlyContinue
            $freed += $cat.Size
            Write-Success -Message 'Recycle Bin' -Right (Format-Bytes -Bytes $cat.Size)
            Write-OpLog "Emptied Recycle Bin (~$(Format-Bytes $cat.Size))"
        } catch {
            Write-Warn "Recycle Bin — could not empty"
        }
        continue
    }

    $catFreed = [long]0
    foreach ($path in $cat.Paths) {
        if (Test-Whitelisted $path $wl) {
            if ($Debug) { Write-Info "Whitelisted: $path" }
            continue
        }
        $catFreed += Remove-ItemSafe -Path $path -Recurse
        if ($Debug) { Write-Info "Removed: $path" }
    }

    Write-OpLog "Cleaned '$($cat.Name)': $(Format-Bytes $catFreed)"
    Write-Success -Message $cat.Name -Right (Format-Bytes -Bytes $catFreed)
    $freed += $catFreed
}

# ── Thumbnail cache: delete thumbcache_*.db files ─────────────────────────────
$thumbDir = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"
if (Test-Path $thumbDir) {
    $thumbSize = [long]0
    foreach ($f in [System.IO.Directory]::EnumerateFiles($thumbDir, 'thumbcache_*.db')) {
        try {
            $info = [System.IO.FileInfo]::new($f)
            $thumbSize += $info.Length
            Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue
        } catch {}
    }
    if ($thumbSize -gt 0) {
        Write-Success -Message 'Thumbnail cache' -Right (Format-Bytes -Bytes $thumbSize)
        $freed += $thumbSize
    }
}

Write-Host ""

$freeNow = [long]0
try {
    $drive = Split-Path -Qualifier $env:SystemDrive
    $di    = [System.IO.DriveInfo]::new($drive)
    $freeNow = $di.AvailableFreeSpace
} catch {}

$summary = "Space freed: $(Format-Bytes $freed)"
if ($freeNow -gt 0) { $summary += " | Free space now: $(Format-Bytes $freeNow)" }
Write-Summary -Label $summary -Value ''