Modules/Public/PublicFunctions/Diagram/Start-AZTIDiagramJob.ps1

<#
.Synopsis
Build the resource lookup the draw.io diagram builders consume.

.DESCRIPTION
Filters the resource set by the types the network and organisation diagrams draw, and returns
them as a hashtable keyed by the short names those builders expect.

.Link
https://github.com/thisismydemo/azure-scout/Modules/Public/PublicFunctions/Diagram/Start-AZSCDiagramJob.ps1

.COMPONENT
This PowerShell Module is part of Azure Scout (AZSC)

.NOTES
Version: 3.6.0
First Release Date: 15th Oct, 2024
Authors: Claudio Merola
#>


Function Start-AZSCDiagramJob {
    Param($Resources, $Automation)
    # ── StrictMode boundary (AB#5633, revised by AB#5649) ────────────────────────────
    # v1 inventory engine (forked from microsoft/ARI), written without StrictMode: it reads
    # optional fields off Azure payloads whose shape varies by tenant. Removing this opt-out
    # altogether is AB#5667's job, with the recorded live-payload fixtures needed to do it
    # safely. StrictMode is dynamically scoped, so this covers this call tree only.
    Set-StrictMode -Off

    <#
        AB#5649 — this was 290 lines and is now a table.

        It used to be TWO copies of the same work behind an if/else on $Automation:

          * -Automation: Start-ThreadJob containing 27 `Where-Object` filters.
          * otherwise: Start-Job containing 27 SEPARATE `[PowerShell]::Create()` runspaces,
                         one per resource type, each running a single Where-Object; then 27
                         BeginInvoke calls, a wait loop, 27 EndInvoke calls and 27 Dispose
                         calls — to perform 27 array filters over an in-memory collection.

        Both assembled the identical 27-key hashtable, so the two copies were pure duplication
        of a mapping that is really just data. The runspace copy also carried its own instance
        of the `$Job.Runspace.IsCompleted` no-op wait (AB#5629/AB#5633): PowerShellAsyncResult
        has no .Runspace, so the loop never waited and EndInvoke raced its own work.

        Filtering an in-memory array needs no concurrency at all. One pass over $Resources now
        bucket-sorts every type at once, which is also strictly less work than 27 passes.

        $Automation is kept on the signature for caller compatibility and is no longer read —
        there is no job left for it to choose a flavour of.
    #>


    # Key -> resource type. The keys are the names the diagram builders index by; several
    # deliberately differ from the type (AKS, VM, AppGtw…), which is why this stays an explicit
    # map rather than something derived from the type string.
    $TypeMap = [ordered]@{
        'AZVGWs'      = 'microsoft.network/virtualnetworkgateways'
        'AZLGWs'      = 'microsoft.network/localnetworkgateways'
        'AZVNETs'     = 'microsoft.network/virtualnetworks'
        'AZCONs'      = 'microsoft.network/connections'
        'AZEXPROUTEs' = 'microsoft.network/expressroutecircuits'
        'PIPs'        = 'microsoft.network/publicipaddresses'
        'AZVWAN'      = 'microsoft.network/virtualwans'
        'AZVHUB'      = 'microsoft.network/virtualhubs'
        'AZVPNSITES'  = 'microsoft.network/vpnsites'
        'AZVERs'      = 'microsoft.network/expressroutegateways'
        'AKS'         = 'microsoft.containerservice/managedclusters'
        'VMSS'        = 'microsoft.compute/virtualmachinescalesets'
        'NIC'         = 'microsoft.network/networkinterfaces'
        'PrivEnd'     = 'microsoft.network/privateendpoints'
        'VM'          = 'microsoft.compute/virtualmachines'
        'ARO'         = 'microsoft.redhatopenshift/openshiftclusters'
        'Kusto'       = 'microsoft.kusto/clusters'
        'AppGtw'      = 'microsoft.network/applicationgateways'
        'Databricks'  = 'microsoft.databricks/workspaces'
        'AppWeb'      = 'microsoft.web/sites'
        'APIM'        = 'microsoft.apimanagement/service'
        'LB'          = 'microsoft.network/loadbalancers'
        'Bastion'     = 'microsoft.network/bastionhosts'
        'FW'          = 'microsoft.network/azurefirewalls'
        'NetProf'     = 'microsoft.network/networkprofiles'
        'Container'   = 'microsoft.containerinstance/containergroups'
        'ANF'         = 'microsoft.netapp/netappaccounts/capacitypools/volumes'
    }

    # Bucket-sort in a single pass. The original compared with -eq, which is case-insensitive
    # for strings, so the types above are lower-cased and matched against a lower-cased key.
    $Buckets = @{}
    foreach ($Key in $TypeMap.Keys) { $Buckets[$TypeMap[$Key]] = [System.Collections.Generic.List[object]]::new() }

    foreach ($Resource in $Resources) {
        if ($null -eq $Resource) { continue }
        $Type = [string]$Resource.Type
        if (-not $Type) { continue }
        $Type = $Type.ToLowerInvariant()
        if ($Buckets.ContainsKey($Type)) { $Buckets[$Type].Add($Resource) }
    }

    $Variables = @{}
    foreach ($Key in $TypeMap.Keys) {
        $Matched = $Buckets[$TypeMap[$Key]]

        # $null for "none", NOT an empty collection — matching what `$Resources | Where-Object`
        # produced. The distinction is load-bearing downstream: under StrictMode, member
        # enumeration over an EMPTY collection throws "property cannot be found", while the same
        # read over $null is safe. That is the AB#5633 crash class; handing the diagram builders
        # empty lists instead of $null would walk straight back into it.
        $Variables[$Key] = if ($Matched.Count -eq 0) { $null }
                           elseif ($Matched.Count -eq 1) { $Matched[0] }
                           else { $Matched.ToArray() }
    }

    $Variables
}