Public/Export-PolicyReport.ps1

function Export-PolicyReport {
    <#
    .SYNOPSIS
        Export policy comparison reports in multiple formats (CSV, HTML).
     
    .DESCRIPTION
        Generates comprehensive policy compliance reports from comparison results.
         
        Supports multiple export formats:
        - CSV: Multiple files (baseline, summary, details, hierarchical, per-initiative)
        - HTML: Interactive report with DataTables, scoring gauges, and visual metrics
        - All: Both CSV and HTML formats
     
    .PARAMETER ComparisonResult
        Comparison result object from Compare-AzPolicyCompliance.
        Must contain .Summary, .Details, .Scores, and .Metrics properties.
     
    .PARAMETER Baseline
        Baseline object from Get-AzPolicyBaseline.
        Required for baseline CSV export and HTML report.
     
    .PARAMETER OutputFolder
        Destination folder for report files.
        Subfolders will be created: reports/, baseline/, initiatives/
     
    .PARAMETER Format
        Report format to generate: CSV, HTML, or All.
        Default: All
     
    .PARAMETER HtmlOptions
        Hashtable with HTML report customization options:
        - LogoPath: Path to logo image file
        - ProjectName: Project name for header
        - ProjectVersion: Version string
        - Theme: Color theme (not yet implemented)
        - Scope: Scope description (e.g., "Management Group: MyMG")
     
    .PARAMETER AssignedPoliciesById
        Hashtable of assigned policies indexed by ID (for HTML report).
        Optional. If not provided, will be computed from comparison result.
     
    .PARAMETER AssignedPoliciesByNormName
        Hashtable of assigned policies indexed by normalized name (for HTML report).
        Optional. If not provided, will be computed from comparison result.
     
    .PARAMETER AssignedEffects
        Hashtable of assigned policy effects (for HTML report).
        Optional. If not provided, will be computed from comparison result.
     
    .PARAMETER MatchByNameOnly
        Boolean indicating if comparison used name-only matching.
        Passed to HTML report for display.
        Default: $false
     
    .EXAMPLE
        # Export all formats with default options
        $result = Compare-AzPolicyCompliance -Baseline $baseline -Assignments $assignments
         
        $files = Export-PolicyReport -ComparisonResult $result `
                                     -Baseline $baseline `
                                     -OutputFolder "C:\Reports" `
                                     -Format All
         
        Write-Host "Generated files:"
        $files | ForEach-Object { Write-Host " $_" }
     
    .EXAMPLE
        # Export only CSV files
        Export-PolicyReport -ComparisonResult $result `
                            -Baseline $baseline `
                            -OutputFolder "C:\Reports" `
                            -Format CSV
     
    .EXAMPLE
        # Export HTML with custom branding
        $htmlOptions = @{
            LogoPath = "C:\Assets\company-logo.png"
            ProjectName = "Enterprise Policy Compliance"
            ProjectVersion = "2.0"
            Scope = "Production Tenant"
        }
         
        Export-PolicyReport -ComparisonResult $result `
                            -Baseline $baseline `
                            -OutputFolder "C:\Reports" `
                            -Format HTML `
                            -HtmlOptions $htmlOptions
     
    .OUTPUTS
        Array of generated file paths (absolute paths).
     
    .NOTES
        File Structure:
        - OutputFolder/
          ├── baseline/
          │ └── Baseline_ALZ_MCSB_Policies.csv
          ├── reports/
          │ ├── Baseline_Compare_Summary.csv
          │ ├── Baseline_Compare_Details.csv
          │ ├── Hierarchical_Initiatives_Policies.csv
          │ └── Baseline_Compare_Report.html
          └── initiatives/
              ├── Initiative-{Name1}-Policies.csv
              ├── Initiative-{Name2}-Policies.csv
              └── ...
         
        CSV Formats:
        - Baseline: All baseline policies with sources
        - Summary: Per-assignment metrics (one row per assignment)
        - Details: Policy-level differences (missing, extra, version mismatches)
        - Hierarchical: Two-level view (initiative rows + policy rows)
        - Per-Initiative: Individual CSV per assignment with expanded policies
         
        HTML Report Sections:
        - Hero score gauge with color coding
        - Metrics cards (ALZ Score, MCSB Score, Assignments, Baseline Policies)
        - Custom Initiatives table
        - MCSB Initiatives table
        - Individual Policies table
        - MCSB Baseline table
        - ALZ Baseline table
        - Interactive DataTables with sorting/filtering
    #>

    
    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]$ComparisonResult,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]$Baseline,
        
        [Parameter()]
        [AllowNull()]
        [object[]]$Assignments = @(),
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputFolder,
        
        [ValidateSet('CSV', 'HTML', 'All')]
        [string]$Format = 'All',
        
        [hashtable]$HtmlOptions = @{},
        
        [hashtable]$AssignedPoliciesById,
        [hashtable]$AssignedPoliciesByNormName,
        [hashtable]$AssignedEffects,
        
        [bool]$MatchByNameOnly = $false
    )
    
    begin {
        Write-Debug "Export-PolicyReport: Starting report generation"
        Write-Debug " OutputFolder: $OutputFolder"
        Write-Debug " Format: $Format"
        
        # Validate comparison result structure
        if ($null -eq $ComparisonResult.Summary -or $null -eq $ComparisonResult.Details) {
            throw "Invalid ComparisonResult object. Must contain .Summary and .Details properties."
        }
        
        # Warn if data is empty but continue
        if ($ComparisonResult.Summary.Count -eq 0) {
            Write-Warning "Summary is empty. No policy assignments were successfully compared."
        }
        
        # Validate baseline structure
        if (-not $Baseline.Policies) {
            throw "Invalid Baseline object. Must contain .Policies property."
        }
    }
    
    process {
        try {
            $generatedFiles = @()
            
            # Create output subfolders
            $reportsFolder = Join-Path $OutputFolder "reports"
            $baselineFolder = Join-Path $OutputFolder "baseline"
            $initiativesFolder = Join-Path $OutputFolder "initiatives"
            
            @($reportsFolder, $baselineFolder, $initiativesFolder) | ForEach-Object {
                if (-not (Test-Path $_)) {
                    New-Item -Path $_ -ItemType Directory -Force | Out-Null
                    Write-Verbose "Created folder: $_"
                }
            }
            
            # Define output file paths
            $baselineCsvPath = Join-Path $baselineFolder "Baseline_ALZ_MCSB_Policies.csv"
            $summaryCsvPath = Join-Path $reportsFolder "Baseline_Compare_Summary.csv"
            $detailsCsvPath = Join-Path $reportsFolder "Baseline_Compare_Details.csv"
            $hierarchicalCsvPath = Join-Path $reportsFolder "Hierarchical_Initiatives_Policies.csv"
            $htmlReportPath = Join-Path $reportsFolder "Baseline_Compare_Report.html"
            
            # Export CSV files
            if ($Format -eq 'CSV' -or $Format -eq 'All') {
                Write-Verbose "Generating CSV reports..."
                
                # 1. Export baseline
                Write-Verbose " Exporting baseline CSV..."
                Export-BaselineCsv -Baseline $Baseline.Policies -OutputPath $baselineCsvPath
                $generatedFiles += $baselineCsvPath
                
                # 2. Export summary
                Write-Verbose " Exporting summary CSV..."
                Export-SummaryCsv -Summary $ComparisonResult.Summary -OutputPath $summaryCsvPath
                $generatedFiles += $summaryCsvPath
                
                # 3. Export details
                Write-Verbose " Exporting details CSV..."
                $allDetails = @()
                $allDetails += $ComparisonResult.Details.Missing
                $allDetails += $ComparisonResult.Details.Extra
                $allDetails += $ComparisonResult.Details.VersionMismatches
                
                Export-DetailsCsv -Details $allDetails -OutputPath $detailsCsvPath
                $generatedFiles += $detailsCsvPath
                
                # 4. Export per-initiative CSVs
                Write-Verbose " Exporting per-initiative CSVs..."
                foreach ($summaryRow in $ComparisonResult.Summary) {
                    # Find corresponding assignment to get expanded policies
                    # Note: We need the original assignment object with ExpandedPolicies
                    # For now, we'll skip this as we don't have access to original assignments
                    # This would be populated during comparison if we stored it
                    
                    # TODO: Store ExpandedPolicies in summary or pass assignments array
                    Write-Debug "Skipping per-initiative CSV for $($summaryRow.AssignmentDisplayName) - requires ExpandedPolicies"
                }
                
                # 5. Export hierarchical view
                Write-Verbose " Exporting hierarchical CSV..."
                Export-HierarchicalCsv -Summary $ComparisonResult.Summary `
                                       -Details $allDetails `
                                       -Baseline $Baseline.Policies `
                                       -InitiativesFolder $initiativesFolder `
                                       -PolicyDefinitionCache $script:policyDefinitionCache `
                                       -OutputPath $hierarchicalCsvPath
                $generatedFiles += $hierarchicalCsvPath
                
                Write-Verbose "CSV reports generated: $($generatedFiles.Count) files"
            }
            
            # Export HTML report
            if ($Format -eq 'HTML' -or $Format -eq 'All') {
                Write-Verbose "Generating HTML report..."
                
                # Prepare HTML options with defaults

# Auto-detect logo path
if ($HtmlOptions.ContainsKey('LogoPath') -and $HtmlOptions.LogoPath) {
    # Custom logo provided
    $logoPath = $HtmlOptions.LogoPath
    Write-Verbose "Using custom logo: $logoPath"
} else {

$publicFolder = $PSScriptRoot  
$moduleFolder = Split-Path -Parent $publicFolder  
$modulesFolder = Split-Path -Parent $moduleFolder
$srcFolder = Split-Path -Parent $modulesFolder 
$projectRoot = Split-Path -Parent $srcFolder

$logoPath = Join-Path $projectRoot "assets\logo-azurepolicywatch.png"
    
    if (-not (Test-Path $logoPath)) {
        Write-Warning "Default logo not found at: $logoPath"
        Write-Verbose "HTML report will use default icon (🔍)"
        $logoPath = ""
    } else {
        Write-Verbose "✅ Using default logo: $logoPath"
    }
}

$projectName = if ($HtmlOptions.ContainsKey('ProjectName')) { $HtmlOptions.ProjectName } else { "AzurePolicyWatch" }
$projectVersion = if ($HtmlOptions.ContainsKey('ProjectVersion')) { $HtmlOptions.ProjectVersion } else { "1.0" }
$scope = if ($HtmlOptions.ContainsKey('Scope')) { $HtmlOptions.Scope } else { "Azure Tenant" }
                
                # Compute assigned policy indexes if not provided
                if (-not $AssignedPoliciesById) {
                    $AssignedPoliciesById = @{}
                    # Build from Assignments.ExpandedPolicies (ALL assigned policies, not just those in baseline)
                    if ($Assignments -and $Assignments.Count -gt 0) {
                        foreach ($assignment in $Assignments) {
                            if ($assignment.ExpandedPolicies) {
                                foreach ($policy in $assignment.ExpandedPolicies) {
                                    if ($policy.PolicyDefinitionId -and -not $AssignedPoliciesById.ContainsKey($policy.PolicyDefinitionId)) {
                                        $AssignedPoliciesById[$policy.PolicyDefinitionId] = $policy
                                    }
                                }
                            }
                        }
                    }
                    
                    Write-Verbose "Built AssignedPoliciesById from Assignments: $($AssignedPoliciesById.Count) policies"
                }
                
                if (-not $AssignedPoliciesByNormName) {
                    $AssignedPoliciesByNormName = @{}
                    # Build from Assignments.ExpandedPolicies
                    if ($Assignments -and $Assignments.Count -gt 0) {
                        foreach ($assignment in $Assignments) {
                            if ($assignment.ExpandedPolicies) {
                                foreach ($policy in $assignment.ExpandedPolicies) {
                                    $normName = Normalize-PolicyName $policy.PolicyDisplayName
                                    if ($normName -and -not $AssignedPoliciesByNormName.ContainsKey($normName)) {
                                        $AssignedPoliciesByNormName[$normName] = $policy
                                    }
                                }
                            }
                        }
                    }
                    
                    Write-Verbose "Built AssignedPoliciesByNormName from Assignments: $($AssignedPoliciesByNormName.Count) policies"
                }
                
                if (-not $AssignedEffects) {
                    $AssignedEffects = @{}
                    # Build from Assignments.ExpandedPolicies
                    if ($Assignments -and $Assignments.Count -gt 0) {
                        foreach ($assignment in $Assignments) {
                            if ($assignment.ExpandedPolicies) {
                                foreach ($policy in $assignment.ExpandedPolicies) {
                                    if ($policy.PolicyDefinitionId -and $policy.Effect) {
                                        $AssignedEffects[$policy.PolicyDefinitionId] = $policy.Effect
                                    }
                                    $normName = Normalize-PolicyName $policy.PolicyDisplayName
                                    if ($normName -and $policy.Effect -and -not $AssignedEffects.ContainsKey($normName)) {
                                        $AssignedEffects[$normName] = $policy.Effect
                                    }
                                }
                            }
                        }
                    }
                    
                    Write-Verbose "Built AssignedEffects from Assignments: $($AssignedEffects.Count) entries"
                }
                
                # Build comprehensive metrics object for HTML
                $metrics = @{
                    TotalAssignments = $ComparisonResult.Summary.Count
                    TotalBaseline = $Baseline.Policies.Count
                    AlzMetrics = $ComparisonResult.Metrics.AlzMetrics
                    McsbMetrics = $ComparisonResult.Metrics.McsbMetrics
                    GlobalMetrics = $ComparisonResult.Metrics.GlobalMetrics
                }
                
                # Export HTML
                Export-HtmlReport -Summary $ComparisonResult.Summary `
                                  -Baseline $Baseline.Policies `
                                  -Scores $ComparisonResult.Scores `
                                  -Metrics $metrics `
                                  -AssignedById $AssignedPoliciesById `
                                  -AssignedByNormName $AssignedPoliciesByNormName `
                                  -AssignedEffects $AssignedEffects `
                                  -MatchByNameOnly $MatchByNameOnly `
                                  -OutputPath $htmlReportPath `
                                  -LogoPath $logoPath `
                                  -ProjectName $projectName `
                                  -ProjectVersion $projectVersion `
                                  -Scope $scope
                
                $generatedFiles += $htmlReportPath
                Write-Verbose "HTML report generated: $htmlReportPath"
            }
            
            Write-Debug "Export-PolicyReport: Completed successfully"
            Write-Verbose "Total files generated: $($generatedFiles.Count)"
            
            return $generatedFiles
            
        } catch {
            Write-Error "Failed to export policy report: $($_.Exception.Message)"
            throw
        }
    }
}