bin/analyze.ps1

# WinMole - Interactive Disk Space Analyzer
# Arrow-key navigation, size visualization, open/delete actions

param(
    [string]$Path = '',
    [switch]$Json
)

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

# ── Resolve start path ────────────────────────────────────────────────────────
if (-not $Path) {
    $Path = $env:USERPROFILE
}
$Path = $Path.TrimEnd('\','/')
if (-not (Test-Path $Path)) {
    Write-Err "Path not found: $Path"
    exit 1
}

# ── Scan top-level entries in a directory ─────────────────────────────────────
function Get-DirEntries {
    param([string]$DirPath)
    $entries = [System.Collections.Generic.List[object]]::new()
    try {
        $di = [System.IO.DirectoryInfo]::new($DirPath)
        foreach ($child in $di.EnumerateFileSystemInfos()) {
            $size = [long]0
            $isDir = $child -is [System.IO.DirectoryInfo]
            if ($isDir) {
                $size = Get-DirectorySize -Path $child.FullName
            } else {
                $size = $child.Length
            }
            $age = ''
            try {
                $days = (New-TimeSpan -Start $child.LastWriteTime -End (Get-Date)).Days
                if ($days -gt 365) { $age = '>1yr' }
                elseif ($days -gt 180) { $age = '>6mo' }
            } catch {}
            $entries.Add([pscustomobject]@{
                Name    = $child.Name
                Path    = $child.FullName
                Size    = $size
                IsDir   = $isDir
                Age     = $age
            })
        }
    } catch {}
    return ($entries | Sort-Object Size -Descending)
}

# ── JSON output mode ──────────────────────────────────────────────────────────
if ($Json) {
    $entries = Get-DirEntries -DirPath $Path
    $total   = ($entries | Measure-Object -Property Size -Sum).Sum
    $large   = @($entries | Where-Object { $_.Size -gt 100MB } |
        Select-Object Name, Path, Size, IsDir)
    $output  = [pscustomobject]@{
        path        = $Path
        entries     = $entries | Select-Object Name, Path, Size, IsDir
        large_files = $large
        total_size  = $total
        total_files = $entries.Count
    }
    $output | ConvertTo-Json -Depth 3
    return
}

# ── Interactive TUI analyzer ──────────────────────────────────────────────────
$stack     = [System.Collections.Generic.Stack[string]]::new()
$curPath   = $Path
$entries   = $null
$cursor    = 0
$offset    = 0
$maxVis    = [Math]::Min(20, $Host.UI.RawUI.WindowSize.Height - 10)
$largeMode = $false

function Reload-Entries {
    $script:entries = @(Get-DirEntries -DirPath $curPath)
    $script:cursor  = 0
    $script:offset  = 0
}

Reload-Entries

Hide-Cursor
[Console]::TreatControlCAsInput = $false

try {
    while ($true) {
        if (-not $entries -or $entries.Count -eq 0) {
            $entries = @()
        }

        $totalSize   = ($entries | Measure-Object -Property Size -Sum).Sum
        $visibleEntries = if ($largeMode) {
            @($entries | Where-Object { $_.Size -gt 100MB })
        } else {
            $entries
        }

        # ── Render ────────────────────────────────────────────────────────────
        [Console]::Clear()
        $relPath = $curPath -replace [regex]::Escape($env:USERPROFILE), '~'
        Write-Host ""
        Write-SpectreHost " [bold deepskyblue1]WinMole Analyze[/] [grey]$(Esc $relPath)[/] [gold1]Total: $(Format-Bytes $totalSize)[/]"
        Write-Host ""

        if ($largeMode) {
            Write-SpectreHost " [gold1]Large files (>100 MB) — press L to return[/]"
            Write-Host ""
        }

        # ── Entry list ────────────────────────────────────────────────────────
        $end = [Math]::Min($offset + $maxVis, $visibleEntries.Count)
        for ($i = $offset; $i -lt $end; $i++) {
            $e       = $visibleEntries[$i]
            $isCur   = ($i -eq $cursor)
            $pointer = if ($isCur) { '[deepskyblue1]▶[/]' } else { ' ' }
            $pct     = if ($totalSize -gt 0) { $e.Size / $totalSize * 100 } else { 0 }
            $barColor = if ($pct -gt 50) { 'red' } elseif ($pct -gt 25) { 'yellow' } else { 'green' }
            $bar     = Get-ProgressBar -Pct $pct -Width 18 -Color $barColor
            $icon    = if ($e.IsDir) { '📁' } else { '��' }
            $ageStr  = if ($e.Age) { " [grey]$($e.Age)[/]" } else { '' }
            $rank    = '{0,3}.' -f ($i + 1)
            $sizePad = (Format-Bytes -Bytes $e.Size).PadLeft(8)
            $namePad = $e.Name
            if ($namePad.Length -gt 30) { $namePad = $namePad.Substring(0,27) + '...' }
            $namePad = $namePad.PadRight(30)

            Write-SpectreHost (" $pointer $rank $bar {0,5:F1}% | $icon [white]$(Esc $namePad)[/] [gold1]$sizePad[/]$ageStr" -f $pct)
        }

        # ── Footer ────────────────────────────────────────────────────────────
        Write-Host ""
        Write-SpectreHost " [grey]↑↓ Navigate → Enter dir ← Back O Open F Reveal Del Delete L Large files Q Quit[/]"
        Write-Host ""

        # ── Input ─────────────────────────────────────────────────────────────
        $key = Read-Key

        switch ($key) {
            'Up' {
                if ($cursor -gt 0) {
                    $cursor--
                    if ($cursor -lt $offset) { $offset = $cursor }
                }
            }
            'Down' {
                $maxCursor = $visibleEntries.Count - 1
                if ($cursor -lt $maxCursor) {
                    $cursor++
                    if ($cursor -ge $offset + $maxVis) { $offset++ }
                }
            }
            'Right' {
                if ($cursor -lt $visibleEntries.Count) {
                    $sel = $visibleEntries[$cursor]
                    if ($sel.IsDir) {
                        $stack.Push($curPath)
                        $curPath = $sel.Path
                        Reload-Entries
                        $largeMode = $false
                    }
                }
            }
            'Enter' {
                if ($cursor -lt $visibleEntries.Count) {
                    $sel = $visibleEntries[$cursor]
                    if ($sel.IsDir) {
                        $stack.Push($curPath)
                        $curPath = $sel.Path
                        Reload-Entries
                        $largeMode = $false
                    }
                }
            }
            'Left' {
                if ($stack.Count -gt 0) {
                    $curPath = $stack.Pop()
                    Reload-Entries
                    $largeMode = $false
                }
            }
            'o' {
                if ($cursor -lt $visibleEntries.Count) {
                    Start-Process -FilePath $visibleEntries[$cursor].Path -ErrorAction SilentlyContinue
                }
            }
            'O' {
                if ($cursor -lt $visibleEntries.Count) {
                    Start-Process -FilePath $visibleEntries[$cursor].Path -ErrorAction SilentlyContinue
                }
            }
            'f' {
                if ($cursor -lt $visibleEntries.Count) {
                    Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$($visibleEntries[$cursor].Path)`"" -ErrorAction SilentlyContinue
                }
            }
            'F' {
                if ($cursor -lt $visibleEntries.Count) {
                    Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$($visibleEntries[$cursor].Path)`"" -ErrorAction SilentlyContinue
                }
            }
            'Delete' {
                if ($cursor -lt $visibleEntries.Count) {
                    $sel = $visibleEntries[$cursor]
                    Show-Cursor
                    Write-Host ""
                    Write-Host " Delete '$($sel.Name)' ($(Format-Bytes $sel.Size))? " -ForegroundColor Red -NoNewline
                    Write-Host "(y/N) " -ForegroundColor DarkGray -NoNewline
                    $conf = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
                    Hide-Cursor
                    if ($conf.Character -in @('y','Y')) {
                        Remove-Item -LiteralPath $sel.Path -Recurse -Force -ErrorAction SilentlyContinue
                        Write-OpLog "Analyze-delete: $($sel.Path) ($(Format-Bytes $sel.Size))"
                        Reload-Entries
                        $largeMode = $false
                    }
                }
            }
            'l' {
                $largeMode = -not $largeMode
                $cursor    = 0
                $offset    = 0
            }
            'L' {
                $largeMode = -not $largeMode
                $cursor    = 0
                $offset    = 0
            }
            'Escape' { break }
        }

        if ($key -eq 'Escape') { break }
    }
} finally {
    Show-Cursor
    [Console]::TreatControlCAsInput = $false
}
Write-Host ""