source/Private/AzureResourceAnalysis.ps1

function Get-RJArmTemplateWhatIf {
    <#
    .SYNOPSIS
    Runs ARM WhatIf analysis on a template and returns resource changes.
 
    .PARAMETER ResourceGroupName
    The target resource group.
 
    .PARAMETER TemplatePath
    Path to the JSON ARM template file.
 
    .PARAMETER Parameters
    Template parameters hashtable.
 
    .OUTPUTS
    Array of change objects with ChangeType, RelativeResourceId, FullyQualifiedResourceId.
    Ignore entries are filtered out.
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [string]$TemplatePath,

        [hashtable]$Parameters = @{}
    )

    Write-Verbose "Running ARM WhatIf for template: $(Split-Path $TemplatePath -Leaf)"

    $result = Get-AzResourceGroupDeploymentWhatIfResult `
        -ResourceGroupName $ResourceGroupName `
        -TemplateFile $TemplatePath `
        -TemplateParameterObject $Parameters `
        -ErrorAction Stop

    # Filter out Ignore entries (Graph extension resources, disabled conditionals)
    $changes = $result.Changes | Where-Object { $_.ChangeType -ne 'Ignore' }

    return ,@($changes)
}

function Get-RJServicePrincipalAnalysis {
    <#
    .SYNOPSIS
    Checks whether the RealmJoin service principal exists in Entra ID.
 
    .OUTPUTS
    PSCustomObject with Exists, DisplayName, ObjectId, ChangeType.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param()

    $rjAzureResourcesAppId = Get-RJAzureResourcesAppId
    Write-Verbose "Checking RealmJoin service principal (AppId: $rjAzureResourcesAppId)..."

    $sp = Get-ServicePrincipalByAppId -AppId $rjAzureResourcesAppId

    if ($sp) {
        Write-Verbose "Service principal found: $($sp.DisplayName)"
        return [PSCustomObject]@{
            Exists      = $true
            DisplayName = $sp.DisplayName
            ObjectId    = $sp.Id
            ChangeType  = 'Exists'
        }
    }

    return [PSCustomObject]@{
        Exists      = $false
        DisplayName = $null
        ObjectId    = $null
        ChangeType  = 'Create'
    }
}

function Get-RJAutomationAccountPermissionsManifest {
    <#
    .SYNOPSIS
    Loads the AA managed identity permissions manifest from the bundled JSON file.
 
    .OUTPUTS
    Array of service objects with Name, Id, and AppRoleAssignments.
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param()

    $manifestPath = Get-ArmTemplatePath -RelativePath "azure-automation-account/RJAutomationAccountPermissionsManifest.json"
    Write-Verbose "Loading permissions manifest from: $manifestPath"
    $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json
    return ,@($manifest)
}

function Get-RJAutomationAccountPermissionsAnalysis {
    <#
    .SYNOPSIS
    Analyzes which API permissions need to be assigned to the AA managed identity.
 
    .PARAMETER ManagedIdentityPrincipalId
    The ObjectId of the existing AA managed identity. If null, all permissions are treated as new.
 
    .OUTPUTS
    Array of PSCustomObjects per service: ServiceName, ServiceAppId, PermissionsToAdd,
    ExistingPermissions, TotalDesired, ChangeType (CreateAll/AddPermissions/NoChanges).
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [AllowNull()]
        [string]$ManagedIdentityPrincipalId
    )

    $manifest = Get-RJAutomationAccountPermissionsManifest

    # Get current assignments if AA already exists with a managed identity
    $currentAssignments = @()
    if ($ManagedIdentityPrincipalId) {
        $currentAssignments = Get-CurrentAppRoleAssignmentList -ServicePrincipalId $ManagedIdentityPrincipalId
    }

    $results = @()

    foreach ($service in $manifest) {
        $desiredPermissions = @($service.AppRoleAssignments)

        if (-not $ManagedIdentityPrincipalId) {
            # New AA — all permissions need to be created
            $results += [PSCustomObject]@{
                ServiceName         = $service.Name
                ServiceAppId        = $service.Id
                PermissionsToAdd    = $desiredPermissions
                ExistingPermissions = @()
                TotalDesired        = $desiredPermissions.Count
                ChangeType          = 'CreateAll'
            }
            continue
        }

        # Existing AA — resolve AppRole IDs to permission names for this service
        $serviceSp = Get-ServicePrincipalByAppId -AppId $service.Id
        if (-not $serviceSp) {
            throw "Service principal not found for '$($service.Name)' (AppId: $($service.Id)). Verify the AppId in RJAutomationAccountPermissionsManifest.json is correct."
        }

        # Build lookup: AppRoleId -> Permission Name for this service
        $roleIdToName = @{}
        foreach ($role in $serviceSp.AppRole) {
            $roleIdToName[$role.Id] = $role.Value
        }

        # Find current permission names assigned to this service
        $serviceAssignments = $currentAssignments | Where-Object { $_.ResourceId -eq $serviceSp.Id }
        $existingNames = @()
        foreach ($assignment in $serviceAssignments) {
            $name = $roleIdToName[$assignment.AppRoleId]
            if ($name) { $existingNames += $name }
        }

        # Diff: desired minus existing
        $toAdd = @($desiredPermissions | Where-Object { $_ -notin $existingNames })

        $changeType = if ($toAdd.Count -eq 0) { 'NoChanges' }
                      elseif ($existingNames.Count -eq 0) { 'CreateAll' }
                      else { 'AddPermissions' }

        $results += [PSCustomObject]@{
            ServiceName         = $service.Name
            ServiceAppId        = $service.Id
            PermissionsToAdd    = $toAdd
            ExistingPermissions = $existingNames
            TotalDesired        = $desiredPermissions.Count
            ChangeType          = $changeType
        }
    }

    return ,@($results)
}

function Get-RJAzureResourceAnalysis {
    <#
    .SYNOPSIS
    Analyzes what an Azure resource deployment will do before executing it.
 
    .DESCRIPTION
    Uses ARM WhatIf for infrastructure templates and manual checks for Graph extension
    resources (service principal, permissions) to build a per-resource analysis showing
    what would be created vs what already exists.
 
    .PARAMETER ResourceGroupName
    The target resource group name.
 
    .PARAMETER DeployAutomationAccount
    Whether the Automation Account deployment is planned.
 
    .PARAMETER DeployLogAnalytics
    Whether the Log Analytics Workspace deployment is planned.
 
    .PARAMETER AutomationAccountName
    Optional. Custom Automation Account name (auto-generated if not provided).
 
    .PARAMETER WorkspaceName
    Optional. Custom Log Analytics Workspace name (auto-generated if not provided).
 
    .PARAMETER DeployAuditLogsDCR
    Whether to deploy the audit logs DCR.
 
    .PARAMETER DeployRunbookLogsDCR
    Whether to deploy the runbook logs DCR.
 
    .PARAMETER DeployOperationalLogsDCR
    Whether to deploy the operational logs DCR.
 
    .OUTPUTS
    PSCustomObject with ResourceGroupName, ResourceGroupExists, ServicePrincipalAnalysis,
    ResourceAnalysis array, and deployment parameters.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [bool]$DeployAutomationAccount = $false,
        [bool]$DeployLogAnalytics = $false,

        [string]$AutomationAccountName,
        [string]$WorkspaceName,

        [bool]$DeployAuditLogsDCR = $DefaultDeployAuditLogsDCR,
        [bool]$DeployRunbookLogsDCR = $DefaultDeployRunbookLogsDCR,
        [bool]$DeployOperationalLogsDCR = $DefaultDeployOperationalLogsDCR
    )

    $analysis = [PSCustomObject]@{
        ResourceGroupName        = $ResourceGroupName
        ResourceGroupExists      = $false
        DeployAutomationAccount  = $DeployAutomationAccount
        DeployLogAnalytics       = $DeployLogAnalytics
        AutomationAccountName    = if ($AutomationAccountName) { $AutomationAccountName } else { "(auto-generated)" }
        WorkspaceName            = if ($WorkspaceName) { $WorkspaceName } else { "(auto-generated)" }
        DeployAuditLogsDCR       = $DeployAuditLogsDCR
        DeployRunbookLogsDCR     = $DeployRunbookLogsDCR
        DeployOperationalLogsDCR = $DeployOperationalLogsDCR
        ServicePrincipalAnalysis = $null
        PermissionsAnalysis      = $null
        ResourceAnalysis         = @()
    }

    # Check resource group existence
    try {
        $null = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction Stop
        $analysis.ResourceGroupExists = $true
    }
    catch {
        $analysis.ResourceGroupExists = $false
        return $analysis
    }

    # Check RealmJoin service principal (not covered by ARM WhatIf)
    if ($DeployAutomationAccount -or $DeployLogAnalytics) {
        $analysis.ServicePrincipalAnalysis = Get-RJServicePrincipalAnalysis
    }

    # Use actual SP ObjectId or a dummy GUID for ARM WhatIf template parameter
    $spObjectId = if ($analysis.ServicePrincipalAnalysis -and $analysis.ServicePrincipalAnalysis.Exists) {
        $analysis.ServicePrincipalAnalysis.ObjectId
    } else {
        '00000000-0000-0000-0000-000000000000'
    }

    # ARM WhatIf for Automation Account template
    if ($DeployAutomationAccount) {
        $aaTemplatePath = Get-ArmTemplatePath -RelativePath "azure-automation-account/automationAccount.json"
        $aaParams = @{
            rjServicePrincipalId = $spObjectId
            contributorRoleId    = $ContributorRoleId
        }
        if ($AutomationAccountName) {
            $aaParams.automationAccountName = $AutomationAccountName
        }

        $aaChanges = Get-RJArmTemplateWhatIf -ResourceGroupName $ResourceGroupName `
            -TemplatePath $aaTemplatePath -Parameters $aaParams

        foreach ($change in $aaChanges) {
            $resourceName = ($change.RelativeResourceId -split '/')[-1]
            # Modify is intentionally treated as Exists - ARM WhatIf's Modify signal is
            # unreliable for our templates (Azure/arm-template-whatif#310); only Create
            # is trustworthy enough to surface as an actionable change.
            $changeType = if ($change.ChangeType -eq 'Create') { 'Create' } else { 'Exists' }

            $analysis.ResourceAnalysis += [PSCustomObject]@{
                ResourceType    = $change.RelativeResourceId -replace '/[^/]+$', ''
                ResourceName    = $resourceName
                ChangeType      = $changeType
                FullResourceId  = $change.FullyQualifiedResourceId
                Category        = 'AutomationAccount'
            }
        }

        # Check if AA already exists to get its managed identity principal ID
        $aaResource = $analysis.ResourceAnalysis | Where-Object { $_.Category -eq 'AutomationAccount' -and $_.ResourceType -match 'automationAccounts$' } | Select-Object -First 1
        $managedIdentityPrincipalId = $null

        # Resolve AA name: explicit param or from ARM WhatIf result
        $aaLookupName = if ($AutomationAccountName) { $AutomationAccountName }
                        elseif ($aaResource) { $aaResource.ResourceName }
                        else { $null }

        if ($aaResource -and $aaResource.ChangeType -eq 'Exists' -and $aaLookupName) {
            $aaAccount = Invoke-AzCommandWithNotFoundHandling -ScriptBlock {
                Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $aaLookupName -ErrorAction Stop
            }
            if ($aaAccount -and $aaAccount.Identity -and $aaAccount.Identity.PrincipalId) {
                $managedIdentityPrincipalId = $aaAccount.Identity.PrincipalId
                Write-Verbose "Found managed identity principal ID: $managedIdentityPrincipalId"
            }
        }

        $analysis.PermissionsAnalysis = Get-RJAutomationAccountPermissionsAnalysis -ManagedIdentityPrincipalId $managedIdentityPrincipalId
    }

    # ARM WhatIf for Log Analytics Workspace template
    if ($DeployLogAnalytics) {
        $lawTemplatePath = Get-ArmTemplatePath -RelativePath "log-analytics-workspace/logAnalyticsWorkspace.json"
        $lawParams = @{
            servicePrincipalId               = $spObjectId
            deployAuditLogsDCR               = $DeployAuditLogsDCR
            deployRunbookLogsDCR             = $DeployRunbookLogsDCR
            deployOperationalLogsDCR         = $DeployOperationalLogsDCR
            logAnalyticsReaderRoleId         = $LogAnalyticsReaderRoleId
            monitoringMetricsPublisherRoleId = $MonitoringMetricsPublisherRoleId
        }
        if ($WorkspaceName) {
            $lawParams.workspaceName = $WorkspaceName
        }

        $lawChanges = Get-RJArmTemplateWhatIf -ResourceGroupName $ResourceGroupName `
            -TemplatePath $lawTemplatePath -Parameters $lawParams

        foreach ($change in $lawChanges) {
            $resourceName = ($change.RelativeResourceId -split '/')[-1]
            # Modify is intentionally treated as Exists - ARM WhatIf's Modify signal is
            # unreliable for our templates (Azure/arm-template-whatif#310); only Create
            # is trustworthy enough to surface as an actionable change.
            $changeType = if ($change.ChangeType -eq 'Create') { 'Create' } else { 'Exists' }

            $analysis.ResourceAnalysis += [PSCustomObject]@{
                ResourceType    = $change.RelativeResourceId -replace '/[^/]+$', ''
                ResourceName    = $resourceName
                ChangeType      = $changeType
                FullResourceId  = $change.FullyQualifiedResourceId
                Category        = 'LogAnalytics'
            }
        }
    }

    return $analysis
}

function Show-RJAzureResourceAnalysis {
    <#
    .SYNOPSIS
    Displays the Azure resource deployment analysis in a formatted, color-coded output.
 
    .PARAMETER Analysis
    The analysis object from Get-RJAzureResourceAnalysis.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$Analysis
    )

    Write-Host ""
    Write-Host "Azure Resource Deployment Analysis:" -ForegroundColor Cyan
    Write-Host ("=" * 80) -ForegroundColor Gray

    # Resource group status
    if (-not $Analysis.ResourceGroupExists) {
        Write-Host ""
        Write-Host "Resource Group: $($Analysis.ResourceGroupName)" -ForegroundColor Cyan
        Write-Host ("-" * 60) -ForegroundColor Gray
        Write-Host " ✗ Resource group not found" -ForegroundColor Red
        Write-Host " The resource group must exist before deployment." -ForegroundColor Yellow
        Write-Host " Create it with: New-AzResourceGroup -Name '$($Analysis.ResourceGroupName)' -Location '<location>'" -ForegroundColor Gray
        Write-Host ""
        return
    }

    Write-Host ""
    Write-Host "Resource Group: $($Analysis.ResourceGroupName)" -ForegroundColor Cyan
    Write-Host ("-" * 60) -ForegroundColor Gray
    Write-Host " ✓ Resource group exists" -ForegroundColor Green

    # Service principal status (Graph extension — not in ARM WhatIf)
    if ($Analysis.ServicePrincipalAnalysis) {
        $sp = $Analysis.ServicePrincipalAnalysis
        $spDisplayName = if ($sp.Exists) { "($($sp.DisplayName))" } else { "" }

        Write-Host ""
        Write-Host "Service Principal: $(Get-RJAzureResourcesAppId) $spDisplayName" -ForegroundColor Cyan
        Write-Host ("-" * 60) -ForegroundColor Gray

        if ($sp.ChangeType -eq 'Create') {
            Write-Host "→ Service principal will be CREATED" -ForegroundColor Yellow
        }
        else {
            Write-Host " ✓ No changes required" -ForegroundColor Green
        }
    }

    # Group ARM WhatIf results by category
    if ($Analysis.DeployAutomationAccount) {
        $aaResources = $Analysis.ResourceAnalysis | Where-Object { $_.Category -eq 'AutomationAccount' }
        $aaDisplayName = if ($Analysis.AutomationAccountName -ne '(auto-generated)') { $Analysis.AutomationAccountName } else { "(auto-generated)" }

        Write-Host ""
        Write-Host "Automation Account: $aaDisplayName" -ForegroundColor Cyan
        Write-Host ("-" * 60) -ForegroundColor Gray

        if ($aaResources.Count -gt 0) {
            foreach ($resource in $aaResources) {
                Write-Host " 📋 Resource: $($resource.ResourceName)" -ForegroundColor White

                if ($resource.ChangeType -eq 'Create') {
                    Write-Host " → Will be created" -ForegroundColor Yellow
                }
                else {
                    Write-Host " ✓ No changes required" -ForegroundColor Green
                }
                Write-Host " Type: $($resource.ResourceType)" -ForegroundColor Gray

                if ($resource -ne $aaResources[-1]) {
                    Write-Host ""
                }
            }
        }
        else {
            Write-Host " (no ARM WhatIf data available)" -ForegroundColor Gray
        }

        # Permissions analysis for AA managed identity
        if ($Analysis.PermissionsAnalysis) {
            Write-Host ""
            Write-Host "Automation Account Permissions:" -ForegroundColor Cyan
            Write-Host ("-" * 60) -ForegroundColor Gray

            foreach ($service in $Analysis.PermissionsAnalysis) {
                Write-Host " 📋 Service: $($service.ServiceName)" -ForegroundColor White

                switch ($service.ChangeType) {
                    'NoChanges' {
                        Write-Host " ✓ No changes required" -ForegroundColor Green
                        Write-Host " Current permissions ($($service.TotalDesired)): $($service.ExistingPermissions -join ', ')" -ForegroundColor Gray
                    }
                    'CreateAll' {
                        Write-Host " → Will assign all permissions (new managed identity)" -ForegroundColor Yellow
                        foreach ($perm in $service.PermissionsToAdd) {
                            Write-Host " + $perm" -ForegroundColor Green
                        }
                    }
                    'AddPermissions' {
                        Write-Host " → Will add $($service.PermissionsToAdd.Count) permission(s)" -ForegroundColor Yellow
                        foreach ($perm in $service.PermissionsToAdd) {
                            Write-Host " + $perm" -ForegroundColor Green
                        }
                        if ($service.ExistingPermissions.Count -gt 0) {
                            Write-Host " Existing ($($service.ExistingPermissions.Count)): $($service.ExistingPermissions -join ', ')" -ForegroundColor Gray
                        }
                    }
                }

                if ($service -ne $Analysis.PermissionsAnalysis[-1]) {
                    Write-Host ""
                }
            }
        }
    }

    if ($Analysis.DeployLogAnalytics) {
        $laResources = $Analysis.ResourceAnalysis | Where-Object { $_.Category -eq 'LogAnalytics' }
        $laDisplayName = if ($Analysis.WorkspaceName -ne '(auto-generated)') { $Analysis.WorkspaceName } else { "(auto-generated)" }

        Write-Host ""
        Write-Host "Log Analytics Workspace: $laDisplayName" -ForegroundColor Cyan
        Write-Host ("-" * 60) -ForegroundColor Gray

        $dcrList = @()
        if ($Analysis.DeployAuditLogsDCR) { $dcrList += 'Audit' }
        if ($Analysis.DeployRunbookLogsDCR) { $dcrList += 'Runbook' }
        if ($Analysis.DeployOperationalLogsDCR) { $dcrList += 'Operational' }
        $dcrText = if ($dcrList.Count -gt 0) { $dcrList -join ', ' } else { "(none)" }
        Write-Host " DCRs: $dcrText" -ForegroundColor Gray

        if ($laResources.Count -gt 0) {
            Write-Host ""
            foreach ($resource in $laResources) {
                Write-Host " 📋 Resource: $($resource.ResourceName)" -ForegroundColor White

                if ($resource.ChangeType -eq 'Create') {
                    Write-Host " → Will be created" -ForegroundColor Yellow
                }
                else {
                    Write-Host " ✓ No changes required" -ForegroundColor Green
                }
                Write-Host " Type: $($resource.ResourceType)" -ForegroundColor Gray

                if ($resource -ne $laResources[-1]) {
                    Write-Host ""
                }
            }
        }
        else {
            Write-Host " (no ARM WhatIf data available)" -ForegroundColor Gray
        }
    }

    # Summary
    $resourcesToCreate = $Analysis.ResourceAnalysis | Where-Object { $_.ChangeType -eq 'Create' }
    $createCount = @($resourcesToCreate).Count
    $existsCount = ($Analysis.ResourceAnalysis | Where-Object { $_.ChangeType -eq 'Exists' }).Count
    if ($Analysis.ServicePrincipalAnalysis) {
        if ($Analysis.ServicePrincipalAnalysis.ChangeType -eq 'Create') { $createCount++ } else { $existsCount++ }
    }

    # Count permission changes
    $permissionsToAddCount = 0
    if ($Analysis.PermissionsAnalysis) {
        $permissionsToAddCount = ($Analysis.PermissionsAnalysis | ForEach-Object { $_.PermissionsToAdd.Count } | Measure-Object -Sum).Sum
    }

    Write-Host ""
    Write-Host ("=" * 80) -ForegroundColor Gray
    Write-Host "Summary: " -NoNewline -ForegroundColor Gray
    Write-Host "$createCount to create" -NoNewline -ForegroundColor Yellow
    Write-Host ", " -NoNewline -ForegroundColor Gray
    Write-Host "$existsCount already exist" -NoNewline -ForegroundColor Green
    if ($permissionsToAddCount -gt 0) {
        Write-Host ", " -NoNewline -ForegroundColor Gray
        Write-Host "$permissionsToAddCount permission(s) to assign" -ForegroundColor Yellow
    }
    else {
        Write-Host ""
    }

    if ($createCount -gt 0) {
        Write-Host ""
        Write-Host "Resources to create ($createCount):" -ForegroundColor Yellow

        if ($Analysis.ServicePrincipalAnalysis -and $Analysis.ServicePrincipalAnalysis.ChangeType -eq 'Create') {
            Write-Host " + Service principal ($(Get-RJAzureResourcesAppId))" -ForegroundColor Green
        }

        foreach ($resource in $resourcesToCreate) {
            Write-Host " + $($resource.ResourceName) ($($resource.ResourceType))" -ForegroundColor Green
        }
    }
    Write-Host ""
}