Get-AzureLocalInventory.ps1

<#PSScriptInfo
.VERSION 1.2.0
.GUID a3f7b2c1-4d8e-4f6a-9b0c-1e2d3f4a5b6c
.AUTHOR Alex ter Neuzen
.COMPANYNAME GetToTheCloud
.COPYRIGHT (c) 2026 Alex ter Neuzen. All rights reserved.
.TAGS Azure AzureLocal AzureStackHCI Inventory HCI Arc
.LICENSEURI https://github.com/GetToThe-Cloud/documenter-azure-local/blob/main/LICENSE
.PROJECTURI https://github.com/GetToThe-Cloud/documenter-azure-local
.ICONURI
.EXTERNALMODULEDEPENDENCIES Az.Accounts, Az.Resources, Az.StackHCI, Az.ConnectedMachine, Az.ArcGateway
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
    v1.2.0 - Security hardening release (dashboard XSS fixes, server request validation)
    v1.1.135 - Changed filtering for OSSku to look for "Azure Stack HCI"
    v1.1.130 - Changed filtering for OSSku to look for "Azure Stack HCI"
    v1.1.124 - Cross-subscription scanning, cluster-level AHB detection, VM-to-cluster mapping via logical network, executive PDF export
    v1.0.0 - Initial release
.PRIVATEDATA
#>


<#
.SYNOPSIS
    Azure Local (Azure Stack HCI) Inventory Collection Module
.DESCRIPTION
    Collects comprehensive inventory data from Azure Local environments including
    clusters, nodes, agent versions, software versions, logical networks, images, and virtual machines.
#>


# Script version
$script:Version = "1.2.0"

# Progress tracking
$script:CollectionProgress = @{
    Message = ""
    Percent = 0
    LastUpdate = (Get-Date)
}

# Update collection progress
function Update-CollectionProgress {
    param(
        [string]$Message,
        [int]$Percent
    )
    
    $script:CollectionProgress.Message = $Message
    $script:CollectionProgress.Percent = $Percent
    $script:CollectionProgress.LastUpdate = (Get-Date)
    
    Write-Host " $('{0,3}' -f $Percent)% $Message" -ForegroundColor Cyan
}

# Get current progress
function Get-CollectionProgress {
    return @{
        message = $script:CollectionProgress.Message
        percent = $script:CollectionProgress.Percent
        lastUpdate = $script:CollectionProgress.LastUpdate.ToString('o')
    }
}

# Check PowerShell version requirement
function Test-PowerShellVersion {
    $minimumVersion = [version]"7.0.0"
    $currentVersion = $PSVersionTable.PSVersion
    
    if ($currentVersion -lt $minimumVersion) {
        Write-Error "PowerShell 7.0 or higher is required. Current version: $currentVersion"
        Write-Host "Please install PowerShell 7 from: https://aka.ms/powershell" -ForegroundColor Yellow
        return $false
    }
    
    Write-Host "✓ PowerShell Version: $currentVersion" -ForegroundColor Green
    return $true
}

# Check required PowerShell modules
function Test-RequiredModules {
    $requiredModules = @(
        @{ Name = 'Az.Accounts'; MinVersion = '2.0.0' },
        @{ Name = 'Az.Resources'; MinVersion = '6.0.0' },
        @{ Name = 'Az.StackHCI'; MinVersion = '2.0.0' },
        @{ Name = 'Az.ConnectedMachine'; MinVersion = '0.5.0' },
        @{ Name = 'Az.ArcGateway'; MinVersion = '0.1.0' }
    )
    
    $missingModules = @()
    $outdatedModules = @()
    $updateAvailable = @()
    
    Write-Host "\n ○ Checking required PowerShell modules and latest versions..." -ForegroundColor Gray
    
    foreach ($module in $requiredModules) {
        # Get installed version
        $installed = Get-Module -ListAvailable -Name $module.Name | 
            Sort-Object Version -Descending | 
            Select-Object -First 1
        
        # Try to get latest version from PowerShell Gallery
        $latestVersion = $null
        try {
            $galleryModule = Find-Module -Name $module.Name -ErrorAction SilentlyContinue
            $latestVersion = $galleryModule.Version
        } catch {
            Write-Host " ⚠️ Unable to check PowerShell Gallery for $($module.Name)" -ForegroundColor Yellow
        }
        
        if (-not $installed) {
            $missingModules += @{
                Name = $module.Name
                Latest = if ($latestVersion) { $latestVersion.ToString() } else { "Unknown" }
            }
            Write-Host " ❌ $($module.Name): Not installed" -ForegroundColor Red
        } elseif ($installed.Version -lt [version]$module.MinVersion) {
            $outdatedModules += @{
                Name = $module.Name
                Required = $module.MinVersion
                Current = $installed.Version.ToString()
                Latest = if ($latestVersion) { $latestVersion.ToString() } else { "Unknown" }
            }
            Write-Host " ⚠️ $($module.Name): v$($installed.Version) is below minimum v$($module.MinVersion)" -ForegroundColor Yellow
        } elseif ($latestVersion -and $installed.Version -lt $latestVersion) {
            $updateAvailable += @{
                Name = $module.Name
                Current = $installed.Version.ToString()
                Latest = $latestVersion.ToString()
            }
            Write-Host " ⚠️ $($module.Name): v$($installed.Version) installed, v$latestVersion available" -ForegroundColor Yellow
        } else {
            $versionInfo = "v$($installed.Version)"
            if ($latestVersion -and $installed.Version -eq $latestVersion) {
                $versionInfo += " (latest)"
            }
            Write-Host " ✓ $($module.Name): $versionInfo" -ForegroundColor Green
        }
    }
    
    # Install missing modules automatically
    if ($missingModules.Count -gt 0) {
        Write-Host "\n 📦 Installing missing modules..." -ForegroundColor Cyan
        foreach ($module in $missingModules) {
            try {
                Write-Host " → Installing $($module.Name)..." -ForegroundColor Gray
                Install-Module -Name $module.Name -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
                Write-Host " ✓ $($module.Name) installed successfully" -ForegroundColor Green
            } catch {
                Write-Host " ❌ Failed to install $($module.Name): $($_.Exception.Message)" -ForegroundColor Red
                return $false
            }
        }
    }
    
    # Update outdated modules (below minimum version)
    if ($outdatedModules.Count -gt 0) {
        Write-Host "\n 🔄 Updating outdated modules to meet minimum requirements..." -ForegroundColor Cyan
        foreach ($module in $outdatedModules) {
            try {
                Write-Host " → Updating $($module.Name) from v$($module.Current) to latest..." -ForegroundColor Gray
                Update-Module -Name $module.Name -Force -ErrorAction Stop
                Write-Host " ✓ $($module.Name) updated successfully" -ForegroundColor Green
            } catch {
                Write-Host " ❌ Failed to update $($module.Name): $($_.Exception.Message)" -ForegroundColor Red
                Write-Host " ⚠️ Continuing with current version, but some features may not work..." -ForegroundColor Yellow
            }
        }
    }
    
    # Update modules that have newer versions available
    if ($updateAvailable.Count -gt 0) {
        Write-Host "\n 🔄 Updating modules to latest versions..." -ForegroundColor Cyan
        foreach ($module in $updateAvailable) {
            try {
                Write-Host " → Updating $($module.Name) from v$($module.Current) to v$($module.Latest)..." -ForegroundColor Gray
                Update-Module -Name $module.Name -Force -ErrorAction Stop
                Write-Host " ✓ $($module.Name) updated successfully" -ForegroundColor Green
            } catch {
                Write-Host " ⚠️ Could not update $($module.Name): $($_.Exception.Message)" -ForegroundColor Yellow
                Write-Host " ℹ️ Continuing with v$($module.Current)..." -ForegroundColor Gray
            }
        }
    }
    
    Write-Host "\n ✓ All required modules are ready" -ForegroundColor Green
    return $true
}

function Get-AzureLocalInventory {
    [CmdletBinding()]
    param()
    
    # Reset progress tracking at the start
    $script:CollectionProgress.Message = ""
    $script:CollectionProgress.Percent = 0
    $script:CollectionProgress.LastUpdate = (Get-Date)
    
    # Check PowerShell version first
    if (-not (Test-PowerShellVersion)) {
        throw "PowerShell 7.0 or higher is required. Please install from: https://aka.ms/powershell"
    }
    
    # Check modules
    if (-not (Test-RequiredModules)) {
        throw "Required PowerShell modules are missing. Please install them and try again."
    }
    
    Write-Host "\n ○ Gathering Azure Local inventory (v$script:Version)..." -ForegroundColor Gray
    Update-CollectionProgress "Initializing inventory collection..." 5
    
    $inventory = @{
        version = $script:Version
        collectionTime = (Get-Date).ToString('o')
        tenantId = (Get-AzContext).Tenant.Id
        subscription = (Get-AzContext).Subscription.Name
        subscriptionId = (Get-AzContext).Subscription.Id
        clusters = @()
        nodes = @()
        agentVersions = @()
        softwareVersions = @()
        logicalNetworks = @()
        images = @()
        virtualMachines = @()
        storagePaths = @()
        customLocations = @()
        arcResourceBridges = @()
        arcGateways = @()
        licenses = @()
        costAnalysis = @{
            pricingDate = "2024-03-04"
            currency = "USD"
            # Azure Local pricing per physical core per month
            # Source: https://azure.microsoft.com/en-us/pricing/details/azure-local/
            corePrice = 10.0  # Base price per core/month
            corePriceWithHybridBenefit = 0.0  # Free with Azure Hybrid Benefit
            totalCores = 0
            nodesWithHybridBenefit = 0
            nodesWithoutHybridBenefit = 0
            estimatedMonthlyCost = 0.0
            estimatedMonthlyCostWithFullHybridBenefit = 0.0
            potentialMonthlySavings = 0.0
            estimatedYearlyCost = 0.0
            potentialYearlySavings = 0.0
        }
        summary = @{
            totalClusters = 0
            totalNodes = 0
            totalLogicalNetworks = 0
            totalImages = 0
            totalVirtualMachines = 0
            totalStoragePaths = 0
            totalCustomLocations = 0
            totalArcResourceBridges = 0
            totalArcGateways = 0
            totalLicensedMachines = 0
            clustersByStatus = @{}
            nodesByStatus = @{}
            vmsByCluster = @{}
        }
        explanations = @{
            overview = @"
Azure Local (formerly Azure Stack HCI) is a hyperconverged infrastructure solution that combines compute, storage, and networking
in a single cluster. It enables you to run virtualized workloads on-premises while managing them from Azure.

Key Components:
• Clusters: Collections of physical servers working together as a single system
• Nodes: Individual physical servers within a cluster
• Logical Networks: Software-defined networking configurations
• Images: Virtual machine OS templates and images
• Virtual Machines: Workloads running on the infrastructure

Azure Local provides cloud-native management, automatic updates, and hybrid capabilities through Azure Arc.
"@

            clusters = @"
Azure Local Clusters are collections of physical servers (nodes) that work together to provide high availability and scalability.

Characteristics:
• Resource Pool: Shared compute, storage, and networking resources
• High Availability: Workloads automatically failover between nodes
• Scale-Out Architecture: Add nodes to increase capacity
• Azure Arc Integration: Managed from Azure portal with hybrid benefits
• Software-Defined Storage: Storage Spaces Direct for resilient storage
• Automatic Updates: Cloud-managed patching and updates

Each cluster can host multiple virtual machines and provides enterprise-grade availability for mission-critical workloads.
"@

            nodes = @"
Nodes are the individual physical servers that make up an Azure Local cluster.

Node Roles:
• Cluster Member: Active participant in the cluster providing compute and storage
• Infrastructure Nodes: Can be dedicated for management workloads
• Status Monitoring: Health and performance tracked through Azure Arc

Hardware Information:
• Manufacturer & Model: Physical server vendor and model number
• Physical Cores: Number of CPU cores available
• Memory (GB): Total RAM installed on the server
• Location: Azure region for management

Software & Updates:
• Solution Version: Azure Local software version installed
• Update Status: Current update state (Up-to-date, Updating, Available)
• Last Updated: Timestamp of last update or sync
• Agent Version: Azure Arc agent version
• OS Version: Operating system and version

Current Workload:
• Virtual Machines: Number of VMs running on this node
• Kubernetes Clusters: Number of Arc-enabled K8s clusters hosted
• Resource Utilization: CPU, memory, and storage usage

Arc Gateway Connection:
• Gateway Enabled: Whether the node connects through an Azure Arc gateway
• Gateway ID: Resource ID of the Arc gateway (if connected)
• Benefits: Reduced public endpoints, centralized connectivity management

Nodes can be added or removed from clusters for maintenance or capacity changes.
"@

            agentVersions = @"
Azure Arc agents enable Azure management capabilities on Azure Local infrastructure.

Agent Types:
• Azure Arc Resource Bridge: Connects cluster to Azure Resource Manager
• Azure Connected Machine Agent: Provides Azure Arc capabilities
• Guest Configuration Agent: Policy and compliance monitoring
• Log Analytics Agent: Monitoring and diagnostics

Version Management:
• Automatic Updates: Agents updated through Azure Arc
• Version Tracking: Monitor agent versions across infrastructure
• Compatibility: Ensures agents work with cluster software version

Keeping agents up-to-date ensures access to latest features and security updates.
"@

            logicalNetworks = @"
Logical Networks define software-defined networking configurations for Azure Local environments.

Network Types:
• VM Networks: Networks for virtual machine connectivity
• Management Networks: Infrastructure management traffic
• Storage Networks: Storage communication between nodes
• Live Migration Networks: VM migration traffic

Configuration:
• IP Address Pools: DHCP or static IP ranges for VMs
• VLAN Tagging: Network segmentation and isolation
• DNS Settings: Name resolution configuration
• Gateway and Routes: Network routing rules

Logical networks enable flexible, software-defined networking without physical reconfiguration.
"@

            images = @"
Images are templates used to create virtual machines on Azure Local infrastructure.

Image Types:
• Marketplace Images: Pre-built images from Azure Marketplace
• Custom Images: Organization-specific VM templates
• Gallery Images: Shared images across environments
• Operating System Images: Windows Server, Linux distributions

Image Management:
• Azure Storage: Images stored in Azure or local storage
• Version Control: Track multiple versions of images
• Replication: Sync images across clusters
• Updates: Refresh images with patches and updates

Using images accelerates VM deployment and ensures consistency across workloads.
"@

            virtualMachines = @"
Virtual Machines (VMs) are the compute workloads running on Azure Local infrastructure.

VM Characteristics:
• Azure Management: Managed through Azure portal like Azure VMs
• High Availability: Automatic failover between cluster nodes
• Live Migration: Move VMs between nodes without downtime
• Performance: Hardware-accelerated compute and storage
• Hybrid Benefits: Use existing licenses with Azure Hybrid Benefit

VM Distribution:
• Per Cluster: Total VMs running on each cluster
• Per Node: VM placement across individual nodes
• Load Balancing: Automatic or manual VM placement

VMs on Azure Local provide on-premises performance with cloud management capabilities.
"@

            storagePaths = @"
Storage Paths define the storage locations used by Azure Local clusters for virtual machine storage.

Path Types:
• CSV (Cluster Shared Volumes): Shared storage accessible by all nodes
• SMB Shares: Network storage paths
• Local Storage: Node-specific storage locations

Configuration:
• Path Location: Physical or network path
• Provisioning State: Deployment status
• Associated Cluster: Which cluster uses the storage

Storage paths enable flexible storage configurations and support for hybrid storage scenarios.
"@

            customLocations = @"
Custom Locations extend Azure management capabilities to on-premises and edge infrastructure.

Characteristics:
• Azure Arc Integration: Enables Azure Resource Manager on Azure Local
• Resource Deployment: Deploy Azure services to on-premises clusters
• Management Plane: Single control plane for hybrid resources
• Authentication: Azure RBAC and identity management

Usage:
• Target for resource deployments (VMs, networks, storage)
• Namespace for cluster resources
• Connection point between Azure and on-premises infrastructure

Custom locations bridge the gap between cloud and on-premises management.
"@

            arcResourceBridges = @"
Azure Arc Resource Bridges enable Azure management plane connectivity for on-premises infrastructure.

Characteristics:
• Resource Connector: Lightweight Kubernetes-based appliance
• Management Proxy: Facilitates communication between Azure and on-premises resources
• Self-Hosted: Runs on the Azure Local infrastructure
• High Availability: Can be deployed in HA configuration

Capabilities:
• VM Management: Enable Azure portal management of on-premises VMs
• Resource Provisioning: Deploy resources from Azure to on-premises
• Identity Integration: Azure Active Directory authentication
• Monitoring & Diagnostics: Azure Monitor integration

Configuration:
• Version: Resource bridge software version
• Distro: Kubernetes distribution used
• Provider: Infrastructure provider (Azure Stack HCI, VMware, etc.)
• Status: Operational state and health

Resource bridges are essential for full Azure Arc capabilities on Azure Local.
"@

            licenses = @"
Azure Arc License Management allows you to apply and track software licenses for Arc-enabled machines.

License Types:
• Windows Server: Core-based or datacenter licenses
• SQL Server: Core-based licensing with Software Assurance
• Extended Security Updates (ESU): Security patches for legacy systems
• Azure Hybrid Benefit: Use on-premises licenses in Azure

License Properties:
• License Type: The specific license product (Windows Server, SQL Server, etc.)
• License State: Active, Inactive, or Expired
• Core Count: Number of cores covered by the license
• Edition: License edition (Standard, Datacenter, Enterprise, etc.)
• Assigned Date: When the license was applied to the machine

Licenses help optimize costs and ensure compliance across your hybrid infrastructure.
"@

        }
    }
    
    try {
        # Get all Arc Gateways first
        Write-Host " ○ Collecting Arc Gateways..." -ForegroundColor Gray
        Update-CollectionProgress "Collecting Arc Gateways..." 10
        $arcGateways = @{}
        try {
            $gateways = Get-AzArcGateway -ErrorAction SilentlyContinue
            if ($gateways) {
                foreach ($gateway in $gateways) {
                    $gatewayInfo = @{
                        name = $gateway.Name
                        resourceGroup = $gateway.ResourceGroupName
                        location = $gateway.Location
                        resourceId = $gateway.Id
                        provisioningState = if ($gateway.ProvisioningState) { $gateway.ProvisioningState } else { "Unknown" }
                        gatewayId = $gateway.Id
                    }
                    
                    $arcGateways[$gateway.Id] = $gatewayInfo
                    $inventory.arcGateways += $gatewayInfo
                    $inventory.summary.totalArcGateways++
                }
                Write-Host " ✓ Found $($gateways.Count) Arc Gateway(s)" -ForegroundColor Green
            } else {
                Write-Host " ○ No Arc Gateways found" -ForegroundColor Yellow
            }
        } catch {
            Write-Host " ⚠️ Failed to collect Arc Gateways: $($_.Exception.Message)" -ForegroundColor Yellow
        }

        # ---------------------------------------------------------------
        # Pre-collect Arc machines and VM resources across ALL accessible
        # subscriptions so that clusters whose nodes or Arc VMs reside in
        # a different subscription or resource group are still discovered.
        # ---------------------------------------------------------------
        Write-Host " ○ Discovering accessible subscriptions for cross-subscription inventory..." -ForegroundColor Gray
        Update-CollectionProgress "Discovering subscriptions..." 12
        $currentSubscriptionId = (Get-AzContext).Subscription.Id
        $allAccessibleSubscriptions = @(Get-AzSubscription -ErrorAction SilentlyContinue |
            Where-Object { $_.State -eq 'Enabled' })

        $allArcMachinesList = [System.Collections.Generic.List[object]]::new()

        # Helper: gather all Arc machines (nodes + HCI VMs) from the current Az context
        function Invoke-CollectMachinesFromCurrentSubscription {
            $machines = Get-AzConnectedMachine -ErrorAction SilentlyContinue
            if ($machines) {
                $subId = (Get-AzContext).Subscription.Id
                foreach ($m in $machines) {
                    # Stamp the subscription ID so we can switch context when needed later
                    $m | Add-Member -NotePropertyName '_SubscriptionId' -NotePropertyValue $subId -Force -ErrorAction SilentlyContinue
                    $allArcMachinesList.Add($m)
                }
            }
        }

        # Collect Arc machines from all accessible subscriptions
        Invoke-CollectMachinesFromCurrentSubscription

        foreach ($sub in $allAccessibleSubscriptions) {
            if ($sub.Id -eq $currentSubscriptionId) { continue }
            try {
                $null = Set-AzContext -SubscriptionId $sub.Id -ErrorAction Stop -WarningAction SilentlyContinue
                Write-Host " ○ Discovering resources in subscription: $($sub.Name)..." -ForegroundColor Gray
                Invoke-CollectMachinesFromCurrentSubscription
            } catch {
                Write-Host " ⚠️ Cannot access subscription $($sub.Name): $($_.Exception.Message)" -ForegroundColor Yellow
            } finally {
                $null = Set-AzContext -SubscriptionId $currentSubscriptionId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            }
        }

        # On Azure Local, guest VMs running on a cluster are registered as
        # Microsoft.HybridCompute/machines with Kind = 'HCI'.
        # Physical cluster nodes (the hypervisor hosts) have a different Kind value.
        $allHCIArcVMs = @($allArcMachinesList | Where-Object { $_.Kind -eq 'HCI' })
        $allArcNodes  = @($allArcMachinesList | Where-Object { $_.Kind -ne 'HCI' })

        Write-Host " ✓ Pre-collected $($allArcMachinesList.Count) Arc machine(s): $($allHCIArcVMs.Count) HCI VM(s) + $($allArcNodes.Count) node/server(s) across $($allAccessibleSubscriptions.Count) subscription(s)" -ForegroundColor Green

        # Get all Azure Local clusters
        Write-Host " ○ Collecting Azure Local clusters..." -ForegroundColor Gray
        Update-CollectionProgress "Collecting Azure Local clusters..." 15
        $hciClusters = Get-AzResource -ResourceType "Microsoft.AzureStackHCI/clusters" -ErrorAction SilentlyContinue
        
        if ($hciClusters) {
            foreach ($cluster in $hciClusters) {
                try {
                    # Get detailed cluster information
                    $clusterDetail = Get-AzResource -ResourceId $cluster.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                    
                    $clusterInfo = @{
                        name = $cluster.Name
                        resourceGroup = $cluster.ResourceGroupName
                        location = $cluster.Location
                        resourceId = $cluster.ResourceId
                        status = if ($clusterDetail.Properties -and $clusterDetail.Properties.status) { $clusterDetail.Properties.status } else { "Unknown" }
                        provisioningState = if ($clusterDetail.Properties -and $clusterDetail.Properties.provisioningState) { $clusterDetail.Properties.provisioningState } else { "Unknown" }
                        cloudId = if ($clusterDetail.Properties -and $clusterDetail.Properties.cloudId) { $clusterDetail.Properties.cloudId } else { "N/A" }
                        desiredVersion = if ($clusterDetail.Properties -and $clusterDetail.Properties.desiredProperties -and $clusterDetail.Properties.desiredProperties.version) { $clusterDetail.Properties.desiredProperties.version } else { "N/A" }
                        softwareVersion = if ($clusterDetail.Properties -and $clusterDetail.Properties.reportedProperties -and $clusterDetail.Properties.reportedProperties.clusterVersion) { $clusterDetail.Properties.reportedProperties.clusterVersion } else { "N/A" }
                        lastSyncTimestamp = if ($clusterDetail.Properties -and $clusterDetail.Properties.lastSyncTimestamp) { $clusterDetail.Properties.lastSyncTimestamp } else { "N/A" }
                        nodeCount = 0
                        vmCount = 0
                        # Azure Hybrid Benefit for Azure Local is a cluster-level setting
                        # controlled via softwareAssuranceProperties on the cluster resource.
                        azureHybridBenefitEnabled = if ($clusterDetail.Properties -and
                            $clusterDetail.Properties.softwareAssuranceProperties -and
                            $clusterDetail.Properties.softwareAssuranceProperties.softwareAssuranceStatus -eq 'Enabled') { $true } else { $false }
                        tags = $cluster.Tags
                    }
                    
                    # Track cluster status
                    $statusKey = $clusterInfo.status
                    if (-not $inventory.summary.clustersByStatus.ContainsKey($statusKey)) {
                        $inventory.summary.clustersByStatus[$statusKey] = 0
                    }
                    $inventory.summary.clustersByStatus[$statusKey]++
                    
                    # Get Arc machines (nodes) for this cluster.
                    # Physical nodes have Kind != 'HCI'; VMs have Kind = 'HCI'.
                    Write-Host " ○ Collecting nodes for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting nodes for cluster: $($cluster.Name)..." 25

                    # Pass 1: nodes in the same resource group as the cluster
                    $arcMachines = @($allArcNodes | Where-Object { $_.ResourceGroupName -eq $cluster.ResourceGroupName -and $_.OSSku -eq "Azure Stack HCI"})

                    # Pass 2: cross-subscription/RG fallback using tags or OS identity
                    if (-not $arcMachines -or $arcMachines.Count -eq 0) {
                        Write-Host " ○ No nodes in cluster RG, searching across all subscriptions..." -ForegroundColor Yellow
                        $arcMachines = @($allArcNodes | Where-Object {
                            ($_.Tags -and $_.Tags['ClusterName'] -eq $cluster.Name) -or
                            ($_.Tags -and $_.Tags['Cluster'] -eq $cluster.Name) -or
                            ($_.OSType -eq 'Windows' -and $_.OSName -like '*Azure Stack HCI*')
                        })
                        if ($arcMachines.Count -gt 0) {
                            Write-Host " ✓ Found $($arcMachines.Count) node(s) across subscriptions" -ForegroundColor Green
                        }
                    }
                    
                    foreach ($machine in $arcMachines) {
                        # Extract agent and OS version with fallbacks
                        $agentVer = if ($machine.AgentVersion) { $machine.AgentVersion } 
                                   elseif ($machine.AgentConfiguration -and $machine.AgentConfiguration.GuestConfigurationAgentVersion) { $machine.AgentConfiguration.GuestConfigurationAgentVersion }
                                   else { "Not Available" }
                        
                        $osVer = if ($machine.OSVersion) { $machine.OSVersion }
                                elseif ($machine.DetectedProperty -and $machine.DetectedProperty['OSVersion']) { $machine.DetectedProperty['OSVersion'] }
                                else { "Not Available" }
                        
                        $osNameVal = if ($machine.OSName) { $machine.OSName }
                                    elseif ($machine.DetectedProperty -and $machine.DetectedProperty['OSName']) { $machine.DetectedProperty['OSName'] }
                                    else { "Unknown" }
                        
                        # Extract hardware information from Arc machine properties
                        $manufacturer = if ($machine.Manufacturer) { $machine.Manufacturer }
                                       elseif ($machine.DetectedProperty -and $machine.DetectedProperty['Manufacturer']) { $machine.DetectedProperty['Manufacturer'] }
                                       elseif ($machine.DetectedProperty -and $machine.DetectedProperty['manufacturer']) { $machine.DetectedProperty['manufacturer'] }
                                       else { "Unknown" }
                        
                        $model = if ($machine.Model) { $machine.Model }
                                elseif ($machine.DetectedProperty -and $machine.DetectedProperty['Model']) { $machine.DetectedProperty['Model'] }
                                elseif ($machine.DetectedProperty -and $machine.DetectedProperty['model']) { $machine.DetectedProperty['model'] }
                                else { "Unknown" }
                        
                        $serialNumber = if ($machine.DetectedProperty -and $machine.DetectedProperty['SerialNumber']) { $machine.DetectedProperty['SerialNumber'] }
                                       elseif ($machine.DetectedProperty -and $machine.DetectedProperty['serialNumber']) { $machine.DetectedProperty['serialNumber'] }
                                       else { "Unknown" }
                        
                        $physicalCores = if ($machine.DetectedProperty -and $machine.DetectedProperty['coreCount']) { $machine.DetectedProperty['coreCount'] }
                                        elseif ($machine.DetectedProperty -and $machine.DetectedProperty['LogicalCoreCount']) { $machine.DetectedProperty['LogicalCoreCount'] }
                                        elseif ($machine.DetectedProperty -and $machine.DetectedProperty['ProcessorCount']) { $machine.DetectedProperty['ProcessorCount'] }
                                        else { "Unknown" }
                        
                        $memoryGB = if ($machine.DetectedProperty -and $machine.DetectedProperty['totalPhysicalMemoryInGigabytes']) { 
                                       $machine.DetectedProperty['totalPhysicalMemoryInGigabytes']
                                   }
                                   elseif ($machine.DetectedProperty -and $machine.DetectedProperty['TotalPhysicalMemoryInBytes']) { 
                                       [math]::Round($machine.DetectedProperty['TotalPhysicalMemoryInBytes'] / 1GB, 2)
                                   }
                                   elseif ($machine.DetectedProperty -and $machine.DetectedProperty['MemoryInMB']) {
                                       [math]::Round($machine.DetectedProperty['MemoryInMB'] / 1024, 2)
                                   }
                                   else { "Unknown" }
                        
                        # Extract update information
                        $updateStatus = if ($machine.DetectedProperty -and $machine.DetectedProperty['UpdateStatus']) { $machine.DetectedProperty['UpdateStatus'] }
                                       elseif ($machine.DetectedProperty -and $machine.DetectedProperty['updateStatus']) { $machine.DetectedProperty['updateStatus'] }
                                       else { "Unknown" }
                        
                        $solutionVersion = if ($machine.DetectedProperty -and $machine.DetectedProperty['azurelocal.solutionversion']) { 
                                             $machine.DetectedProperty['azurelocal.solutionversion'] 
                                          }
                                          elseif ($clusterDetail.Properties -and $clusterDetail.Properties.reportedProperties -and $clusterDetail.Properties.reportedProperties.clusterVersion) { 
                                             $clusterDetail.Properties.reportedProperties.clusterVersion 
                                          }
                                          else { "Unknown" }
                        
                        $lastUpdated = if ($machine.DetectedProperty -and $machine.DetectedProperty['LastUpdated']) { $machine.DetectedProperty['LastUpdated'] }
                                      elseif ($machine.LastStatusChange) { $machine.LastStatusChange }
                                      elseif ($clusterDetail.Properties -and $clusterDetail.Properties.lastSyncTimestamp) { $clusterDetail.Properties.lastSyncTimestamp }
                                      else { "Unknown" }
                        
                        # Collect license information and Azure Hybrid Benefit status
                        $licenseInfo = @()
                        $hasLicense = $false
                        # Nodes inherit Azure Hybrid Benefit from their cluster's
                        # softwareAssuranceProperties.softwareAssuranceStatus.
                        $azureHybridBenefitEnabled = $clusterInfo.azureHybridBenefitEnabled
                        
                        if ($machine.LicenseProfile) {
                            $hasLicense = $true
                            
                            if ($machine.LicenseProfile.EsuProfile -and $machine.LicenseProfile.EsuProfile.LicenseAssignmentState) {
                                $licenseInfo += @{
                                    type = "ESU (Extended Security Updates)"
                                    state = $machine.LicenseProfile.EsuProfile.LicenseAssignmentState
                                    assignedDate = if ($machine.LicenseProfile.EsuProfile.AssignedLicenseImmutableId) { "Assigned" } else { "N/A" }
                                }
                            }
                            if ($machine.LicenseProfile.ProductProfile -and $machine.LicenseProfile.ProductProfile.ProductType) {
                                $licenseInfo += @{
                                    type = $machine.LicenseProfile.ProductProfile.ProductType
                                    state = if ($machine.LicenseProfile.ProductProfile.SubscriptionStatus) { $machine.LicenseProfile.ProductProfile.SubscriptionStatus } else { "Active" }
                                    edition = if ($machine.LicenseProfile.ProductProfile.ProductFeatures) { $machine.LicenseProfile.ProductProfile.ProductFeatures.Edition } else { "N/A" }
                                }
                            }
                        }
                        
                        # Collect installed extensions
                        # If the machine is in a different subscription, temporarily switch context.
                        $extensions = @()
                        try {
                            $machineSubId = ""
                            if ($machine.ResourceId -match '/subscriptions/([^/]+)/') { $machineSubId = $matches[1] }
                            $needsSubSwitch = $machineSubId -and $machineSubId -ne $currentSubscriptionId
                            if ($needsSubSwitch) {
                                $null = Set-AzContext -SubscriptionId $machineSubId -ErrorAction Stop -WarningAction SilentlyContinue
                            }
                            $machineExtensions = Get-AzConnectedMachineExtension -ResourceGroupName $machine.ResourceGroupName -MachineName $machine.Name -ErrorAction SilentlyContinue
                            if ($machineExtensions) {
                                foreach ($ext in $machineExtensions) {
                                    $extensions += @{
                                        name = $ext.Name
                                        type = $ext.MachineExtensionType
                                        publisher = $ext.Publisher
                                        version = $ext.TypeHandlerVersion
                                        status = if ($ext.ProvisioningState) { $ext.ProvisioningState } else { "Unknown" }
                                        autoUpgrade = if ($ext.EnableAutomaticUpgrade) { "Enabled" } else { "Disabled" }
                                    }
                                }
                            }
                        } catch {
                            Write-Host " ⚠️ Could not retrieve extensions for $($machine.Name): $($_.Exception.Message)" -ForegroundColor Yellow
                        } finally {
                            # Always restore the primary context
                            $null = Set-AzContext -SubscriptionId $currentSubscriptionId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
                        }
                        
                        $nodeInfo = @{
                            name = $machine.Name
                            clusterName = $cluster.Name
                            resourceGroup = $machine.ResourceGroupName
                            status = $machine.Status
                            provisioningState = $machine.ProvisioningState
                            agentVersion = $agentVer
                            osType = $machine.OSType
                            osName = $osNameVal
                            osSku = if ($machine.OSSku) { $machine.OSSku } else { "N/A" }
                            osVersion = $osVer
                            location = $machine.Location
                            resourceId = $machine.ResourceId
                            lastStatusChange = if ($machine.LastStatusChange) { $machine.LastStatusChange } else { "N/A" }
                            # Hardware information
                            manufacturer = $manufacturer
                            model = $model
                            serialNumber = $serialNumber
                            physicalCores = $physicalCores
                            memoryGB = $memoryGB
                            # Update information
                            updateStatus = $updateStatus
                            solutionVersion = $solutionVersion
                            lastUpdated = $lastUpdated
                            # Workload tracking (will be populated later)
                            vmCount = 0
                            vmNames = @()
                            k8sClusterCount = 0
                            k8sClusterNames = @()
                            detectedProperty = $machine.DetectedProperty
                            # License information
                            hasLicense = $hasLicense
                            azureHybridBenefitEnabled = $azureHybridBenefitEnabled
                            licenses = $licenseInfo
                            # Extensions
                            extensions = $extensions
                        }
                        
                        $inventory.nodes += $nodeInfo
                        
                        # Track cost calculation data
                        $nodeCores = 0
                        if ($physicalCores -and $physicalCores -ne "Unknown") {
                            try {
                                $nodeCores = [int]$physicalCores
                                $inventory.costAnalysis.totalCores += $nodeCores
                            } catch {
                                Write-Host " ⚠️ Could not parse core count for $($machine.Name): $physicalCores" -ForegroundColor Yellow
                            }
                        }
                        
                        if ($azureHybridBenefitEnabled) {
                            $inventory.costAnalysis.nodesWithHybridBenefit++
                        } else {
                            $inventory.costAnalysis.nodesWithoutHybridBenefit++
                        }
                        
                        # Track licensed machines
                        if ($hasLicense -and $licenseInfo.Count -gt 0) {
                            $inventory.licenses += @{
                                machineName = $machine.Name
                                clusterName = $cluster.Name
                                resourceGroup = $machine.ResourceGroupName
                                location = $machine.Location
                                licenses = $licenseInfo
                                azureHybridBenefitEnabled = $azureHybridBenefitEnabled
                                physicalCores = if ($nodeCores -gt 0) { $nodeCores } else { "Unknown" }
                                resourceId = $machine.ResourceId
                            }
                            $inventory.summary.totalLicensedMachines++
                        }
                        $clusterInfo.nodeCount++
                        
                        # Track agent version
                        if ($agentVer -and $agentVer -ne "Not Available") {
                            $existingAgent = $inventory.agentVersions | Where-Object { $_.version -eq $agentVer }
                            if (-not $existingAgent) {
                                $inventory.agentVersions += @{
                                    version = $agentVer
                                    nodeCount = 1
                                    nodes = @($machine.Name)
                                }
                            } else {
                                $existingAgent.nodeCount++
                                $existingAgent.nodes += $machine.Name
                            }
                        }
                        
                        # Track software version
                        if ($osVer -and $osVer -ne "Not Available") {
                            $existingSoftware = $inventory.softwareVersions | Where-Object { 
                                $_.version -eq $osVer -and $_.osName -eq $osNameVal 
                            }
                            if (-not $existingSoftware) {
                                $inventory.softwareVersions += @{
                                    osName = $osNameVal
                                    osSku = if ($machine.OSSku) { $machine.OSSku } else { "N/A" }
                                    version = $osVer
                                    nodeCount = 1
                                    nodes = @($machine.Name)
                                }
                            } else {
                                $existingSoftware.nodeCount++
                                $existingSoftware.nodes += $machine.Name
                            }
                        }
                        
                        # Track node status
                        $nodeStatusKey = $machine.Status
                        if (-not $inventory.summary.nodesByStatus.ContainsKey($nodeStatusKey)) {
                            $inventory.summary.nodesByStatus[$nodeStatusKey] = 0
                        }
                        $inventory.summary.nodesByStatus[$nodeStatusKey]++
                    }
                    
                    # Get logical networks for this cluster
                    Write-Host " ○ Collecting logical networks for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting logical networks for cluster: $($cluster.Name)..." 40
                    $networks = Get-AzResource -ResourceType "Microsoft.AzureStackHCI/logicalnetworks" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue

                    foreach ($network in $networks) {
                        $networkDetail = Get-AzResource -ResourceId $network.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $networkInfo = @{
                            name = $network.Name
                            clusterName = $cluster.Name
                            resourceGroup = $network.ResourceGroupName
                            location = $network.Location
                            resourceId = $network.ResourceId
                            provisioningState = if ($networkDetail.Properties -and $networkDetail.Properties.provisioningState) { $networkDetail.Properties.provisioningState } else { "Unknown" }
                            vmSwitchName = if ($networkDetail.Properties -and $networkDetail.Properties.vmSwitchName) { $networkDetail.Properties.vmSwitchName } else { "N/A" }
                            dhcpEnabled = if ($networkDetail.Properties -and $null -ne $networkDetail.Properties.dhcpOptions) { $true } else { $false }
                            subnets = @()
                            tags = $network.Tags
                        }
                        
                        # Get subnets if available
                        if ($networkDetail.Properties -and $networkDetail.Properties.subnets) {
                            foreach ($subnet in $networkDetail.Properties.subnets) {
                                $prefix = if ($subnet.properties -and $subnet.properties.addressPrefix) { $subnet.properties.addressPrefix } else { $null }
                                $networkInfo.subnets += @{
                                    name          = if ($subnet.name) { $subnet.name } else { "Default" }
                                    addressPrefix = if ($prefix) { $prefix } else { "N/A" }
                                    vlan          = if ($subnet.properties.vlan) { $subnet.properties.vlan } else { "N/A" }
                                    ipPools       = if ($subnet.properties.ipPools) { $subnet.properties.ipPools } else { @() }
                                }
                            }
                        }
                        
                        $inventory.logicalNetworks += $networkInfo
                    }
                    
                    # Get gallery images for this cluster
                    Write-Host " ○ Collecting gallery images for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting gallery images for cluster: $($cluster.Name)..." 50
                    $galleryImages = Get-AzResource -ResourceType "Microsoft.AzureStackHCI/galleryimages" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($image in $galleryImages) {
                        $imageDetail = Get-AzResource -ResourceId $image.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $imageInfo = @{
                            name = $image.Name
                            clusterName = $cluster.Name
                            resourceGroup = $image.ResourceGroupName
                            location = $image.Location
                            resourceId = $image.ResourceId
                            provisioningState = if ($imageDetail.Properties -and $imageDetail.Properties.provisioningState) { $imageDetail.Properties.provisioningState } else { "Unknown" }
                            osType = if ($imageDetail.Properties -and $imageDetail.Properties.osType) { $imageDetail.Properties.osType } else { "Unknown" }
                            hyperVGeneration = if ($imageDetail.Properties -and $imageDetail.Properties.hyperVGeneration) { $imageDetail.Properties.hyperVGeneration } else { "N/A" }
                            sourceImageId = if ($imageDetail.Properties -and $imageDetail.Properties.identifier -and $imageDetail.Properties.identifier.publisher) { 
                                "$($imageDetail.Properties.identifier.publisher):$($imageDetail.Properties.identifier.offer):$($imageDetail.Properties.identifier.sku)" 
                            } else { "Custom" }
                            version = if ($imageDetail.Properties -and $imageDetail.Properties.version -and $imageDetail.Properties.version.name) { $imageDetail.Properties.version.name } else { "N/A" }
                            sizeInGB = if ($imageDetail.Properties -and $imageDetail.Properties.version -and $imageDetail.Properties.version.sizeInGB) { $imageDetail.Properties.version.sizeInGB } else { 0 }
                            tags = $image.Tags
                        }
                        
                        $inventory.images += $imageInfo
                    }
                    
                    # Also check for marketplace gallery images
                    $marketplaceImages = Get-AzResource -ResourceType "Microsoft.AzureStackHCI/marketplacegalleryimages" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($image in $marketplaceImages) {
                        $imageDetail = Get-AzResource -ResourceId $image.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $imageInfo = @{
                            name = $image.Name
                            clusterName = $cluster.Name
                            resourceGroup = $image.ResourceGroupName
                            location = $image.Location
                            resourceId = $image.ResourceId
                            provisioningState = if ($imageDetail.Properties -and $imageDetail.Properties.provisioningState) { $imageDetail.Properties.provisioningState } else { "Unknown" }
                            osType = if ($imageDetail.Properties -and $imageDetail.Properties.osType) { $imageDetail.Properties.osType } else { "Unknown" }
                            hyperVGeneration = if ($imageDetail.Properties -and $imageDetail.Properties.hyperVGeneration) { $imageDetail.Properties.hyperVGeneration } else { "N/A" }
                            sourceImageId = if ($imageDetail.Properties -and $imageDetail.Properties.identifier -and $imageDetail.Properties.identifier.publisher) { 
                                "$($imageDetail.Properties.identifier.publisher):$($imageDetail.Properties.identifier.offer):$($imageDetail.Properties.identifier.sku)" 
                            } else { "Marketplace" }
                            version = if ($imageDetail.Properties -and $imageDetail.Properties.version -and $imageDetail.Properties.version.name) { $imageDetail.Properties.version.name } else { "Latest" }
                            sizeInGB = 0
                            tags = $image.Tags
                        }
                        
                        $inventory.images += $imageInfo
                    }
                    
                    # Get storage paths for this cluster
                    Write-Host " ○ Collecting storage paths for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting storage paths for cluster: $($cluster.Name)..." 55
                    # Try multiple resource types for storage
                    $storagePaths = @()
                    $storagePaths += Get-AzResource -ResourceType "Microsoft.AzureStackHCI/storagepaths" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    $storagePaths += Get-AzResource -ResourceType "Microsoft.AzureStackHCI/storagecontainers" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($path in $storagePaths) {
                        $pathDetail = Get-AzResource -ResourceId $path.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $pathInfo = @{
                            name = $path.Name
                            clusterName = $cluster.Name
                            resourceGroup = $path.ResourceGroupName
                            location = $path.Location
                            resourceId = $path.ResourceId
                            resourceType = $path.ResourceType
                            provisioningState = if ($pathDetail.Properties -and $pathDetail.Properties.provisioningState) { $pathDetail.Properties.provisioningState } else { "Unknown" }
                            path = if ($pathDetail.Properties -and $pathDetail.Properties.path) { $pathDetail.Properties.path } 
                                   elseif ($pathDetail.Properties -and $pathDetail.Properties.containerPath) { $pathDetail.Properties.containerPath }
                                   else { "N/A" }
                            tags = $path.Tags
                        }
                        
                        $inventory.storagePaths += $pathInfo
                    }
                    
                    # Get Arc Resource Bridges for this cluster
                    Write-Host " ○ Collecting Arc Resource Bridges for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting Arc Resource Bridges..." 60
                    $arcBridges = Get-AzResource -ResourceType "Microsoft.ResourceConnector/appliances" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($bridge in $arcBridges) {
                        $bridgeDetail = Get-AzResource -ResourceId $bridge.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $bridgeInfo = @{
                            name = $bridge.Name
                            clusterName = $cluster.Name
                            resourceGroup = $bridge.ResourceGroupName
                            location = $bridge.Location
                            resourceId = $bridge.ResourceId
                            provisioningState = if ($bridgeDetail.Properties -and $bridgeDetail.Properties.provisioningState) { $bridgeDetail.Properties.provisioningState } else { "Unknown" }
                            status = if ($bridgeDetail.Properties -and $bridgeDetail.Properties.status) { $bridgeDetail.Properties.status } else { "Unknown" }
                            version = if ($bridgeDetail.Properties -and $bridgeDetail.Properties.version) { $bridgeDetail.Properties.version } else { "N/A" }
                            distro = if ($bridgeDetail.Properties -and $bridgeDetail.Properties.distro) { $bridgeDetail.Properties.distro } else { "N/A" }
                            infrastructureConfig = if ($bridgeDetail.Properties -and $bridgeDetail.Properties.infrastructureConfig -and $bridgeDetail.Properties.infrastructureConfig.provider) { 
                                $bridgeDetail.Properties.infrastructureConfig.provider 
                            } else { "N/A" }
                            tags = $bridge.Tags
                        }
                        
                        $inventory.arcResourceBridges += $bridgeInfo
                    }
                    
                    # Get custom locations for this cluster
                    Write-Host " ○ Collecting custom locations for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    Update-CollectionProgress "Collecting custom locations..." 65
                    $customLocs = Get-AzResource -ResourceType "Microsoft.ExtendedLocation/customLocations" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($customLoc in $customLocs) {
                        $locDetail = Get-AzResource -ResourceId $customLoc.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        $locInfo = @{
                            name = $customLoc.Name
                            clusterName = $cluster.Name
                            resourceGroup = $customLoc.ResourceGroupName
                            location = $customLoc.Location
                            resourceId = $customLoc.ResourceId
                            provisioningState = if ($locDetail.Properties -and $locDetail.Properties.provisioningState) { $locDetail.Properties.provisioningState } else { "Unknown" }
                            hostResourceId = if ($locDetail.Properties -and $locDetail.Properties.hostResourceId) { $locDetail.Properties.hostResourceId } else { "N/A" }
                            namespace = if ($locDetail.Properties -and $locDetail.Properties.namespace) { $locDetail.Properties.namespace } else { "N/A" }
                            tags = $customLoc.Tags
                        }
                        
                        $inventory.customLocations += $locInfo
                    }
                    
                    # Virtual machines are collected after the cluster loop (no cluster matching)
                    
                    # Get Kubernetes clusters for this cluster (Arc-enabled Kubernetes)
                    Write-Host " ○ Collecting Kubernetes clusters for cluster: $($cluster.Name)..." -ForegroundColor Gray
                    $k8sClusters = Get-AzResource -ResourceType "Microsoft.Kubernetes/connectedClusters" -ResourceGroupName $cluster.ResourceGroupName -ErrorAction SilentlyContinue
                    
                    foreach ($k8s in $k8sClusters) {
                        # Try to determine if this K8s cluster is related to this HCI cluster
                        # K8s clusters on Azure Local typically have tags or properties linking them
                        $k8sDetail = Get-AzResource -ResourceId $k8s.ResourceId -ExpandProperties -ErrorAction SilentlyContinue
                        
                        # Check if infrastructure field points to this cluster
                        $isOnCluster = $false
                        $k8sNodeName = "Unknown"
                        
                        if (($k8sDetail.Properties -and $k8sDetail.Properties.infrastructure -eq "azure_stack_hci") -or 
                            ($k8s.Tags -and $k8s.Tags['ClusterName'] -eq $cluster.Name) -or
                            ($k8s.Tags -and $k8s.Tags['Cluster'] -eq $cluster.Name)) {
                            $isOnCluster = $true
                            
                            # Try to find which node hosts this K8s cluster
                            # This might be in custom properties or tags
                            if ($k8s.Tags -and $k8s.Tags['NodeName']) {
                                $k8sNodeName = $k8s.Tags['NodeName']
                            } elseif ($k8s.Tags -and $k8s.Tags['HostName']) {
                                $k8sNodeName = $k8s.Tags['HostName']
                            }
                        }
                        
                        if ($isOnCluster) {
                            # Update node K8s count
                            if ($k8sNodeName -ne "Unknown") {
                                $nodeEntry = $inventory.nodes | Where-Object { $_.name -eq $k8sNodeName -and $_.clusterName -eq $cluster.Name }
                                if ($nodeEntry) {
                                    $nodeEntry.k8sClusterCount++
                                    $nodeEntry.k8sClusterNames += $k8s.Name
                                }
                            }
                        }
                    }
                    
                    # Update cluster VM count in summary
                    $inventory.summary.vmsByCluster[$cluster.Name] = $clusterInfo.vmCount
                    
                    $inventory.clusters += $clusterInfo
                    
                } catch {
                    Write-Warning "Failed to get details for cluster $($cluster.Name): $_"
                }
            }
        }

        # Collect all HCI Arc VMs (Kind = 'HCI') without cluster matching
        Write-Host " ○ Collecting virtual machines..." -ForegroundColor Gray
        Update-CollectionProgress "Collecting virtual machines..." 70
        foreach ($vm in $allHCIArcVMs) {
            $vmInst = $null
            $clusterName = 'N/A'
            $logicalNetworkName = 'N/A'
            $vmSubId = $vm._SubscriptionId
            $vmNeedsSwitch = $vmSubId -and $vmSubId -ne $currentSubscriptionId
            try {
                if ($vmNeedsSwitch) { $null = Set-AzContext -SubscriptionId $vmSubId -ErrorAction Stop -WarningAction SilentlyContinue }
                $vmInstRest = Invoke-AzRestMethod -Path "$($vm.ResourceId)/providers/Microsoft.AzureStackHCI/virtualMachineInstances/default?api-version=2024-02-01-preview" -Method GET -ErrorAction SilentlyContinue
                if ($vmInstRest -and $vmInstRest.StatusCode -eq 200) {
                    $vmInst = ($vmInstRest.Content | ConvertFrom-Json).properties
                }
            } catch {}
            finally {
                if ($vmNeedsSwitch) { $null = Set-AzContext -SubscriptionId $currentSubscriptionId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue }
            }

                # Map VM to cluster via HCI network interface -> logical network resource ID.
                # The NIC lookup runs inside the try block so we are still in the VM's
                # subscription context (important for cross-subscription VMs).
            # if ($vmInst -and $vmInst.networkProfile -and $vmInst.networkProfile.networkInterfaces) {
            # :nicLoop foreach ($vmNic in $vmInst.networkProfile.networkInterfaces) {
            # if (-not $vmNic.id) { continue }
            # try {
            # $nicRest = Invoke-AzRestMethod -Path "$($vmNic.id)?api-version=2024-02-01-preview" -Method GET -ErrorAction SilentlyContinue
            # if ($nicRest -and $nicRest.StatusCode -eq 200) {
            # $nicProps = ($nicRest.Content | ConvertFrom-Json).properties
            # if ($nicProps.ipConfigurations) {
            # foreach ($ipCfg in $nicProps.ipConfigurations) {
            # $lnId = if ($ipCfg.properties -and $ipCfg.properties.subnet -and $ipCfg.properties.subnet.id) { $ipCfg.properties.subnet.id } else { $null }
            # if ($lnId) {
            # $matchedNet = $inventory.logicalNetworks | Where-Object { $_.resourceId -eq $lnId } | Select-Object -First 1
            # if ($matchedNet) {
            # $clusterName = $matchedNet.clusterName
            # $logicalNetworkName = $matchedNet.name
            # break nicLoop
            # }
            # }
            # }
            # }
            # }
            # } catch {}
            # }
            # }
            # } catch {}
            # finally {
            # if ($vmNeedsSwitch) { $null = Set-AzContext -SubscriptionId $currentSubscriptionId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue }
            # }

            $powerState = switch ($vm.Status) {
                'Connected'    { 'Running' }
                'Disconnected' { 'Stopped' }
                default        { if ($vm.Status) { $vm.Status } else { 'Unknown' } }
            }
            if ($vmInst -and $vmInst.instanceView -and $vmInst.instanceView.powerState) {
                $powerState = $vmInst.instanceView.powerState
            }

            # Extract IP address from Arc agent NIC data
            $ipAddress = 'N/A'
            if ($vm.NetworkProfileNetworkInterface) {
                foreach ($nic in $vm.NetworkProfileNetworkInterface) {
                    $ipv4 = $nic.ipAddress | Where-Object { $_.Version -eq 'IPv4' } | Select-Object -First 1
                    if ($ipv4 -and $ipv4.address) { $ipAddress = $ipv4.address; break }
                }
            }

            # Fallback: match Arc agent NIC subnet prefix against inventoried logical network subnets
            if ($clusterName -eq 'N/A' -and $vm.NetworkProfileNetworkInterface) {
                foreach ($nic in $vm.NetworkProfileNetworkInterface) {
                    foreach ($ipEntry in $nic.ipAddress) {
                        if ($ipEntry.Version -eq 'IPv4' -and $ipEntry.SubnetAddressPrefix) {
                            $matchedNet = $inventory.logicalNetworks.Subnets | Where-Object { $_.addressPrefix -eq $ipEntry.SubnetAddressPrefix }
                            | Select-Object -First 1
                            if ($matchedNet) {
                                $filter = $inventory.logicalNetworks | Where-Object { $_.subnets.name -contains $matchedNet.name } | Select -first 1
                                $clusterName = $filter.clusterName
                                $logicalNetworkName = $filter.name
                                break
                            }
                        }
                    }
                    if ($clusterName -ne 'N/A') { break }
                }
            }

            $inventory.virtualMachines += @{
                name              = $vm.Name
                clusterName       = $clusterName
                logicalNetwork    = $logicalNetworkName
                resourceGroup     = $vm.ResourceGroupName
                location          = $vm.Location
                resourceId        = $vm.ResourceId
                arcStatus         = $vm.Status
                powerState        = $powerState
                ipAddress         = $ipAddress
                provisioningState = if ($vm.ProvisioningState) { $vm.ProvisioningState } else { 'Unknown' }
                osType            = if ($vm.OSType) { $vm.OSType } else { 'Unknown' }
                osName            = if ($vm.OSName) { $vm.OSName } else { 'Unknown' }
                osVersion         = if ($vm.OSVersion) { $vm.OSVersion } else { 'N/A' }
                agentVersion      = if ($vm.AgentVersion) { $vm.AgentVersion } else { 'N/A' }
                vmSize            = if ($vmInst -and $vmInst.hardwareProfile -and $vmInst.hardwareProfile.vmSize) { $vmInst.hardwareProfile.vmSize } else { 'Custom' }
                cpuCount          = if ($vmInst -and $vmInst.hardwareProfile -and $vmInst.hardwareProfile.processors) { $vmInst.hardwareProfile.processors }
                                    elseif ($vm.DetectedProperty -and $vm.DetectedProperty['logicalCoreCount']) { [int]$vm.DetectedProperty['logicalCoreCount'] }
                                    elseif ($vm.DetectedProperty -and $vm.DetectedProperty['coreCount']) { [int]$vm.DetectedProperty['coreCount'] }
                                    else { 0 }
                memoryMB          = if ($vmInst -and $vmInst.hardwareProfile -and $vmInst.hardwareProfile.memoryMB) { $vmInst.hardwareProfile.memoryMB }
                                    elseif ($vm.DetectedProperty -and $vm.DetectedProperty['totalPhysicalMemoryInBytes']) { [math]::Round([double]$vm.DetectedProperty['totalPhysicalMemoryInBytes'] / 1MB) }
                                    elseif ($vm.DetectedProperty -and $vm.DetectedProperty['totalPhysicalMemoryInGigabytes']) { [math]::Round([double]$vm.DetectedProperty['totalPhysicalMemoryInGigabytes'] * 1024) }
                                    else { 0 }
                imageReference    = if ($vmInst -and $vmInst.storageProfile -and $vmInst.storageProfile.imageReference -and $vmInst.storageProfile.imageReference.id) { $vmInst.storageProfile.imageReference.id } else { 'N/A' }
                tags              = $vm.Tags
            }
        }
        Write-Host " ✓ Found $($inventory.virtualMachines.Count) virtual machine(s)" -ForegroundColor Green

        # Update per-cluster VM counts now that clusterName is resolved via logical network mapping
        foreach ($vmEntry in $inventory.virtualMachines) {
            if ($vmEntry.clusterName -and $vmEntry.clusterName -ne 'N/A') {
                if (-not $inventory.summary.vmsByCluster.ContainsKey($vmEntry.clusterName)) {
                    $inventory.summary.vmsByCluster[$vmEntry.clusterName] = 0
                }
                $inventory.summary.vmsByCluster[$vmEntry.clusterName]++
                $clusterEntry = $inventory.clusters | Where-Object { $_.name -eq $vmEntry.clusterName }
                if ($clusterEntry) { $clusterEntry.vmCount++ }
            }
        }

        $inventory.summary.totalClusters = $inventory.clusters.Count
        $inventory.summary.totalNodes = $inventory.nodes.Count
        $inventory.summary.totalLogicalNetworks = $inventory.logicalNetworks.Count
        $inventory.summary.totalImages = $inventory.images.Count
        $inventory.summary.totalVirtualMachines = $inventory.virtualMachines.Count
        $inventory.summary.totalStoragePaths = $inventory.storagePaths.Count
        $inventory.summary.totalCustomLocations = $inventory.customLocations.Count
        $inventory.summary.totalArcResourceBridges = $inventory.arcResourceBridges.Count
        
        Write-Host " ✓ Inventory collection complete" -ForegroundColor Green
        Update-CollectionProgress "Processing collected data..." 75
        Write-Host " • Clusters: $($inventory.summary.totalClusters)" -ForegroundColor Gray
        Write-Host " • Nodes: $($inventory.summary.totalNodes)" -ForegroundColor Gray
        Write-Host " • Logical Networks: $($inventory.summary.totalLogicalNetworks)" -ForegroundColor Gray
        Write-Host " • Images: $($inventory.summary.totalImages)" -ForegroundColor Gray
        Write-Host " • Virtual Machines: $($inventory.summary.totalVirtualMachines)" -ForegroundColor Gray
        Write-Host " • Storage Paths: $($inventory.summary.totalStoragePaths)" -ForegroundColor Gray
        Write-Host " • Custom Locations: $($inventory.summary.totalCustomLocations)" -ForegroundColor Gray
        Write-Host " • Arc Resource Bridges: $($inventory.summary.totalArcResourceBridges)" -ForegroundColor Gray
        
        # Calculate costs based on Azure Local pricing
        Write-Host "\n ○ Calculating cost estimates..." -ForegroundColor Gray
        Update-CollectionProgress "Analyzing Azure Hybrid Benefit savings..." 80
        
        $costAnalysis = $inventory.costAnalysis
        $totalCores = $costAnalysis.totalCores
        $nodesWithHybrid = $costAnalysis.nodesWithHybridBenefit
        $nodesWithoutHybrid = $costAnalysis.nodesWithoutHybridBenefit
        $corePrice = $costAnalysis.corePrice
        
        if ($totalCores -gt 0) {
            # Calculate cores with and without hybrid benefit
            # Assuming even distribution of cores across nodes for simplicity
            $avgCoresPerNode = if ($inventory.summary.totalNodes -gt 0) { 
                [math]::Round($totalCores / $inventory.summary.totalNodes, 2) 
            } else { 0 }
            
            # Calculate costs
            # Nodes with Hybrid Benefit pay $0/core (free with Windows Server licenses)
            # Nodes without Hybrid Benefit pay $10/core/month
            
            $coresWithHybrid = 0
            $coresWithoutHybrid = 0
            
            # Calculate actual cores with/without hybrid benefit based on node data
            foreach ($node in $inventory.nodes) {
                $nodeCoreCount = 0
                if ($node.physicalCores -and $node.physicalCores -ne "Unknown") {
                    try {
                        $nodeCoreCount = [int]$node.physicalCores
                    } catch {
                        $nodeCoreCount = 0
                    }
                }
                
                if ($node.azureHybridBenefitEnabled) {
                    $coresWithHybrid += $nodeCoreCount
                } else {
                    $coresWithoutHybrid += $nodeCoreCount
                }
            }
            
            # Current monthly cost (what you're currently paying)
            $currentMonthlyCost = ($coresWithoutHybrid * $corePrice)
            
            # Potential monthly cost if all had hybrid benefit (best case)
            $monthlyCostWithFullHybrid = 0.0  # Free with Azure Hybrid Benefit
            
            # Potential savings
            $potentialMonthlySavings = $currentMonthlyCost - $monthlyCostWithFullHybrid
            
            # Yearly calculations
            $currentYearlyCost = $currentMonthlyCost * 12
            $potentialYearlySavings = $potentialMonthlySavings * 12
            
            # Update cost analysis
            $costAnalysis.estimatedMonthlyCost = [math]::Round($currentMonthlyCost, 2)
            $costAnalysis.estimatedMonthlyCostWithFullHybridBenefit = [math]::Round($monthlyCostWithFullHybrid, 2)
            $costAnalysis.potentialMonthlySavings = [math]::Round($potentialMonthlySavings, 2)
            $costAnalysis.estimatedYearlyCost = [math]::Round($currentYearlyCost, 2)
            $costAnalysis.potentialYearlySavings = [math]::Round($potentialYearlySavings, 2)
            
            Write-Host " ✓ Total Physical Cores: $totalCores" -ForegroundColor Green
            Write-Host " • Cores with Azure Hybrid Benefit: $coresWithHybrid (FREE)" -ForegroundColor Green
            Write-Host " • Cores without Azure Hybrid Benefit: $coresWithoutHybrid" -ForegroundColor $(if ($coresWithoutHybrid -gt 0) { "Yellow" } else { "Green" })
            Write-Host " • Estimated Monthly Cost: `$$($costAnalysis.estimatedMonthlyCost)" -ForegroundColor Cyan
            Write-Host " • Estimated Yearly Cost: `$$($costAnalysis.estimatedYearlyCost)" -ForegroundColor Cyan
            
            if ($potentialMonthlySavings -gt 0) {
                Write-Host " 💰 Potential Monthly Savings with Full Hybrid Benefit: `$$($costAnalysis.potentialMonthlySavings)" -ForegroundColor Yellow
                Write-Host " 💰 Potential Yearly Savings: `$$($costAnalysis.potentialYearlySavings)" -ForegroundColor Yellow
            } else {
                Write-Host " ✓ All nodes using Azure Hybrid Benefit - Maximum savings achieved!" -ForegroundColor Green
            }
            Update-CollectionProgress "Building summary..." 85
        } else {
            Write-Host " ⚠️ Unable to calculate costs: No core count information available" -ForegroundColor Yellow
            Update-CollectionProgress "Building summary..." 85
        }
        
        Update-CollectionProgress "Finalizing inventory..." 95
        
    } catch {
        Write-Error "Error collecting inventory: $_"
        throw
    }
    
    Update-CollectionProgress "Inventory collection complete!" 100
    return $inventory
}