Private/Read-TagProfile.ps1

function Read-TagProfile {
    <#
    .SYNOPSIS
        Parses a YAML tag profile file into a PowerShell object.
    .DESCRIPTION
        Reads a VM-AutoTagger YAML tag profile and converts it to a structured
        PowerShell object suitable for use by Sync-VMTags and other functions.
 
        If the powershell-yaml module is installed, it will be used for parsing.
        Otherwise, the built-in ConvertFrom-BasicYaml function is used as a fallback.
 
        The returned object includes the profile name, description, and an array
        of category definitions with their matching rules, tiers, patterns, and
        other configuration.
    .PARAMETER Path
        The file system path to the YAML profile file.
    .PARAMETER YamlContent
        Raw YAML string content to parse instead of reading from a file.
    .EXAMPLE
        $profile = Read-TagProfile -Path "C:\Profiles\default.yml"
        $profile.categories | ForEach-Object { $_.name }
    .EXAMPLE
        $yaml = Get-Content .\enterprise.yml -Raw
        $profile = Read-TagProfile -YamlContent $yaml
    .NOTES
        Author: Larry Roberts
        Part of the VM-AutoTagger module.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName = 'File')]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$Path,

        [Parameter(Mandatory, ParameterSetName = 'Content')]
        [string]$YamlContent
    )

    process {
        # Get the raw YAML content
        if ($PSCmdlet.ParameterSetName -eq 'File') {
            Write-Verbose "Reading tag profile from: $Path"
            $YamlContent = Get-Content -Path $Path -Raw -ErrorAction Stop
        }

        # Attempt to use powershell-yaml if available, fall back to built-in parser
        $parsed = $null
        $usedModule = $false

        if (Get-Module -ListAvailable -Name 'powershell-yaml' -ErrorAction SilentlyContinue) {
            try {
                Import-Module powershell-yaml -ErrorAction Stop
                $parsed = ConvertFrom-Yaml -Yaml $YamlContent -ErrorAction Stop
                $usedModule = $true
                Write-Verbose "Parsed YAML using powershell-yaml module."
            }
            catch {
                Write-Verbose "powershell-yaml failed, falling back to built-in parser: $_"
            }
        }

        if (-not $usedModule -or $null -eq $parsed) {
            $parsed = ConvertFrom-BasicYaml -YamlContent $YamlContent
            Write-Verbose "Parsed YAML using built-in ConvertFrom-BasicYaml."
        }

        # Normalize the parsed structure into a consistent profile object
        $profile = [PSCustomObject]@{
            Name        = if ($parsed.name) { $parsed.name } else { 'unnamed' }
            Description = if ($parsed.description) { $parsed.description } else { '' }
            Categories  = [System.Collections.ArrayList]::new()
        }

        # Parse categories array
        $rawCategories = $null
        if ($parsed.categories) {
            $rawCategories = $parsed.categories
        }
        elseif ($parsed.Contains('categories')) {
            $rawCategories = $parsed['categories']
        }

        if ($null -ne $rawCategories) {
            foreach ($cat in $rawCategories) {
                $categoryObj = [PSCustomObject]@{
                    Name           = $cat.name
                    Source         = if ($cat.source) { $cat.source } else { 'direct' }
                    Cardinality    = if ($cat.cardinality) { $cat.cardinality } else { 'Single' }
                    Description    = if ($cat.description) { $cat.description } else { "Auto-tagged by VM-AutoTagger: $($cat.name)" }
                    Values         = @()
                    Tiers          = @()
                    Pattern        = if ($cat.pattern) { $cat.pattern } else { $null }
                    AttributeName  = if ($cat.attribute_name) { $cat.attribute_name } else { $null }
                    NamePattern    = if ($cat.name_pattern) { $cat.name_pattern } else { $null }
                    FolderMapping  = if ($cat.folder_mapping) { $cat.folder_mapping } else { $null }
                }

                # Parse values (wildcard/match-based)
                if ($cat.values) {
                    $valList = [System.Collections.ArrayList]::new()
                    foreach ($v in $cat.values) {
                        $valObj = [PSCustomObject]@{
                            Match   = if ($v.match) { $v.match } else { $null }
                            Tag     = if ($v.tag) { $v.tag } else { '' }
                            Default = if ($v.default) { [bool]$v.default } else { $false }
                        }
                        [void]$valList.Add($valObj)
                    }
                    $categoryObj.Values = $valList.ToArray()
                }

                # Parse tiers (numeric threshold-based)
                if ($cat.tiers) {
                    $tierList = [System.Collections.ArrayList]::new()
                    foreach ($t in $cat.tiers) {
                        $tierObj = [PSCustomObject]@{
                            Max     = if ($null -ne $t.max) { [double]$t.max } else { [double]::MaxValue }
                            Min     = if ($null -ne $t.min) { [double]$t.min } else { 0 }
                            Tag     = if ($t.tag) { $t.tag } else { '' }
                            Default = if ($t.default) { [bool]$t.default } else { $false }
                        }
                        [void]$tierList.Add($tierObj)
                    }
                    $categoryObj.Tiers = $tierList.ToArray()
                }

                [void]$profile.Categories.Add($categoryObj)
            }
        }

        Write-Verbose "Profile '$($profile.Name)' loaded with $($profile.Categories.Count) categories."
        return $profile
    }
}

function Get-DefaultTagProfile {
    <#
    .SYNOPSIS
        Returns the built-in default tag profile when no YAML file is specified.
    .DESCRIPTION
        Provides the standard 10-category tag profile used by Sync-VMTags when
        no custom profile path is provided. Covers OS-Family, OS-Version, CPU-Tier,
        Memory-Tier, Tools-Status, Power-State, Snapshot-Risk, VM-Age, Network,
        and Datastore.
    .EXAMPLE
        $defaults = Get-DefaultTagProfile
    .NOTES
        Author: Larry Roberts
    #>

    [CmdletBinding()]
    param()

    $defaultProfilePath = Join-Path -Path $script:ProfileDir -ChildPath 'default.yml'
    if (Test-Path $defaultProfilePath) {
        return Read-TagProfile -Path $defaultProfilePath
    }

    # Hardcoded fallback if profile file is missing
    Write-Verbose "Default profile file not found, using hardcoded defaults."

    $profile = [PSCustomObject]@{
        Name        = 'default'
        Description = 'Default tag profile with standard VM categories'
        Categories  = [System.Collections.ArrayList]::new()
    }

    # OS-Family
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'OS-Family'
        Source        = 'guest_family'
        Cardinality   = 'Single'
        Description   = 'Operating system family detected from VMware Tools'
        Values        = @(
            [PSCustomObject]@{ Match = '*Windows*'; Tag = 'Windows'; Default = $false }
            [PSCustomObject]@{ Match = '*Linux*';   Tag = 'Linux';   Default = $false }
            [PSCustomObject]@{ Match = '*darwin*';  Tag = 'macOS';   Default = $false }
            [PSCustomObject]@{ Match = '*freebsd*'; Tag = 'FreeBSD'; Default = $false }
            [PSCustomObject]@{ Match = $null;       Tag = 'Other';   Default = $true }
        )
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # OS-Version
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'OS-Version'
        Source        = 'guest_fullname'
        Cardinality   = 'Single'
        Description   = 'Full OS version string from VMware Tools'
        Values        = @()
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # CPU-Tier
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'CPU-Tier'
        Source        = 'cpu_count'
        Cardinality   = 'Single'
        Description   = 'vCPU count tier for capacity planning'
        Values        = @()
        Tiers         = @(
            [PSCustomObject]@{ Max = 2;                    Min = 0; Tag = '1-2 vCPU';  Default = $false }
            [PSCustomObject]@{ Max = 8;                    Min = 0; Tag = '4-8 vCPU';  Default = $false }
            [PSCustomObject]@{ Max = 16;                   Min = 0; Tag = '9-16 vCPU'; Default = $false }
            [PSCustomObject]@{ Max = [double]::MaxValue;   Min = 0; Tag = '16+ vCPU';  Default = $true }
        )
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Memory-Tier
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Memory-Tier'
        Source        = 'memory_gb'
        Cardinality   = 'Single'
        Description   = 'Memory allocation tier for capacity planning'
        Values        = @()
        Tiers         = @(
            [PSCustomObject]@{ Max = 4;                    Min = 0; Tag = 'Small (<=4GB)';    Default = $false }
            [PSCustomObject]@{ Max = 16;                   Min = 0; Tag = 'Medium (8-16GB)';  Default = $false }
            [PSCustomObject]@{ Max = 64;                   Min = 0; Tag = 'Large (32-64GB)';  Default = $false }
            [PSCustomObject]@{ Max = [double]::MaxValue;   Min = 0; Tag = 'XLarge (64GB+)';   Default = $true }
        )
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Tools-Status
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Tools-Status'
        Source        = 'tools_status'
        Cardinality   = 'Single'
        Description   = 'VMware Tools installation and currency status'
        Values        = @(
            [PSCustomObject]@{ Match = 'toolsOk';          Tag = 'Current';       Default = $false }
            [PSCustomObject]@{ Match = 'toolsOld';         Tag = 'Outdated';      Default = $false }
            [PSCustomObject]@{ Match = 'toolsNotInstalled'; Tag = 'Not Installed'; Default = $false }
            [PSCustomObject]@{ Match = 'toolsNotRunning';  Tag = 'Not Running';   Default = $false }
            [PSCustomObject]@{ Match = $null;              Tag = 'Not Installed';  Default = $true }
        )
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Power-State
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Power-State'
        Source        = 'power_state'
        Cardinality   = 'Single'
        Description   = 'VM power state'
        Values        = @(
            [PSCustomObject]@{ Match = 'PoweredOn';  Tag = 'Running';   Default = $false }
            [PSCustomObject]@{ Match = 'PoweredOff'; Tag = 'Stopped';   Default = $false }
            [PSCustomObject]@{ Match = 'Suspended';  Tag = 'Suspended'; Default = $false }
            [PSCustomObject]@{ Match = $null;        Tag = 'Stopped';   Default = $true }
        )
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Snapshot-Risk
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Snapshot-Risk'
        Source        = 'snapshot_risk'
        Cardinality   = 'Single'
        Description   = 'Snapshot presence and age risk assessment'
        Values        = @()
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # VM-Age
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'VM-Age'
        Source        = 'vm_age'
        Cardinality   = 'Single'
        Description   = 'VM age since creation'
        Values        = @()
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Network
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Network'
        Source        = 'network'
        Cardinality   = 'Multiple'
        Description   = 'Network port group assignments'
        Values        = @()
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    # Datastore
    [void]$profile.Categories.Add([PSCustomObject]@{
        Name          = 'Datastore'
        Source        = 'datastore'
        Cardinality   = 'Multiple'
        Description   = 'Datastore locations for VM files'
        Values        = @()
        Tiers         = @()
        Pattern       = $null
        AttributeName = $null
        NamePattern   = $null
        FolderMapping = $null
    })

    return $profile
}