Private/Output/Write-DiagnosticOutput.ps1

function Write-DiagnosticOutput {
    <#
    .SYNOPSIS
        Writes detailed diagnostic information about Conditional Access policy evaluation.

    .DESCRIPTION
        This function provides rich, color-coded diagnostic information about the evaluation
        of Conditional Access policies. It can be used to troubleshoot policy evaluation
        results and understand why policies are or are not applying to a sign-in scenario.

    .PARAMETER PolicyId
        The ID of the policy being evaluated.

    .PARAMETER PolicyName
        The display name of the policy being evaluated.

    .PARAMETER Stage
        The evaluation stage (e.g., UserExclusion, UserInclusion, NetworkCheck).

    .PARAMETER Result
        The result of the evaluation (true/false).

    .PARAMETER Message
        Additional details about the evaluation.

    .PARAMETER Details
        An object containing detailed information about the evaluation.

    .PARAMETER Level
        The level of the diagnostic message (Info, Warning, Error, Success).

    .PARAMETER ExportPath
        When specified, diagnostics are also written to this file path.

    .PARAMETER Source
        The source component generating the diagnostic message. Alternative to PolicyId for utility functions.

    .EXAMPLE
        Write-DiagnosticOutput -PolicyId "123" -PolicyName "MFA Policy" -Stage "UserExclusion" -Result $false -Message "User not excluded" -Level "Info"

    .EXAMPLE
        Write-DiagnosticOutput -Source "Get-CAPolicy" -Message "Retrieving all policies from Microsoft Graph" -Level "Info"
    #>

    [CmdletBinding(DefaultParameterSetName = 'Policy')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Policy')]
        [string]$PolicyId,

        [Parameter(Mandatory = $false, ParameterSetName = 'Policy')]
        [string]$PolicyName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Policy')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Utility')]
        [string]$Stage,

        [Parameter(Mandatory = $true, ParameterSetName = 'Policy')]
        [Parameter(Mandatory = $false, ParameterSetName = 'Utility')]
        [bool]$Result,

        [Parameter(Mandatory = $false, ParameterSetName = 'Policy')]
        [Parameter(Mandatory = $true, ParameterSetName = 'Utility')]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        [object]$Details,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Info', 'Warning', 'Error', 'Success')]
        [string]$Level = 'Info',

        [Parameter(Mandatory = $false)]
        [string]$ExportPath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Utility')]
        [string]$Source
    )

    # Only proceed if diagnostic output is enabled via Verbose stream
    if (-not $VerbosePreference -or $VerbosePreference -eq 'SilentlyContinue') {
        return
    }

    # Set colors based on level and result
    $headerColor = switch ($Level) {
        'Info' { 'Cyan' }
        'Warning' { 'Yellow' }
        'Error' { 'Red' }
        'Success' { 'Green' }
        default { 'White' }
    }

    $resultColor = if ($Result) { 'Green' } else { 'Yellow' }
    $detailColor = 'Gray'

    # Format the output
    $outputLines = @()

    if ($PSCmdlet.ParameterSetName -eq 'Policy') {
        # Build the header with policy info
        $policyInfo = if ($PolicyName) { "[$PolicyId] $PolicyName" } else { "[$PolicyId]" }
        $outputLines += "Policy $policyInfo - Stage: $Stage"
        $outputLines += "Result: $(if ($Result) { "PASS" } else { "FAIL" })"

        if ($Message) {
            $outputLines += "Message: $Message"
        }
    }
    else {
        # Utility function diagnostics
        $outputLines += "[$Source] $Message"
    }

    # Add detailed output if provided
    if ($Details) {
        $outputLines += "Details:"

        # Serialize details object for display (extract key properties)
        if ($Details -is [System.Collections.IDictionary] -or $Details -is [PSCustomObject]) {
            foreach ($key in $Details.Keys) {
                $value = $Details[$key]
                $outputLines += " ${key}: ${value}"
            }
        }
        elseif ($Details -is [System.Collections.IEnumerable] -and $Details -isnot [string]) {
            $i = 0
            foreach ($item in $Details) {
                $outputLines += " [${i}] ${item}"
                $i++
            }
        }
        else {
            $outputLines += " $Details"
        }
    }

    # Output to console with colors
    Write-Verbose "--- DIAGNOSTIC: $Level ---" -Verbose

    foreach ($line in $outputLines) {
        Write-Verbose $line -Verbose
    }

    Write-Verbose "-----------------------" -Verbose

    # Export to file if path provided
    if ($ExportPath) {
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

        if ($PSCmdlet.ParameterSetName -eq 'Policy') {
            $logLine = "[$timestamp] $Level - Policy $policyInfo - Stage: $Stage - Result: $(if ($Result) { "PASS" } else { "FAIL" })"
            if ($Message) {
                $logLine += " - Message: $Message"
            }
        }
        else {
            $logLine = "[$timestamp] $Level - [$Source] $Message"
        }

        if ($Details) {
            $detailsJson = $Details | ConvertTo-Json -Compress
            $logLine += " - Details: $detailsJson"
        }

        try {
            Add-Content -Path $ExportPath -Value $logLine -ErrorAction Stop
        }
        catch {
            Write-Warning "Failed to write diagnostic output to file: $_"
        }
    }
}

function Export-DiagnosticReport {
    <#
    .SYNOPSIS
        Exports a comprehensive diagnostic report for Conditional Access policy evaluation.

    .DESCRIPTION
        This function exports detailed information about the evaluation of Conditional Access policies
        to a file, including all evaluation details, conditions, and reasons for the result.

    .PARAMETER Results
        The detailed results from Invoke-CAWhatIf.

    .PARAMETER Path
        The file path to export the report to.

    .PARAMETER Format
        The format of the export (JSON, CSV, XML).

    .EXAMPLE
        Export-DiagnosticReport -Results $results -Path "C:\Temp\ca-diagnostic-report.json" -Format "JSON"
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]$Results,

        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [ValidateSet('JSON', 'CSV', 'XML')]
        [string]$Format = 'JSON'
    )

    process {
        try {
            # Create a report object with metadata
            $report = @{
                Metadata        = @{
                    GeneratedAt  = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                    GeneratedBy  = $env:USERNAME
                    ComputerName = $env:COMPUTERNAME
                }
                Summary         = @{
                    TotalPolicies       = $Results.DetailedResults.Count
                    ApplicablePolicies  = ($Results.DetailedResults | Where-Object { $_.Applies -eq $true }).Count
                    BlockingPolicies    = ($Results.DetailedResults | Where-Object { $_.AccessResult -eq "Blocked" }).Count
                    ConditionalPolicies = ($Results.DetailedResults | Where-Object { $_.AccessResult -eq "ConditionallyGranted" }).Count
                    GrantingPolicies    = ($Results.DetailedResults | Where-Object { $_.AccessResult -eq "Granted" }).Count
                    AccessAllowed       = $Results.AccessAllowed
                    RequiredControls    = $Results.RequiredControls
                    SessionControls     = $Results.SessionControls
                }
                DetailedResults = $Results.DetailedResults | ForEach-Object {
                    # Enrich each policy result with additional diagnostic details
                    $evaluationReasons = @()

                    # Add reasons for the policy not applying
                    if (-not $_.Applies) {
                        if (-not $_.EvaluationDetails.UserInScope) {
                            $evaluationReasons += "User not in scope: $($_.EvaluationDetails.Reasons.User)"
                        }
                        if (-not $_.EvaluationDetails.ResourceInScope) {
                            $evaluationReasons += "Resource not in scope: $($_.EvaluationDetails.Reasons.Resource)"
                        }
                        if (-not $_.EvaluationDetails.NetworkInScope) {
                            $evaluationReasons += "Network not in scope: $($_.EvaluationDetails.Reasons.Network)"
                        }
                        if (-not $_.EvaluationDetails.ClientAppInScope) {
                            $evaluationReasons += "Client app not in scope: $($_.EvaluationDetails.Reasons.ClientApp)"
                        }
                        if (-not $_.EvaluationDetails.DevicePlatformInScope) {
                            $evaluationReasons += "Device platform not in scope: $($_.EvaluationDetails.Reasons.DevicePlatform)"
                        }
                        if (-not $_.EvaluationDetails.DeviceStateInScope) {
                            $evaluationReasons += "Device state not in scope: $($_.EvaluationDetails.Reasons.DeviceState)"
                        }
                        if (-not $_.EvaluationDetails.UserRiskLevelInScope) {
                            $evaluationReasons += "User risk level not in scope: $($_.EvaluationDetails.Reasons.UserRiskLevel)"
                        }
                        if (-not $_.EvaluationDetails.SignInRiskLevelInScope) {
                            $evaluationReasons += "Sign-in risk level not in scope: $($_.EvaluationDetails.Reasons.SignInRiskLevel)"
                        }
                    }
                    else {
                        # Add access result and controls
                        $evaluationReasons += "Access result: $($_.AccessResult)"
                        if ($_.GrantControlsRequired.Count -gt 0) {
                            $evaluationReasons += "Required controls: $($_.GrantControlsRequired -join ', ')"
                        }
                        if ($_.SessionControlsApplied.Count -gt 0) {
                            $evaluationReasons += "Session controls: $($_.SessionControlsApplied -join ', ')"
                        }
                    }

                    # Return enriched object
                    return @{
                        PolicyId          = $_.PolicyId
                        DisplayName       = $_.DisplayName
                        State             = $_.State
                        Applies           = $_.Applies
                        AccessResult      = $_.AccessResult
                        EvaluationDetails = $_.EvaluationDetails
                        DiagnosticReasons = $evaluationReasons
                    }
                }
            }

            # Export in the desired format
            switch ($Format) {
                'JSON' {
                    $report | ConvertTo-Json -Depth 10 | Out-File -FilePath $Path -Encoding utf8
                }
                'CSV' {
                    # Convert to a flattened structure for CSV
                    $csvRecords = $report.DetailedResults | ForEach-Object {
                        $flatRecord = @{
                            PolicyId               = $_.PolicyId
                            DisplayName            = $_.DisplayName
                            State                  = $_.State
                            Applies                = $_.Applies
                            AccessResult           = $_.AccessResult
                            UserInScope            = $_.EvaluationDetails.UserInScope
                            ResourceInScope        = $_.EvaluationDetails.ResourceInScope
                            NetworkInScope         = $_.EvaluationDetails.NetworkInScope
                            ClientAppInScope       = $_.EvaluationDetails.ClientAppInScope
                            DevicePlatformInScope  = $_.EvaluationDetails.DevicePlatformInScope
                            DeviceStateInScope     = $_.EvaluationDetails.DeviceStateInScope
                            UserRiskLevelInScope   = $_.EvaluationDetails.UserRiskLevelInScope
                            SignInRiskLevelInScope = $_.EvaluationDetails.SignInRiskLevelInScope
                            DiagnosticReasons      = ($_.DiagnosticReasons -join "; ")
                        }
                        return [PSCustomObject]$flatRecord
                    }
                    $csvRecords | Export-Csv -Path $Path -NoTypeInformation -Encoding utf8
                }
                'XML' {
                    $report | Export-Clixml -Path $Path
                }
            }

            Write-Verbose "Diagnostic report exported to $Path"
            return $Path
        }
        catch {
            Write-Error "Failed to export diagnostic report: $_"
        }
    }
}

Export-ModuleMember -Function Write-DiagnosticOutput, Export-DiagnosticReport