Private/Get-WUPendingUpdates.ps1

function Get-WUPendingUpdates {
    <#
    .SYNOPSIS
        Retrieves information about pending Windows Updates.
 
    .DESCRIPTION
        Uses the Windows Update API to check for pending updates including their
        status, size, and installation requirements.
 
    .PARAMETER LogPath
        Path to the log file for detailed logging.
 
    .EXAMPLE
        $pendingUpdates = Get-WUPendingUpdates -LogPath "C:\Logs\wu.log"
 
    .NOTES
        This is a private function used internally by the WindowsUpdateTools module.
        Returns array of pending update objects with detailed information.
    #>


    [CmdletBinding()]
    param(
        [string]$LogPath
    )

    Write-WULog -Message "Checking for pending Windows Updates" -LogPath $LogPath

    $pendingUpdates = @()
    
    # Pre-load update history to check for previous attempts
    $updateHistory = @{}
    try {
        $historySession = New-Object -ComObject Microsoft.Update.Session
        $historySearcher = $historySession.CreateUpdateSearcher()
        $historyCount = $historySearcher.GetTotalHistoryCount()
        if ($historyCount -gt 0) {
            $history = $historySearcher.QueryHistory(0, [Math]::Min($historyCount, 200))
            foreach ($entry in $history) {
                # Extract KB number from title
                if ($entry.Title -match 'KB(\d+)') {
                    $kb = "KB$($matches[1])"
                    if (-not $updateHistory.ContainsKey($kb)) {
                        $updateHistory[$kb] = @()
                    }
                    $updateHistory[$kb] += [PSCustomObject]@{
                        Date = $entry.Date
                        ResultCode = $entry.ResultCode
                        HResult = $entry.HResult
                        Operation = $entry.Operation
                    }
                }
            }
        }
    }
    catch {
        Write-WULog -Message "Could not load update history for attempt tracking: $($_.Exception.Message)" -Level Warning -LogPath $LogPath
    }

    try {
        # Create Windows Update session
        $updateSession = New-Object -ComObject Microsoft.Update.Session
        $updateSearcher = $updateSession.CreateUpdateSearcher()
        
        Write-WULog -Message "Searching for pending updates..." -LogPath $LogPath
        
        # Search for updates that are not installed
        $searchResult = $updateSearcher.Search("IsInstalled=0")
        
        if ($searchResult.Updates.Count -eq 0) {
            Write-WULog -Message "No pending updates found" -LogPath $LogPath
            return $pendingUpdates
        }

        Write-WULog -Message "Found $($searchResult.Updates.Count) pending updates" -LogPath $LogPath

        # Process each pending update
        foreach ($update in $searchResult.Updates) {
            try {
                # Calculate update size - prefer actual DownloadContents size over MaxDownloadSize
                # MaxDownloadSize for UUP updates shows full manifest (~91 GB) not actual differential download
                $actualDownloadSize = 0
                $downloadContentsCount = 0
                try {
                    if ($update.DownloadContents -and $update.DownloadContents.Count -gt 0) {
                        $downloadContentsCount = $update.DownloadContents.Count
                        # Sum up individual content sizes if available
                        foreach ($content in $update.DownloadContents) {
                            if ($content.Size -gt 0) {
                                $actualDownloadSize += $content.Size
                            }
                        }
                    }
                } catch {
                    # DownloadContents not accessible until download starts for UUP
                    $actualDownloadSize = 0
                }
                
                # Use actual size if available, otherwise fall back to MaxDownloadSize
                $effectiveSize = if ($actualDownloadSize -gt 0) { $actualDownloadSize } else { $update.MaxDownloadSize }
                $sizeKB = if ($effectiveSize -gt 0) { [math]::Round($effectiveSize / 1KB, 1) } else { 0 }
                $sizeMB = if ($sizeKB -gt 0) { [math]::Round($sizeKB / 1024, 1) } else { 0 }
                $isEstimatedSize = ($actualDownloadSize -eq 0)

                # Determine update category
                $categories = @()
                if ($update.Categories) {
                    foreach ($category in $update.Categories) {
                        $categories += $category.Name
                    }
                }

                # Check if update is downloaded
                $downloadStatus = "Not Downloaded"
                if ($update.IsDownloaded) {
                    $downloadStatus = "Downloaded"
                } elseif ($update.DownloadContents.Count -gt 0) {
                    $downloadStatus = "Partially Downloaded"
                }

                # Determine update severity/importance
                $importance = "Unknown"
                if ($update.MsrcSeverity) {
                    $importance = $update.MsrcSeverity
                } elseif ($update.AutoSelectOnWebSites) {
                    $importance = "Important"
                } else {
                    $importance = "Optional"
                }

                # Check for reboot requirement
                $rebootRequired = $false
                if ($update.InstallationBehavior) {
                    $rebootRequired = $update.InstallationBehavior.RebootBehavior -ne 0
                }

                # Extract KB number if available
                $kbNumber = $null
                if ($update.Title -match 'KB(\d+)') {
                    $kbNumber = "KB$($matches[1])"
                }
                
                # Check for previous installation attempts from history
                $previousAttempts = 0
                $lastAttemptDate = $null
                $lastAttemptResult = $null
                $installationState = "Pending Download"
                
                if ($kbNumber -and $updateHistory.ContainsKey($kbNumber)) {
                    $attempts = $updateHistory[$kbNumber]
                    $previousAttempts = $attempts.Count
                    
                    # Get most recent attempt
                    $lastAttempt = $attempts | Sort-Object Date -Descending | Select-Object -First 1
                    if ($lastAttempt) {
                        $lastAttemptDate = $lastAttempt.Date
                        # ResultCode: 0=Not Started, 1=In Progress, 2=Succeeded, 3=Succeeded with errors, 4=Failed, 5=Aborted
                        $lastAttemptResult = switch ($lastAttempt.ResultCode) {
                            0 { "Not Started" }
                            1 { "In Progress" }
                            2 { "Succeeded" }
                            3 { "Succeeded with Errors" }
                            4 { "Failed (0x{0:X8})" -f $lastAttempt.HResult }
                            5 { "Aborted" }
                            default { "Unknown ($($lastAttempt.ResultCode))" }
                        }
                    }
                }
                
                # Determine installation state
                if ($update.IsDownloaded) {
                    $installationState = "Pending Install"
                    $downloadStatus = "Downloaded (100%)"
                } elseif ($update.DownloadContents.Count -gt 0) {
                    # Try to get download percentage
                    $downloadPercent = 0
                    try {
                        # Check download progress via DownloadContents
                        $totalSize = $update.MaxDownloadSize
                        $downloadedSize = 0
                        foreach ($content in $update.DownloadContents) {
                            if ($content.IsDeltaCompressedContent) {
                                # Delta content is typically smaller
                            }
                        }
                        # Estimate based on partial presence
                        $downloadPercent = 50  # Estimate if we can't get exact percentage
                    }
                    catch {
                        $downloadPercent = 0
                    }
                    $downloadStatus = "Downloading (~$downloadPercent%)"
                    $installationState = "Downloading"
                } else {
                    $downloadStatus = "Not Downloaded (0%)"
                    $installationState = "Pending Download"
                }
                
                # If there were failed attempts, update the state
                if ($previousAttempts -gt 0 -and $lastAttemptResult -match "Failed|Aborted") {
                    $installationState = "Retry Pending ($previousAttempts previous attempts)"
                }

                # Create update object
                $updateInfo = [PSCustomObject]@{
                    Title = $update.Title
                    KBNumber = $kbNumber
                    Description = $update.Description
                    Categories = $categories -join ', '
                    Importance = $importance
                    SizeKB = $sizeKB
                    SizeMB = $sizeMB
                    MaxDownloadSizeMB = [math]::Round($update.MaxDownloadSize / 1MB, 1)
                    IsEstimatedSize = $isEstimatedSize
                    DownloadContentsCount = $downloadContentsCount
                    DownloadStatus = $downloadStatus
                    IsDownloaded = $update.IsDownloaded
                    InstallationState = $installationState
                    PreviousAttempts = $previousAttempts
                    LastAttemptDate = $lastAttemptDate
                    LastAttemptResult = $lastAttemptResult
                    RebootRequired = $rebootRequired
                    IsHidden = $update.IsHidden
                    IsMandatory = $update.IsMandatory
                    ReleaseDate = $update.LastDeploymentChangeTime
                    SupportUrl = $update.SupportUrl
                    UpdateID = $update.Identity.UpdateID
                    RevisionNumber = $update.Identity.RevisionNumber
                }

                $pendingUpdates += $updateInfo

                # Log update details
                Write-WULog -Message " Update: $($update.Title)" -LogPath $LogPath
                Write-WULog -Message " Size: $sizeMB MB, Status: $downloadStatus, Importance: $importance" -LogPath $LogPath
                Write-WULog -Message " State: $installationState" -LogPath $LogPath
                
                if ($previousAttempts -gt 0) {
                    Write-WULog -Message " Previous attempts: $previousAttempts, Last: $lastAttemptDate - $lastAttemptResult" -LogPath $LogPath
                }
                
                if ($rebootRequired) {
                    Write-WULog -Message " Reboot required after installation" -LogPath $LogPath
                }

            }
            catch {
                Write-WULog -Message "Error processing update '$($update.Title)': $($_.Exception.Message)" -Level Warning -LogPath $LogPath
            }
        }

        # Summary statistics
        $totalSizeMB = ($pendingUpdates | Measure-Object -Property SizeMB -Sum).Sum
        $downloadedUpdates = @($pendingUpdates | Where-Object { $_.IsDownloaded -eq $true })
        $importantUpdates = @($pendingUpdates | Where-Object { $_.Importance -in @('Critical', 'Important') })
        $rebootRequiredUpdates = @($pendingUpdates | Where-Object { $_.RebootRequired -eq $true })
        
        $downloadedCount = $downloadedUpdates.Count
        $importantCount = $importantUpdates.Count
        $rebootRequiredCount = $rebootRequiredUpdates.Count
        
        # Count updates with previous failed attempts
        $retriesNeeded = @($pendingUpdates | Where-Object { $_.PreviousAttempts -gt 0 -and $_.LastAttemptResult -match 'Failed|Aborted' })
        $pendingDownload = @($pendingUpdates | Where-Object { $_.InstallationState -eq 'Pending Download' })
        $pendingInstall = @($pendingUpdates | Where-Object { $_.InstallationState -eq 'Pending Install' })
        $downloading = @($pendingUpdates | Where-Object { $_.InstallationState -eq 'Downloading' })

        # Debug output
        Write-WULog -Message "Debug: downloadedCount=$downloadedCount, importantCount=$importantCount, rebootRequiredCount=$rebootRequiredCount" -LogPath $LogPath
        if ($pendingUpdates.Count -gt 0) {
            Write-WULog -Message "Debug: First update IsDownloaded=$($pendingUpdates[0].IsDownloaded), Importance=$($pendingUpdates[0].Importance), RebootRequired=$($pendingUpdates[0].RebootRequired)" -LogPath $LogPath
        }

        Write-WULog -Message "Pending updates summary:" -LogPath $LogPath
        Write-WULog -Message " Total updates: $($pendingUpdates.Count)" -LogPath $LogPath
        Write-WULog -Message " Total size: $([math]::Round($totalSizeMB, 1)) MB" -LogPath $LogPath
        Write-WULog -Message " Pending download: $($pendingDownload.Count)" -LogPath $LogPath
        Write-WULog -Message " Downloading: $($downloading.Count)" -LogPath $LogPath
        Write-WULog -Message " Ready to install: $($pendingInstall.Count)" -LogPath $LogPath
        Write-WULog -Message " Important/Critical: $importantCount" -LogPath $LogPath
        Write-WULog -Message " Requiring reboot: $rebootRequiredCount" -LogPath $LogPath
        
        if ($retriesNeeded.Count -gt 0) {
            Write-WULog -Message " Previously failed (need retry): $($retriesNeeded.Count)" -Level Warning -LogPath $LogPath
            foreach ($retry in $retriesNeeded) {
                Write-WULog -Message " $($retry.KBNumber): $($retry.PreviousAttempts) attempts, last: $($retry.LastAttemptResult)" -Level Warning -LogPath $LogPath
            }
        }

        # Check for stuck downloads (downloading state but partial)
        $partialDownloads = $pendingUpdates | Where-Object { $_.DownloadStatus -match 'Downloading' }
        if ($partialDownloads) {
            Write-WULog -Message "$($partialDownloads.Count) updates have partial downloads (may indicate download issues)" -Level Warning -LogPath $LogPath
        }
        
        # Note: Large sizes (e.g., ~91 GB) for cumulative updates are NORMAL for UUP (Unified Update Platform)
        # MaxDownloadSize represents the full UUP manifest size, not the actual differential download
        # Actual downloads are typically 500 MB - 4 GB depending on what's already installed
        # We track IsEstimatedSize to indicate when actual size is unknown
        $estimatedSizeUpdates = $pendingUpdates | Where-Object { $_.IsEstimatedSize -eq $true }
        if ($estimatedSizeUpdates.Count -gt 0) {
            Write-WULog -Message "$($estimatedSizeUpdates.Count) update(s) show UUP manifest size (actual download will be smaller)" -LogPath $LogPath
        }

        # Check for very old pending updates
        $oldUpdates = $pendingUpdates | Where-Object { 
            $_.ReleaseDate -and (Get-Date) - $_.ReleaseDate -gt [TimeSpan]::FromDays(30) 
        }
        if ($oldUpdates) {
            Write-WULog -Message "$($oldUpdates.Count) updates are over 30 days old" -Level Warning -LogPath $LogPath
        }

    }
    catch {
        Write-WULog -Message "Error checking for pending updates: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        
        # Try alternative method using WUA API
        try {
            Write-WULog -Message "Attempting alternative update check method..." -LogPath $LogPath
            
            $updateServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
            $updateService = $updateServiceManager.Services | Where-Object { $_.IsDefaultAUService }
            
            if ($updateService) {
                Write-WULog -Message "Using update service: $($updateService.Name)" -LogPath $LogPath
            } else {
                Write-WULog -Message "Could not identify default update service" -Level Warning -LogPath $LogPath
            }
        }
        catch {
            Write-WULog -Message "Alternative update check method also failed: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        }
    }

    return $pendingUpdates
}