Private/Export/Export-HierarchicalCsv.ps1

function Export-HierarchicalCsv {
    <#
    .SYNOPSIS
    Exports a hierarchical view of initiatives and their member policies.
     
    .DESCRIPTION
    Creates a two-level hierarchical CSV where each initiative is followed by
    its member policies. Includes comparison status, versions, effects, and lifecycle info.
     
    This provides a comprehensive view of all assignments with their policy details
    in a single denormalized file.
     
    ⚡ OPTIMIZED VERSION - Uses hashtable indexes for O(1) lookups instead of Where-Object.
     
    .PARAMETER Summary
    Array of summary objects from comparisons.
     
    .PARAMETER Details
    Array of detail objects (Missing, Extra, VersionMismatch).
     
    .PARAMETER Baseline
    Array of baseline policy objects.
     
    .PARAMETER InitiativesFolder
    Path to folder containing Initiative-{Name}-Policies.csv files.
     
    .PARAMETER PolicyDefinitionCache
    Hashtable cache for policy definitions.
     
    .PARAMETER OutputPath
    Full path to the output CSV file.
     
    .EXAMPLE
    Export-HierarchicalCsv -Summary $summary `
                           -Details $details `
                           -Baseline $baseline `
                           -InitiativesFolder $initiativesFolder `
                           -PolicyDefinitionCache $script:policyDefinitionCache `
                           -OutputPath "C:\Reports\Hierarchical_Initiatives_Policies.csv"
     
    Exports hierarchical view to specified CSV file.
     
    .OUTPUTS
    None. Writes CSV file to disk with initiative and policy rows.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Summary,
        
        [Parameter(Mandatory)]
        [object[]]$Details,
        
        [Parameter(Mandatory)]
        [object[]]$Baseline,
        
        [Parameter(Mandatory)]
        [string]$InitiativesFolder,
        
        [Parameter(Mandatory)]
        [hashtable]$PolicyDefinitionCache,
        
        [Parameter(Mandatory)]
        [string]$OutputPath
    )
    
    Write-Verbose "Building hierarchical CSV..."
    
    # ========== ✅ OPTIMISATION: Créer des index AVANT la boucle ==========
    Write-Verbose " Building Details and Baseline indexes for fast lookups..."
    
    # ✅ Index des Details par "AssignmentName|PolicyDefinitionId"
    $detailsIndex = @{}
    foreach ($detail in $Details) {
        $key = "$($detail.AssignmentName)|$($detail.PolicyDefinitionId)"
        if (-not $detailsIndex.ContainsKey($key)) {
            $detailsIndex[$key] = $detail
        }
    }
    Write-Verbose " ├─ Details index: $($detailsIndex.Count) entries"
    
    # ✅ Index du Baseline par PolicyDefinitionId
    $baselineIndex = @{}
    foreach ($bp in $Baseline) {
        if (-not $baselineIndex.ContainsKey($bp.PolicyDefinitionId)) {
            $baselineIndex[$bp.PolicyDefinitionId] = $bp
        }
    }
    Write-Verbose " └─ Baseline index: $($baselineIndex.Count) entries"
    
    # ✅ Cache des CSV déjà lus (évite Import-Csv répétés)
    $csvCache = @{}
    
    $hierarchicalRows = @()
    
    # Process initiatives only (exclude individual policies)
    $summaryInitiatives = $Summary | Where-Object { -not $_.IsIndividualPolicy }
    
    Write-Verbose " Processing $($summaryInitiatives.Count) initiatives..."
    
    $progressCounter = 0
    foreach ($summaryRow in $summaryInitiatives) {
        $progressCounter++
        
        # Progress every 10 initiatives
        if ($progressCounter % 10 -eq 0 -or $progressCounter -eq $summaryInitiatives.Count) {
            Write-Progress -Activity "Generating Hierarchical CSV" `
                           -Status "Processing initiative $progressCounter of $($summaryInitiatives.Count)" `
                           -PercentComplete (($progressCounter / $summaryInitiatives.Count) * 100)
        }
        
        # Add initiative row
        $hierarchicalRows += [PSCustomObject]@{
            Type               = "Initiative"
            InitiativeName     = $summaryRow.AssignmentDisplayName
            InitiativeScope    = $summaryRow.AssignmentScope
            InitiativeId       = $summaryRow.InitiativeDefinitionId
            TotalPolicies      = $summaryRow.TotalPolicies_Assignment
            Matched            = $summaryRow.InCommon
            Missing            = $summaryRow.TotalPolicies_Assignment - $summaryRow.InCommon - $summaryRow.ExtraInAssignment_Count
            Extra              = $summaryRow.ExtraInAssignment_Count
            VersionMismatches  = $summaryRow.VersionDiffs_Count
            PolicyName         = ""
            PolicyId           = ""
            Effect             = ""
            BaselineSource     = ""
            Version            = ""
            Status             = ""
            DeployedVersion    = ""
            Lifecycle          = ""
            Category           = "Uncategorized"
        }
        
        # Add policy rows
        $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($summaryRow.AssignmentName)-Policies.csv"
        
        if (Test-Path $initiativeCsvPath) {
            # ✅ OPTIMISATION: Lire CSV une seule fois et le mettre en cache
            if (-not $csvCache.ContainsKey($initiativeCsvPath)) {
                $csvCache[$initiativeCsvPath] = Import-Csv $initiativeCsvPath
                Write-Debug "Cached CSV: $initiativeCsvPath"
            }
            
            $initiativePolicies = $csvCache[$initiativeCsvPath]
            
            foreach ($policy in $initiativePolicies) {
                # ✅ OPTIMISÉ: Lookup O(1) au lieu de Where-Object O(n)
                $detailKey = "$($summaryRow.AssignmentName)|$($policy.PolicyDefinitionId)"
                $policyDetail = $detailsIndex[$detailKey]
                
                $baselinePolicy = $baselineIndex[$policy.PolicyDefinitionId]
                
                # Status WITH icons (will be properly encoded in UTF-8 with BOM)
                $status = if ($policyDetail) {
                    switch ($policyDetail.DifferenceType) {
                        'MissingFromAssignment' { '❌ Missing' }
                        'ExtraInAssignment'     { 'ℹ️ Extra' }
                        'VersionMismatch'       { '⚠️ Version Mismatch' }
                        default                 { '✅ Match' }
                    }
                } else {
                    '✅ Match'
                }
                
                # Resolve effect
                $effect = "N/A"
                if ($policy.Effect -and $policy.Effect -ne "N/A") {
                    $effect = $policy.Effect
                }
                elseif ($baselinePolicy -and $baselinePolicy.Effect -and $baselinePolicy.Effect -ne "N/A") {
                    $effect = $baselinePolicy.Effect
                }
                else {
                    # Fallback: try to load from cache
                    try {
                        $pol = Get-PolicyDefinitionCached -PolicyDefinitionId $policy.PolicyDefinitionId `
                                                          -Cache $PolicyDefinitionCache
                        $effect = Resolve-PolicyEffect -Ref $null `
                                                       -PolicyDefinition $pol `
                                                       -PolicyDisplayName $pol.DisplayName `
                                                       -PolicyDefinitionId $pol.Id
                    } catch {
                        Write-Debug "Cannot load policy to resolve effect: $($policy.PolicyDefinitionId)"
                        $effect = "N/A"
                    }
                }
                
                $hierarchicalRows += [PSCustomObject]@{
                    Type               = "Policy"
                    InitiativeName     = $summaryRow.AssignmentDisplayName
                    InitiativeScope    = ""
                    InitiativeId       = ""
                    TotalPolicies      = ""
                    Matched            = ""
                    Missing            = ""
                    Extra              = ""
                    VersionMismatches  = ""
                    PolicyName         = $policy.PolicyDisplayName
                    PolicyId           = $policy.PolicyDefinitionId
                    Effect             = $effect
                    BaselineSource     = if ($policyDetail) { $policyDetail.BaselineSources } elseif ($baselinePolicy) { $baselinePolicy.BaselineSources } else { "Custom" }
                    Version            = if ($policyDetail) { $policyDetail.BaselineVersion } elseif ($baselinePolicy) { $baselinePolicy.Version } else { $policy.Version }
                    Status             = $status
                    DeployedVersion    = $policy.Version
                    Lifecycle          = if ($policy.PolicyDisplayName -match '\[?Deprecated\]?|\(Deprecated\)') { 'Deprecated' } 
                                         elseif ($policy.PolicyDisplayName -match '\[?Preview\]?|\(Preview\)') { 'Preview' } 
                                         else { '' }
                    Category           = if ($policy.Category -and $policy.Category.Trim() -ne '') { 
                             $policy.Category 
                         } elseif ($baselinePolicy -and $baselinePolicy.Category -and $baselinePolicy.Category.Trim() -ne '') { 
                             $baselinePolicy.Category 
                         } else { 
                             "Uncategorized" 
                         }
                }
            }
        } else {
            Write-Warning "Initiative CSV not found: $initiativeCsvPath"
        }
    }
    
    Write-Progress -Activity "Generating Hierarchical CSV" -Completed
    
    # ✅ Nettoyer le cache CSV pour libérer la mémoire
    $csvCache.Clear()
    
    try {
        # Force UTF-8 with BOM for proper Unicode icon rendering
        # PowerShell 7+: Use utf8BOM, PowerShell 5.1: UTF8 already includes BOM
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $hierarchicalRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8BOM
        } else {
            # PowerShell 5.1: UTF8 = UTF-8 with BOM by default
            $hierarchicalRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
        }
        
        Write-Host ("✅ Hierarchical CSV exported: {0}" -f (Split-Path $OutputPath -Leaf)) -ForegroundColor Green
        Write-Host (" ├─ Total rows: {0}" -f $hierarchicalRows.Count) -ForegroundColor DarkCyan
        Write-Host (" ├─ Initiatives: {0}" -f ($hierarchicalRows | Where-Object { $_.Type -eq 'Initiative' }).Count) -ForegroundColor DarkCyan
        Write-Host (" └─ Policies: {0}" -f ($hierarchicalRows | Where-Object { $_.Type -eq 'Policy' }).Count) -ForegroundColor DarkCyan
    }
    catch {
        Write-Warning "Failed to export hierarchical CSV: $($_.Exception.Message)"
    }
}