modules/WorkloadProfile/WorkloadProfile.psm1

<#
    WorkloadProfile.psm1 - one row per server describing its resources and workload footprint,
    for target-capacity and workload-placement comparison across a fleet (read-only).

    Produces: WorkloadProfile.

    Built almost entirely from datasets the other collectors already produced (runs last), plus
    one live free-memory read. It does NOT recommend a VM size: the performance data here is a
    point-in-time snapshot, and sizing needs sustained counters and an explicit sizing workflow.
    tools\Merge-FleetDiscoveryResults.ps1 compares these rows between source and target servers.
#>


function Get-DiscoveryModuleMetadata {
    [pscustomobject]@{
        ModuleName='WorkloadProfile'; DisplayName='Workload & Target-Capacity Profile'; Category='Performance'; Version='1.0.0'
        DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false
        RequiresRole=$null; EstimatedImpact='Minimal'; CanRunAsSystem=$true
        ProducesDatasets=@('WorkloadProfile')
        ProducesRisks=$true; ProducesFollowUpQuestions=$false; SupportsDeepMode=$true; SupportsComplianceLens=$false
    }
}

function Test-DiscoveryPrerequisites {
    param([object]$Context)
    [pscustomobject]@{ ModuleName='WorkloadProfile'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() }
}

function Get-WorkloadProfileRow {
    <#
        Pure apart from -FreeMemoryGB (passed in): builds the profile from a dataset accessor.
        Unknown stays $null, never 0 - a missing share scan is "not measured", not "no data".
    #>

    param([Parameter(Mandatory)][scriptblock]$GetRows, [string]$ComputerName = $env:COMPUTERNAME, $FreeMemoryGB = $null, [string[]]$Functions = @())
    $first = { param($n) @(& $GetRows $n) | Select-Object -First 1 }
    $sum = { param($rows, $prop) $m = (@($rows) | Where-Object { $null -ne $_.$prop -and [string]$_.$prop -ne '' } | Measure-Object -Property $prop -Sum); if ($m.Count -gt 0) { [math]::Round([double]$m.Sum, 2) } else { $null } }

    $os = & $first 'OperatingSystem'; $hw = & $first 'Hardware'; $pv = & $first 'PhysicalVirtualDetection'; $perf = & $first 'PerformanceSnapshot'
    $procs = @(& $GetRows 'Processors')
    $vols = @(& $GetRows 'Volumes' | Where-Object { $_.DriveType -ne 'Removable' })
    $disks = @(& $GetRows 'Disks')
    $apps = @(& $GetRows 'InstalledApplications' | Where-Object { -not $_.SystemComponent })
    $sqlSum = & $first 'SqlServerSummary'
    $sqlInst = @(& $GetRows 'SqlInstances' | Where-Object { -not $_.IsWindowsInternalDatabase })
    $shares = @(& $GetRows 'SmbShares' | Where-Object { $_.IsUserShare })
    $scans = @(& $GetRows 'NtfsAclSummary')
    $ports = @(& $GetRows 'ListeningPorts' | Where-Object { $_.Protocol -ne 'UDP' -and -not $_.IsEphemeral })
    $agents = @(& $GetRows 'VendorAgents')
    $backup = @(& $GetRows 'BackupDiscovery')
    $stacks = @(& $GetRows 'AppStackSummary')
    $vms = @(& $GetRows 'HyperVVMs')

    $totalGB = & $sum $vols 'SizeGB'; $freeGB = & $sum $vols 'FreeGB'
    $memGB = if ($hw) { $hw.TotalPhysicalMemoryGB } else { $null }
    $dbRows = @(& $GetRows 'SqlDatabases')
    $sqlGB = if ($sqlSum -and $null -ne $sqlSum.TotalDatabaseSizeGB) { $sqlSum.TotalDatabaseSizeGB } else { & $sum $dbRows 'SizeGB' }
    [pscustomobject]@{
        ComputerName=$ComputerName
        OperatingSystem=$(if ($os) { [string]$os.Caption } else { '' })
        Platform=$(if ($pv) { [string]$pv.Platform } else { '' }); IsVirtual=$(if ($pv) { [bool]$pv.IsVirtual } else { $null })
        CpuModel=((@($procs | ForEach-Object { [string]$_.Name } | Select-Object -Unique)) -join '; ')
        Sockets=$procs.Count; PhysicalCores=$(& $sum $procs 'NumberOfCores'); LogicalProcessors=$(if ($hw) { $hw.LogicalProcessors } else { & $sum $procs 'NumberOfLogicalProcessors' })
        TotalMemoryGB=$memGB; AvailableMemoryGB=$FreeMemoryGB
        CpuPercentSnapshot=$(if ($perf) { $perf.CpuPercent } else { $null }); MemoryUsedPercentSnapshot=$(if ($perf) { $perf.MemoryUsedPercent } else { $null })
        VolumeCount=$vols.Count; TotalCapacityGB=$totalGB; FreeCapacityGB=$freeGB
        UsedCapacityGB=$(if ($null -ne $totalGB -and $null -ne $freeGB) { [math]::Round($totalGB - $freeGB, 2) } else { $null })
        DiskTypes=((@($disks | ForEach-Object { [string]$_.BusType } | Where-Object { $_ } | Select-Object -Unique)) -join ', ')
        DetectedRoles=($Functions -join '; ')
        InstalledApplicationCount=$apps.Count
        SqlInstanceCount=$sqlInst.Count; SqlDataGB=$sqlGB; SqlVersions=$(if ($sqlSum) { [string]$sqlSum.ProductVersions } else { '' })
        IisSiteCount=@(& $GetRows 'IisSites').Count
        UserShareCount=$shares.Count; ShareDataGB=$(if ($scans.Count) { & $sum $scans 'TotalSizeGB' } else { $null })
        ShareDataMeasured=[bool]($scans.Count -gt 0); ShareSizeUnknown=[bool]($shares.Count -gt 0 -and $scans.Count -eq 0)
        ListeningTcpPorts=((@($ports | ForEach-Object { [string]$_.LocalPort } | Select-Object -Unique | Sort-Object { [int]$_ })) -join ', ')
        HostedVmCount=$vms.Count
        ApplicationStacks=((@($stacks | ForEach-Object { '{0} ({1})' -f $_.Stack, $_.HostRole })) -join '; ')
        BackupIndicators=((@($backup | ForEach-Object { [string]$_.Product }) + @($agents | Where-Object { $_.Category -eq 'Backup' } | ForEach-Object { [string]$_.AgentName }) | Select-Object -Unique) -join ', ')
        MonitoringIndicators=((@($agents | Where-Object { $_.Category -in @('RMM','Monitoring','CloudMgmt') } | ForEach-Object { [string]$_.AgentName }) | Select-Object -Unique) -join ', ')
        SecurityAgentIndicators=((@($agents | Where-Object { $_.Category -eq 'EDR/AV' } | ForEach-Object { [string]$_.AgentName }) | Select-Object -Unique) -join ', ')
        PerformanceEvidence='Point-in-time snapshot only'
        SizingNote='Not a sizing recommendation: collect sustained CPU/memory/disk counters before choosing a target size.'
    }
}

function Invoke-DiscoveryCollection {
    param([object]$Context)
    $free = $null
    try { $os = Invoke-CimSafe -ClassName 'Win32_OperatingSystem' | Select-Object -First 1; if ($os -and $os.FreePhysicalMemory) { $free = [math]::Round(([double]$os.FreePhysicalMemory * 1KB) / 1GB, 2) } } catch { }
    $funcs = @()
    try { $funcs = @(Get-LikelyServerFunctions -Context $Context) } catch { }
    $getRows = { param($name) if ($Context.DataSets.Contains($name)) { @($Context.DataSets[$name].Rows) } else { @() } }
    $row = Get-WorkloadProfileRow -GetRows $getRows -ComputerName $Context.ComputerName -FreeMemoryGB $free -Functions $funcs
    return ,@{ WorkloadProfile=@($row) }
}

function ConvertTo-DiscoveryDatasets {
    param([object]$Context, $RawData)
    $rows = @(if ($RawData -and $RawData.WorkloadProfile) { @($RawData.WorkloadProfile) } else { @() })
    Add-DataSet -Context $Context -Name 'WorkloadProfile' -Description 'Server resources and workload footprint for target-capacity comparison (not a sizing recommendation).' -Rows $rows -Visibility 'Internal' -SourceModule 'WorkloadProfile' | Out-Null
}

Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Get-WorkloadProfileRow'