Public/FieldSettings.ps1

## TM Field Settings
Function Get-TMNextSharedColumn {
    <#
    .SYNOPSIS
        Gets the next available shared custom column name.
 
    .DESCRIPTION
        Inspects the provided FieldSpecs set and returns the first custom
        column name that is not already used across the supported asset domains.
 
    .PARAMETER FieldSpecs
        The FieldSpecs object to inspect.
 
    .EXAMPLE
        $fieldSpecs = Get-TMFieldSpecs
        Get-TMNextSharedColumn -FieldSpecs $fieldSpecs
 
        Returns the next unused shared custom column name.
 
    .NOTES
        This function checks the application, device, database, and storage
        domains together.
    #>

    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)][PSObject]$FieldSpecs
    )

    ## Get furthest Custom field number number in each

    for ($i = 1; $i -ne 100; $i++) {
        if (
            (-Not ($FieldSpecs.APPLICATION.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.DEVICE.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.DATABASE.fields | Where-Object { $_.field -eq 'custom' + $i })) `
                -and (-Not ($FieldSpecs.STORAGE.fields | Where-Object { $_.field -eq 'custom' + $i }))
        ) {
            return 'custom' + $i
        }
    }
}
Function Get-TMNextCustomColumn {
    <#
    .SYNOPSIS
        Gets the next available custom column name for a single class.
 
    .DESCRIPTION
        Inspects the provided class field definition and returns the first unused
        custom column name for that class.
 
    .PARAMETER ClassFields
        The class field definition object to inspect.
 
    .EXAMPLE
        $classFields = (Get-TMFieldSpecs).DEVICE
        Get-TMNextCustomColumn -ClassFields $classFields
 
        Returns the next unused custom column name for the device class.
 
    .NOTES
        Use `Get-TMNextSharedColumn` when you need a name that is free across all
        supported classes.
    #>

    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)][PSObject]$ClassFields
    )

    for ($i = 1; $i -ne 100; $i++) {
        if (-Not ($ClassFields.fields | Where-Object { $_.field -eq 'custom' + $i })) {
            return 'custom' + $i
        }
    }
}

Function Get-TMFieldSpecs {
    <#
    .SYNOPSIS
        Gets FieldSpecs from TransitionManager.
 
    .DESCRIPTION
        Retrieves TM FieldSpecs metadata and optionally resets ids,
        returns custom fields only, emits label-oriented output, or bypasses any
        cached FieldSpecs data.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER ResetIDs
        Switch indicating that identifiers on the returned FieldSpecs should
        be normalized.
 
    .PARAMETER CustomOnly
        Switch indicating that only custom field definitions should be returned.
 
    .PARAMETER Label
        Switch indicating that label-oriented output should be returned.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        Get-TMFieldSpecs -CustomOnly -SkipCache
 
        Retrieves only custom FieldSpecs and bypasses cached data.
 
    .NOTES
        This function is commonly used before field updates and field map
        generation.
    #>

    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $false)][Switch]$ResetIDs,
        [Parameter(Mandatory = $false)][Switch]$CustomOnly,
        [Parameter(Mandatory = $false)][Switch]$Label,
        [Parameter(Mandatory = $false)][Switch]$SkipCache
    )

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    if (-Not $SkipCache) {
        $FieldSpecs = $TMSession.DataCache.FieldSpecs
    }

    if (-Not ($FieldSpecs.PSObject.Properties.Name -contains 'APPLICATION') -or $SkipCache) {

        #Honor SSL Settings
        $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

        $Instance = $TMSession.TMServer.Replace('/tdstm', '')
        $instance = $instance.Replace('https://', '')
        $instance = $instance.Replace('http://', '')

        $uri = 'https://'
        $uri += $instance
        $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

        try {
            $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings
        } catch {
            return $_
        }

        if ($response.StatusCode -eq 200) {
            $FieldSpecs = ($response.Content | ConvertFrom-Json)
            if (-Not $SkipCache) {
                $TMSession.DataCache.FieldSpecs = ($response.Content | ConvertFrom-Json)
            }
        } else {
            return 'Unable to collect Field Settings.'
        }
    }

    if ($CustomOnly) {
        foreach ($AssetClass in $FieldSpecs.PSObject.Properties.Value) {
            $AssetClass.fields = $AssetClass.fields | Where-Object { $_.udf -ne '0' }
        }
    }

    if ($ResetIDs) {
        ## Read each domain field to reset the IDs
        foreach ($AssetClass in $FieldSpecs.PSObject.Properties.Value) {

            ## Sort the fields by label and collect a new ordered list of the fields
            $AssetClass.fields = $AssetClass.fields | Sort-Object -Property 'label'

            ## Iterate over all of the fields
            for ($i = 0; $i -lt $AssetClass.fields.Count; $i++) {

                ## If the Field is a custom field.
                if ($AssetClass.fields[$i].field -like 'custom*') {

                    ## Change the custom field number to be set on import
                    $AssetClass.fields[$i].field = 'customN'
                }
                ## If the Field Control is a JSON, sets the JSONFieldID to Null.
                if ($AssetClass.fields[$i].control -eq 'JSON') {

                    $AssetClass.fields[$i].jsonFieldId = $null
                }
            }
        }
    }
    return $FieldSpecs
}
Function Update-TMFieldSpecs {
    <#
    .SYNOPSIS
        Updates FieldSpecs in TransitionManager.
 
    .DESCRIPTION
        Sends an updated FieldSpecs object back to TM and optionally
        bypasses cached FieldSpecs data during the operation.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER FieldSpecs
        The FieldSpecs object containing the updates to apply.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        $fieldSpecs = Get-TMFieldSpecs -SkipCache
        $fieldSpecs.Application.fields[0].tip = ‘New Tooltip’
        Update-TMFieldSpecs -FieldSpecs $fieldSpecs -SkipCache
 
        Updates FieldSpecs using a prepared FieldSpecs object.
 
    .NOTES
        Use `Get-TMFieldSpecs` to retrieve a baseline object before modifying it.
    #>

    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$FieldSpecs,
        [Parameter(Mandatory = $false)][Switch]$SkipCache
    )


    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    ## Clear related caches that will need to be updated
    $TMSession.DataCache.FieldSpecsLabelToFieldMap = $null
    $TMSession.DataCache.FieldSpecsFieldToLabelMap = $null

    ## Define the domain classes
    $DomainClasses = @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')

    # Get the Existing FieldSpecs
    $ServerFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache

    # workaround for a bug not present since 6.4.2
    if ( $TMSession.TMVersion -lt [version] '6.4.2' ) {
        $ValuesToAdd = @('', 'Yes', 'No')
        foreach ($Domain in $DomainClasses) {
            $FieldsToUpdate = $ServerFields.$Domain.fields | Where-Object control -EQ 'YesNo'
            foreach ($field in $FieldsToUpdate) {
                try {
                    $field.constraint | Add-Member -MemberType NoteProperty -Name 'values' -Value $ValuesToAdd -ErrorAction Stop
                } catch {
                    foreach ($ValueToAdd in $ValuesToAdd) {
                        try {
                            $field.constraint.values += $ValueToAdd
                        } catch {
                            Write-Debug "Field $($field.field) already had value '$ValueToAdd'"
                        }
                    }
                }
            }
        }
    }

    # Create the Updated FieldSpec Object and clear the fields
    $UpdateFields = $ServerFields | ConvertTo-Json -Depth 100 | ConvertFrom-Json -Depth 100
    foreach ($DomainClass in $DomainClasses) {
        $UpdateFields.$DomainClass.fields = @()
    }

    # Collect the existing Asset Class Fields, updating them if they exist
    foreach ($DomainClass in $DomainClasses) {

        $ServerClassFields = $ServerFields.$DomainClass.fields
        $NewFields = $FieldSpecs.$DomainClass.fields

        foreach ($Field in $ServerClassFields) {

            ## Look for an existing field name
            $MatchingNewField = $NewFields | Where-Object { $_.label -eq $Field.label }
            if ($MatchingNewField) {

                ## Get the UPDATED Field
                $ReturnField = $MatchingNewField | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json

                if ($ReturnField.Count -gt 1) {
                    Throw "Duplicate Field Detected - $($Field.label)"
                }

                ## Update with the original Field ID to keep the existing column
                $ReturnField.field = $Field.field

                ## Set the Constraints iist
                if ($ReturnField.control -in @('List', 'YesNo')) {
                    ## Ensure that the incomming data has the right constraint(s) property
                    Add-Member -InputObject $ReturnField -NotePropertyName 'constraint' -NotePropertyValue ($ReturnField.constraint ?? $ReturnField.constraints) -Force

                    ## Check to see if it's in the list
                    ## For Each field that already exists, get the list of constraints
                    $Field.constraint.PSObject.Properties.Name | ForEach-Object {

                        ## If the ReturnField Constraints doesn't have the value, add it
                        if ($ReturnField.constraint.PSObject.Properties.Name -notcontains $_) {
                            $ReturnField.constraint.values += $_
                        }
                    }

                    ## Remove the Added field from the Incoming FieldSpecs object so it doesn't get added again
                    $NewFields.PSObject.Properties.Remove($MatchingNewField)
                }

                #### TODO: This is now no longe working against 61. This line has been moved iunto
                ## Remove the Added field from the Incoming FieldSpecs object so it doesn't get added again
                # $NewFields.PSObject.Properties.Remove($MatchingNewField)
                # $NewFields.PSObject.Properties.Remove($MatchingNewField)

            } else {

                ## There is no matching field, return the original Field
                $ReturnField = $Field
            }

            $UpdateFields.$DomainClass.fields += $ReturnField
        }
    }

    # Add Shared Fields
    ## This is only done on one asset class (0, which exists every time). Since shared fields go to all asset classes.
    $NewSharedFields = $FieldSpecs.$DomainClass[0].fields | Where-Object { $_.shared -eq 1 }
    foreach ($NewSharedField in $NewSharedFields) {

        ## Don't allow duplicates by label
        if (
            ($UpdateFields.APPLICATION.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.DEVICE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.DATABASE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
                -or ($UpdateFields.STORAGE.fields | Where-Object { $_.label -eq $NewSharedField.label }) `
        ) {

            ## Field Already Exists and has been updated
            # Write-Host "Shared Field Name:"$NewSharedField.label"is already in use."

            Continue
        }

        ## Field doesn't exist
        $ReturnField = $NewSharedField | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json
        if ($ReturnField.field -eq 'customN') {
            $ReturnField.field = Get-TMNextSharedColumn $UpdateFields
        }
        $UpdateFields.APPLICATION.fields += $ReturnField
        $UpdateFields.DEVICE.fields += $ReturnField
        $UpdateFields.DATABASE.fields += $ReturnField
        $UpdateFields.STORAGE.fields += $ReturnField
        # Write-Host 'ADDING SHARED Field Name: ' $ReturnField.label
    }

    ## Add NON Shared fields, one for each Asset Class
    foreach ($DomainClass in $DomainClasses) {
        $NewNonSharedFields = $FieldSpecs.$DomainClass.fields | Where-Object { $_.shared -eq 0 }

        foreach ($Field in $NewNonSharedFields) {

            ## Don't allow duplicates by label
            if ($UpdateFields.$DomainClass.fields | Where-Object { $_.label -eq $Field.label }) {

                ## Field Already Exists and has been updated
                # Write-Host $DomainClass 'Field Name: ' $Field.label ' is already in use.'
                Continue
            }

            ## Field doesn't exist
            $ReturnField = $Field
            if ($ReturnField.field -eq 'customN') {
                $ReturnField.field = Get-TMNextCustomColumn $UpdateFields.$DomainClass
            }
            $UpdateFields.$DomainClass.fields += $ReturnField
            # Write-Host 'ADDING '$DomainClass 'Field Name: ' $Field.label

        }
    }

    ## Validate the number of fields being created
    foreach ($DomainClass in $DomainClasses) {

        ## Count the number of Custom Fields
        $CustomFieldCount = ($UpdateFields.$DomainClass.fields | Where-Object { $_.field -like 'custom*' }).Count

        ## Disallow an upload if there are more than 96 custom fields
        if ($CustomFieldCount -gt 96) {
            Throw "Adding these fields would result in a total of $CustomFieldCount fields in the $DomainClass class."
        }

        ## Adjust every domain FieldSpecs to the correct format
        ## Ensure that the incomming data has the right constraint(s) property
        $UpdateFields.$DomainClass.fields | ForEach-Object {
            Add-Member -InputObject $_ -NotePropertyName 'constraint' -NotePropertyValue ($_.constraint ?? $_.constraints) -Force
            if ($_.PSObject.Properties.Name.Contains('constraints')) {
                $_.PSObject.Properties.Remove('constraints')
   }
            Add-Member -InputObject $_ -NotePropertyName 'defaultValue' -NotePropertyValue ($_.defaultValue ?? $_.default) -Force
            if ($_.PSObject.Properties.Name.Contains('default')) {
                $_.PSObject.Properties.Remove('default')
   }
        }
    }

    ## Build the URL to post back to
    $uri = 'https://'
    $uri += $TMSession.TMServer
    $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

    # Capitalize the domain
    $DomainClasses | ForEach-Object {
        $UpdateFields.$_.domain = $_
    }

    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $UpdateFields
        projectId     = $TMSession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    $UpdateFieldsWebRequest = @{
        Uri                  = $uri
        WebSession           = $TMSession.TMWebSession
        Body                 = $PostBody
        SkipCertificateCheck = $TMSession.AllowInsecureSSL
        Method               = 'Post'
    }

    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    try {
        $response = Invoke-WebRequest @UpdateFieldsWebRequest
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {

                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $UpdateFields
                }

                return
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            throw $_
  }
    } catch {
        return $_
    }

}

Function Set-TMFieldSpecs {
    <#
    .SYNOPSIS
        Sets FieldSpecs in TransitionManager.
 
    .DESCRIPTION
        Applies the supplied FieldSpecs object to TM. Optional switches
        can force the update and bypass cached FieldSpecs data.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER FieldSpecs
        The FieldSpecs object to apply.
 
    .PARAMETER Force
        Switch indicating that the FieldSpecs update should be forced.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        $fieldSpecs = Get-TMFieldSpecs
        Set-TMFieldSpecs -FieldSpecs $fieldSpecs -Force
 
        Applies a prepared FieldSpecs object and forces the update.
 
    .NOTES
        This command is intended for FieldSpecs writes rather than reads.
    #>

    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$FieldSpecs,
        [Switch]$Force,
        [Switch]$SkipCache

    )

    ## This command is potentially destructive
    if (-Not $Force) {
        Write-Host 'This command is used to overwrite an existing FieldSpecs object and should be used with care.'
        Write-Host 'The counterpart command ' -NoNewline
        Write-Host 'Update-TMFieldSpecs ' -NoNewline -ForegroundColor Yellow
        Write-Host 'might be more useful as it merges changes into an existing FieldSpec.'
        Write-Host 'To use this command to overwrite a FieldSpec, use of the -Force switch is required'
        throw 'You must use the force switch with this command'
    }

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    ## Clear related caches that will need to be updated
    $TMSession.DataCache.FieldSpecsLabelToFieldMap = $null
    $TMSession.DataCache.FieldSpecsFieldToLabelMap = $null

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    ## Build the URL to post back to
    $uri = 'https://'
    $uri += $TMSession.TMServer
    $uri += '/tdstm/ws/customDomain/fieldSpec/ASSETS'

    ## Create the Field Settings body
    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $FieldSpecs
        projectId     = $TMsession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    try {
        $response = Invoke-WebRequest -Method Post -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBody @TMCertSettings
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {
                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $FieldSpecs
                }
                return
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            throw $_
  }
    } catch {
        return $_
    }

}

Function Add-TMFieldListValues {
    <#
    .SYNOPSIS
        Adds values to a field list in TransitionManager.
 
    .DESCRIPTION
        Adds one or more list values to the specified field label within the chosen
        asset domain. Optional switches can control list sorting behavior and cache
        usage.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER Domain
        The TM asset domain that owns the field.
 
    .PARAMETER FieldLabel
        The label of the field whose list values should be updated.
 
    .PARAMETER Values
        The list values to add to the field.
 
    .PARAMETER SortConstraintList
        Controls how the field list should be sorted when TM supports that option.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        Add-TMFieldListValues -Domain Device -FieldLabel 'Migration Method' -Values 'Retire in place', 'HCX Bulk'
 
        Adds the specified values to the `Environment` field list for devices.
 
    .NOTES
        Use the field label as it appears in TM.
    #>

    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][ValidateSet('Application', 'Database', 'Device', 'Storage')][String]$Domain,
        [Parameter(Mandatory = $true)][String]$FieldLabel,
        [Parameter(Mandatory = $true)][Array]$Values,
        [Parameter(Mandatory = $false)][ValidateSet('Asc', 'Desc')]
        [String]$SortConstraintList,
        [Switch]$SkipCache
    )

    if ($Values.Count -eq 0) {
        Write-Verbose 'There were no updates to make to the field'
        return
    }

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    ## Define the domain classes
    $DomainClasses = @{
        Application = 'APPLICATION'
        Device      = 'DEVICE'
        Database    = 'DATABASE'
        Storage     = 'STORAGE'
    }

    # Get the Existing FieldSpecs
    $FieldSpecs = Get-TMFieldSpecs -TMSession $TMSession -SkipCache

    ## Create the Updated FieldSpec Object and clear the fields
    $Field = $FieldSpecs.($DomainClasses[$Domain]).fields | Where-Object { $_.label -eq $FieldLabel }
    if (-Not $Field) {
        Write-Host "Field: $($FieldLabel) not found!" -ForegroundColor Red
        return
    }

    ## Only Add values to a List control
    if ($Field -and $Field.control -ne 'List') {
        Write-Verbose "Skipping update on $($Field.label) as it is a $($Field.control) type."
        return
    }

    foreach ($NewItem in $Values) {

        ## Version 6.0.2.1+ (and TODO version 6.1?, 5x?)
        ## Requires the class names to be lower case
        $CONSTRAINTS_PROPERTY_NAME = 'constraint'

        ## Convert each Asset Class name to lowercase
        ## Check to see if it's in the list
        if (-Not $Field.$CONSTRAINTS_PROPERTY_NAME.values.Contains($NewItem)) {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values += $NewItem
        }

        ## Sort the list if required
        if ($SortConstraintList -eq 'Asc') {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values = $Field.$CONSTRAINTS_PROPERTY_NAME.values | Sort-Object
        }
        if ($SortConstraintList -eq 'Desc') {
            $Field.$CONSTRAINTS_PROPERTY_NAME.values = $Field.$CONSTRAINTS_PROPERTY_NAME.values | Sort-Object -Descending
        }
    }

    ## Put the data back
    $FieldSpecs.($DomainClasses[$Domain]).fields | Where-Object { $_.label -eq $FieldLabel } | ForEach-Object {
        $_ = $Field
    }

    ## Construct an update call
    $uri = "https://$($TMSession.TMServer)/tdstm/ws/customDomain/fieldSpec/ASSETS"
    Set-TMHeaderContentType -ContentType 'JSON' -TMSession $TMSession

    ## Create the Field Settings body
    ## Build an API post body with the Project ID
    $PostBody = [PSCustomObject]@{
        fieldSettings = $FieldSpecs
        projectId     = $TMsession.UserContext.project.id
    } | ConvertTo-Json -Depth 100 -Compress

    try {
        $response = Invoke-WebRequest -Method Post -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBody @TMCertSettings
        if ($response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'success') {
                if (-Not $SkipCache) {
                    $TMSession.DataCache.FieldSpecs = $FieldSpecs
                }
            } else {
                throw $ResponseJson.errors
            }
        } elseif ($response.StatusCode -eq 204) {
            return
        } else {
            ThrowError 'Unable to save Field Settings'
  }


    } catch {
        throw $_
    }
}
function Get-TMFieldToLabelMap {
    <#
    .SYNOPSIS
        Gets a field-to-label map from TransitionManager FieldSpecs.
 
    .DESCRIPTION
        Builds a lookup that maps internal field names to display labels, either
        for a single asset domain or for all supported domains.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER Domain
        The asset domain whose field-to-label map should be returned.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        Get-TMFieldToLabelMap -Domain DEVICE
 
        Returns a map of internal device field names to their display labels.
 
    .NOTES
        This function depends on the FieldSpecs metadata returned by
        `Get-TMFieldSpecs`.
    #>

    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [ValidateSet('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')]
        [String]$Domain,
        [Switch]$SkipCache
    )

    $TMSession = Get-TMSession $TMSession
    if (-Not $SkipCache) {
        $FieldLabelMap = $TMSession.DataCache.FieldSpecsFieldToLabelMap
    }

    if (-Not $FieldLabelMap.Values -or $SkipCache) {

        $AllFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache:$SkipCache
        $FieldLabelMap = @{
            TemplateKeys = @{}
        }

        foreach ($DomainName in @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')) {
            $FieldLabelMap.Add($DomainName, @{})
            foreach ($Spec in $AllFields.$DomainName.fields) {
                # Add-Member -InputObject $FieldLabelMap.$DomainName -NotePropertyName $Spec.field -NotePropertyValue $Spec.label -Force
                Write-Verbose "Domain: $($DomainName), Field: $($Spec.field), Label: $($Spec.label)"
                $FieldLabelMap.$DomainName.Add($Spec.field, $Spec.label)

                $TemplateKey = "customN|$($DomainName)|$($Spec.label)|"
                Write-Verbose "Adding TemplateKey $TemplateKey with value $($Spec.field)"
                $FieldLabelMap.TemplateKeys.Add($TemplateKey, $Spec.field)
            }
        }
    }

    if (-Not $SkipCache) {
        $TMSession.DataCache.FieldSpecsFieldToLabelMap = $FieldLabelMap
    }

    if ($Domain) {
        $FieldLabelMap.$Domain
    } else {
        $FieldLabelMap
    }
}


function Get-TMLabelToFieldMap {
    <#
    .SYNOPSIS
        Gets a label-to-field map from TransitionManager FieldSpecs.
 
    .DESCRIPTION
        Builds a lookup that maps display labels to internal field names, either
        for a single asset domain or for all supported domains.
 
    .PARAMETER TMSession
        A TMSession object or session name to use for the request. Defaults to
        `'Default'`.
 
    .PARAMETER Domain
        The asset domain whose label-to-field map should be returned.
 
    .PARAMETER SkipCache
        Switch indicating that cached FieldSpecs should be ignored.
 
    .EXAMPLE
        Get-TMLabelToFieldMap -Domain APPLICATION
 
        Returns a map of application field labels to their internal field names.
 
    .NOTES
        This function depends on the FieldSpecs metadata returned by
        `Get-TMFieldSpecs`.
    #>

    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [ValidateSet('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')]
        [String]$Domain,
        [Switch]$SkipCache
    )
    $TMSession = Get-TMSession $TMSession
    if (-Not $SkipCache) {
        $LabelFieldMap = $TMSession.DataCache.FieldSpecsLabelToFieldMap
    }
    if (-Not $LabelFieldMap.Values -or $SkipCache) {
        $AllFields = Get-TMFieldSpecs -TMSession $TMSession -SkipCache:$SkipCache
        $LabelFieldMap = @{}
        foreach ($DomainName in @('APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE')) {
            $LabelFieldMap.Add($DomainName, @{})
            foreach ($Spec in $AllFields.$DomainName.fields) {
                $LabelFieldMap.$DomainName.Add($Spec.label, $Spec.field)
            }
        }
    }
    if (-Not $SkipCache) {
        $TMSession.DataCache.FieldSpecsLabelToFieldMap = $LabelFieldMap
    }
    if ($Domain) {
        $LabelFieldMap.$Domain
    } else {
        $LabelFieldMap
    }
}