#Requires -Version 5.1

#########################################################################################
#### windows-disk-usage-reporter.ps1 - Find what's consuming disk space on Windows   ####
#### Scans volumes, ranks largest directories and files, flags old data              ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   .\windows-disk-usage-reporter.ps1                                             ####
####   .\windows-disk-usage-reporter.ps1 -Path C:\Users                              ####
####   .\windows-disk-usage-reporter.ps1 -TopN 50 -MinSizeMB 100                     ####
####   .\windows-disk-usage-reporter.ps1 -Json                                       ####
####                                                                                 ####
#########################################################################################

[CmdletBinding()]
param(
    [string]$Path = "C:\",
    [int]$TopN = 20,
    [int]$MinSizeMB = 1,
    [int]$MaxDepth = 3,
    [int]$AgeWarnDays = 90,
    [switch]$Json,
    [switch]$NoColor
)

$Version = "1.00"
$MinSizeBytes = [int64]$MinSizeMB * 1048576
$AgeCutoff = (Get-Date).AddDays(-$AgeWarnDays)
$Separator = [string]::new([char]0x2500, 88)

# ============================================================================
# HELPERS
# ============================================================================

function Format-FileSize {
    param([int64]$Bytes)
    if ($Bytes -ge 1TB) { return "{0:N2} TB" -f ($Bytes / 1TB) }
    if ($Bytes -ge 1GB) { return "{0:N2} GB" -f ($Bytes / 1GB) }
    if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
    if ($Bytes -ge 1KB) { return "{0:N1} KB" -f ($Bytes / 1KB) }
    return "$Bytes B"
}

function Write-ColorLine {
    param(
        [string]$Text,
        [ConsoleColor]$Color = [ConsoleColor]::Gray
    )
    if ($NoColor) {
        Write-Output $Text
    } else {
        Write-Host $Text -ForegroundColor $Color
    }
}

function Write-Header {
    param([string]$Title)
    Write-ColorLine ""
    Write-ColorLine ("=" * 56) Cyan
    Write-ColorLine "  $Title" Cyan
    Write-ColorLine ("=" * 56) Cyan
    Write-ColorLine ""
}

function Get-DirectorySizeRecursive {
    param(
        [string]$DirPath,
        [int]$CurrentDepth,
        [int]$Limit
    )
    if ($CurrentDepth -gt $Limit) { return @() }

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    try {
        $items = Get-ChildItem -LiteralPath $DirPath -Directory -ErrorAction SilentlyContinue
    } catch {
        return @()
    }

    foreach ($dir in $items) {
        try {
            $files = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File -ErrorAction SilentlyContinue
            $totalSize = ($files | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
            if ($null -eq $totalSize) { $totalSize = 0 }

            $results.Add([PSCustomObject]@{
                Path = $dir.FullName
                SizeBytes = [int64]$totalSize
            })
        } catch {
            continue
        }

        if ($CurrentDepth -lt $Limit) {
            $children = Get-DirectorySizeRecursive -DirPath $dir.FullName -CurrentDepth ($CurrentDepth + 1) -Limit $Limit
            foreach ($child in $children) {
                $results.Add($child)
            }
        }
    }

    return $results
}

# ============================================================================
# VOLUME OVERVIEW
# ============================================================================

function Get-VolumeOverview {
    Write-Header "Volume Overview"

    $headerLine = "  {0,-12} {1,-20} {2,10} {3,10} {4,6}" -f "Drive", "Label", "Size", "Free", "Used%"
    Write-ColorLine $headerLine White
    Write-ColorLine "  $Separator"

    $volumes = @()
    try {
        $disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
    } catch {
        $disks = Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
    }

    foreach ($disk in $disks) {
        $total = [int64]$disk.Size
        $free = [int64]$disk.FreeSpace
        if ($total -eq 0) { continue }
        $used = $total - $free
        $pct = [math]::Round(($used / $total) * 100, 1)

        $color = [ConsoleColor]::Green
        if ($pct -ge 90) { $color = [ConsoleColor]::Red }
        elseif ($pct -ge 80) { $color = [ConsoleColor]::Yellow }

        $line = "  {0,-12} {1,-20} {2,10} {3,10} {4,5}%" -f $disk.DeviceID, $disk.VolumeName, (Format-FileSize $total), (Format-FileSize $free), $pct
        Write-ColorLine $line $color

        $volumes += [PSCustomObject]@{
            Drive     = $disk.DeviceID
            Label     = $disk.VolumeName
            SizeBytes = $total
            FreeBytes = $free
            UsedPct   = $pct
        }
    }

    return $volumes
}

# ============================================================================
# TOP DIRECTORIES BY SIZE
# ============================================================================

function Get-TopDirectories {
    Write-Header "Top $TopN Directories by Size"

    $headerLine = "  {0,4}  {1,-60} {2,10}" -f "#", "Directory", "Size"
    Write-ColorLine $headerLine White
    Write-ColorLine "  $Separator"

    $allDirs = Get-DirectorySizeRecursive -DirPath $Path -CurrentDepth 1 -Limit $MaxDepth
    $topDirs = $allDirs | Sort-Object SizeBytes -Descending | Select-Object -First $TopN

    $rank = 0
    foreach ($dir in $topDirs) {
        $rank++
        $color = [ConsoleColor]::Gray
        if ($dir.SizeBytes -ge 10GB) { $color = [ConsoleColor]::Red }
        elseif ($dir.SizeBytes -ge 1GB) { $color = [ConsoleColor]::Yellow }

        $displayPath = $dir.Path
        if ($displayPath.Length -gt 58) { $displayPath = "..." + $displayPath.Substring($displayPath.Length - 55) }

        $line = "  {0,4}  {1,-60} {2,10}" -f $rank, $displayPath, (Format-FileSize $dir.SizeBytes)
        Write-ColorLine $line $color
    }

    return $topDirs
}

# ============================================================================
# TOP FILES BY SIZE
# ============================================================================

function Get-TopFiles {
    Write-Header "Top $TopN Files by Size"

    $headerLine = "  {0,4}  {1,-60} {2,10}" -f "#", "File", "Size"
    Write-ColorLine $headerLine White
    Write-ColorLine "  $Separator"

    $files = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Length -ge $MinSizeBytes } |
        Sort-Object Length -Descending |
        Select-Object -First $TopN

    $rank = 0
    $result = @()
    foreach ($file in $files) {
        $rank++
        $color = [ConsoleColor]::Gray
        if ($file.Length -ge 1GB) { $color = [ConsoleColor]::Red }
        elseif ($file.Length -ge 100MB) { $color = [ConsoleColor]::Yellow }

        $displayPath = $file.FullName
        if ($displayPath.Length -gt 58) { $displayPath = "..." + $displayPath.Substring($displayPath.Length - 55) }

        $line = "  {0,4}  {1,-60} {2,10}" -f $rank, $displayPath, (Format-FileSize $file.Length)
        Write-ColorLine $line $color

        $result += [PSCustomObject]@{
            Path      = $file.FullName
            SizeBytes = $file.Length
        }
    }

    return $result
}

# ============================================================================
# OLD LARGE FILES
# ============================================================================

function Get-OldLargeFiles {
    Write-Header "Old Large Files (> ${MinSizeMB} MB, older than $AgeWarnDays days)"

    $headerLine = "  {0,4}  {1,-50} {2,10} {3,12}" -f "#", "File", "Size", "Last Modified"
    Write-ColorLine $headerLine White
    Write-ColorLine "  $Separator"

    $files = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Length -ge $MinSizeBytes -and $_.LastWriteTime -lt $AgeCutoff } |
        Sort-Object Length -Descending |
        Select-Object -First $TopN

    if (-not $files -or $files.Count -eq 0) {
        Write-ColorLine "  No files found matching criteria."
        return @()
    }

    $rank = 0
    $result = @()
    foreach ($file in $files) {
        $rank++
        $displayPath = $file.FullName
        if ($displayPath.Length -gt 48) { $displayPath = "..." + $displayPath.Substring($displayPath.Length - 45) }

        $line = "  {0,4}  {1,-50} {2,10} {3,12}" -f $rank, $displayPath, (Format-FileSize $file.Length), $file.LastWriteTime.ToString("yyyy-MM-dd")
        Write-ColorLine $line Yellow

        $result += [PSCustomObject]@{
            Path         = $file.FullName
            SizeBytes    = $file.Length
            LastModified = $file.LastWriteTime.ToString("yyyy-MM-dd")
        }
    }

    return $result
}

# ============================================================================
# SUMMARY
# ============================================================================

function Write-Summary {
    param(
        [int64]$TotalScanned,
        [array]$OldFiles
    )

    Write-Header "Summary"

    $oldCount = 0
    $oldBytes = [int64]0
    if ($OldFiles) {
        $oldCount = $OldFiles.Count
        $oldBytes = ($OldFiles | Measure-Object -Property SizeBytes -Sum).Sum
        if ($null -eq $oldBytes) { $oldBytes = 0 }
    }

    Write-ColorLine ("  Scan path:              $Path")
    Write-ColorLine ("  Total scanned:          $(Format-FileSize $TotalScanned)")
    Write-ColorLine ("  Min file size:           ${MinSizeMB} MB")
    Write-ColorLine ("  Age threshold:           $AgeWarnDays days")
    Write-ColorLine ""
    Write-ColorLine ("  Old large files:         $oldCount files")
    Write-ColorLine ("  Reclaimable space:       $(Format-FileSize $oldBytes)") Yellow
    Write-ColorLine ""

    if ($oldBytes -gt 0) {
        Write-ColorLine "  -> Review old files above - candidates for cleanup or archival" Yellow
    } else {
        Write-ColorLine "  OK - No old large files found" Green
    }
    Write-ColorLine ""
}

# ============================================================================
# JSON OUTPUT
# ============================================================================

function Get-JsonReport {
    param(
        [array]$Volumes,
        [array]$TopDirs,
        [array]$TopFilesList,
        [array]$OldFiles,
        [int64]$TotalScanned
    )

    $oldBytes = [int64]0
    if ($OldFiles) {
        $oldBytes = ($OldFiles | Measure-Object -Property SizeBytes -Sum -ErrorAction SilentlyContinue).Sum
        if ($null -eq $oldBytes) { $oldBytes = 0 }
    }

    $report = [PSCustomObject]@{
        scan_path      = $Path
        timestamp      = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
        min_size_mb    = $MinSizeMB
        age_warn_days  = $AgeWarnDays
        max_depth      = $MaxDepth
        volumes        = @($Volumes)
        top_directories = @($TopDirs)
        top_files      = @($TopFilesList)
        old_large_files = @($OldFiles)
        summary        = [PSCustomObject]@{
            total_scanned_bytes = $TotalScanned
            old_file_count      = if ($OldFiles) { $OldFiles.Count } else { 0 }
            reclaimable_bytes   = $oldBytes
        }
    }

    return $report | ConvertTo-Json -Depth 5
}

# ============================================================================
# MAIN
# ============================================================================

if (-not (Test-Path -LiteralPath $Path)) {
    Write-Error "Path does not exist: $Path"
    exit 1
}

# Calculate total size of scanned path
$totalScanned = [int64]0
try {
    $allFiles = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue
    $totalScanned = ($allFiles | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
    if ($null -eq $totalScanned) { $totalScanned = 0 }
} catch {
    $totalScanned = 0
}

if ($Json) {
    # Collect data silently for JSON - redirect console output
    $volumes = @()
    try {
        $disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
    } catch {
        $disks = Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
    }
    foreach ($disk in $disks) {
        $total = [int64]$disk.Size
        $free = [int64]$disk.FreeSpace
        if ($total -eq 0) { continue }
        $used = $total - $free
        $pct = [math]::Round(($used / $total) * 100, 1)
        $volumes += [PSCustomObject]@{
            Drive     = $disk.DeviceID
            Label     = $disk.VolumeName
            SizeBytes = $total
            FreeBytes = $free
            UsedPct   = $pct
        }
    }

    $topDirs = Get-DirectorySizeRecursive -DirPath $Path -CurrentDepth 1 -Limit $MaxDepth |
        Sort-Object SizeBytes -Descending | Select-Object -First $TopN

    $topFilesList = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Length -ge $MinSizeBytes } |
        Sort-Object Length -Descending |
        Select-Object -First $TopN |
        ForEach-Object { [PSCustomObject]@{ Path = $_.FullName; SizeBytes = $_.Length } }

    $oldFiles = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Length -ge $MinSizeBytes -and $_.LastWriteTime -lt $AgeCutoff } |
        Sort-Object Length -Descending |
        Select-Object -First $TopN |
        ForEach-Object { [PSCustomObject]@{ Path = $_.FullName; SizeBytes = $_.Length; LastModified = $_.LastWriteTime.ToString("yyyy-MM-dd") } }

    Get-JsonReport -Volumes $volumes -TopDirs $topDirs -TopFilesList $topFilesList -OldFiles $oldFiles -TotalScanned $totalScanned
    exit 0
}

# Interactive output
Write-ColorLine ""
Write-ColorLine "Disk Usage Report" White
Write-ColorLine ("{0}  -  Scanning: {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Path)

$volumes   = Get-VolumeOverview
$topDirs   = Get-TopDirectories
$topFiles  = Get-TopFiles
$oldFiles  = Get-OldLargeFiles

Write-Summary -TotalScanned $totalScanned -OldFiles $oldFiles
