PSPatchManager.psm1

# Helper function for Windows Update error handling
function Get-WindowsUpdateErrorMessage {
    param([int]$ErrorCode)
    
    $hexError = "0x{0:X8}" -f $ErrorCode
    switch ($hexError) {
        "0x80240032" { return "No updates were found to install. ($hexError)" }
        "0x80240022" { return "The update is not applicable to this computer. ($hexError)" }
        "0x80240006" { return "The search may have been interrupted. ($hexError)" }
        "0x8024000B" { return "Service registration is missing or corrupt. ($hexError)" }
        "0x8024000C" { return "Windows Update Agent is missing or corrupt. ($hexError)" }
        "0x8024000D" { return "Windows Update Agent is missing or corrupt. ($hexError)" }
        "0x8024000E" { return "Windows Update Agent is missing or corrupt. ($hexError)" }
        "0x8024001E" { return "Service registration is missing or corrupt. ($hexError)" }
        "0x8024002E" { return "Windows Update Agent was unable to provide the service. ($hexError)" }
        default { return "Windows Update error: $hexError" }
    }
}

# Helper function to translate update operation result codes
function Get-UpdateOperationResult {
    param(
        [Parameter(Mandatory)]
        [string]$ResultCode,
        [string]$Operation
    )
    
    $status = switch ($ResultCode) {
        "0" { "Failed" }
        "1" { "In Progress" }
        "2" { "Succeeded" }
        "3" { "Succeeded with Errors" }
        "4" { "Cancelled" }
        "5" { "Aborted" }
        default { "Unknown ($ResultCode)" }
    }
    
    return "$Operation $status"
}

# Common helper function for error handling and verbose output
function Write-PMLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,
        [ValidateSet('Info', 'Warning', 'Error')]
        [string]$Type = 'Info'
    )
    
    switch ($Type) {
        'Info' { 
            if ($Message -match '^(Processing|Found|Status:|Download Summary|Total updates|Starting download)') {
                Write-Verbose "`n$Message" 
            } 
            elseif ($Message -match '^(Adding update|Downloading)') {
                Write-Verbose " $Message"
            }
            else {
                Write-Verbose $Message
            }
        }
        'Warning' { Write-Warning "`n$Message" }
        'Error' { Write-Error "`n$Message" }
    }
}

function Get-PMWinUpdate {
    <#
    .SYNOPSIS
        Gets available Windows updates with advanced filtering options.
     
    .DESCRIPTION
        Retrieves a list of available Windows updates with the ability to filter by various criteria
        including severity, type, reboot requirement, date range, size, and title search.
     
    .PARAMETER Title
        Filter updates by title. Supports wildcard patterns unless -ExactMatch is specified.
     
    .PARAMETER ExactMatch
        When used with -Title, requires an exact match instead of wildcard pattern matching.
     
    .PARAMETER Hidden
        If specified, returns hidden updates instead of visible ones.
     
    .PARAMETER NotInstalled
        If specified, returns only updates that are not installed.
     
    .PARAMETER Severity
        Filters updates by severity level. Valid values are:
        - Critical
        - Important
        - Moderate
        - Low
        - Unspecified
     
    .PARAMETER Type
        Filters updates by type. Valid values are:
        - Software
        - Driver
        - Security
        - FeaturePack
     
    .PARAMETER RebootRequired
        If specified, returns only updates that require a reboot.
     
    .PARAMETER FromDate
        If specified, returns updates released on or after this date.
     
    .PARAMETER ToDate
        If specified, returns updates released on or before this date.
     
    .PARAMETER MaxSize
        If specified, returns updates with size less than or equal to this value (in MB).
 
    .PARAMETER SortBy
        Specifies the property to sort results by. Valid values are:
        - Size
        - ReleaseDate
        - Title
        - Severity
 
    .PARAMETER Descending
        If specified, sorts the results in descending order.
 
    .PARAMETER ExcludeBeta
        If specified, excludes beta updates from the results.
 
    .PARAMETER OnlyDownloaded
        If specified, returns only updates that have been downloaded.
 
    .PARAMETER CategoryFilter
        If specified, filters updates by category name (supports wildcards).
     
    .PARAMETER Repository
        Specifies the repository to use for updates. Valid values are:
        - WindowsUpdate
        - MicrosoftUpdate
     
    .EXAMPLE
        Get-PMWinUpdate -Title "Security Update for Windows"
        Returns all updates containing "Security Update for Windows" in their title.
     
    .EXAMPLE
        Get-PMWinUpdate -Title "KB5025221" -ExactMatch
        Returns the update with exact title match for KB5025221.
     
    .EXAMPLE
        Get-PMWinUpdate -Severity Critical, Important
        Returns all critical and important updates.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName='Title')]
        [string]$Title,
        [switch]$Hidden,
        [switch]$NotInstalled,
        [ValidateSet('Critical', 'Important', 'Moderate', 'Low', 'Unspecified')]
        [string[]]$Severity,
        [ValidateSet('Software', 'Driver', 'Security', 'FeaturePack')]
        [string[]]$Type,
        [switch]$RebootRequired,
        [DateTime]$FromDate,
        [DateTime]$ToDate,
        [int]$MaxSize, # Size in MB
        [ValidateSet('Size', 'ReleaseDate', 'Title', 'Severity')]
        [string]$SortBy = 'ReleaseDate',
        [switch]$Descending,
        [switch]$ExcludeBeta,
        [switch]$OnlyDownloaded,
        [string]$CategoryFilter,
        [ValidateSet('WindowsUpdate', 'MicrosoftUpdate')]
        [string]$Repository = 'WindowsUpdate'
    )
    
    begin {
        Write-PMLog -Message 'Initializing Windows Update session...' -Type Info
        try {
            $UpdateSession = New-Object -ComObject Microsoft.Update.Session
            $UpdateSearcher = $UpdateSession.CreateUpdateSearcher()

            # Configure repository
            if ($Repository -eq 'MicrosoftUpdate') {
                $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
                $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d"
                
                # Check if Microsoft Update is registered
                $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID }
                if (-not $Service) {
                    Write-PMLog -Message "Microsoft Update Service is not registered. Use Register-PMMicrosoftUpdate to enable it." -Type Warning
                    return
                }
                
                Write-PMLog -Message "Using Microsoft Update Service..." -Type Info
                $UpdateSearcher.ServerSelection = 3 # ssOthers
                $UpdateSearcher.ServiceID = $ServiceID
            } else {
                Write-PMLog -Message "Using Windows Update Service..." -Type Info
                $UpdateSearcher.ServerSelection = 2 # ssWindowsUpdate
            }
        }
        catch {
            $errorMessage = 'Failed to initialize Windows Update session: ' + $_.Exception.Message
            Write-PMLog -Message $errorMessage -Type Error
            return
        }
    }
    
    end {
        try {
            Write-PMLog -Message 'Searching for Windows Updates...' -Type Info
            $searchCriteria = 'IsInstalled=0'
            if ($Hidden) {
                $searchCriteria = $searchCriteria + ' AND IsHidden=1'
            }
            elseif ($NotInstalled) {
                $searchCriteria = $searchCriteria + ' AND IsHidden=0'
            }
            
            $SearchResult = $UpdateSearcher.Search($searchCriteria)
            
            $updates = $SearchResult.Updates | ForEach-Object {
                # Convert size to readable format
                $sizeString = if ($_.MaxDownloadSize -ge 1GB) {
                    '{0:N2} GB' -f ($_.MaxDownloadSize / 1GB)
                } elseif ($_.MaxDownloadSize -ge 1MB) {
                    '{0:N2} MB' -f ($_.MaxDownloadSize / 1MB)
                } elseif ($_.MaxDownloadSize -ge 1KB) {
                    '{0:N2} KB' -f ($_.MaxDownloadSize / 1KB)
                } else {
                    '{0} B' -f $_.MaxDownloadSize
                }

                [PSCustomObject]@{
                    PSTypeName = 'PSPatchManager.WindowsUpdate'
                    Title = $_.Title
                    KB = $_.KBArticleIDs -join ','
                    Size = $sizeString
                    SizeBytes = $_.MaxDownloadSize
                    IsHidden = $_.IsHidden
                    Categories = ($_.Categories | Select-Object -ExpandProperty Name) -join '; '
                    Severity = $_.MsrcSeverity
                    Type = if ($_.Type -eq 1) { 'Software' } 
                          elseif ($_.Type -eq 2) { 'Driver' }
                          elseif ($_.Categories.Name -contains 'Security Updates') { 'Security' }
                          elseif ($_.Categories.Name -contains 'Feature Packs') { 'FeaturePack' }
                          else { 'Other' }
                    RebootRequired = $_.RebootRequired
                    ReleaseDate = $_.LastDeploymentChangeTime
                    Description = $_.Description
                    SupportUrl = $_.SupportUrl
                    IsBeta = $_.IsBeta
                    IsDownloaded = $_.IsDownloaded
                    Impact = $_.MsrcImpact
                }
            }

            # Apply additional filters
            if ($Severity) {
                $updates = $updates | Where-Object { $_.Severity -in $Severity }
            }
            if ($Type) {
                $updates = $updates | Where-Object { $_.Type -in $Type }
            }
            if ($RebootRequired) {
                $updates = $updates | Where-Object { $_.RebootRequired -eq $true }
            }
            if ($FromDate) {
                $updates = $updates | Where-Object { $_.ReleaseDate -ge $FromDate }
            }
            if ($ToDate) {
                $updates = $updates | Where-Object { $_.ReleaseDate -le $ToDate }
            }
            if ($MaxSize) {
                $maxBytes = $MaxSize * 1MB
                $updates = $updates | Where-Object { $_.SizeBytes -le $maxBytes }
            }

            # Apply new filters
            if ($ExcludeBeta) {
                $updates = $updates | Where-Object { -not $_.IsBeta }
            }
            if ($OnlyDownloaded) {
                $updates = $updates | Where-Object { $_.IsDownloaded }
            }
            if ($CategoryFilter) {
                $updates = $updates | Where-Object { $_.Categories -like "*$CategoryFilter*" }
            }

            # Add title filtering
            if ($Title) {
                # Check if title contains wildcard characters
                if ($Title -match '[*?]') {
                    $updates = $updates | Where-Object { $_.Title -like $Title }
                } else {
                    $updates = $updates | Where-Object { $_.Title -eq $Title }
                }
            }

            # Sort the results
            $updates = switch ($SortBy) {
                'Size' { $updates | Sort-Object { $_.SizeBytes } }
                'ReleaseDate' { $updates | Sort-Object ReleaseDate }
                'Title' { $updates | Sort-Object Title }
                'Severity' { $updates | Sort-Object { 
                    switch ($_.Severity) {
                        'Critical' { 1 }
                        'Important' { 2 }
                        'Moderate' { 3 }
                        'Low' { 4 }
                        default { 5 }
                    }
                } }
            }

            if ($Descending) {
                $updates = [array]($updates | Sort-Object -Descending)
            }

            # Add summary information
            if ($updates) {
                $totalSize = ($updates | Measure-Object -Property SizeBytes -Sum).Sum
                $totalSizeString = if ($totalSize -ge 1GB) {
                    '{0:N2} GB' -f ($totalSize / 1GB)
                } elseif ($totalSize -ge 1MB) {
                    '{0:N2} MB' -f ($totalSize / 1MB)
                } elseif ($totalSize -ge 1KB) {
                    '{0:N2} KB' -f ($totalSize / 1KB)
                } else {
                    '{0} B' -f $totalSize
                }
                
                Write-PMLog -Message ('Found {0} update(s). Total size: {1}' -f $updates.Count, $totalSizeString) -Type Info
            }
            else {
                Write-PMLog -Message 'No updates found matching the specified criteria.' -Type Info
            }
            
            return $updates
        }
        catch {
            $errorMessage = 'Error searching for updates: ' + $_.Exception.Message
            Write-PMLog -Message $errorMessage -Type Error
        }
    }
}

function Install-PMWinUpdate {
    <#
    .SYNOPSIS
        Installs specified Windows updates with advanced reboot control.
     
    .DESCRIPTION
        Downloads and installs Windows updates with granular control over reboot behavior.
        Updates can be specified by KB number, title search, or installed automatically.
        Supports recursive update cycles and custom reboot actions.
     
    .PARAMETER KB
        One or more KB article IDs to install specific updates.
     
    .PARAMETER Title
        Filter and install updates by title. Supports wildcard patterns unless -ExactMatch is specified.
     
    .PARAMETER ExactMatch
        When used with -Title, requires an exact match instead of wildcard pattern matching.
     
    .PARAMETER AcceptAll
        If specified, automatically accepts and installs all available updates.
     
    .PARAMETER RebootBehavior
        Controls how reboots are handled:
        - Never: Never reboot automatically (default)
        - IfRequired: Reboot only if required by updates
        - Always: Always reboot after installing updates
     
    .PARAMETER Repository
        Specifies the repository to use for updates. Valid values are:
        - WindowsUpdate
        - MicrosoftUpdate
     
    .EXAMPLE
        Install-PMWinUpdate -Title "Security Update for Windows"
        Installs all updates containing "Security Update for Windows" in their title.
     
    .EXAMPLE
        Install-PMWinUpdate -Title "KB5025221" -ExactMatch
        Installs the specific update matching KB5025221.
     
    .EXAMPLE
        Get-PMWinUpdate -Severity Critical | Install-PMWinUpdate -RebootBehavior IfRequired
        Installs all critical updates, rebooting if required.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName='KB')]
        [string[]]$KB,
        [Parameter(ParameterSetName='Title')]
        [string]$Title,
        [switch]$AcceptAll = $true,
        [ValidateSet('Never', 'IfRequired', 'Always')]
        [string]$RebootBehavior = 'Never',
        [switch]$ContinueAfterReboot,
        [int]$MaxCycles = 3,
        [int]$RebootTimeout = 300,
        [scriptblock]$PreRebootAction,
        [scriptblock]$PostRebootAction,
        [DateTime]$ScheduledReboot,
        [ValidateSet('WindowsUpdate', 'MicrosoftUpdate')]
        [string]$Repository = 'WindowsUpdate'
    )

    begin {
        $script:cycleCount = 0
        $script:pendingReboot = $false
        
        Write-PMLog -Message 'Initializing Windows Update installation session...' -Type Info

        try {
            $script:updateSession = New-Object -ComObject Microsoft.Update.Session
            $script:updateSearcher = $updateSession.CreateUpdateSearcher()

            # Configure repository
            if ($Repository -eq 'MicrosoftUpdate') {
                $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
                $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d"
                
                # Check if Microsoft Update is registered
                $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID }
                if (-not $Service) {
                    Write-PMLog -Message "Microsoft Update Service is not registered. Use Register-PMMicrosoftUpdate to enable it." -Type Warning
                    return
                }
                
                Write-PMLog -Message "Using Microsoft Update Service..." -Type Info
                $script:updateSearcher.ServerSelection = 3 # ssOthers
                $script:updateSearcher.ServiceID = $ServiceID
            } else {
                Write-PMLog -Message "Using Windows Update Service..." -Type Info
                $script:updateSearcher.ServerSelection = 2 # ssWindowsUpdate
            }

            $script:updateDownloader = $updateSession.CreateUpdateDownloader()
            $script:updateInstaller = $updateSession.CreateUpdateInstaller()
        }
        catch {
            Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error
            return
        }

        function Test-PendingReboot {
            $regKeys = @(
                @{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\'; Value='RebootPending'},
                @{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\'; Value='RebootRequired'}
            )
            
            foreach ($key in $regKeys) {
                if (Test-Path $key.Path) {
                    if (Get-ItemProperty -Path $key.Path -Name $key.Value -ErrorAction SilentlyContinue) {
                        return $true
                    }
                }
            }
            return $false
        }

        function Invoke-SystemReboot {
            if ($PreRebootAction) {
                Write-PMLog -Message 'Executing pre-reboot action...' -Type Info
                & $PreRebootAction
            }

            if ($ScheduledReboot) {
                $waitSeconds = [math]::Max(0, [int]($ScheduledReboot - (Get-Date)).TotalSeconds)
                if ($waitSeconds -gt 0) {
                    Write-PMLog -Message "System will reboot at $($ScheduledReboot.ToString('yyyy-MM-dd HH:mm:ss'))" -Type Warning
                    $rebootJob = Start-Job -ScriptBlock {
                        param($timeout)
                        Start-Sleep -Seconds $timeout
                        Restart-Computer -Force
                    } -ArgumentList $waitSeconds
                } else {
                    Write-PMLog -Message "Scheduled reboot time has already passed, rebooting now..." -Type Warning
                    $rebootJob = Start-Job -ScriptBlock {
                        param($timeout)
                        Start-Sleep -Seconds $timeout
                        Restart-Computer -Force
                    } -ArgumentList $RebootTimeout
                }
            } else {
                Write-PMLog -Message "System will reboot in $RebootTimeout seconds..." -Type Warning
                $rebootJob = Start-Job -ScriptBlock {
                    param($timeout)
                    Start-Sleep -Seconds $timeout
                    Restart-Computer -Force
                } -ArgumentList $RebootTimeout
            }

            if ($ContinueAfterReboot) {
                $taskName = 'PSPatchManager_ContinueUpdates'
                $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-Command `"Import-Module PSPatchManager; Install-PMWinUpdate -AcceptAll -RebootBehavior $RebootBehavior -ContinueAfterReboot -MaxCycles $MaxCycles -RebootTimeout $RebootTimeout`""
                $trigger = New-ScheduledTaskTrigger -AtStartup
                Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Force | Out-Null
            }

            $rebootJob | Wait-Job
            if ($PostRebootAction) {
                Write-PMLog -Message 'Executing post-reboot action...' -Type Info
                & $PostRebootAction
            }
        }
    }

    end {
        try {
            $script:cycleCount++
            if ($script:cycleCount -gt $MaxCycles) {
                Write-PMLog -Message 'Maximum update cycles reached.' -Type Warning
                return
            }

            Write-PMLog -Message "Retrieving available updates..." -Type Info
            try {
                $SearchResult = $script:updateSearcher.Search("IsInstalled=0 AND IsHidden=0")
                Write-PMLog -Message "Found $($SearchResult.Updates.Count) total available updates." -Type Info
                if ($Title) {
                    Write-PMLog -Message "Filtering updates with title pattern: $Title" -Type Info
                } elseif ($KB) {
                    Write-PMLog -Message "Filtering updates for KB(s): $($KB -join ', ')" -Type Info
                }
            }
            catch {
                $errorMsg = Get-WindowsUpdateErrorMessage $_.Exception.HResult
                Write-PMLog -Message "Error searching for updates: $errorMsg" -Type Error
                throw
            }

            if ($SearchResult.Updates.Count -eq 0) {
                Write-PMLog -Message "No updates available." -Type Info
                return
            }

            $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
            $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
            $totalUpdates = $SearchResult.Updates.Count
            $currentUpdate = 0

            foreach ($Update in $SearchResult.Updates) {
                $currentUpdate++
                Write-Progress -Activity 'Processing Windows Updates' -Status "Processing update $currentUpdate of $totalUpdates" -PercentComplete ($currentUpdate / $totalUpdates * 100)

                try {
                    $updateKB = $Update.KBArticleIDs | Select-Object -First 1
                    $shouldInstall = $false

                    if ($KB) {
                        $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' }
                        $shouldInstall = $updateKB -in $kbNumbers
                    }
                    elseif ($Title) {
                        # Check if title contains wildcard characters
                        if ($Title -match '[*?]') {
                            $shouldInstall = $Update.Title -like $Title
                        } else {
                            $shouldInstall = $Update.Title -eq $Title
                        }
                    }
                    else {
                        $shouldInstall = $true
                    }

                    if ($shouldInstall) {
                        if (-not $Update.EulaAccepted) {
                            $Update.AcceptEula()
                        }

                        if ($AcceptAll -or $PSCmdlet.ShouldProcess($Update.Title, 'Install update')) {
                            Write-PMLog -Message "Processing update: $($Update.Title) (KB$updateKB)" -Type Info
                            $UpdatesToInstall.Add($Update) | Out-Null
                            
                            if (-not $Update.IsDownloaded) {
                                $UpdatesToDownload.Add($Update) | Out-Null
                            }
                        }
                    }
                }
                catch {
                    Write-PMLog -Message "Error processing update $($Update.Title): $($_.Exception.Message)" -Type Warning
                    continue
                }
            }

            Write-Progress -Activity 'Processing Windows Updates' -Completed
            Write-PMLog -Message "$($UpdatesToInstall.Count) update(s) matched the specified criteria." -Type Info

            if ($UpdatesToInstall.Count -gt 0) {
                # Download updates if needed
                if ($UpdatesToDownload.Count -gt 0) {
                    Write-PMLog -Message "Downloading $($UpdatesToDownload.Count) updates..." -Type Info
                    $script:updateDownloader.Updates = $UpdatesToDownload

                    try {
                        $DownloadResult = $script:updateDownloader.Download()
                        
                        switch ($DownloadResult.ResultCode) {
                            0 { Write-PMLog -Message "Download failed." -Type Error; throw "Download failed" }
                            1 { Write-PMLog -Message "Download in progress..." -Type Info }
                            2 { Write-PMLog -Message "Download completed successfully." -Type Info }
                            3 { Write-PMLog -Message "Download completed with errors." -Type Warning }
                            4 { Write-PMLog -Message "Download cancelled." -Type Warning; throw "Download cancelled" }
                            5 { Write-PMLog -Message "Download aborted." -Type Error; throw "Download aborted" }
                            default { Write-PMLog -Message "Unknown download status: $($DownloadResult.ResultCode)" -Type Warning }
                        }

                        if ($DownloadResult.ResultCode -ne 2) {
                            throw "Download failed with status: $($DownloadResult.ResultCode)"
                        }
                    }
                    catch {
                        Write-PMLog -Message "Error during download: $($_.Exception.Message)" -Type Error
                        throw
                    }
                } else {
                    Write-PMLog -Message "All updates are already downloaded." -Type Info
                }

                Write-PMLog -Message 'Installing updates...' -Type Info
                try {
                    $script:updateInstaller.Updates = $UpdatesToInstall
                    $InstallResult = $script:updateInstaller.Install()

                    if ($InstallResult.ResultCode -ne 2) {
                        Write-PMLog -Message "Installation failed with status: $($InstallResult.ResultCode)" -Type Error
                        throw "Installation operation failed with status: $($InstallResult.ResultCode)"
                    }

                    # Get the results immediately to avoid COM object issues
                    $resultCode = $InstallResult.ResultCode.ToString()
                    $rebootRequired = $InstallResult.RebootRequired
                    $updateResults = @()

                    # Process each update result immediately
                    for ($i = 0; $i -lt $UpdatesToInstall.Count; $i++) {
                        $update = $UpdatesToInstall.Item($i)
                        $updateResult = $InstallResult.GetUpdateResult($i)
                        
                        $updateResults += [PSCustomObject]@{
                            Title = $update.Title
                            KB = $update.KBArticleIDs -join ','
                            Result = $updateResult.ResultCode.ToString()
                            Status = switch ($updateResult.ResultCode.ToString()) {
                                "0" { "Failed" }
                                "1" { "In Progress" }
                                "2" { "Success" }
                                "3" { "Partial Success" }
                                "4" { "Cancelled" }
                                "5" { "Aborted" }
                                default { "Unknown" }
                            }
                            HResult = if ($updateResult.HResult -ne 0) { "0x{0:X8}" -f $updateResult.HResult } else { "Success" }
                        }
                    }

                    Write-PMLog -Message "Installation Summary:" -Type Info
                    Write-PMLog -Message "Status: $(Get-UpdateOperationResult -ResultCode $resultCode -Operation 'Installation')" -Type Info
                    Write-PMLog -Message "Updates Processed: $($UpdatesToInstall.Count)" -Type Info
                    Write-PMLog -Message "Reboot Required: $rebootRequired" -Type Info

                    $script:pendingReboot = $rebootRequired -or (Test-PendingReboot)

                    if ($script:pendingReboot) {
                        Write-PMLog -Message 'System requires a reboot.' -Type Warning
                        switch ($RebootBehavior) {
                            'Always' { Invoke-SystemReboot }
                            'IfRequired' { 
                                if ($script:pendingReboot) { Invoke-SystemReboot }
                            }
                        }
                    }

                    return $updateResults
                }
                catch {
                    throw "Installation failed: $($_.Exception.Message)"
                }
            }
            else {
                Write-PMLog -Message "No updates matched the specified criteria." -Type Info
            }
        }
        catch {
            $errorCode = if ($_.Exception.HResult) { 
                "0x{0:X8}" -f $_.Exception.HResult 
            } else { 
                $_.Exception.Message 
            }
            Write-PMLog -Message "Error during update installation (Error code: $errorCode): $($_.Exception.Message)" -Type Error
            throw
        }
        finally {
            Write-Progress -Activity 'Processing Windows Updates' -Completed
            Write-Progress -Activity 'Downloading Updates' -Completed
        }
    }
}

function Set-PMWinUpdate {
    <#
    .SYNOPSIS
        Modifies Windows update properties such as hidden status.
     
    .DESCRIPTION
        Allows modification of Windows update properties including:
        - Hiding/Unhiding updates
        - Additional properties can be added as needed
         
        Updates can be specified by KB number or title search.
     
    .PARAMETER KB
        One or more KB article IDs of the updates to modify.
     
    .PARAMETER Title
        Filter updates by title. Supports wildcard patterns.
     
    .PARAMETER Hidden
        If specified, sets the hidden status of the matching updates.
        Use $true to hide updates, $false to unhide them.
     
    .EXAMPLE
        Set-PMWinUpdate -KB 'KB5025221' -Hidden $true
        Hides the specified update.
     
    .EXAMPLE
        Set-PMWinUpdate -KB 'KB5025221', 'KB5025222' -Hidden $false
        Unhides multiple updates.
     
    .EXAMPLE
        Set-PMWinUpdate -Title "Security Update for Windows*" -Hidden $true
        Hides all updates matching the title pattern.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName='KB')]
        [string[]]$KB,
        [Parameter(ParameterSetName='Title')]
        [string]$Title,
        [Parameter(Mandatory)]
        [bool]$Hidden
    )
    
    begin {
        Write-PMLog -Message 'Initializing Windows Update session...' -Type Info
        try {
            $updateSession = New-Object -ComObject Microsoft.Update.Session
            $updateSearcher = $updateSession.CreateUpdateSearcher()
        }
        catch {
            Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error
            return
        }
    }
    
    process {
        try {
            $searchCriteria = ""
            
            if ($KB) {
                $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' }
                $kbFilter = $kbNumbers | ForEach-Object { "KBArticleID='$_'" }
                $searchCriteria = "($($kbFilter -join ' OR '))"
            }
            elseif ($Title) {
                if ($Title -match '[*?]') {
                    # Need to search all and filter later for wildcard
                    $searchCriteria = "IsInstalled=0"
                } else {
                    $searchCriteria = "Title='$Title'"
                }
            }
            
            Write-PMLog -Message 'Searching for updates to modify...' -Type Info
            $searchResult = $updateSearcher.Search($searchCriteria)
            
            # Apply title filter for wildcards
            $updates = if ($Title -and $Title -match '[*?]') {
                $searchResult.Updates | Where-Object { $_.Title -like $Title }
            } else {
                $searchResult.Updates
            }
            
            if ($updates.Count -eq 0) {
                $criteria = if ($KB) { "KB(s): $($KB -join ', ')" } else { "Title: $Title" }
                Write-PMLog -Message "No updates found matching $criteria" -Type Warning
                return
            }

            Write-PMLog -Message "Found $($updates.Count) update(s) to process." -Type Info
            $action = if ($Hidden) { "hide" } else { "unhide" }
            
            $results = @()
            foreach ($update in $updates) {
                $updateKB = $update.KBArticleIDs -join ','
                
                if ($PSCmdlet.ShouldProcess($update.Title, "$action update")) {
                    if ($update.IsHidden -eq $Hidden) {
                        $status = if ($Hidden) { "already hidden" } else { "already visible" }
                        Write-PMLog -Message "Update $($update.Title) (KB$updateKB) is $status" -Type Info
                    } else {
                        Write-PMLog -Message "$(if ($Hidden) { 'Hiding' } else { 'Unhiding' }) update: $($update.Title) (KB$updateKB)" -Type Info
                        $update.IsHidden = $Hidden
                    }
                    
                    $results += [PSCustomObject]@{
                        Title = $update.Title
                        KB = $updateKB
                        Result = "2" # Success code for consistency
                        Status = if ($Hidden) { "Hidden" } else { "Unhidden" }
                        HResult = "Success"
                    }
                }
            }
            
            Write-PMLog -Message "Successfully processed $action requests." -Type Info
            return $results
        }
        catch {
            if ($_.Exception.HResult -eq 0x80240032) {
                $criteria = if ($KB) { "KB(s): $($KB -join ', ')" } else { "Title: $Title" }
                Write-PMLog -Message "No updates found matching $criteria" -Type Warning
                return
            }
            Write-PMLog -Message "Error modifying updates: $($_.Exception.Message)" -Type Error
            throw
        }
    }
}

function Invoke-PMWinUpdate {
    <#
    .SYNOPSIS
        Manages Windows updates on remote computers without requiring module installation on remote systems.
     
    .DESCRIPTION
        Installs Windows updates on one or more remote computers with detailed progress tracking and reporting.
        Does not require PSPatchManager to be installed on remote systems as all necessary code is included
        in the remote execution context.
     
    .PARAMETER ComputerName
        One or more computer names to manage updates on.
     
    .PARAMETER Credential
        Optional credentials to use for remote connections.
     
    .EXAMPLE
        Invoke-PMWinUpdate -ComputerName 'Server1'
        Installs updates on a single remote computer.
     
    .EXAMPLE
        Invoke-PMWinUpdate -ComputerName 'Server1', 'Server2' -Credential (Get-Credential)
        Installs updates on multiple servers using specified credentials.
     
    .NOTES
        - Does not require PSPatchManager to be installed on remote computers
        - Provides detailed progress and error reporting
        - Returns detailed update installation results
        - Requires WinRM to be enabled on remote systems
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$ComputerName,
        [System.Management.Automation.PSCredential]
        $Credential
    )
    
    end {
        foreach ($Computer in $ComputerName) {
            try {
                Write-PMLog -Message "Connecting to $Computer..." -Type Info
                
                # Define the remote scriptblock with embedded functions
                $remoteScript = {
                    function Write-RemoteLog {
                        param([string]$Message, [string]$Type = 'Info')
                        switch ($Type) {
                            'Info' { Write-Verbose $Message -Verbose }
                            'Warning' { Write-Warning $Message }
                            'Error' { Write-Error $Message }
                        }
                    }

                    try {
                        Write-RemoteLog "Initializing Windows Update session..."
                        $UpdateSession = New-Object -ComObject Microsoft.Update.Session
                        $UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
                        
                        Write-RemoteLog "Searching for updates..."
                        $SearchResult = $UpdateSearcher.Search("IsInstalled=0")
                        
                        if ($SearchResult.Updates.Count -gt 0) {
                            $UpdateDownloader = $UpdateSession.CreateUpdateDownloader()
                            $UpdateInstaller = $UpdateSession.CreateUpdateInstaller()
                            
                            $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
                            
                            Write-RemoteLog "Processing $($SearchResult.Updates.Count) updates..."
                            foreach ($Update in $SearchResult.Updates) {
                                if (-not $Update.EulaAccepted) {
                                    $Update.AcceptEula()
                                }
                                Write-RemoteLog "Adding update: $($Update.Title)"
                                $UpdatesToInstall.Add($Update) | Out-Null
                            }
                            
                            if ($UpdatesToInstall.Count -gt 0) {
                                Write-RemoteLog "Downloading updates..."
                                $UpdateDownloader.Updates = $UpdatesToInstall
                                $DownloadResult = $UpdateDownloader.Download()

                                if ($DownloadResult.ResultCode -eq 2) { # Success
                                    Write-RemoteLog "Installing updates..."
                                    $UpdateInstaller.Updates = $UpdatesToInstall
                                    $InstallResult = $UpdateInstaller.Install()

                                    # Get the results immediately to avoid COM object issues
                                    $resultCode = $InstallResult.ResultCode.ToString()
                                    $rebootRequired = $InstallResult.RebootRequired
                                    $updateResults = @()

                                    # Process each update result immediately
                                    for ($i = 0; $i -lt $UpdatesToInstall.Count; $i++) {
                                        $update = $UpdatesToInstall.Item($i)
                                        $updateResult = $InstallResult.GetUpdateResult($i)
                                        
                                        $updateResults += @{
                                            Title = $update.Title
                                            KB = $update.KBArticleIDs -join ','
                                            Result = Get-UpdateOperationResult -ResultCode $updateResult.ResultCode.ToString() -Operation "Update"
                                            HResult = if ($updateResult.HResult -ne 0) { 
                                                "0x{0:X8}" -f $updateResult.HResult 
                                            } else { 
                                                "Success" 
                                            }
                                        }
                                    }

                                    $result = @{
                                        ComputerName = $env:COMPUTERNAME
                                        UpdateCount = $SearchResult.Updates.Count
                                        Result = Get-UpdateOperationResult -ResultCode $resultCode -Operation "Installation"
                                        RebootRequired = $rebootRequired
                                        Updates = $updateResults
                                    }

                                }
                                else {
                                    Write-RemoteLog "Download failed with status: $($DownloadResult.ResultCode)" -Type Error
                                    throw "Download operation failed with status: $($DownloadResult.ResultCode)"
                                }
                            }
                        }
                        else {
                            $result = @{
                                ComputerName = $env:COMPUTERNAME
                                UpdateCount = 0
                                Result = "No updates found"
                                RebootRequired = $false
                                Updates = @()
                            }
                        }
                        
                        return $result
                    }
                    catch {
                        Write-RemoteLog "Error: $_" -Type Error
                        throw $_
                    }
                }

                $params = @{
                    ComputerName = $Computer
                    ScriptBlock = $remoteScript
                }
                
                if ($Credential) {
                    $params['Credential'] = $Credential
                }
                
                $result = Invoke-Command @params
                Write-PMLog -Message "Completed update process on $Computer" -Type Info
                
                # Format and output the result
                [PSCustomObject]@{
                    ComputerName = $result.ComputerName
                    UpdateCount = $result.UpdateCount
                    Result = $result.Result
                    RebootRequired = $result.RebootRequired
                    Updates = $result.Updates
                }
            }
            catch {
                Write-PMLog -Message "Error processing updates on $Computer`: $($_.Exception.Message)" -Type Error
            }
        }
    }
}

function Download-PMWinUpdate {
    <#
    .SYNOPSIS
        Downloads Windows updates without installing them.
     
    .DESCRIPTION
        Downloads Windows updates to prestage them on the system without installation.
        Updates can be specified by KB number, title search, or downloaded automatically.
     
    .PARAMETER KB
        One or more KB article IDs to download specific updates.
     
    .PARAMETER Title
        Filter and download updates by title. Supports wildcard patterns unless -ExactMatch is specified.
     
    .PARAMETER ExactMatch
        When used with -Title, requires an exact match instead of wildcard pattern matching.
     
    .PARAMETER Type
        Filter updates by type (Software, Driver, Security, FeaturePack).
     
    .PARAMETER Severity
        Filter updates by severity (Critical, Important, Moderate, Low).
     
    .PARAMETER RebootRequired
        Filter updates by reboot requirement.
     
    .PARAMETER ReleaseDate
        Filter updates by release date.
     
    .PARAMETER AcceptAll
        If specified, automatically accepts and downloads all available updates.
     
    .EXAMPLE
        Download-PMWinUpdate -Title "Security Update for Windows"
        Downloads all updates containing "Security Update for Windows" in their title.
     
    .EXAMPLE
        Download-PMWinUpdate -Title "KB5025221" -ExactMatch
        Downloads the specific update matching KB5025221.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName='KB')]
        [string[]]$KB,
        [Parameter(ParameterSetName='Title')]
        [string]$Title,
        [ValidateSet('Software', 'Driver', 'Security', 'FeaturePack')]
        [string]$Type = '',
        [ValidateSet('Critical', 'Important', 'Moderate', 'Low', 'Unspecified')]
        [string]$Severity = '',
        [bool]$RebootRequired,
        [DateTime]$ReleaseDate,
        [switch]$AcceptAll = $true
    )

    begin {
        $script:updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
        
        Write-PMLog -Message 'Initializing Windows Update download session...' -Type Info
        try {
            $script:updateSession = New-Object -ComObject Microsoft.Update.Session
            $script:updateSearcher = $updateSession.CreateUpdateSearcher()
            $script:updateDownloader = $updateSession.CreateUpdateDownloader()
        }
        catch {
            Write-PMLog -Message "Failed to initialize Windows Update session: $($_.Exception.Message)" -Type Error
            return
        }
    }

    end {
        try {
            Write-PMLog -Message "Retrieving available updates..." -Type Info
            $SearchResult = $script:updateSearcher.Search("IsInstalled=0 AND IsHidden=0")
            Write-PMLog -Message "Found $($SearchResult.Updates.Count) total available updates." -Type Info

            # Display filter criteria if any are set
            $activeFilters = @()
            if ($Title) { 
                $activeFilters += "Title: '$Title'" + $(if ($Title -match '[*?]') { " (Wildcard)" } else { " (Exact Match)" }) 
            }
            if ($KB) { $activeFilters += "KB: $($KB -join ', ')" }
            if ($Type) { $activeFilters += "Type: $Type" }
            if ($Severity) { $activeFilters += "Severity: $Severity" }
            if ($PSBoundParameters.ContainsKey('RebootRequired')) { $activeFilters += "RebootRequired: $RebootRequired" }
            if ($ReleaseDate) { $activeFilters += "ReleaseDate: >= $($ReleaseDate.ToString('yyyy-MM-dd'))" }

            if ($activeFilters.Count -gt 0) {
                Write-PMLog -Message "Active filters:`n $($activeFilters -join "`n ")" -Type Info
            }

            foreach ($Update in $SearchResult.Updates) {
                $updateKB = $Update.KBArticleIDs | Select-Object -First 1
                $shouldDownload = $false

                # Apply filters
                $matchesFilters = (!$Type -or $Update.Type -eq $Type) -and
                                (!$Severity -or $Update.MsrcSeverity -eq $Severity) -and
                                (!$PSBoundParameters.ContainsKey('RebootRequired') -or $Update.RebootRequired -eq $RebootRequired) -and
                                (!$ReleaseDate -or $Update.LastDeploymentChangeTime -ge $ReleaseDate)

                if ($KB) {
                    $kbNumbers = $KB | ForEach-Object { $_ -replace '^KB', '' }
                    $shouldDownload = $updateKB -in $kbNumbers -and $matchesFilters
                }
                elseif ($Title) {
                    # Check if title contains wildcard characters
                    if ($Title -match '[*?]') {
                        $shouldDownload = $Update.Title -like $Title -and $matchesFilters
                    } else {
                        $shouldDownload = $Update.Title -eq $Title -and $matchesFilters
                    }
                }
                else {
                    $shouldDownload = $matchesFilters
                }

                if ($shouldDownload) {
                    if ($Update.IsDownloaded) {
                        Write-PMLog -Message "Update already downloaded: $($Update.Title)" -Type Info
                        continue
                    }
                    if (-not $Update.EulaAccepted) {
                        $Update.AcceptEula()
                    }
                    if ($AcceptAll -or $PSCmdlet.ShouldProcess($Update.Title, 'Download update')) {
                        Write-PMLog -Message "Adding update for download: $($Update.Title)" -Type Info
                        $script:updatesToDownload.Add($Update) | Out-Null
                    }
                }
            }

            $alreadyDownloaded = $SearchResult.Updates | Where-Object { 
                $_.IsDownloaded -and (
                    ($KB -and ($_.KBArticleIDs | Where-Object { $KB -contains "KB$_" })) -or
                    ($Title -and $(if ($Title -match '[*?]') { $_.Title -like $Title } else { $_.Title -eq $Title }))
                )
            }

            if ($alreadyDownloaded) {
                Write-PMLog -Message "`nAlready downloaded updates:" -Type Info
                foreach ($update in $alreadyDownloaded) {
                    Write-PMLog -Message " $($update.Title) (KB$($update.KBArticleIDs -join ','))" -Type Info
                }
            }

            Write-PMLog -Message "`n$($script:updatesToDownload.Count) update(s) require download." -Type Info

            if ($script:updatesToDownload.Count -gt 0) {
                Write-PMLog -Message "`nDownload Summary:" -Type Info
                Write-PMLog -Message "Total updates to download: $($script:updatesToDownload.Count)" -Type Info
                Write-PMLog -Message "Starting download process...`n" -Type Info
                
                try {
                    $script:updateDownloader.Updates = $script:updatesToDownload
                    $DownloadResult = $script:updateDownloader.Download()

                    $resultCode = $DownloadResult.ResultCode.ToString()
                    
                    switch ($resultCode) {
                        "0" { Write-PMLog -Message "Download failed." -Type Error; throw "Download failed" }
                        "1" { Write-PMLog -Message "Download in progress..." -Type Info }
                        "2" { Write-PMLog -Message "Download completed successfully." -Type Info }
                        "3" { Write-PMLog -Message "Download completed with errors." -Type Warning }
                        "4" { Write-PMLog -Message "Download cancelled." -Type Warning; throw "Download cancelled" }
                        "5" { Write-PMLog -Message "Download aborted." -Type Error; throw "Download aborted" }
                        default { Write-PMLog -Message "Unknown download result: $resultCode" -Type Warning }
                    }

                    # Return download results
                    $script:updatesToDownload | ForEach-Object {
                        [PSCustomObject]@{
                            Title = $_.Title
                            KB = $_.KBArticleIDs -join ','
                            Result = $resultCode
                            Status = switch ($resultCode) {
                                "0" { "Failed" }
                                "1" { "In Progress" }
                                "2" { "Success" }
                                "3" { "Partial Success" }
                                "4" { "Cancelled" }
                                "5" { "Aborted" }
                                default { "Unknown" }
                            }
                        }
                    }
                }
                catch {
                    Write-PMLog -Message "Error during update download: $($_.Exception.Message)" -Type Error
                    throw
                }
            }
            else {
                Write-PMLog -Message "No updates matched the specified criteria." -Type Info
            }
        }
        catch {
            Write-PMLog -Message "Download failed: $($_.Exception.Message)" -Type Error
            throw
        }
        finally {
            Write-Progress -Activity 'Processing Windows Updates' -Completed
            Write-Progress -Activity 'Downloading Updates' -Completed
        }
    }
}

function Register-PMMicrosoftUpdate {
    <#
    .SYNOPSIS
        Registers the system with Microsoft Update Service.
     
    .DESCRIPTION
        Enables Microsoft Update Service on the system, allowing access to both Windows Updates
        and updates for other Microsoft products.
     
    .EXAMPLE
        Register-PMMicrosoftUpdate
        Registers the system with Microsoft Update Service.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param()
    
    begin {
        Write-PMLog -Message 'Initializing Microsoft Update registration...' -Type Info
    }
    
    process {
        try {
            $ServiceManager = New-Object -ComObject Microsoft.Update.ServiceManager
            $ServiceID = "7971f918-a847-4430-9279-4a52d1efe18d"
            
            if ($PSCmdlet.ShouldProcess("Microsoft Update Service", "Register")) {
                # Check if already registered
                $Service = $ServiceManager.Services | Where-Object { $_.ServiceID -eq $ServiceID }
                if ($Service) {
                    Write-PMLog -Message "System is already registered with Microsoft Update Service." -Type Info
                    return $true
                }
                
                Write-PMLog -Message "Registering system with Microsoft Update Service..." -Type Info
                $AddServiceResult = $ServiceManager.AddService2($ServiceID, 7, "")
                Write-PMLog -Message "Successfully registered with Microsoft Update Service." -Type Info
                return $true
            }
        }
        catch {
            Write-PMLog -Message "Failed to register Microsoft Update Service: $($_.Exception.Message)" -Type Error
            return $false
        }
    }
}

# Export functions
Export-ModuleMember -Function Get-PMWinUpdate, Install-PMWinUpdate, Set-PMWinUpdate, Invoke-PMWinUpdate, Download-PMWinUpdate, Register-PMMicrosoftUpdate