Modules/Collectors/60-ManagementPerformanceCollector.ps1

function Invoke-RangerManagementPerformanceCollector {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$Config,

        [Parameter(Mandatory = $true)]
        $CredentialMap,

        [Parameter(Mandatory = $true)]
        [object]$Definition,

        [Parameter(Mandatory = $true)]
        [string]$PackageRoot
    )

    $fixture = Get-RangerCollectorFixtureData -Config $Config -CollectorId $Definition.Id
    if ($fixture) {
        return ConvertTo-RangerHashtable -InputObject $fixture
    }

    $nodeSnapshots = @(
        Invoke-RangerSafeAction -Label 'Management and performance snapshot' -DefaultValue @() -ScriptBlock {
            Invoke-RangerClusterCommand -Config $Config -Credential $CredentialMap.cluster -ScriptBlock {
                $managementServices = @(
                    foreach ($name in @('ServerManagementGateway', 'HealthService', 'SCVMMAgent', 'MOMAgent', 'Dell*')) {
                        Get-Service -Name $name -ErrorAction SilentlyContinue | Select-Object Name, DisplayName, Status, StartType
                    }
                )

                # Third-party management agent discovery
                $thirdPartyPatterns = @('VeeamAgent*', 'VeeamBackup*', 'VeeamTransport*', 'CBAComm*', 'ZertoVSS*', 'ZertoService*',
                    'dattowin*', 'DattoBackup*', 'Rubrik*', 'DatadogAgent', 'datadogagent', 'splunkd*', 'SplunkForwarder*',
                    'prometheus*', 'node_exporter*', 'SolarWinds*', 'SolarwindsOrion*', 'CcmExec', 'IntuneManagementExtension',
                    'SCOM*', 'healthservice', 'omi*', 'OMSAgentForLinux')
                $thirdPartyAgents = @(foreach ($pattern in $thirdPartyPatterns) {
                    Get-Service -Name $pattern -ErrorAction SilentlyContinue | Select-Object Name, DisplayName, Status, StartType
                })
                # Deduplicate on Name
                $agentNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
                $thirdPartyAgents = @($thirdPartyAgents | Where-Object { $agentNames.Add($_.Name) })

                # WAC/Windows Admin Center TLS certificate and extensions
                $wacCert = $null
                $wacExtensions = @()
                $wacService = Get-Service -Name 'ServerManagementGateway' -ErrorAction SilentlyContinue
                if ($wacService) {
                    $wacCert = try {
                        $wacCertThumb = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'SslCertificateThumbprint' -ErrorAction Stop).SslCertificateThumbprint
                        if ($wacCertThumb) {
                            $cert = Get-ChildItem -Path "Cert:\LocalMachine\" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Thumbprint -eq $wacCertThumb } | Select-Object -First 1
                            if ($cert) {
                                [ordered]@{ thumbprint = $cert.Thumbprint; subject = $cert.Subject; notAfter = $cert.NotAfter; daysUntilExpiry = [math]::Round(($cert.NotAfter - [datetime]::UtcNow).TotalDays, 1) }
                            }
                        }
                    } catch { $null }
                    $wacPort = try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'SmePort' -ErrorAction Stop).SmePort } catch { 443 }
                    $wacExtensions = @(try {
                        $extPath = Join-Path $env:ProgramFiles 'Windows Admin Center\extensions'
                        if (Test-Path $extPath) {
                            Get-ChildItem -Path $extPath -Directory -ErrorAction SilentlyContinue | Select-Object Name, @{N='LastWriteTime';E={$_.LastWriteTime}}
                        }
                    } catch { @() })
                    # Issue #68: WAC version, gateway mode, URL, Azure registration, auth mode
                    $wacVersion         = try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'InstalledVersion' -ErrorAction Stop).InstalledVersion } catch { $null }
                    $wacGatewayMode     = try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'GatewayMode' -ErrorAction Stop).GatewayMode } catch { 'standalone' }
                    $wacUrl             = "https://$($env:COMPUTERNAME):$wacPort"
                    $wacAuthMode        = try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'AuthenticationMode' -ErrorAction Stop).AuthenticationMode } catch { 'unknown' }
                    $wacAzureResourceId = try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -Name 'AzureResourceId' -ErrorAction Stop).AzureResourceId } catch { $null }
                } else {
                    $wacVersion = $null; $wacGatewayMode = $null; $wacUrl = $null; $wacAuthMode = $null; $wacAzureResourceId = $null
                }

                $scvmmInfo = $null
                $scvmmAgentSvc = Get-Service -Name 'SCVMMAgent' -ErrorAction SilentlyContinue
                if ($scvmmAgentSvc) {
                    $scvmmInfo = try {
                        $vmmReg = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\VirtualMachineManager\Agent' -ErrorAction Stop
                        [ordered]@{
                            version       = $vmmReg.Version
                            vmmServerName = $vmmReg.VMMServerName
                            machineId     = $vmmReg.MachineId
                            agentStatus   = [string]$scvmmAgentSvc.Status
                            hostGroup     = try { $vmmReg.HostGroupPath } catch { $null }
                        }
                    } catch { [ordered]@{ agentStatus = [string]$scvmmAgentSvc.Status } }
                }

                # SCOM agent depth (management group, management server, heartbeat)
                $scomInfo = $null
                $momAgentSvc = Get-Service -Name 'HealthService' -ErrorAction SilentlyContinue
                if ($momAgentSvc -and $momAgentSvc.DisplayName -match 'Operations Manager|SCOM') {
                    $scomInfo = [ordered]@{ agentStatus = [string]$momAgentSvc.Status }
                    try {
                        $scomSetup = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup' -ErrorAction Stop
                        $scomInfo['version'] = $scomSetup.ServerVersion
                    } catch { }
                    try {
                        $mgGroupKeys = @(Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Agent Management Groups' -ErrorAction Stop)
                        if (@($mgGroupKeys).Count -gt 0) {
                            $firstMg = Get-ItemProperty -Path $mgGroupKeys[0].PSPath -ErrorAction Stop
                            $scomInfo['managementGroupName'] = $mgGroupKeys[0].PSChildName
                            $scomInfo['managementServer']    = try { $firstMg.ManagementServerDNSName } catch { $null }
                            $scomInfo['lastHeartbeat']       = try { (Get-ItemProperty -Path "$($mgGroupKeys[0].PSPath)\Agent Parameters" -Name 'LastHeartbeatRequestTime' -ErrorAction Stop).LastHeartbeatRequestTime } catch { $null }
                        }
                    } catch { }
                }

                # Performance baseline
                # Issue #69: Multi-sample CPU (average and peak over 3 samples)
                $cpuSamples = @(try {
                    (Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 3 -MaxSamples 3 -ErrorAction Stop).CounterSamples | ForEach-Object { $_.CookedValue }
                } catch {
                    $wmiCpu = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" -ErrorAction SilentlyContinue
                    if ($wmiCpu) { @($wmiCpu.PercentProcessorTime) } else { @() }
                })
                $cpuAvg  = if ($cpuSamples.Count -gt 0) { [math]::Round(($cpuSamples | Measure-Object -Average).Average, 1) } else { $null }
                $cpuPeak = if ($cpuSamples.Count -gt 0) { [math]::Round(($cpuSamples | Measure-Object -Maximum).Maximum, 1) } else { $null }
                $memory = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
                # Issue #69: Committed memory counters
                $memoryPerf = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Memory -ErrorAction SilentlyContinue
                # Issue #69: Hyper-V Hypervisor Logical Processor % Total Run Time
                $hypervRunTime = try { [math]::Round((Get-Counter '\Hyper-V Hypervisor Logical Processor(_Total)\% Total Run Time' -ErrorAction Stop).CounterSamples[0].CookedValue, 1) } catch { $null }
                $disks = @(Get-CimInstance -ClassName Win32_PerfFormattedData_PerfDisk_LogicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne '_Total' } | Select-Object Name, PercentFreeSpace, AvgDiskSecPerTransfer, DiskTransfersPerSec)
                $network = @(Get-CimInstance -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface -ErrorAction SilentlyContinue | Select-Object Name, BytesTotalPersec, CurrentBandwidth)

                # Issue #69: Storage IOPS, throughput, and avg latency
                $storageIopsDetail = try {
                    $pCounters = Get-Counter @(
                        '\PhysicalDisk(_Total)\Disk Reads/sec',
                        '\PhysicalDisk(_Total)\Disk Writes/sec',
                        '\PhysicalDisk(_Total)\Disk Read Bytes/sec',
                        '\PhysicalDisk(_Total)\Disk Write Bytes/sec',
                        '\PhysicalDisk(_Total)\Avg. Disk sec/Read',
                        '\PhysicalDisk(_Total)\Avg. Disk sec/Write'
                    ) -ErrorAction Stop
                    $oidx = [ordered]@{ readIops = $null; writeIops = $null; readThroughputMBps = $null; writeThroughputMBps = $null; avgReadLatencyMs = $null; avgWriteLatencyMs = $null }
                    $pCounters.CounterSamples | ForEach-Object {
                        if     ($_.Path -match 'Reads/sec$')       { $oidx['readIops']            = [math]::Round($_.CookedValue, 1) }
                        elseif ($_.Path -match 'Writes/sec$')      { $oidx['writeIops']           = [math]::Round($_.CookedValue, 1) }
                        elseif ($_.Path -match 'Read Bytes/sec$')  { $oidx['readThroughputMBps']  = [math]::Round($_.CookedValue / 1MB, 2) }
                        elseif ($_.Path -match 'Write Bytes/sec$') { $oidx['writeThroughputMBps'] = [math]::Round($_.CookedValue / 1MB, 2) }
                        elseif ($_.Path -match 'sec/Read$')        { $oidx['avgReadLatencyMs']    = [math]::Round($_.CookedValue * 1000, 2) }
                        elseif ($_.Path -match 'sec/Write$')       { $oidx['avgWriteLatencyMs']   = [math]::Round($_.CookedValue * 1000, 2) }
                    }
                    $oidx
                } catch { $null }
                # Issue #69: S2D cache hit ratio
                $s2dCacheHitRatio = try {
                    $cacheSamples = (Get-Counter '\Cluster Storage Hybrid Disks(*)\Cache Hit %' -ErrorAction Stop).CounterSamples
                    [math]::Round(($cacheSamples | Measure-Object -Property CookedValue -Average).Average, 1)
                } catch { $null }
                # Issue #69: Network adapter errors and discards
                $networkErrors = @(try {
                    Get-NetAdapterStatistics -ErrorAction Stop | ForEach-Object {
                        $stat = $_
                        $nic = Get-NetAdapter -Name $stat.Name -ErrorAction SilentlyContinue
                        if ($stat.OutboundPacketErrors -gt 0 -or $stat.ReceivedPacketErrors -gt 0 -or $stat.OutboundDiscardedPackets -gt 0 -or $stat.ReceivedDiscardedPackets -gt 0) {
                            [ordered]@{
                                name                     = $stat.Name
                                linkSpeedMbps            = if ($nic) { [math]::Round($nic.LinkSpeed / 1MB, 0) } else { $null }
                                outboundDiscardedPackets  = $stat.OutboundDiscardedPackets
                                outboundPacketErrors      = $stat.OutboundPacketErrors
                                receivedDiscardedPackets  = $stat.ReceivedDiscardedPackets
                                receivedPacketErrors      = $stat.ReceivedPacketErrors
                            }
                        }
                    } | Where-Object { $_ }
                } catch { @() })

                # RDMA Activity counters
                $rdmaCounters = @(try {
                    Get-CimInstance -ClassName Win32_PerfFormattedData_RdmaCounterSet_RDMAActivity -ErrorAction Stop |
                        Select-Object -First 10 Name, RDMAInboundBytes, RDMAOutboundBytes, RDMAInboundFrames, RDMAOutboundFrames |
                        ForEach-Object { [ordered]@{ adapter = $_.Name; inboundBytes = $_.RDMAInboundBytes; outboundBytes = $_.RDMAOutboundBytes; inboundFrames = $_.RDMAInboundFrames; outboundFrames = $_.RDMAOutboundFrames } }
                } catch {
                    # Fallback: SMB Direct counters
                    @(try {
                        Get-CimInstance -ClassName Win32_PerfFormattedData_SmbClientShares_SMBClientShares -ErrorAction Stop |
                            Select-Object -First 5 Name, BytesReceivedPersec, BytesSentPersec |
                            ForEach-Object { [ordered]@{ adapter = $_.Name; inboundBytes = $_.BytesReceivedPersec; outboundBytes = $_.BytesSentPersec; inboundFrames = $null; outboundFrames = $null } }
                    } catch { @() })
                })

                # CSV cache hit statistics
                $csvCacheStats = @(try {
                    if (Get-Command -Name Get-ClusterSharedVolume -ErrorAction SilentlyContinue) {
                        Get-ClusterSharedVolume -ErrorAction SilentlyContinue | ForEach-Object {
                            $csv = $_
                            try {
                                $state = $csv | Get-ClusterSharedVolumeState -ErrorAction Stop
                                [ordered]@{ name = $csv.Name; ownerNode = $csv.OwnerNode.Name; volumeName = $state.VolumeFriendlyName; blockMode = $state.BlockRedirectedIOReason; fileMode = $state.FileSystemRedirectedIOReason }
                            } catch {
                                [ordered]@{ name = $csv.Name; ownerNode = $csv.OwnerNode.Name }
                            }
                        }
                    }
                } catch { @() })

                # Storage reliability counters (drive latency outliers)
                $driveLatencyOutliers = @(try {
                    if (Get-Command -Name Get-StorageReliabilityCounter -ErrorAction SilentlyContinue) {
                        Get-PhysicalDisk -ErrorAction Stop | ForEach-Object {
                            $disk = $_
                            $counter = $disk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue
                            if ($counter -and ($counter.ReadLatencyMax -gt 1000 -or $counter.WriteLatencyMax -gt 1000)) {
                                [ordered]@{
                                    diskFriendlyName   = $disk.FriendlyName
                                    serialNumber       = $disk.SerialNumber
                                    readLatencyMaxMs   = $counter.ReadLatencyMax
                                    writeLatencyMaxMs  = $counter.WriteLatencyMax
                                    readLatencyAvgMs   = $counter.ReadLatencyAverage
                                    writeLatencyAvgMs  = $counter.WriteLatencyAverage
                                    temperature        = $counter.Temperature
                                    wearLevel          = $counter.Wear
                                }
                            }
                        } | Where-Object { $null -ne $_ }
                    }
                } catch { @() })

                # Multi-source event log analysis
                $eventLogAnalysis = @(foreach ($logName in @(
                    'Microsoft-Windows-Health/Operational',
                    'Microsoft-Windows-StorageSpaces-Driver/Operational',
                    'Microsoft-Windows-FailoverClustering/Operational',
                    'Microsoft-Windows-SDDC-Management/Operational',
                    'Microsoft-Windows-Hyper-V-VMMS-Admin',
                    'Microsoft-Windows-Hyper-V-Worker-Admin',
                    'Microsoft-Windows-StorageReplica/Admin',
                    'Application'
                )) {
                    try {
                        $logEvents = @(Get-WinEvent -LogName $logName -MaxEvents 500 -ErrorAction Stop)
                        $topIds = @($logEvents | Group-Object -Property Id | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object {
                            $sample = $_.Group[0].Message
                            [ordered]@{ eventId = $_.Name; count = $_.Count; level = $_.Group[0].LevelDisplayName; sample = $sample.Substring(0, [Math]::Min(200, $sample.Length)) }
                        })
                        [ordered]@{ logName = $logName; eventCount = @($logEvents).Count; topEventIds = @($topIds) }
                    } catch {
                        [ordered]@{ logName = $logName; eventCount = 0; topEventIds = @() }
                    }
                })

                # System event log (kept for backward compat)
                $events = if (Get-Command -Name Get-WinEvent -ErrorAction SilentlyContinue) {
                    @(Get-WinEvent -LogName System -MaxEvents 15 -ErrorAction SilentlyContinue | Select-Object TimeCreated, Id, LevelDisplayName, ProviderName)
                } else { @() }

                [ordered]@{
                    node              = $env:COMPUTERNAME
                    tools             = @($managementServices)
                    thirdPartyAgents  = @($thirdPartyAgents)
                    wac               = [ordered]@{
                        installed       = $null -ne $wacService
                        status          = if ($wacService) { [string]$wacService.Status } else { 'not-installed' }
                        version         = $wacVersion
                        gatewayMode     = $wacGatewayMode
                        url             = $wacUrl
                        authMode        = $wacAuthMode
                        azureResourceId = $wacAzureResourceId
                        cert            = $wacCert
                        extensionCount  = @($wacExtensions).Count
                        extensions      = @($wacExtensions)
                    }
                    scvmm             = $scvmmInfo
                    scom              = $scomInfo
                    compute           = [ordered]@{
                        cpuUtilizationPercent      = $cpuAvg
                        cpuPeakPercent             = $cpuPeak
                        cpuSampleCount             = @($cpuSamples).Count
                        availableMemoryMb          = if ($memory) { [math]::Round(($memory.FreePhysicalMemory / 1KB), 2) } else { $null }
                        totalMemoryMb              = if ($memory) { [math]::Round(($memory.TotalVisibleMemorySize / 1KB), 2) } else { $null }
                        committedBytesMb           = if ($memoryPerf) { [math]::Round($memoryPerf.CommittedBytes / 1MB, 0) } else { $null }
                        commitLimitMb              = if ($memoryPerf) { [math]::Round($memoryPerf.CommitLimit / 1MB, 0) } else { $null }
                        committedRatio             = if ($memoryPerf -and $memoryPerf.CommitLimit -gt 0) { [math]::Round($memoryPerf.CommittedBytes / $memoryPerf.CommitLimit, 3) } else { $null }
                        hypervHypervisorRunTimePct = $hypervRunTime
                        storageIops                = $storageIopsDetail
                        csvCacheHitRatio           = $s2dCacheHitRatio
                        networkErrors              = @($networkErrors)
                    }
                    storage           = @($disks)
                    networking        = @($network)
                    rdmaCounters      = @($rdmaCounters)
                    csvCacheStats     = @($csvCacheStats)
                    driveLatencyOutliers = @($driveLatencyOutliers)
                    eventLogAnalysis  = @($eventLogAnalysis)
                    events            = @($events)
                }
            }
        }
    )

    if ($nodeSnapshots.Count -eq 0) {
        throw 'Management and performance collector did not return any usable node data.'
    }

    $tools = @($nodeSnapshots | ForEach-Object { $_.tools } | Where-Object { $null -ne $_ })
    $allThirdPartyAgents = @($nodeSnapshots | ForEach-Object { $sn = $_; $_.thirdPartyAgents | ForEach-Object { [ordered]@{ node = $sn.node; name = $_.Name; displayName = $_.DisplayName; status = [string]$_.Status } } })
    $compute = @($nodeSnapshots | ForEach-Object { [ordered]@{ node = $_.node; metrics = $_.compute } })
    $storage = @($nodeSnapshots | ForEach-Object { [ordered]@{ node = $_.node; metrics = $_.storage } })
    $network = @($nodeSnapshots | ForEach-Object { [ordered]@{ node = $_.node; metrics = $_.networking } })
    $allRdmaCounters = @($nodeSnapshots | ForEach-Object { $sn = $_; $_.rdmaCounters | ForEach-Object { [ordered]@{ node = $sn.node; adapter = $_.adapter; inboundBytes = $_.inboundBytes; outboundBytes = $_.outboundBytes } } })
    $allDriveLatencyOutliers = @($nodeSnapshots | ForEach-Object { $sn = $_; $_.driveLatencyOutliers | ForEach-Object { $_ + [ordered]@{ node = $sn.node } } })
    $allEventLogAnalysis = @($nodeSnapshots | ForEach-Object { $sn = $_; $_.eventLogAnalysis | ForEach-Object { $_ + [ordered]@{ node = $sn.node } } })
    $outliers = @($nodeSnapshots | Where-Object { $_.compute.cpuUtilizationPercent -gt 85 })
    $events = @($nodeSnapshots | ForEach-Object { [ordered]@{ node = $_.node; values = $_.events } })
    $wacNodes = @($nodeSnapshots | ForEach-Object { [ordered]@{ node = $_.node; wac = $_.wac } })
    $scvmmNodes = @($nodeSnapshots | Where-Object { $null -ne $_.scvmm } | ForEach-Object { [ordered]@{ node = $_.node; scvmm = $_.scvmm } })
    $scomNodes = @($nodeSnapshots | Where-Object { $null -ne $_.scom } | ForEach-Object { [ordered]@{ node = $_.node; scom = $_.scom } })

    $relationships = New-Object System.Collections.ArrayList
    foreach ($snapshot in @($nodeSnapshots)) {
        foreach ($tool in @($snapshot.tools)) {
            [void]$relationships.Add((New-RangerRelationship -SourceType 'cluster-node' -SourceId $snapshot.node -TargetType 'management-tool' -TargetId $tool.Name -RelationshipType 'managed-by' -Properties ([ordered]@{ status = $tool.Status; displayName = $tool.DisplayName })))
        }
        foreach ($agent in @($snapshot.thirdPartyAgents)) {
            [void]$relationships.Add((New-RangerRelationship -SourceType 'cluster-node' -SourceId $snapshot.node -TargetType 'third-party-agent' -TargetId $agent.Name -RelationshipType 'monitored-by' -Properties ([ordered]@{ status = $agent.Status })))
        }
    }

    $wacInstalled = @($nodeSnapshots | Where-Object { $_.wac.installed }).Count
    $wacCertExpiring = @($nodeSnapshots | Where-Object { $null -ne $_.wac.cert -and $_.wac.cert.daysUntilExpiry -lt 90 }).Count

    $summary = [ordered]@{
        averageCpuUtilizationPercent = Get-RangerAverageValue -Values @($nodeSnapshots | ForEach-Object { $_.compute.cpuUtilizationPercent })
        averageAvailableMemoryMb     = Get-RangerAverageValue -Values @($nodeSnapshots | ForEach-Object { $_.compute.availableMemoryMb })
        runningManagementServices    = @($tools | Where-Object { $_.Status -eq 'Running' }).Count
        toolNames                    = @(Get-RangerGroupedCount -Items $tools -PropertyName 'Name')
        highCpuNodes                 = @($outliers).Count
        eventSeverities              = @(Get-RangerGroupedCount -Items @($nodeSnapshots | ForEach-Object { $_.events }) -PropertyName 'LevelDisplayName')
        wacInstalledNodes            = $wacInstalled
        wacCertExpiringNodes         = $wacCertExpiring
        scvmmAgentNodes              = @($scvmmNodes).Count
        scomAgentNodes               = @($scomNodes).Count
        thirdPartyAgentTypes         = @(Get-RangerGroupedCount -Items $allThirdPartyAgents -PropertyName 'name')
        rdmaAdaptersDetected         = @($allRdmaCounters).Count
        driveLatencyOutlierCount     = @($allDriveLatencyOutliers).Count
    }

    $findings = New-Object System.Collections.ArrayList
    if ($outliers.Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'Compute baseline indicates high sustained CPU on one or more nodes' -Description 'The performance baseline returned total CPU values above 85 percent for at least one node.' -AffectedComponents (@($outliers | ForEach-Object { $_.node })) -CurrentState 'performance outlier' -Recommendation 'Validate whether the baseline was captured during an expected workload peak or whether capacity and placement need review.'))
    }

    if (@($tools | Where-Object { $_.Name -eq 'HealthService' -and $_.Status -ne 'Running' }).Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'HealthService is not running on one or more nodes' -Description 'Management tooling inventory found the Windows Admin Center or Azure Local health service stopped on at least one node.' -AffectedComponents (@($tools | Where-Object { $_.Name -eq 'HealthService' -and $_.Status -ne 'Running' } | ForEach-Object { $_.DisplayName })) -CurrentState 'management service stopped' -Recommendation 'Review service health, dependent agents, and management-plane readiness before using the environment for operational handoff.'))
    }

    $lowMemoryNodes = @($nodeSnapshots | Where-Object { $null -ne $_.compute.availableMemoryMb -and $_.compute.availableMemoryMb -lt 65536 })
    if ($lowMemoryNodes.Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity informational -Title 'One or more nodes report less than 64 GB of free memory' -Description 'The performance baseline found nodes with relatively low available memory for a formal handoff snapshot.' -AffectedComponents (@($lowMemoryNodes | ForEach-Object { $_.node })) -CurrentState 'memory headroom reduced' -Recommendation 'Confirm whether the memory posture is expected for this collection window or whether workload placement and capacity should be reviewed.'))
    }

    if ($wacCertExpiring -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'Windows Admin Center TLS certificate is expiring within 90 days' -Description "WAC certificate inventory found $wacCertExpiring node(s) with a WAC SSL certificate expiring within 90 days." -CurrentState "$wacCertExpiring nodes with expiring WAC certificate" -Recommendation 'Renew the WAC TLS certificate before handoff to avoid browser-side security warnings for operational users.'))
    }

    if (@($allDriveLatencyOutliers).Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'Drive latency outliers detected via Storage Reliability Counters' -Description "Performance baselines found $(@($allDriveLatencyOutliers).Count) physical disk(s) with maximum read or write latency above 1000 ms." -AffectedComponents (@($allDriveLatencyOutliers | ForEach-Object { "$($_.node) - $($_.diskFriendlyName)" })) -CurrentState "$(@($allDriveLatencyOutliers).Count) disks with high latency" -Recommendation 'Review drive health and predictive failure indicators. Replace any disks with sustained high latency or elevated wear levels before handoff.'))
    }

    return @{
        Status        = if ($findings.Count -gt 0) { 'partial' } else { 'success' }
        Domains       = @{
            managementTools = [ordered]@{
                tools           = ConvertTo-RangerHashtable -InputObject @($tools)
                agents          = ConvertTo-RangerHashtable -InputObject @($tools | Where-Object { $_.Status -eq 'Running' })
                thirdPartyAgents = ConvertTo-RangerHashtable -InputObject $allThirdPartyAgents
                wac             = ConvertTo-RangerHashtable -InputObject $wacNodes
                scvmm           = ConvertTo-RangerHashtable -InputObject $scvmmNodes
                scom            = ConvertTo-RangerHashtable -InputObject $scomNodes
                summary         = [ordered]@{
                    totalServices    = @($tools).Count
                    runningServices  = @($tools | Where-Object { $_.Status -eq 'Running' }).Count
                    serviceNames     = $summary.toolNames
                    wacInstalled     = $wacInstalled
                    scvmmNodes       = $summary.scvmmAgentNodes
                    scomNodes        = $summary.scomAgentNodes
                    thirdPartyTypes  = $summary.thirdPartyAgentTypes
                }
            }
            performance = [ordered]@{
                nodes                = ConvertTo-RangerHashtable -InputObject @($nodeSnapshots | ForEach-Object { $_.node })
                compute              = ConvertTo-RangerHashtable -InputObject $compute
                storage              = ConvertTo-RangerHashtable -InputObject $storage
                networking           = ConvertTo-RangerHashtable -InputObject $network
                rdmaCounters         = ConvertTo-RangerHashtable -InputObject $allRdmaCounters
                csvCacheStats        = ConvertTo-RangerHashtable -InputObject @($nodeSnapshots | ForEach-Object { $sn = $_; $_.csvCacheStats | ForEach-Object { $_ + [ordered]@{ node = $sn.node } } })
                driveLatencyOutliers = ConvertTo-RangerHashtable -InputObject $allDriveLatencyOutliers
                eventLogAnalysis     = ConvertTo-RangerHashtable -InputObject $allEventLogAnalysis
                outliers             = ConvertTo-RangerHashtable -InputObject @($outliers | ForEach-Object { [ordered]@{ node = $_.node; cpuUtilizationPercent = $_.compute.cpuUtilizationPercent } })
                events               = ConvertTo-RangerHashtable -InputObject $events
                summary              = $summary
            }
        }
        Findings      = @($findings)
        Relationships = @($relationships)
        RawEvidence   = ConvertTo-RangerHashtable -InputObject $nodeSnapshots
    }
}