<#
.SYNOPSIS
    Daily VM metadata report for PowerBI trending and hardware capacity planning.

.DESCRIPTION
    Collects VM metadata from vCenter including compute, storage, OS, and VMware Tools
    information using only data available within vCenter (no direct guest connections).
    Exports a timestamped CSV each run -- append daily runs to build a historical dataset
    suitable for PowerBI trend analysis and physical hardware purchasing decisions.

.PARAMETER vCenterServers
    One or more vCenter server hostnames. Defaults to itdvmvc1.nd.gov and itdvmvc2.nd.gov.

.PARAMETER DatacenterFilter
    Wildcard filter applied to datacenter names. Defaults to 'Primary*'.

.PARAMETER OutputPath
    Directory where CSV and log files are written.
    Defaults to C:\ITDSCRIPT\Reports\VMMetadata.

.PARAMETER CredentialPath
    Path to a saved PSCredential XML file for unattended/scheduled runs.
    Create one interactively with:
        Get-Credential | Export-Clixml -Path C:\ITDSCRIPT\Creds\vCenter.xml
    When omitted the script prompts for credentials.

.EXAMPLE
    # Interactive run
    .\VMware-VMDailyMetadataReport.ps1

.EXAMPLE
    # Scheduled / unattended run
    .\VMware-VMDailyMetadataReport.ps1 -CredentialPath 'C:\ITDSCRIPT\Creds\vCenter.xml'

.NOTES
    Run the following DDL once to create the destination table:

        DROP TABLE IF EXISTS [dbo].[VMware_Trends_VM]

        CREATE TABLE [dbo].[VMware_Trends_VM] (
            [ReportDate]          DATETIME2      NOT NULL,
            [VMName]              NVARCHAR(255)  NOT NULL,
            [Datacenter]          NVARCHAR(100)  NULL,
            [Cluster]             NVARCHAR(100)  NULL,
            [PowerState]          NVARCHAR(50)   NULL,
            [IsSRMPlaceholder]    BIT            NOT NULL,
            [StoragePlatform]     NVARCHAR(100)  NULL,
            [GuestOS]             NVARCHAR(255)  NULL,
            [vCPUs]               INT            NULL,
            [MemoryGB]            DECIMAL(10,2)  NULL,
            [ProvisionedSpaceGB]  DECIMAL(12,2)  NULL,
            [UsedSpaceGB]         DECIMAL(12,2)  NULL,
            [GuestDiskCapacityGB] DECIMAL(12,2)  NULL,
            [GuestDiskUsedGB]     DECIMAL(12,2)  NULL,
            [ToolsRunningStatus]        NVARCHAR(100)  NULL,
            [ToolsVersionStatus]        NVARCHAR(100)  NULL,
            [ToolsVersion]              NVARCHAR(50)   NULL,
            [Tag_DRProtection]          NVARCHAR(100)  NULL,
            [Tag_AppName]               NVARCHAR(255)  NULL,
            [Tag_VRDatastores]          NVARCHAR(255)  NULL,
            [Tag_VRRPO]                 NVARCHAR(100)  NULL,
            [Tag_DTAP]                  NVARCHAR(50)   NULL,
            [Tag_StartupPriority]       NVARCHAR(100)  NULL,
            [Tag_SRMRecoveryType]       NVARCHAR(100)  NULL,
            [Tag_LicensingRestrictions] NVARCHAR(255)  NULL
        )

    Guest OS disk capacity / used columns are NULL for powered-off VMs and SRM placeholders.
#>
[CmdletBinding()]
param(
)

#region --- Setup ---------------------------------------------------------------
[string] $OutputPath = 'C:\temp\VM_Trends\'
[string] $ServerInstance = 'itdintsql22p1.nd.gov\INTSQL22P1'
[string] $Database = 'ITD-Systems-Automation'
[string] $Table = 'VMware_Trends_VM'

[System.Management.Automation.PSCredential] $SqlCredential = $Secret:sql_itdpsu1
Connect-ITDvCenter -Credential $Secret:ndgov_svcitdvmvcro

$RunDate = Get-Date
$DateStamp = $RunDate.ToString('yyyyMMdd')
$Timestamp = $RunDate.ToString('yyyy-MM-dd')

if (-not (Test-Path -Path $OutputPath)) {
    New-Item -ItemType Directory -Path $OutputPath | Out-Null
}

Start-Transcript -Path (Join-Path $OutputPath "VMMetadataReport_$DateStamp.log") -Append

#endregion

#region --- Build VMHost -> Cluster/Datacenter Lookups (avoids per-VM API calls) -

Write-Verbose 'Building host-to-cluster and host-to-datacenter maps...'
$HostClusterMap = @{}
$HostDatacenterMap = @{}

Get-Datacenter | ForEach-Object {
    $DatacenterName = $_.Name
    Get-VMHost -Location $_ | ForEach-Object {
        $HostDatacenterMap[$_.Name] = $DatacenterName
    }
}

Get-Cluster | ForEach-Object {
    $ClusterName = $_.Name
    Get-VMHost -Location $_ | ForEach-Object {
        $HostClusterMap[$_.Name] = $ClusterName
    }
}

#endregion

#region --- Collect VM Data -----------------------------------------------------

Write-Verbose "Gathering VMs"

# Include ALL VMs (SRM placeholders flagged via column, not excluded).
# vCLS agent VMs are excluded -- they are vSphere internal and not customer workloads.
$AllVMs = Get-VM | Where-Object { $_.Name -notlike 'vCLS*' }

#--- Pre-fetch all tag assignments in one API call (avoids per-VM Get-TagAssignment)
Write-Verbose 'Pre-fetching VM tag assignments...'
$TagLookup = @{}
Get-TagAssignment -Entity $AllVMs | ForEach-Object {
    $VMId    = $_.Entity.Uid
    $Cat     = $_.Tag.Category.Name
    $TagName = $_.Tag.Name
    if (-not $TagLookup.ContainsKey($VMId)) { $TagLookup[$VMId] = @{} }
    if ($TagLookup[$VMId].ContainsKey($Cat)) {
        $TagLookup[$VMId][$Cat] += "; $TagName"
    } else {
        $TagLookup[$VMId][$Cat] = $TagName
    }
}

Write-Verbose "Processing $($AllVMs.Count) VMs..."

$Results = foreach ($VM in $AllVMs) {
    Write-Verbose -Message ("Start " + $VM.Name) -Verbose
    $StoragePlatforms = $null
    $StoragePlatform = $null

    $Ext = $VM.ExtensionData  # single API object -- reuse for all fields

    #--- SRM placeholder detection
    $IsSRMPlaceholder = $Ext.Summary.Config.ManagedBy.Type -eq 'placeholderVm'

    #--- Cluster / Datacenter (null-safe: standalone hosts have no cluster entry)
    $ClusterName    = $HostClusterMap[$VM.VMHost.Name]
    $DatacenterName = $HostDatacenterMap[$VM.VMHost.Name]

    #--- Tag assignments (pre-fetched; null when category not assigned to this VM)
    $VMTags = if ($TagLookup.ContainsKey($VM.Uid)) { $TagLookup[$VM.Uid] } else { @{} }

    #--- Storage platform parsed from datastore name convention: VMCLUSTER_LUN_PLATFORM_Desc
    #    Segment 2 = storage platform identifier (e.g. FS92, A9K).
    #    Cluster grouping uses the compute Cluster column -- no need to re-derive it here.
    $StoragePlatforms = foreach ($DSName in $Ext.Config.DatastoreUrl.Name) {
        $Segments = $DSName -split '_'
        if ($Segments.Count -ge 3) { $Segments[2] }
    }
    $StoragePlatform = ($StoragePlatforms | Sort-Object -Unique) -join '; '

    #--- VMware Tools guest disk info
    #    Populated only when Tools is running; null otherwise.
    $GuestDiskCapacityGB = $null
    $GuestDiskUsedGB     = $null
    if ($Ext.Guest.Disk) {
        $TotalCapBytes = ($Ext.Guest.Disk | Measure-Object -Property Capacity  -Sum).Sum
        $TotalFreeBytes = ($Ext.Guest.Disk | Measure-Object -Property FreeSpace -Sum).Sum
        $GuestDiskCapacityGB = [Math]::Round($TotalCapBytes / 1GB, 2)
        $GuestDiskUsedGB = [Math]::Round(($TotalCapBytes - $TotalFreeBytes) / 1GB, 2)
    }

    [PSCustomObject]@{
        # --- Identity & grouping
        ReportDate          = $Timestamp          # for PowerBI time-series/trend axis
        VMName              = $VM.Name
        Datacenter          = $DatacenterName
        Cluster             = $ClusterName
        PowerState          = $VM.PowerState
        IsSRMPlaceholder    = $IsSRMPlaceholder
        StoragePlatform     = $StoragePlatform
        GuestOS             = $Ext.Guest.GuestFullName

        # --- Compute
        vCPUs               = $VM.NumCpu
        MemoryGB            = $VM.MemoryGB

        # --- Datastore-level storage
        # ProvisionedSpaceGB : maximum the VM could consume (thin disks counted at max size)
        # UsedSpaceGB        : bytes actually committed on datastores right now
        ProvisionedSpaceGB  = [Math]::Round($VM.ProvisionedSpaceGB, 2)
        UsedSpaceGB         = [Math]::Round($VM.UsedSpaceGB, 2)

        # --- Guest OS-level storage (from VMware Tools; null when Tools not running)
        # GuestDiskCapacityGB : sum of all volume capacities seen inside the guest
        # GuestDiskUsedGB     : sum of space consumed across those volumes
        GuestDiskCapacityGB = $GuestDiskCapacityGB
        GuestDiskUsedGB     = $GuestDiskUsedGB

        # --- VMware Tools
        # ToolsRunningStatus : guestToolsRunning | guestToolsNotRunning | guestToolsExecutingScripts
        # ToolsVersionStatus : guestToolsCurrent | guestToolsNeedUpgrade | guestToolsUnmanaged | guestToolsTooNew
        # ToolsVersion       : numeric build version string reported by vCenter
        ToolsRunningStatus  = $Ext.Guest.ToolsRunningStatus
        ToolsVersionStatus  = $Ext.Guest.ToolsVersionStatus
        ToolsVersion        = $Ext.Guest.ToolsVersion

        # --- vCenter Tags
        Tag_DRProtection          = $VMTags['DR Protection']
        Tag_AppName               = if ($null -ne $VMTags['AppName']) { $VMTags['AppName'] } else { 'N/A' }
        Tag_VRDatastores          = $VMTags['VR Datastores']
        Tag_VRRPO                 = $VMTags['VR RPO']
        Tag_DTAP                  = $VMTags['DTAP']
        Tag_StartupPriority       = $VMTags['StartupPriority']
        Tag_SRMRecoveryType       = $VMTags['SRM Recovery Type']
        Tag_LicensingRestrictions = $VMTags['LicensingRestrictions']
    }
}

#region --- Back-fill SRM placeholder fields from real VM ----------------------
# SRM placeholder VMs share the same name as their real counterpart but lack
# guest-level data.  For the fields below, substitute the real VM's values so
# placeholders carry useful metadata for reporting.

$RealVMLookup = @{}
foreach ($R in $Results) {
    if (-not $R.IsSRMPlaceholder) { $RealVMLookup[$R.VMName] = $R }
}

$PlaceholderFieldsToFix = @(
    'GuestOS'
    'ProvisionedSpaceGB'
    'UsedSpaceGB'
    'GuestDiskCapacityGB'
    'GuestDiskUsedGB'
    'ToolsVersionStatus'
    'Tag_DRProtection'
    'Tag_AppName'
    'Tag_VRDatastores'
    'Tag_VRRPO'
    'Tag_DTAP'
    'Tag_StartupPriority'
    'Tag_SRMRecoveryType'
    'Tag_LicensingRestrictions'
)

foreach ($R in $Results) {
    if ($R.IsSRMPlaceholder -and $RealVMLookup.ContainsKey($R.VMName)) {
        $Real = $RealVMLookup[$R.VMName]
        foreach ($Field in $PlaceholderFieldsToFix) { $R.$Field = $Real.$Field }
    }
}

#endregion

#endregion

<#region --- Export CSV ---------------------------------------------------------

$OutputFile = Join-Path $OutputPath "VMMetadata_$DateStamp.csv"
$Results | Export-Csv -Path $OutputFile -NoTypeInformation
Write-Verbose "Exported $($Results.Count) VM records to: $OutputFile"

#endregion
#>
#region --- SQL Insert ---------------------------------------------------------

# Build a typed DataTable so Write-SqlTableData knows each column's type
# even when nullable columns contain null values. Type inference from raw
# PSCustomObjects fails when the first row has a null in a numeric column.
$DataTable = [System.Data.DataTable]::new()

$ColDefs = [ordered]@{
    ReportDate          = [datetime]
    VMName              = [string]
    Datacenter          = [string]
    Cluster             = [string]
    PowerState          = [string]
    IsSRMPlaceholder    = [bool]
    StoragePlatform     = [string]
    GuestOS             = [string]
    vCPUs               = [int]
    MemoryGB            = [decimal]
    ProvisionedSpaceGB  = [decimal]
    UsedSpaceGB         = [decimal]
    GuestDiskCapacityGB = [decimal]
    GuestDiskUsedGB     = [decimal]
    ToolsRunningStatus        = [string]
    ToolsVersionStatus        = [string]
    ToolsVersion              = [string]
    Tag_DRProtection          = [string]
    Tag_AppName               = [string]
    Tag_VRDatastores          = [string]
    Tag_VRRPO                 = [string]
    Tag_DTAP                  = [string]
    Tag_StartupPriority       = [string]
    Tag_SRMRecoveryType       = [string]
    Tag_LicensingRestrictions = [string]
}

foreach ($Col in $ColDefs.GetEnumerator()) {
    $Column = [System.Data.DataColumn]::new($Col.Key, $Col.Value)
    $Column.AllowDBNull = $true
    [void]$DataTable.Columns.Add($Column)
}

foreach ($Row in $Results) {
    $DataRow = $DataTable.NewRow()
    foreach ($Col in $ColDefs.Keys) {
        $Val = $Row.$Col
        $DataRow[$Col] = if ($null -ne $Val) { $Val } else { [DBNull]::Value }
    }
    [void]$DataTable.Rows.Add($DataRow)
}

$SqlParams = @{
    ServerInstance = $ServerInstance
    DatabaseName   = $Database
    SchemaName     = 'dbo'
    TableName      = $Table
    Credential     = $SqlCredential
    InputData      = $DataTable
}
Write-SqlTableData @SqlParams

Write-Verbose "Inserted $($DataTable.Rows.Count) VM records into [$Database].[dbo].[$Table]"

#endregion

#region --- Cleanup -------------------------------------------------------------
Stop-Transcript

#endregion
