bin/status.ps1

# WinMole - Live System Health Dashboard
# Real-time CPU, memory, disk I/O, network, and process stats

param(
    [switch]$Json,
    [double]$ProcCpuThreshold = 50.0,
    [int]$ProcCpuWindow       = 10,
    [switch]$NoProcCpuAlerts
)

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

# ── Data collection helpers ───────────────────────────────────────────────────
function Get-CpuUsage {
    try {
        $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue
        $total = ($cpu | Measure-Object -Property LoadPercentage -Average).Average
        $cores = $cpu | ForEach-Object { $_.LoadPercentage }
        return [pscustomobject]@{ Total = $total; Cores = @($cores) }
    } catch { return [pscustomobject]@{ Total = 0; Cores = @() } }
}

function Get-MemoryInfo {
    try {
        $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
        $cs = Get-CimInstance -ClassName Win32_ComputerSystem  -ErrorAction SilentlyContinue
        $total = $cs.TotalPhysicalMemory
        $free  = $os.FreePhysicalMemory * 1KB
        $used  = $total - $free
        return [pscustomobject]@{
            TotalGB = [Math]::Round($total / 1GB, 1)
            UsedGB  = [Math]::Round($used  / 1GB, 1)
            FreeGB  = [Math]::Round($free  / 1GB, 1)
            UsedPct = [Math]::Round($used / $total * 100, 1)
        }
    } catch { return [pscustomobject]@{ TotalGB = 0; UsedGB = 0; FreeGB = 0; UsedPct = 0 } }
}

function Get-DiskInfo {
    try {
        $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" `
            -ErrorAction SilentlyContinue | Select-Object -First 1
        $total = $disk.Size
        $free  = $disk.FreeSpace
        $used  = $total - $free
        return [pscustomobject]@{
            Drive   = $disk.DeviceID
            TotalGB = [Math]::Round($total / 1GB, 1)
            UsedGB  = [Math]::Round($used  / 1GB, 1)
            FreeGB  = [Math]::Round($free  / 1GB, 1)
            UsedPct = [Math]::Round($used / $total * 100, 1)
        }
    } catch { return [pscustomobject]@{ Drive='C:'; TotalGB=0; UsedGB=0; FreeGB=0; UsedPct=0 } }
}

function Get-NetworkIO {
    param([hashtable]$Previous)
    try {
        $adapters = Get-CimInstance -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface `
            -ErrorAction SilentlyContinue
        $rx = [long]0; $tx = [long]0
        foreach ($a in $adapters) {
            $rx += $a.BytesReceivedPersec
            $tx += $a.BytesSentPersec
        }
        return [pscustomobject]@{ RxBps = $rx; TxBps = $tx }
    } catch { return [pscustomobject]@{ RxBps = 0; TxBps = 0 } }
}

function Get-DiskIO {
    try {
        $perf = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfDisk_PhysicalDisk `
            -Filter "Name='_Total'" -ErrorAction SilentlyContinue
        return [pscustomobject]@{
            ReadBps  = if ($perf) { $perf.DiskReadBytesPersec } else { 0 }
            WriteBps = if ($perf) { $perf.DiskWriteBytesPersec } else { 0 }
        }
    } catch { return [pscustomobject]@{ ReadBps = 0; WriteBps = 0 } }
}

function Get-TopProcesses {
    param([int]$Count = 5)
    try {
        return Get-Process -ErrorAction SilentlyContinue |
            Sort-Object CPU -Descending |
            Select-Object -First $Count |
            ForEach-Object {
                [pscustomobject]@{
                    Name   = $_.Name
                    CpuPct = [Math]::Min(100, [Math]::Round($_.CPU / ([Environment]::ProcessorCount * 10), 1))
                }
            }
    } catch { return @() }
}

function Get-HealthScore {
    param($Cpu, $Mem, $Disk)
    $score = 100
    if ($Cpu.Total -gt 90)  { $score -= 30 }
    elseif ($Cpu.Total -gt 70)  { $score -= 15 }
    elseif ($Cpu.Total -gt 50)  { $score -= 5  }
    if ($Mem.UsedPct -gt 90) { $score -= 25 }
    elseif ($Mem.UsedPct -gt 75) { $score -= 10 }
    elseif ($Mem.UsedPct -gt 60) { $score -= 5  }
    if ($Disk.UsedPct -gt 95) { $score -= 20 }
    elseif ($Disk.UsedPct -gt 85) { $score -= 10 }
    elseif ($Disk.UsedPct -gt 75) { $score -= 5  }
    return [Math]::Max(0, $score)
}

function Get-HealthColor {
    param([int]$Score)
    if ($Score -ge 80) { return 'green' }
    if ($Score -ge 60) { return 'gold1' }
    return 'red1'
}

function Format-NetSpeed {
    param([long]$Bps)
    if ($Bps -ge 1MB) { return '{0:F1} MB/s' -f ($Bps / 1MB) }
    if ($Bps -ge 1KB) { return '{0:F0} KB/s' -f ($Bps / 1KB) }
    return "${Bps} B/s"
}

# Helper: clear current line then write Spectre markup
function CL { Write-Host "`e[2K" -NoNewline }

# ── JSON mode ─────────────────────────────────────────────────────────────────
if ($Json -or [Console]::IsOutputRedirected) {
    $cpu   = Get-CpuUsage
    $mem   = Get-MemoryInfo
    $disk  = Get-DiskInfo
    $net   = Get-NetworkIO
    $io    = Get-DiskIO
    $procs = Get-TopProcesses
    $score = Get-HealthScore $cpu $mem $disk
    $os    = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
    $uptime = if ($os) {
        Format-Uptime -Seconds ([long](New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).TotalSeconds)
    } else { 'Unknown' }

    [pscustomobject]@{
        host         = $env:COMPUTERNAME
        health_score = $score
        cpu          = [pscustomobject]@{ usage = $cpu.Total; logical_cpu = [Environment]::ProcessorCount }
        memory       = [pscustomobject]@{ total_gb = $mem.TotalGB; used_gb = $mem.UsedGB; used_percent = $mem.UsedPct }
        disk         = [pscustomobject]@{ drive = $disk.Drive; total_gb = $disk.TotalGB; free_gb = $disk.FreeGB; used_percent = $disk.UsedPct }
        network      = [pscustomobject]@{ rx_bps = $net.RxBps; tx_bps = $net.TxBps }
        uptime       = $uptime
        top_processes = $procs
    } | ConvertTo-Json -Depth 3
    return
}

# ── Live TUI dashboard ─────────────────────────────────────────────────────────
$info    = Get-SysInfoSummary
$refresh = 2  # seconds between refreshes

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

$hwStr = "$($info.MachineName) · $($info.CPU.Split(',')[0]) · $($info.RamTotalGB)GB · $($info.OSCaption -replace 'Microsoft ','')"

# Track high-CPU processes for alerts
$highCpuTracker = @{}

try {
    while ($true) {
        # Check for keypress without blocking
        $quit = $false
        while ([Console]::KeyAvailable) {
            $k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
            if ($k.Character -in @('q','Q') -or $k.VirtualKeyCode -eq 27) {
                $quit = $true
            }
        }
        if ($quit) { break }

        # Collect data
        $cpu   = Get-CpuUsage
        $mem   = Get-MemoryInfo
        $disk  = Get-DiskInfo
        $net   = Get-NetworkIO
        $io    = Get-DiskIO
        $procs = Get-TopProcesses -Count 5
        $score = Get-HealthScore $cpu $mem $disk
        $hColor= Get-HealthColor $score

        # CPU alert check
        if (-not $NoProcCpuAlerts) {
            $currentProcNames = [System.Collections.Generic.HashSet[string]]::new(
                [string[]]@($procs | ForEach-Object { $_.Name }))
            # Evict processes no longer in top list
            @($highCpuTracker.Keys) | Where-Object { -not $currentProcNames.Contains($_) } |
                ForEach-Object { $highCpuTracker.Remove($_) }
            foreach ($p in $procs) {
                if ($p.CpuPct -ge $ProcCpuThreshold) {
                    if (-not $highCpuTracker.ContainsKey($p.Name)) {
                        $highCpuTracker[$p.Name] = 0
                    }
                    $highCpuTracker[$p.Name]++
                } else {
                    $highCpuTracker.Remove($p.Name)
                }
            }
        }

        # ── Render ────────────────────────────────────────────────────────────
        [Console]::SetCursorPosition(0, 0)

        # Header
        CL; Write-SpectreHost " [bold deepskyblue1]WinMole Status[/] Health [$hColor]● $score[/] [grey]$(Esc $hwStr)[/]"
        CL; Write-Host ""

        # CPU
        $cpuColor = if ($cpu.Total -gt 80) { 'red' } elseif ($cpu.Total -gt 60) { 'yellow' } else { 'green' }
        $cpuBar = Get-ProgressBar -Pct $cpu.Total -Width 18 -Color $cpuColor
        CL; Write-SpectreHost " [bold]⚙ CPU[/]"
        CL; Write-SpectreHost (" Total $cpuBar [gold1]{0,5:F1}%[/] [grey]{1} logical cores[/]" -f $cpu.Total, [Environment]::ProcessorCount)
        CL; Write-Host ""

        # Memory
        $memColor = if ($mem.UsedPct -gt 80) { 'red' } elseif ($mem.UsedPct -gt 60) { 'yellow' } else { 'green' }
        $memBar = Get-ProgressBar -Pct $mem.UsedPct -Width 18 -Color $memColor
        CL; Write-SpectreHost " [bold]▦ Memory[/]"
        CL; Write-SpectreHost (" Used $memBar [gold1]{0,5:F1}%[/] [grey]{1:F1} / {2:F1} GB[/]" -f $mem.UsedPct, $mem.UsedGB, $mem.TotalGB)
        CL; Write-SpectreHost (" Avail [grey]{0:F1} GB[/]" -f $mem.FreeGB)
        CL; Write-Host ""

        # Disk
        $diskColor = if ($disk.UsedPct -gt 90) { 'red' } elseif ($disk.UsedPct -gt 75) { 'yellow' } else { 'green' }
        $diskBar = Get-ProgressBar -Pct $disk.UsedPct -Width 18 -Color $diskColor
        $rdBar = Get-SparkBar -Pct ([Math]::Min(100, $io.ReadBps / 50MB * 100))
        $wrBar = Get-SparkBar -Pct ([Math]::Min(100, $io.WriteBps / 50MB * 100))
        CL; Write-SpectreHost " [bold]▤ Disk ($($disk.Drive))[/]"
        CL; Write-SpectreHost (" Used $diskBar [gold1]{0,5:F1}%[/] Free [grey]{1:F1} GB[/]" -f $disk.UsedPct, $disk.FreeGB)
        CL; Write-SpectreHost " Read [teal]$rdBar[/] [grey]$(Format-NetSpeed $io.ReadBps)[/] Write [fuchsia]$wrBar[/] [grey]$(Format-NetSpeed $io.WriteBps)[/]"
        CL; Write-Host ""

        # Network
        $dlBar = Get-SparkBar -Pct ([Math]::Min(100, $net.RxBps / 10MB * 100))
        $ulBar = Get-SparkBar -Pct ([Math]::Min(100, $net.TxBps / 10MB * 100))
        CL; Write-SpectreHost " [bold]⇅ Network[/]"
        CL; Write-SpectreHost " Down [green]$dlBar[/] [grey]$(Format-NetSpeed $net.RxBps)[/] Up [dodgerblue1]$ulBar[/] [grey]$(Format-NetSpeed $net.TxBps)[/]"
        CL; Write-Host ""

        # Top Processes
        CL; Write-SpectreHost " [bold]▶ Top Processes[/]"
        $procLines = @($procs)
        for ($i = 0; $i -lt 5; $i++) {
            if ($i -lt $procLines.Count) {
                $pName = $procLines[$i].Name
                if ($pName.Length -gt 14) { $pName = $pName.Substring(0, 11) + '...' }
                $pName = $pName.PadRight(14)
                $pBar = Get-ProgressBar -Pct $procLines[$i].CpuPct -Width 5
                CL; Write-SpectreHost " [grey]$(Esc $pName)[/] $pBar [gold1]$($procLines[$i].CpuPct)%[/]"
            } else {
                CL; Write-Host ""
            }
        }
        CL; Write-Host ""

        # CPU alerts
        $alerts = @($highCpuTracker.GetEnumerator() | Where-Object { $_.Value -ge $ProcCpuWindow })
        if ($alerts -and -not $NoProcCpuAlerts) {
            $alertMsg = ($alerts | ForEach-Object { "$(Esc $_.Key) sustained $($_.Value)× above ${ProcCpuThreshold}%" }) -join ', '
            CL; Write-SpectreHost " [red1]⚠ High CPU Alert:[/] $alertMsg"
            CL; Write-Host ""
        }

        CL; Write-SpectreHost " [grey]Refreshes every ${refresh}s | Q Quit[/]"

        # Clear anything remaining below
        Write-Host "`e[J" -NoNewline

        Start-Sleep -Seconds $refresh
    }
} finally {
    Show-Cursor
    [Console]::TreatControlCAsInput = $false
}
Write-Host ""