src/pipeline/diagram/New-ScoutUniversalRelationshipDiagram.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Write a universal draw.io page from every discovered resource and ARM reference.
 
.DESCRIPTION
    The curated network diagram remains useful for presentation, but it cannot be the
    completeness boundary because it knows a finite resource-type map. This page uses the same
    generic ARM-reference extraction as the discovery report. Every discovered ARM resource is a
    node, every ARM id found in its control-plane properties is an edge, and referenced targets
    outside the discovered set are retained as explicit external-reference nodes.
 
.NOTES
    AB#7367.
#>

function New-ScoutUniversalRelationshipDiagram {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Resources,
        [Parameter(Mandatory)][string]$Path
    )

    if (-not (Get-Command Get-ScoutResourceCompleteness -ErrorAction SilentlyContinue)) {
        . (Join-Path (Split-Path $PSScriptRoot -Parent) 'Get-ScoutResourceCompleteness.ps1')
    }
    $discovery = Get-ScoutResourceCompleteness -Resources $Resources
    $nodesById = [ordered]@{}
    foreach ($resource in @($discovery.Resources)) {
        if (-not $resource.Id) { continue }
        $nodesById[[string]$resource.Id] = [pscustomobject]@{
            Id       = [string]$resource.Id
            Name     = [string]$resource.Name
            Type     = [string]$resource.Type
            Exposure = [string]$resource.Exposure
            Detail   = [string]$resource.DetailStatus
            External = $false
        }
    }
    foreach ($relationship in @($discovery.Relationships)) {
        $targetId = [string]$relationship.TargetId
        if ([string]::IsNullOrWhiteSpace($targetId) -or $nodesById.Contains($targetId)) { continue }
        $targetSegments = @($targetId.TrimEnd('/') -split '/')
        $nodesById[$targetId] = [pscustomobject]@{
            Id       = $targetId
            Name     = if ($targetSegments.Count -gt 0) { $targetSegments[-1] } else { $targetId }
            Type     = 'Referenced resource outside discovered set'
            Exposure = 'Unknown'
            Detail   = 'ExternalReference'
            External = $true
        }
    }

    $settings = [System.Xml.XmlWriterSettings]::new()
    $settings.Indent = $true
    $settings.Encoding = [System.Text.UTF8Encoding]::new($false)
    $writer = [System.Xml.XmlWriter]::Create($Path, $settings)
    try {
        $writer.WriteStartDocument()
        $writer.WriteStartElement('mxfile')
        $writer.WriteAttributeString('host', 'AzureScout')
        $writer.WriteStartElement('diagram')
        $writer.WriteAttributeString('id', 'azure-scout-universal-resource-graph')
        $writer.WriteAttributeString('name', 'Universal Resource Graph')
        $writer.WriteStartElement('mxGraphModel')
        $writer.WriteAttributeString('grid', '1')
        $writer.WriteAttributeString('page', '1')
        $writer.WriteAttributeString('pageScale', '1')
        $writer.WriteAttributeString('pageWidth', '1169')
        $writer.WriteAttributeString('pageHeight', '827')
        $writer.WriteStartElement('root')
        foreach ($base in @(@{ Id = '0'; Parent = $null }, @{ Id = '1'; Parent = '0' })) {
            $writer.WriteStartElement('mxCell')
            $writer.WriteAttributeString('id', $base.Id)
            if ($base.Parent) { $writer.WriteAttributeString('parent', $base.Parent) }
            $writer.WriteEndElement()
        }

        $cellByResourceId = @{}
        $index = 0
        foreach ($entry in $nodesById.GetEnumerator()) {
            $node = $entry.Value
            $cellId = "resource-$index"
            $cellByResourceId[[string]$entry.Key] = $cellId
            $column = $index % 8
            $row = [math]::Floor($index / 8)
            $fill = switch ($node.Exposure) {
                'Public'  { '#f8cecc' }
                'Mixed'   { '#ffe6cc' }
                'Private' { '#d5e8d4' }
                default   { if ($node.External) { '#e1d5e7' } else { '#dae8fc' } }
            }
            $writer.WriteStartElement('mxCell')
            $writer.WriteAttributeString('id', $cellId)
            $label = '<b>{0}</b><br><font style="font-size: 9px">{1}</font>' -f $node.Name, $node.Type
            $writer.WriteAttributeString('value', $label)
            $writer.WriteAttributeString('style', "rounded=1;whiteSpace=wrap;html=1;fillColor=$fill;strokeColor=#666666;fontSize=10;")
            $writer.WriteAttributeString('vertex', '1')
            $writer.WriteAttributeString('parent', '1')
            $writer.WriteAttributeString('ResourceId', $node.Id)
            $writer.WriteAttributeString('ResourceType', $node.Type)
            $writer.WriteAttributeString('Exposure', $node.Exposure)
            $writer.WriteAttributeString('DetailStatus', $node.Detail)
            $writer.WriteStartElement('mxGeometry')
            $writer.WriteAttributeString('x', [string](20 + ($column * 225)))
            $writer.WriteAttributeString('y', [string](20 + ($row * 90)))
            $writer.WriteAttributeString('width', '205')
            $writer.WriteAttributeString('height', '64')
            $writer.WriteAttributeString('as', 'geometry')
            $writer.WriteEndElement()
            $writer.WriteEndElement()
            $index++
        }

        $edgeIndex = 0
        foreach ($relationship in @($discovery.Relationships)) {
            $sourceId = [string]$relationship.SourceId
            $targetId = [string]$relationship.TargetId
            if (-not $cellByResourceId.ContainsKey($sourceId) -or -not $cellByResourceId.ContainsKey($targetId)) { continue }
            $writer.WriteStartElement('mxCell')
            $writer.WriteAttributeString('id', "relationship-$edgeIndex")
            $writer.WriteAttributeString('value', [string]$relationship.RelationshipType)
            $writer.WriteAttributeString('style', 'edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;fontSize=8;')
            $writer.WriteAttributeString('edge', '1')
            $writer.WriteAttributeString('parent', '1')
            $writer.WriteAttributeString('source', $cellByResourceId[$sourceId])
            $writer.WriteAttributeString('target', $cellByResourceId[$targetId])
            $writer.WriteAttributeString('PropertyPath', [string]$relationship.PropertyPath)
            $writer.WriteStartElement('mxGeometry')
            $writer.WriteAttributeString('relative', '1')
            $writer.WriteAttributeString('as', 'geometry')
            $writer.WriteEndElement()
            $writer.WriteEndElement()
            $edgeIndex++
        }

        $writer.WriteEndElement()
        $writer.WriteEndElement()
        $writer.WriteEndElement()
        $writer.WriteEndElement()
        $writer.WriteEndDocument()
    }
    finally { $writer.Dispose() }

    [pscustomobject]@{ Path = $Path; Resources = $nodesById.Count; Relationships = @($discovery.Relationships).Count }
}