#requires -Version 7.2
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidatePattern('^[0-9a-fA-F-]{36}$')]
    [string]$SubscriptionId,

    [ValidateSet('westeurope')]
    [string]$Location = 'westeurope',

    [ValidatePattern('^rg-avnm-deep-dive-we$')]
    [string]$ResourceGroupName = 'rg-avnm-deep-dive-we',

    [Parameter(Mandatory)]
    [ValidateSet('Preflight', 'Foundation', 'Ipam', 'Networks', 'DynamicGroup', 'Mesh', 'RepairMembership', 'SecurityAdmin', 'Validate')]
    [string]$Stage
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$script:NetworkApi = '2025-05-01'
$script:ComputeApi = '2025-04-01'
$script:PolicyApi = '2025-11-01'
$script:RoleApi = '2022-04-01'
$script:NetworkManagerName = 'avnm-governance-we'
$script:RootPoolName = 'ipam-avnm-root'
$script:ChildPoolName = 'ipam-avnm-westeurope'
$script:ReservedCidrName = 'reserved-platform-services'
$script:NetworkGroupName = 'ng-avnm-mesh-we'
$script:ConnectivityConfigurationName = 'cc-avnm-regional-mesh-we'
$script:SecurityConfigurationName = 'sac-avnm-central-we'
$script:RuleCollectionName = 'rc-avnm-mesh-we'
$script:AdminRuleName = 'Deny-Unmanaged-App-Port'
$script:PolicyDefinitionName = 'avnm-add-tagged-vnets-to-mesh'
$script:PolicyAssignmentName = 'avnm-mesh-dynamic-membership'
$script:PrivateRoot = Join-Path $PSScriptRoot '..\.private'
$script:TranscriptPath = Join-Path $PSScriptRoot '..\validation-transcript.md'
$script:Tags = @{
    Workshop = 'AVNM-DeepDive'
    ManagedBy = 'AzureCLI-PowerShell'
    Purpose = 'AzureWorkshopz'
}
$script:Vnets = @(
    [ordered]@{ Name = 'vnet-avnm-core-we'; Role = 'core'; GroupTag = 'Mesh'; HasVm = $true; VmName = 'vm-avnm-core-we' },
    [ordered]@{ Name = 'vnet-avnm-app1-we'; Role = 'app1'; GroupTag = 'Mesh'; HasVm = $false; VmName = $null },
    [ordered]@{ Name = 'vnet-avnm-app2-we'; Role = 'app2'; GroupTag = 'Mesh'; HasVm = $false; VmName = $null },
    [ordered]@{ Name = 'vnet-avnm-edge-we'; Role = 'edge'; GroupTag = 'Mseh'; HasVm = $true; VmName = 'vm-avnm-edge-we' }
)

function Write-Lab {
    param([string]$Message, [ValidateSet('INFO', 'PASS', 'WAIT', 'WARN')][string]$Level = 'INFO')
    $safe = $Message -replace [regex]::Escape($SubscriptionId), '<subscription-redacted>'
    $safe = $safe -replace '(?i)/subscriptions/[0-9a-f-]{36}', '/subscriptions/<redacted>'
    $safe = $safe -replace '(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b', '<guid-redacted>'
    $line = '[{0}] {1}' -f $Level, $safe
    Write-Host $line
    if (-not (Test-Path -LiteralPath $script:TranscriptPath)) {
        "# Sanitized validation transcript`n" | Out-File -LiteralPath $script:TranscriptPath -Encoding utf8
    }
    "- ``$line``" | Out-File -LiteralPath $script:TranscriptPath -Append -Encoding utf8
}

function Invoke-Az {
    param(
        [Parameter(Mandatory)][string[]]$Arguments,
        [switch]$Json,
        [switch]$AllowFailure
    )
    $output = & az @Arguments 2>&1
    $code = $LASTEXITCODE
    if ($code -ne 0 -and -not $AllowFailure) {
        $safe = ($output -join "`n") -replace [regex]::Escape($SubscriptionId), '<subscription-redacted>'
        throw "Azure CLI failed: $safe"
    }
    if ($AllowFailure) {
        return [pscustomobject]@{ ExitCode = $code; Output = ($output -join "`n") }
    }
    if ($Json) {
        $text = ($output -join "`n").Trim()
        if (-not $text) { return $null }
        return $text | ConvertFrom-Json -Depth 100
    }
    return ($output -join "`n").Trim()
}

function Assert-ExactSubscription {
    $active = Invoke-Az -Arguments @('account', 'show', '--query', '{id:id,state:state,name:name}', '-o', 'json') -Json
    if ($active.state -ne 'Enabled' -or $active.id -ne $SubscriptionId) {
        throw 'The active Azure CLI subscription is not the exact enabled subscription supplied to this script.'
    }
    Write-Lab "Exact active subscription confirmed: $($active.name) (identifier redacted)." PASS
}

function Get-ResourceId {
    param([Parameter(Mandatory)][string]$RelativePath)
    return "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/$RelativePath"
}

function Get-TagArguments {
    param([hashtable]$Additional = @{})
    $all = [ordered]@{}
    foreach ($entry in $script:Tags.GetEnumerator()) { $all[$entry.Key] = $entry.Value }
    foreach ($entry in $Additional.GetEnumerator()) { $all[$entry.Key] = $entry.Value }
    $arguments = @()
    foreach ($key in $all.Keys) { $arguments += "$key=$($all[$key])" }
    return $arguments
}

function Wait-Until {
    param(
        [Parameter(Mandatory)][scriptblock]$Condition,
        [Parameter(Mandatory)][string]$Description,
        [int]$TimeoutSeconds = 1800,
        [int]$IntervalSeconds = 20
    )
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    do {
        try {
            if (& $Condition) {
                Write-Lab "$Description is ready." PASS
                return
            }
        }
        catch {
            Write-Lab "$Description is still converging." WAIT
        }
        if ((Get-Date) -ge $deadline) { throw "Timed out waiting for $Description." }
        Start-Sleep -Seconds $IntervalSeconds
    } while ($true)
}

function ConvertTo-First24 {
    param([Parameter(Mandatory)][string]$Prefix)
    $parts = $Prefix.Split('/')
    if ([int]$parts[1] -gt 24) { throw "Cannot derive a /24 subnet from $Prefix." }
    return "$($parts[0])/24"
}

function Get-AllocatedPrefix {
    param([Parameter(Mandatory)][string]$VnetName)
    $value = Invoke-Az -Arguments @('network', 'vnet', 'show', '-g', $ResourceGroupName, '-n', $VnetName, '--query', 'addressSpace.ipamPoolPrefixAllocations[0].allocatedAddressPrefixes[0]', '-o', 'tsv')
    if (-not $value -or $value -eq 'null') { return $null }
    return $value
}

function Get-PrivateAddress {
    param([Parameter(Mandatory)][string]$Role)
    return Invoke-Az -Arguments @('network', 'nic', 'show', '-g', $ResourceGroupName, '-n', "nic-avnm-$Role-we", '--query', 'ipConfigurations[0].privateIPAddress', '-o', 'tsv')
}

function Get-NetworkGroupMemberCount {
    $groupId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/networkGroups/$($script:NetworkGroupName)"
    $query = "networkresources | where type =~ 'microsoft.network/networkgroupmemberships' | mv-expand membership=properties.groupMemberships | where tostring(membership.networkGroupId) =~ '$groupId' | summarize members=count() | project members"
    $result = Invoke-Az -Arguments @('graph', 'query', '-q', $query, '--first', '100', '-o', 'json') -Json
    if (-not $result.data -or $result.data.Count -eq 0) { return 0 }
    return [int]$result.data[0].members
}

function Get-GuestPortResults {
    $edgeAddress = Get-PrivateAddress -Role 'edge'
    $scriptText = "for port in 8080 8443; do timeout 6 bash -c '</dev/tcp/$edgeAddress/'`$port >/dev/null 2>&1; rc=`$?; if [ `$rc -eq 0 ]; then echo PORT=`$port RESULT=OPEN; else echo PORT=`$port RESULT=BLOCKED; fi; done; exit 0"
    $result = Invoke-Az -Arguments @('vm', 'run-command', 'invoke', '-g', $ResourceGroupName, '-n', 'vm-avnm-core-we', '--command-id', 'RunShellScript', '--scripts', $scriptText, '--query', 'value[0].message', '-o', 'tsv')
    return $result.Trim()
}

function Invoke-Preflight {
    Assert-ExactSubscription
    $versionDocument = Invoke-Az -Arguments @('version', '-o', 'json') -Json
    $cliVersion = $versionDocument.PSObject.Properties['azure-cli'].Value
    $pwshVersion = $PSVersionTable.PSVersion.ToString()
    Write-Lab "PowerShell $pwshVersion and Azure CLI $cliVersion detected." PASS

    $extensionVersion = Invoke-Az -Arguments @('extension', 'show', '--name', 'virtual-network-manager', '--query', 'version', '-o', 'tsv') -AllowFailure
    if ($extensionVersion.ExitCode -ne 0) {
        Invoke-Az -Arguments @('extension', 'add', '--name', 'virtual-network-manager', '--upgrade', '--only-show-errors', '-o', 'none') | Out-Null
        $extensionVersion = Invoke-Az -Arguments @('extension', 'show', '--name', 'virtual-network-manager', '--query', 'version', '-o', 'tsv')
    }
    else { $extensionVersion = $extensionVersion.Output.Trim() }
    Write-Lab "virtual-network-manager CLI extension $extensionVersion is available." PASS

    foreach ($provider in @('Microsoft.Network', 'Microsoft.Compute', 'Microsoft.PolicyInsights')) {
        $state = Invoke-Az -Arguments @('provider', 'show', '--namespace', $provider, '--query', 'registrationState', '-o', 'tsv')
        if ($state -ne 'Registered') { Invoke-Az -Arguments @('provider', 'register', '--namespace', $provider, '--wait', '-o', 'none') | Out-Null }
        Write-Lab "$provider is registered." PASS
    }

    $existingGroup = Invoke-Az -Arguments @('group', 'exists', '--name', $ResourceGroupName, '-o', 'tsv')
    if ($existingGroup -eq 'true') {
        $tag = Invoke-Az -Arguments @('group', 'show', '--name', $ResourceGroupName, '--query', 'tags.Workshop', '-o', 'tsv')
        if ($tag -ne 'AVNM-DeepDive') { throw "Safety stop: $ResourceGroupName already exists without the expected workshop tag." }
        Write-Lab 'An idempotent rerun of the dedicated workshop resource group is permitted.' WARN
    }

    $overlap = Invoke-Az -Arguments @('graph', 'query', '-q', "resources | where type =~ 'microsoft.network/virtualnetworks' | mv-expand prefix=properties.addressSpace.addressPrefixes | project name, prefix=tostring(prefix)", '--first', '1000', '-o', 'json') -Json
    $conflicts = @($overlap.data | Where-Object { $_.prefix -like '10.140.*' -and $_.name -notlike 'vnet-avnm-*-we' })
    if ($conflicts.Count -gt 0) { throw 'Safety stop: the intended 10.140.0.0/14 workshop space overlaps an existing Azure VNet.' }
    Write-Lab 'The intended 10.140.0.0/14 workshop range has no existing Azure VNet conflict.' PASS

    $sku = Invoke-Az -Arguments @('vm', 'list-skus', '--location', $Location, '--size', 'Standard_B1s', '--all', '--query', '[?restrictions==`[]`].name | [0]', '-o', 'tsv')
    if ($sku -ne 'Standard_B1s') { throw 'Standard_B1s is not currently available in West Europe for this subscription.' }
    Write-Lab 'Ubuntu 24.04 and Standard_B1s capacity preflight passed.' PASS
}

function Invoke-Foundation {
    Assert-ExactSubscription
    Invoke-Az -Arguments (@('group', 'create', '--name', $ResourceGroupName, '--location', $Location, '--tags') + (Get-TagArguments) + @('-o', 'none')) | Out-Null

    $manager = Invoke-Az -Arguments @('network', 'manager', 'show', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '-o', 'none') -AllowFailure
    if ($manager.ExitCode -ne 0) {
        Invoke-Az -Arguments (@('network', 'manager', 'create', '--name', $script:NetworkManagerName, '--location', $Location, '--description', 'Azure Workshopz IPAM, mesh, and central security lab', '--scope-accesses', 'Connectivity', 'SecurityAdmin', '--network-manager-scopes', "subscriptions=/subscriptions/$SubscriptionId", '--resource-group', $ResourceGroupName, '--tags') + (Get-TagArguments) + @('--only-show-errors', '-o', 'none')) | Out-Null
    }
    Write-Lab 'Dedicated resource group and subscription-scoped Virtual Network Manager are provisioned.' PASS
}

function Invoke-Ipam {
    Assert-ExactSubscription
    $root = Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'show', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '-n', $script:RootPoolName, '-o', 'none') -AllowFailure
    if ($root.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'create', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '-n', $script:RootPoolName, '--location', $Location, '--address-prefixes', '10.140.0.0/14', '--description', 'Root IPv4 pool for the Azure Workshopz AVNM lab', '--only-show-errors', '-o', 'none') | Out-Null
    }
    $child = Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'show', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '-n', $script:ChildPoolName, '-o', 'none') -AllowFailure
    if ($child.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'create', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '-n', $script:ChildPoolName, '--location', $Location, '--address-prefixes', '10.140.0.0/16', '--parent-pool-name', $script:RootPoolName, '--description', 'West Europe allocation pool', '--only-show-errors', '-o', 'none') | Out-Null
    }
    $reserved = Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'static-cidr', 'show', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '--pool-name', $script:ChildPoolName, '-n', $script:ReservedCidrName, '-o', 'none') -AllowFailure
    if ($reserved.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'static-cidr', 'create', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '--pool-name', $script:ChildPoolName, '-n', $script:ReservedCidrName, '--address-prefixes', '10.140.240.0/20', '--description', 'Reserved and intentionally unused platform-services range', '--only-show-errors', '-o', 'none') | Out-Null
    }
    Write-Lab 'Root and West Europe IPAM pools are ready; the platform-services /20 is reserved.' PASS
}

function Initialize-PrivateSshKey {
    New-Item -ItemType Directory -Path $script:PrivateRoot -Force | Out-Null
    $keyPath = Join-Path $script:PrivateRoot 'id_ed25519'
    if (-not (Test-Path -LiteralPath $keyPath)) {
        & ssh-keygen -t ed25519 -N '' -f $keyPath -C 'azure-workshopz-avnm' | Out-Null
        if ($LASTEXITCODE -ne 0) { throw 'Unable to generate the temporary workshop SSH key.' }
    }
    return $keyPath
}

function Invoke-Networks {
    Assert-ExactSubscription
    $poolId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/ipamPools/$($script:ChildPoolName)"
    foreach ($vnet in $script:Vnets) {
        $exists = Invoke-Az -Arguments @('network', 'vnet', 'show', '-g', $ResourceGroupName, '-n', $vnet.Name, '-o', 'none') -AllowFailure
        if ($exists.ExitCode -ne 0) {
            Invoke-Az -Arguments (@('network', 'vnet', 'create', '-g', $ResourceGroupName, '-n', $vnet.Name, '-l', $Location, '--ipam-allocations', '[0].number-of-ip-addresses=4096', "[0].id=$poolId", '--tags') + (Get-TagArguments @{ NetworkGroup = $vnet.GroupTag; NetworkRole = $vnet.Role }) + @('--only-show-errors', '-o', 'none')) | Out-Null
        }
        Wait-Until -Description "$($vnet.Name) IPAM allocation" -TimeoutSeconds 900 -IntervalSeconds 15 -Condition { [bool](Get-AllocatedPrefix -VnetName $vnet.Name) }
        $prefix = Get-AllocatedPrefix -VnetName $vnet.Name
        $subnetPrefix = ConvertTo-First24 -Prefix $prefix
        $nsgName = "nsg-avnm-$($vnet.Role)-we"
        $subnetName = 'snet-workload'
        $nsgExists = Invoke-Az -Arguments @('network', 'nsg', 'show', '-g', $ResourceGroupName, '-n', $nsgName, '-o', 'none') -AllowFailure
        if ($nsgExists.ExitCode -ne 0) {
            Invoke-Az -Arguments (@('network', 'nsg', 'create', '-g', $ResourceGroupName, '-n', $nsgName, '-l', $Location, '--tags') + (Get-TagArguments @{ NetworkRole = $vnet.Role }) + @('-o', 'none')) | Out-Null
            Invoke-Az -Arguments @('network', 'nsg', 'rule', 'create', '-g', $ResourceGroupName, '--nsg-name', $nsgName, '-n', 'Allow-Workshop-Proof-Ports', '--priority', '200', '--direction', 'Inbound', '--access', 'Allow', '--protocol', 'Tcp', '--source-address-prefixes', '10.140.0.0/14', '--destination-port-ranges', '8080', '8443', '-o', 'none') | Out-Null
        }
        $subnetExists = Invoke-Az -Arguments @('network', 'vnet', 'subnet', 'show', '-g', $ResourceGroupName, '--vnet-name', $vnet.Name, '-n', $subnetName, '-o', 'none') -AllowFailure
        if ($subnetExists.ExitCode -ne 0) {
            Invoke-Az -Arguments @('network', 'vnet', 'subnet', 'create', '-g', $ResourceGroupName, '--vnet-name', $vnet.Name, '-n', $subnetName, '--address-prefixes', $subnetPrefix, '--network-security-group', $nsgName, '-o', 'none') | Out-Null
        }
        Write-Lab "$($vnet.Name) received an IPAM-allocated /20; its workload subnet is $subnetPrefix." PASS
    }

    $sshKey = Initialize-PrivateSshKey
    foreach ($vnet in @($script:Vnets | Where-Object HasVm)) {
        $role = $vnet.Role
        $vmExists = Invoke-Az -Arguments @('vm', 'show', '-g', $ResourceGroupName, '-n', $vnet.VmName, '-o', 'none') -AllowFailure
        if ($vmExists.ExitCode -ne 0) {
            $pipName = "pip-avnm-$role-we"
            $nicName = "nic-avnm-$role-we"
            Invoke-Az -Arguments (@('network', 'public-ip', 'create', '-g', $ResourceGroupName, '-n', $pipName, '-l', $Location, '--sku', 'Standard', '--allocation-method', 'Static', '--version', 'IPv4', '--tags') + (Get-TagArguments @{ NetworkRole = $role }) + @('-o', 'none')) | Out-Null
            Invoke-Az -Arguments @('network', 'nic', 'create', '-g', $ResourceGroupName, '-n', $nicName, '-l', $Location, '--vnet-name', $vnet.Name, '--subnet', 'snet-workload', '--network-security-group', "nsg-avnm-$role-we", '--public-ip-address', $pipName, '-o', 'none') | Out-Null
            Invoke-Az -Arguments (@('vm', 'create', '-g', $ResourceGroupName, '-n', $vnet.VmName, '-l', $Location, '--nics', $nicName, '--image', 'Canonical:ubuntu-24_04-lts:server:latest', '--size', 'Standard_B1s', '--admin-username', 'azureworkshop', '--ssh-key-values', "$sshKey.pub", '--storage-sku', 'Standard_LRS', '--security-type', 'Standard', '--tags') + (Get-TagArguments @{ NetworkRole = $role }) + @('--only-show-errors', '-o', 'none')) | Out-Null
        }
        Write-Lab "$($vnet.VmName) is private-admin only; no inbound SSH rule was created." PASS
    }

    $edgeSetupPath = (Resolve-Path (Join-Path $PSScriptRoot 'guest\Configure-EdgeProof.sh')).Path
    Invoke-Az -Arguments @('vm', 'run-command', 'invoke', '-g', $ResourceGroupName, '-n', 'vm-avnm-edge-we', '--command-id', 'RunShellScript', '--scripts', "@$edgeSetupPath", '--only-show-errors', '-o', 'none') | Out-Null
    Write-Lab 'Edge proof listeners are running on TCP 8080 and 8443.' PASS
}

function Invoke-DynamicGroup {
    Assert-ExactSubscription
    $groupExists = Invoke-Az -Arguments @('network', 'manager', 'group', 'show', '-g', $ResourceGroupName, '--network-manager-name', $script:NetworkManagerName, '-n', $script:NetworkGroupName, '-o', 'none') -AllowFailure
    if ($groupExists.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'group', 'create', '-g', $ResourceGroupName, '--network-manager-name', $script:NetworkManagerName, '-n', $script:NetworkGroupName, '--member-type', 'VirtualNetwork', '--description', 'Tag-driven West Europe regional mesh members', '-o', 'none') | Out-Null
    }
    $groupId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/networkGroups/$($script:NetworkGroupName)"
    $policyRule = [ordered]@{
        if = [ordered]@{
            allOf = @(
                @{ field = 'type'; equals = 'Microsoft.Network/virtualNetworks' },
                @{ field = 'tags[Workshop]'; equals = 'AVNM-DeepDive' },
                @{ field = 'tags[NetworkGroup]'; equals = 'Mesh' },
                @{ field = 'id'; like = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/*" }
            )
        }
        then = [ordered]@{ effect = 'addToNetworkGroup'; details = [ordered]@{ networkGroupId = $groupId } }
    }
    New-Item -ItemType Directory -Path $script:PrivateRoot -Force | Out-Null
    $policyPath = Join-Path $script:PrivateRoot 'network-group-policy.json'
    $policyRule | ConvertTo-Json -Depth 20 | Out-File -LiteralPath $policyPath -Encoding utf8
    $definitionId = Invoke-Az -Arguments @('policy', 'definition', 'show', '-n', $script:PolicyDefinitionName, '--subscription', $SubscriptionId, '--query', 'id', '-o', 'tsv') -AllowFailure
    if ($definitionId.ExitCode -ne 0) {
        $definitionId = Invoke-Az -Arguments @('policy', 'definition', 'create', '-n', $script:PolicyDefinitionName, '--display-name', 'Add tagged AVNM workshop VNets to the regional mesh', '--description', 'Exact workshop tag and resource-group membership policy.', '--mode', 'Microsoft.Network.Data', '--rules', "@$policyPath", '--subscription', $SubscriptionId, '--query', 'id', '-o', 'tsv')
    }
    else { $definitionId = $definitionId.Output.Trim() }
    $scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName"
    $assignment = Invoke-Az -Arguments @('policy', 'assignment', 'show', '-n', $script:PolicyAssignmentName, '--scope', $scope, '-o', 'none') -AllowFailure
    if ($assignment.ExitCode -ne 0) {
        Invoke-Az -Arguments @('policy', 'assignment', 'create', '-n', $script:PolicyAssignmentName, '--display-name', 'AVNM workshop tag-driven mesh membership', '--scope', $scope, '--policy', $definitionId, '--description', 'Adds only the correctly tagged workshop VNets to the AVNM network group.', '--enforcement-mode', 'Default', '-o', 'none') | Out-Null
    }
    foreach ($vnet in $script:Vnets) {
        Invoke-Az -Arguments @('resource', 'update', '--ids', (Get-ResourceId "Microsoft.Network/virtualNetworks/$($vnet.Name)"), '--set', "tags.Workshop=AVNM-DeepDive", "tags.NetworkGroup=$($vnet.GroupTag)", '-o', 'none') | Out-Null
    }
    Invoke-Az -Arguments @('policy', 'state', 'trigger-scan', '--resource-group', $ResourceGroupName, '--no-wait', '-o', 'none') -AllowFailure | Out-Null
    Write-Lab 'Dynamic Microsoft.Network.Data policy is assigned at the exact workshop resource group.' PASS
}

function Invoke-Mesh {
    Assert-ExactSubscription
    $groupId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/networkGroups/$($script:NetworkGroupName)"
    $appliesToGroups = ConvertTo-Json -InputObject @([ordered]@{
        networkGroupId = $groupId
        groupConnectivity = 'DirectlyConnected'
        isGlobal = $false
        useHubGateway = $false
    }) -Compress
    $connectivityCapabilities = ConvertTo-Json -InputObject ([ordered]@{
        connectedGroupAddressOverlap = 'Disallowed'
        connectedGroupPrivateEndpointsScale = 'Standard'
        peeringEnforcement = 'Unenforced'
    }) -Compress
    Invoke-Az -Arguments @('network', 'manager', 'connect-config', 'create', '-g', $ResourceGroupName, '--network-manager-name', $script:NetworkManagerName, '--configuration-name', $script:ConnectivityConfigurationName, '--description', 'West Europe tag-driven regional mesh', '--connectivity-topology', 'Mesh', '--applies-to-groups', $appliesToGroups, '--connect-capabilities', $connectivityCapabilities, '--delete-existing-peering', 'False', '--is-global', 'False', '-o', 'none') | Out-Null
    $configId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/connectivityConfigurations/$($script:ConnectivityConfigurationName)"
    Invoke-Az -Arguments @('network', 'manager', 'post-commit', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--commit-type', 'Connectivity', '--configuration-ids', $configId, '--target-locations', $Location, '--only-show-errors', '-o', 'none') | Out-Null
    Wait-Until -Description 'initial three-member dynamic network group' -TimeoutSeconds 1800 -IntervalSeconds 30 -Condition { (Get-NetworkGroupMemberCount) -eq 3 }
    $ports = Get-GuestPortResults
    if ($ports -notmatch 'PORT=8080 RESULT=BLOCKED' -or $ports -notmatch 'PORT=8443 RESULT=BLOCKED') { throw 'The deliberate pre-repair Core-to-Edge connectivity failure was not reproduced on both proof ports.' }
    Write-Lab 'The Edge typo leaves exactly three mesh members; Core-to-Edge TCP 8080 and 8443 are blocked.' PASS
}

function Invoke-RepairMembership {
    Assert-ExactSubscription
    $edgeId = Get-ResourceId 'Microsoft.Network/virtualNetworks/vnet-avnm-edge-we'
    Invoke-Az -Arguments @('resource', 'update', '--ids', $edgeId, '--set', 'tags.NetworkGroup=Mesh', '-o', 'none') | Out-Null
    Invoke-Az -Arguments @('policy', 'state', 'trigger-scan', '--resource-group', $ResourceGroupName, '--no-wait', '-o', 'none') -AllowFailure | Out-Null
    Wait-Until -Description 'repaired four-member dynamic network group' -TimeoutSeconds 1800 -IntervalSeconds 30 -Condition { (Get-NetworkGroupMemberCount) -eq 4 }
    $groupId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/networkGroups/$($script:NetworkGroupName)"
    $appliesToGroups = ConvertTo-Json -InputObject @([ordered]@{
        networkGroupId = $groupId
        groupConnectivity = 'DirectlyConnected'
        isGlobal = $false
        useHubGateway = $false
    }) -Compress
    $connectivityCapabilities = ConvertTo-Json -InputObject ([ordered]@{
        connectedGroupAddressOverlap = 'Disallowed'
        connectedGroupPrivateEndpointsScale = 'Standard'
        peeringEnforcement = 'Unenforced'
    }) -Compress
    Invoke-Az -Arguments @('network', 'manager', 'connect-config', 'create', '-g', $ResourceGroupName, '--network-manager-name', $script:NetworkManagerName, '--configuration-name', $script:ConnectivityConfigurationName, '--description', 'West Europe tag-driven regional mesh', '--connectivity-topology', 'Mesh', '--applies-to-groups', $appliesToGroups, '--connect-capabilities', $connectivityCapabilities, '--delete-existing-peering', 'False', '--is-global', 'False', '-o', 'none') | Out-Null
    $configId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/connectivityConfigurations/$($script:ConnectivityConfigurationName)"
    Invoke-Az -Arguments @('network', 'manager', 'post-commit', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--commit-type', 'Connectivity', '--configuration-ids', $configId, '--target-locations', $Location, '--only-show-errors', '-o', 'none') | Out-Null
    Wait-Until -Description 'regional mesh propagation to Edge' -TimeoutSeconds 1800 -IntervalSeconds 30 -Condition {
        $ports = Get-GuestPortResults
        $ports -match 'PORT=8080 RESULT=OPEN' -and $ports -match 'PORT=8443 RESULT=OPEN'
    }
    Write-Lab 'Correcting the Edge tag produces four dynamic members and both proof ports work without manual peering.' PASS
}

function Invoke-SecurityAdmin {
    Assert-ExactSubscription
    $groupId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/networkGroups/$($script:NetworkGroupName)"
    $configExists = Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'show', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '-o', 'none') -AllowFailure
    if ($configExists.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'create', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '--description', 'Central deny demonstrating Security Admin precedence', '--apply-on', 'None', '-o', 'none') | Out-Null
    }
    $collectionExists = Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'rule-collection', 'show', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '--rule-collection-name', $script:RuleCollectionName, '-o', 'none') -AllowFailure
    if ($collectionExists.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'rule-collection', 'create', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '--rule-collection-name', $script:RuleCollectionName, '--description', 'Mesh-wide workshop rule collection', '--applies-to-groups', "network-group-id=$groupId", '-o', 'none') | Out-Null
    }
    $ruleExists = Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'rule-collection', 'rule', 'show', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '--rule-collection-name', $script:RuleCollectionName, '--rule-name', $script:AdminRuleName, '-o', 'none') -AllowFailure
    if ($ruleExists.ExitCode -ne 0) {
        Invoke-Az -Arguments @('network', 'manager', 'security-admin-config', 'rule-collection', 'rule', 'create', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--configuration-name', $script:SecurityConfigurationName, '--rule-collection-name', $script:RuleCollectionName, '--rule-name', $script:AdminRuleName, '--kind', 'Custom', '--protocol', 'Tcp', '--access', 'Deny', '--priority', '100', '--direction', 'Inbound', '--sources', 'address-prefix=10.140.0.0/14', 'address-prefix-type=IPPrefix', '--destinations', 'address-prefix=10.140.0.0/14', 'address-prefix-type=IPPrefix', '--dest-port-ranges', '8080', '--description', 'Deny unmanaged application port before local NSG evaluation', '-o', 'none') | Out-Null
    }
    $configId = Get-ResourceId "Microsoft.Network/networkManagers/$($script:NetworkManagerName)/securityAdminConfigurations/$($script:SecurityConfigurationName)"
    Invoke-Az -Arguments @('network', 'manager', 'post-commit', '-g', $ResourceGroupName, '-n', $script:NetworkManagerName, '--commit-type', 'SecurityAdmin', '--configuration-ids', $configId, '--target-locations', $Location, '--only-show-errors', '-o', 'none') | Out-Null
    Wait-Until -Description 'Security Admin rule propagation' -TimeoutSeconds 1800 -IntervalSeconds 30 -Condition {
        $ports = Get-GuestPortResults
        $ports -match 'PORT=8080 RESULT=BLOCKED' -and $ports -match 'PORT=8443 RESULT=OPEN'
    }
    Write-Lab 'Security Admin blocks TCP 8080 before the explicit NSG allow; TCP 8443 remains open.' PASS
}

function Invoke-Validate {
    Assert-ExactSubscription
    $memberCount = Get-NetworkGroupMemberCount
    if ($memberCount -ne 4) { throw "Expected four dynamic members, found $memberCount." }
    $allocations = foreach ($vnet in $script:Vnets) {
        $prefix = Get-AllocatedPrefix -VnetName $vnet.Name
        if ($prefix -notmatch '/20$') { throw "$($vnet.Name) does not have a /20 IPAM allocation." }
        [pscustomobject]@{ VNet = $vnet.Name; Prefix = $prefix }
    }
    if (($allocations.Prefix | Sort-Object -Unique).Count -ne 4) { throw 'The four IPAM prefixes are not unique.' }
    $reserved = Invoke-Az -Arguments @('network', 'manager', 'ipam-pool', 'static-cidr', 'show', '-g', $ResourceGroupName, '--manager-name', $script:NetworkManagerName, '--pool-name', $script:ChildPoolName, '-n', $script:ReservedCidrName, '--query', 'properties.addressPrefixes[0]', '-o', 'tsv')
    if ($reserved -ne '10.140.240.0/20' -or $allocations.Prefix -contains $reserved) { throw 'The reserved /20 was not preserved as unused.' }
    $nsgs = Invoke-Az -Arguments @('network', 'nsg', 'list', '-g', $ResourceGroupName, '-o', 'json') -Json
    $sshRules = @($nsgs.securityRules | Where-Object {
        $singlePort = if ($_.PSObject.Properties['destinationPortRange']) { $_.destinationPortRange } else { $null }
        $portList = if ($_.PSObject.Properties['destinationPortRanges']) { @($_.destinationPortRanges) } else { @() }
        $_.access -eq 'Allow' -and $_.direction -eq 'Inbound' -and (
            $singlePort -eq '22' -or $portList -contains '22'
        )
    })
    if ($sshRules.Count -ne 0) { throw 'An inbound Internet-facing SSH allow rule exists.' }
    $ports = Get-GuestPortResults
    if ($ports -notmatch 'PORT=8080 RESULT=BLOCKED' -or $ports -notmatch 'PORT=8443 RESULT=OPEN') { throw 'Final Security Admin/NSG connectivity proof does not match the required result.' }
    Write-Lab 'Four distinct /20 allocations, unused reservation, four members, no SSH allow, blocked 8080, and open 8443 are verified.' PASS
    $allocations | Format-Table -AutoSize
}

switch ($Stage) {
    'Preflight' { Invoke-Preflight }
    'Foundation' { Invoke-Foundation }
    'Ipam' { Invoke-Ipam }
    'Networks' { Invoke-Networks }
    'DynamicGroup' { Invoke-DynamicGroup }
    'Mesh' { Invoke-Mesh }
    'RepairMembership' { Invoke-RepairMembership }
    'SecurityAdmin' { Invoke-SecurityAdmin }
    'Validate' { Invoke-Validate }
}
