Private/Comparison/Calculate-ComplianceScores.ps1

function Calculate-ComplianceScores {
    <#
    .SYNOPSIS
    Calculates compliance scores for ALZ, MCSB, and overall baseline coverage.
     
    .DESCRIPTION
    Analyzes which baseline policies are actually deployed by examining all
    assignments and their expanded policies. Computes separate scores for:
    - ALZ baseline coverage
    - MCSB baseline coverage
    - Global (combined) compliance score
     
    This function reads initiative CSV files to determine which policies are deployed.
     
    ⚡ OPTIMIZED VERSION - Uses normalized indexes for O(1) lookups instead of O(n) loops.
     
    .PARAMETER Baseline
    Array of baseline policy objects with BaselineSources property.
     
    .PARAMETER Summary
    Array of summary objects from assignments (with IsIndividualPolicy flag).
     
    .PARAMETER InitiativesFolder
    Path to folder containing Initiative-{Name}-Policies.csv files.
     
    .PARAMETER PolicyDefinitionCache
    Hashtable cache for policy definitions (for individual policies).
     
    .EXAMPLE
    $scores = Calculate-ComplianceScores -Baseline $baseline `
                                         -Summary $summary `
                                         -InitiativesFolder $initiativesFolder `
                                         -PolicyDefinitionCache $script:policyDefinitionCache
     
    Returns PSCustomObject with AlzScore, McsbScore, GlobalScore, and detailed metrics.
     
    .OUTPUTS
    PSCustomObject with properties:
    - AlzScore: Percentage of ALZ baseline deployed (0-100)
    - McsbScore: Percentage of MCSB baseline deployed (0-100)
    - GlobalScore: Percentage of total baseline deployed (0-100)
    - AlzMetrics: Hashtable with BaselineCount, Deployed, Missing
    - McsbMetrics: Hashtable with BaselineCount, Deployed, Missing
    - GlobalMetrics: Hashtable with TotalBaseline, TotalDeployed, ComplianceLevel
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Baseline,
        
        [Parameter()]
        [AllowEmptyCollection()]
        [object[]]$Summary = @(),
        
        [Parameter(Mandatory)]
        [string]$InitiativesFolder,
        
        [Parameter(Mandatory)]
        [hashtable]$PolicyDefinitionCache
    )
    
    Write-Host "📊 Computing baseline coverage scores..." -ForegroundColor Cyan
    
    # ========== ✅ OPTIMISATION: Build Baseline Indexes for O(1) lookups ==========
    Write-Verbose "Building baseline policy indexes..."
    $alzPoliciesIndex = @{}
    $mcsbPoliciesIndex = @{}
    
    # ✅ NOUVEAU: Créer des index normalisés UNE SEULE FOIS
    $alzNormalizedIndex = @{}
    $mcsbNormalizedIndex = @{}
    
    foreach ($policy in $Baseline) {
        if ($policy.BaselineSources -like "*ALZ*") {
            $alzPoliciesIndex[$policy.PolicyDefinitionId] = $policy
            
            # Ajouter à l'index normalisé
            $normName = Normalize-PolicyName $policy.PolicyDisplayName
            if ($normName -and -not $alzNormalizedIndex.ContainsKey($normName)) {
                $alzNormalizedIndex[$normName] = $policy.PolicyDefinitionId
            }
        }
        if ($policy.BaselineSources -like "*MCSB*") {
            $mcsbPoliciesIndex[$policy.PolicyDefinitionId] = $policy
            
            # Ajouter à l'index normalisé
            $normName = Normalize-PolicyName $policy.PolicyDisplayName
            if ($normName -and -not $mcsbNormalizedIndex.ContainsKey($normName)) {
                $mcsbNormalizedIndex[$normName] = $policy.PolicyDefinitionId
            }
        }
    }
    
    Write-Verbose " ALZ index: $($alzPoliciesIndex.Count) policies, $($alzNormalizedIndex.Count) normalized names"
    Write-Verbose " MCSB index: $($mcsbPoliciesIndex.Count) policies, $($mcsbNormalizedIndex.Count) normalized names"
    
    # ========== ALZ Score ==========
    Write-Host " ├─ Analyzing ALZ baseline coverage..." -ForegroundColor DarkCyan
    
    $alzBaselineCount = $alzPoliciesIndex.Count
    $alzMatchedPoliciesSet = [System.Collections.Generic.HashSet[string]]::new()
    
    # ✅ OPTIMISATION: Cache des CSV déjà lus
    $csvCache = @{}
    
    foreach ($sumItem in $Summary) {
        $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($sumItem.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 ($pol in $initiativePolicies) {
                # ✅ Match by ID first (O(1) lookup)
                if ($alzPoliciesIndex.ContainsKey($pol.PolicyDefinitionId)) {
                    [void]$alzMatchedPoliciesSet.Add($pol.PolicyDefinitionId)
                    Write-Debug "ALZ match by ID: $($pol.PolicyDefinitionId)"
                }
                # ✅ OPTIMISÉ: Utiliser l'index normalisé (O(1) au lieu de O(n))
                else {
                    $normName = Normalize-PolicyName $pol.PolicyDisplayName
                    if ($normName -and $alzNormalizedIndex.ContainsKey($normName)) {
                        # Utiliser l'ID du baseline pour éviter les duplicatas
                        $baselineId = $alzNormalizedIndex[$normName]
                        [void]$alzMatchedPoliciesSet.Add($baselineId)
                        Write-Debug "ALZ match by name: $normName -> $baselineId"
                    }
                }
            }
        } else {
            Write-Debug "Initiative CSV not found: $initiativeCsvPath"
        }
    }
    
    $alzDeployed = $alzMatchedPoliciesSet.Count
    $alzMissing = $alzBaselineCount - $alzDeployed
    
    if ($alzBaselineCount -gt 0) {
        $alzScore = [math]::Round(($alzDeployed / $alzBaselineCount) * 100, 1)
    } else {
        $alzScore = 0
    }
    
    Write-Host (" │ ├─ ALZ Baseline: {0} policies" -f $alzBaselineCount) -ForegroundColor DarkGray
    Write-Host (" │ ├─ Deployed: {0}" -f $alzDeployed) -ForegroundColor DarkGray
    Write-Host (" │ ├─ Missing: {0}" -f $alzMissing) -ForegroundColor DarkGray
    Write-Host (" │ └─ ALZ Score: {0}%" -f $alzScore) -ForegroundColor $(if ($alzScore -ge 50) { "Green" } else { "Yellow" })
    
    # ========== MCSB Score ==========
    Write-Host " ├─ Analyzing MCSB baseline coverage..." -ForegroundColor DarkCyan
    
    $mcsbBaselineCount = $mcsbPoliciesIndex.Count
    $mcsbMatchedPoliciesSet = [System.Collections.Generic.HashSet[string]]::new()
    
    foreach ($sumItem in $Summary) {
        $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($sumItem.AssignmentName)-Policies.csv"
        
        if (Test-Path $initiativeCsvPath) {
            # ✅ Réutiliser le cache CSV
            if (-not $csvCache.ContainsKey($initiativeCsvPath)) {
                $csvCache[$initiativeCsvPath] = Import-Csv $initiativeCsvPath
            }
            
            $initiativePolicies = $csvCache[$initiativeCsvPath]
            
            foreach ($pol in $initiativePolicies) {
                # ✅ Match by ID first (O(1) lookup)
                if ($mcsbPoliciesIndex.ContainsKey($pol.PolicyDefinitionId)) {
                    [void]$mcsbMatchedPoliciesSet.Add($pol.PolicyDefinitionId)
                    Write-Debug "MCSB match by ID: $($pol.PolicyDefinitionId)"
                }
                # ✅ OPTIMISÉ: Utiliser l'index normalisé (O(1))
                else {
                    $normName = Normalize-PolicyName $pol.PolicyDisplayName
                    if ($normName -and $mcsbNormalizedIndex.ContainsKey($normName)) {
                        $baselineId = $mcsbNormalizedIndex[$normName]
                        [void]$mcsbMatchedPoliciesSet.Add($baselineId)
                        Write-Debug "MCSB match by name: $normName -> $baselineId"
                    }
                }
            }
        } else {
            Write-Debug "Initiative CSV not found: $initiativeCsvPath"
        }
    }
    
    $mcsbDeployed = $mcsbMatchedPoliciesSet.Count
    $mcsbMissing = $mcsbBaselineCount - $mcsbDeployed
    
    if ($mcsbBaselineCount -gt 0) {
        $mcsbScore = [math]::Round(($mcsbDeployed / $mcsbBaselineCount) * 100, 1)
    } else {
        $mcsbScore = 0
    }
    
    Write-Host (" │ ├─ MCSB Baseline: {0} policies" -f $mcsbBaselineCount) -ForegroundColor DarkGray
    Write-Host (" │ ├─ Deployed: {0}" -f $mcsbDeployed) -ForegroundColor DarkGray
    Write-Host (" │ ├─ Missing: {0}" -f $mcsbMissing) -ForegroundColor DarkGray
    Write-Host (" │ └─ MCSB Score: {0}%" -f $mcsbScore) -ForegroundColor $(if ($mcsbScore -ge 75) { "Green" } elseif ($mcsbScore -ge 50) { "Yellow" } else { "Red" })
    
    Write-Host " └─ Baseline coverage analysis complete" -ForegroundColor Green
    Write-Host ""
    
    # ========== Global Score ==========
    Write-Host "📊 Computing global compliance score..." -ForegroundColor Cyan
    
    $totalBaselinePolicies = $alzBaselineCount + $mcsbBaselineCount
    $totalDeployedPolicies = $alzDeployed + $mcsbDeployed
    
    if ($totalBaselinePolicies -gt 0) {
        $globalScore = [math]::Round(($totalDeployedPolicies / $totalBaselinePolicies) * 100, 1)
    } else {
        $globalScore = 0
    }
    
    # Determine compliance level
    $complianceLevel = if ($globalScore -ge 90) { "Excellent" }
                       elseif ($globalScore -ge 75) { "Bon" }
                       elseif ($globalScore -ge 50) { "Moyen" }
                       else { "Faible" }
    
    $scoreColor = if ($globalScore -ge 90) { "Green" }
                  elseif ($globalScore -ge 75) { "Cyan" }
                  elseif ($globalScore -ge 50) { "Yellow" }
                  else { "Red" }
    
    Write-Host ""
    Write-Host "╔═══════════════════════════════════╗" -ForegroundColor Cyan
    Write-Host " 🎯 GLOBAL COMPLIANCE SCORE: $globalScore%" -ForegroundColor $scoreColor
    Write-Host " Compliance Level: $complianceLevel" -ForegroundColor $scoreColor
    Write-Host "╚═══════════════════════════════════╝" -ForegroundColor Cyan
    Write-Host ""
    
    # ✅ Nettoyer le cache CSV pour libérer la mémoire
    $csvCache.Clear()
    
    # Return structured result
    return [PSCustomObject]@{
        AlzScore = $alzScore
        McsbScore = $mcsbScore
        GlobalScore = $globalScore
        AlzMetrics = @{
            BaselineCount = $alzBaselineCount
            Deployed = $alzDeployed
            Missing = $alzMissing
        }
        McsbMetrics = @{
            BaselineCount = $mcsbBaselineCount
            Deployed = $mcsbDeployed
            Missing = $mcsbMissing
        }
        GlobalMetrics = @{
            TotalBaseline = $totalBaselinePolicies
            TotalDeployed = $totalDeployedPolicies
            ComplianceLevel = $complianceLevel
        }
    }
}