Public/Compare-IntuneGroupAssignment.ps1

function Compare-IntuneGroupAssignment {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$CompareGroupNames,

        [Parameter()]
        [switch]$IncludeNestedGroups,

        [Parameter()]
        [switch]$ExportToCSV,

        [Parameter()]
        [string]$ExportPath
    )

    Write-Host "Compare Group Assignments chosen" -ForegroundColor Green

    # Get Group names to compare from parameter or prompt
    if ($CompareGroupNames) {
        $groupInput = $CompareGroupNames
    }
    else {
        # Prompt for Group names or IDs
        Write-Host "Please enter Group names or Object IDs to compare, separated by commas (,): " -ForegroundColor Cyan
        Write-Host "Example: 'Marketing Team, 12345678-1234-1234-1234-123456789012'" -ForegroundColor Gray
        $groupInput = Read-Host
    }

    $groupInputs = $groupInput -split ',' | ForEach-Object { $_.Trim() }

    if ($groupInputs.Count -lt 2) {
        Write-Host "Please provide at least two groups to compare." -ForegroundColor Red
        return
    }

    # Determine if nested group checking should be enabled
    $checkNestedGroupsCompare = $false
    if ($IncludeNestedGroups) {
        $checkNestedGroupsCompare = $true
    }
    elseif (-not $CompareGroupNames) {
        $nestedPromptCompare = Read-Host "Include assignments inherited from parent groups? (y/n)"
        if ($nestedPromptCompare -match '^[Yy]') {
            $checkNestedGroupsCompare = $true
        }
    }

    # Before caching starts, initialize the group assignments hashtable
    $groupAssignments = [ordered]@{}

    # Process each group input
    $resolvedGroups = @{}
    foreach ($groupInput in $groupInputs) {
        Write-Host "`nProcessing input: $groupInput" -ForegroundColor Yellow

        # Initialize variables
        $groupId = $null
        $groupName = $null
        $allGroupIds = @()
        $parentGroupMap = @{}

        # Check if input is a GUID
        if ($groupInput -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') {
            try {
                # Get group info from Graph API
                $groupUri = "$GraphEndpoint/v1.0/groups/$groupInput"
                $groupResponse = Invoke-MgGraphRequest -Uri $groupUri -Method Get
                $groupId = $groupResponse.id
                $groupName = $groupResponse.displayName
                $resolvedGroups[$groupId] = $groupName

                # Initialize collections for this group
                $groupAssignments[$groupName] = @{
                    DeviceConfigs              = [System.Collections.ArrayList]::new()
                    SettingsCatalog            = [System.Collections.ArrayList]::new()
                    CompliancePolicies         = [System.Collections.ArrayList]::new()
                    RequiredApps               = [System.Collections.ArrayList]::new()
                    AvailableApps              = [System.Collections.ArrayList]::new()
                    UninstallApps              = [System.Collections.ArrayList]::new()
                    PlatformScripts            = [System.Collections.ArrayList]::new()
                    HealthScripts              = [System.Collections.ArrayList]::new()
                    AntivirusProfiles          = [System.Collections.ArrayList]::new()
                    DiskEncryptionProfiles     = [System.Collections.ArrayList]::new()
                    FirewallProfiles           = [System.Collections.ArrayList]::new()
                    EndpointDetectionProfiles  = [System.Collections.ArrayList]::new()
                    AttackSurfaceProfiles      = [System.Collections.ArrayList]::new()
                    AccountProtectionProfiles  = [System.Collections.ArrayList]::new()
                }

                Write-Host "Found group by ID: $groupName" -ForegroundColor Green

                # Build effective group IDs for nested group support
                $allGroupIds = @($groupId)
                $parentGroupMap = @{}
                if ($checkNestedGroupsCompare) {
                    $parentGroups = Get-TransitiveGroupMembership -GroupId $groupId
                    if ($parentGroups.Count -gt 0) {
                        foreach ($pg in $parentGroups) {
                            $allGroupIds += $pg.id
                            $parentGroupMap[$pg.id] = $pg.displayName
                        }
                        Write-Host " Found $($parentGroups.Count) parent group(s)" -ForegroundColor Green
                    }
                }
            }
            catch {
                Write-Host "No group found with ID: $groupInput" -ForegroundColor Red
                continue
            }
        }
        else {
            # Try to find group by display name
            $groupUri = "$GraphEndpoint/v1.0/groups?`$filter=displayName eq '$groupInput'"
            $groupResponse = Invoke-MgGraphRequest -Uri $groupUri -Method Get

            if ($groupResponse.value.Count -eq 0) {
                Write-Host "No group found with name: $groupInput" -ForegroundColor Red
                continue
            }
            elseif ($groupResponse.value.Count -gt 1) {
                Write-Host "Multiple groups found with name: $groupInput. Please use the Object ID instead:" -ForegroundColor Red
                foreach ($group in $groupResponse.value) {
                    Write-Host " - $($group.displayName) (ID: $($group.id))" -ForegroundColor Yellow
                }
                continue
            }

            $groupId = $groupResponse.value[0].id
            $groupName = $groupResponse.value[0].displayName
            $resolvedGroups[$groupId] = $groupName

            # Initialize collections for this group
            $groupAssignments[$groupName] = @{
                DeviceConfigs              = [System.Collections.ArrayList]::new()
                SettingsCatalog            = [System.Collections.ArrayList]::new()
                CompliancePolicies         = [System.Collections.ArrayList]::new()
                RequiredApps               = [System.Collections.ArrayList]::new()
                AvailableApps              = [System.Collections.ArrayList]::new()
                UninstallApps              = [System.Collections.ArrayList]::new()
                PlatformScripts            = [System.Collections.ArrayList]::new()
                HealthScripts              = [System.Collections.ArrayList]::new()
                AntivirusProfiles          = [System.Collections.ArrayList]::new()
                DiskEncryptionProfiles     = [System.Collections.ArrayList]::new()
                FirewallProfiles           = [System.Collections.ArrayList]::new()
                EndpointDetectionProfiles  = [System.Collections.ArrayList]::new()
                AttackSurfaceProfiles      = [System.Collections.ArrayList]::new()
                AccountProtectionProfiles  = [System.Collections.ArrayList]::new()
            }

            Write-Host "Found group by name: $groupName (ID: $groupId)" -ForegroundColor Green

            # Build effective group IDs for nested group support
            $allGroupIds = @($groupId)
            $parentGroupMap = @{}
            if ($checkNestedGroupsCompare) {
                $parentGroups = Get-TransitiveGroupMembership -GroupId $groupId
                if ($parentGroups.Count -gt 0) {
                    foreach ($pg in $parentGroups) {
                        $allGroupIds += $pg.id
                        $parentGroupMap[$pg.id] = $pg.displayName
                    }
                    Write-Host " Found $($parentGroups.Count) parent group(s)" -ForegroundColor Green
                }
            }
        }

        # Process Device Configurations
        $deviceConfigsUri = "$GraphEndpoint/beta/deviceManagement/deviceConfigurations"
        $deviceConfigsResponse = Invoke-MgGraphRequest -Uri $deviceConfigsUri -Method Get
        $allDeviceConfigs = $deviceConfigsResponse.value
        while ($deviceConfigsResponse.'@odata.nextLink') {
            $deviceConfigsResponse = Invoke-MgGraphRequest -Uri $deviceConfigsResponse.'@odata.nextLink' -Method Get
            $allDeviceConfigs += $deviceConfigsResponse.value
        }
        $totalDeviceConfigs = $allDeviceConfigs.Count
        $currentDeviceConfig = 0
        foreach ($config in $allDeviceConfigs) {
            $currentDeviceConfig++
            Write-Host "`rFetching Device Configuration $currentDeviceConfig of $totalDeviceConfigs" -NoNewline
            $configId = $config.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/deviceConfigurations('$configId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            # Check for both inclusion and exclusion assignments
            $hasAssignment = $assignmentResponse.value | Where-Object {
                $allGroupIds -contains $_.target.groupId -and
                ($_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -or
                $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget')
            }
            if ($hasAssignment) {
                $isExclusion = $hasAssignment | Where-Object {
                    $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget'
                }
                $isInherited = $hasAssignment | Where-Object {
                    $_.target.groupId -ne $groupId
                }
                $suffix = ""
                if ($isExclusion) { $suffix += " [EXCLUDED]" }
                if ($isInherited) { $suffix += " [INHERITED]" }
                $displayName = "$($config.displayName)$suffix"
                [void]$groupAssignments[$groupName].DeviceConfigs.Add($displayName)
            }
        }
        Write-Host "`rFetching Device Configuration $totalDeviceConfigs of $totalDeviceConfigs" -NoNewline
        Start-Sleep -Milliseconds 100
        Write-Host ""  # Move to the next line after the loop

        # Process Settings Catalog
        $settingsCatalogUri = "$GraphEndpoint/beta/deviceManagement/configurationPolicies"
        $settingsCatalogResponse = Invoke-MgGraphRequest -Uri $settingsCatalogUri -Method Get

        foreach ($policy in $settingsCatalogResponse.value) {
            $policyId = $policy.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/configurationPolicies('$policyId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            # Check for both inclusion and exclusion assignments
            $hasAssignment = $assignmentResponse.value | Where-Object {
                $allGroupIds -contains $_.target.groupId -and
                ($_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -or
                $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget')
            }
            if ($hasAssignment) {
                $isExclusion = $hasAssignment | Where-Object {
                    $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget'
                }
                $isInherited = $hasAssignment | Where-Object {
                    $_.target.groupId -ne $groupId
                }
                $suffix = ""
                if ($isExclusion) { $suffix += " [EXCLUDED]" }
                if ($isInherited) { $suffix += " [INHERITED]" }
                $displayName = "$($policy.name)$suffix"
                [void]$groupAssignments[$groupName].SettingsCatalog.Add($displayName)
            }
        }

        # Process Compliance Policies
        $complianceUri = "$GraphEndpoint/beta/deviceManagement/deviceCompliancePolicies"
        $complianceResponse = Invoke-MgGraphRequest -Uri $complianceUri -Method Get

        foreach ($policy in $complianceResponse.value) {
            $policyId = $policy.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/deviceCompliancePolicies('$policyId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            # Check for both inclusion and exclusion assignments
            $hasAssignment = $assignmentResponse.value | Where-Object {
                $allGroupIds -contains $_.target.groupId -and
                ($_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -or
                $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget')
            }
            if ($hasAssignment) {
                $isExclusion = $hasAssignment | Where-Object {
                    $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget'
                }
                $isInherited = $hasAssignment | Where-Object {
                    $_.target.groupId -ne $groupId
                }
                $suffix = ""
                if ($isExclusion) { $suffix += " [EXCLUDED]" }
                if ($isInherited) { $suffix += " [INHERITED]" }
                $displayName = "$($policy.displayName)$suffix"
                [void]$groupAssignments[$groupName].CompliancePolicies.Add($displayName)
            }
        }

        # Process Apps
        $appUri = "$GraphEndpoint/beta/deviceAppManagement/mobileApps?`$filter=isAssigned eq true"
        $appResponse = Invoke-MgGraphRequest -Uri $appUri -Method Get

        foreach ($app in $appResponse.value) {
            # Skip built-in and Microsoft apps
            if ($app.isFeatured -or $app.isBuiltIn) {
                continue
            }

            $appId = $app.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceAppManagement/mobileApps('$appId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            foreach ($assignment in $assignmentResponse.value) {
                if ($allGroupIds -contains $assignment.target.groupId) {
                    $inheritedSuffix = if ($assignment.target.groupId -ne $groupId) { " [INHERITED]" } else { "" }
                    switch ($assignment.intent) {
                        "required" { [void]$groupAssignments[$groupName].RequiredApps.Add("$($app.displayName)$inheritedSuffix") }
                        "available" { [void]$groupAssignments[$groupName].AvailableApps.Add("$($app.displayName)$inheritedSuffix") }
                        "uninstall" { [void]$groupAssignments[$groupName].UninstallApps.Add("$($app.displayName)$inheritedSuffix") }
                    }
                }
            }
        }

        # Process Platform Scripts (PowerShell)
        $scriptsUri = "$GraphEndpoint/beta/deviceManagement/deviceManagementScripts"
        $scriptsResponse = Invoke-MgGraphRequest -Uri $scriptsUri -Method Get
        # For PowerShell scripts, we need to check the script type
        foreach ($script in $scriptsResponse.value) {
            $scriptId = $script.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/deviceManagementScripts('$scriptId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            $hasAssignment = $assignmentResponse.value | Where-Object { $allGroupIds -contains $_.target.groupId }
            if ($hasAssignment) {
                $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                $scriptInfo = "$($script.displayName) (PowerShell)$suffix"
                [void]$groupAssignments[$groupName].PlatformScripts.Add($scriptInfo)
            }
        }

        # Process Shell Scripts (macOS)
        $shellScriptsUri = "$GraphEndpoint/beta/deviceManagement/deviceShellScripts"
        $shellScriptsResponse = Invoke-MgGraphRequest -Uri $shellScriptsUri -Method Get
        # For Shell scripts, we need to check the script type
        foreach ($script in $shellScriptsResponse.value) {
            $scriptId = $script.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/deviceShellScripts('$scriptId')/groupAssignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            $hasAssignment = $assignmentResponse.value | Where-Object { $allGroupIds -contains $_.targetGroupId }
            if ($hasAssignment) {
                $isInherited = $hasAssignment | Where-Object { $_.targetGroupId -ne $groupId }
                $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                $scriptInfo = "$($script.displayName) (Shell)$suffix"
                [void]$groupAssignments[$groupName].PlatformScripts.Add($scriptInfo)
            }
        }

        # Fetch and process Proactive Remediation Scripts (deviceHealthScripts)
        $healthScriptsUri = "$GraphEndpoint/beta/deviceManagement/deviceHealthScripts"
        $healthScriptsResponse = Invoke-MgGraphRequest -Uri $healthScriptsUri -Method Get
        foreach ($script in $healthScriptsResponse.value) {
            $scriptId = $script.id
            $assignmentsUri = "$GraphEndpoint/beta/deviceManagement/deviceHealthScripts('$scriptId')/assignments"
            $assignmentResponse = Invoke-MgGraphRequest -Uri $assignmentsUri -Method Get

            $hasAssignment = $assignmentResponse.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
            if ($hasAssignment) {
                $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                [void]$groupAssignments[$groupName].HealthScripts.Add("$($script.displayName)$suffix")
            }
        }

        # Get Endpoint Security - Antivirus Policies
        $allIntentsForAntivirusCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForAntivirusCompare
        $antivirusPolicies = $allIntentsForAntivirusCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityAntivirus' }
        if ($antivirusPolicies) {
            foreach ($policy in $antivirusPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].AntivirusProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }

        # Get Endpoint Security - Disk Encryption Policies
        $allIntentsForDiskEncCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForDiskEncCompare
        $diskEncryptionPolicies = $allIntentsForDiskEncCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityDiskEncryption' }
        if ($diskEncryptionPolicies) {
            foreach ($policy in $diskEncryptionPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].DiskEncryptionProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }

        # Get Endpoint Security - Firewall Policies
        $allIntentsForFirewallCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForFirewallCompare
        $firewallPolicies = $allIntentsForFirewallCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityFirewall' }
        if ($firewallPolicies) {
            foreach ($policy in $firewallPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].FirewallProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }

        # Get Endpoint Security - Endpoint Detection and Response Policies
        $allIntentsForEDRCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForEDRCompare
        $edrPolicies = $allIntentsForEDRCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityEndpointDetectionAndResponse' }
        if ($edrPolicies) {
            foreach ($policy in $edrPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].EndpointDetectionProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }

        # Get Endpoint Security - Attack Surface Reduction Policies
        $allIntentsForASRCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForASRCompare
        $asrPolicies = $allIntentsForASRCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityAttackSurfaceReduction' }
        if ($asrPolicies) {
            foreach ($policy in $asrPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].AttackSurfaceProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }

        # Get Endpoint Security - Account Protection Policies
        $allIntentsForAccountProtectionCompare = Get-IntuneEntities -EntityType "deviceManagement/intents"
        Add-IntentTemplateFamilyInfo -IntentPolicies $allIntentsForAccountProtectionCompare
        $accountProtectionPolicies = $allIntentsForAccountProtectionCompare | Where-Object { $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityAccountProtection' }
        if ($accountProtectionPolicies) {
            foreach ($policy in $accountProtectionPolicies) {
                $assignments = Invoke-MgGraphRequest -Uri "$GraphEndpoint/beta/deviceManagement/intents/$($policy.id)/assignments" -Method Get
                $hasAssignment = $assignments.value | Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' -and $allGroupIds -contains $_.target.groupId }
                if ($hasAssignment) {
                    $isInherited = $hasAssignment | Where-Object { $_.target.groupId -ne $groupId }
                    $suffix = if ($isInherited) { " [INHERITED]" } else { "" }
                    [void]$groupAssignments[$groupName].AccountProtectionProfiles.Add("$($policy.displayName)$suffix")
                }
            }
        }
    }

    # Comparison Results section
    Write-Host "`nComparison Results:" -ForegroundColor Cyan
    Write-Host "Comparing assignments between groups:" -ForegroundColor White
    foreach ($groupName in $groupAssignments.Keys) {
        Write-Host " * $groupName" -ForegroundColor White
    }
    Write-Host ""

    # Update categories to include "Proactive Remediation Scripts"
    $categories = [ordered]@{
        "Device Configurations"               = "DeviceConfigs"
        "Settings Catalog"                    = "SettingsCatalog"
        "Compliance Policies"                 = "CompliancePolicies"
        "Required Apps"                       = "RequiredApps"
        "Available Apps"                      = "AvailableApps"
        "Uninstall Apps"                      = "UninstallApps"
        "Platform Scripts"                    = "PlatformScripts"
        "Proactive Remediation Scripts"       = "HealthScripts"
        "Endpoint Security - Antivirus"       = "AntivirusProfiles"
        "Endpoint Security - Disk Encryption" = "DiskEncryptionProfiles"
        "Endpoint Security - Firewall"        = "FirewallProfiles"
        "Endpoint Security - EDR"             = "EndpointDetectionProfiles"
        "Endpoint Security - ASR"             = "AttackSurfaceProfiles"
        "Endpoint Security - Account Protection" = "AccountProtectionProfiles"
    }

    # Collect all unique base policy names (strip tag suffixes for deduplication)
    $uniqueBasePolicies = [System.Collections.ArrayList]@()
    foreach ($groupName in $groupAssignments.Keys) {
        foreach ($categoryKey in $categories.Values) {
            foreach ($policy in $groupAssignments[$groupName][$categoryKey]) {
                $baseName = $policy -replace ' \[(EXCLUDED|INHERITED)\]', ''
                $baseName = $baseName.Trim()
                if ($uniqueBasePolicies -notcontains $baseName) {
                    $null = $uniqueBasePolicies.Add($baseName)
                }
            }
        }
    }

    Write-Host "Found $($uniqueBasePolicies.Count) unique policies/apps/scripts across all groups`n" -ForegroundColor Yellow

    $groupNames = @($groupAssignments.Keys)

    # Display comparison for each category in table format
    foreach ($category in $categories.Keys) {
        $categoryKey = $categories[$category]

        # Collect base policy names that belong to this category
        $categoryPolicies = [System.Collections.ArrayList]@()
        foreach ($baseName in $uniqueBasePolicies) {
            $isInCategory = $false
            foreach ($g in $groupNames) {
                $matchFound = $groupAssignments[$g][$categoryKey] | Where-Object {
                    ($_ -replace ' \[(EXCLUDED|INHERITED)\]', '').Trim() -eq $baseName
                }
                if ($matchFound) {
                    $isInCategory = $true
                    break
                }
            }
            if ($isInCategory) {
                $null = $categoryPolicies.Add($baseName)
            }
        }

        Write-Host "=== $category ===" -ForegroundColor Cyan

        if ($categoryPolicies.Count -eq 0) {
            Write-Host "No assignments found in this category" -ForegroundColor Gray
            Write-Host ""
            continue
        }

        # Calculate column widths
        $maxPolicyLen = ($categoryPolicies | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum
        $maxPolicyLen = [Math]::Max($maxPolicyLen, 6)   # min width for "Policy" header
        $maxPolicyLen = [Math]::Min($maxPolicyLen, 50)  # cap at 50 chars

        $groupColWidths = @{}
        foreach ($g in $groupNames) {
            $groupColWidths[$g] = [Math]::Max($g.Length, 10)
        }

        # Header row
        $header = ("Policy".PadRight($maxPolicyLen + 2))
        foreach ($g in $groupNames) {
            $header += ($g.PadRight($groupColWidths[$g] + 2))
        }
        Write-Host $header -ForegroundColor White

        # Separator row
        $sep = ("-" * ($maxPolicyLen + 2))
        foreach ($g in $groupNames) {
            $sep += ("-" * ($groupColWidths[$g] + 2))
        }
        Write-Host $sep -ForegroundColor Gray

        # Data rows
        foreach ($baseName in $categoryPolicies) {
            $displayName = if ($baseName.Length -gt 50) { $baseName.Substring(0, 47) + "..." } else { $baseName }
            $row = $displayName.PadRight($maxPolicyLen + 2)

            foreach ($g in $groupNames) {
                $assignments = $groupAssignments[$g][$categoryKey]
                # Find all matching entries for this base name
                $matchingEntries = $assignments | Where-Object {
                    ($_ -replace ' \[(EXCLUDED|INHERITED)\]', '').Trim() -eq $baseName
                }
                $cell = ""
                if ($matchingEntries) {
                    $hasExcluded = $matchingEntries | Where-Object { $_ -match '\[EXCLUDED\]' }
                    $hasInherited = $matchingEntries | Where-Object { $_ -match '\[INHERITED\]' }
                    if ($hasExcluded -and $hasInherited) {
                        $cell = "IE"
                    }
                    elseif ($hasExcluded) {
                        $cell = "E"
                    }
                    elseif ($hasInherited) {
                        $cell = "I"
                    }
                    else {
                        $cell = "X"
                    }
                }
                $row += $cell.PadRight($groupColWidths[$g] + 2)
            }
            Write-Host $row -ForegroundColor Yellow
        }
        Write-Host ""
    }

    # Legend
    Write-Host "Legend: X = Included, E = Excluded, I = Inherited, IE = Inherited+Excluded" -ForegroundColor Gray
    Write-Host ""

    # Summary section
    Write-Host "=== Summary ===" -ForegroundColor Cyan
    foreach ($groupName in $groupAssignments.Keys) {
        $totalAssignments = 0
        foreach ($categoryKey in $categories.Values) {
            $totalAssignments += $groupAssignments[$groupName][$categoryKey].Count
        }
        Write-Host "$groupName has $totalAssignments total assignments" -ForegroundColor Yellow
    }
    Write-Host ""

    # Create comparison results with one column per group
    $comparisonResults = [System.Collections.ArrayList]@()
    foreach ($category in $categories.Keys) {
        $categoryKey = $categories[$category]
        foreach ($baseName in $uniqueBasePolicies) {
            # Check if this policy belongs to this category
            $isInCategory = $false
            foreach ($g in $groupNames) {
                $matchFound = $groupAssignments[$g][$categoryKey] | Where-Object {
                    ($_ -replace ' \[(EXCLUDED|INHERITED)\]', '').Trim() -eq $baseName
                }
                if ($matchFound) {
                    $isInCategory = $true
                    break
                }
            }
            if (-not $isInCategory) { continue }

            $props = [ordered]@{
                Category   = $category
                PolicyName = $baseName
            }
            foreach ($g in $groupNames) {
                $matchingEntries = $groupAssignments[$g][$categoryKey] | Where-Object {
                    ($_ -replace ' \[(EXCLUDED|INHERITED)\]', '').Trim() -eq $baseName
                }
                $val = ""
                if ($matchingEntries) {
                    $hasExcluded = $matchingEntries | Where-Object { $_ -match '\[EXCLUDED\]' }
                    $hasInherited = $matchingEntries | Where-Object { $_ -match '\[INHERITED\]' }
                    if ($hasExcluded -and $hasInherited) {
                        $val = "Inherited+Excluded"
                    }
                    elseif ($hasExcluded) {
                        $val = "Excluded"
                    }
                    elseif ($hasInherited) {
                        $val = "Inherited"
                    }
                    else {
                        $val = "Included"
                    }
                }
                $props[$g] = $val
            }
            [void]$comparisonResults.Add([PSCustomObject]$props)
        }
    }

    # Export results if requested
    if ($ExportToCSV) {
        $csvExportPath = if ($ExportPath) {
            $ExportPath
        }
        else {
            $null
        }

        if ($csvExportPath) {
            $comparisonResults | Export-Csv -Path $csvExportPath -NoTypeInformation
            Write-Host "Results exported to $csvExportPath" -ForegroundColor Green
        }
    }
    elseif (-not $CompareGroupNames) {
        $export = Read-Host "Would you like to export the comparison results to CSV? (y/n)"
        if ($export -match '^[Yy]') {
            $csvExportPath = Show-SaveFileDialog -DefaultFileName "IntuneGroupAssignmentComparison.csv"
            if ($csvExportPath) {
                $comparisonResults | Export-Csv -Path $csvExportPath -NoTypeInformation
                Write-Host "Results exported to $csvExportPath" -ForegroundColor Green
            }
        }
    }
}