Private/Invoke-WUSetupDiag.ps1

function Invoke-WUSetupDiag {
    <#
    .SYNOPSIS
        Downloads, executes, and parses Microsoft SetupDiag tool for Windows upgrade analysis.
 
    .DESCRIPTION
        Comprehensive wrapper for Microsoft SetupDiag tool that handles download, execution,
        and parsing of results. SetupDiag analyzes Windows Setup log files to diagnose
        upgrade failures and provides detailed failure analysis.
 
    .PARAMETER OutputPath
        Directory path where SetupDiag will be downloaded and results saved.
 
    .PARAMETER LogPath
        Path to the log file for detailed logging.
 
    .PARAMETER SetupDiagPath
        Custom path to existing SetupDiag.exe. If not provided, will download latest version.
 
    .PARAMETER OutputFormat
        Output format for SetupDiag results. Default is JSON for better parsing.
 
    .EXAMPLE
        $setupDiagResults = Invoke-WUSetupDiag -OutputPath "C:\Temp" -LogPath "C:\Logs\wu.log"
 
    .NOTES
        This is a private function used internally by the WindowsUpdateTools module.
        Requires .NET Framework 4.7.2+ for SetupDiag functionality.
        Returns detailed analysis results from SetupDiag execution.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$OutputPath,

        [string]$LogPath,

        [string]$SetupDiagPath,

        [ValidateSet('JSON', 'XML', 'Text')]
        [string]$OutputFormat = 'JSON'
    )

    Write-WULog -Message "Starting SetupDiag analysis" -LogPath $LogPath

    # Initialize results object
    $results = [PSCustomObject]@{
        ExecutionSuccessful = $false
        FailuresDetected = 0
        FailureReasons = @()
        SystemInfo = $null
        RawOutput = $null
        OutputFile = $null
        UpgradeAttemptDetected = $false
        Issues = @()
        ExecutionTime = $null
        SetupDiagVersion = $null
        LogAnalysis = @()
        ErrorMessage = $null
    }

    try {
        # Check .NET Framework version first
        $dotNetVersion = Get-WUDotNetVersion
        Write-WULog -Message ".NET Framework Version: $dotNetVersion" -LogPath $LogPath

        if ($dotNetVersion -lt [Version]"4.7.2") {
            $errorMsg = ".NET Framework 4.7.2 or newer required for SetupDiag (found: $dotNetVersion)"
            Write-WULog -Message $errorMsg -Level Warning -LogPath $LogPath
            $results.Issues += $errorMsg
            $results.ErrorMessage = $errorMsg
            return $results
        }

        # Check for Windows Setup logs availability
        $logsFound = Test-WUSetupLogAvailability -LogPath $LogPath
        if (-not $logsFound) {
            $errorMsg = "No Windows Setup logs found - SetupDiag requires upgrade attempt logs"
            Write-WULog -Message $errorMsg -Level Warning -LogPath $LogPath
            $results.Issues += $errorMsg
            $results.ErrorMessage = $errorMsg
            return $results
        }

        # Get or download SetupDiag executable
        if (-not $SetupDiagPath) {
            $SetupDiagPath = Get-WUSetupDiagExecutable -OutputPath $OutputPath -LogPath $LogPath
        }

        if (-not $SetupDiagPath -or -not (Test-Path $SetupDiagPath)) {
            $errorMsg = "SetupDiag executable not available"
            Write-WULog -Message $errorMsg -Level Error -LogPath $LogPath
            $results.ErrorMessage = $errorMsg
            return $results
        }

        Write-WULog -Message "SetupDiag path validated: $SetupDiagPath" -LogPath $LogPath

        # Prepare output file
        $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
        $outputExtension = switch ($OutputFormat) {
            'JSON' { 'json' }
            'XML' { 'xml' }
            'Text' { 'txt' }
        }
        $outputFile = Join-Path $OutputPath "SetupDiagResults-$timestamp.$outputExtension"
        $results.OutputFile = $outputFile

        # Prepare SetupDiag arguments
        $setupDiagArgs = @(
            "/Output:$outputFile",
            "/Format:$($OutputFormat.ToLower())",
            "/ZipLogs:False"
        )

        Write-WULog -Message "Running SetupDiag with output: $outputFile" -LogPath $LogPath
        Write-WULog -Message "SetupDiag arguments: $($setupDiagArgs -join ' ')" -LogPath $LogPath

        # Execute SetupDiag
        $startTime = Get-Date
        Write-WULog -Message "Starting SetupDiag execution at $($startTime.ToString('HH:mm:ss')) - this may take several minutes" -LogPath $LogPath

        try {
            $process = Start-Process -FilePath $SetupDiagPath -ArgumentList $setupDiagArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput (Join-Path $OutputPath "setupdiag-stdout.txt") -RedirectStandardError (Join-Path $OutputPath "setupdiag-stderr.txt")
            $exitCode = $process.ExitCode
            $endTime = Get-Date
            $executionTime = $endTime - $startTime
            $results.ExecutionTime = $executionTime

            Write-WULog -Message "SetupDiag completed in $($executionTime.ToString('mm\:ss')) with exit code: $exitCode" -LogPath $LogPath

            # Handle different exit codes
            switch ($exitCode) {
                0 { 
                    if (Test-Path $outputFile) {
                        Write-WULog -Message "SetupDiag completed successfully" -LogPath $LogPath
                        $results.ExecutionSuccessful = $true
                    } else {
                        Write-WULog -Message "SetupDiag exit code 0 but no output file created" -Level Warning -LogPath $LogPath
                        $results.Issues += "SetupDiag completed but no output file was created"
                    }
                }
                -2146233079 { 
                    Write-WULog -Message "SetupDiag .NET runtime exception - possible missing .NET 4.7.2+ or corrupted installation" -Level Error -LogPath $LogPath
                    $results.Issues += "SetupDiag .NET runtime error"
                    $results.ErrorMessage = "SetupDiag .NET runtime exception"
                }
                -2147467259 { 
                    Write-WULog -Message "SetupDiag access denied - insufficient permissions" -Level Error -LogPath $LogPath
                    $results.Issues += "SetupDiag access denied"
                    $results.ErrorMessage = "SetupDiag access denied"
                }
                default {
                    if (Test-Path $outputFile) {
                        Write-WULog -Message "SetupDiag exited with code: $exitCode - but created output file" -Level Warning -LogPath $LogPath
                        $results.ExecutionSuccessful = $true
                        $results.Issues += "SetupDiag completed with warnings (exit code: $exitCode)"
                    } else {
                        Write-WULog -Message "SetupDiag exited with code: $exitCode - no output file created" -Level Error -LogPath $LogPath
                        $results.Issues += "SetupDiag failed with exit code: $exitCode"
                        $results.ErrorMessage = "SetupDiag failed with exit code: $exitCode"
                    }
                }
            }

            # Check for stdout/stderr output
            $stderrFile = Join-Path $OutputPath "setupdiag-stderr.txt"
            
            if (Test-Path $stderrFile) {
                $stderrContent = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue
                if ($stderrContent -and $stderrContent.Trim()) {
                    Write-WULog -Message "SetupDiag stderr output: $stderrContent" -Level Warning -LogPath $LogPath
                }
            }

        }
        catch {
            Write-WULog -Message "Failed to execute SetupDiag: $($_.Exception.Message)" -Level Error -LogPath $LogPath
            $results.ErrorMessage = "Failed to execute SetupDiag: $($_.Exception.Message)"
            return $results
        }

        # Parse results if output file was created
        if ($results.ExecutionSuccessful -and (Test-Path $outputFile)) {
            Write-WULog -Message "Parsing SetupDiag results from: $outputFile" -LogPath $LogPath
            
            try {
                if ($OutputFormat -eq 'JSON') {
                    $parseResults = ConvertFrom-WUSetupDiagJSON -FilePath $outputFile -LogPath $LogPath
                } else {
                    $parseResults = ConvertFrom-WUSetupDiagText -FilePath $outputFile -LogPath $LogPath
                }

                # Merge parse results
                $results.FailuresDetected = $parseResults.FailuresDetected
                $results.FailureReasons = $parseResults.FailureReasons
                $results.SystemInfo = $parseResults.SystemInfo
                $results.RawOutput = $parseResults.RawOutput
                $results.UpgradeAttemptDetected = $parseResults.UpgradeAttemptDetected
                $results.LogAnalysis = $parseResults.LogAnalysis
                $results.SetupDiagVersion = $parseResults.SetupDiagVersion

                Write-WULog -Message "SetupDiag analysis completed - found $($results.FailuresDetected) failure(s)" -LogPath $LogPath
                
                if ($results.FailureReasons.Count -gt 0) {
                    Write-WULog -Message "Failure reasons identified:" -LogPath $LogPath
                    foreach ($reason in $results.FailureReasons) {
                        Write-WULog -Message " - $reason" -Level Warning -LogPath $LogPath
                    }
                }

            }
            catch {
                Write-WULog -Message "Error parsing SetupDiag results: $($_.Exception.Message)" -Level Error -LogPath $LogPath
                $results.Issues += "Error parsing SetupDiag results"
                $results.ErrorMessage = "Error parsing SetupDiag results: $($_.Exception.Message)"
            }
        }

    }
    catch {
        Write-WULog -Message "Critical error during SetupDiag analysis: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        $results.ErrorMessage = "Critical error during SetupDiag analysis: $($_.Exception.Message)"
    }

    # Summary
    if ($results.ExecutionSuccessful) {
        Write-WULog -Message "SetupDiag analysis completed successfully" -LogPath $LogPath
        if ($results.FailuresDetected -gt 0) {
            Write-WULog -Message "SetupDiag detected $($results.FailuresDetected) upgrade failure(s)" -LogPath $LogPath
        } else {
            Write-WULog -Message "SetupDiag did not detect any upgrade failures" -LogPath $LogPath
        }
    } else {
        Write-WULog -Message "SetupDiag analysis failed: $($results.ErrorMessage)" -Level Error -LogPath $LogPath
    }

    return $results
}

function Test-WUSetupLogAvailability {
    <#
    .SYNOPSIS
        Tests availability of Windows Setup log files for SetupDiag analysis.
    #>

    param([string]$LogPath)

    Write-WULog -Message "Checking Windows Setup log availability for SetupDiag analysis" -LogPath $LogPath
    
    $foundAnyLogs = $false
    
    foreach ($path in $script:SetupLogPaths) {
        if (Test-Path $path) {
            $logFiles = Get-ChildItem -Path $path -Filter "*.log" -ErrorAction SilentlyContinue
            if ($logFiles) {
                Write-WULog -Message "Found $($logFiles.Count) log files in: $path" -LogPath $LogPath
                $foundAnyLogs = $true
                
                # Check for recent logs (less than 90 days old)
                $recentLogs = $logFiles | Where-Object { (Get-Date) - $_.LastWriteTime -lt [TimeSpan]::FromDays(90) }
                if ($recentLogs) {
                    Write-WULog -Message " $($recentLogs.Count) recent logs (< 90 days old)" -LogPath $LogPath
                }
            }
        }
    }
    
    if (-not $foundAnyLogs) {
        Write-WULog -Message "No Windows Setup logs found in standard locations" -LogPath $LogPath
        Write-WULog -Message "This typically indicates no recent Windows upgrade attempts" -LogPath $LogPath
    }

    return $foundAnyLogs
}

function Get-WUSetupDiagExecutable {
    <#
    .SYNOPSIS
        Downloads the latest SetupDiag executable from Microsoft.
    #>

    param(
        [string]$OutputPath,
        [string]$LogPath
    )

    $downloadUrl = $script:SetupDiagUrl
    $destination = Join-Path $OutputPath "SetupDiag.exe"
    
    Write-WULog -Message "Attempting to download SetupDiag from: $downloadUrl" -LogPath $LogPath
    
    try {
        # Check if we already have a recent version
        if (Test-Path $destination) {
            $fileInfo = Get-Item $destination
            $fileAge = (Get-Date) - $fileInfo.LastWriteTime
            if ($fileAge.TotalDays -lt 7) {
                Write-WULog -Message "Using existing SetupDiag (less than 7 days old)" -LogPath $LogPath
                return $destination
            } else {
                Write-WULog -Message "Existing SetupDiag is $([math]::Round($fileAge.TotalDays)) days old - downloading newer version" -LogPath $LogPath
            }
        }

        # Ensure output directory exists
        if (-not (Test-Path $OutputPath)) {
            New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
        }

        # Download with progress suppression for cleaner logs
        $progressPreference = 'SilentlyContinue'
        Invoke-WebRequest -Uri $downloadUrl -OutFile $destination -UseBasicParsing -ErrorAction Stop

        # Verify download
        if (Test-Path $destination) {
            $fileSize = (Get-Item $destination).Length
            Write-WULog -Message "SetupDiag downloaded successfully ($([math]::Round($fileSize / 1KB)) KB)" -LogPath $LogPath
            return $destination
        } else {
            throw "Download completed but file not found at destination"
        }
    }
    catch {
        Write-WULog -Message "Failed to download SetupDiag: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        return $null
    }
}

function ConvertFrom-WUSetupDiagJSON {
    <#
    .SYNOPSIS
        Parses SetupDiag JSON output for failure analysis.
    #>

    param(
        [string]$FilePath,
        [string]$LogPath
    )

    $parseResults = [PSCustomObject]@{
        FailuresDetected = 0
        FailureReasons = @()
        SystemInfo = $null
        RawOutput = $null
        UpgradeAttemptDetected = $false
        LogAnalysis = @()
        SetupDiagVersion = $null
    }

    try {
        $jsonContent = Get-Content $FilePath -Raw | ConvertFrom-Json
        $parseResults.RawOutput = $jsonContent

        # Extract system information
        if ($jsonContent.SystemInfo) {
            $parseResults.SystemInfo = $jsonContent.SystemInfo
            $parseResults.UpgradeAttemptDetected = $true
            
            Write-WULog -Message "SetupDiag System Info - Host: $($jsonContent.SystemInfo.HostOSVersion), Target: $($jsonContent.SystemInfo.TargetOSBuildString)" -LogPath $LogPath
        }

        # Check for failure data
        if ($jsonContent.FailureData -and $jsonContent.FailureData.Count -gt 0) {
            $parseResults.FailuresDetected = $jsonContent.FailureData.Count
            $parseResults.FailureReasons = $jsonContent.FailureData
            
            Write-WULog -Message "SetupDiag detected $($parseResults.FailuresDetected) failure(s)" -LogPath $LogPath
        }

        # Check rules for additional context
        if ($jsonContent.Rules -and $jsonContent.Rules.Count -gt 0) {
            $highSeverityRules = $jsonContent.Rules | Where-Object { $_.Severity -gt 2 }
            if ($highSeverityRules -and -not $parseResults.FailureReasons) {
                $parseResults.FailureReasons = $highSeverityRules | ForEach-Object { $_.Description }
                $parseResults.FailuresDetected = $highSeverityRules.Count
            }
        }

        # Check for remediation suggestions
        if ($jsonContent.Remediation) {
            $parseResults.FailureReasons += $jsonContent.Remediation
            if ($parseResults.FailuresDetected -eq 0) {
                $parseResults.FailuresDetected = 1
            }
        }

        # Check version information
        if ($jsonContent.Version) {
            $parseResults.SetupDiagVersion = $jsonContent.Version
        }

    }
    catch {
        Write-WULog -Message "Error parsing SetupDiag JSON: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        throw
    }

    return $parseResults
}

function ConvertFrom-WUSetupDiagText {
    <#
    .SYNOPSIS
        Parses SetupDiag text output for failure analysis.
    #>

    param(
        [string]$FilePath,
        [string]$LogPath
    )

    $parseResults = [PSCustomObject]@{
        FailuresDetected = 0
        FailureReasons = @()
        SystemInfo = $null
        RawOutput = $null
        UpgradeAttemptDetected = $false
        LogAnalysis = @()
        SetupDiagVersion = $null
    }

    try {
        $textContent = Get-Content $FilePath -Raw
        $parseResults.RawOutput = $textContent

        # Look for failure indicators in text
        $failureLines = $textContent -split "`n" | Where-Object { 
            $_ -like "*failure*" -or $_ -like "*error*" -or $_ -like "*failed*" 
        }

        if ($failureLines) {
            $parseResults.FailuresDetected = $failureLines.Count
            $parseResults.FailureReasons = $failureLines | ForEach-Object { $_.Trim() }
        }

        # Check for upgrade attempt indicators
        if ($textContent -like "*upgrade*" -or $textContent -like "*setup*") {
            $parseResults.UpgradeAttemptDetected = $true
        }

    }
    catch {
        Write-WULog -Message "Error parsing SetupDiag text: $($_.Exception.Message)" -Level Error -LogPath $LogPath
        throw
    }

    return $parseResults
}

function Get-WUDotNetVersion {
    <#
    .SYNOPSIS
        Gets the installed .NET Framework version.
     
    .DESCRIPTION
        Determines the highest .NET Framework version installed on the system.
    #>

    
    try {
        $netVersions = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP" -Recurse -ErrorAction SilentlyContinue |
            Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue |
            Where-Object { $_.PSChildName -match '^(?!S)\d+' } |
            Sort-Object Version -Descending |
            Select-Object -First 1

        if ($netVersions.Release -ge 528040) { return [Version]"4.8" }
        elseif ($netVersions.Release -ge 461808) { return [Version]"4.7.2" }
        elseif ($netVersions.Release -ge 460798) { return [Version]"4.7" }
        elseif ($netVersions.Release -ge 394802) { return [Version]"4.6.2" }
        elseif ($netVersions.Release -ge 394254) { return [Version]"4.6.1" }
        elseif ($netVersions.Release -ge 393295) { return [Version]"4.6" }
        else { return [Version]$netVersions.Version }
    }
    catch {
        return [Version]"0.0"
    }
}